diff options
Diffstat (limited to 'jfr_playoff/gui')
20 files changed, 3476 insertions, 0 deletions
diff --git a/jfr_playoff/gui/__init__.py b/jfr_playoff/gui/__init__.py new file mode 100644 index 0000000..37d6cef --- /dev/null +++ b/jfr_playoff/gui/__init__.py @@ -0,0 +1,304 @@ +#coding=utf-8 + +import codecs, copy, json, os, sys, tempfile, threading, traceback, webbrowser +from collections import OrderedDict +import logging as log + +import tkinter as tk +from tkinter import ttk +import tkFileDialog as tkfd +import tkMessageBox as tkmb + +from jfr_playoff.filemanager import PlayoffFileManager +from jfr_playoff.generator import PlayoffGenerator +from jfr_playoff.settings import PlayoffSettings + +from .tabs import * +from .icons import GuiImage +from .frames import LabelButton, NumericSpinbox +from .variables import NumericVar +from .logframe import LogWindow + +class PlayoffGUI(tk.Tk): + def __init__(self): + tk.Tk.__init__(self) + ttk.Style().configure('TLabelframe.Label', foreground='black') + ttk.Style().configure('TLabelframe', padding=5) + self.geometry('920x640') + self.iconbitmap(GuiImage.get_path('icons', 'playoff', 'ico')) + self.tabs = {} + self.logWindow = LogWindow(self) + self.logWindow.title('Dziennik komunikatów') + self.logWindow.iconbitmap(GuiImage.get_path('icons', 'playoff', 'ico')) + self._buildMenu() + self.newFileIndex = 0 + self._title = tk.StringVar() + self._title.trace('w', self._setTitle) + self._dirty = tk.BooleanVar() + self._dirty.trace('w', self._setTitle) + self._dirty.trace('w', self._setMenuButtons) + self._runTimer = None + self._runtimeError = None + self._filepath = None + self.protocol('WM_DELETE_WINDOW', self.onClose) + + def run(self): + self.notebook = ttk.Notebook(self) + self.notebook.pack(fill=tk.BOTH, expand=True) + for tab in tabs.__all__: + self.tabs[tab] = globals()[tab](self.notebook) + self.notebook.add(self.tabs[tab], text=self.tabs[tab].title) + if len(sys.argv) > 1: + self.openFile(sys.argv[1]) + else: + self.newFile() + self.bind('<<ValueChanged>>', self._onFileChange, add='+') + self.bind('<<BracketGenerated>>', self._onBracketGenerated, add='+') + self.bind('<<BracketError>>', self._onBracketError, add='+') + self.mainloop() + + def _onFileChange(self, *args): + self._dirty.set(True) + + def _checkSave(self): + if self._dirty.get(): + if tkmb.askyesno( + 'Zapisz zmiany', + 'Czy chcesz zapisać zmiany w bieżącej drabince?'): + self.onSave() + + def _setTitle(self, *args): + self.title('%s - %s%s' % ( + 'TeamyPlayOff', + self._title.get(), + ' *' if self._dirty.get() else '' + )) + + def _setMenuButtons(self, *args): + self.menuButtons['save'].configure( + state=tk.NORMAL if self._dirty.get() else tk.DISABLED) + + def _setValues(self, config): + for tab in self.tabs.values(): + tab.setValues(config) + + def _resetValues(self): + self._setValues({}) + + def _buildMenu(self): + menu = tk.Frame(self) + menu.pack(side=tk.TOP, fill=tk.X) + statusBar = ttk.Label(menu) + statusBar.pack(side=tk.RIGHT) + self.menuButtons = {} + for icon, command, tooltip in [ + ('new', self.onNewFile, 'Nowa drabinka...'), + ('open', self.onFileOpen, 'Otwórz drabinkę...'), + ('save', self.onSave, 'Zapisz'), + ('saveas', self.onSaveAs, 'Zapisz jako...')]: + self.menuButtons[icon] = LabelButton( + menu, image=GuiImage.get_icon(icon), command=command, + tooltip=tooltip, label=statusBar) + self.menuButtons[icon].pack(side=tk.LEFT) + (ttk.Separator(menu, orient=tk.VERTICAL)).pack( + side=tk.LEFT, fill=tk.Y, padx=3, pady=1) + for icon, command, tooltip in [ + ('run-once', self.onRunOnce, 'Wygeneruj')]: + self.menuButtons[icon] = LabelButton( + menu, image=GuiImage.get_icon(icon), command=command, + tooltip=tooltip, label=statusBar) + self.menuButtons[icon].pack(side=tk.LEFT) + self.runningLabel = ttk.Label(menu, width=10, text='') + self.runningLabel.pack(side=tk.LEFT) + (ttk.Separator(menu, orient=tk.VERTICAL)).pack( + side=tk.LEFT, fill=tk.Y, padx=3, pady=1) + for icon, command, tooltip in [ + ('run-timed', self.onRunTimed, 'Generuj co X sekund')]: + self.menuButtons[icon] = LabelButton( + menu, image=GuiImage.get_icon(icon), command=command, + tooltip=tooltip, label=statusBar) + self.menuButtons[icon].pack(side=tk.LEFT) + self.interval = NumericVar() + self.intervalField = NumericSpinbox( + menu, width=5, + textvariable=self.interval, from_=30, to=3600) + self.intervalField.pack(side=tk.LEFT) + self.intervalLabel = ttk.Label(menu, text='sekund') + self.intervalLabel.pack(side=tk.LEFT) + (ttk.Separator(menu, orient=tk.VERTICAL)).pack( + side=tk.LEFT, fill=tk.Y, padx=3, pady=1) + for icon, command, tooltip in [ + ('log', self.onLogWindowOpen, 'Dziennik komunikatów')]: + self.menuButtons[icon] = LabelButton( + menu, image=GuiImage.get_icon(icon), command=command, + tooltip=tooltip, label=statusBar) + self.menuButtons[icon].pack(side=tk.LEFT) + + def onNewFile(self): + self._checkSave() + self.newFile() + + def onFileOpen(self): + self._checkSave() + filename = tkfd.askopenfilename( + title='Wybierz plik drabniki', + filetypes=(('JFR Teamy Play-Off files', '*.jtpo'), + ('JSON files', '*.json'),)) + if filename: + self.openFile(filename) + + def onSave(self): + if self._filepath is not None: + self.saveFile(self._filepath) + else: + self.onSaveAs() + + def onSaveAs(self): + filename = tkfd.asksaveasfilename( + title='Wybierz plik drabniki', + filetypes=(('JFR Teamy Play-Off files', '*.jtpo'), + ('JSON files', '*.json'),)) + if filename: + if not filename.lower().endswith('.jtpo'): + filename = filename + '.jtpo' + self.saveFile(filename) + + def onClose(self, *args): + self._checkSave() + self.destroy() + + def _run(self, config, interactive=True): + self._interactive = interactive + try: + tempPath = None + if not len(config.get('output', '')): + tempDir = tempfile.mkdtemp(prefix='jfrplayoff-') + tempPath = os.path.join( + tempDir, next(tempfile._get_candidate_names())) + config['output'] = tempPath + '.html' + self._outputPath = config['output'] + settings = PlayoffSettings(config_obj=config) + generator = PlayoffGenerator(settings) + content = generator.generate_content() + file_manager = PlayoffFileManager(settings) + file_manager.write_content(content) + file_manager.copy_scripts() + file_manager.copy_styles() + file_manager.send_files() + self.event_generate('<<BracketGenerated>>', when='tail') + if tempPath is not None: + os.remove(config['output']) + except Exception as e: + log.getLogger().error(str(e)) + traceback.print_exc() + if interactive: + self._runtimeError = e + self.event_generate('<<BracketError>>', when='tail') + + def _onBracketGenerated(self, *args): + self._setRunWidgetState(tk.NORMAL) + if self._interactive: + if tkmb.askyesno( + 'Otwórz drabinkę', + 'Otworzyć drabinkę w domyślnej przeglądarce?'): + webbrowser.open(self._outputPath) + + def _onBracketError(self, *args): + tkmb.showerror('Błąd generowania drabinki', str(self._runtimeError)) + self._setRunWidgetState(tk.NORMAL) + self._runtimeError = None + + def _setRunWidgetState(self, state): + self.menuButtons['run-once'].configure(state=state) + self.runningLabel.configure( + text='' if state == tk.NORMAL else 'pracuję...') + + def _setTimerWidgetState(self, state): + for widget in [self.intervalField, self.intervalLabel]: + widget.configure(state=state) + self.menuButtons['run-timed'].configure( + image=GuiImage.get_icon('run-timed') + if state == tk.NORMAL else GuiImage.get_icon('stop-timed')) + + def onRunOnce(self, interactive=True): + self._setRunWidgetState(tk.DISABLED) + if not interactive: + self._runTimer = self.after( + 1000 * self.interval.get(default=30), self.onRunOnce, False) + config = self.getConfig() + thread = threading.Thread( + target=self._run, args=(config, interactive,)) + thread.start() + + def onRunTimed(self): + if self._runTimer is None: + self.after(100, self.onRunOnce, False) + self._setTimerWidgetState(tk.DISABLED) + else: + self.after_cancel(self._runTimer) + self._runTimer = None + self._setTimerWidgetState(tk.NORMAL) + + def onLogWindowOpen(self): + self.logWindow.update() + self.logWindow.deiconify() + + def newFile(self): + self._filepath = None + self.newFileIndex += 1 + self._title.set('Nowa drabinka %d' % (self.newFileIndex)) + self._resetValues() + self.after(0, self._dirty.set, False) + + def openFile(self, filepath): + self._filepath = filepath + self._title.set(os.path.basename(filepath)) + self._setValues(json.load(open(filepath))) + self.after(0, self._dirty.set, False) + + def saveFile(self, filepath): + json.dump( + self.getConfig(), codecs.open(filepath, 'w', encoding='utf8'), + indent=4, ensure_ascii=False) + self._filepath = filepath + self._title.set(os.path.basename(filepath)) + self.after(0, self._dirty.set, False) + + def getConfig(self): + config = OrderedDict() + for tab in self.tabs.values(): + tabConfig = tab.getConfig() + if tabConfig is not None: + config = self._mergeConfig(config, tab.getConfig()) + return config + + def _mergeConfig(self, base, update): + result = copy.copy(base) + for key, value in update.iteritems(): + if key in result: + if isinstance(result[key], dict): + result[key] = self._mergeConfig( + result[key], update[key]) + else: + result[key] = update[key] + else: + result[key] = value + return result + + def getDbConfig(self): + return self.tabs['NetworkTab'].getDB() + + def getTeams(self): + return self.tabs['TeamsTab'].getTeams() + + def getDBs(self): + return self.tabs['NetworkTab'].getDBList() + + def getMatches(self): + return self.tabs['MatchesTab'].getMatches() + + def getNewMatchID(self, match): + matches = self.tabs['MatchesTab'].getMatches() + if len(matches) > 0: + return max([m.getMatchID() for m in matches]) + 1 + return 1 diff --git a/jfr_playoff/gui/frames/__init__.py b/jfr_playoff/gui/frames/__init__.py new file mode 100644 index 0000000..a02791d --- /dev/null +++ b/jfr_playoff/gui/frames/__init__.py @@ -0,0 +1,503 @@ +#coding=utf-8 + +from functools import partial +import types + +import tkinter as tk +from tkinter import ttk +import tkMessageBox + +from ..variables import NotifyStringVar, NotifyIntVar, 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, + *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): + headeridx = int(self.headerFrame is not None) + removeButton = ttk.Button( + self, text='[-]', width=5, + command=lambda i=len(self.widgets): self._removeWidget(i)) + removeButton.grid( + row=len(self.widgets)+headeridx, 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)+headeridx, 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 = 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('<Configure>', self._onFrameConfigure) + self.canvas.bind('<Configure>', self._onCanvasConfigure) + self.bind('<Enter>', partial(self._setScroll, value=True)) + self.bind('<Leave>', partial(self._setScroll, value=False)) + self.renderContent(frame) + + def _setScroll(self, event, value): + if value: + self.bind_all('<MouseWheel>', self._onVscroll) + self.bind_all('<Shift-MouseWheel>', self._onHscroll) + else: + self.unbind_all('<MouseWheel>') + self.unbind_all('<Shift-MouseWheel>') + + 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 = NotifyIntVar() + 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 __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] = NotifyIntVar() + 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('<Enter>', self._onEnter) + self.bind('<Leave>', 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) diff --git a/jfr_playoff/gui/frames/match.py b/jfr_playoff/gui/frames/match.py new file mode 100644 index 0000000..12d02a5 --- /dev/null +++ b/jfr_playoff/gui/frames/match.py @@ -0,0 +1,829 @@ +#coding=utf-8 + +from collections import OrderedDict + +import tkinter as tk +from tkinter.font import Font +from tkinter import ttk + +from ..frames import GuiFrame, RepeatableFrame, ScrollableFrame +from ..frames import WidgetRepeater, RepeatableEntry, NumericSpinbox +from ..frames import SelectionFrame, SelectionButton, RefreshableOptionMenu +from ..frames.team import DBSelectionField, TeamSelectionFrame +from ..frames.team import TeamSelectionButton +from ..frames.visual import PositionsSelectionFrame +from ..variables import NotifyStringVar, NotifyIntVar, NotifyNumericVar + +class SwissSettingsFrame(RepeatableFrame): + SOURCE_LINK = 0 + SOURCE_DB = 1 + + def _setPositionInfo(self, *args): + tournamentFrom = self.setFrom.get(default=1) + tournamentTo = min( + self.setTo.get(default=1) \ + if self.setToEnabled.get() else 9999, + len(self.winfo_toplevel().getTeams())) + swissFrom = self.fetchFrom.get(default=1) + swissTo = swissFrom + tournamentTo - tournamentFrom + if tournamentTo < tournamentFrom: + self.positionsInfo.configure(text='brak miejsc do ustawienia') + else: + self.positionsInfo.configure(text='%d-%d -> %d-%d' % ( + swissFrom, swissTo, tournamentFrom, tournamentTo)) + + def _setFields(self, *args): + checkFields = [self.setToEnabled, self.fetchFromEnabled] + for child in self.winfo_children(): + info = child.grid_info() + row = int(info['row']) + if row in [1, 2] and not isinstance(child, ttk.Radiobutton): + child.configure( + state=tk.NORMAL if self.source.get() == 2 - row \ + else tk.DISABLED) + elif row in [5, 6] and isinstance(child, tk.Spinbox): + child.configure( + state=tk.NORMAL if checkFields[row-5].get() \ + else tk.DISABLED) + + def renderContent(self): + self.source = NotifyIntVar() + self.fetchDB = NotifyStringVar() + self.fetchLink = NotifyStringVar() + self.setFrom = NotifyNumericVar() + self.setToEnabled = NotifyIntVar() + self.setTo = NotifyNumericVar() + self.fetchFromEnabled = NotifyIntVar() + self.fetchFrom = NotifyNumericVar() + self.linkLabel = NotifyStringVar() + self.linkRelPath = NotifyStringVar() + + self.columnconfigure(1, weight=1) + self.columnconfigure(2, weight=1) + self.columnconfigure(3, weight=1) + + (ttk.Label(self, text='Źródło danych:')).grid( + row=0, column=0, sticky=tk.W) + (ttk.Radiobutton( + self, text='Baza danych', + variable=self.source, value=self.SOURCE_DB)).grid( + row=1, column=0, sticky=tk.W) + self.fetchDBField = DBSelectionField( + self, self.fetchDB, self.fetchDB.get()) + self.fetchDBField.grid(row=1, column=1, sticky=tk.W+tk.E) + (ttk.Radiobutton( + self, text='Strona turnieju', + variable=self.source, value=self.SOURCE_LINK)).grid( + row=2, column=0, sticky=tk.W) + (ttk.Entry(self, textvariable=self.fetchLink, width=20)).grid( + row=2, column=1, sticky=tk.W+tk.E) + + (ttk.Separator(self, orient=tk.HORIZONTAL)).grid( + row=3, column=0, columnspan=6, sticky=tk.E+tk.W) + + (ttk.Label( + self, text='Ustaw od miejsca: ')).grid( + row=4, column=0, sticky=tk.W, padx=18) + (NumericSpinbox( + self, textvariable=self.setFrom, + from_=1, to=999, width=5)).grid( + row=4, column=1, sticky=tk.W) + (ttk.Checkbutton( + self, variable=self.setToEnabled, + text='Ustaw do miejsca: ')).grid( + row=5, column=0, sticky=tk.W) + (NumericSpinbox( + self, textvariable=self.setTo, + from_=1, to=999, width=5)).grid( + row=5, column=1, sticky=tk.W) + (ttk.Checkbutton( + self, variable=self.fetchFromEnabled, + text='Pobierz od miejsca: ')).grid( + row=6, column=0, sticky=tk.W) + (NumericSpinbox( + self, textvariable=self.fetchFrom, + from_=1, to=999, width=5)).grid( + row=6, column=1, sticky=tk.W) + + (ttk.Label(self, text='Miejsca w swissie')).grid( + row=4, column=2) + (ttk.Label(self, text='Miejsca w klasyfikacji')).grid( + row=4, column=3) + self.positionsInfo = ttk.Label(self, text=' -> ', font=Font(size=16)) + self.positionsInfo.grid(row=5, column=2, columnspan=2, rowspan=2) + + (ttk.Label(self, text='Etykieta linku:')).grid( + row=8, column=0, sticky=tk.E) + (ttk.Entry(self, textvariable=self.linkLabel, width=20)).grid( + row=8, column=1, sticky=tk.W+tk.E) + (ttk.Label(self, text='(domyślnie: "Turniej o #. miejsce")')).grid( + row=8, column=2, sticky=tk.W) + + (ttk.Label(self, text='Względna ścieżka linku do swissa:')).grid( + row=1, column=2, sticky=tk.E) + (ttk.Entry(self, textvariable=self.linkRelPath, width=20)).grid( + row=1, column=3, sticky=tk.W+tk.E) + + (ttk.Separator(self, orient=tk.HORIZONTAL)).grid( + row=7, column=0, columnspan=6, sticky=tk.E+tk.W) + (ttk.Separator(self, orient=tk.HORIZONTAL)).grid( + row=9, column=0, columnspan=6, sticky=tk.E+tk.W) + + self._setFields() + self._setPositionInfo() + + self.fetchFromEnabled.trace('w', self._setFields) + self.setToEnabled.trace('w', self._setFields) + self.source.trace('w', self._setFields) + self.setFrom.trace('w', self._setPositionInfo) + self.setTo.trace('w', self._setPositionInfo) + self.fetchFrom.trace('w', self._setPositionInfo) + self.fetchFromEnabled.trace('w', self._setPositionInfo) + self.setToEnabled.trace('w', self._setPositionInfo) + self.winfo_toplevel().bind( + '<<TeamListChanged>>', self._setPositionInfo, add='+') + + def setValue(self, value): + if 'database' in value: + self.source.set(self.SOURCE_DB) + self.fetchDB.set(value['database']) + self.fetchLink.set('') + else: + self.source.set(self.SOURCE_LINK) + self.fetchDB.set('') + if 'link' in value: + self.fetchLink.set(value['link']) + else: + self.fetchLink.set('') + self.setFrom.set(value['position'] if 'position' in value else 1) + if 'position_to' in value: + self.setToEnabled.set(1) + self.setTo.set(value['position_to']) + else: + self.setToEnabled.set(0) + self.setTo.set(1) + if 'swiss_position' in value: + self.fetchFromEnabled.set(1) + self.fetchFrom.set(value['swiss_position']) + else: + self.fetchFromEnabled.set(0) + self.fetchFrom.set(1) + self.linkLabel.set(value['label'] if 'label' in value else '') + self.linkRelPath.set( + value['relative_path'] if 'relative_path' in value else '') + + def getValue(self): + config = OrderedDict() + if self.source.get() == self.SOURCE_DB: + config['database'] = self.fetchDB.get() + if self.linkRelPath.get(): + config['relative_path'] = self.linkRelPath.get() + if self.source.get() == self.SOURCE_LINK: + config['link'] = self.fetchLink.get() + if self.linkLabel.get(): + config['label'] = self.linkLabel.get() + config['position'] = self.setFrom.get() + if self.setToEnabled.get(): + config['position_to'] = self.setTo.get() + if self.fetchFromEnabled.get(): + config['swiss_position'] = self.fetchFrom.get() + return config + +class SwissesFrame(ScrollableFrame): + def renderContent(self, container): + self.swisses = WidgetRepeater(container, SwissSettingsFrame) + self.swisses.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + def setValues(self, values): + self.swisses.setValue(values) + + def getValues(self): + return self.swisses.getValue() + +class MatchSelectionButton(SelectionButton): + @property + def defaultPrompt(self): + return 'Wybierz mecze:' + + @property + def title(self): + return 'Wybór meczów' + + @property + def errorMessage(self): + return 'W turnieju nie zdefiniowano żadnych meczów' + + def getOptions(self): + return self.winfo_toplevel().getMatches() + + +class MatchSelectionFrame(SelectionFrame): + def renderOption(self, container, option, idx): + (ttk.Label( + container, text='[%d]' % (self._mapValue(idx, option)))).grid( + row=idx+1, column=0) + (ttk.Checkbutton( + container, text=option.label, + variable=self.values[self._mapValue(idx, option)] + )).grid(row=idx+1, column=1, sticky=tk.W) + + def _mapValue(self, idx, value): + return self.options[idx].getMatchID() + + +class SelectedTeamList(RefreshableOptionMenu): + VALUE_LABELS = { + 'none': u'%s', + 'winner': u'Zwycięzca meczu %d', + 'loser': u'Przegrany meczu %d', + 'place': u'Drużyna z miejsca %d' + } + + def __init__(self, *args, **kwargs): + RefreshableOptionMenu.__init__(self, *args, **kwargs) + self.master.bind( + '<<BracketConfigChanged>>', self.refreshOptions, add='+') + + def getValues(self): + config = self.master.getConfig() + values = [('none', '')] + if isinstance(config, dict): + for key in ['winner', 'loser', 'place']: + if key in config: + for value in config[key]: + values.append((key, value)) + return values + + def getLabel(self, value): + return self.VALUE_LABELS[value[0]] % (value[1]) + + def getVarValue(self, value): + return unicode(value) + + +class BracketMatchSettingsFrame(GuiFrame): + SOURCE_TEAM=0 + SOURCE_BRACKET=1 + LIST_WIDGETS = {'place': 5, 'winner': 1, 'loser': 3} + + def _enablePanels(self, *args): + for widget in self.teamWidgets: + widget.configure( + state=tk.NORMAL if self.source.get() == self.SOURCE_TEAM + else tk.DISABLED) + for widget in self.bracketWidgets: + widget.configure( + state=tk.NORMAL if self.source.get() == self.SOURCE_BRACKET + else tk.DISABLED) + if self.source.get() == self.SOURCE_BRACKET \ + and isinstance(widget, SelectedTeamList) \ + and not self.selected.get(): + widget.configure(state=tk.DISABLED) + + def _configChangeNotify(self, *args): + self.event_generate('<<BracketConfigChanged>>', when='tail') + + def _setPositions(self, positions): + self.positions = positions + self._configChangeNotify() + + def _setLosers(self, matches): + self.losers = matches + self._configChangeNotify() + + def _setWinners(self, matches): + self.winners = matches + self._configChangeNotify() + + def _setTeams(self, teams): + if not self._lockTeams: + allTeams = [team[0] for team in self.teamWidgets[0].getOptions()] + self.teams = [allTeams[idx-1] for idx in teams] + + def renderContent(self): + self.source = NotifyIntVar() + self.source.trace('w', self._enablePanels) + self.source.trace('w', self._configChangeNotify) + self.selected = NotifyIntVar() + self.selected.trace('w', self._enablePanels) + self.selectedIndex = NotifyStringVar() + self.positions = [] + self.winners = [] + self.losers = [] + self.teams = [] + self._lockTeams = True + self.winfo_toplevel().bind( + '<<TeamListChanged>>', self._onTeamListChange, add='+') + self.winfo_toplevel().bind( + '<<MatchListChanged>>', self._onMatchListChange, add='+') + + buttons = [ + ttk.Radiobutton( + self, variable=self.source, value=self.SOURCE_TEAM, + text='Konkretne teamy'), + ttk.Radiobutton( + self, variable=self.source, value=self.SOURCE_BRACKET, + text='Z drabinki')] + self.teamWidgets = [ + TeamSelectionButton( + self, prompt='Wybierz drużyny:', + dialogclass=TeamSelectionFrame, + callback=self._setTeams)] + self.bracketWidgets = [ + ttk.Label(self, text='Zwycięzcy meczów:'), + MatchSelectionButton( + self, prompt='Wybierz mecze:', + dialogclass=MatchSelectionFrame, + callback=self._setWinners), + ttk.Label(self, text='Przegrani meczów:'), + MatchSelectionButton( + self, prompt='Wybierz mecze:', + dialogclass=MatchSelectionFrame, + callback=self._setLosers), + ttk.Label(self, text='Pozycje początkowe:'), + TeamSelectionButton( + self, prompt='Wybierz pozycje początkowe:', + dialogclass=PositionsSelectionFrame, + callback=self._setPositions), + ttk.Checkbutton( + self, text='Uczestnik został wybrany:', + variable=self.selected), + SelectedTeamList(self, self.selectedIndex) + ] + + for idx, button in enumerate(buttons): + button.grid(row=idx, column=0, sticky=tk.W) + self.teamWidgets[0].grid(row=0, column=1, sticky=tk.W) + for idx, widget in enumerate(self.bracketWidgets): + widget.grid(row=1+idx/2, column=1+idx%2, sticky=tk.W) + + self._lockTeams = False + + def _onTeamListChange(self, *args): + teamsToSet = [] + teams = [team[0] for team in self.teamWidgets[0].getOptions()] + for team in self.teams: + try: + teamsToSet.append(teams.index(team)+1) + except ValueError: + pass + self._lockTeams = True + self.teamWidgets[0].setPositions(teamsToSet) + self._lockTeams = False + + def _onMatchListChange(self, *args): + try: + matches = [ + match.getMatchID() for match + in self.bracketWidgets[ + self.LIST_WIDGETS['winner']].getOptions()] + self.bracketWidgets[self.LIST_WIDGETS['winner']].setPositions([ + winner for winner in self.winners if winner in matches]) + self.bracketWidgets[self.LIST_WIDGETS['loser']].setPositions([ + loser for loser in self.losers if loser in matches]) + except tk.TclError as e: + # we're probably trying to update our widget when + # WE'RE the match that's being destroyed + pass + + def setValue(self, value): + if isinstance(value, (str, unicode)): + value = [value] + if isinstance(value, list): + self.source.set(self.SOURCE_TEAM) + self.teams = list(set(value)) + for idx in self.LIST_WIDGETS.values(): + self.bracketWidgets[idx].setPositions([]) + else: + self.source.set(self.SOURCE_BRACKET) + self.teams = [] + for key, idx in self.LIST_WIDGETS.iteritems(): + self.bracketWidgets[idx].setPositions( + value[key] + if key in value and isinstance(value[key], list) + else []) + + def setSelectedTeam(self, team): + if team > -1: + self.selectedIndex.set(self.bracketWidgets[7].getValues()[team+1]) + self.selected.set(1) + else: + self.selectedIndex.set(('none', '')) + self.selected.set(0) + + def getSelectedTeam(self): + if self.selected.get(): + try: + return self.bracketWidgets[7].getValues().index( + self.selectedIndex.get()) + except ValueError: + return -1 + else: + return -1 + + def getConfig(self): + if self.source.get() == self.SOURCE_TEAM: + return self.teams + else: + config = OrderedDict() + lists = { + 5: self.positions, + 1: self.winners, + 3: self.losers + } + for key, idx in self.LIST_WIDGETS.iteritems(): + values = lists[idx] + if len(values) > 0: + config[key] = values + return config + + def getValue(self): + return self.getConfig() + +class MatchSettingsFrame(RepeatableFrame): + SCORE_SOURCE_DB = 0 + SCORE_SOURCE_LINK = 1 + SCORE_SOURCE_CUSTOM = 2 + + def destroy(self, *args, **kwargs): + self.winfo_toplevel().event_generate( + '<<MatchListChanged>>', when='tail') + RepeatableFrame.destroy(self, *args, **kwargs) + + def _enablePanels(self, *args): + for val, fields in self.scoreWidgets.iteritems(): + for field in fields: + field.configure( + state=tk.NORMAL if self.source.get() == val + else tk.DISABLED) + if not self.scoreNotFinished.get(): + self.scoreWidgets[self.SCORE_SOURCE_CUSTOM][-2].configure( + state=tk.DISABLED) + + def _updateName(self, *args): + self.nameLabel.configure(text=self.label) + + def _setWinnerPositions(self, values): + self.winnerPositions = values + + def _setLoserPositions(self, values): + self.loserPositions = values + + def renderContent(self): + self.nameLabel = ttk.Label(self) + self.matchID = NotifyIntVar() + self.matchID.trace('w', self._updateName) + self.matchID.set(self.winfo_toplevel().getNewMatchID(self)) + self.winfo_toplevel().bind( + '<<PhaseRenamed>>', self._updateName, add='+') + self.link = NotifyStringVar() + + self.source = NotifyIntVar() + self.source.trace('w', self._enablePanels) + + self.scoreDB = NotifyStringVar() + self.scoreRound = NotifyNumericVar() + self.scoreTable = NotifyNumericVar() + self.scoreCustom = [NotifyStringVar(), NotifyStringVar()] + self.scoreNotFinished = NotifyIntVar() + self.scoreNotFinished.trace('w', self._enablePanels) + self.scoreBoards = NotifyNumericVar() + + self.winnerPositions = [] + self.loserPositions = [] + + self.columnconfigure(1, weight=1) + self.columnconfigure(2, weight=1) + + self.nameLabel.grid(row=0, column=0, sticky=tk.W) + (ttk.Label(self, text='Link:')).grid(row=0, column=1, sticky=tk.E) + (ttk.Entry(self, textvariable=self.link)).grid( + row=0, column=2, sticky=tk.W+tk.E) + + bracketGroup = ttk.LabelFrame(self, text='Dane drabinki') + bracketGroup.grid(row=1, column=0, columnspan=3, sticky=tk.W+tk.E) + + bracketGroup.columnconfigure(0, weight=1) + bracketGroup.columnconfigure(1, weight=1) + + homeTeam = ttk.LabelFrame(bracketGroup, text='Team gospodarzy') + homeTeam.grid(row=0, column=0, sticky=tk.W+tk.E) + awayTeam = ttk.LabelFrame(bracketGroup, text='Team gości') + awayTeam.grid(row=0, column=1, sticky=tk.W+tk.E) + + teamFrames = [homeTeam, awayTeam] + self.bracketSettings = [] + for frame in teamFrames: + bracket = BracketMatchSettingsFrame(frame) + bracket.grid(row=0, column=0, sticky=tk.N+tk.S+tk.W+tk.E) + self.bracketSettings.append(bracket) + + scoreGroup = ttk.LabelFrame(self, text='Dane wyniku meczu') + scoreGroup.grid(row=4, column=0, columnspan=3, sticky=tk.W+tk.E) + + scoreGroup.columnconfigure(1, weight=1) + scoreGroup.columnconfigure(3, weight=1) + + self.scoreWidgets = { + self.SCORE_SOURCE_DB: [ + DBSelectionField(scoreGroup, self.scoreDB, self.scoreDB.get()), + ttk.Label(scoreGroup, text='Runda:'), + NumericSpinbox( + scoreGroup, width=3, + textvariable=self.scoreRound, from_=1, to=999), + ttk.Label(scoreGroup, text='Stół:'), + NumericSpinbox( + scoreGroup, width=3, + textvariable=self.scoreTable, from_=1, to=999) + ], + self.SCORE_SOURCE_LINK: [ + ttk.Entry(scoreGroup, textvariable=self.link), + # TODO: TC support (Round/Session) + #ttk.Label(scoreGroup, text='Sesja:'), + #NumericSpinbox( + # scoreGroup, + #textvariable=self.scoreSession, from_=1, to=999), + #ttk.Label(scoreGroup, text='Runda:'), + #NumericSpinbox( + # scoreGroup, + #textvariable=self.scoreRound, from_=1, to=999), + ttk.Label(scoreGroup, text='Stół:'), + NumericSpinbox( + scoreGroup, width=3, + textvariable=self.scoreTable, from_=1, to=999) + ], + self.SCORE_SOURCE_CUSTOM: [ + ttk.Entry( + scoreGroup, textvariable=self.scoreCustom[0], + width=10, justify=tk.RIGHT), + ttk.Label(scoreGroup, text=':'), + ttk.Entry( + scoreGroup, textvariable=self.scoreCustom[1], + width=10, justify=tk.RIGHT), + ttk.Checkbutton( + scoreGroup, variable=self.scoreNotFinished, + text='mecz nie został zakończony, rozegrano:'), + NumericSpinbox( + scoreGroup, width=3, + textvariable=self.scoreBoards, from_=0, to=999), + ttk.Label(scoreGroup, text='rozdań') + ] + } + + (ttk.Radiobutton( + scoreGroup, variable=self.source, value=self.SCORE_SOURCE_DB, + text='Baza danych')).grid(row=0, column=0, sticky=tk.W) + self.scoreWidgets[self.SCORE_SOURCE_DB][0].grid( + row=0, column=1, columnspan=3, sticky=tk.W+tk.E) + for idx in range(1, 5): + self.scoreWidgets[self.SCORE_SOURCE_DB][idx].grid( + row=0, column=idx+3) + (ttk.Radiobutton( + scoreGroup, variable=self.source, value=self.SCORE_SOURCE_LINK, + text='Strona z wynikami')).grid(row=1, column=0, sticky=tk.W) + self.scoreWidgets[self.SCORE_SOURCE_LINK][0].grid( + row=1, column=1, columnspan=3, sticky=tk.W+tk.E) + self.scoreWidgets[self.SCORE_SOURCE_LINK][1].grid( + row=1, column=4) + self.scoreWidgets[self.SCORE_SOURCE_LINK][2].grid( + row=1, column=5) + (ttk.Radiobutton( + scoreGroup, variable=self.source, value=self.SCORE_SOURCE_CUSTOM, + text='Ustaw ręcznie')).grid(row=2, column=0, sticky=tk.W) + for idx in range(0, 3): + self.scoreWidgets[self.SCORE_SOURCE_CUSTOM][idx].grid( + row=2, column=idx+1, sticky=tk.E if idx == 0 else tk.W) + self.scoreWidgets[self.SCORE_SOURCE_CUSTOM][3].grid( + row=2, column=4, columnspan=4) + for idx in range(4, 6): + self.scoreWidgets[self.SCORE_SOURCE_CUSTOM][idx].grid( + row=2, column=idx+5) + + (ttk.Label(bracketGroup, text='Zwycięzca zajmie miejsca:')).grid( + row=1, column=0, sticky=tk.E) + self.winnerPositionsBtn = TeamSelectionButton( + bracketGroup, prompt='Wybierz pozycje końcowe:', + dialogclass=PositionsSelectionFrame, + callback=self._setWinnerPositions) + self.winnerPositionsBtn.grid(row=1, column=1, sticky=tk.W) + (ttk.Label(bracketGroup, text='Przegrany zajmie miejsca:')).grid( + row=2, column=0, sticky=tk.E) + self.loserPositionsBtn = TeamSelectionButton( + bracketGroup, prompt='Wybierz pozycje końcowe:', + dialogclass=PositionsSelectionFrame, + callback=self._setLoserPositions) + self.loserPositionsBtn.grid(row=2, column=1, sticky=tk.W) + + self.winfo_toplevel().event_generate( + '<<MatchListChanged>>', when='tail') + + @classmethod + def info(cls): + return 'Nowy mecz' + + def getMatchID(self): + return self.matchID.get() + + def _getPhase(self): + obj = self + while not isinstance(obj, MatchPhaseFrame): + obj = obj.master + if obj is None: + break + return obj + + @property + def label(self): + try: + phase = self._getPhase() + return 'Mecz #%d (%s)' % ( + self.getMatchID(), + phase.master.tab(phase)['text'] if phase is not None else '') + except tk.TclError: + # we're probably just being created, ignore + return '' + + def setValue(self, value): + self.matchID.set(value['id'] if 'id' in value else 0) + self.link.set(value['link'] if 'link' in value else '') + + self.scoreDB.set(value['database'] if 'database' in value else '') + self.scoreRound.set(value['round'] if 'round' in value else 1) + self.scoreTable.set(value['table'] if 'table' in value else 1) + + if 'score' in value: + for idx in range(0, 2): + self.scoreCustom[idx].set( + value['score'][idx] + if isinstance(value['score'], list) + and len(value['score']) > 1 + else 0) + self.scoreNotFinished.set( + 'running' in value and value['running'] >= 0) + self.scoreBoards.set( + value['running'] if 'running' in value + and value['running'] >= 0 else 0) + else: + self.scoreNotFinished.set(0) + self.scoreBoards.set(0) + + self.source.set( + self.SCORE_SOURCE_DB if 'database' in value else ( + self.SCORE_SOURCE_CUSTOM if 'table' not in value + else self.SCORE_SOURCE_LINK + )) + + if 'teams' in value and isinstance(value['teams'], list): + for idx, val in enumerate(value['teams']): + if idx < 2: + self.bracketSettings[idx].setValue(val) + else: + for idx in range(0, 2): + self.bracketSettings[idx].setValue({}) + + self.winnerPositionsBtn.setPositions( + value['winner'] + if 'winner' in value and isinstance(value['winner'], list) + else []) + self.loserPositionsBtn.setPositions( + value['loser'] + if 'loser' in value and isinstance(value['loser'], list) + else []) + + if 'selected_teams' in value \ + and isinstance(value['selected_teams'], list): + for idx, val in enumerate(value['selected_teams']): + if idx < 2: + self.bracketSettings[idx].setSelectedTeam(val) + else: + for idx in range(0, 2): + self.bracketSettings[idx].setSelectedTeam(-1) + + def getValue(self): + config = OrderedDict() + config['id'] = self.matchID.get() + if self.link.get(): + config['link'] = self.link.get() + + config['teams'] = [bracket.getValue() + for bracket in self.bracketSettings] + + if len(self.winnerPositions): + config['winner'] = self.winnerPositions + if len(self.loserPositions): + config['loser'] = self.loserPositions + + selected = [bracket.getSelectedTeam() + for bracket in self.bracketSettings] + if len([s for s in selected if s > -1]): + config['selected_teams'] = selected + + if self.source.get() == self.SCORE_SOURCE_DB: + config['database'] = self.scoreDB.get() + config['round'] = self.scoreRound.get() + if self.source.get() != self.SCORE_SOURCE_CUSTOM: + config['table'] = self.scoreTable.get() + + if self.source.get() == self.SCORE_SOURCE_CUSTOM: + config['score'] = [] + for score in self.scoreCustom: + try: + config['score'].append(float(score.get())) + except ValueError: + config['score'].append(0.0) + if self.scoreNotFinished.get(): + config['running'] = self.scoreBoards.get() + + return config + + if 'selected_teams' in value \ + and isinstance(value['selected_teams'], list): + for idx, val in enumerate(value['selected_teams']): + if idx < 2: + self.bracketSettings[idx].setSelectedTeam(val) + else: + for idx in range(0, 2): + self.bracketSettings[idx].setSelectedTeam(-1) + + + +class MatchSeparator(RepeatableFrame): + def renderContent(self): + (ttk.Separator(self, orient=tk.HORIZONTAL)).pack( + side=tk.TOP, fill=tk.X, expand=True) + + @classmethod + def info(cls): + return 'Odstęp między meczami' + + +class MatchPhaseFrame(ScrollableFrame): + def _updateLinks(self, *args): + for match in self.matches.widgets: + if isinstance(match, MatchSettingsFrame): + match_link = match.link.get() + if not len(match_link) or match_link == self.previousLink: + match.link.set(self.link.get()) + self.previousLink = self.link.get() + + def _signalPhaseRename(self, *args): + self.winfo_toplevel().event_generate('<<PhaseRenamed>>', when='tail') + + def renderContent(self, container): + self.name = NotifyStringVar() + self.link = NotifyStringVar() + self.previousLink = '' + + headerFrame = tk.Frame(container) + headerFrame.pack(side=tk.TOP, fill=tk.X, expand=True) + (ttk.Label(headerFrame, text='Nazwa:')).pack(side=tk.LEFT) + (ttk.Entry(headerFrame, textvariable=self.name)).pack( + side=tk.LEFT, fill=tk.X, expand=True) + (ttk.Label(headerFrame, text='Link:')).pack(side=tk.LEFT) + (ttk.Entry(headerFrame, textvariable=self.link)).pack( + side=tk.LEFT, fill=tk.X, expand=True) + + self.matches = WidgetRepeater( + container, [MatchSettingsFrame, MatchSeparator]) + self.matches.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + self.link.trace('w', self._updateLinks) + self.name.trace('w', self._signalPhaseRename) + + def setValues(self, values): + matches = values['matches'] if 'matches' in values else [] + dummies = values['dummies'] if 'dummies' in values else [] + objects = [(MatchSeparator, None)] * (len(matches) + len(dummies)) + idx = 0 + for match in matches: + while idx in dummies: + idx += 1 + objects[idx] = (MatchSettingsFrame, match) + idx += 1 + self.matches.setValue(objects) + self.link.set(values['link'] if 'link' in values else '') + self.name.set(values['title'] if 'title' in values else '') + self.winfo_toplevel().event_generate( + '<<MatchListChanged>>', when='tail') + + def getConfig(self): + config = OrderedDict() + if self.name.get(): + config['title'] = self.name.get() + if self.link.get(): + config['link'] = self.link.get() + values = self.matches.getValue() + dummies = [] + matches = [] + for idx, value in enumerate(values): + if value is None: + dummies.append(idx) + else: + matches.append(value) + if len(dummies): + config['dummies'] = dummies + config['matches'] = matches + return config + + +__all__ = ['SwissesFrame', 'MatchPhaseFrame', 'MatchSettingsFrame'] diff --git a/jfr_playoff/gui/frames/network.py b/jfr_playoff/gui/frames/network.py new file mode 100644 index 0000000..90c21d1 --- /dev/null +++ b/jfr_playoff/gui/frames/network.py @@ -0,0 +1,194 @@ +#coding=utf-8 + +import socket +from collections import OrderedDict + +import tkinter as tk +from tkinter import ttk +import tkMessageBox as tkmb + +from ...db import PlayoffDB +from ..frames import RepeatableEntry, WidgetRepeater, NumericSpinbox +from ..frames import GuiFrame, ScrollableFrame +from ..variables import NotifyStringVar, NotifyIntVar, NotifyNumericVar + +def network_test(connFunction, testLabel): + try: + connFunction() + testLabel.configure(text='✓') + testLabel.configure(foreground='green') + return None + except Exception as e: + testLabel.configure(text='✗') + testLabel.configure(foreground='red') + return unicode(str(e).decode('utf-8', errors='replace')) + +class MySQLConfigurationFrame(GuiFrame): + DEFAULT_PORT = 3306 + + def getConfig(self): + if len(self.host.get().strip()): + return OrderedDict({ + 'host': self.host.get().strip(), + 'port': self.port.get(default=3306), + 'user': self.user.get().strip(), + 'pass': self.pass_.get().strip() + }) + return None + + def _testDB(self): + def test(): + dbConfig = self.getConfig() + if dbConfig is None: + raise AttributeError('Database not configured') + db = PlayoffDB(dbConfig) + self.dbError = network_test(test, self.dbTestLabel) + + def _dbError(self, event): + if self.dbError is not None: + tkmb.showerror('Błąd połączenia z bazą danych', self.dbError) + + def _changeNotify(self, *args): + self.winfo_toplevel().event_generate( + '<<DBSettingsChanged>>', when='tail') + + def renderContent(self): + self.host = NotifyStringVar() + self.host.trace('w', self._changeNotify) + self.port = NotifyNumericVar() + self.port.trace('w', self._changeNotify) + self.user = NotifyStringVar() + self.user.trace('w', self._changeNotify) + self.pass_ = NotifyStringVar() + self.pass_.trace('w', self._changeNotify) + + self.columnconfigure(0, weight=1) + + frame = ttk.LabelFrame(self, text='Ustawienia MySQL') + frame.grid(row=0, column=0, columnspan=4, sticky=tk.E+tk.W+tk.N+tk.S) + + (ttk.Label(frame, text='Host:')).grid( + row=0, column=0, sticky=tk.E) + (ttk.Entry(frame, textvariable=self.host)).grid( + row=0, column=1, sticky=tk.E+tk.W) + + (ttk.Label(frame, text='Port:')).grid( + row=0, column=2, sticky=tk.E) + (NumericSpinbox( + frame, textvariable=self.port, width=5, + from_=0, to=65535)).grid(row=0, column=3, sticky=tk.W) + + (ttk.Label(frame, text='Użytkownik:')).grid( + row=1, column=0, sticky=tk.E) + (ttk.Entry(frame, textvariable=self.user)).grid( + row=1, column=1, sticky=tk.E+tk.W) + + (ttk.Button( + frame, text='Testuj ustawienia', command=self._testDB)).grid( + row=1, column=3) + self.dbError = None + self.dbTestLabel = ttk.Label(frame) + self.dbTestLabel.grid(row=1, column=4) + self.dbTestLabel.bind('<Button-1>', self._dbError) + + (ttk.Label(frame, text='Hasło:')).grid( + row=2, column=0, sticky=tk.E) + (ttk.Entry(frame, textvariable=self.pass_, show='*')).grid( + row=2, column=1, sticky=tk.E+tk.W) + + self.setValues({}) + + def setValues(self, values): + self.host.set(values['host'] if 'host' in values else '') + self.port.set( + values['port'] if 'port' in values else self.DEFAULT_PORT) + self.user.set(values['user'] if 'user' in values else '') + self.pass_.set(values['pass'] if 'pass' in values else '') + +class GoniecConfigurationFrame(GuiFrame): + DEFAULT_HOST = 'localhost' + DEFAULT_PORT = 8090 + + def _enableWidgets(self, *args): + for field in [self.portField, self.hostField, self.testButton]: + field.configure( + state=tk.NORMAL if self.enable.get() else tk.DISABLED) + + def _test(self): + def test(): + goniec = socket.socket() + goniec.connect( + (self.host.get().strip(), + self.port.get(default=self.DEFAULT_PORT))) + goniec.close() + self.testError = network_test(test, self.testLabel) + + def _testError(self, event): + if self.testError is not None: + tkmb.showerror('Błąd połączenia z Gońcem', self.testError) + + def renderContent(self): + self.enable = NotifyIntVar() + self.enable.trace('w', self._enableWidgets) + self.host = NotifyStringVar() + self.port = NotifyNumericVar() + + self.columnconfigure(0, weight=1) + + frame = ttk.LabelFrame(self, text='Konfiguracja Gońca:') + frame.grid(row=0, column=0, columnspan=4, sticky=tk.W+tk.E+tk.N+tk.S) + (ttk.Checkbutton( + frame, text='Włącz obsługę Gońca', variable=self.enable)).grid( + row=0, column=0, columnspan=2, sticky=tk.W) + + (ttk.Label(frame, text='Host:')).grid(row=1, column=0) + self.hostField = ttk.Entry(frame, textvariable=self.host) + self.hostField.grid(row=1, column=1) + + (ttk.Label(frame, text='Port:')).grid(row=1, column=2) + self.portField = NumericSpinbox( + frame, textvariable=self.port, width=5) + self.portField.grid(row=1, column=3) + + self.testButton = ttk.Button( + frame, text='Testuj ustawienia', command=self._test) + self.testButton.grid(row=2, column=1, sticky=tk.E) + self.testError = None + self.testLabel = ttk.Label(frame) + self.testLabel.grid(row=2, column=2, sticky=tk.W) + self.testLabel.bind('<Button-1>', self._testError) + + self.setValues({}) + + def setValues(self, values): + self.host.set( + values['host'] if 'host' in values else self.DEFAULT_HOST) + self.port.set( + values['port'] if 'port' in values else self.DEFAULT_PORT) + self.enable.set(values['enabled'] if 'enabled' in values else 0) + + def getValues(self): + config = OrderedDict({ + 'enabled': self.enable.get() + }) + if self.enable.get(): + config['host'] = self.host.get() + config['port'] = self.port.get() + return config + +class RemoteConfigurationFrame(ScrollableFrame): + def renderContent(self, container): + frame = ttk.LabelFrame(container, text='Zdalne pliki konfiguracyjne:') + frame.pack( + side=tk.TOP, fill=tk.BOTH, expand=True) + self.repeater = WidgetRepeater( + frame, RepeatableEntry, classParams={'width':100}) + self.repeater.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + def setValues(self, values): + self.repeater.setValue(values) + + def getValues(self): + return self.repeater.getValue() + +__all__ = ['MySQLConfigurationFrame', 'GoniecConfigurationFrame', 'RemoteConfigurationFrame'] diff --git a/jfr_playoff/gui/frames/team.py b/jfr_playoff/gui/frames/team.py new file mode 100644 index 0000000..dbef680 --- /dev/null +++ b/jfr_playoff/gui/frames/team.py @@ -0,0 +1,483 @@ +#coding=utf-8 + +from collections import OrderedDict + +import tkinter as tk +from tkinter.font import Font +from tkinter import ttk + +from ..frames import GuiFrame, RepeatableFrame, ScrollableFrame +from ..frames import WidgetRepeater, RepeatableEntry, NumericSpinbox +from ..frames import SelectionButton, SelectionFrame, RefreshableOptionMenu +from ..frames import setPanelState +from ..variables import NotifyStringVar, NotifyIntVar, NotifyNumericVar + +class ManualTeamRow(RepeatableFrame): + def renderContent(self): + self.fullname = NotifyStringVar() + self.shortname = NotifyStringVar() + self.flag = NotifyStringVar() + self.position = NotifyNumericVar() + for var in [self.fullname, self.shortname, self.flag, self.position]: + var.trace('w', self._changeNotify) + + fullnameField = ttk.Entry(self, width=20, textvariable=self.fullname) + fullnameField.grid(row=0, column=0) + shortnameField = ttk.Entry(self, width=20, textvariable=self.shortname) + shortnameField.grid(row=0, column=1) + flagField = ttk.Entry(self, width=10, textvariable=self.flag) + flagField.grid(row=0, column=2) + positionField = ttk.Entry(self, width=10, textvariable=self.position) + positionField.grid(row=0, column=3) + + self._changeNotify(None) + + def getValue(self): + flag = self.flag.get().strip() + position = self.position.get() + return [ + self.fullname.get().strip(), self.shortname.get().strip(), + flag if len(flag) else None, position + ] + + def setValue(self, value): + self.fullname.set(value[0]) + self.shortname.set(value[1]) + if len(value) > 2: + if value[2] is not None: + self.flag.set(value[2]) + if len(value) > 3: + if value[3] is not None: + self.position.set(value[3]) + + def _changeNotify(self, *args): + self.winfo_toplevel().event_generate( + '<<TeamSettingsChanged>>', when='tail') + +class TeamManualSettingsFrame(GuiFrame): + def renderContent(self): + headers = [ + (ttk.Label, {'text': 'Pełna nazwa', 'width': 20}), + (ttk.Label, {'text': 'Skrócona nazwa', 'width': 20}), + (ttk.Label, {'text': 'Ikona', 'width': 10}), + (ttk.Label, {'text': 'Poz. końc.', 'width': 10}), + ] + self.repeater = WidgetRepeater(self, ManualTeamRow, headers=headers) + self.repeater.grid(row=1, column=0, columnspan=5) + + def getTeams(self): + return [val for val in self.repeater.getValue() if len(val[0].strip())] + + def setValues(self, values): + self.repeater.setValue(values) + +class TeamSelectionFrame(SelectionFrame): + def renderOption(self, container, option, idx): + (ttk.Label( + container, text='[%d]' % (self._mapValue(idx, option)))).grid( + row=idx+1, column=0) + (ttk.Checkbutton( + container, text=option[0], + variable=self.values[self._mapValue(idx, option)] + )).grid(row=idx+1, column=1, sticky=tk.W) + +class TeamSelectionButton(SelectionButton): + @property + def prompt(self): + return 'Wybierz teamy:' + + @property + def title(self): + return 'Wybór teamów' + + @property + def errorMessage(self): + return 'W turnieju nie ma teamów do wyboru' + + def getOptions(self): + return self.winfo_toplevel().getTeams() + + +class DBSelectionField(ttk.Entry): + def __init__(self, master, variable, value, *options, **kwargs): + kwargs['textvariable'] = variable + ttk.Entry.__init__(self, master, **kwargs) + self._variable = variable + self._variable.set(value) + self._optionDict = options if options is not None else [] + self._matches = [] + self._prevValue = None + self._setOptions() + self.bind('<KeyPress>', self._onPress) + self.bind('<KeyRelease>', self._onChange) + self.winfo_toplevel().bind( + '<<DBListChanged>>', self._setOptions, add='+') + + def _setOptions(self, *args): + try: + self._optionDict = self.winfo_toplevel().getDBs() + except: + # some stuff may not be available yet + # don't worry, the event will fire when it is + self._optionDict = [] + + def _onPress(self, event): + if event.keysym == 'Tab': + try: + suggestion = self.selection_get() + if len(suggestion) > 0: + prefix = self._variable.get()[0:-len(suggestion)] + phrase = prefix + suggestion + next_suggestion = self._matches[ + (self._matches.index(phrase)+1) % len(self._matches)] + prev_suggestion = self._matches[ + (self._matches.index(phrase)-1) % len(self._matches)] + new_suggestion = prev_suggestion if event.state & 1 \ + else next_suggestion + self.delete(0, tk.END) + self.insert(0, new_suggestion) + self.selection_range(len(prefix), tk.END) + return 'break' + except (tk.TclError, ValueError): + # no text selection or selection was altered, ignore + pass + + def _onChange(self, event): + if self._prevValue == self._variable.get() or event.keysym == 'Tab': + return + self._prevValue = self._variable.get() + prefix = self._variable.get() + if len(prefix) > 0: + matches = [d for d in self._optionDict if d.startswith(prefix)] + if len(matches) > 0: + self._matches = matches + text_to_add = matches[0][len(prefix):] + self.insert(tk.END, text_to_add) + self.selection_range(len(prefix), tk.END) + return + self._matches = [] + +class TeamFetchSettingsFrame(GuiFrame): + SOURCE_LINK = 0 + SOURCE_DB = 1 + + def _setFinishingPositions(self, positions): + self.finishingPositions = positions + self._changeNotify(None) + + def _changeNotify(self, *args): + self.winfo_toplevel().event_generate( + '<<TeamSettingsChanged>>', when='tail') + + def getTeams(self): + teams = OrderedDict() + if self.fetchSource.get() == self.SOURCE_LINK: + teams['link'] = self.fetchLink.get() + elif self.fetchSource.get() == self.SOURCE_DB: + teams['database'] = self.fetchDB.get() + if len(self.finishingPositions): + teams['final_positions'] = self.finishingPositions + maxTeams = self.fetchLimit.get() + if maxTeams: + teams['max_teams'] = maxTeams + return teams + + def _sourceChange(self, *args): + self.fetchDBField.configure(state=tk.DISABLED) + self.fetchLink.configure(state=tk.DISABLED) + if self.fetchSource.get() == self.SOURCE_LINK: + self.fetchLink.configure(state=tk.NORMAL) + elif self.fetchSource.get() == self.SOURCE_DB: + self.fetchDBField.configure(state=tk.NORMAL) + + def renderContent(self): + self.fetchSource = NotifyIntVar() + self.fetchSource.trace('w', self._sourceChange) + self.fetchSource.trace('w', self._changeNotify) + self.fetchDB = NotifyStringVar() + self.fetchDB.trace('w', self._changeNotify) + self.link = NotifyStringVar() + self.link.trace('w', self._changeNotify) + self.fetchLimit = NotifyNumericVar() + self.fetchLimit.trace('w', self._changeNotify) + + self.columnconfigure(3, weight=1) + + (ttk.Label(self, text=' ')).grid(row=0, column=0, rowspan=2) + + (ttk.Radiobutton( + self, text='Baza danych', + variable=self.fetchSource, value=self.SOURCE_DB)).grid( + row=0, column=1, columnspan=2, sticky=tk.W) + self.fetchDBField = DBSelectionField( + self, self.fetchDB, self.fetchDB.get()) + self.fetchDBField.grid(row=0, column=3, sticky=tk.W+tk.E) + + (ttk.Radiobutton( + self, text='Strona wyników', + variable=self.fetchSource, value=self.SOURCE_LINK)).grid( + row=1, column=1, columnspan=2, sticky=tk.W) + self.fetchLink = ttk.Entry(self, textvariable=self.link) + self.fetchLink.grid(row=1, column=3, sticky=tk.W+tk.E) + + (ttk.Label(self, text='Pobierz do ')).grid( + row=2, column=0, columnspan=2, sticky=tk.W) + (NumericSpinbox( + self, from_=0, to=9999, width=5, justify=tk.RIGHT, + textvariable=self.fetchLimit)).grid( + row=2, column=2, sticky=tk.W) + (ttk.Label(self, text=' miejsca (0 = wszystkie)')).grid( + row=2, column=3, sticky=tk.W+tk.E) + + (ttk.Label(self, text='Pozycje końcowe: ')).grid( + row=3, column=0, columnspan=3, sticky=tk.W+tk.E) + self.finishingPositionsBtn = TeamSelectionButton( + self, callback=self._setFinishingPositions, + prompt='Wybierz teamy, które zakończyły rozgrywki ' + \ + 'na swojej pozycji:', + dialogclass=TeamSelectionFrame) + self.finishingPositionsBtn.grid(row=3, column=3, sticky=tk.W) + self.finishingPositionsBtn.setPositions([]) + + def setValues(self, values): + if 'database' in values: + self.fetchSource.set(self.SOURCE_DB) + self.fetchDB.set(values['database']) + self.link.set('') + else: + self.fetchSource.set(self.SOURCE_LINK) + self.fetchDB.set('') + self.link.set(values['link'] if 'link' in values else '') + self.fetchLimit.set( + values['max_teams'] if 'max_teams' in values else 0) + self.finishingPositionsBtn.setPositions( + values['final_positions'] if 'final_positions' in values else []) + +class TeamSettingsFrame(ScrollableFrame): + FORMAT_FETCH = 0 + FORMAT_MANUAL = 1 + + def _enablePanels(self, *args): + panels = {self.FORMAT_FETCH: self.fetchSettingsFrame, + self.FORMAT_MANUAL: self.manualSettingsFrame} + for value, panel in panels.iteritems(): + setPanelState( + frame=panel, + state=tk.NORMAL \ + if self.teamFormat.get()==value else tk.DISABLED) + + def _changeNotify(self, *args): + self.winfo_toplevel().event_generate( + '<<TeamSettingsChanged>>', when='tail') + + def setTeams(self, event): + self.teams = self.winfo_toplevel().getTeams() + + def renderContent(self, container): + self.teamFormat = NotifyIntVar() + self.teamFormat.trace('w', self._enablePanels) + self.teamFormat.trace('w', self._changeNotify) + + container.columnconfigure(0, weight=1) + + (ttk.Radiobutton( + container, text='Pobierz z JFR Teamy:', + variable=self.teamFormat, value=self.FORMAT_FETCH)).grid( + row=0, column=0, sticky=tk.W) + + self.fetchSettingsFrame = TeamFetchSettingsFrame(container) + self.fetchSettingsFrame.grid(row=1, column=0, sticky=tk.W+tk.E) + + (ttk.Separator( + container, orient=tk.HORIZONTAL)).grid( + row=2, column=0, sticky=tk.W+tk.E) + + (ttk.Radiobutton( + container, text='Ustaw ręcznie:', + variable=self.teamFormat, value=self.FORMAT_MANUAL)).grid( + row=3, column=0, sticky=tk.W+tk.E) + + self.manualSettingsFrame = TeamManualSettingsFrame(container) + self.manualSettingsFrame.grid(row=4, column=0, sticky=tk.W+tk.E) + + self.teams = [] + self.winfo_toplevel().bind( + '<<TeamListChanged>>', self.setTeams, add='+') + + def getConfig(self): + if self.teamFormat.get() == self.FORMAT_MANUAL: + return self.manualSettingsFrame.getTeams() + elif self.teamFormat.get() == self.FORMAT_FETCH: + return self.fetchSettingsFrame.getTeams() + return [] + + def setValues(self, values): + if isinstance(values, list): + self.teamFormat.set(self.FORMAT_MANUAL) + self.manualSettingsFrame.setValues(values) + self.fetchSettingsFrame.setValues({}) + else: + self.teamFormat.set(self.FORMAT_FETCH) + self.manualSettingsFrame.setValues([]) + self.fetchSettingsFrame.setValues(values) + +class TeamList(RefreshableOptionMenu): + def __init__(self, *args, **kwargs): + RefreshableOptionMenu.__init__(self, *args, **kwargs) + self.winfo_toplevel().bind( + '<<TeamListChanged>>', self.refreshOptions, add='+') + self.configure(width=10) + + def getLabel(self, team): + return team[0] + + def getValues(self): + return self.winfo_toplevel().getTeams() + + +class TeamAliasRow(RepeatableFrame): + def renderContent(self): + self.columnconfigure(0, weight=0) + self.columnconfigure(1, weight=1) + self.teamName = NotifyStringVar() + list = TeamList(self, self.teamName, self.teamName.get()) + list.configure(width=20) + list.grid( + row=0, column=0, sticky=tk.W+tk.E+tk.N) + self.names = WidgetRepeater(self, RepeatableEntry) + self.names.grid(row=0, column=1, sticky=tk.W+tk.E) + + def getValue(self): + return ( + self.teamName.get().strip(), + [val.strip() for val in self.names.getValue()]) + + def setValue(self, value): + self.teamName.set(value[0]) + self.names.setValue(value[1]) + + +class TeamAliasFrame(ScrollableFrame): + def renderContent(self, container): + container.columnconfigure(0, weight=1) + (ttk.Label(container, text='Aliasy teamów')).grid( + row=0, column=0, sticky=tk.W+tk.E) + self.repeater = WidgetRepeater(container, TeamAliasRow) + self.repeater.grid(row=1, column=0, sticky=tk.W+tk.E) + + def getConfig(self): + return OrderedDict( + {val[0]: val[1] for val in self.repeater.getValue() if val[0]}) + + def setValues(self, values): + self.repeater.setValue(list(values.iteritems())) + +class TeamPreviewFrame(ScrollableFrame): + def __init__(self, *args, **kwags): + self.tieValues = [] + self.tieFields = [] + self.labels = [] + ScrollableFrame.__init__(self, *args, **kwags) + self.winfo_toplevel().bind( + '<<TeamListChanged>>', self.refreshTeams, add='+') + self.winfo_toplevel().bind( + '<<TieConfigChanged>>', self._collectTieConfig, add='+') + self._tieConfig = [] + self._lockTieValues = False + + def setTeams(self, container, teams): + self.teamList.grid( + row=1, column=0, rowspan=len(teams)+2, sticky=tk.W+tk.E+tk.N+tk.S) + self.tieValues = self.tieValues[0:len(teams)] + for idx in range(len(teams), len(self.tieFields)): + self.tieFields[idx].destroy() + self.tieFields = self.tieFields[0:len(teams)] + for label in self.labels: + label.destroy() + self.teamList.delete(*self.teamList.get_children()) + for idx, team in enumerate(teams): + if len(team) > 2 and team[2] is None: + team[2] = '' + self.teamList.insert('', tk.END, values=team, tag=idx) + if idx >= len(self.tieFields): + self.tieValues.append(NotifyNumericVar()) + self.tieValues[idx].trace('w', self._tieValueChangeNotify) + self.tieFields.append( + NumericSpinbox( + container, from_=0, to=9999, + width=5, font=Font(size=10), + textvariable=self.tieValues[idx])) + self.tieFields[idx].grid( + row=idx+2, column=1, sticky=tk.W+tk.E+tk.N) + container.rowconfigure(idx+2, weight=0) + self.labels.append(ttk.Label(container, text=' ')) + self.labels[-1].grid(row=1, column=1, pady=3) + self.labels.append(ttk.Label(container, text=' ')) + self.labels[-1].grid(row=len(teams)+2, column=1) + container.rowconfigure(1, weight=0) + container.rowconfigure(len(teams)+2, weight=1) + self.labels.append(ttk.Label( + container, + text='Kolejność rozstrzygania remisów w klasyfikacji ' + \ + 'pobranej z bazy JFR Teamy', + anchor=tk.E)) + self.labels[-1].grid(row=len(teams)+3, column=0, sticky=tk.N+tk.E) + self.labels.append(ttk.Label(container, text='⬏', font=Font(size=20))) + self.labels[-1].grid( + row=len(teams)+3, column=1, sticky=tk.W+tk.N) + container.rowconfigure(len(teams)+3, weight=1) + + def renderContent(self, container): + container.columnconfigure(0, weight=1) + (ttk.Label(container, text='Podgląd listy teamów')).grid( + row=0, column=0, columnspan=2, sticky=tk.W+tk.E) + self.teamList = ttk.Treeview( + container, show='headings', + columns=['fullname','shortname','icon','position'], + selectmode='browse') + for col, heading in enumerate( + [('Nazwa', 100), ('Skrócona nazwa', 100), + ('Ikona', 20), ('Poz. końc.', 20)]): + self.teamList.heading(col, text=heading[0]) + if heading[1]: + self.teamList.column(col, width=heading[1], stretch=True) + self.container = container + + def getTieConfig(self): + teams = self._getTeams() + ties = [(teams[idx], val.get(default=0)) + for idx, val in enumerate(self.tieValues)] + return [team[0][0] for team + in sorted(ties, key=lambda t: t[1]) + if team[1] > 0] + + def setTieConfig(self, values): + self._tieConfig = values + self.refreshTeams(None) + + def _tieValueChangeNotify(self, *args): + if not self._lockTieValues: + self.winfo_toplevel().event_generate( + '<<TieConfigChanged>>', when='tail') + + def _getTeams(self): + return self.winfo_toplevel().getTeams() + + def _collectTieConfig(self, *args): + if not self._lockTieValues: + self._tieConfig = self.getTieConfig() + + def refreshTeams(self, event): + self._lockTieValues = True + teams = self._getTeams() + self.setTeams(self.container, teams) + for tidx, team in enumerate(teams): + self.tieValues[tidx].set(0) + for idx, tie in enumerate(self._tieConfig): + if team[0] == tie: + self.tieValues[tidx].set(idx+1) + break + self._lockTieValues = False + + +__all__ = ['TeamSettingsFrame', 'TeamAliasFrame', 'TeamPreviewFrame'] diff --git a/jfr_playoff/gui/frames/translations.py b/jfr_playoff/gui/frames/translations.py new file mode 100644 index 0000000..a369159 --- /dev/null +++ b/jfr_playoff/gui/frames/translations.py @@ -0,0 +1,49 @@ +#coding=utf-8 + +import copy +from collections import OrderedDict + +import tkinter as tk +from tkinter import ttk + +from ..frames import RepeatableFrame, WidgetRepeater, ScrollableFrame +from ...i18n import PLAYOFF_I18N_DEFAULTS +from ..variables import NotifyStringVar + +class TranslationRow(RepeatableFrame): + def renderContent(self): + self.key = NotifyStringVar() + self.value = NotifyStringVar() + + (ttk.Entry(self, textvariable=self.key, width=40)).pack( + side=tk.LEFT, fill=tk.BOTH, expand=True) + (ttk.Entry(self, textvariable=self.value, width=80)).pack( + side=tk.RIGHT, fill=tk.BOTH, expand=True) + + def setValue(self, value): + self.key.set(value[0]) + self.value.set(value[1]) + + def getValue(self): + return (self.key.get(), self.value.get()) + +class TranslationConfigurationFrame(ScrollableFrame): + + def setTranslations(self, translations): + default_translations = copy.copy(PLAYOFF_I18N_DEFAULTS) + default_translations.update(translations) + values = [] + for value in default_translations.iteritems(): + values.append(value) + self.repeater.setValue(values) + + def getTranslations(self): + return OrderedDict({ + key: value for key, value in self.repeater.getValue() + }) + + def renderContent(self, container): + self.repeater = WidgetRepeater(container, TranslationRow) + self.repeater.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + +__all__ = ['TranslationConfigurationFrame'] diff --git a/jfr_playoff/gui/frames/visual.py b/jfr_playoff/gui/frames/visual.py new file mode 100644 index 0000000..147d6fe --- /dev/null +++ b/jfr_playoff/gui/frames/visual.py @@ -0,0 +1,452 @@ +#coding=utf-8 + +from collections import OrderedDict + +import tkinter as tk +from tkinter import ttk +import tkColorChooser as tkcc + +from ..frames import GuiFrame, RepeatableFrame, ScrollableFrame +from ..frames import WidgetRepeater +from ..frames import SelectionFrame, RefreshableOptionMenu, NumericSpinbox +from ..frames.team import TeamSelectionButton +from ..variables import NotifyStringVar, NotifyIntVar, NotifyNumericVar + +class VisualSettingsFrame(GuiFrame): + DEFAULT_VALUES = { + 'width': 250, + 'height': 80, + 'margin': 60, + 'starting_position_indicators': 0, + 'finishing_position_indicators': 0, + 'team_boxes': { + 'label_length_limit': 25, + 'predict_teams': 1, + 'label_separator': ' / ', + 'label_placeholder': '??', + 'label_ellipsis': '(...)', + 'name_separator': '<br />', + 'name_prefix': ' ', + 'sort_eligible_first': 1 + } + } + + def renderContent(self): + self.startingPositionIndicators = NotifyIntVar() + self.finishingPositionIndicators = NotifyIntVar() + self.boxWidth = NotifyNumericVar() + self.boxHeight = NotifyNumericVar() + self.boxMargin = NotifyNumericVar() + self.shortenTeamNames = NotifyIntVar() + self.teamNameLength = NotifyNumericVar() + self.teamNameEllipsis = NotifyStringVar() + self.teamNamePredict = NotifyIntVar() + self.teamNamePlaceholder = NotifyStringVar() + self.teamNameSortPredictions = NotifyIntVar() + self.teamLabelSeparator = NotifyStringVar() + self.teamNameSeparator = NotifyStringVar() + self.teamNamePrefix = NotifyStringVar() + + indicatorsFrame = ttk.LabelFrame(self, text='Znaczniki pozycji:') + indicatorsFrame.grid(row=0, column=0, sticky=tk.W+tk.E+tk.N+tk.S) + dimensionsFrame = ttk.LabelFrame(self, text='Wymiary tabelki meczu:') + dimensionsFrame.grid(row=0, column=1, sticky=tk.W+tk.E+tk.N+tk.S) + teamNamesFrame = ttk.LabelFrame(self, text='Nazwy teamów:') + teamNamesFrame.grid(row=1, column=0, sticky=tk.W+tk.E+tk.N+tk.S) + separatorsFrame = ttk.LabelFrame(self, text='Separatory nazw teamów:') + separatorsFrame.grid(row=1, column=1, sticky=tk.W+tk.E+tk.N+tk.S) + + self._fieldsToEnable = [] + + (ttk.Checkbutton( + indicatorsFrame, text='początkowych', + variable=self.startingPositionIndicators)).grid( + row=0, column=0, sticky=tk.W) + (ttk.Checkbutton( + indicatorsFrame, text='końcowych', + variable=self.finishingPositionIndicators)).grid( + row=1, column=0, sticky=tk.W) + + (NumericSpinbox( + dimensionsFrame, width=5, from_=1, to=999, + textvariable=self.boxWidth)).grid( + row=0, column=0, sticky=tk.W) + (ttk.Label(dimensionsFrame, text='x')).grid(row=0, column=1) + (NumericSpinbox( + dimensionsFrame, width=5, from_=1, to=999, + textvariable=self.boxHeight)).grid( + row=0, column=2, sticky=tk.W) + (ttk.Label(dimensionsFrame, text='odstępy')).grid( + row=1, column=0, columnspan=2, sticky=tk.E) + (NumericSpinbox( + dimensionsFrame, width=5, from_=1, to=999, + textvariable=self.boxMargin)).grid( + row=1, column=2, sticky=tk.W) + + (ttk.Checkbutton( + teamNamesFrame, text='skracaj do', + variable=self.shortenTeamNames)).grid( + row=0, column=0, columnspan=2) + nameLength = NumericSpinbox( + teamNamesFrame, width=5, from_=1, to=999, + textvariable=self.teamNameLength) + nameLength.grid(row=0, column=2, sticky=tk.W) + lengthLabel = ttk.Label(teamNamesFrame, text='znaków') + lengthLabel.grid(row=0, column=3, sticky=tk.W) + ellipsisLabel = ttk.Label(teamNamesFrame, text='znacznik:') + ellipsisLabel.grid(row=1, column=0, columnspan=2, sticky=tk.E) + nameEllipsis = ttk.Entry( + teamNamesFrame, width=5, + textvariable=self.teamNameEllipsis) + nameEllipsis.grid(row=1, column=2, sticky=tk.W) + (ttk.Checkbutton( + teamNamesFrame, + text='przewiduj na podstawie trwających meczów', + variable=self.teamNamePredict)).grid( + row=2, column=0, columnspan=5) + placeholderLabel = ttk.Label( + teamNamesFrame, text='etykieta nieznanych teamów') + placeholderLabel.grid(row=3, column=1, columnspan=3, sticky=tk.W) + namePlaceholder = ttk.Entry( + teamNamesFrame, width=5, + textvariable=self.teamNamePlaceholder) + namePlaceholder.grid(row=3, column=4, sticky=tk.W) + predictSort = ttk.Checkbutton( + teamNamesFrame, text='wyświetlaj najpierw pewne teamy', + variable=self.teamNameSortPredictions) + predictSort.grid(row=4, column=1, columnspan=4, sticky=tk.W) + self._fieldsToEnable.append( + (self.shortenTeamNames, + [nameLength, nameEllipsis, lengthLabel, ellipsisLabel])) + self._fieldsToEnable.append( + (self.teamNamePredict, + [namePlaceholder, placeholderLabel, predictSort])) + + (ttk.Label(separatorsFrame, text=' ')).grid(row=0, column=0) + (ttk.Label(separatorsFrame, text='w drabince (skrócone nazwy)')).grid( + row=0, column=1, sticky=tk.E) + (ttk.Entry( + separatorsFrame, width=8, + textvariable=self.teamLabelSeparator)).grid( + row=0, column=2, sticky=tk.W) + (ttk.Label(separatorsFrame, text='w "dymkach" (pełne nazwy)')).grid( + row=1, column=1, sticky=tk.E) + (ttk.Entry( + separatorsFrame, width=8, + textvariable=self.teamNameSeparator)).grid( + row=1, column=2, sticky=tk.W) + (ttk.Label(separatorsFrame, text='prefiks pełnych nazw')).grid( + row=2, column=1, sticky=tk.E) + (ttk.Entry( + separatorsFrame, width=8, + textvariable=self.teamNamePrefix)).grid( + row=2, column=2, sticky=tk.W) + + for var, fields in self._fieldsToEnable: + var.trace('w', self._enableFields) + self._enableFields() + + self.setValues({}) + + def _enableFields(self, *args): + for var, fields in self._fieldsToEnable: + for field in fields: + field.configure(state=tk.NORMAL if var.get() else tk.DISABLED) + + def setValues(self, values): + default_values = self.DEFAULT_VALUES + if 'team_boxes' in values: + default_values['team_boxes'].update(values['team_boxes']) + del values['team_boxes'] + default_values.update(values) + values = default_values + + self.startingPositionIndicators.set( + values['starting_position_indicators']) + self.finishingPositionIndicators.set( + values['finishing_position_indicators']) + self.boxWidth.set(values['width']) + self.boxHeight.set(values['height']) + self.boxMargin.set(values['margin']) + self.shortenTeamNames.set( + values['team_boxes']['label_length_limit'] > 0) + self.teamNameLength.set(values['team_boxes']['label_length_limit']) + self.teamNameEllipsis.set(values['team_boxes']['label_ellipsis']) + self.teamNamePredict.set(values['team_boxes']['predict_teams']) + self.teamNamePlaceholder.set(values['team_boxes']['label_placeholder']) + self.teamNameSortPredictions.set( + values['team_boxes']['sort_eligible_first']) + self.teamLabelSeparator.set(values['team_boxes']['label_separator']) + self.teamNameSeparator.set(values['team_boxes']['name_separator']) + self.teamNamePrefix.set(values['team_boxes']['name_prefix']) + + def getValues(self): + return OrderedDict( + { + 'width': self.boxWidth.get(default=250), + 'height': self.boxHeight.get(default=80), + 'margin': self.boxMargin.get(default=60), + 'starting_position_indicators': self.startingPositionIndicators.get(), + 'finishing_position_indicators': self.finishingPositionIndicators.get(), + 'team_boxes': { + 'label_length_limit': self.teamNameLength.get(default=25) if self.shortenTeamNames else 0, + 'predict_teams': self.teamNamePredict.get(), + 'label_separator': self.teamLabelSeparator.get(), + 'label_placeholder': self.teamNamePlaceholder.get(), + 'label_ellipsis': self.teamNameEllipsis.get(), + 'name_separator': self.teamNameSeparator.get(), + 'name_prefix': self.teamNamePrefix.get(), + 'sort_eligible_first': self.teamNameSortPredictions.get() + } + }) + + +class MatchList(RefreshableOptionMenu): + def __init__(self, *args, **kwargs): + RefreshableOptionMenu.__init__(self, *args, **kwargs) + self.winfo_toplevel().bind( + '<<MatchListChanged>>', self.refreshOptions, add='+') + self.configure(width=10) + + def getLabel(self, match): + return match.label + + def getValues(self): + try: + return self.winfo_toplevel().getMatches() + except tk.TclError: + # we're probably being destroyed, ignore + return [] + + def getVarValue(self, match): + return unicode(match.getMatchID()) + + +class BoxPositionFrame(RepeatableFrame): + def renderContent(self): + self.match = NotifyStringVar() + self.vertical = NotifyNumericVar() + self.horizontal = NotifyNumericVar() + self.matchBox = MatchList(self, self.match) + self.matchBox.configure(width=20) + self.matchBox.grid(row=0, column=0) + + (ttk.Label(self, text=' w pionie:')).grid(row=0, column=1) + (NumericSpinbox( + self, textvariable=self.vertical, from_=0, to=9999, + width=5)).grid( + row=0, column=2) + (ttk.Label(self, text=' w poziomie (-1 = automatyczna):')).grid( + row=0, column=3) + (NumericSpinbox( + self, textvariable=self.horizontal, from_=-1, to=9999, + width=5)).grid( + row=0, column=4) + self.setValue([]) + + def setValue(self, value): + if len(value) > 1: + self.match.set(value[0]) + self.vertical.set(value[1]) + if len(value) > 2: + self.horizontal.set(value[2]) + else: + self.horizontal.set(-1) + else: + self.match.set('') + self.vertical.set(0) + self.horizontal.set(-1) + + def getValue(self): + horizontal = self.horizontal.get(default=-1) + vertical = self.vertical.get(default=0) + return ( + self.match.get(), + [vertical, horizontal] if horizontal >= 0 else vertical) + +class BoxPositionsFrame(ScrollableFrame): + def renderContent(self, container): + (ttk.Label(container, text='Pozycje tabelek meczów:')).pack( + side=tk.TOP, anchor=tk.W) + self.positions = WidgetRepeater(container, BoxPositionFrame) + self.positions.pack( + side=tk.TOP, fill=tk.BOTH, expand=True) + + def setValues(self, values): + values = sorted(list(values.iteritems()), key=lambda v: int(v[0])) + values_to_set = [] + for idx, val in enumerate(values): + value = [val[0]] + if isinstance(val[1], list): + value += val[1] + else: + value.append(val[1]) + values_to_set.append(value) + self.positions.setValue(values_to_set) + + def getValues(self): + return OrderedDict( + { match: config for match, config in self.positions.getValue() }) + +class LineStyle(GuiFrame): + def _selectColour(self): + colour = tkcc.askcolor(self._getColour()) + if colour is not None: + self._setColour(colour[1]) + + def _getColour(self): + return self.colourBtn.cget('bg') + + def _setColour(self, colour): + self.colourBtn.configure(bg=colour) + + def renderContent(self): + self.hOffset = NotifyNumericVar() + self.vOffset = NotifyNumericVar() + + (ttk.Label(self, text='kolor:')).grid(row=0, column=0) + self.colourBtn = tk.Button(self, width=2, command=self._selectColour) + self.colourBtn.grid(row=0, column=1) + (ttk.Label(self, text='margines w poziomie:')).grid(row=0, column=2) + (NumericSpinbox( + self, textvariable=self.hOffset, from_=-50, to=50, + width=5)).grid(row=0, column=3) + (ttk.Label(self, text='margines w pionie:')).grid(row=0, column=4) + (NumericSpinbox( + self, textvariable=self.vOffset, from_=-50, to=50, + width=5)).grid(row=0, column=5) + + def setValue(self, value): + self._setColour(value[0]) + self.hOffset.set(value[1]) + self.vOffset.set(value[2]) + + def getValue(self): + return [self._getColour(), self.hOffset.get(), self.vOffset.get()] + +class LineStylesFrame(GuiFrame): + DEFAULT_VALUES = [ + ('winner', ('#00ff00', 5, -10), + 'Zwycięzcy meczów: '), + ('loser', ('#ff0000', 20, 10), + 'Przegrani meczów: '), + ('place_winner', ('#00dddd', 10, 2), + 'Pozycje startowe (wybierający): '), + ('place_loser', ('#dddd00', 15, 9), + 'Pozycje startowe (wybierani): '), + ('finish_winner', ('#00ff00', 5, -10), + 'Zwycięzcy meczów kończący rozgrywki: '), + ('finish_loser', ('#ff0000', 20, 10), + 'Przegrani meczów kończący rozgrywki: ') + ] + CONFIG_KEYS = ['colour', 'h_offset', 'v_offset'] + + def renderContent(self): + self.lines = OrderedDict() + for idx, line in enumerate(self.DEFAULT_VALUES): + self.lines[line[0]] = LineStyle(self) + self.lines[line[0]].grid(row=idx+1, column=1, sticky=tk.W) + (ttk.Label(self, text=line[2])).grid( + row=idx+1, column=0, sticky=tk.E) + (ttk.Label(self, text='Kolory linii')).grid( + row=0, column=0, columnspan=2, sticky=tk.W) + + def setValues(self, values): + for line in self.DEFAULT_VALUES: + value = list(line[1]) + for idx, key in enumerate(self.CONFIG_KEYS): + key = '%s_%s' % (line[0], key) + if key in values: + value[idx] = values[key] + self.lines[line[0]].setValue(value) + + def getValues(self): + config = OrderedDict() + for line, widget in self.lines.iteritems(): + value = widget.getValue() + for idx, key in enumerate(self.CONFIG_KEYS): + config['%s_%s' % (line, key)] = value[idx] + return config + + +class PositionsSelectionFrame(SelectionFrame): + COLUMN_COUNT=10 + + def __init__(self, *args, **kwargs): + SelectionFrame.__init__(self, *args, **kwargs) + self.winfo_toplevel().geometry( + '%dx%d' % ( + self.COLUMN_COUNT * 40, + (len(self.options) / self.COLUMN_COUNT + 2) * 25 + 30 + )) + + def renderHeader(self, container): + (ttk.Label(container, text=self.title)).grid( + row=0, column=0, columnspan=self.COLUMN_COUNT, sticky=tk.W) + + def renderOption(self, container, option, idx): + (ttk.Checkbutton( + container, text=str(self._mapValue(idx, option)), + variable=self.values[self._mapValue(idx, option)] + )).grid( + row=(idx/self.COLUMN_COUNT)+1, column=idx%self.COLUMN_COUNT, + sticky=tk.W) + +class PositionStyleFrame(RepeatableFrame): + def _setPositions(self, values): + self.positions = values + + def renderContent(self): + self.name = NotifyStringVar() + self.description = NotifyStringVar() + + self.columnconfigure(1, weight=1) + self.columnconfigure(5, weight=1) + + (ttk.Label(self, text='Styl:')).grid(row=0, column=0) + (ttk.Entry(self, textvariable=self.name)).grid( + row=0, column=1, sticky=tk.W+tk.E) + + (ttk.Label(self, text='Pozycje końcowe:')).grid(row=0, column=2) + self.positionBtn = TeamSelectionButton( + self, prompt='Wybierz pozycje końcowe:', + dialogclass=PositionsSelectionFrame, + callback=self._setPositions) + self.positionBtn.grid(row=0, column=3) + + (ttk.Label(self, text='Opis w legendzie:')).grid(row=0, column=4) + (ttk.Entry(self, textvariable=self.description)).grid( + row=0, column=5, sticky=tk.W+tk.E) + + self.setValue({}) + + def setValue(self, value): + self.name.set(value['class'] if 'class' in value else '') + self.positionBtn.setPositions( + value['positions'] if 'positions' in value else []) + self.description.set(value['caption'] if 'caption' in value else '') + + def getValue(self): + config = OrderedDict({ + 'class': self.name.get(), + 'positions': self.positions + }) + caption = self.description.get() + if caption: + config['caption'] = caption + return config + +class PositionStylesFrame(ScrollableFrame): + def renderContent(self, container): + (ttk.Label(container, text='Klasyfikacja końcowa')).pack( + side=tk.TOP, anchor=tk.W) + self.styles = WidgetRepeater(container, PositionStyleFrame) + self.styles.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + def setValues(self, values): + self.styles.setValue(values) + + def getValues(self): + return self.styles.getValue() + +__all__ = ['VisualSettingsFrame', 'BoxPositionsFrame', 'LineStylesFrame', 'PositionStylesFrame'] diff --git a/jfr_playoff/gui/icons.py b/jfr_playoff/gui/icons.py new file mode 100644 index 0000000..94d3d63 --- /dev/null +++ b/jfr_playoff/gui/icons.py @@ -0,0 +1,31 @@ +import os, sys + +import Tkinter as tk + +class GuiImage(object): + icons = {} + + @staticmethod + def __get_base_path(): + try: + return os.path.join(sys._MEIPASS, 'res').decode( + sys.getfilesystemencoding()) + except: + return os.path.abspath(os.path.dirname(__file__)).decode( + sys.getfilesystemencoding()) + + @staticmethod + def get_path(imageType, code, fileType='gif'): + return os.path.join( + GuiImage.__get_base_path(), imageType, '%s.%s' % (code, fileType)) + + @staticmethod + def __get_image(imageType, cache, code, fileType='gif'): + if code not in cache: + path = GuiImage.get_path(imageType, code, fileType) + cache[code] = tk.PhotoImage(file=path) + return cache[code] + + @staticmethod + def get_icon(code): + return GuiImage.__get_image('icons', GuiImage.icons, code) diff --git a/jfr_playoff/gui/icons/log.gif b/jfr_playoff/gui/icons/log.gif Binary files differnew file mode 100644 index 0000000..7915311 --- /dev/null +++ b/jfr_playoff/gui/icons/log.gif diff --git a/jfr_playoff/gui/icons/new.gif b/jfr_playoff/gui/icons/new.gif Binary files differnew file mode 100644 index 0000000..e816390 --- /dev/null +++ b/jfr_playoff/gui/icons/new.gif diff --git a/jfr_playoff/gui/icons/open.gif b/jfr_playoff/gui/icons/open.gif Binary files differnew file mode 100644 index 0000000..c7adaf2 --- /dev/null +++ b/jfr_playoff/gui/icons/open.gif diff --git a/jfr_playoff/gui/icons/playoff.ico b/jfr_playoff/gui/icons/playoff.ico Binary files differnew file mode 100644 index 0000000..0cdb123 --- /dev/null +++ b/jfr_playoff/gui/icons/playoff.ico diff --git a/jfr_playoff/gui/icons/run-once.gif b/jfr_playoff/gui/icons/run-once.gif Binary files differnew file mode 100644 index 0000000..842afd8 --- /dev/null +++ b/jfr_playoff/gui/icons/run-once.gif diff --git a/jfr_playoff/gui/icons/run-timed.gif b/jfr_playoff/gui/icons/run-timed.gif Binary files differnew file mode 100644 index 0000000..1b8340e --- /dev/null +++ b/jfr_playoff/gui/icons/run-timed.gif diff --git a/jfr_playoff/gui/icons/save.gif b/jfr_playoff/gui/icons/save.gif Binary files differnew file mode 100644 index 0000000..4452f38 --- /dev/null +++ b/jfr_playoff/gui/icons/save.gif diff --git a/jfr_playoff/gui/icons/saveas.gif b/jfr_playoff/gui/icons/saveas.gif Binary files differnew file mode 100644 index 0000000..25cde78 --- /dev/null +++ b/jfr_playoff/gui/icons/saveas.gif diff --git a/jfr_playoff/gui/icons/stop-timed.gif b/jfr_playoff/gui/icons/stop-timed.gif Binary files differnew file mode 100644 index 0000000..b5fc3fd --- /dev/null +++ b/jfr_playoff/gui/icons/stop-timed.gif diff --git a/jfr_playoff/gui/logframe.py b/jfr_playoff/gui/logframe.py new file mode 100644 index 0000000..078226e --- /dev/null +++ b/jfr_playoff/gui/logframe.py @@ -0,0 +1,104 @@ +#coding=utf-8 + +import datetime +import logging as log +from collections import OrderedDict + +import tkinter as tk +from tkinter import ttk +import tkFileDialog as tkfd + +class LogWindow(tk.Toplevel): + def __init__(self, *args, **kwargs): + tk.Toplevel.__init__(self, *args, **kwargs) + self.withdraw() + self.protocol('WM_DELETE_WINDOW', self.withdraw) + self.renderContents() + self._records = [] + self._counter = -1 + self._registerLogging() + + def renderContents(self): + columns = [ + ('level', 'Poziom komunikatu', 150), + ('category', 'Moduł', 150), + ('message', 'Komunikat', None)] + self.logList = ttk.Treeview( + self, show='headings', + columns=[c[0] for c in columns], + selectmode='browse') + for column, heading, width in columns: + self.logList.heading(column, text=heading) + if width is not None: + self.logList.column(column, width=width, stretch=False) + else: + self.logList.column(column, stretch=True) + self.logList.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + btnFrame = tk.Frame(self) + btnFrame.pack(side=tk.BOTTOM) + (ttk.Button( + btnFrame, text='Zapisz dziennik...', + command=self.onRecordsSave)).pack(side=tk.LEFT) + (ttk.Button( + btnFrame, text='Wyczyść dziennik', + command=self.resetRecords)).pack(side=tk.LEFT) + + def _getGUIHandler(self): + return LogHandler(log.INFO, window=self) + + def _getConsoleHandler(self): + consoleHandler = log.StreamHandler() + consoleHandler.setFormatter( + log.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s')) + return consoleHandler + + def _registerLogging(self): + logger = log.getLogger() + logger.setLevel(log.INFO) + for handler in [self._getConsoleHandler, self._getGUIHandler]: + logger.addHandler(handler()) + + def addRecord(self, record): + self._counter += 1 + self._records.append((record, datetime.datetime.now())) + if not isinstance(record.message, unicode): + record.message = unicode(record.message, errors='replace') + self.logList.insert( + '', tk.END, tag=self._counter, values=[ + record.levelname, record.name, record.message + ]) + self.logList.yview_moveto(1) + + def resetRecords(self): + self._records = [] + self.logList.delete(*self.logList.get_children()) + + def onRecordsSave(self, *args): + filename = tkfd.asksaveasfilename( + title='Wybierz plik dziennika', + filetypes=(('Log files', '*.log'),)) + if filename: + if not filename.lower().endswith('.log'): + filename = filename + '.log' + self._saveRecords(filename) + + def _saveRecords(self, filename): + with open(filename, 'w') as fileObj: + for record, timestamp in self._records: + fileObj.write(( + u'%s\t%s\t%s\t%s\n' % ( + timestamp, + record.levelname, record.name, record.message)).encode( + 'utf8')) + + +class LogHandler(log.Handler): + def __init__(self, *args, **kwargs): + self._window = kwargs['window'] + del kwargs['window'] + log.Handler.__init__(self, *args, **kwargs) + + def handle(self, record): + self.format(record) + self._window.addRecord(record) diff --git a/jfr_playoff/gui/tabs.py b/jfr_playoff/gui/tabs.py new file mode 100644 index 0000000..cc777fa --- /dev/null +++ b/jfr_playoff/gui/tabs.py @@ -0,0 +1,497 @@ +#coding=utf-8 + +import os +from collections import OrderedDict + +import tkinter as tk +from tkinter import ttk +import tkFileDialog as tkfd +import tkMessageBox as tkmb + +from .frames import TraceableText, NumericSpinbox +from .frames.match import * +from .frames.network import * +from .frames.team import * +from .frames.translations import * +from .frames.visual import * +from .variables import NotifyStringVar, NotifyIntVar, NotifyNumericVar + +from ..data import PlayoffData +from ..db import PlayoffDB + +class PlayoffTab(ttk.Frame): + def __init__(self, master): + ttk.Frame.__init__(self, master) + self.frame = ttk.Frame(self) + self.frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + self.initData() + self.renderContent(self.frame) + + @property + def title(self): + pass + + def initData(self): + pass + + def renderContent(self, container): + pass + + def setValues(self, config): + pass + + def getConfig(self): + pass + +class MainSettingsTab(PlayoffTab): + DEFAULT_INTERVAL = 60 + + @property + def title(self): + return 'Główne ustawienia' + + def initData(self): + self.outputPath = NotifyStringVar() + self.pageTitle = NotifyStringVar() + self.pageLogoh = NotifyStringVar() + self.refresh = NotifyIntVar() + self.refresh.trace('w', self._updateRefreshFields) + self.refreshInterval = NotifyNumericVar() + + def _chooseOutputPath(self): + currentPath = self.outputPath.get() + filename = tkfd.asksaveasfilename( + initialdir=os.path.dirname(currentPath) if currentPath else '.', + title='Wybierz plik wyjściowy', + filetypes=(('HTML files', '*.html'),)) + if filename: + if not filename.lower().endswith('.html'): + filename = filename + '.html' + self.outputPath.set(filename) + + def _updateRefreshFields(self, *args): + self.intervalField.configure( + state=tk.NORMAL if self.refresh.get() else tk.DISABLED) + + def setValues(self, config): + self.outputPath.set(config['output'] if 'output' in config else '') + if 'page' in config: + self.pageTitle.set( + config['page']['title'] if 'title' in config['page'] else '') + self.pageLogoh.set( + config['page']['logoh'] if 'logoh' in config['page'] else '') + try: + interval = int(config['page']['refresh']) + if interval > 0: + self.refresh.set(1) + self.refreshInterval.set(interval) + else: + self.refresh.set(0) + self.refreshInterval.set(self.DEFAULT_INTERVAL) + except: + self.refresh.set(0) + self.refreshInterval.set(self.DEFAULT_INTERVAL) + else: + self.pageTitle.set('') + self.pageLogoh.set('') + self.refresh.set(0) + self.refreshInterval.set(self.DEFAULT_INTERVAL) + + def renderContent(self, container): + (ttk.Label(container, text='Plik wynikowy:')).grid( + row=0, column=0, sticky=tk.E, pady=2) + outputPath = tk.Frame(container) + outputPath.grid(row=0, column=1, sticky=tk.E+tk.W, pady=2) + (ttk.Entry(outputPath, width=60, textvariable=self.outputPath)).grid( + row=0, column=0, sticky=tk.W+tk.E) + (ttk.Button( + outputPath, + text='wybierz...', command=self._chooseOutputPath)).grid( + row=0, column=1) + outputPath.columnconfigure(0, weight=1) + + (ttk.Separator(container, orient=tk.HORIZONTAL)).grid( + row=1, column=0, columnspan=2, sticky=tk.E+tk.W, pady=2) + + pageSettings = ttk.LabelFrame( + container, text='Ustawienia strony') + pageSettings.grid( + row=2, column=0, columnspan=2, sticky=tk.W+tk.E+tk.N+tk.S, pady=5) + + pageSettings.columnconfigure(1, weight=1) + + (ttk.Label(pageSettings, text='Tytuł:')).grid( + row=0, column=0, sticky=tk.E, pady=2) + (tk.Entry(pageSettings, textvariable=self.pageTitle)).grid( + row=0, column=1, sticky=tk.W+tk.E, pady=2) + (ttk.Label(pageSettings, text='Logoh:')).grid( + row=1, column=0, sticky=tk.E+tk.N, pady=2) + (TraceableText(pageSettings, width=45, height=10, + variable=self.pageLogoh)).grid( + row=1, column=1, + sticky=tk.W+tk.N+tk.E+tk.S, pady=2) + + (ttk.Label(pageSettings, text='Odświeżaj:')).grid( + row=2, column=0, sticky=tk.E, pady=2) + refreshPanel = tk.Frame(pageSettings) + refreshPanel.grid(row=2, column=1, sticky=tk.W+tk.E, pady=2) + (ttk.Checkbutton( + refreshPanel, + command=self._updateRefreshFields, variable=self.refresh)).grid( + row=0, column=0) + (ttk.Label(refreshPanel, text='co:')).grid(row=0, column=1) + self.intervalField = NumericSpinbox( + refreshPanel, from_=30, to=3600, width=5, justify=tk.RIGHT, + textvariable=self.refreshInterval) + self.intervalField.grid(row=0, column=2) + (ttk.Label(refreshPanel, text='sekund')).grid(row=0, column=3) + + container.columnconfigure(1, weight=1) + container.rowconfigure(4, weight=1) + + def getConfig(self): + return OrderedDict({ + 'output': self.outputPath.get(), + 'page': OrderedDict({ + 'title': self.pageTitle.get(), + 'logoh': self.pageLogoh.get(), + 'refresh': self.refreshInterval.get() \ + if self.refresh.get() > 0 else 0 + }) + }) + +class TeamsTab(PlayoffTab): + @property + def title(self): + return 'Uczestnicy' + + def renderContent(self, container): + leftFrame = tk.Frame(container) + leftFrame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + self.settingsFrame = TeamSettingsFrame( + leftFrame, vertical=True, padx=5, pady=5) + self.settingsFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + (ttk.Separator( + leftFrame, orient=tk.HORIZONTAL)).pack( + side=tk.TOP, fill=tk.X) + + self.aliasFrame = TeamAliasFrame( + leftFrame, vertical=True, padx=5, pady=5) + self.aliasFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + self.previewFrame = TeamPreviewFrame( + container, vertical=True, padx=5, pady=5) + self.previewFrame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) + + self._teamList = [] + self._teamListFetcher = None + + self.winfo_toplevel().bind( + '<<TeamSettingsChanged>>', self.onTeamSettingsChange, add='+') + + def onTeamSettingsChange(self, event): + if self._teamListFetcher is not None: + self.after_cancel(self._teamListFetcher) + self._teamListFetcher = self.after(500, self._fetchTeamList) + + def _fetchTeamList(self): + config = self.collectConfig() + dbConfig = self.winfo_toplevel().getDbConfig() + if dbConfig is not None: + config['database'] = dbConfig + data = PlayoffData() + db = None + try: + db = PlayoffDB(dbConfig) + except Exception: + pass + self._teamList = data.fetch_team_list(config['teams'], db) + self.winfo_toplevel().event_generate( + '<<TeamListChanged>>', when='tail') + + def getTeams(self): + return self._teamList + + def collectConfig(self): + config = OrderedDict({ + 'teams': self.settingsFrame.getConfig(), + 'team_aliases': self.aliasFrame.getConfig() + }) + tieConfig = self.previewFrame.getTieConfig() + if tieConfig is not None and isinstance(config['teams'], dict): + config['teams']['ties'] = tieConfig + return config + + def setValues(self, config): + self.settingsFrame.setValues( + config['teams'] if 'teams' in config else []) + self.aliasFrame.setValues( + config['team_aliases'] if 'team_aliases' in config else {}) + self.previewFrame.setTieConfig( + config['teams']['ties'] + if 'teams' in config and 'ties' in config['teams'] else []) + + def getConfig(self): + return self.collectConfig() + +class MatchesTab(PlayoffTab): + @property + def title(self): + return 'Mecze' + + def addPhase(self): + phase = MatchPhaseFrame( + self.phaseFrame, vertical=True, padx=10, pady=10) + newPhase = max(self.phases.keys()) + 1 if len(self.phases) else 1 + self.phaseFrame.add(phase, text='Faza #%d' % (newPhase)) + self.phases[newPhase] = phase + self.winfo_toplevel().event_generate( + '<<MatchListChanged>>', when='tail') + return newPhase + + def removePhase(self, phase=None): + selected = self.phaseFrame.select() if phase is None \ + else self.phases[phase] + if selected: + self.phaseFrame.forget(selected) + key_to_delete = None + for key, tab in self.phases.iteritems(): + if str(selected) == str(tab): + key_to_delete = key + break + if key_to_delete: + self.phases.pop(key_to_delete) + self.winfo_toplevel().event_generate( + '<<MatchListChanged>>', when='tail') + + def _renameTabs(self, *args): + for idx, tab in self.phases.iteritems(): + title = tab.name.get().strip() + self.phaseFrame.tab( + tab, text=title if len(title) else 'Faza #%d' % (idx)) + + def renderContent(self, container): + container.columnconfigure(1, weight=1) + container.rowconfigure(2, weight=1) + (ttk.Label(container, text='Fazy rozgrywek:')).grid( + row=0, column=0, columnspan=2, sticky=tk.W) + (ttk.Button( + container, text='+', command=self.addPhase, width=5)).grid( + row=1, column=0, sticky=tk.W) + (ttk.Button( + container, text='-', command=self.removePhase, width=5)).grid( + row=1, column=1, sticky=tk.W) + self.phases = {} + self.phaseFrame = ttk.Notebook(container) + self.phaseFrame.grid( + row=2, column=0, columnspan=2, sticky=tk.W+tk.E+tk.N+tk.S) + + self.winfo_toplevel().bind( + '<<PhaseRenamed>>', self._renameTabs, add='+') + + def getMatches(self): + matches = [] + for phase in self.phases.values(): + matches += [w for w in phase.matches.widgets + if isinstance(w, MatchSettingsFrame)] + return matches + + def setValues(self, config): + phases = config['phases'] if 'phases' in config else [] + for idx in self.phases.keys(): + self.removePhase(idx) + for phase in phases: + newPhase = self.addPhase() + self.phases[newPhase].setValues(phase) + for phase in self.phases.values(): + for match in phase.matches.widgets: + if isinstance(match, MatchSettingsFrame) \ + and match.getMatchID == 0: + match.matchID.set( + self.winfo_toplevel().getNewMatchID(match)) + + def getConfig(self): + return OrderedDict({ + 'phases': [phase.getConfig() for phase in self.phases.values()] + }) + +class SwissesTab(PlayoffTab): + @property + def title(self): + return 'Swissy' + + def renderContent(self, container): + self.swisses = SwissesFrame(container, vertical=True) + self.swisses.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + def setValues(self, config): + self.swisses.setValues(config['swiss'] if 'swiss' in config else []) + + def getConfig(self): + swisses = self.swisses.getValues() + if len(swisses): + return OrderedDict({ + 'swiss': swisses + }) + else: + return None + +class NetworkTab(PlayoffTab): + @property + def title(self): + return 'Sieć' + + def _onDBSettingsChange(self, event): + if self.dbFetchTimer is not None: + self.after_cancel(self.dbFetchTimer) + self.dbFetchTimer = self.after(1500, self._fetchDBList) + + def _fetchDBList(self): + self._dbList = [] + try: + db = PlayoffDB(self.getDB()) + for row in db.fetch_all( + 'information_schema', + 'SELECT TABLE_SCHEMA FROM information_schema.COLUMNS WHERE TABLE_NAME = "admin" AND COLUMN_NAME = "teamcnt" ORDER BY TABLE_SCHEMA;', {}): + self._dbList.append(row[0]) + except Exception as e: + pass + self.winfo_toplevel().event_generate('<<DBListChanged>>', when='tail') + + def getDBList(self): + return self._dbList + + def getDB(self): + return self.mysqlFrame.getConfig() + + def renderContent(self, container): + container.columnconfigure(0, weight=1) + container.columnconfigure(1, weight=1) + container.rowconfigure(1, weight=1) + + self.mysqlFrame = MySQLConfigurationFrame(container) + self.mysqlFrame.grid(row=0, column=0, sticky=tk.W+tk.E+tk.N+tk.S) + + self.goniecFrame = GoniecConfigurationFrame(container) + self.goniecFrame.grid(row=0, column=1, sticky=tk.W+tk.E+tk.N+tk.S) + + self.remoteFrame = RemoteConfigurationFrame(container, vertical=True) + self.remoteFrame.grid( + row=1, column=0, columnspan=2, sticky=tk.W+tk.E+tk.N+tk.S) + + self._dbList = [] + self.dbFetchTimer = None + self.winfo_toplevel().bind( + '<<DBSettingsChanged>>', self._onDBSettingsChange, add='+') + + def setValues(self, config): + self.mysqlFrame.setValues( + config['database'] if 'database' in config else {}) + self.goniecFrame.setValues( + config['goniec'] if 'goniec' in config else {}) + self.remoteFrame.setValues( + config['remotes'] if 'remotes' in config else []) + + def getConfig(self): + config = OrderedDict() + mysql = self.getDB() + if mysql is not None: + config['database'] = mysql + config['goniec'] = self.goniecFrame.getValues() + remotes = self.remoteFrame.getValues() + if len(remotes): + config['remotes'] = remotes + return config + +class VisualTab(PlayoffTab): + @property + def title(self): + return 'Wygląd' + + def renderContent(self, container): + container.columnconfigure(0, weight=1) + container.rowconfigure(1, weight=1) + + self.settingsFrame = VisualSettingsFrame(container) + self.settingsFrame.grid(row=0, column=0, sticky=tk.S+tk.N+tk.E+tk.W) + + self.positionFrame = BoxPositionsFrame(container, vertical=True) + self.positionFrame.grid(row=1, column=0, sticky=tk.S+tk.N+tk.E+tk.W) + + def setValues(self, config): + if 'page' in config: + self.settingsFrame.setValues(config['page']) + else: + self.settingsFrame.setValues({}) + if 'canvas' in config and 'box_positioning' in config['canvas']: + self.positionFrame.setValues(config['canvas']['box_positioning']) + else: + self.positionFrame.setValues({}) + + def getConfig(self): + config = OrderedDict({ + 'page': self.settingsFrame.getValues() + }) + boxConfig = self.positionFrame.getValues() + if boxConfig: + config['canvas'] = OrderedDict() + config['canvas']['box_positioning'] = boxConfig + return config + +class StyleTab(PlayoffTab): + @property + def title(self): + return 'Style' + + def renderContent(self, container): + self.linesFrame = LineStylesFrame(container) + self.linesFrame.pack(side=tk.TOP, anchor=tk.W) + + (ttk.Separator(container, orient=tk.HORIZONTAL)).pack( + side=tk.TOP, fill=tk.X) + + self.positionStylesFrame = PositionStylesFrame( + container, vertical=True) + self.positionStylesFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + def setValues(self, config): + if 'canvas' in config: + self.linesFrame.setValues(config['canvas']) + else: + self.linesFrame.setValues({}) + if 'position_styles' in config: + self.positionStylesFrame.setValues(config['position_styles']) + else: + self.positionStylesFrame.setValues([]) + + def getConfig(self): + return OrderedDict({ + 'canvas': self.linesFrame.getValues(), + 'position_styles': self.positionStylesFrame.getValues() + }) + +class TranslationsTab(PlayoffTab): + @property + def title(self): + return 'Tłumaczenia' + + def renderContent(self, container): + self.translationsFrame = TranslationConfigurationFrame( + container, vertical=True) + self.translationsFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + def setValues(self, config): + if 'i18n' in config: + self.translationsFrame.setTranslations(config['i18n']) + else: + self.translationsFrame.setTranslations({}) + + def getConfig(self): + return OrderedDict({ + 'i18n': self.translationsFrame.getTranslations() + }) + +__all__ = ['MainSettingsTab', 'TeamsTab', 'MatchesTab', 'SwissesTab', + 'NetworkTab', 'VisualTab', 'StyleTab', 'TranslationsTab'] diff --git a/jfr_playoff/gui/variables.py b/jfr_playoff/gui/variables.py new file mode 100644 index 0000000..ff65207 --- /dev/null +++ b/jfr_playoff/gui/variables.py @@ -0,0 +1,30 @@ +#coding=utf-8 + +import tkinter as tk + +class NotifyVar(tk.Variable): + def __init__(self, *args, **kwargs): + tk.Variable.__init__(self, *args, **kwargs) + self._prevValue = self.get() + self._root.after(0, self.trace, 'w', self._onChange) + + def _onChange(self, *args): + if self._prevValue != self.get(): + self._root.event_generate('<<ValueChanged>>', when='tail') + self._prevValue = self.get() + +class NumericVar(tk.StringVar): + def get(self, default=None): + try: + return int(str(tk.StringVar.get(self)).strip()) + except ValueError: + return default + +class NotifyStringVar(NotifyVar, tk.StringVar): + pass + +class NotifyIntVar(NotifyVar, tk.IntVar): + pass + +class NotifyNumericVar(NumericVar, NotifyVar): + pass |