def __init__(self, buildSpec, *args, **kwargs): super(GooeyApplication, self).__init__(None, *args, **kwargs) self._state = {} self.buildSpec = buildSpec self.header = FrameHeader(self, buildSpec) self.configs = self.buildConfigPanels(self) self.navbar = self.buildNavigation() self.footer = Footer(self, buildSpec) self.console = Console(self, buildSpec) self.layoutComponent() self.clientRunner = ProcessController( self.buildSpec.get('progress_regex'), self.buildSpec.get('progress_expr'), self.buildSpec.get('encoding')) pub.subscribe(events.WINDOW_START, self.onStart) pub.subscribe(events.WINDOW_RESTART, self.onStart) pub.subscribe(events.WINDOW_STOP, self.onStopExecution) pub.subscribe(events.WINDOW_CLOSE, self.onClose) pub.subscribe(events.WINDOW_CANCEL, self.onCancel) pub.subscribe(events.WINDOW_EDIT, self.onEdit) pub.subscribe(events.CONSOLE_UPDATE, self.console.logOutput) pub.subscribe(events.EXECUTION_COMPLETE, self.onComplete) pub.subscribe(events.PROGRESS_UPDATE, self.footer.updateProgressBar) # Top level wx close event self.Bind(wx.EVT_CLOSE, self.onClose) if self.buildSpec['poll_external_updates']: self.fetchExternalUpdates() if self.buildSpec.get('auto_start', False): self.onStart()
def test_extract_progress(self): # should pull out a number based on the supplied # regex and expression processor = ProcessController("^progress: (\d+)%$", None, 'utf-8') self.assertEqual(processor._extract_progress(b'progress: 50%'), 50) processor = ProcessController("total: (\d+)%$", None, 'utf-8') self.assertEqual(processor._extract_progress(b'my cool total: 100%'), 100)
def test_extract_progress(self): # should pull out a number based on the supplied # regex and expression processor = ProcessController(r"^progress: (\d+)%$", None, False, 'utf-8') self.assertEqual(processor._extract_progress(b'progress: 50%'), 50) processor = ProcessController(r"total: (\d+)%$", None, False, 'utf-8') self.assertEqual(processor._extract_progress(b'my cool total: 100%'), 100)
def __init__(self, buildSpec, *args, **kwargs): super(GooeyApplication, self).__init__(None, *args, **kwargs) self._state = {} self.buildSpec = buildSpec self.header = FrameHeader(self, buildSpec) self.configs = self.buildConfigPanels(self) self.navbar = self.buildNavigation() self.footer = Footer(self, buildSpec) self.console = Console(self, buildSpec) self.layoutComponent() self.clientRunner = ProcessController( self.buildSpec.get('progress_regex'), self.buildSpec.get('progress_expr'), self.buildSpec.get('encoding') ) pub.subscribe(events.WINDOW_START, self.onStart) pub.subscribe(events.WINDOW_RESTART, self.onStart) pub.subscribe(events.WINDOW_STOP, self.onStopExecution) pub.subscribe(events.WINDOW_CLOSE, self.onClose) pub.subscribe(events.WINDOW_CANCEL, self.onCancel) pub.subscribe(events.WINDOW_EDIT, self.onEdit) pub.subscribe(events.CONSOLE_UPDATE, self.console.logOutput) pub.subscribe(events.EXECUTION_COMPLETE, self.onComplete) pub.subscribe(events.PROGRESS_UPDATE, self.footer.updateProgressBar) # Top level wx close event self.Bind(wx.EVT_CLOSE, self.onClose) if self.buildSpec['poll_external_updates']: self.fetchExternalUpdates() if self.buildSpec.get('auto_start', False): self.onStart()
def test_eval_progress_returns_none_on_failure(self): # given a match in the string, should eval the result regex = r'(\d+)/(\d+)$' processor = ProcessController(regex, r'x[0] *^/* x[1]', False, False, 'utf-8') match = re.search(regex, '50/50') self.assertIsNone(processor._eval_progress(match))
def test_eval_progress(self): # given a match in the string, should eval the result regex = r'(\d+)/(\d+)$' processor = ProcessController(regex, r'x[0] / x[1]', False, False, 'utf-8') match = re.search(regex, '50/50') self.assertEqual(processor._eval_progress(match), 1.0)
def __init__(self, props): super().__init__(props) self.frameRef = Ref() self.consoleRef = Ref() self.configRef = Ref() self.buildSpec = props self.state = initial_state(props) self.headerprops = lambda state: { 'background_color': self.buildSpec['header_bg_color'], 'title': state['title'], 'show_title': state['header_show_title'], 'subtitle': state['subtitle'], 'show_subtitle': state['header_show_subtitle'], 'flag': wx.EXPAND, 'height': self.buildSpec['header_height'], 'image_uri': state['image'], 'image_size': (six.MAXSIZE, self.buildSpec['header_height'] - 10) } self.fprops = lambda state: { 'buttons': state['buttons'], 'progress': state['progress'], 'timing': state['timing'], 'bg_color': self.buildSpec['footer_bg_color'], 'flag': wx.EXPAND, } self.clientRunner = ProcessController.of(self.buildSpec) self.timer = None
def __init__(self, view, model): self.view = view self.model = model self.client_runner = ProcessController(self.model.progress_regex, self.model.progress_expr) pub.subscribe(self.on_cancel, events.WINDOW_CANCEL) pub.subscribe(self.on_stop, events.WINDOW_STOP) pub.subscribe(self.on_start, events.WINDOW_START) pub.subscribe(self.on_restart, events.WINDOW_RESTART) pub.subscribe(self.on_edit, events.WINDOW_EDIT) pub.subscribe(self.on_close, events.WINDOW_CLOSE) # console statuses from the other thread pub.subscribe(self.on_new_message, 'console_update') pub.subscribe(self.on_progress_change, 'progress_update') pub.subscribe(self.on_client_done, 'execution_complete') pub.subscribe(self.on_selection_change, events.LIST_BOX)
def test_extract_progress(): # should pull out a number based on the supplied # regex and expression processor = ProcessController(r"^progress: (\d+)%$", None) assert processor._extract_progress('progress: 50%') == 50 processor = ProcessController(r"total: (\d+)%$", None) assert processor._extract_progress('my cool total: 100%') == 100
def test_ignore_sigint_family_signals(self): try: import _winapi signals = [signal.CTRL_BREAK_EVENT, signal.CTRL_C_EVENT] programs = ['ignore_break.py', 'ignore_interrupt.py'] except ModuleNotFoundError: signals = [signal.SIGINT] programs = ['ignore_interrupt.py'] for program, sig in zip(programs, signals): cmd = sys.executable + ' ' + os.path.join(os.getcwd(), 'files', program) process = processor = ProcessController(None, None, False, 'utf-8', True, shutdown_signal=sig, testmode=True) process.run(cmd) # super-duper important sleep so that the # signal is actually received by the child process # see: https://stackoverflow.com/questions/32023719/how-to-simulate-a-terminal-ctrl-c-event-from-a-unittest time.sleep(1) process.send_shutdown_signal() # wait to give stdout enough time to write time.sleep(1) # now our signal should have been received, but rejected. self.assertTrue(processor.running()) # so we sigterm to actually shut down the process. process._send_signal(signal.SIGTERM) # sanity wait max_wait = time.time() + 2 while processor.running() and time.time() < max_wait: time.sleep(0.1) # now we should be shut down due to killing the process. self.assertFalse(processor.running()) # and we'll see in the stdout out from the process that our # interrupt was received output = process._process.stdout.read().decode('utf-8') self.assertIn("INTERRUPT", str(output)) # but indeed ignored. It continued running and writing to stdout after # receiving the signal self.assertTrue(output.index("INTERRUPT") < len(output))
def test_all_interrupts_halt_process(self): """ TODO: These tests are hella flaky. I'm confident that the feature works. However, getting signals, subprocesses and unittest to all play together reliably is proving tricky. It primarily seems to come down to how long the time.sleep() is before sending the shutdown signal. """ cmd = 'python ' + os.path.join(os.getcwd(), 'files', 'infinite_loop.py') try: import _winapi signals = [ signal.SIGTERM, signal.CTRL_BREAK_EVENT, signal.CTRL_C_EVENT ] except ModuleNotFoundError: signals = [signal.SIGTERM, signal.SIGINT] try: for sig in signals: print('sig', sig) processor = ProcessController(None, None, False, 'utf-8', True, shutdown_signal=sig) processor.run(cmd) self.assertTrue(processor.running()) # super-duper important sleep so that the # signal is actually received by the child process # see: https://stackoverflow.com/questions/32023719/how-to-simulate-a-terminal-ctrl-c-event-from-a-unittest time.sleep(1) processor.stop() max_wait = time.time() + 4 while processor.running() and time.time() < max_wait: time.sleep(0.1) self.assertFalse(processor.running()) except KeyboardInterrupt: pass
def __init__(self, view, model): self.view = view self.model = model self.client_runner = ProcessController( self.model.progress_regex, self.model.progress_expr ) pub.subscribe(self.on_cancel, events.WINDOW_CANCEL) pub.subscribe(self.on_stop, events.WINDOW_STOP) pub.subscribe(self.on_start, events.WINDOW_START) pub.subscribe(self.on_restart, events.WINDOW_RESTART) pub.subscribe(self.on_edit, events.WINDOW_EDIT) pub.subscribe(self.on_close, events.WINDOW_CLOSE) # console statuses from the other thread pub.subscribe(self.on_new_message, 'console_update') pub.subscribe(self.on_progress_change, 'progress_update') pub.subscribe(self.on_client_done, 'execution_complete') pub.subscribe(self.on_selection_change, events.LIST_BOX)
def test_eval_progress_returns_none_on_failure(self): # given a match in the string, should eval the result regex = r'(\d+)/(\d+)$' processor = ProcessController(regex, r'x[0] *^/* x[1]', 'utf-8') match = re.search(regex, '50/50') self.assertIsNone(processor._eval_progress(match))
def test_extract_progress_returns_none_if_no_regex_supplied(self): processor = ProcessController(None, None, False, 'utf-8') self.assertIsNone(processor._extract_progress(b'Total progress: 100%'))
def test_extract_progress_returns_none_if_no_match_found(self): processor = ProcessController(r'(\d+)%$', None, 'utf-8') self.assertIsNone(processor._extract_progress(b'No match in dis string'))
def test_eval_progress(self): # given a match in the string, should eval the result regex = r'(\d+)/(\d+)$' processor = ProcessController(regex, r'x[0] / x[1]', 'utf-8') match = re.search(regex, '50/50') self.assertEqual(processor._eval_progress(match), 1.0)
class GooeyApplication(wx.Frame): """ Main window for Gooey. """ def __init__(self, buildSpec, *args, **kwargs): super(GooeyApplication, self).__init__(None, *args, **kwargs) self._state = {} self.buildSpec = buildSpec self.header = FrameHeader(self, buildSpec) self.configs = self.buildConfigPanels(self) self.navbar = self.buildNavigation() self.footer = Footer(self, buildSpec) self.console = Console(self, buildSpec) self.layoutComponent() self.clientRunner = ProcessController( self.buildSpec.get('progress_regex'), self.buildSpec.get('progress_expr'), self.buildSpec.get('encoding') ) pub.subscribe(events.WINDOW_START, self.onStart) pub.subscribe(events.WINDOW_RESTART, self.onStart) pub.subscribe(events.WINDOW_STOP, self.onStopExecution) pub.subscribe(events.WINDOW_CLOSE, self.onClose) pub.subscribe(events.WINDOW_CANCEL, self.onCancel) pub.subscribe(events.WINDOW_EDIT, self.onEdit) pub.subscribe(events.CONSOLE_UPDATE, self.console.logOutput) pub.subscribe(events.EXECUTION_COMPLETE, self.onComplete) pub.subscribe(events.PROGRESS_UPDATE, self.footer.updateProgressBar) # Top level wx close event self.Bind(wx.EVT_CLOSE, self.onClose) if self.buildSpec['poll_external_updates']: self.fetchExternalUpdates() if self.buildSpec.get('auto_start', False): self.onStart() def onStart(self, *args, **kwarg): """ Verify user input and kick off the client's program if valid """ with transactUI(self): config = self.navbar.getActiveConfig() config.resetErrors() if config.isValid(): self.clientRunner.run(self.buildCliString()) self.showConsole() else: config.displayErrors() self.Layout() def onEdit(self): """Return the user to the settings screen for further editing""" with transactUI(self): if self.buildSpec['poll_external_updates']: self.fetchExternalUpdates() self.showSettings() def buildCliString(self): """ Collect all of the required information from the config screen and build a CLI string which can be used to invoke the client program """ config = self.navbar.getActiveConfig() group = self.buildSpec['widgets'][self.navbar.getSelectedGroup()] positional = config.getPositionalArgs() optional = config.getOptionalArgs() print(cli.buildCliString( self.buildSpec['target'], group['command'], positional, optional )) return cli.buildCliString( self.buildSpec['target'], group['command'], positional, optional ) def onComplete(self, *args, **kwargs): """ Display the appropriate screen based on the success/fail of the host program """ with transactUI(self): if self.clientRunner.was_success(): if self.buildSpec.get('return_to_config', False): self.showSettings() else: self.showSuccess() if self.buildSpec.get('show_success_modal', True): wx.CallAfter(modals.showSuccess) else: if self.clientRunner.wasForcefullyStopped: self.showForceStopped() else: self.showError() wx.CallAfter(modals.showFailure) def onStopExecution(self): """Displays a scary message and then force-quits the executing client code if the user accepts""" if self.buildSpec['show_stop_warning'] and modals.confirmForceStop(): self.clientRunner.stop() def fetchExternalUpdates(self): """ !Experimental! Calls out to the client code requesting seed values to use in the UI !Experimental! """ seeds = seeder.fetchDynamicProperties( self.buildSpec['target'], self.buildSpec['encoding'] ) for config in self.configs: config.seedUI(seeds) def onCancel(self): """Close the program after confirming""" if modals.confirmExit(): self.onClose() def onClose(self, *args, **kwargs): """Cleanup the top level WxFrame and shutdown the process""" self.Destroy() sys.exit() def layoutComponent(self): sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.header, 0, wx.EXPAND) sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND) sizer.Add(self.navbar, 1, wx.EXPAND) sizer.Add(self.console, 1, wx.EXPAND) sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND) sizer.Add(self.footer, 0, wx.EXPAND) self.SetMinSize((400, 300)) self.SetSize(self.buildSpec['default_size']) self.SetSizer(sizer) self.console.Hide() self.Layout() self.SetIcon(wx.Icon(self.buildSpec['images']['programIcon'], wx.BITMAP_TYPE_ICO)) def buildNavigation(self): """ Chooses the appropriate layout navigation component based on user prefs """ if self.buildSpec['navigation'] == constants.TABBED: navigation = Tabbar(self, self.buildSpec, self.configs) else: navigation = Sidebar(self, self.buildSpec, self.configs) if self.buildSpec['navigation'] == constants.HIDDEN: navigation.Hide() return navigation def buildConfigPanels(self, parent): page_class = TabbedConfigPage if self.buildSpec['tabbed_groups'] else ConfigPage return [page_class(parent, widgets) for widgets in self.buildSpec['widgets'].values()] def showSettings(self): self.navbar.Show(True) self.console.Show(False) self.header.setImage('settings_img') self.header.setTitle(_("settings_title")) self.header.setSubtitle(self.buildSpec['program_description']) self.footer.showButtons('cancel_button', 'start_button') self.footer.progress_bar.Show(False) def showConsole(self): self.navbar.Show(False) self.console.Show(True) self.header.setImage('running_img') self.header.setTitle(_("running_title")) self.header.setSubtitle(_('running_msg')) self.footer.showButtons('stop_button') self.footer.progress_bar.Show(True) if not self.buildSpec['progress_regex']: self.footer.progress_bar.Pulse() def showComplete(self): self.navbar.Show(False) self.console.Show(True) self.footer.showButtons('edit_button', 'restart_button', 'close_button') self.footer.progress_bar.Show(False) def showSuccess(self): self.showComplete() self.header.setImage('check_mark') self.header.setTitle(_('finished_title')) self.header.setSubtitle(_('finished_msg')) self.Layout() def showError(self): self.showComplete() self.header.setImage('error_symbol') self.header.setTitle(_('finished_title')) self.header.setSubtitle(_('finished_error')) def showForceStopped(self): self.showComplete() if self.buildSpec.get('force_stop_is_error', True): self.showError() else: self.showSuccess() self.header.setSubtitle(_('finished_forced_quit'))
def test_extract_progress_returns_none_if_no_regex_supplied(self): processor = ProcessController(None, None, 'utf-8') self.assertIsNone(processor._extract_progress(b'Total progress: 100%'))
def test_eval_progress(): # given a match in the string, should eval the result regex = r'(\d+)/(\d+)$' processor = ProcessController(regex, r'x[0] / x[1]') match = re.search(regex, '50/50') assert processor._eval_progress(match) == 1.0
def test_eval_progress_returns_none_on_failure(): # given a match in the string, should eval the result regex = r'(\d+)/(\d+)$' processor = ProcessController(regex, r'x[0] *^/* x[1]') match = re.search(regex, '50/50') assert processor._eval_progress(match) == None
def test_extract_progress_returns_none_if_no_regex_supplied(): processor = ProcessController(None, None) assert processor._extract_progress('Total progress: 100%') == None
def test_extract_progress_returns_none_if_no_match_found(): processor = ProcessController(r'(\d+)%$', None) assert processor._extract_progress('No match in dis string') == None
class Presenter(object): def __init__(self, view, model): self.view = view self.model = model self.client_runner = ProcessController(self.model.progress_regex, self.model.progress_expr) pub.subscribe(self.on_cancel, events.WINDOW_CANCEL) pub.subscribe(self.on_stop, events.WINDOW_STOP) pub.subscribe(self.on_start, events.WINDOW_START) pub.subscribe(self.on_restart, events.WINDOW_RESTART) pub.subscribe(self.on_edit, events.WINDOW_EDIT) pub.subscribe(self.on_close, events.WINDOW_CLOSE) # console statuses from the other thread pub.subscribe(self.on_new_message, 'console_update') pub.subscribe(self.on_progress_change, 'progress_update') pub.subscribe(self.on_client_done, 'execution_complete') pub.subscribe(self.on_selection_change, events.LIST_BOX) def on_selection_change(self, selection): self.update_model() self.model.active_group = selection self.redraw_from_model() self.syncronize_from_model() def initialize_view(self): self.view.window_title = self.model.program_name self.view.window_size = self.model.default_size self.view.required_section.clear() self.view.optional_section.clear() self.view.required_section.populate(self.model.required_args, self.model.num_required_cols) self.view.optional_section.populate(self.model.optional_args, self.model.num_optional_cols) if self.model.use_monospace_font: self.view.set_display_font_style('monospace') if self.should_disable_stop_button(): self.view.disable_stop_button() else: self.view.enable_stop_button() if self.model.layout_type == layouts.COLUMN: self.view.set_list_contents(list( self.model.argument_groups.keys())) if self.model.auto_start: self.model.update_state(States.RUNNNING) self.on_start() self.syncronize_from_model() def update_model(self): self.update_list(self.model.required_args, self.view.required_section.get_values()) self.update_list(self.model.optional_args, self.view.optional_section.get_values()) self.syncronize_from_model() def syncronize_from_model(self): #TODO move this out of the presenter #TODO Make all view interactions thread safe wx.CallAfter(self.syncronize_from_model_async) def syncronize_from_model_async(self): # update heading titles self.view.heading_title = self.model.heading_title self.view.heading_subtitle = self.model.heading_subtitle # refresh the widgets for index, widget in enumerate(self.view.required_section): widget.set_value(self.model.required_args[index]._value) for index, widget in enumerate(self.view.optional_section): widget.set_value(self.model.optional_args[index]._value) # swap the views getattr(self, self.model.current_state)() def redraw_from_model(self): self.view.freeze() self.view.required_section.clear() self.view.optional_section.clear() self.view.required_section.populate(self.model.required_args, self.model.num_required_cols) self.view.optional_section.populate(self.model.optional_args, self.model.num_optional_cols) getattr(self, self.model.current_state)() self.view.thaw() def should_disable_stop_button(self): return self.model.stop_button_disabled def on_start(self): self.update_model() if not self.model.is_valid(): return self.view.show_missing_args_dialog() command = self.model.build_command_line_string() self.client_runner.run(command) self.model.update_state(States.RUNNNING) self.syncronize_from_model() def on_stop(self): self.ask_stop() def on_edit(self): self.model.update_state(States.CONFIGURING) self.syncronize_from_model() def on_restart(self): self.on_start() def on_cancel(self): if self.view.confirm_exit_dialog(): self.view.Destroy() sys.exit() def on_close(self): self.view.Destroy() sys.exit() def on_new_message(self, msg): # observes changes coming from the subprocess self.view.update_console_async(msg) def on_progress_change(self, progress): # observes changes coming from the subprocess self.view.update_progress_aync( progress, self.model.disable_progress_bar_animation) def on_client_done(self): if self.client_runner.was_success(): self.model.update_state(States.SUCCESS) else: self.model.update_state(States.ERROR) self.syncronize_from_model() def ask_stop(self): if self.view.confirm_stop_dialog(): self.stop() return True return False def stop(self): self.client_runner.stop() def configuring(self): self.view.hide_all_buttons() self.view.hide('check_mark', 'running_img', 'error_symbol', 'runtime_display') self.view.show('settings_img', 'cancel_button', 'start_button', 'config_panel') self.view.Layout() def running(self): self.view.hide_all_buttons() self.view.hide('check_mark', 'settings_img', 'error_symbol', 'config_panel') self.view.show('running_img', 'stop_button', 'progress_bar', 'runtime_display') self.view.progress_bar.Pulse() self.view.Layout() def success(self): self.view.hide_all_buttons() self.view.hide('running_img', 'progress_bar', 'config_panel') self.view.show('check_mark', 'edit_button', 'restart_button', 'close_button', 'runtime_display') self.view.Layout() def error(self): self.view.hide_all_buttons() self.view.hide('running_img', 'progress_bar', 'config_panel') self.view.show('error_symbol', 'edit_button', 'restart_button', 'close_button', 'runtime_display') self.view.Layout() @staticmethod def partition(collection, condition): return list( filter(condition, collection)), [x for x in collection if not condition(x)] def update_list(self, collection, new_values): # convenience method for syncronizing the model -> widget list collections for index, val in enumerate(new_values): collection[index].value = val
class GooeyApplication(wx.Frame): """ Main window for Gooey. """ def __init__(self, buildSpec, *args, **kwargs): super(GooeyApplication, self).__init__(None, *args, **kwargs) self._state = {} self.buildSpec = buildSpec self.header = FrameHeader(self, buildSpec) self.configs = self.buildConfigPanels(self) self.navbar = self.buildNavigation() self.footer = Footer(self, buildSpec) self.console = Console(self, buildSpec) self.layoutComponent() self.clientRunner = ProcessController( self.buildSpec.get('progress_regex'), self.buildSpec.get('progress_expr'), self.buildSpec.get('encoding')) pub.subscribe(events.WINDOW_START, self.onStart) pub.subscribe(events.WINDOW_RESTART, self.onStart) pub.subscribe(events.WINDOW_STOP, self.onStopExecution) pub.subscribe(events.WINDOW_CLOSE, self.onClose) pub.subscribe(events.WINDOW_CANCEL, self.onCancel) pub.subscribe(events.WINDOW_EDIT, self.onEdit) pub.subscribe(events.CONSOLE_UPDATE, self.console.logOutput) pub.subscribe(events.EXECUTION_COMPLETE, self.onComplete) pub.subscribe(events.PROGRESS_UPDATE, self.footer.updateProgressBar) # Top level wx close event self.Bind(wx.EVT_CLOSE, self.onClose) if self.buildSpec['poll_external_updates']: self.fetchExternalUpdates() if self.buildSpec.get('auto_start', False): self.onStart() def onStart(self, *args, **kwarg): """ Verify user input and kick off the client's program if valid """ with transactUI(self): config = self.navbar.getActiveConfig() config.resetErrors() if config.isValid(): self.clientRunner.run(self.buildCliString()) self.showConsole() else: config.displayErrors() self.Layout() def onEdit(self): """Return the user to the settings screen for further editing""" with transactUI(self): if self.buildSpec['poll_external_updates']: self.fetchExternalUpdates() self.showSettings() def buildCliString(self): """ Collect all of the required information from the config screen and build a CLI string which can be used to invoke the client program """ config = self.navbar.getActiveConfig() group = self.buildSpec['widgets'][self.navbar.getSelectedGroup()] positional = config.getPositionalArgs() optional = config.getOptionalArgs() print( cli.buildCliString(self.buildSpec['target'], group['command'], positional, optional)) return cli.buildCliString(self.buildSpec['target'], group['command'], positional, optional) def onComplete(self, *args, **kwargs): """ Display the appropriate screen based on the success/fail of the host program """ with transactUI(self): if self.clientRunner.was_success(): if self.buildSpec.get('return_to_config', False): self.showSettings() else: self.showSuccess() if self.buildSpec.get('show_success_modal', True): wx.CallAfter(modals.showSuccess) else: if self.clientRunner.wasForcefullyStopped: self.showForceStopped() else: self.showError() wx.CallAfter(modals.showFailure) def onStopExecution(self): """Displays a scary message and then force-quits the executing client code if the user accepts""" if self.buildSpec['show_stop_warning'] and modals.confirmForceStop(): self.clientRunner.stop() def fetchExternalUpdates(self): """ !Experimental! Calls out to the client code requesting seed values to use in the UI !Experimental! """ seeds = seeder.fetchDynamicProperties(self.buildSpec['target'], self.buildSpec['encoding']) for config in self.configs: config.seedUI(seeds) def onCancel(self): """Close the program after confirming""" if modals.confirmExit(): self.onClose() def onClose(self, *args, **kwargs): """Cleanup the top level WxFrame and shutdown the process""" self.Destroy() sys.exit() def layoutComponent(self): sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.header, 0, wx.EXPAND) sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND) sizer.Add(self.navbar, 1, wx.EXPAND) sizer.Add(self.console, 1, wx.EXPAND) sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND) sizer.Add(self.footer, 0, wx.EXPAND) self.SetMinSize((400, 300)) self.SetSize(self.buildSpec['default_size']) self.SetSizer(sizer) self.console.Hide() self.Layout() self.SetIcon( wx.Icon(self.buildSpec['images']['programIcon'], wx.BITMAP_TYPE_ICO)) def buildNavigation(self): """ Chooses the appropriate layout navigation component based on user prefs """ if self.buildSpec['navigation'] == constants.TABBED: navigation = Tabbar(self, self.buildSpec, self.configs) else: navigation = Sidebar(self, self.buildSpec, self.configs) if self.buildSpec['navigation'] == constants.HIDDEN: navigation.Hide() return navigation def buildConfigPanels(self, parent): page_class = TabbedConfigPage if self.buildSpec[ 'tabbed_groups'] else ConfigPage return [ page_class(parent, widgets) for widgets in self.buildSpec['widgets'].values() ] def showSettings(self): self.navbar.Show(True) self.console.Show(False) self.header.setImage('settings_img') self.header.setTitle(_("settings_title")) self.header.setSubtitle(self.buildSpec['program_description']) self.footer.showButtons('cancel_button', 'start_button') self.footer.progress_bar.Show(False) def showConsole(self): self.navbar.Show(False) self.console.Show(True) self.header.setImage('running_img') self.header.setTitle(_("running_title")) self.header.setSubtitle(_('running_msg')) self.footer.showButtons('stop_button') self.footer.progress_bar.Show(True) if not self.buildSpec['progress_regex']: self.footer.progress_bar.Pulse() def showComplete(self): self.navbar.Show(False) self.console.Show(True) self.footer.showButtons('edit_button', 'restart_button', 'close_button') self.footer.progress_bar.Show(False) def showSuccess(self): self.showComplete() self.header.setImage('check_mark') self.header.setTitle(_('finished_title')) self.header.setSubtitle(_('finished_msg')) self.Layout() def showError(self): self.showComplete() self.header.setImage('error_symbol') self.header.setTitle(_('finished_title')) self.header.setSubtitle(_('finished_error')) def showForceStopped(self): self.showComplete() if self.buildSpec.get('force_stop_is_error', True): self.showError() else: self.showSuccess() self.header.setSubtitle(_('finished_forced_quit'))
def __init__(self, buildSpec, *args, **kwargs): super(GooeyApplication, self).__init__(None, *args, **kwargs) self._state = {} self.buildSpec = buildSpec self.applyConfiguration() self.menu = MenuBar(buildSpec) self.SetMenuBar(self.menu) self.header = FrameHeader(self, buildSpec) self.configs = self.buildConfigPanels(self) self.navbar = self.buildNavigation() self.footer = Footer(self, buildSpec) self.console = Console(self, buildSpec) self.props = { 'background_color': self.buildSpec['header_bg_color'], 'title': self.buildSpec['program_name'], 'subtitle': self.buildSpec['program_description'], 'height': self.buildSpec['header_height'], 'image_uri': self.buildSpec['images']['configIcon'], 'image_size': (six.MAXSIZE, self.buildSpec['header_height'] - 10) } state = form_page(initial_state(self.buildSpec)) self.fprops = { 'buttons': state['buttons'], 'progress': state['progress'], 'timing': state['timing'], 'bg_color': self.buildSpec['footer_bg_color'] } # self.hhh = render(create_element(RHeader, self.props), self) # self.fff = render(create_element(RFooter, self.fprops), self) # patch(self.hhh, create_element(RHeader, {**self.props, 'image_uri': self.buildSpec['images']['runningIcon']})) self.layoutComponent() self.timer = Timing(self) self.clientRunner = ProcessController( self.buildSpec.get('progress_regex'), self.buildSpec.get('progress_expr'), self.buildSpec.get('hide_progress_msg'), self.buildSpec.get('encoding'), self.buildSpec.get('requires_shell'), self.buildSpec.get('shutdown_signal', signal.SIGTERM)) pub.subscribe(events.WINDOW_START, self.onStart) pub.subscribe(events.WINDOW_RESTART, self.onStart) pub.subscribe(events.WINDOW_STOP, self.onStopExecution) pub.subscribe(events.WINDOW_CLOSE, self.onClose) pub.subscribe(events.WINDOW_CANCEL, self.onCancel) pub.subscribe(events.WINDOW_EDIT, self.onEdit) pub.subscribe(events.CONSOLE_UPDATE, self.console.logOutput) pub.subscribe(events.EXECUTION_COMPLETE, self.onComplete) pub.subscribe(events.PROGRESS_UPDATE, self.footer.updateProgressBar) pub.subscribe(events.TIME_UPDATE, self.footer.updateTimeRemaining) # Top level wx close event # self.Bind(wx.EVT_CLOSE, self.onClose) # TODO: handle child focus for per-field level validation. # self.Bind(wx.EVT_CHILD_FOCUS, self.handleFocus) if self.buildSpec.get('auto_start', False): self.onStart()
class Presenter(object): def __init__(self, view, model): self.view = view self.model = model self.client_runner = ProcessController( self.model.progress_regex, self.model.progress_expr ) pub.subscribe(self.on_cancel, events.WINDOW_CANCEL) pub.subscribe(self.on_stop, events.WINDOW_STOP) pub.subscribe(self.on_start, events.WINDOW_START) pub.subscribe(self.on_restart, events.WINDOW_RESTART) pub.subscribe(self.on_edit, events.WINDOW_EDIT) pub.subscribe(self.on_close, events.WINDOW_CLOSE) # console statuses from the other thread pub.subscribe(self.on_new_message, 'console_update') pub.subscribe(self.on_progress_change, 'progress_update') pub.subscribe(self.on_client_done, 'execution_complete') pub.subscribe(self.on_selection_change, events.LIST_BOX) def on_selection_change(self, selection): self.update_model() self.model.active_group = selection self.redraw_from_model() self.syncronize_from_model() def initialize_view(self): self.view.window_title = self.model.program_name self.view.window_size = self.model.default_size self.view.required_section.clear() self.view.optional_section.clear() self.view.required_section.populate(self.model.required_args, self.model.num_required_cols) self.view.optional_section.populate(self.model.optional_args, self.model.num_optional_cols) if self.model.use_monospace_font: self.view.set_display_font_style('monospace') if self.should_disable_stop_button(): self.view.disable_stop_button() else: self.view.enable_stop_button() if self.model.layout_type == layouts.COLUMN: self.view.set_list_contents(self.model.argument_groups.keys()) if self.model.auto_start: self.model.update_state(States.RUNNNING) self.on_start() self.syncronize_from_model() def update_model(self): self.update_list(self.model.required_args, self.view.required_section.get_values()) self.update_list(self.model.optional_args, self.view.optional_section.get_values()) self.syncronize_from_model() def syncronize_from_model(self): #TODO move this out of the presenter #TODO Make all view interactions thread safe wx.CallAfter(self.syncronize_from_model_async) def syncronize_from_model_async(self): # update heading titles self.view.heading_title = self.model.heading_title self.view.heading_subtitle = self.model.heading_subtitle # refresh the widgets for index, widget in enumerate(self.view.required_section): widget.set_value(self.model.required_args[index]._value) for index, widget in enumerate(self.view.optional_section): widget.set_value(self.model.optional_args[index]._value) # swap the views getattr(self, self.model.current_state)() def redraw_from_model(self): self.view.freeze() self.view.required_section.clear() self.view.optional_section.clear() self.view.required_section.populate(self.model.required_args, self.model.num_required_cols) self.view.optional_section.populate(self.model.optional_args, self.model.num_optional_cols) getattr(self, self.model.current_state)() self.view.thaw() def should_disable_stop_button(self): return self.model.stop_button_disabled def on_start(self): self.update_model() if not self.model.is_valid(): return self.view.show_missing_args_dialog() command = self.model.build_command_line_string() self.client_runner.run(command) self.model.update_state(States.RUNNNING) self.syncronize_from_model() def on_stop(self): self.ask_stop() def on_edit(self): self.model.update_state(States.CONFIGURING) self.syncronize_from_model() def on_restart(self): self.on_start() def on_cancel(self): if self.view.confirm_exit_dialog(): self.view.Destroy() sys.exit() def on_close(self): self.view.Destroy() sys.exit() def on_new_message(self, msg): # observes changes coming from the subprocess self.view.update_console_async(msg) def on_progress_change(self, progress): # observes changes coming from the subprocess self.view.update_progress_aync(progress, self.model.disable_progress_bar_animation) def on_client_done(self): if self.client_runner.was_success(): self.model.update_state(States.SUCCESS) else: self.model.update_state(States.ERROR) self.syncronize_from_model() def ask_stop(self): if self.view.confirm_stop_dialog(): self.stop() return True return False def stop(self): self.client_runner.stop() def configuring(self): self.view.hide_all_buttons() self.view.hide('check_mark', 'running_img', 'error_symbol', 'runtime_display') self.view.show('settings_img', 'cancel_button', 'start_button', 'config_panel') self.view.Layout() def running(self): self.view.hide_all_buttons() self.view.hide('check_mark', 'settings_img', 'error_symbol', 'config_panel') self.view.show('running_img', 'stop_button', 'progress_bar', 'runtime_display') self.view.progress_bar.Pulse() self.view.Layout() def success(self): self.view.hide_all_buttons() self.view.hide('running_img', 'progress_bar', 'config_panel') self.view.show('check_mark', 'edit_button', 'restart_button', 'close_button', 'runtime_display') self.view.Layout() def error(self): self.view.hide_all_buttons() self.view.hide('running_img', 'progress_bar', 'config_panel') self.view.show('error_symbol', 'edit_button', 'restart_button', 'close_button', 'runtime_display') self.view.Layout() @staticmethod def partition(collection, condition): return filter(condition, collection), filter(lambda x: not condition(x), collection) def update_list(self, collection, new_values): # convenience method for syncronizing the model -> widget list collections for index, val in enumerate(new_values): collection[index].value = val
class GooeyApplication(wx.Frame): """ Main window for Gooey. """ def __init__(self, buildSpec, *args, **kwargs): super(GooeyApplication, self).__init__(None, *args, **kwargs) self._state = {} self.buildSpec = buildSpec self.applyConfiguration() self.menu = MenuBar(buildSpec) self.SetMenuBar(self.menu) self.header = FrameHeader(self, buildSpec) self.configs = self.buildConfigPanels(self) self.navbar = self.buildNavigation() self.footer = Footer(self, buildSpec) self.console = Console(self, buildSpec) self.layoutComponent() self.timer = Timing(self) self.clientRunner = ProcessController( self.buildSpec.get('progress_regex'), self.buildSpec.get('progress_expr'), self.buildSpec.get('hide_progress_msg'), self.buildSpec.get('encoding'), self.buildSpec.get('requires_shell'), ) pub.subscribe(events.WINDOW_START, self.onStart) pub.subscribe(events.WINDOW_RESTART, self.onStart) pub.subscribe(events.WINDOW_STOP, self.onStopExecution) pub.subscribe(events.WINDOW_CLOSE, self.onClose) pub.subscribe(events.WINDOW_CANCEL, self.onCancel) pub.subscribe(events.WINDOW_EDIT, self.onEdit) pub.subscribe(events.CONSOLE_UPDATE, self.console.logOutput) pub.subscribe(events.EXECUTION_COMPLETE, self.onComplete) pub.subscribe(events.PROGRESS_UPDATE, self.footer.updateProgressBar) pub.subscribe(events.TIME_UPDATE, self.footer.updateTimeRemaining) # Top level wx close event self.Bind(wx.EVT_CLOSE, self.onClose) if self.buildSpec['poll_external_updates']: self.fetchExternalUpdates() if self.buildSpec.get('auto_start', False): self.onStart() def applyConfiguration(self): self.SetTitle(self.buildSpec['program_name']) self.SetBackgroundColour(self.buildSpec.get('body_bg_color')) def onStart(self, *args, **kwarg): """ Verify user input and kick off the client's program if valid """ with transactUI(self): config = self.navbar.getActiveConfig() config.resetErrors() if config.isValid(): if self.buildSpec['clear_before_run']: self.console.clear() self.clientRunner.run(self.buildCliString()) self.showConsole() else: config.displayErrors() self.Layout() def onEdit(self): """Return the user to the settings screen for further editing""" with transactUI(self): if self.buildSpec['poll_external_updates']: self.fetchExternalUpdates() self.showSettings() def buildCliString(self): """ Collect all of the required information from the config screen and build a CLI string which can be used to invoke the client program """ config = self.navbar.getActiveConfig() group = self.buildSpec['widgets'][self.navbar.getSelectedGroup()] positional = config.getPositionalArgs() optional = config.getOptionalArgs() return cli.buildCliString( self.buildSpec['target'], group['command'], positional, optional, suppress_gooey_flag=self.buildSpec['suppress_gooey_flag']) def onComplete(self, *args, **kwargs): """ Display the appropriate screen based on the success/fail of the host program """ with transactUI(self): if self.clientRunner.was_success(): if self.buildSpec.get('return_to_config', False): self.showSettings() else: self.showSuccess() if self.buildSpec.get('show_success_modal', True): wx.CallAfter(modals.showSuccess) else: if self.clientRunner.wasForcefullyStopped: self.showForceStopped() else: self.showError() if self.buildSpec.get('show_failure_modal'): wx.CallAfter(modals.showFailure) def onCancel(self): """Close the program after confirming We treat the behavior of the "cancel" button slightly differently than the general window close X button only because this is 'part of' the form. """ if modals.confirmExit(): self.onClose() def onStopExecution(self): """Displays a scary message and then force-quits the executing client code if the user accepts""" if self.shouldStopExecution(): self.clientRunner.stop() def onClose(self, *args, **kwargs): """Stop any actively running client program, cleanup the top level WxFrame and shutdown the current process""" # issue #592 - we need to run the same onStopExecution machinery # when the exit button is clicked to ensure everything is cleaned # up correctly. if self.clientRunner.running(): if self.shouldStopExecution(): self.clientRunner.stop() self.destroyGooey() else: self.destroyGooey() def shouldStopExecution(self): return not self.buildSpec[ 'show_stop_warning'] or modals.confirmForceStop() def destroyGooey(self): self.Destroy() sys.exit() def fetchExternalUpdates(self): """ !Experimental! Calls out to the client code requesting seed values to use in the UI !Experimental! """ seeds = seeder.fetchDynamicProperties(self.buildSpec['target'], self.buildSpec['encoding']) for config in self.configs: config.seedUI(seeds) def layoutComponent(self): sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.header, 0, wx.EXPAND) sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND) sizer.Add(self.navbar, 1, wx.EXPAND) sizer.Add(self.console, 1, wx.EXPAND) sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND) sizer.Add(self.footer, 0, wx.EXPAND) self.SetMinSize((400, 300)) self.SetSize(self.buildSpec['default_size']) self.SetSizer(sizer) self.console.Hide() self.Layout() if self.buildSpec.get('fullscreen', True): self.ShowFullScreen(True) # Program Icon (Windows) icon = wx.Icon(self.buildSpec['images']['programIcon'], wx.BITMAP_TYPE_PNG) self.SetIcon(icon) if sys.platform != 'win32': # OSX needs to have its taskbar icon explicitly set # bizarrely, wx requires the TaskBarIcon to be attached to the Frame # as instance data (self.). Otherwise, it will not render correctly. self.taskbarIcon = TaskBarIcon(iconType=wx.adv.TBI_DOCK) self.taskbarIcon.SetIcon(icon) def buildNavigation(self): """ Chooses the appropriate layout navigation component based on user prefs """ if self.buildSpec['navigation'] == constants.TABBED: navigation = Tabbar(self, self.buildSpec, self.configs) else: navigation = Sidebar(self, self.buildSpec, self.configs) if self.buildSpec['navigation'] == constants.HIDDEN: navigation.Hide() return navigation def buildConfigPanels(self, parent): page_class = TabbedConfigPage if self.buildSpec[ 'tabbed_groups'] else ConfigPage return [ page_class(parent, widgets, self.buildSpec) for widgets in self.buildSpec['widgets'].values() ] def showSettings(self): self.navbar.Show(True) self.console.Show(False) self.header.setImage('settings_img') self.header.setTitle(_("settings_title")) self.header.setSubtitle(self.buildSpec['program_description']) self.footer.showButtons('cancel_button', 'start_button') self.footer.progress_bar.Show(False) self.footer.time_remaining_text.Show(False) def showConsole(self): self.navbar.Show(False) self.console.Show(True) self.header.setImage('running_img') self.header.setTitle(_("running_title")) self.header.setSubtitle(_('running_msg')) self.footer.showButtons('stop_button') self.footer.progress_bar.Show(True) self.footer.time_remaining_text.Show(False) if self.buildSpec.get('timing_options')['show_time_remaining']: self.timer.start() self.footer.time_remaining_text.Show(True) if not self.buildSpec['progress_regex']: self.footer.progress_bar.Pulse() def showComplete(self): self.navbar.Show(False) self.console.Show(True) buttons = (['edit_button', 'restart_button', 'close_button'] if self.buildSpec.get('show_restart_button', True) else ['edit_button', 'close_button']) self.footer.showButtons(*buttons) self.footer.progress_bar.Show(False) if self.buildSpec.get('timing_options')['show_time_remaining']: self.timer.stop() self.footer.time_remaining_text.Show(True) if self.buildSpec.get( 'timing_options')['hide_time_remaining_on_complete']: self.footer.time_remaining_text.Show(False) def showSuccess(self): self.showComplete() self.header.setImage('check_mark') self.header.setTitle(_('finished_title')) self.header.setSubtitle(_('finished_msg')) self.Layout() def showError(self): self.showComplete() self.header.setImage('error_symbol') self.header.setTitle(_('finished_title')) self.header.setSubtitle(_('finished_error')) def showForceStopped(self): self.showComplete() if self.buildSpec.get('force_stop_is_error', True): self.showError() else: self.showSuccess() self.header.setSubtitle(_('finished_forced_quit'))
def test_extract_progress_returns_none_if_no_match_found(self): processor = ProcessController(r'(\d+)%$', None, False, 'utf-8') self.assertIsNone( processor._extract_progress(b'No match in dis string'))
class GooeyApplication(wx.Frame): """ Main window for Gooey. """ def __init__(self, buildSpec, *args, **kwargs): super(GooeyApplication, self).__init__(None, *args, **kwargs) self._state = {} self.buildSpec = buildSpec self.applyConfiguration() self.menu = MenuBar(buildSpec) self.SetMenuBar(self.menu) self.header = FrameHeader(self, buildSpec) self.configs = self.buildConfigPanels(self) self.navbar = self.buildNavigation() self.footer = Footer(self, buildSpec) self.console = Console(self, buildSpec) self.props = { 'background_color': self.buildSpec['header_bg_color'], 'title': self.buildSpec['program_name'], 'subtitle': self.buildSpec['program_description'], 'height': self.buildSpec['header_height'], 'image_uri': self.buildSpec['images']['configIcon'], 'image_size': (six.MAXSIZE, self.buildSpec['header_height'] - 10) } state = form_page(initial_state(self.buildSpec)) self.fprops = { 'buttons': state['buttons'], 'progress': state['progress'], 'timing': state['timing'], 'bg_color': self.buildSpec['footer_bg_color'] } # self.hhh = render(create_element(RHeader, self.props), self) # self.fff = render(create_element(RFooter, self.fprops), self) # patch(self.hhh, create_element(RHeader, {**self.props, 'image_uri': self.buildSpec['images']['runningIcon']})) self.layoutComponent() self.timer = Timing(self) self.clientRunner = ProcessController( self.buildSpec.get('progress_regex'), self.buildSpec.get('progress_expr'), self.buildSpec.get('hide_progress_msg'), self.buildSpec.get('encoding'), self.buildSpec.get('requires_shell'), self.buildSpec.get('shutdown_signal', signal.SIGTERM)) pub.subscribe(events.WINDOW_START, self.onStart) pub.subscribe(events.WINDOW_RESTART, self.onStart) pub.subscribe(events.WINDOW_STOP, self.onStopExecution) pub.subscribe(events.WINDOW_CLOSE, self.onClose) pub.subscribe(events.WINDOW_CANCEL, self.onCancel) pub.subscribe(events.WINDOW_EDIT, self.onEdit) pub.subscribe(events.CONSOLE_UPDATE, self.console.logOutput) pub.subscribe(events.EXECUTION_COMPLETE, self.onComplete) pub.subscribe(events.PROGRESS_UPDATE, self.footer.updateProgressBar) pub.subscribe(events.TIME_UPDATE, self.footer.updateTimeRemaining) # Top level wx close event # self.Bind(wx.EVT_CLOSE, self.onClose) # TODO: handle child focus for per-field level validation. # self.Bind(wx.EVT_CHILD_FOCUS, self.handleFocus) if self.buildSpec.get('auto_start', False): self.onStart() def applyConfiguration(self): self.SetTitle(self.buildSpec['program_name']) self.SetBackgroundColour(self.buildSpec.get('body_bg_color')) def onStart(self, *args, **kwarg): """ Verify user input and kick off the client's program if valid """ # navigates away from the button because a # disabled focused button still looks enabled. self.footer.cancel_button.Disable() self.footer.start_button.Disable() self.footer.start_button.Navigate() if Events.VALIDATE_FORM in self.buildSpec.get('use_events', []): # TODO: make this wx thread safe so that it can # actually run asynchronously Thread(target=self.onStartAsync).run() else: Thread(target=self.onStartAsync).run() def onStartAsync(self, *args, **kwargs): with transactUI(self): try: errors = self.validateForm().getOrThrow() if errors: # TODO config = self.navbar.getActiveConfig() config.setErrors(errors) self.Layout() # TODO: account for tabbed layouts # TODO: scroll the first error into view # TODO: rather than just snapping to the top self.configs[0].Scroll(0, 0) else: if self.buildSpec['clear_before_run']: self.console.clear() self.clientRunner.run(self.buildCliString()) self.showConsole() except CalledProcessError as e: self.showError() self.console.appendText(str(e)) self.console.appendText( '\n\nThis failure happens when Gooey tries to invoke your ' 'code for the VALIDATE_FORM event and receives an expected ' 'error code in response.') wx.CallAfter(modals.showFailure) except JSONDecodeError as e: self.showError() self.console.appendText(str(e)) self.console.appendText( '\n\nGooey was unable to parse the response to the VALIDATE_FORM event. ' 'This can happen if you have additional logs to stdout beyond what Gooey ' 'expects.') wx.CallAfter(modals.showFailure) # for some reason, we have to delay the re-enabling of # the buttons by a few ms otherwise they pickup pending # events created while they were disabled. Trial and error # let to this solution. wx.CallLater(20, self.footer.start_button.Enable) wx.CallLater(20, self.footer.cancel_button.Enable) def onEdit(self): """Return the user to the settings screen for further editing""" with transactUI(self): for config in self.configs: config.resetErrors() self.showSettings() def onComplete(self, *args, **kwargs): """ Display the appropriate screen based on the success/fail of the host program """ with transactUI(self): if self.clientRunner.was_success(): if self.buildSpec.get('return_to_config', False): self.showSettings() else: self.showSuccess() if self.buildSpec.get('show_success_modal', True): wx.CallAfter(modals.showSuccess) else: if self.clientRunner.wasForcefullyStopped: self.showForceStopped() else: self.showError() if self.buildSpec.get('show_failure_modal'): wx.CallAfter(modals.showFailure) def onCancel(self): """Close the program after confirming We treat the behavior of the "cancel" button slightly differently than the general window close X button only because this is 'part of' the form. """ if modals.confirmExit(): self.onClose() def onStopExecution(self): """Displays a scary message and then force-quits the executing client code if the user accepts""" if self.shouldStopExecution(): self.clientRunner.stop() def onClose(self, *args, **kwargs): """Stop any actively running client program, cleanup the top level WxFrame and shutdown the current process""" # issue #592 - we need to run the same onStopExecution machinery # when the exit button is clicked to ensure everything is cleaned # up correctly. if self.clientRunner.running(): if self.shouldStopExecution(): self.clientRunner.stop() self.destroyGooey() else: self.destroyGooey() def buildCliString(self) -> str: """ Collect all of the required information from the config screen and build a CLI string which can be used to invoke the client program """ cmd = self.getCommandDetails() return cli.cliCmd( cmd.target, cmd.subcommand, cmd.positionals, cmd.optionals, suppress_gooey_flag=self.buildSpec['suppress_gooey_flag']) def validateForm(self) -> Try[Mapping[str, str]]: config = self.navbar.getActiveConfig() localErrors: Mapping[str, str] = config.getErrors() dynamicResult: Try[Mapping[str, str]] = self.fetchDynamicValidations() combineErrors = lambda m: merge(localErrors, m) return dynamicResult.map(combineErrors) def fetchDynamicValidations(self) -> Try[Mapping[str, str]]: # only run the dynamic validation if the user has # specifically subscribed to that event if Events.VALIDATE_FORM in self.buildSpec.get('use_events', []): cmd = self.getCommandDetails() return seeder.communicate( cli.formValidationCmd(cmd.target, cmd.subcommand, cmd.positionals, cmd.optionals), self.buildSpec['encoding']) else: # shim response if nothing to do. return Success({}) def getCommandDetails(self) -> CommandDetails: """ Temporary helper for getting the state of the current Config. To be deprecated upon (the desperately needed) refactor. """ config = self.navbar.getActiveConfig() group = self.buildSpec['widgets'][self.navbar.getSelectedGroup()] return CommandDetails( self.buildSpec['target'], group['command'], config.getPositionalValues(), config.getOptionalValues(), ) def shouldStopExecution(self): return not self.buildSpec[ 'show_stop_warning'] or modals.confirmForceStop() def destroyGooey(self): self.Destroy() sys.exit() def block(self, **kwargs): pass def layoutComponent(self): sizer = wx.BoxSizer(wx.VERTICAL) # sizer.Add(self.hhh, 0, wx.EXPAND) sizer.Add(self.header, 0, wx.EXPAND) sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND) sizer.Add(self.navbar, 1, wx.EXPAND) sizer.Add(self.console, 1, wx.EXPAND) sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND) # sizer.Add(self.fff, 0, wx.EXPAND) sizer.Add(self.footer, 0, wx.EXPAND) self.SetMinSize((400, 300)) self.SetSize(self.buildSpec['default_size']) self.SetSizer(sizer) self.console.Hide() self.Layout() if self.buildSpec.get('fullscreen', True): self.ShowFullScreen(True) # Program Icon (Windows) icon = wx.Icon(self.buildSpec['images']['programIcon'], wx.BITMAP_TYPE_PNG) self.SetIcon(icon) if sys.platform != 'win32': # OSX needs to have its taskbar icon explicitly set # bizarrely, wx requires the TaskBarIcon to be attached to the Frame # as instance data (self.). Otherwise, it will not render correctly. self.taskbarIcon = TaskBarIcon(iconType=wx.adv.TBI_DOCK) self.taskbarIcon.SetIcon(icon) def buildNavigation(self): """ Chooses the appropriate layout navigation component based on user prefs """ if self.buildSpec['navigation'] == constants.TABBED: navigation = Tabbar(self, self.buildSpec, self.configs) else: navigation = Sidebar(self, self.buildSpec, self.configs) if self.buildSpec['navigation'] == constants.HIDDEN: navigation.Hide() return navigation def buildConfigPanels(self, parent): page_class = TabbedConfigPage if self.buildSpec[ 'tabbed_groups'] else ConfigPage return [ page_class(parent, widgets, self.buildSpec) for widgets in self.buildSpec['widgets'].values() ] def showSettings(self): self.navbar.Show(True) self.console.Show(False) self.header.setImage('settings_img') self.header.setTitle(_("settings_title")) self.header.setSubtitle(self.buildSpec['program_description']) self.footer.showButtons('cancel_button', 'start_button') self.footer.progress_bar.Show(False) self.footer.time_remaining_text.Show(False) def showConsole(self): self.navbar.Show(False) self.console.Show(True) self.header.setImage('running_img') self.header.setTitle(_("running_title")) self.header.setSubtitle(_('running_msg')) self.footer.showButtons('stop_button') if not self.buildSpec.get('disable_progress_bar_animation', False): self.footer.progress_bar.Show(True) self.footer.time_remaining_text.Show(False) if self.buildSpec.get('timing_options')['show_time_remaining']: self.timer.start() self.footer.time_remaining_text.Show(True) if not self.buildSpec['progress_regex']: self.footer.progress_bar.Pulse() def showComplete(self): self.navbar.Show(False) self.console.Show(True) buttons = (['edit_button', 'restart_button', 'close_button'] if self.buildSpec.get('show_restart_button', True) else ['edit_button', 'close_button']) self.footer.showButtons(*buttons) self.footer.progress_bar.Show(False) if self.buildSpec.get('timing_options')['show_time_remaining']: self.timer.stop() self.footer.time_remaining_text.Show(True) if self.buildSpec.get( 'timing_options')['hide_time_remaining_on_complete']: self.footer.time_remaining_text.Show(False) def showSuccess(self): self.showComplete() self.header.setImage('check_mark') self.header.setTitle(_('finished_title')) self.header.setSubtitle(_('finished_msg')) self.Layout() def showError(self): self.showComplete() self.header.setImage('error_symbol') self.header.setTitle(_('finished_title')) self.header.setSubtitle(_('finished_error')) def showForceStopped(self): self.showComplete() if self.buildSpec.get('force_stop_is_error', True): self.showError() else: self.showSuccess() self.header.setSubtitle(_('finished_forced_quit'))