You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

360 lines
14 KiB

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)]]]
)