diff --git a/lib/gui/about.py b/lib/gui/about.py index 5644966..9970d30 100755 --- a/lib/gui/about.py +++ b/lib/gui/about.py @@ -5,6 +5,9 @@ -Christopher Welborn 01-05-2019 """ +import random +import string + from platform import platform from .common import ( @@ -122,8 +125,9 @@ def __init__( self.lbl_img.image = self.img_icon self.lbl_img.grid(row=0, column=0, sticky=tk.NSEW) self.lbl_img.bind('', self.cmd_lbl_img_click) - # See self.cmd_lbl_img_click - self.img_dimmed = False + # See cmd_lbl_img_click().. + self.lbl_clicks = 0 + self.appending_garbage = False # ..Main information panel. self.frm_tt = ttk.Frame( @@ -170,88 +174,107 @@ def __init__( self.scroll_info = ttk.Scrollbar(self.frm_info) self.scroll_info.grid(row=0, column=1, sticky=tk.NSEW) # ..Sys Info Text - max_info_lines = 11 - max_info_cols = len(PLATFORM) + 5 + sysinfo = self.get_info() + sysinfolines = sysinfo.split('\n') + self.max_info_lines = len(sysinfolines) + self.max_info_cols = len(PLATFORM) + 5 self.text_info = tk.Text( self.frm_info, - width=max_info_cols, - height=max_info_lines, + width=self.max_info_cols, + height=self.max_info_lines, yscrollcommand=self.scroll_info.set, bg='#ffffff', fg='#000000', ) + self.text_info.tag_configure('error', foreground='#FF0000') self.scroll_info.configure(command=self.text_info.yview) self.text_info.grid(row=0, column=0, sticky=tk.NSEW) # Insert all information into the Text widget. - self.build_info() + self.append_info(sysinfo) # Make text read-only. self.text_info.configure(state=tk.DISABLED) - def append_info(self, text): - """ Append lines of information into the text_info Text() widget. + def append_garbage(self, delay=0.025, callback=None): + """ Append a bunch of garbage characters to `text_info`. """ + self.appending_garbage = True + chars = ''.join((string.ascii_letters, string.punctuation)) + self.clear_info() + delay = int(delay * 1000) + msgs = [ + 'Error', + '[Deleting configuration...]', + 'ERROR', + '[Deleting projects...]', + 'Deleting license...', + 'ID10T ERROR', + '[Calling the boss...]', + 'PEBCAK FATAL ERROR', + 'You shouldn\'t mess with things you don\'t understand.', + ] + maxchars = (self.max_info_lines * self.max_info_cols) + maxjunkchars = maxchars - len(''.join(msgs)) + chunk = maxjunkchars // len(msgs) + while msgs: + for i in range(chunk): + self.append_info(random.choice(chars)) + self.after(delay) + self.update_idletasks() + for c in msgs.pop(0): + self.append_info(c) + self.after(delay) + self.update_idletasks() + if callback is None: + return + return callback() + + def append_info(self, text, tag=None): + """ Append lines of information into the `text_info` Text() widget. If a `str` is passed, the text is simply appended with no newline. """ + self.text_info.configure(state=tk.NORMAL) if not isinstance(text, str): for line in text: if not line.endswith('\n'): line = '{}\n'.format(line) self.append_info(line) return None - - self.text_info.insert(tk.END, text) + if tag: + self.text_info.insert(tk.END, text, tag) + else: + self.text_info.insert(tk.END, text) + self.text_info.configure(state=tk.DISABLED) return None - def build_info(self): - """ Insert machine/app/runtime info into the text_info Text(). """ - d = get_system_info() - avgtime = (d['runtime_secs'] or 1) / (d['runs'] or 1) - debug('Total Time: {}, Runs: {}, Average: {}'.format( - d['runtime_secs'], - d['runs'], - avgtime, - )) - self.append_info( - '\n'.join(( - ' Total Runs: {d[runs]}', - ' Total Time: {totaltime}', - ' Average Time: {avgtime}', - ' Master Files: {d[master_files]}', - ' Tiger Files: {d[tiger_files]}', - ' Archived Files: {d[archive_files]}', - 'Unarchived Files: {d[unarchive_files]}', - ' Removed Files: {d[remove_files]}', - ' Python Version: {d[python_ver]}', - ' Platform:', - ' {d[platform]}', + def change_info(self, text, tag=None): + """ Change the text_info Text() widget's text. """ + self.clear_info() + self.append_info(text, tag=None) - )).format( - d=d, - avgtime=humantime_fromsecs(avgtime) or 'n/a', - totaltime=humantime_fromsecs(d['runtime_secs']) or 'n/a', - ) - ) + def clear_info(self): + """ Clear the `text_info` Text() widget. """ + self.text_info.configure(state=tk.NORMAL) + self.text_info.delete('0.0', tk.END) + self.text_info.configure(state=tk.DISABLED) def cmd_lbl_img_click(self, event): """ Handles lbl_img click. """ - if self.img_dimmed: - self.destroy() + self.lbl_clicks += 1 + if self.lbl_clicks < 2: return - # Dim the icon image, for no good reason. - rows = [] - for y in range(64): - cols = [] - for x in range(64): - cols.append(self.img_icon.get(x, y)) - rows.append(cols) - - for y, row in enumerate(rows): - for x, col in enumerate(row): - if all(_ == 0 for _ in col): - continue - r, g, b = (max(i - 32, 0) for i in col) - hexval = '#{r:0>2x}{g:0>2x}{b:0>2x}'.format(r=r, g=g, b=b) - self.img_icon.put(hexval.join(('{', '}')), to=(x, y)) - self.img_dimmed = True + msgs = [ + 'Stop doing that.', + 'Stop.', + 'Seriously, you don\'t know what is going to happen.', + ] + msglen = len(msgs) + max_clicks = msglen + 1 + if self.lbl_clicks == max_clicks: + self.change_info('Final warning.', tag='error') + elif self.lbl_clicks > max_clicks: + if not self.appending_garbage: + self.append_garbage(callback=self.destroy) + else: + self.change_info(msgs[(self.lbl_clicks - 1) % msglen]) def destroy(self): debug('Saving gui-about config...') @@ -265,3 +288,35 @@ def destroy(self): super().destroy() debug('Calling destroy_cb({})...'.format(self.destroy_cb)) handle_cb(self.destroy_cb) + + def get_info(self): + """ Get machine/app/runtime info. """ + # TODO: Use insert(index, chars, tags) to tag labels/values and + # colorize them? + d = get_system_info() + avgtime = (d['runtime_secs'] or 1) / (d['runs'] or 1) + debug('Total Time: {}, Runs: {}, Average: {}'.format( + d['runtime_secs'], + d['runs'], + avgtime, + )) + + return '\n'.join(( + ' Total Runs: {d[runs]}', + ' Total Time: {totaltime}', + ' Average Time: {avgtime}', + ' Master Files: {d[master_files]}', + ' Tiger Files: {d[tiger_files]}', + ' Archived Files: {d[archive_files]}', + 'Unarchived Files: {d[unarchive_files]}', + ' Removed Files: {d[remove_files]}', + ' Fatal Errors: {d[fatal_errors]}', + ' Python Version: {d[python_ver]}', + ' Platform:', + ' {d[platform]}', + + )).format( + d=d, + avgtime=humantime_fromsecs(avgtime) or 'n/a', + totaltime=humantime_fromsecs(d['runtime_secs']) or 'n/a', + ) diff --git a/lib/gui/common.py b/lib/gui/common.py index 581654d..70f036a 100755 --- a/lib/gui/common.py +++ b/lib/gui/common.py @@ -15,6 +15,7 @@ from ..util.logger import ( debug, debugprinter, + debug_err, debug_exc, debug_obj, print_err, @@ -168,12 +169,11 @@ def __call__(self, *args): raise except Exception as ex: # Log the message, and show an error dialog. - print_err('GUI Error: ({})'.format(type(ex).__name__)) + print_err('GUI Error: ({})'.format( + type(ex).__name__, + )) debug_exc() - messagebox.showerror( - title='{} - Error'.format(NAME), - message=str(ex), - ) + show_error(ex) raise @@ -218,14 +218,28 @@ def debug_settings(self, level=0): """ Shortcut for self.debug_attr('settings'). """ return self.debug_attr('settings', level=level + 1) - def show_error(self, msg): - """ Use show_error, but make sure this window is out of the way. """ + def show_error(self, msg, fatal=False): + """ Use show_error, but make sure this window is out of the way. + If `fatal` is truthy, call `self.destroy()` afterwards. + """ old_topmost = self.attributes('-topmost') self.attributes('-topmost', 0) - self.lower() + if fatal: + self.withdraw() + else: + self.lower() show_error(msg) - self.lift() self.attributes('-topmost', old_topmost) + if fatal: + debug_err( + 'Closing {} for fatal error:\n{}'.format( + type(self).__name__, + msg, + ) + ) + self.after_idle(self.destroy, False) + else: + self.lift() # Use TkErrorLogger diff --git a/lib/gui/main.py b/lib/gui/main.py index 9c445b3..a95a166 100755 --- a/lib/gui/main.py +++ b/lib/gui/main.py @@ -518,6 +518,9 @@ def cmd_btn_run(self): """ Handles btn_run click. """ # Validate dirs, but allow an empty archive dir. if not self.validate_dirs(ignore_dirs=('archive', )): + if self.settings.get('auto_run', False): + # Close on errors when auto-running. + self.destroy() return self.enable_interface(False) @@ -530,17 +533,23 @@ def cmd_btn_run(self): split_parts=not self.var_no_part_split.get(), ) except OSError as ex: - self.show_error('Cannot load .dat files in: {}\n{}'.format( - mozdir, - ex, - )) + self.show_error( + 'Cannot load .dat files in: {}\n{}'.format( + mozdir, + ex, + ), + fatal=self.settings.get('auto_run', False), + ) self.enable_interface(True) return if not mozfiles: - self.show_error('No Mozaik (.dat) files found in: {}'.format( - mozdir, - )) + self.show_error( + 'No Mozaik (.dat) files found in: {}'.format( + mozdir, + ), + fatal=self.settings.get('auto_run', False), + ) self.enable_interface(True) return @@ -766,21 +775,24 @@ def confirm_remove(self, files): title='Remove {} {}?'.format(filelen, plural) ) - def destroy(self): - debug('Saving gui config...') - self.settings['dat_dir'] = [self.entry_dat.get()] - self.settings['tiger_dir'] = self.entry_tiger.get() - self.settings['archive_dir'] = self.entry_arch.get() - self.settings['geometry'] = self.geometry() - self.settings['theme'] = self.theme - self.settings['auto_exit'] = self.var_auto_exit.get() - self.settings['extra_data'] = self.var_extra_data.get() - self.settings['no_part_split'] = self.var_no_part_split.get() - # Child windows have saved their geometry already. - for key in list(self.settings): - if key.startswith('geometry_'): - self.settings.pop(key) - config_save(self.settings) + def destroy(self, save_config=True): + if save_config: + debug('Saving gui config...') + self.settings['dat_dir'] = [self.entry_dat.get()] + self.settings['tiger_dir'] = self.entry_tiger.get() + self.settings['archive_dir'] = self.entry_arch.get() + self.settings['geometry'] = self.geometry() + self.settings['theme'] = self.theme + self.settings['auto_exit'] = self.var_auto_exit.get() + self.settings['extra_data'] = self.var_extra_data.get() + self.settings['no_part_split'] = self.var_no_part_split.get() + # Child windows have saved their geometry already. + for key in list(self.settings): + if key.startswith('geometry_'): + self.settings.pop(key) + config_save(self.settings) + else: + debug('Not saving gui config!') debug('Closing main window (geometry={!r}).'.format(self.geometry())) super().destroy() @@ -838,14 +850,29 @@ def report_closed(self, allow_auto_exit=False): if allow_auto_exit and self.var_auto_exit.get(): self.destroy() - def show_error(self, msg): - """ Use show_error, but make sure this window is out of the way. """ + def show_error(self, msg, fatal=False): + """ Use show_error, but make sure this window is out of the way. + If `fatal` is truthy, call `self.destroy()` afterwards. + """ old_topmost = self.attributes('-topmost') self.attributes('-topmost', 0) - self.lower() + if fatal: + self.withdraw() + else: + self.lower() show_error(msg) self.attributes('-topmost', old_topmost) - self.lift() + if fatal: + config_increment(fatal_errors=1, default=0) + debug_err( + 'Closing {} for fatal error:\n{}'.format( + type(self).__name__, + msg, + ) + ) + self.after_idle(self.destroy, False) + else: + self.lift() def show_report( self, parent_files, error_files, success_files, @@ -907,8 +934,6 @@ def load_gui(**kwargs): tiger_files = kwargs.pop('tiger_files') preview_files = kwargs.pop('preview_files') debug('Starting main window...') - debug('tiger_files: {}'.format(tiger_files)) - debug('preview_files: {}'.format(preview_files)) win = WinMain(**kwargs) # noqa if (tiger_files is not None) and (not tiger_files): # --view was used without file paths. diff --git a/lib/util/config.py b/lib/util/config.py index d4b269d..fa49d77 100755 --- a/lib/util/config.py +++ b/lib/util/config.py @@ -25,7 +25,7 @@ debugprinter.enable(('-D' in sys.argv) or ('--debug' in sys.argv)) NAME = 'Tiger Tamer' -VERSION = '0.2.7' +VERSION = '0.2.8' AUTHOR = 'Christopher Joseph Welborn' VERSIONSTR = '{} v. {}'.format(NAME, VERSION) SCRIPT = os.path.split(os.path.abspath(sys.argv[0]))[1] @@ -189,6 +189,7 @@ def get_system_info(): 'archive_files': config_get('archive_files', 0), 'unarchive_files': config_get('unarchive_files', 0), 'remove_files': config_get('remove_files', 0), + 'fatal_errors': config_get('fatal_errors', 0), }