diff options
Diffstat (limited to 'jfr_playoff/gui/frames')
-rw-r--r-- | jfr_playoff/gui/frames/__init__.py | 508 | ||||
-rw-r--r-- | jfr_playoff/gui/frames/match.py | 856 | ||||
-rw-r--r-- | jfr_playoff/gui/frames/network.py | 194 | ||||
-rw-r--r-- | jfr_playoff/gui/frames/team.py | 545 | ||||
-rw-r--r-- | jfr_playoff/gui/frames/translations.py | 49 | ||||
-rw-r--r-- | jfr_playoff/gui/frames/visual.py | 452 |
6 files changed, 2604 insertions, 0 deletions
diff --git a/jfr_playoff/gui/frames/__init__.py b/jfr_playoff/gui/frames/__init__.py new file mode 100644 index 0000000..052e832 --- /dev/null +++ b/jfr_playoff/gui/frames/__init__.py @@ -0,0 +1,508 @@ +#coding=utf-8 + +from functools import partial +import types + +import tkinter as tk +from tkinter import ttk +import tkMessageBox + +from ..variables import NotifyStringVar, NotifyIntVar +from ..variables import NotifyBoolVar, NotifyNumericVar, NumericVar + +def setPanelState(frame, state): + for child in frame.winfo_children(): + if isinstance(child, tk.Frame): + setPanelState(child, state) + else: + child.configure(state=state) + +class WidgetRepeater(tk.Frame): + def __init__(self, master, widgetClass, headers=None, classParams=None, + onAdd=None, *args, **kwargs): + widgetList = widgetClass + if not isinstance(widgetClass, list): + widgetList = [widgetClass] + for widget in widgetList: + if not issubclass(widget, RepeatableFrame): + raise AttributeError( + 'WidgetRepeater widget must be a RepeatableFrame') + tk.Frame.__init__(self, master, **kwargs) + self.widgetClass = widgetClass + self.widgetClassParams = classParams + self.widgets = [] + self.headers = headers + self.headerFrame = None + self.addButton = ttk.Button( + self, text='[+]', width=5, command=self._addWidget) + self.onAdd = onAdd + self.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() + if self.onAdd is not None: + self.onAdd(widget) + + 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] = NotifyBoolVar() + 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..2cf88bf --- /dev/null +++ b/jfr_playoff/gui/frames/match.py @@ -0,0 +1,856 @@ +#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 +from ..variables import NotifyNumericVar, NotifyBoolVar + +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 = NotifyBoolVar() + self.setTo = NotifyNumericVar() + self.fetchFromEnabled = NotifyBoolVar() + 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): + matches = self.winfo_toplevel().getMatches() + values = [] + prev_match = None + for match in matches: + if prev_match is not None: + if prev_match.getPhase() != match.getPhase(): + values.append(None) + values.append(match) + prev_match = match + return values + + +class MatchSelectionFrame(SelectionFrame): + def renderOption(self, container, option, idx): + if option is not None: + (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) + else: + (ttk.Separator( + container, orient=tk.HORIZONTAL)).grid( + row=idx+1, column=0, columnspan=2, + sticky=tk.E+tk.W, pady=2) + + def _mapValue(self, idx, value): + if self.options[idx] is not None: + 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 = NotifyBoolVar() + 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._enablePanels() + + 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() + if match is not None] + 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 = NotifyBoolVar() + 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._enablePanels() + + 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 [faza: %s]' % ( + self.getMatchID(), + phase.master.tab(phase)['text'].strip() + 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], + onAdd=self._matchAdded) + self.matches.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + self.link.trace('w', self._updateLinks) + self.name.trace('w', self._signalPhaseRename) + + def _matchAdded(self, widget): + self.after(100, self.canvas.yview_moveto, 1.0) + + 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..fb27434 --- /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, NotifyNumericVar, NotifyBoolVar + +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 = NotifyBoolVar() + 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..b1d9841 --- /dev/null +++ b/jfr_playoff/gui/frames/team.py @@ -0,0 +1,545 @@ +#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.orderValues = [] + self.orderFields = [] + 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 + self.winfo_toplevel().bind( + '<<OrderConfigChanged>>', self._collectOrderConfig, add='+') + self._orderConfig = [] + self._lockOrderValues = 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)] + self.orderValues = self.orderValues[0:len(teams)] + for idx in range(len(teams), len(self.orderFields)): + self.orderFields[idx].destroy() + self.orderFields = self.orderFields[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) + if idx >= len (self.orderFields): + self.orderValues.append(NotifyNumericVar()) + self.orderValues[idx].trace('w', self._orderValueChangeNotify) + self.orderFields.append( + NumericSpinbox( + container, from_=0, to=9999, + width=5, font=Font(size=10), + textvariable=self.orderValues[idx])) + self.orderFields[idx].grid( + row=idx+2, column=2, 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) + self.labels.append(ttk.Label( + container, + text='Ręczne rozstrzyganie kolejności w klasyfikacji końcowej', + anchor=tk.E)) + self.labels[-1].grid(row=len(teams)+4, column=0, columnspan=2, sticky=tk.N+tk.E) + self.labels.append(ttk.Label(container, text='⬏', font=Font(size=20))) + self.labels[-1].grid( + row=len(teams)+4, column=2, 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 _getTeams(self): + return self.winfo_toplevel().getTeams() + + 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 _collectTieConfig(self, *args): + if not self._lockTieValues: + self._tieConfig = self.getTieConfig() + + def getOrderConfig(self): + teams = self._getTeams() + order = [(teams[idx], val.get(default=0)) + for idx, val in enumerate(self.orderValues)] + return [team[0][0] for team + in sorted(order, key=lambda t: t[1]) + if team[1] > 0] + + def setOrderConfig(self, values): + self._orderConfig = values + self.refreshTeams(None) + + def _orderValueChangeNotify(self, *args): + if not self._lockOrderValues: + self.winfo_toplevel().event_generate( + '<<OrderConfigChanged>>', when='tail') + + def _collectOrderConfig(self, *args): + if not self._lockOrderValues: + self._orderConfig = self.getOrderConfig() + + def refreshTeams(self, event): + self._lockTieValues = True + self._lockOrderValues = 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 + for idx, order in enumerate(self._orderConfig): + if isinstance(order, int): + if tidx+1 == order: + self.orderValues[tidx].set(idx+1) + break + else: + if team[0] == order: + self.orderValues[tidx].set(idx+1) + break + self._lockOrderValues = False + 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..1cbe177 --- /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, NotifyNumericVar, NotifyBoolVar + +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 = NotifyBoolVar() + self.finishingPositionIndicators = NotifyBoolVar() + self.boxWidth = NotifyNumericVar() + self.boxHeight = NotifyNumericVar() + self.boxMargin = NotifyNumericVar() + self.shortenTeamNames = NotifyBoolVar() + self.teamNameLength = NotifyNumericVar() + self.teamNameEllipsis = NotifyStringVar() + self.teamNamePredict = NotifyBoolVar() + self.teamNamePlaceholder = NotifyStringVar() + self.teamNameSortPredictions = NotifyBoolVar() + 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'] |