app.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import sys
  2. from pathlib import Path
  3. from outdated import check_outdated
  4. from PySide6.QtWidgets import (
  5. QApplication,
  6. QMenu,
  7. QPlainTextEdit,
  8. QPushButton,
  9. QStatusBar,
  10. QTabWidget,
  11. QTreeWidget,
  12. QWidget,
  13. QMessageBox,
  14. )
  15. from PySide6.QtCore import (
  16. QObject,
  17. Qt,
  18. Signal,
  19. )
  20. from PySide6.QtGui import (
  21. QAction,
  22. QCursor,
  23. )
  24. from datalad import cfg as dlcfg
  25. from datalad import __version__ as dlversion
  26. import datalad.ui as dlui
  27. from .utils import load_ui
  28. from .datalad_ui import GooeyUI
  29. from .dataladcmd_exec import GooeyDataladCmdExec
  30. from .dataladcmd_ui import GooeyDataladCmdUI
  31. from .dataset_actions import add_dataset_actions_to_menu
  32. from .fsbrowser import GooeyFilesystemBrowser
  33. from .resource_provider import gooey_resources
  34. class GooeyApp(QObject):
  35. # Mapping of key widget names used in the main window to their widget
  36. # classes. This mapping is used (and needs to be kept up-to-date) to look
  37. # up widget (e.g. to connect their signals/slots)
  38. _main_window_widgets = {
  39. 'contextTabs': QTabWidget,
  40. 'cmdTab': QWidget,
  41. 'clearLogPB': QPushButton,
  42. 'fsBrowser': QTreeWidget,
  43. 'logViewer': QPlainTextEdit,
  44. 'menuDataset': QMenu,
  45. 'menuView': QMenu,
  46. 'menuUtilities': QMenu,
  47. 'statusbar': QStatusBar,
  48. 'actionCheck_for_new_version': QAction,
  49. }
  50. execute_dataladcmd = Signal(str, dict, dict)
  51. configure_dataladcmd = Signal(str, dict)
  52. def __init__(self, path: Path = None):
  53. super().__init__()
  54. # bend datalad to our needs
  55. # we cannot handle ANSI coloring
  56. dlcfg.set('datalad.ui.color', 'off', scope='override', force=True)
  57. # set default path
  58. if not path:
  59. path = Path.cwd()
  60. self._dlapi = None
  61. self._path = path
  62. self._main_window = None
  63. self._cmdexec = GooeyDataladCmdExec()
  64. self._cmdui = GooeyDataladCmdUI(self, self.get_widget('cmdTab'))
  65. # setup UI
  66. self._fsbrowser = GooeyFilesystemBrowser(
  67. self,
  68. path,
  69. self.get_widget('fsBrowser'),
  70. )
  71. # remember what backend was in use
  72. self._prev_ui_backend = dlui.ui.backend
  73. # ask datalad to use our UI
  74. # looks silly with the uiuiuiuiui, but these are the real names ;-)
  75. dlui.KNOWN_BACKENDS['gooey'] = GooeyUI
  76. dlui.ui.set_backend('gooey')
  77. dlui.ui.ui.set_app(self)
  78. # connect the generic cmd execution signal to the handler
  79. self.execute_dataladcmd.connect(self._cmdexec.execute)
  80. # connect the generic cmd configuration signal to the handler
  81. self.configure_dataladcmd.connect(self._cmdui.configure)
  82. # when a command was configured, pass it to the executor
  83. self._cmdui.configured_dataladcmd.connect(self._cmdexec.execute)
  84. # connect execution handler signals to the setup methods
  85. self._cmdexec.execution_started.connect(self._setup_ongoing_cmdexec)
  86. self._cmdexec.execution_finished.connect(self._setup_stopped_cmdexec)
  87. self._cmdexec.execution_failed.connect(self._setup_stopped_cmdexec)
  88. # arrange for the dataset menu to populate itself lazily once
  89. # necessary
  90. self.get_widget('menuDataset').aboutToShow.connect(
  91. self._populate_dataset_menu)
  92. # connect pushbutton clicked signal to clear slot of logViewer
  93. self.get_widget('clearLogPB').clicked.connect(
  94. self.get_widget('logViewer').clear)
  95. self.main_window.actionCheck_for_new_version.triggered.connect(
  96. self._check_new_version)
  97. # reset the command configuration tab whenever the item selection in
  98. # tree view changed
  99. self._fsbrowser._tree.currentItemChanged.connect(
  100. lambda cur, prev: self._cmdui.reset_form())
  101. self._connect_menu_view(self.get_widget('menuView'))
  102. self._setup_looknfeel()
  103. def _setup_ongoing_cmdexec(self, thread_id, cmdname, cmdargs, exec_params):
  104. self.get_widget('statusbar').showMessage(f'Started `{cmdname}`')
  105. self.main_window.setCursor(QCursor(Qt.BusyCursor))
  106. def _setup_stopped_cmdexec(
  107. self, thread_id, cmdname, cmdargs, exec_params, ce=None):
  108. if ce is None:
  109. self.get_widget('statusbar').showMessage(f'Finished `{cmdname}`',
  110. timeout=1000)
  111. else:
  112. self.get_widget('statusbar').showMessage(
  113. f'`{cmdname}` failed: {ce.format_short()}')
  114. if not self._cmdexec.n_running:
  115. self.main_window.setCursor(QCursor(Qt.ArrowCursor))
  116. def deinit(self):
  117. dlui.ui.set_backend(self._prev_ui_backend)
  118. #@cached_property not available for PY3.7
  119. @property
  120. def main_window(self):
  121. if not self._main_window:
  122. self._main_window = load_ui('main_window')
  123. return self._main_window
  124. def get_widget(self, name):
  125. wgt_cls = GooeyApp._main_window_widgets.get(name)
  126. if not wgt_cls:
  127. raise ValueError(f"Unknown widget {name}")
  128. wgt = self.main_window.findChild(wgt_cls, name=name)
  129. if not wgt:
  130. # if this happens, our internal _widgets is out of sync
  131. # with the UI declaration
  132. raise RuntimeError(
  133. f"Could not locate widget {name} ({wgt_cls.__name__})")
  134. return wgt
  135. @property
  136. def rootpath(self):
  137. return self._path
  138. def _populate_dataset_menu(self):
  139. """Private slot to populate connected QMenus with dataset actions"""
  140. add_dataset_actions_to_menu(self, self._cmdui.configure, self.sender())
  141. # immediately sever the connection to avoid repopulating the menu
  142. # over and over
  143. self.get_widget('menuDataset').aboutToShow.disconnect(
  144. self._populate_dataset_menu)
  145. def _check_new_version(self):
  146. self.get_widget('statusbar').showMessage('Checking latest version')
  147. try:
  148. is_outdated, latest = check_outdated('datalad', dlversion)
  149. except ValueError:
  150. # thrown when one is in a development version (ie., more
  151. # recent than the most recent release)
  152. is_outdated = False
  153. pass
  154. if is_outdated:
  155. self.get_widget('logViewer').appendPlainText(
  156. f'Update-alert: Consider updating DataLad from '
  157. f'version {dlversion} to {latest}')
  158. else:
  159. self.get_widget('logViewer').appendPlainText(
  160. f'Your DataLad version is up to date')
  161. self.get_widget('statusbar').showMessage('Done', timeout=500)
  162. def _connect_menu_view(self, menu: QMenu):
  163. uimode = dlcfg.obtain('datalad.gooey.ui-mode')
  164. menu_intf = menu.findChild(QMenu, 'menuInterface')
  165. for a in menu_intf.actions():
  166. a.triggered.connect(self._set_interface_mode)
  167. if a.objectName().split('_')[-1] == uimode:
  168. a.setDisabled(True)
  169. def _set_interface_mode(self):
  170. action = self.sender()
  171. uimode = action.objectName().split('_')[-1]
  172. assert uimode in ('novice', 'expert')
  173. dlcfg.set('datalad.gooey.ui-mode', uimode, scope='global')
  174. QMessageBox.information(
  175. self.main_window,
  176. 'Note',
  177. 'The new interface mode is enabled at the next application start.'
  178. )
  179. def _setup_looknfeel(self):
  180. # set application icon
  181. qtapp = QApplication.instance()
  182. qtapp.setWindowIcon(gooey_resources.get_icon('app_icon_32'))
  183. # go dark, if supported
  184. try:
  185. import qdarktheme
  186. qtapp.setStyleSheet(qdarktheme.load_stylesheet('dark'))
  187. except ImportError:
  188. pass
  189. def main():
  190. qtapp = QApplication(sys.argv)
  191. gooey = GooeyApp()
  192. gooey.main_window.show()
  193. sys.exit(qtapp.exec())