From 63d97c47108f2e2ae025a987497acd9223e1327e Mon Sep 17 00:00:00 2001 From: emkael Date: Tue, 27 Oct 2015 15:05:24 +0100 Subject: * GUI documentation --- src/bidding_data_gui.py | 242 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 172 insertions(+), 70 deletions(-) diff --git a/src/bidding_data_gui.py b/src/bidding_data_gui.py index 606a3a9..ebc789c 100644 --- a/src/bidding_data_gui.py +++ b/src/bidding_data_gui.py @@ -1,91 +1,48 @@ # 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 logging as log - import os class BiddingGUI(tk.Frame): - __tour_filename = None - __bws_filename = None - - def __init__(self, master=None): - tk.Frame.__init__(self, master) - self.__tour_filename = tk.StringVar(master=self) - self.__bws_filename = tk.StringVar(master=self) - self.master.title('JBBD - JFR/BWS bidding data') - self.set_icon(self.master) - self.create_widgets() - self.configure_grid_cells([1], [3]) - self.pack(expand=1, fill=tk.BOTH) - - def configure_grid_cells(self, columns, rows): - for column in columns: - self.columnconfigure(column, weight=1) - for row in rows: - self.rowconfigure(row, weight=1) - - def set_icon(self, master): - # pylint: disable=protected-access - img = tk.PhotoImage(data=self.__icon_data, master=master) - master.tk.call('wm', 'iconphoto', master._w, img) - - def tour_select(self): - self.__tour_filename.set(tkFileDialog.askopenfilename( - title='Wybierz główny plik wyników turnieju', - filetypes=[('HTML files', '.htm*'), ('all files', '.*')])) - - def bws_select(self): - self.__bws_filename.set(tkFileDialog.askopenfilename( - title='Wybierz plik z danymi licytacji', - filetypes=[('BWS files', '.bws'), ('all files', '.*')])) - - class GUILogHandler(log.Handler): - def __init__(self, text): - log.Handler.__init__(self) - self.text = text - - def emit(self, record): - msg = self.format(record) - - def append(): - self.text.insert(tk.END, msg + '\n') - self.text.yview(tk.END) - self.text.after(0, append) + """GUI frame class.""" - def handle(self, record): - log.Handler.handle(self, record) - if record.levelname == 'WARNING': - self.__warning_count += 1 - if record.levelname == 'ERROR': - self.__error_count += 1 - - __warning_count = 0 - __error_count = 0 - - def warnings(self): - return self.__warning_count - - def errors(self): - return self.__error_count - - def reset_counts(self): - self.__warning_count = 0 - self.__error_count = 0 + # 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. + """ try: - self.log_field.delete(1.0, tk.END) + # 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') - self.__gui_logger.reset_counts() + + # do the magic from bidding_data import JFRBidding parser = JFRBidding( bws_file=self.__bws_filename.get(), @@ -93,6 +50,8 @@ class BiddingGUI(tk.Frame): 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(): tkMessageBox.showerror( 'Błąd!', @@ -106,62 +65,204 @@ class BiddingGUI(tk.Frame): 'w liczbie: %d\n' + 'Sprawdź dziennik logów') % self.__gui_logger.warnings()) 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) tkMessageBox.showerror('Błąd!', ex) raise - def create_widgets(self): + 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', '.*')])) + + 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() + + 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 __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 run_btn = tk.Button( self, text='No to sru!', height=3, command=self.run_bidding_data) + # 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 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) + + def append(): + """Append message to the Text widget, at the end.""" + self.text.insert(tk.END, msg + '\n') + # scroll to the bottom, afterwards + self.text.yview(tk.END) + + # call log output asynchronously + self.text.after(0, append) + + 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.delete(1.0, tk.END) + + # disable default logging limits/thresholds log.basicConfig( level=log.NOTSET, streamhandler=log.NullHandler) - self.__gui_logger = self.GUILogHandler(self.log_field) + # 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 @@ -186,6 +287,7 @@ PKMWxAQccTOBAB1BWyYEURCLYmBMmMLVggl4WVEUkjZRp5plopmlmJFUcRQFwcB5GwT8BAQA7""" def main(): + """Entry point for application - spawn main window.""" app = BiddingGUI() app.mainloop() -- cgit v1.2.3