summaryrefslogtreecommitdiff
path: root/jfr_playoff/gui
diff options
context:
space:
mode:
Diffstat (limited to 'jfr_playoff/gui')
-rw-r--r--jfr_playoff/gui/__init__.py313
-rw-r--r--jfr_playoff/gui/frames/__init__.py508
-rw-r--r--jfr_playoff/gui/frames/match.py856
-rw-r--r--jfr_playoff/gui/frames/network.py194
-rw-r--r--jfr_playoff/gui/frames/team.py545
-rw-r--r--jfr_playoff/gui/frames/translations.py49
-rw-r--r--jfr_playoff/gui/frames/visual.py452
-rw-r--r--jfr_playoff/gui/icons.py31
-rw-r--r--jfr_playoff/gui/icons/log.gifbin0 -> 1014 bytes
-rw-r--r--jfr_playoff/gui/icons/new.gifbin0 -> 581 bytes
-rw-r--r--jfr_playoff/gui/icons/open.gifbin0 -> 1015 bytes
-rw-r--r--jfr_playoff/gui/icons/playoff.icobin0 -> 1167 bytes
-rw-r--r--jfr_playoff/gui/icons/run-once.gifbin0 -> 189 bytes
-rw-r--r--jfr_playoff/gui/icons/run-timed.gifbin0 -> 567 bytes
-rw-r--r--jfr_playoff/gui/icons/save.gifbin0 -> 621 bytes
-rw-r--r--jfr_playoff/gui/icons/saveas.gifbin0 -> 603 bytes
-rw-r--r--jfr_playoff/gui/icons/stop-timed.gifbin0 -> 197 bytes
-rw-r--r--jfr_playoff/gui/logframe.py104
-rw-r--r--jfr_playoff/gui/tabs.py506
-rw-r--r--jfr_playoff/gui/variables.py37
20 files changed, 3595 insertions, 0 deletions
diff --git a/jfr_playoff/gui/__init__.py b/jfr_playoff/gui/__init__.py
new file mode 100644
index 0000000..5d9e40f
--- /dev/null
+++ b/jfr_playoff/gui/__init__.py
@@ -0,0 +1,313 @@
+#coding=utf-8
+
+import codecs, copy, json, os, shutil, 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._bindKeyboardShortcuts()
+ 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 _bindKeyboardShortcuts(self):
+ self.bind('<Control-n>', self.onNewFile)
+ self.bind('<Control-s>', self.onSave)
+ self.bind('<Control-S>', self.onSaveAs)
+ self.bind('<Control-o>', self.onFileOpen)
+ self.bind('<Control-q>', self.onClose)
+ self.bind('<F9>', self.onRunOnce)
+
+ def onNewFile(self, *args):
+ self._checkSave()
+ self.newFile()
+
+ def onFileOpen(self, *args):
+ 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, *args):
+ if self._filepath is not None:
+ self.saveFile(self._filepath)
+ else:
+ self.onSaveAs()
+
+ def onSaveAs(self, *args):
+ 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:
+ self._tempPath = None
+ if not len(config.get('output', '')):
+ tempDir = tempfile.mkdtemp(prefix='jfrplayoff-')
+ self._tempPath = os.path.join(
+ tempDir, next(tempfile._get_candidate_names()))
+ config['output'] = self._tempPath + '.html'
+ settings = PlayoffSettings(config_obj=config)
+ generator = PlayoffGenerator(settings)
+ content = generator.generate_content()
+ file_manager = PlayoffFileManager(settings)
+ self._outputPath = file_manager.write_content(content)
+ file_manager.copy_scripts()
+ file_manager.copy_styles()
+ file_manager.send_files()
+ self.event_generate('<<BracketGenerated>>', when='tail')
+ 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(os.path.realpath(self._outputPath))
+ else:
+ if self._tempPath is not None:
+ shutil.rmtree(os.path.dirname(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..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': '&nbsp;&nbsp;',
+ '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']
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
new file mode 100644
index 0000000..7915311
--- /dev/null
+++ b/jfr_playoff/gui/icons/log.gif
Binary files differ
diff --git a/jfr_playoff/gui/icons/new.gif b/jfr_playoff/gui/icons/new.gif
new file mode 100644
index 0000000..e816390
--- /dev/null
+++ b/jfr_playoff/gui/icons/new.gif
Binary files differ
diff --git a/jfr_playoff/gui/icons/open.gif b/jfr_playoff/gui/icons/open.gif
new file mode 100644
index 0000000..c7adaf2
--- /dev/null
+++ b/jfr_playoff/gui/icons/open.gif
Binary files differ
diff --git a/jfr_playoff/gui/icons/playoff.ico b/jfr_playoff/gui/icons/playoff.ico
new file mode 100644
index 0000000..0cdb123
--- /dev/null
+++ b/jfr_playoff/gui/icons/playoff.ico
Binary files differ
diff --git a/jfr_playoff/gui/icons/run-once.gif b/jfr_playoff/gui/icons/run-once.gif
new file mode 100644
index 0000000..842afd8
--- /dev/null
+++ b/jfr_playoff/gui/icons/run-once.gif
Binary files differ
diff --git a/jfr_playoff/gui/icons/run-timed.gif b/jfr_playoff/gui/icons/run-timed.gif
new file mode 100644
index 0000000..1b8340e
--- /dev/null
+++ b/jfr_playoff/gui/icons/run-timed.gif
Binary files differ
diff --git a/jfr_playoff/gui/icons/save.gif b/jfr_playoff/gui/icons/save.gif
new file mode 100644
index 0000000..4452f38
--- /dev/null
+++ b/jfr_playoff/gui/icons/save.gif
Binary files differ
diff --git a/jfr_playoff/gui/icons/saveas.gif b/jfr_playoff/gui/icons/saveas.gif
new file mode 100644
index 0000000..25cde78
--- /dev/null
+++ b/jfr_playoff/gui/icons/saveas.gif
Binary files differ
diff --git a/jfr_playoff/gui/icons/stop-timed.gif b/jfr_playoff/gui/icons/stop-timed.gif
new file mode 100644
index 0000000..b5fc3fd
--- /dev/null
+++ b/jfr_playoff/gui/icons/stop-timed.gif
Binary files differ
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..2a8c889
--- /dev/null
+++ b/jfr_playoff/gui/tabs.py
@@ -0,0 +1,506 @@
+#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, NotifyNumericVar, NotifyBoolVar
+
+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 = NotifyBoolVar()
+ 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
+ orderConfig = self.previewFrame.getOrderConfig()
+ if orderConfig:
+ config['custom_final_order'] = orderConfig
+ 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 [])
+ self.previewFrame.setOrderConfig(
+ config.get('custom_final_order', []))
+
+ 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)
+ self.phases[newPhase] = phase
+ self._renameTabs()
+ self.phaseFrame.select(phase)
+ self.winfo_toplevel().event_generate(
+ '<<MatchListChanged>>', when='tail')
+ self.winfo_toplevel().event_generate(
+ '<<ValueChanged>>', 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 '') + ' (#%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..7e6989c
--- /dev/null
+++ b/jfr_playoff/gui/variables.py
@@ -0,0 +1,37 @@
+#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 NotifyBoolVar(NotifyVar, tk.StringVar):
+ def get(self, *args, **kwargs):
+ value = tk.StringVar.get(self, *args, **kwargs)
+ return int(value == '1')
+
+class NotifyNumericVar(NumericVar, NotifyVar):
+ def __init__(self, *args, **kwargs):
+ NotifyVar.__init__(self, *args, **kwargs)
+ NumericVar.__init__(self, *args, **kwargs)