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

2 years ago
  1. import sys
  2. from json import JSONDecodeError
  3. import six
  4. import wx # type: ignore
  5. from gooey import Events
  6. from gooey.gui import events
  7. from gooey.gui import host
  8. from gooey.gui import state as s
  9. from gooey.gui.application.components import RHeader, ProgressSpinner, ErrorWarning, RTabbedLayout, \
  10. RSidebar, RFooter
  11. from gooey.gui.components import modals
  12. from gooey.gui.components.config import ConfigPage
  13. from gooey.gui.components.config import TabbedConfigPage
  14. from gooey.gui.components.console import Console
  15. from gooey.gui.components.menubar import MenuBar
  16. from gooey.gui.lang.i18n import _
  17. from gooey.gui.processor import ProcessController
  18. from gooey.gui.pubsub import pub
  19. from gooey.gui.state import FullGooeyState
  20. from gooey.gui.state import initial_state, ProgressEvent, TimingEvent
  21. from gooey.gui.util.wx_util import transactUI, callafter
  22. from gooey.python_bindings import constants
  23. from gooey.python_bindings.dynamics import unexpected_exit_explanations, \
  24. deserialize_failure_explanations
  25. from gooey.python_bindings.types import PublicGooeyState
  26. from gooey.python_bindings.types import Try
  27. from gooey.util.functional import assoc
  28. from gooey.gui.util.time import Timing
  29. from rewx import components as c # type: ignore
  30. from rewx import wsx # type: ignore
  31. from rewx.core import Component, Ref # type: ignore
  32. class RGooey(Component):
  33. """
  34. Main Application container for Gooey.
  35. State Management
  36. ----------------
  37. Pending further refactor, state is tracked in two places:
  38. 1. On this instance (React style)
  39. 2. In the WX Form Elements themselves[0]
  40. As needed, these two states are merged to form the `FullGooeyState`, which
  41. is the canonical state object against which all logic runs.
  42. Dynamic Updates
  43. ---------------
  44. [0] this is legacy and will (eventually) be refactored away
  45. """
  46. def __init__(self, props):
  47. super().__init__(props)
  48. self.frameRef = Ref()
  49. self.consoleRef = Ref()
  50. self.configRef = Ref()
  51. self.buildSpec = props
  52. self.state = initial_state(props)
  53. self.headerprops = lambda state: {
  54. 'background_color': self.buildSpec['header_bg_color'],
  55. 'title': state['title'],
  56. 'show_title': state['header_show_title'],
  57. 'subtitle': state['subtitle'],
  58. 'show_subtitle': state['header_show_subtitle'],
  59. 'flag': wx.EXPAND,
  60. 'height': self.buildSpec['header_height'],
  61. 'image_uri': state['image'],
  62. 'image_size': (six.MAXSIZE, self.buildSpec['header_height'] - 10)}
  63. self.fprops = lambda state: {
  64. 'buttons': state['buttons'],
  65. 'progress': state['progress'],
  66. 'timing': state['timing'],
  67. 'bg_color': self.buildSpec['footer_bg_color'],
  68. 'flag': wx.EXPAND,
  69. }
  70. self.clientRunner = ProcessController.of(self.buildSpec)
  71. self.timer = None
  72. def component_did_mount(self):
  73. pub.subscribe(events.WINDOW_START, self.onStart)
  74. pub.subscribe(events.WINDOW_RESTART, self.onStart)
  75. pub.subscribe(events.WINDOW_STOP, self.handleInterrupt)
  76. pub.subscribe(events.WINDOW_CLOSE, self.handleClose)
  77. pub.subscribe(events.WINDOW_CANCEL, self.handleCancel)
  78. pub.subscribe(events.WINDOW_EDIT, self.handleEdit)
  79. pub.subscribe(events.CONSOLE_UPDATE, self.consoleRef.instance.logOutput)
  80. pub.subscribe(events.EXECUTION_COMPLETE, self.handleComplete)
  81. pub.subscribe(events.PROGRESS_UPDATE, self.updateProgressBar)
  82. pub.subscribe(events.TIME_UPDATE, self.updateTime)
  83. # # Top level wx close event
  84. frame: wx.Frame = self.frameRef.instance
  85. frame.Bind(wx.EVT_CLOSE, self.handleClose)
  86. frame.SetMenuBar(MenuBar(self.buildSpec))
  87. self.timer = Timing(frame)
  88. if self.state['fullscreen']:
  89. frame.ShowFullScreen(True)
  90. if self.state['show_preview_warning'] and not 'unittest' in sys.modules.keys():
  91. wx.MessageDialog(None, caption='YOU CAN DISABLE THIS MESSAGE',
  92. message="""
  93. This is a preview build of 1.2.0! There may be instability or
  94. broken functionality. If you encounter any issues, please open an issue
  95. here: https://github.com/chriskiehl/Gooey/issues
  96. The current stable version is 1.0.8.
  97. NOTE! You can disable this message by setting `show_preview_warning` to False.
  98. e.g.
  99. `@Gooey(show_preview_warning=False)`
  100. """).ShowModal()
  101. def getActiveConfig(self):
  102. return [item
  103. for child in self.configRef.instance.Children
  104. # we descend down another level of children to account
  105. # for Notebook layouts (which have wrapper objects)
  106. for item in [child] + list(child.Children)
  107. if isinstance(item, ConfigPage)
  108. or isinstance(item, TabbedConfigPage)][self.state['activeSelection']]
  109. def getActiveFormState(self):
  110. """
  111. This boiler-plate and manual interrogation of the UIs
  112. state is required until we finish porting the Config Form
  113. over to rewx (which is a battle left for another day given
  114. its complexity)
  115. """
  116. return self.getActiveConfig().getFormState()
  117. def fullState(self):
  118. """
  119. Re: final porting is a to do. For now we merge the UI
  120. state into the main tracked state.
  121. """
  122. formState = self.getActiveFormState()
  123. return s.combine(self.state, self.props, formState)
  124. def onStart(self, *args, **kwargs):
  125. """
  126. Dispatches the start behavior.
  127. """
  128. if Events.VALIDATE_FORM in self.state['use_events']:
  129. self.runAsyncValidation()
  130. else:
  131. self.startRun()
  132. def startRun(self):
  133. """
  134. Kicks off a run by invoking the host's code
  135. and pumping its stdout to Gooey's Console window.
  136. """
  137. state = self.fullState()
  138. if state['clear_before_run']:
  139. self.consoleRef.instance.Clear()
  140. self.set_state(s.consoleScreen(_, state))
  141. self.clientRunner.run(s.buildInvocationCmd(state))
  142. self.timer.start()
  143. self.frameRef.instance.Layout()
  144. for child in self.frameRef.instance.Children:
  145. child.Layout()
  146. def syncExternalState(self, state: FullGooeyState):
  147. """
  148. Sync the UI's state to what the host program has requested.
  149. """
  150. self.getActiveConfig().syncFormState(s.activeFormState(state))
  151. self.frameRef.instance.Layout()
  152. for child in self.frameRef.instance.Children:
  153. child.Layout()
  154. def handleInterrupt(self, *args, **kwargs):
  155. if self.shouldStopExecution():
  156. self.clientRunner.stop()
  157. def handleComplete(self, *args, **kwargs):
  158. self.timer.stop()
  159. if self.clientRunner.was_success():
  160. self.handleSuccessfulRun()
  161. if Events.ON_SUCCESS in self.state['use_events']:
  162. self.runAsyncExternalOnCompleteHandler(was_success=True)
  163. else:
  164. self.handleErrantRun()
  165. if Events.ON_ERROR in self.state['use_events']:
  166. self.runAsyncExternalOnCompleteHandler(was_success=False)
  167. def handleSuccessfulRun(self):
  168. if self.state['return_to_config']:
  169. self.set_state(s.editScreen(_, self.state))
  170. else:
  171. self.set_state(s.successScreen(_, self.state))
  172. if self.state['show_success_modal']:
  173. wx.CallAfter(modals.showSuccess)
  174. def handleErrantRun(self):
  175. if self.clientRunner.wasForcefullyStopped:
  176. self.set_state(s.interruptedScreen(_, self.state))
  177. else:
  178. self.set_state(s.errorScreen(_, self.state))
  179. if self.state['show_failure_modal']:
  180. wx.CallAfter(modals.showFailure)
  181. def successScreen(self):
  182. strings = {'title': _('finished_title'), 'subtitle': _('finished_msg')}
  183. self.set_state(s.success(self.state, strings, self.buildSpec))
  184. def handleEdit(self, *args, **kwargs):
  185. self.set_state(s.editScreen(_, self.state))
  186. def handleCancel(self, *args, **kwargs):
  187. if modals.confirmExit():
  188. self.handleClose()
  189. def handleClose(self, *args, **kwargs):
  190. """Stop any actively running client program, cleanup the top
  191. level WxFrame and shutdown the current process"""
  192. # issue #592 - we need to run the same onStopExecution machinery
  193. # when the exit button is clicked to ensure everything is cleaned
  194. # up correctly.
  195. frame: wx.Frame = self.frameRef.instance
  196. if self.clientRunner.running():
  197. if self.shouldStopExecution():
  198. self.clientRunner.stop()
  199. frame.Destroy()
  200. # TODO: NOT exiting here would allow
  201. # spawing the gooey to input params then
  202. # returning control to the CLI
  203. sys.exit()
  204. else:
  205. frame.Destroy()
  206. sys.exit()
  207. def shouldStopExecution(self):
  208. return not self.state['show_stop_warning'] or modals.confirmForceStop()
  209. def updateProgressBar(self, *args, progress=None):
  210. self.set_state(s.updateProgress(self.state, ProgressEvent(progress=progress)))
  211. def updateTime(self, *args, elapsed_time=None, estimatedRemaining=None, **kwargs):
  212. event = TimingEvent(elapsed_time=elapsed_time, estimatedRemaining=estimatedRemaining)
  213. self.set_state(s.updateTime(self.state, event))
  214. def handleSelectAction(self, event):
  215. self.set_state(assoc(self.state, 'activeSelection', event.Selection))
  216. def runAsyncValidation(self):
  217. def handleHostResponse(hostState: PublicGooeyState):
  218. self.set_state(s.finishUpdate(self.state))
  219. currentState = self.fullState()
  220. self.syncExternalState(s.mergeExternalState(currentState, hostState))
  221. if not s.has_errors(self.fullState()):
  222. self.startRun()
  223. else:
  224. self.set_state(s.editScreen(_, s.show_alert(self.fullState())))
  225. def onComplete(result: Try[PublicGooeyState]):
  226. result.onSuccess(handleHostResponse)
  227. result.onError(self.handleHostError)
  228. self.set_state(s.beginUpdate(self.state))
  229. fullState = self.fullState()
  230. host.communicateFormValidation(fullState, callafter(onComplete))
  231. def runAsyncExternalOnCompleteHandler(self, was_success):
  232. def handleHostResponse(hostState):
  233. if hostState:
  234. self.syncExternalState(s.mergeExternalState(self.fullState(), hostState))
  235. def onComplete(result: Try[PublicGooeyState]):
  236. result.onError(self.handleHostError)
  237. result.onSuccess(handleHostResponse)
  238. if was_success:
  239. host.communicateSuccessState(self.fullState(), callafter(onComplete))
  240. else:
  241. host.communicateErrorState(self.fullState(), callafter(onComplete))
  242. def handleHostError(self, ex):
  243. """
  244. All async errors get pumped here where we dump out the
  245. error and they hopefully provide a lot of helpful debugging info
  246. for the user.
  247. """
  248. try:
  249. self.set_state(s.errorScreen(_, self.state))
  250. self.consoleRef.instance.appendText(str(ex))
  251. self.consoleRef.instance.appendText(str(getattr(ex, 'output', '')))
  252. self.consoleRef.instance.appendText(str(getattr(ex, 'stderr', '')))
  253. raise ex
  254. except JSONDecodeError as e:
  255. self.consoleRef.instance.appendText(deserialize_failure_explanations)
  256. except Exception as e:
  257. self.consoleRef.instance.appendText(unexpected_exit_explanations)
  258. finally:
  259. self.set_state({**self.state, 'fetchingUpdate': False})
  260. def render(self):
  261. return wsx(
  262. [c.Frame, {'title': self.buildSpec['program_name'],
  263. 'background_color': self.buildSpec['body_bg_color'],
  264. 'double_buffered': True,
  265. 'min_size': (400, 300),
  266. 'icon_uri': self.state['images']['programIcon'],
  267. 'size': self.buildSpec['default_size'],
  268. 'ref': self.frameRef},
  269. [c.Block, {'orient': wx.VERTICAL},
  270. [RHeader, self.headerprops(self.state)],
  271. [c.StaticLine, {'style': wx.LI_HORIZONTAL, 'flag': wx.EXPAND}],
  272. [ProgressSpinner, {'show': self.state['fetchingUpdate']}],
  273. [ErrorWarning, {'show': self.state['show_error_alert'],
  274. 'uri': self.state['images']['errorIcon']}],
  275. [Console, {**self.buildSpec,
  276. 'flag': wx.EXPAND,
  277. 'proportion': 1,
  278. 'show': self.state['screen'] == 'CONSOLE',
  279. 'ref': self.consoleRef}],
  280. [RTabbedLayout if self.buildSpec['navigation'] == constants.TABBED else RSidebar,
  281. {'bg_color': self.buildSpec['sidebar_bg_color'],
  282. 'label': 'Some Action!',
  283. 'tabbed_groups': self.buildSpec['tabbed_groups'],
  284. 'show_sidebar': self.state['show_sidebar'],
  285. 'ref': self.configRef,
  286. 'show': self.state['screen'] == 'FORM',
  287. 'activeSelection': self.state['activeSelection'],
  288. 'options': list(self.buildSpec['widgets'].keys()),
  289. 'on_change': self.handleSelectAction,
  290. 'config': self.buildSpec['widgets'],
  291. 'flag': wx.EXPAND,
  292. 'proportion': 1}],
  293. [c.StaticLine, {'style': wx.LI_HORIZONTAL, 'flag': wx.EXPAND}],
  294. [RFooter, self.fprops(self.state)]]]
  295. )