#coding=utf-8 from functools import partial import types import tkinter as tk from tkinter import ttk import tkMessageBox from ..variables import BoolVar from ..variables import NotifyStringVar, NotifyNumericVar, NumericVar def setPanelState(frame, state): for child in frame.winfo_children(): if isinstance(child, tk.Frame): setPanelState(child, state) else: child.configure(state=state) class WidgetRepeater(tk.Frame): def __init__(self, master, widgetClass, headers=None, classParams=None, onAdd=None, reorderable=False, *args, **kwargs): widgetList = widgetClass if not isinstance(widgetClass, list): widgetList = [widgetClass] for widget in widgetList: if not issubclass(widget, RepeatableFrame): raise AttributeError( 'WidgetRepeater widget must be a RepeatableFrame') tk.Frame.__init__(self, master, **kwargs) self.widgetClass = widgetClass self.widgetClassParams = classParams self.widgets = [] self.headers = headers self.headerFrame = None self.addButton = ttk.Button( self, text='[+]', width=5, command=self._addWidget) self.onAdd = onAdd self.reorderable = reorderable self.renderContent() def _findWidget(self, row, column): for children in self.children.values(): info = children.grid_info() if info['row'] == str(row) and info['column'] == str(column): return children return None def _createWidget(self, widgetClass, widgetClassParams=None): headeridx = int(self.headerFrame is not None) buttonFrame = tk.Frame(self) buttonFrame.grid( row=len(self.widgets)+headeridx, column=0, sticky=tk.N) removeButton = ttk.Button( buttonFrame, text='[-]', width=6, command=lambda i=len(self.widgets): self._removeWidget(i)) removeButton.grid( row=0, column=0, columnspan=2, sticky=tk.N+tk.E+tk.W) if self.reorderable: moveUpButton = ttk.Button( buttonFrame, text='[↑]', width=3, command=lambda i=len(self.widgets): self._moveWidgetUp(i)) moveUpButton.grid(row=1, column=0, sticky=tk.N+tk.S+tk.E+tk.W) moveDownButton = ttk.Button( buttonFrame, text='[↓]', width=3, command=lambda i=len(self.widgets): self._moveWidgetDown(i)) moveDownButton.grid(row=1, column=1, sticky=tk.N+tk.S+tk.E+tk.W) widget = widgetClass(self) if widgetClassParams is not None: widget.configureContent(**widgetClassParams) self.widgets.append(widget) self._updateGrid() if self.onAdd is not None: self.onAdd(widget) def _moveWidgetUp(self, idx): self._swapWidgets(idx, idx-1) def _moveWidgetDown(self, idx): self._swapWidgets(idx, idx+1) def _swapWidgets(self, idx1, idx2): if (idx1 >= 0) and (idx2 >= 0) and \ (idx1 < len(self.widgets)) and (idx2 < len(self.widgets)): temp = self.widgets[idx1] self.widgets[idx1] = self.widgets[idx2] self.widgets[idx2] = temp self._updateGrid() def _handleWidgetSelection(self, selected): if selected < len(self.widgetClass): params = None if isinstance(self.widgetClassParams, list) and \ selected < len(self.widgetClassParams): params = self.widgetClassParams[selected] self._createWidget(self.widgetClass[selected], params) def _widgetSelectionDialog(self): dialog = tk.Toplevel(self) dialog.title('Wybór elementu do dodania') dialog.geometry('%dx%d' % (300, len(self.widgetClass) * 20 + 30)) dialog.grab_set() dialog.focus_force() frame = WidgetSelectionFrame( dialog, vertical=True, widgets=self.widgetClass, callback=self._handleWidgetSelection) frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) def _addWidget(self): if isinstance(self.widgetClass, list): self._widgetSelectionDialog() else: self._createWidget(self.widgetClass, self.widgetClassParams) def _removeWidget(self, idx): self.widgets.pop(idx).destroy() self._findWidget(row=len(self.widgets), column=0).destroy() self._updateGrid() def _updateGrid(self): headeridx = int(self.headerFrame is not None) for idx, widget in enumerate(self.widgets): widget.grid( row=idx+headeridx, column=1, sticky=tk.W+tk.E+tk.N+tk.S) if self.headerFrame is not None: self.headerFrame.grid(row=0, column=1, sticky=tk.W+tk.E+tk.N+tk.S) self.addButton.grid( row=len(self.widgets)+headeridx, column=0, columnspan=1, sticky=tk.W+tk.N) self.event_generate('<>', when='tail') def _renderHeader(self): if self.headers: self.headerFrame = tk.Frame(self) for idx, header in enumerate(self.headers): self.headerFrame.columnconfigure(idx, weight=1) widget = header[0](self.headerFrame, **header[1]) widget.grid(row=0, column=idx, sticky=tk.W+tk.E+tk.N) (tk.Label(self, text=' ')).grid( row=0, column=0, sticky=tk.W+tk.E+tk.N) def renderContent(self): self.columnconfigure(1, weight=1) self._renderHeader() self._updateGrid() def getValue(self): return [widget.getValue() for widget in self.widgets] def _getParamsForWidgetClass(self, widgetClass): if not isinstance(self.widgetClass, list): return self.widgetClassParams if not isinstance(self.widgetClassParams, list): return self.widgetClassParams for idx, widget in enumerate(self.widgetClass): if widget == widgetClass: return self.widgetClassParams[idx] return None def setValue(self, values): for i, value in enumerate(values): typedWidget = isinstance(value, tuple) \ and isinstance(value[0], (types.TypeType, types.ClassType)) if i >= len(self.widgets): if typedWidget: self._createWidget( value[0], self._getParamsForWidgetClass(value[0])) else: self._addWidget() self.widgets[i].setValue(value[1] if typedWidget else value) for idx in range(len(values), len(self.widgets)): self._removeWidget(len(self.widgets)-1) class GuiFrame(tk.Frame): def __init__(self, *args, **kwargs): tk.Frame.__init__(self, *args, **kwargs) self.renderContent() def renderContent(self): pass def getSpecificParent(self, parentType): obj = self while not isinstance(obj, parentType): obj = obj.master if obj is None: break return obj class RepeatableFrame(GuiFrame): def configureContent(self, **kwargs): pass def getValue(self): pass def setValue(self, value): pass @classmethod def info(cls): return cls.__name__ class RepeatableEntry(RepeatableFrame): def renderContent(self): self.value = NotifyStringVar() self.field = ttk.Entry(self, textvariable=self.value) self.field.pack(expand=True, fill=tk.BOTH) def configureContent(self, **kwargs): for param, value in kwargs.iteritems(): self.field[param] = value def getValue(self): return self.value.get() def setValue(self, value): return self.value.set(value) class ScrollableFrame(tk.Frame): def __init__(self, *args, **kwargs): vertical = False if 'vertical' in kwargs: vertical = kwargs['vertical'] del kwargs['vertical'] horizontal = False if 'horizontal' in kwargs: horizontal = kwargs['horizontal'] del kwargs['horizontal'] tk.Frame.__init__(self, *args, **kwargs) self.canvas = tk.Canvas(self, borderwidth=0, highlightthickness=0) if horizontal: hscroll = tk.Scrollbar( self, orient=tk.HORIZONTAL, command=self.canvas.xview) hscroll.pack(side=tk.BOTTOM, fill=tk.X) self.canvas.configure(xscrollcommand=hscroll.set) if vertical: vscroll = tk.Scrollbar( self, orient=tk.VERTICAL, command=self.canvas.yview) vscroll.pack(side=tk.RIGHT, fill=tk.Y) self.canvas.configure(yscrollcommand=vscroll.set) frame = tk.Frame(self.canvas, borderwidth=0, highlightthickness=0) self.canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True) self.canvasFrame = self.canvas.create_window( (0,0), window=frame, anchor=tk.N+tk.W) frame.bind('', self._onFrameConfigure) self.canvas.bind('', self._onCanvasConfigure) self.bind('', partial(self._setScroll, value=True)) self.bind('', partial(self._setScroll, value=False)) self.renderContent(frame) def _setScroll(self, event, value): if value: self.bind_all('', self._onVscroll) self.bind_all('', self._onHscroll) else: self.unbind_all('') self.unbind_all('') def _onHscroll(self, event): self._onScroll(tk.X, -1 if event.delta > 0 else 1) def _onVscroll(self, event): self._onScroll(tk.Y, -1 if event.delta > 0 else 1) def _onScroll(self, direction, delta): getattr( self.canvas, '%sview' % (direction))(tk.SCROLL, delta, tk.UNITS) def _onFrameConfigure(self, event): self.canvas.configure(scrollregion=self.canvas.bbox('all')) def _onCanvasConfigure(self, event): self.canvas.itemconfig(self.canvasFrame, width=event.width) def renderContent(self, container): pass class WidgetSelectionFrame(ScrollableFrame): def __init__(self, *args, **kwargs): self.widgets = [] self.callback = None for var in ['widgets', 'callback']: if var in kwargs: setattr(self, var, kwargs[var]) del kwargs[var] ScrollableFrame.__init__(self, *args, **kwargs) addBtn = ttk.Button( self.master, text='Dodaj', command=self._onConfirm) addBtn.pack(side=tk.BOTTOM) def renderContent(self, container): self.value = tk.IntVar() for idx, widget in enumerate(self.widgets): (ttk.Radiobutton( container, variable=self.value, value=idx, text=widget.info())).pack(side=tk.TOP, fill=tk.X, expand=True) def _onConfirm(self): if self.callback is not None: self.callback(self.value.get()) self.winfo_toplevel().destroy() class SelectionButton(ttk.Button): @property def defaultPrompt(self): pass @property def title(self): pass @property def errorMessage(self): pass def getOptions(self): pass def __init__(self, *args, **kwargs): for arg in ['callback', 'prompt', 'dialogclass']: setattr(self, arg, kwargs[arg] if arg in kwargs else None) if arg in kwargs: del kwargs[arg] kwargs['command'] = self._choosePositions if self.prompt is None: self.prompt = self.defaultPrompt self._trackChanges = False ttk.Button.__init__(self, *args, **kwargs) self.setPositions([]) def setPositions(self, values): self.selected = values self.configure( text='[wybrano: %d]' % (len(values))) if self.callback is not None: self.callback(values) if self._trackChanges: self.event_generate('<>', when='tail') def _choosePositions(self): options = self.getOptions() if not len(options): tkMessageBox.showerror( self.title, self.errorMessage) self.setPositions([]) else: dialog = tk.Toplevel(self) dialog.title(self.title) dialog.grab_set() dialog.focus_force() self._trackChanges = True selectionFrame = self.dialogclass( dialog, title=self.prompt, options=options, selected=self.selected, callback=self.setPositions, vertical=True) selectionFrame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) class SelectionFrame(ScrollableFrame): def __init__(self, master, title='', options=[], selected=None, callback=None, *args, **kwargs): self.values = {} self.title = title self.options = options self.selected = selected self.callback = callback ScrollableFrame.__init__(self, master=master, *args, **kwargs) (ttk.Button(master, text='Zapisz', command=self._save)).pack( side=tk.BOTTOM, fill=tk.Y) def renderOption(self, container, option, idx): pass def _mapValue(self, idx, value): return idx + 1 def _save(self): if self.callback: self.callback( [idx for idx, value in self.values.iteritems() if value.get()]) self.master.destroy() def renderHeader(self, container): container.columnconfigure(1, weight=1) (ttk.Label(container, text=self.title)).grid( row=0, column=0, columnspan=2) def renderContent(self, container): self.renderHeader(container) for idx, option in enumerate(self.options): key = self._mapValue(idx, option) self.values[key] = BoolVar() self.renderOption(container, option, idx) if self.selected and key in self.selected: self.values[key].set(True) class RefreshableOptionMenu(ttk.OptionMenu): def __init__(self, master, variable, *args, **kwargs): self._valueVariable = variable self._valueVariable.trace('w', self._valueSet) self._lastValue = variable.get() newVar = NotifyStringVar() ttk.OptionMenu.__init__(self, master, newVar, *args, **kwargs) self._valueLock = False self.refreshOptions() class _setit(tk._setit): def __init__(self, var, valVar, label, value, callback=None): tk._setit.__init__(self, var, label, callback) self._valueVariable = valVar self._properValue = value def __call__(self, *args): self.__var.set(self.__value) self._valueVariable.set(self._properValue) if self.__callback: self.__callback(self._valueVariable, *args) def refreshOptions(self, *args): try: options = self.getOptions() self['menu'].delete(0, tk.END) for label, option in options: self['menu'].add_command( label=label, command=self._setit( self._variable, self._valueVariable, label, option, self._callback)) self._valueLock = True self._valueVariable.set( self._lastValue if self._lastValue in [option[1] for option in options] else '') self._valueLock = False except tk.TclError: # we're probably being destroyed, ignore pass def getOptions(self): return [ (self.getLabel(value), self.getVarValue(value)) for value in self.getValues()] def getLabel(self, value): pass def getValues(self): pass def getVarValue(self, value): return self.getLabel(value) def _valueSet(self, *args): if not self._valueLock: self._lastValue = self._valueVariable.get() options = self.getOptions() value = self._valueVariable.get() for label, val in options: if unicode(value) == unicode(val): tk._setit(self._variable, label)() return tk._setit(self._variable, '')() class TraceableText(tk.Text): def __init__(self, *args, **kwargs): self._variable = None self._variableLock = False if 'variable' in kwargs: self._variable = kwargs['variable'] del kwargs['variable'] tk.Text.__init__(self, *args, **kwargs) if self._variable is not None: self._orig = self._w + '_orig' self.tk.call('rename', self._w, self._orig) self.tk.createcommand(self._w, self._proxy) self._variable.trace('w', self._fromVariable) def _fromVariable(self, *args): if not self._variableLock: self._variableLock = True self.delete('1.0', tk.END) self.insert(tk.END, self._variable.get()) self._variableLock = False def _proxy(self, command, *args): cmd = (self._orig, command) + args # https://stackoverflow.com/a/53418346 <- it's his fault. try: result = self.tk.call(cmd) except: return None if command in ('insert', 'delete', 'replace') and \ not self._variableLock: text = self.get('1.0', tk.END).strip() self._variableLock = True self._variable.set(text) self._variableLock = False return result class NumericSpinbox(tk.Spinbox): def __init__(self, *args, **kwargs): kwargs['justify'] = tk.RIGHT self._variable = None if 'textvariable' in kwargs: self._variable = kwargs['textvariable'] self._default = kwargs['from_'] if 'from_' in kwargs else 0 tk.Spinbox.__init__(self, *args, **kwargs) if self._variable is not None: if not isinstance(self._variable, NumericVar): raise AttributeError( 'NumericSpinbox variable must be NumericVar') self._variable.trace('w', self._onChange) def _onChange(self, *args): val = self._variable.get() if val is None: self._variable.set(self._default) class LabelButton(ttk.Button): def __init__(self, *args, **kwargs): self.label = None self.tooltip = None self._prevTooltip = None for param in ['label', 'tooltip']: if param in kwargs: setattr(self, param, kwargs[param]) del kwargs[param] ttk.Button.__init__(self, *args, **kwargs) if self.label and self.tooltip: self.bind('', self._onEnter) self.bind('', self._onLeave) def _onEnter(self, *args): self._prevTooltip = self.label.cget('text') self.label.configure(text=self.tooltip) def _onLeave(self, *args): self.label.configure(text=self._prevTooltip)