#coding=utf-8 from functools import partial import types import tkinter as tk from tkinter import ttk import tkMessageBox def getIntVal(widget, default=0): try: return int(widget.get().strip()) except ValueError: return default 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, *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.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): removeButton = ttk.Button( self, text='[-]', width=5, command=lambda i=len(self.widgets): self._removeWidget(i)) removeButton.grid(row=len(self.widgets), column=0, sticky=tk.N) widget = widgetClass(self) if widgetClassParams is not None: widget.configureContent(**widgetClassParams) self.widgets.append(widget) 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), column=0, columnspan=1, sticky=tk.W+tk.N) 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 class RepeatableFrame(tk.Frame): def __init__(self, *args, **kwargs): tk.Frame.__init__(self, *args, **kwargs) self.renderContent() def renderContent(self): pass 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 = tk.StringVar() 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) canvas = tk.Canvas(self, borderwidth=0, highlightthickness=0) if horizontal: hscroll = tk.Scrollbar( self, orient=tk.HORIZONTAL, command=canvas.xview) hscroll.pack(side=tk.BOTTOM, fill=tk.X) canvas.configure(xscrollcommand=hscroll.set) if vertical: vscroll = tk.Scrollbar( self, orient=tk.VERTICAL, command=canvas.yview) vscroll.pack(side=tk.RIGHT, fill=tk.Y) canvas.configure(yscrollcommand=vscroll.set) frame = tk.Frame(canvas, borderwidth=0, highlightthickness=0) canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True) canvas.create_window((0,0), window=frame, anchor=tk.N+tk.W) frame.bind( '', lambda ev: canvas.configure(scrollregion=canvas.bbox('all'))) self.renderContent(frame) 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 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) 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() 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 renderOption(self, container, option, idx): pass 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 _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] = tk.IntVar() 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, *args, **kwargs): ttk.OptionMenu.__init__(self, *args, **kwargs) self._oldValue = self._variable.get() self._variable.trace('w', self._valueSet) self._valueLock = False self.refreshOptions() def refreshOptions(self, *args): options = self.getOptions() self['menu'].delete(0, tk.END) for option in options: self['menu'].add_command( label=option, command=tk._setit(self._variable, option)) self._valueLock = True self._variable.set(self._oldValue if self._oldValue in options else '') self._valueLock = False def getOptions(self): return [self.getLabel(value) for value in self.getValues()] def getLabel(self, value): pass def getValues(self): pass def cmpValue(self, value): return self._variable.get() == self.getLabel(value) def _valueSet(self, *args): if not self._valueLock: self._valueLock = True self._oldValue = self._variable.get() for value in self.getValues(): if self.cmpValue(value): self._variable.set(self.getLabel(value)) self._valueLock = False return self._variable.set('') self._valueLock = False 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._varaibleLock = False def _proxy(self, command, *args): cmd = (self._orig, command) + args result = self.tk.call(cmd) 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 # TODO: NumericSpinBox instead of getIntVal