summaryrefslogtreecommitdiff
path: root/src/bidding_data_gui.py
blob: da7531ad9cdb84f110416dfc106b90df5b433985 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# encoding=utf-8

"""
Bidding data for JFR Pary result pages - GUI.

Graphical user interface to insert HTML tables with bidding data into traveller
files generated by JFR Pary.
"""

import Tkinter as tk
import tkFileDialog
import tkMessageBox
import Queue

import logging as log
import os
import threading


class BiddingGUI(tk.Frame):
    """GUI frame class."""

    # Tk variable to store tournament result file path
    __tour_filename = None
    # Tk variable to store BWS file path
    __bws_filename = None

    def run_bidding_data(self):
        """
        Run the parser and do all the actual work.

        "On-click" event for analysis start button.
        Sanitizes input parameters, handles warning/error messages,
        imports main module (from CLI script) and runs it.
        """
        self.queue(self.run_btn.__setitem__, 'state', tk.DISABLED)
        try:
            # reset error/warning count and log output field
            self.__gui_logger.reset_counts()

            # check for input parameter paths
            if not os.path.exists(self.__bws_filename.get()):
                raise Exception('BWS file not found')
            if not os.path.exists(self.__tour_filename.get()):
                raise Exception('Tournament results file not found')

            # do the magic
            from bidding_data import JFRBidding
            parser = JFRBidding(
                bws_file=self.__bws_filename.get(),
                file_prefix=self.__tour_filename.get())
            parser.write_bidding_tables()
            parser.write_bidding_scripts()
            parser.write_bidding_links()

            # inform of any warnings/errors that might have occuerd
            if self.__gui_logger.errors():
                self.queue(tkMessageBox.showerror,
                           'Błąd!',
                           ('Podczas wykonywania programu wystąpiły błędy ' +
                            'w liczbie: %d\n' +
                            'Sprawdź dziennik logów')
                           % self.__gui_logger.errors())
            elif self.__gui_logger.warnings():
                self.queue(tkMessageBox.showwarning,
                           'Uwaga!',
                           ('Podczas wykonywania programu wystąpiły ' +
                            'ostrzeżenia w liczbie: %d\n' +
                            'Sprawdź dziennik logów')
                           % self.__gui_logger.warnings())
            else:
                self.queue(
                    tkMessageBox.showinfo, 'Cudownie!', 'Wszystko wporzo.')
        except Exception as ex:
            # JFRBidding errors are logged
            # (and notified of after entire execution),
            # other exceptions should halt execution and display error message
            log.getLogger('root').error(ex)
            self.queue(tkMessageBox.showerror, 'Błąd!', ex)
            raise
        finally:
            self.queue(self.run_btn.__setitem__, 'state', tk.NORMAL)

    def tour_select(self):
        """
        Allow for tournament file selection.

        "On-click" event for tournament select button.
        Displays file selection dialog for tournament file and stores user's
        choice in Tk variable.
        """
        self.__tour_filename.set(tkFileDialog.askopenfilename(
            title='Wybierz główny plik wyników turnieju',
            filetypes=[('HTML files', '.htm*'), ('all files', '.*')]))

    def bws_select(self):
        """
        Allow for BWS file selection.

        "On-click" event for BWS select button.
        Displays file selection dialog for tournament file and stores user's
        choice in Tk variable.
        """
        self.__bws_filename.set(tkFileDialog.askopenfilename(
            title='Wybierz plik z danymi licytacji',
            filetypes=[('BWS files', '.bws'), ('all files', '.*')]))

    __queue = None

    def queue(self, callback, *args, **kwargs):
        """Add message (function call) to GUI interaction queue."""
        if self.__queue is None:
            self.__queue = Queue.Queue()
        self.__queue.put((callback, args, kwargs))

    def process_queue(self):
        """Process GUI interaction queue from other threads."""
        if self.__queue is None:
            self.__queue = Queue.Queue()
        try:
            callback, args, kwargs = self.__queue.get_nowait()
        except Queue.Empty:
            self.master.after(100, self.process_queue)
        else:
            callback(*args, **kwargs)
            self.master.after(10, self.process_queue)

    def __init__(self, master=None):
        """
        Construct the frame.

        Initializes window appearence, controls, layout and logging facility.
        """
        tk.Frame.__init__(self, master)
        # bind Tk variables to input parameter paths
        self.__tour_filename = tk.StringVar(master=self)
        self.__bws_filename = tk.StringVar(master=self)
        # set window title and icon
        self.master.title('JBBD - JFR/BWS bidding data')
        self.__set_icon(self.__icon_data)
        # create controls
        self.__create_widgets()
        # and align them within a layout
        # second column and fourth row should expand
        self.__configure_grid_cells([1], [3])
        # main frame should fill entire application window
        self.pack(expand=1, fill=tk.BOTH)
        # finally, set logging up
        self.__configure_logging()
        # fire up interthread queue
        self.after(100, self.process_queue)

    def __configure_grid_cells(self, columns, rows):
        """Set expand with window resize for cells of layout grid."""
        for column in columns:
            self.columnconfigure(column, weight=1)
        for row in rows:
            self.rowconfigure(row, weight=1)

    def __set_icon(self, data):
        """Set app icon from base64-encoded data."""
        # pylint: disable=protected-access
        img = tk.PhotoImage(data=data, master=self.master)
        # protected access is necessary since Tkinter does not expose
        # the wm.iconphoto call
        self.master.tk.call('wm', 'iconphoto', self.master._w, img)

    def __dispatch_run_button_action(self):
        """Dispatch main button action asynchronously."""
        run_thread = threading.Thread(target=self.run_bidding_data)
        run_thread.start()

    def __create_widgets(self):
        """
        Create main application window controls and align them on grid.

        Grid has 6 columns (so that 1/2, 1/3 or 1-4-1 layouts are configurable.
        """
        # label for tournament file selection
        tour_label = tk.Label(
            self, text='Plik turnieju:')
        # text field for tournament file path
        tour_entry = tk.Entry(
            self, state=tk.DISABLED, textvariable=self.__tour_filename)
        # tournament selection button
        tour_select_btn = tk.Button(
            self, text='Szukaj', command=self.tour_select)
        # first row, label aligned to the right, text field expands
        tour_label.grid(row=0, column=0, sticky=tk.E)
        tour_entry.grid(row=0, column=1, columnspan=4, sticky=tk.E+tk.W)
        tour_select_btn.grid(row=0, column=5)

        # label for BWS file selection
        bws_label = tk.Label(
            self, text='BWS:')
        # text field for BWS file path
        bws_entry = tk.Entry(
            self, state=tk.DISABLED, textvariable=self.__bws_filename)
        # BWS selection button
        bws_select_btn = tk.Button(
            self, text='Szukaj', command=self.bws_select)
        # second row, label aligned to the right, text field expands
        bws_label.grid(row=1, column=0, sticky=tk.E)
        bws_entry.grid(row=1, column=1, columnspan=4, sticky=tk.E+tk.W)
        bws_select_btn.grid(row=1, column=5)

        # main command button
        self.run_btn = tk.Button(
            self, text='No to sru!', height=3,
            command=self.__dispatch_run_button_action)
        # application exit button
        quit_btn = tk.Button(
            self, text='Koniec tego dobrego', command=self.quit)
        # third row, leftmost 2/3 of window width, fills entire cell
        self.run_btn.grid(
            row=2, column=0, columnspan=4, sticky=tk.N+tk.S+tk.E+tk.W)
        # third row, rightmost 1/3 of window width
        quit_btn.grid(row=2, column=4, columnspan=2)

        # vertical scrollbar for log output field
        log_scroll_y = tk.Scrollbar(self, orient=tk.VERTICAL)
        # horizontal scrollbar for log output field
        log_scroll_x = tk.Scrollbar(self, orient=tk.HORIZONTAL)
        # log field, bound (both ways) to scrollbars
        self.log_field = tk.Text(
            self, height=5, width=80, wrap=tk.NONE,
            xscrollcommand=log_scroll_x.set,
            yscrollcommand=log_scroll_y.set)
        log_scroll_x['command'] = self.log_field.xview
        log_scroll_y['command'] = self.log_field.yview
        # fourth row, entries window width, expands with window
        self.log_field.grid(
            row=3, column=0, columnspan=6, sticky=tk.N+tk.S+tk.E+tk.W)
        # scrollbars to the right and to the bottom of the field
        log_scroll_y.grid(row=3, column=6, sticky=tk.N+tk.S)
        log_scroll_x.grid(row=4, column=0, columnspan=6, sticky=tk.E+tk.W)

    def __configure_logging(self):
        """Set up logging facility, bound to log output field."""
        class GUILogHandler(log.Handler):
            """Log handler which allows output to Tk Text widget."""

            def __init__(self, text):
                """Construct the handler, provided TExt widget to bind to."""
                log.Handler.__init__(self)
                self.text = text

            def emit(self, record):
                """Output the message."""
                msg = self.format(record)
                # Append message to the Text widget, at the end."""
                self.text.master.queue(self.text.insert, tk.END, msg + '\n')
                # scroll to the bottom, afterwards
                self.text.master.queue(self.text.yview, tk.END)

            def handle(self, record):
                """Handle log message record (count errors/warnings)."""
                log.Handler.handle(self, record)
                if record.levelname == 'WARNING':
                    self.__warning_count += 1
                if record.levelname == 'ERROR':
                    self.__error_count += 1

            # message stats, for summary purposes
            __warning_count = 0
            __error_count = 0

            def warnings(self):
                """Return number of accumulated warnings."""
                return self.__warning_count

            def errors(self):
                """Return number of accumulated errors."""
                return self.__error_count

            def reset_counts(self):
                """Reset stats and log output."""
                self.__warning_count = 0
                self.__error_count = 0
                self.text.master.queue(self.text.delete, 1.0, tk.END)

        # disable default logging limits/thresholds
        log.basicConfig(
            level=log.NOTSET,
            streamhandler=log.NullHandler)
        # set up GUI logging
        self.__gui_logger = GUILogHandler(self.log_field)
        self.__gui_logger.setLevel(log.INFO)
        self.__gui_logger.setFormatter(log.Formatter(
            '%(levelname)-8s %(name)-8s %(message)s'))
        # register GUI handler
        log.getLogger().addHandler(self.__gui_logger)
        # remove default (console) handler
        log.getLogger().removeHandler(log.getLogger().handlers[0])

    # embedded image data for app icon
    __icon_data = """R0lGODlhIAAgAOeRAAAAAAQEBAUFBQYGBggICAAAcQAAcgAAcwAAdAAAdQA
AdgAAdwAAeAAAeQEBegsLEQQEewUFew4ODgYGfA8PDwgIfQoKfgoKfxMTEwsLfxQUIQ0NgBERghISghQ
UgxQUhBUVhRYWhhkZhhkZhxoajxsbhx4eiSgoKCIijCQkjC0tRzAwMCoqkSsrkSwskCwskjExkzU1lzg
4ljg4mUFBQTs7mjw8mUJChEhISEVFn0hIolBQVUpKo01NoVBQgU9Pok9PplVVqlhYp1pakllZp1lZq1t
bklpaqF1dlFxcqV5eq19fqmBgsGFhsGJirGJisWNjsWVlrWVlrmZmrmhotG5ubmlpvG1tsHFxuXR0tXR
0u3V1u3V1vHV1vXZ2vHd3vXh4vXt7xnt7x3x8x3x8yH19wH5+u4GBvIKCyoaGv4eHwJSUxpWVx5ubzp6
e0p+fzKWl26io0aur0q+v1Lq62ry83L+/3cDA3sHB3sbG4MbG4cnJycjI4svL49TU6NfX6tjY69nZ69v
b7ODg7ubm8urq9O3t9e7u9u/v9vPz+Pf3+/n5/Pr6/Pz8/Pz8/f39/v7+/v/////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
//////////////////////yH5BAEKAP8ALAAAAAAgACAAQAj+AP9JAECwYMEAGP4pXMiwIUOCaMo0qeE
FCpMcQZAAOOGwY0OCkUJGcsQmCyGRKEMSWYCgT8qQBlH6gVBhykuRSTzchFlQDJgXMbQILaLFCEGPSP+
B3MmU6Z6YfBJAmBOFQYOrCXokQplTZKAINiDxXNq0bEqDKrqQWcv2ioajSTsStPLhyYwvLKxaMAogrlw
AYoQKFhzmgcHDiAsqBWC2sciYd+pESnG1wYFBN7uuQZDmbMFIetREklGZwRKRbwogOLDAMoLXCFoLiRT
TcWOCBBLr3o2YwEDeff06JMhFy5cvgrdo8QFX+EKCY3SAmGGcRQYsQ5o7X0xiRIgLVBS1MLiQ4Ebw7Qo
JBobCQ8uTFiiAaASwAv1iOG3y69fvZgeO/wAGGCANAgAwAFm2NVWbbYQAUshOMdmBSCRCXGUAHjslIYI
ZBsjxElQuRHJGa1cp8MdLXUVCxwJKoAQVAnkcQmIDDMgQ0iOMMALJETrluIghG5SgCG2f1SFIJDBcxcA
PKMWxAQccTOBAB1BWyYEURCLYmBMmMLVggl4WVEUkjZRp5plopmlmJFUcRQFwcB5GwT8BAQA7"""


def main():
    """Entry point for application - spawn main window."""
    app = BiddingGUI()
    app.mainloop()

if __name__ == '__main__':
    main()