|
|
import sys from json import JSONDecodeError
import six import wx # type: ignore
from gooey import Events from gooey.gui import events from gooey.gui import host from gooey.gui import state as s from gooey.gui.application.components import RHeader, ProgressSpinner, ErrorWarning, RTabbedLayout, \ RSidebar, RFooter from gooey.gui.components import modals from gooey.gui.components.config import ConfigPage from gooey.gui.components.config import TabbedConfigPage from gooey.gui.components.console import Console from gooey.gui.components.menubar import MenuBar from gooey.gui.lang.i18n import _ from gooey.gui.processor import ProcessController from gooey.gui.pubsub import pub from gooey.gui.state import FullGooeyState from gooey.gui.state import initial_state, ProgressEvent, TimingEvent from gooey.gui.util.wx_util import transactUI, callafter from gooey.python_bindings import constants from gooey.python_bindings.dynamics import unexpected_exit_explanations, \ deserialize_failure_explanations from gooey.python_bindings.types import PublicGooeyState from gooey.python_bindings.types import Try from gooey.util.functional import assoc from gooey.gui.util.time import Timing from rewx import components as c # type: ignore from rewx import wsx # type: ignore from rewx.core import Component, Ref # type: ignore
class RGooey(Component): """
Main Application container for Gooey.
State Management ----------------
Pending further refactor, state is tracked in two places: 1. On this instance (React style) 2. In the WX Form Elements themselves[0]
As needed, these two states are merged to form the `FullGooeyState`, which is the canonical state object against which all logic runs.
Dynamic Updates ---------------
[0] this is legacy and will (eventually) be refactored away
"""
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 component_did_mount(self): pub.subscribe(events.WINDOW_START, self.onStart) pub.subscribe(events.WINDOW_RESTART, self.onStart) pub.subscribe(events.WINDOW_STOP, self.handleInterrupt) pub.subscribe(events.WINDOW_CLOSE, self.handleClose) pub.subscribe(events.WINDOW_CANCEL, self.handleCancel) pub.subscribe(events.WINDOW_EDIT, self.handleEdit) pub.subscribe(events.CONSOLE_UPDATE, self.consoleRef.instance.logOutput) pub.subscribe(events.EXECUTION_COMPLETE, self.handleComplete) pub.subscribe(events.PROGRESS_UPDATE, self.updateProgressBar) pub.subscribe(events.TIME_UPDATE, self.updateTime) # # Top level wx close event frame: wx.Frame = self.frameRef.instance frame.Bind(wx.EVT_CLOSE, self.handleClose) frame.SetMenuBar(MenuBar(self.buildSpec)) self.timer = Timing(frame)
if self.state['fullscreen']: frame.ShowFullScreen(True)
if self.state['show_preview_warning'] and not 'unittest' in sys.modules.keys(): wx.MessageDialog(None, caption='YOU CAN DISABLE THIS MESSAGE', message="""
This is a preview build of 1.2.0! There may be instability or broken functionality. If you encounter any issues, please open an issue here: https://github.com/chriskiehl/Gooey/issues The current stable version is 1.0.8. NOTE! You can disable this message by setting `show_preview_warning` to False. e.g. `@Gooey(show_preview_warning=False)` """).ShowModal()
def getActiveConfig(self): return [item for child in self.configRef.instance.Children # we descend down another level of children to account # for Notebook layouts (which have wrapper objects) for item in [child] + list(child.Children) if isinstance(item, ConfigPage) or isinstance(item, TabbedConfigPage)][self.state['activeSelection']]
def getActiveFormState(self): """
This boiler-plate and manual interrogation of the UIs state is required until we finish porting the Config Form over to rewx (which is a battle left for another day given its complexity) """
return self.getActiveConfig().getFormState()
def fullState(self): """
Re: final porting is a to do. For now we merge the UI state into the main tracked state. """
formState = self.getActiveFormState() return s.combine(self.state, self.props, formState)
def onStart(self, *args, **kwargs): """
Dispatches the start behavior. """
if Events.VALIDATE_FORM in self.state['use_events']: self.runAsyncValidation() else: self.startRun()
def startRun(self): """
Kicks off a run by invoking the host's code and pumping its stdout to Gooey's Console window. """
state = self.fullState() if state['clear_before_run']: self.consoleRef.instance.Clear() self.set_state(s.consoleScreen(_, state)) self.clientRunner.run(s.buildInvocationCmd(state)) self.timer.start() self.frameRef.instance.Layout() for child in self.frameRef.instance.Children: child.Layout()
def syncExternalState(self, state: FullGooeyState): """
Sync the UI's state to what the host program has requested. """
self.getActiveConfig().syncFormState(s.activeFormState(state)) self.frameRef.instance.Layout() for child in self.frameRef.instance.Children: child.Layout()
def handleInterrupt(self, *args, **kwargs): if self.shouldStopExecution(): self.clientRunner.stop()
def handleComplete(self, *args, **kwargs): self.timer.stop() if self.clientRunner.was_success(): self.handleSuccessfulRun() if Events.ON_SUCCESS in self.state['use_events']: self.runAsyncExternalOnCompleteHandler(was_success=True) else: self.handleErrantRun() if Events.ON_ERROR in self.state['use_events']: self.runAsyncExternalOnCompleteHandler(was_success=False)
def handleSuccessfulRun(self): if self.state['return_to_config']: self.set_state(s.editScreen(_, self.state)) else: self.set_state(s.successScreen(_, self.state)) if self.state['show_success_modal']: wx.CallAfter(modals.showSuccess)
def handleErrantRun(self): if self.clientRunner.wasForcefullyStopped: self.set_state(s.interruptedScreen(_, self.state)) else: self.set_state(s.errorScreen(_, self.state)) if self.state['show_failure_modal']: wx.CallAfter(modals.showFailure)
def successScreen(self): strings = {'title': _('finished_title'), 'subtitle': _('finished_msg')} self.set_state(s.success(self.state, strings, self.buildSpec))
def handleEdit(self, *args, **kwargs): self.set_state(s.editScreen(_, self.state))
def handleCancel(self, *args, **kwargs): if modals.confirmExit(): self.handleClose()
def handleClose(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. frame: wx.Frame = self.frameRef.instance if self.clientRunner.running(): if self.shouldStopExecution(): self.clientRunner.stop() frame.Destroy() # TODO: NOT exiting here would allow # spawing the gooey to input params then # returning control to the CLI sys.exit() else: frame.Destroy() sys.exit()
def shouldStopExecution(self): return not self.state['show_stop_warning'] or modals.confirmForceStop()
def updateProgressBar(self, *args, progress=None): self.set_state(s.updateProgress(self.state, ProgressEvent(progress=progress)))
def updateTime(self, *args, elapsed_time=None, estimatedRemaining=None, **kwargs): event = TimingEvent(elapsed_time=elapsed_time, estimatedRemaining=estimatedRemaining) self.set_state(s.updateTime(self.state, event))
def handleSelectAction(self, event): self.set_state(assoc(self.state, 'activeSelection', event.Selection))
def runAsyncValidation(self): def handleHostResponse(hostState: PublicGooeyState): self.set_state(s.finishUpdate(self.state)) currentState = self.fullState() self.syncExternalState(s.mergeExternalState(currentState, hostState)) if not s.has_errors(self.fullState()): self.startRun() else: self.set_state(s.editScreen(_, s.show_alert(self.fullState())))
def onComplete(result: Try[PublicGooeyState]): result.onSuccess(handleHostResponse) result.onError(self.handleHostError)
self.set_state(s.beginUpdate(self.state)) fullState = self.fullState() host.communicateFormValidation(fullState, callafter(onComplete))
def runAsyncExternalOnCompleteHandler(self, was_success): def handleHostResponse(hostState): if hostState: self.syncExternalState(s.mergeExternalState(self.fullState(), hostState))
def onComplete(result: Try[PublicGooeyState]): result.onError(self.handleHostError) result.onSuccess(handleHostResponse)
if was_success: host.communicateSuccessState(self.fullState(), callafter(onComplete)) else: host.communicateErrorState(self.fullState(), callafter(onComplete))
def handleHostError(self, ex): """
All async errors get pumped here where we dump out the error and they hopefully provide a lot of helpful debugging info for the user. """
try: self.set_state(s.errorScreen(_, self.state)) self.consoleRef.instance.appendText(str(ex)) self.consoleRef.instance.appendText(str(getattr(ex, 'output', ''))) self.consoleRef.instance.appendText(str(getattr(ex, 'stderr', ''))) raise ex except JSONDecodeError as e: self.consoleRef.instance.appendText(deserialize_failure_explanations) except Exception as e: self.consoleRef.instance.appendText(unexpected_exit_explanations) finally: self.set_state({**self.state, 'fetchingUpdate': False})
def render(self): return wsx( [c.Frame, {'title': self.buildSpec['program_name'], 'background_color': self.buildSpec['body_bg_color'], 'double_buffered': True, 'min_size': (400, 300), 'icon_uri': self.state['images']['programIcon'], 'size': self.buildSpec['default_size'], 'ref': self.frameRef}, [c.Block, {'orient': wx.VERTICAL}, [RHeader, self.headerprops(self.state)], [c.StaticLine, {'style': wx.LI_HORIZONTAL, 'flag': wx.EXPAND}], [ProgressSpinner, {'show': self.state['fetchingUpdate']}], [ErrorWarning, {'show': self.state['show_error_alert'], 'uri': self.state['images']['errorIcon']}], [Console, {**self.buildSpec, 'flag': wx.EXPAND, 'proportion': 1, 'show': self.state['screen'] == 'CONSOLE', 'ref': self.consoleRef}], [RTabbedLayout if self.buildSpec['navigation'] == constants.TABBED else RSidebar, {'bg_color': self.buildSpec['sidebar_bg_color'], 'label': 'Some Action!', 'tabbed_groups': self.buildSpec['tabbed_groups'], 'show_sidebar': self.state['show_sidebar'], 'ref': self.configRef, 'show': self.state['screen'] == 'FORM', 'activeSelection': self.state['activeSelection'], 'options': list(self.buildSpec['widgets'].keys()), 'on_change': self.handleSelectAction, 'config': self.buildSpec['widgets'], 'flag': wx.EXPAND, 'proportion': 1}], [c.StaticLine, {'style': wx.LI_HORIZONTAL, 'flag': wx.EXPAND}], [RFooter, self.fprops(self.state)]]] )
|