class UndoAction(ActionController): history = RequiredFeature(HistoryController.name) content_ctrl: ContentController = RequiredFeature(ContentController.name) def __init__(self): ActionController.__init__(self) @property def item_config(self) -> ItemConfig: return ItemConfig().setTitle("History").setMenuPath("Edit/Undo").addSupportedData( DataType.ANY).addSupportedViewer(ViewerType.ANY).setShortcut(QtGui.QKeySequence("Ctrl+Z")) def onAction(self): id = self.content_ctrl.getCurrentId() self.history.undo(id)
class HistoryController(Controller): viewers = {} skip_next_change = False content_ctrl: ContentController = RequiredFeature(ContentController.name) def __init__(self): self.content_ctrl.subscribeViewerAdded(self.onViewerAdded) self.content_ctrl.subscribeViewerClosed(self.onViewerClosed) def undo(self, id): history = self.viewers[id] if len(history) <= 1: return None del history[-1] # change event will be triggered by undo self.skip_next_change = True self.content_ctrl.setDataModel(copy.deepcopy(history[-1]), id) def onViewerClosed(self, viewer_ctrl): del self.viewers[viewer_ctrl.v_id] def onViewerAdded(self, viewer_ctrl): self.viewers[viewer_ctrl.v_id] = [viewer_ctrl.model] self.content_ctrl.subscribeDataChanged(viewer_ctrl.v_id, self.onDataChanged) def onDataChanged(self, viewer_ctrl): if self.skip_next_change: self.skip_next_change = False return history = self.viewers[viewer_ctrl.v_id] if (len(history) > 10): del history[0] history.append(viewer_ctrl.model)
class QueryCallistoActionController(ActionController): item_config = ItemConfig().setMenuPath( "File/Open Spectrogram/Callisto/Query") content_ctrl: ContentController = RequiredFeature(ContentController.name) def onAction(self): dlg = QDialog() ui = Ui_QueryCallisto() ui.setupUi(dlg) now = QDateTime.currentDateTimeUtc() ui.start_time.setDateTime(now.addSecs(-2 * 60 * 60)) ui.end_time.setDateTime(now.addSecs(-1.50 * 60 * 60)) if dlg.exec_(): start_time = ui.start_time.dateTime().toString( QtCore.Qt.ISODate).replace("T", " ") end_time = ui.end_time.dateTime().toString( QtCore.Qt.ISODate).replace("T", " ") executeLongRunningTask( CallistoSpectrogram.from_range, [ui.instrument.currentText(), start_time, end_time], "Downloading", self._openSpectrogram) def _openSpectrogram(self, spectrogram): viewer_ctrl = CallistoViewerController.fromSpectrogram(spectrogram) self.content_ctrl.addViewerController(viewer_ctrl)
class ErrorLogTool(ToolController): app_ctrl: AppController = RequiredFeature(AppController.__name__) def __init__(self): ToolController.__init__(self) self._view = QtWidgets.QWidget() self._ui = Ui_ErrorLog() self._ui.setupUi(self._view) self._base_excepthook = sys.excepthook sys.excepthook = self._exception_hook def _exception_hook(self, exctype, value, traceback): # Print the error and traceback to console self._base_excepthook(exctype, value, traceback) # write to ui self._ui.log.append("{}: {}".format(datetime.datetime.now(), value)) # open tool self.app_ctrl.openController(self.name) @property def item_config(self) -> ItemConfig: return ItemConfig().setMenuPath("Help/Error Log").setTitle( "Error Log").setOrientation(QtCore.Qt.BottomDockWidgetArea) @property def view(self) -> QWidget: return self._view
class SaveProjectAction(ActionController): content_ctrl: ContentController = RequiredFeature(ContentController.name) @property def item_config(self) -> ItemConfig: return ItemConfig().setMenuPath("File/Save").addSupportedData( DataType.ANY).addSupportedViewer(ViewerType.ANY) def onAction(self): ctrl = self.content_ctrl.getViewerController() model = ctrl.model if model.path: file = model.path else: file, _ = QtWidgets.QFileDialog.getSaveFileName( filter="Solar Viewer Project (*.svp)") if not file: return model.path = file wrapper = ProjectSaveWrapper(type(ctrl), model) # write to file bin_file = open(file, mode="wb") pickle.dump(wrapper, bin_file) bin_file.close()
class MplToolbarController(ToolbarController): connection_ctrl: ViewerConnectionController = RequiredFeature( ViewerConnectionController.name) def __init__(self): ToolbarController.__init__(self) @property def item_config(self) -> ItemConfig: return ToolbarConfig().setMenuPath( "View/Toolbar/Default").addSupportedViewer( ViewerType.MPL).addSupportedData(DataType.ANY) def setup(self, toolbar_widget: QtWidgets.QToolBar): toolbar_widget.setOrientation(QtCore.Qt.Vertical) pan_icon = QtGui.QIcon(":/image/pan.png") pan = toolbar_widget.addAction(pan_icon, "Pan") pan.setCheckable(True) zoom_icon = QtGui.QIcon(":/image/zoom.png") zoom = toolbar_widget.addAction(zoom_icon, "Zoom") zoom.setCheckable(True) reset_icon = QtGui.QIcon(":/image/home.png") reset = toolbar_widget.addAction(reset_icon, "Reset") def f(checked, a): if checked: a.setChecked(False) pan.triggered.connect(self._onPan) zoom.triggered.connect(self._onZoom) reset.triggered.connect(self._onReset) self.pan_action = pan self.zoom_action = zoom def onClose(self): if self.pan_action.isChecked() or self.zoom_action.isChecked(): self.connection_ctrl.remove_lock() def _onPan(self): if not self.pan_action.isChecked(): self.connection_ctrl.remove_lock() return pan_lock = _PanLock(self.item_config, self.pan_action) self.connection_ctrl.add_lock(pan_lock) def _onZoom(self): if not self.zoom_action.isChecked(): self.connection_ctrl.remove_lock() return zoom_lock = _ZoomLock(self.item_config, self.zoom_action) self.connection_ctrl.add_lock(zoom_lock) def _onReset(self): view = self.content_ctrl.getViewer() view.toolbar.home()
class SaveImageController(ActionController): content_ctrl: ContentController = RequiredFeature(ContentController.name) @property def item_config(self) -> ItemConfig: return ItemConfig().setMenuPath("File/Export/Image").addSupportedData( DataType.ANY).addSupportedViewer(ViewerType.MPL) def onAction(self): ui = Ui_SaveImage() dlg = QtWidgets.QDialog() ui.setupUi(dlg) ui.dpi_spin.setEnabled(False) view = self.content_ctrl.getViewerController().view figure = view.figure canvas = view.canvas filters, selectedFilter = self.getFilter(canvas) def selectFile(): path = \ QFileDialog.getSaveFileName(directory=ui.file_path.text(), filter=filters, initialFilter=selectedFilter)[0] if path: ui.file_path.setText(path) ui.file_select.clicked.connect(selectFile) startpath = os.path.expanduser( matplotlib.rcParams['savefig.directory']) start = os.path.join(startpath, canvas.get_default_filename()) ui.file_path.setText(start) if not dlg.exec_(): return path = ui.file_path.text() dpi = ui.dpi_spin.value() if ui.dpi_check.isChecked() else None figure.savefig(path, dpi=dpi, transparent=ui.transparent_check.isChecked()) def getFilter(self, canvas): filetypes = canvas.get_supported_filetypes_grouped() sorted_filetypes = [(k, v) for k, v in filetypes.items()] sorted_filetypes.sort() default_filetype = canvas.get_default_filetype() filters = [] selectedFilter = None for name, exts in sorted_filetypes: exts_list = " ".join(['*.%s' % ext for ext in exts]) filter = '%s (%s)' % (name, exts_list) if default_filetype in exts: selectedFilter = filter filters.append(filter) filters = ';;'.join(filters) return filters, selectedFilter
class SpectraToolbarController(ToolbarController): item_config = ToolbarConfig().addSupportedViewer(ViewerType.ANY).addSupportedData(DataType.SPECTROGRAM).setMenuPath( "View/Toolbar/Spectra").setOrientation(QtCore.Qt.TopToolBarArea) content_ctrl: ContentController = RequiredFeature(ContentController.name) def setup(self, toolbar_widget: QtWidgets.QToolBar): left_icon = QtGui.QIcon(":/image/double_left.png") right_icon = QtGui.QIcon(":/image/double_right.png") range_icon = QtGui.QIcon(":/image/range.png") add_icon = QtGui.QIcon(":/image/add.png") extend_start = toolbar_widget.addAction(left_icon, "Extend Start") extend_end = toolbar_widget.addAction(right_icon, "Extend End") range = toolbar_widget.addAction(range_icon, "Set Range") add = toolbar_widget.addAction(add_icon, "Add File") extend_start.triggered.connect(self.onExtendStart) extend_end.triggered.connect(self.onExtendEnd) range.triggered.connect(self.onSetRange) add.triggered.connect(self.onAddFile) def onExtendStart(self): v_id = self.content_ctrl.getViewerController().v_id executeLongRunningTask(self._extendAction, [-15], "Downloading", self.content_ctrl.setDataModel, [v_id]) def onExtendEnd(self): v_id = self.content_ctrl.getViewerController().v_id executeLongRunningTask(self._extendAction, [15], "Downloading", self.content_ctrl.setDataModel, [v_id]) def onSetRange(self): v_id = self.content_ctrl.getViewerController().v_id model: CallistoModel = self.content_ctrl.getDataModel() dlg = _RangeDialog(model.spectrogram.start, model.spectrogram.end) if dlg.exec_(): model_c = copy.deepcopy(model) model_c.spectrogram = model.spectrogram.in_interval(dlg.getStartTime(), dlg.getEndTime()) self.content_ctrl.setDataModel(model_c, v_id) def onAddFile(self): v_id = self.content_ctrl.getViewerController().v_id paths, _ = QtWidgets.QFileDialog.getOpenFileNames(None, caption="Select Extend File", filter="FITS files (*.fits; *.fit; *.fts)") if paths: model: CallistoModel = copy.deepcopy(self.content_ctrl.getDataModel()) spectra = [CallistoSpectrogram.read(p) for p in paths] spectra.append(model.spectrogram) model.spectrogram = CallistoSpectrogram.join_many(spectra) self.content_ctrl.setDataModel(model, v_id) def _extendAction(self, minutes): model: CallistoModel = copy.deepcopy(self.content_ctrl.getDataModel()) model.spectrogram = model.spectrogram.extend(minutes) return model def onClose(self): pass
class SaveFitsAction(ActionController): content_ctrl: ContentController = RequiredFeature(ContentController.name) @property def item_config(self) -> ItemConfig: return ItemConfig().setMenuPath("File/Export/FITS").addSupportedData( DataType.MAP).addSupportedViewer(ViewerType.ANY) def onAction(self): map: GenericMap = self.content_ctrl.getDataModel().map saveFits(map)
class CreateCompositeMapTool(ActionController): content_ctrl: ContentController = RequiredFeature(ContentController.name) @property def item_config(self) -> ItemConfig: return ItemConfig().setMenuPath( "File/Open SunPy Composite Map/From Active") def onAction(self): dlg = QtWidgets.QDialog() ui = Ui_OpenComposite() ui.setupUi(dlg) model = QStandardItemModel() viewer_ctrls = self.content_ctrl.getViewerControllers(DataType.MAP) for v in viewer_ctrls: item = QStandardItem("{}: {}".format(v.v_id, v.getTitle())) item.setCheckable(True) item.v_id = v.v_id model.appendRow(item) ui.list.setModel(model) if not dlg.exec_(): return map_models = self._getSelectedDataModels(model) if len(map_models) == 0: return model = self._createModel(map_models) ctrl = CompositeMapViewerController.fromModel(model) self.content_ctrl.addViewerController(ctrl) def _createModel(self, models): # preserve plot settings for i, model in enumerate(models): settings = { "cmap": model.cmap, "norm": model.norm, "interpolation": model.interpolation, "origin": model.origin } model.map.plot_settings = settings comp_model = CompositeMapModel([model.map for model in models]) return comp_model def _getSelectedDataModels(self, model): checked_ids = [] for index in range(model.rowCount()): if model.item(index).checkState() == QtCore.Qt.Checked: checked_ids.append(model.item(index).v_id) models = [self.content_ctrl.getDataModel(v_id) for v_id in checked_ids] return models
class MPLCoordinatesMixin: status_bar_ctrl: StatusBarController = RequiredFeature(StatusBarController.name) def __init__(self): # add coordinates of mouse courser to status bar self.view.canvas.mpl_connect('motion_notify_event', self.onMapMotion) def onMapMotion(self, event): if event.inaxes: message = event.inaxes.format_coord(event.xdata, event.ydata) self.status_bar_ctrl.setText(message)
class TestIoC(unittest.TestCase): feature: Object = RequiredFeature("TEST1") method: Object = RequiredFeature("TEST2", HasMethods("required_method")) attr: Object = RequiredFeature("TEST3", HasAttributes("required_attribute")) features.Provide("1", "1") features.Provide("2", "2") features.Provide("3", "3") features: List[str] = MatchingFeatures(IsInstanceOf(str)) def test_provide(self): mock = Object() features.Provide("TEST1", mock) self.feature.test() mock.test.assert_called_once() def test_has_method(self): mock = Object() mock.required_method = Mock() features.Provide("TEST2", mock) self.method.required_method() mock.required_method.assert_called_once() def test_has_attribute(self): mock = Object() mock.required_attribute = "TEST3" features.Provide("TEST3", mock) self.assertEqual(self.attr.required_attribute, "TEST3") def test_matching(self): self.assertEqual(["1", "2", "3"], self.features)
class SNRController(ActionController): content_ctrl: ContentController = RequiredFeature(ContentController.name) @property def item_config(self) -> ItemConfig: return ItemConfig().setMenuPath( "Help/Calculate SNR").addSupportedViewer( ViewerType.MPL).addSupportedData(DataType.MAP) def onAction(self): data = self.content_ctrl.getViewerController().getZoomedData() snr = np.mean(data) / np.std(data) message = "Estimated SNR: {0:.7}".format(float(snr)) QtWidgets.QMessageBox.information(None, "SNR", message)
class SaveSpectraFitsAction(ActionController): content_ctrl: ContentController = RequiredFeature(ContentController.name) @property def item_config(self) -> ItemConfig: return ItemConfig().setMenuPath("File/Export/FITS").addSupportedData( DataType.SPECTROGRAM).addSupportedViewer(ViewerType.ANY) def onAction(self): spec: CallistoSpectrogram = self.content_ctrl.getDataModel( ).spectrogram name, _ = QtWidgets.QFileDialog.getSaveFileName( filter=getExtensionString(FileType.FITS.value)) if name: spec.save(name)
class ActionManager: content_ctrl: ContentController = RequiredFeature(ContentController.name) def __init__(self, action: QtWidgets.QAction): self.action = action action.triggered.connect(self._onTriggered) action.setEnabled(False) self.controllers = {} self._active_ctrl = None self._shortcut = False self.content_ctrl.subscribeViewerChanged(self._checkActionSupported) def register(self, ctrl: Controller, action, parent): self.controllers[ctrl] = action self._checkActionSupported(self.content_ctrl.getViewerController()) self._registerShortcut(ctrl, parent) def _registerShortcut(self, ctrl, parent): if self._shortcut is not False: assert ctrl.item_config.shortcut == self._shortcut, \ "invalid configuration. different shortcuts for same action encountered." return if not ctrl.item_config.shortcut: self._shortcut = None else: self._shortcut = ctrl.item_config.shortcut QShortcut( ctrl.item_config.shortcut, parent, lambda: self.action.trigger() if self.action.isEnabled() else None) def _checkActionSupported(self, vc: ViewerController): dt = vc.data_type if vc else None vt = vc.viewer_type if vc else None enabled = False for c in self.controllers.keys(): if supported(dt, vt, c.item_config.supported_data_types, c.item_config.supported_viewer_types): enabled = True self._active_ctrl = c break self.action.setEnabled(enabled) def _onTriggered(self, checked): self.controllers[self._active_ctrl]()
class DataActionController(ActionController): """Base class for actions related to the currently viewed data""" content_ctrl = RequiredFeature(ContentController.name) def onAction(self): data_model = self.content_ctrl.getDataModel() call_after = lambda result: self.content_ctrl.setDataModel(result) executeLongRunningTask(self._action, [data_model], "Action in Progress", call_after) def _action(self, data_model): data_copy = copy.deepcopy(data_model) modified = self.modifyData(data_copy) return modified @abstractmethod def modifyData(self, data_model: DataModel) -> DataModel: raise NotImplementedError
class OpenProjectAction(ActionController): content_ctrl: ContentController = RequiredFeature(ContentController.name) @property def item_config(self) -> ItemConfig: return ItemConfig().setMenuPath("File/Open SV Project") def onAction(self): file, _ = QtWidgets.QFileDialog.getOpenFileName( filter="Solar Viewer Project (*.svp)") if not file: return bin_file = open(file, mode="rb") wrapper = pickle.load(bin_file) ctrl = wrapper.viewer_ctrl_type.fromModel(wrapper.model) self.content_ctrl.addViewerController(ctrl) bin_file.close()
class DialogController(Controller): """Base class for dialog items""" content_ctrl = RequiredFeature(content_ctrl_name) def __init__(self): self._dlg_view = QtWidgets.QDialog() self._dlg_view.setWindowTitle(self.item_config.title) self._dlg_ui = Ui_Dialog() self._dlg_ui.setupUi(self._dlg_view) self.setupContent(self._dlg_ui.content) self._dlg_ui.button_box.accepted.connect(self._onOk) self._dlg_ui.button_box.rejected.connect(self._onCancel) @property @abstractmethod def item_config(self) -> ItemConfig: raise NotImplementedError @abstractmethod def setupContent(self, content_widget): raise NotImplementedError @abstractmethod def onDataChanged(self, viewer_ctrl: ViewerController): raise NotImplementedError @abstractmethod def modifyData(self, data_model: DataModel) -> DataModel: raise NotImplementedError @property def view(self) -> QtWidgets.QDialog: viewer_ctrl = self.content_ctrl.getViewerController() self.onDataChanged(viewer_ctrl) return self._dlg_view def _onOk(self): viewer_ctrl = self.content_ctrl.getViewerController() model = self.modifyData(copy.deepcopy(viewer_ctrl.model)) self.content_ctrl.setDataModel(model) def _onCancel(self): pass
class CutController(ActionController): content_ctrl: ContentController = RequiredFeature(ContentController.name) def onAction(self): viewer_ctrl = self.content_ctrl.getViewerController() call_after = lambda result: self.content_ctrl.setDataModel(result) executeLongRunningTask(self._action, [viewer_ctrl], "Executing Action", call_after) def _action(self, viewer_ctrl): submap = viewer_ctrl.getZoomedMap() data_copy = copy.deepcopy(viewer_ctrl.model) data_copy.map = submap return data_copy @property def item_config(self) -> ItemConfig: return ItemConfig().setMenuPath( "Edit/Crop To Current View").addSupportedViewer( ViewerType.MPL).addSupportedData(DataType.MAP)
class ViewerToolController(ToolController, ConnectionMixin): """Base class for viewer aware tool controllers""" connection_ctrl: ViewerConnectionController = RequiredFeature( ViewerConnectionController.name) def __init__(self): ToolController.__init__(self) self._tool_view = QWidget() self._tool_ui = Ui_ViewerTool() self._tool_ui.setupUi(self._tool_view) self._sub_id = None self.setupContent(self._tool_ui.content) self._tool_ui.content.resizeEvent = lambda evt: self._tool_ui.scrollArea.setMinimumWidth( self._tool_ui.content.sizeHint().width() + self._tool_ui.scrollArea .verticalScrollBar().sizeHint().width()) def supports(self, viewer_ctrl: ViewerController) -> bool: return viewer_ctrl is not None and supported( viewer_ctrl.data_type, viewer_ctrl.viewer_type, self.item_config.supported_data_types, self.item_config.supported_viewer_types) def enabled(self, value: bool): self._tool_view.setEnabled(value) @abstractmethod def setupContent(self, content_widget): raise NotImplementedError @property def view(self) -> QtWidgets.QWidget: self._sub_id = self.connection_ctrl.subscribe(self) self._tool_view.closeEvent = self._onClose return self._tool_view def _onClose(self, *args): self.connection_ctrl.unsubscribe(self._sub_id)
class ViewerConnectionController(Controller): """ Main controller for connection handling to the active viewer. """ content_ctrl: ContentController = RequiredFeature(ContentController.name) def __init__(self): self.subscribers: Dict[int, ConnectionMixin] = {} self.connections = {} self.sub_vc_id = None self.sub_id = 0 self.lock: ViewerLock = None self.active_id = -1 self.active_sub: ConnectionMixin = None self.content_ctrl.subscribeViewerChanged(self._connect) self.content_ctrl.subscribeViewerClosed(self._closed) def subscribe(self, sub: ConnectionMixin): """ Subscribe a controller to the connection managing :param sub: The subscribed controller :return: The unique connection id """ self.sub_id += 1 self.subscribers[self.sub_id] = sub if self.lock: self.lock.release() self.lock = None self._connect(self.content_ctrl.getViewerController()) return self.sub_id def unsubscribe(self, s_id): """ Removes the subscription from the connection managing and stops active connections of this subscription. :param s_id: The unique connection id :return: None """ self.subscribers.pop(s_id) self._connect(self.content_ctrl.getViewerController(self.active_id)) def add_lock(self, lock: ViewerLock): """ Adds a viewer lock. This disconnects currently active lock. :param lock: The viewer lock implementation :return: None """ if self.lock: self.lock.release() self.lock = lock self._connect(self.content_ctrl.getViewerController()) def remove_lock(self): """ Removes the active viewer lock. :return: None """ if self.lock: self.lock.release() self.lock = None self._connect(self.content_ctrl.getViewerController()) def _connect(self, viewer_ctrl: ViewerController): self._disconnectActive() self.active_id = viewer_ctrl.v_id if viewer_ctrl else -1 if self.active_id != -1: self.active_sub = self._getFirstSupported(viewer_ctrl) if self.active_sub: self.active_sub.connect(viewer_ctrl) self._setEnabled() def _closed(self, viewer_ctrl: ViewerController): if not self.active_id == viewer_ctrl.v_id: return self.active_sub = None def _setEnabled(self): for sub in self.subscribers.values(): sub.enabled(sub is self.active_sub) def _disconnectActive(self): if self.active_sub is None: return self.active_sub.disconnect( self.content_ctrl.getViewerController(self.active_id)) self.active_sub = None def _getFirstSupported(self, viewer_ctrl): if self.lock and self.lock.supports(viewer_ctrl): return self.lock for sub in reversed(list(self.subscribers.values())): if sub.supports(viewer_ctrl): return sub return None
class ToolbarController(Controller): """Base class for toolbars""" content_ctrl: ContentController = RequiredFeature(content_ctrl_name) def __init__(self): self._sub_id = None self._tab_sub_id = None @property @abstractmethod def item_config(self) -> ToolbarConfig: raise NotImplementedError @abstractmethod def setup(self, toolbar_widget: QtWidgets.QToolBar): raise NotImplementedError @abstractmethod def onClose(self): pass def supports(self, viewer_ctrl: ViewerController) -> bool: return viewer_ctrl is not None and supported( viewer_ctrl.data_type, viewer_ctrl.viewer_type, self.item_config.supported_data_types, self.item_config.supported_viewer_types) @property def view(self) -> QtWidgets.QToolBar: self._toolbar_view = QtWidgets.QToolBar() self.setup(self._toolbar_view) viewer_ctrl = self.content_ctrl.getViewerController() self._onTabChanged(viewer_ctrl) self._tab_sub_id = self.content_ctrl.subscribeViewerChanged( self._onTabChanged) self._toolbar_view.closeEvent = self._onClose return self._toolbar_view def _onTabChanged(self, viewer_ctrl: ViewerController): if self._sub_id is not None: self.content_ctrl.unsubscribe(self._sub_id) if viewer_ctrl is None: self._sub_id = None else: self._sub_id = self.content_ctrl.subscribeDataChanged( viewer_ctrl.v_id, self._onDataChanged) self._onDataChanged(viewer_ctrl) def _onClose(self, *args): self.onClose() self.content_ctrl.unsubscribe(self._tab_sub_id) if self._sub_id: self.content_ctrl.unsubscribe(self._sub_id) def _onDataChanged(self, viewer_ctrl): if viewer_ctrl is None or not supported( viewer_ctrl.data_type, viewer_ctrl.viewer_type, self.item_config.supported_data_types, self.item_config.supported_viewer_types): self._toolbar_view.setEnabled(False) else: self._toolbar_view.setEnabled(True)
class DataToolController(ToolController): """Base class for tool items with relation to the currently viewed data""" content_ctrl: ContentController = RequiredFeature(content_ctrl_name) def __init__(self): ToolController.__init__(self) self._tool_view = QWidget() self._tool_ui = Ui_DataTool() self._tool_ui.setupUi(self._tool_view) self._sub_id = None self._tab_sub_id = None self._v_id = None self.setupContent(self._tool_ui.content) self._tool_ui.content.resizeEvent = lambda evt: self._tool_ui.scrollArea.setMinimumWidth( self._tool_ui.content.sizeHint().width() + self._tool_ui.scrollArea .verticalScrollBar().sizeHint().width()) apply_btn = self._tool_ui.button_box.button(QDialogButtonBox.Apply) apply_btn.clicked.connect(self._onApply) @abstractmethod def setupContent(self, content_widget: QWidget): raise NotImplementedError @abstractmethod def onDataChanged(self, viewer_ctrl): """Triggered action for data changes (switched tab, modified data)""" raise NotImplementedError @abstractmethod def modifyData(self, data_model: DataModel) -> DataModel: """ Triggered apply action. :param data_model: The selected data model :return: The modified data model """ raise NotImplementedError @property def view(self) -> QtWidgets: viewer_ctrl = self.content_ctrl.getViewerController() self._onTabChanged(viewer_ctrl) self._tab_sub_id = self.content_ctrl.subscribeViewerChanged( self._onTabChanged) self._tool_view.closeEvent = self._onClose return self._tool_view def _onApply(self): if not self._v_id: return self._tool_ui.message_box.hide() self._tool_ui.button_box.setEnabled(False) data_model = self.content_ctrl.getDataModel(self._v_id) executeTask(self._apply, [data_model], self._onResult) def _apply(self, data_model): try: data_copy = copy.deepcopy(data_model) result = self.modifyData(data_copy) return result if result else data_copy except Exception as ex: return ex def _onResult(self, result): self._tool_ui.button_box.setEnabled(True) if isinstance(result, Exception): self._tool_ui.message_box.showMessage(str(result)) return self.content_ctrl.setDataModel(result) def _onTabChanged(self, viewer_ctrl: ViewerController): if self._sub_id is not None: self.content_ctrl.unsubscribe(self._sub_id) if viewer_ctrl is None: self._sub_id = None self._v_id = None else: self._sub_id = self.content_ctrl.subscribeDataChanged( viewer_ctrl.v_id, self._handleDataChanged) self._v_id = viewer_ctrl.v_id self._handleDataChanged(viewer_ctrl) def _onClose(self, *args): self.content_ctrl.unsubscribe(self._tab_sub_id) if self._sub_id: self.content_ctrl.unsubscribe(self._sub_id) def _handleDataChanged(self, viewer_ctrl): if viewer_ctrl is None or not supported( viewer_ctrl.data_type, viewer_ctrl.viewer_type, self.item_config.supported_data_types, self.item_config.supported_viewer_types): self._tool_view.setEnabled(False) else: self._tool_view.setEnabled(True) self.onDataChanged(viewer_ctrl)
class DialogController(Controller): """Base class for dialog items.""" content_ctrl = RequiredFeature(content_ctrl_name) def __init__(self): self._dlg_view = QtWidgets.QDialog() self._dlg_view.setWindowTitle(self.item_config.title) self._dlg_ui = Ui_Dialog() self._dlg_ui.setupUi(self._dlg_view) self.setupContent(self._dlg_ui.content) self._dlg_ui.button_box.accepted.connect(self._onOk) self._dlg_ui.button_box.rejected.connect(self._onCancel) @property @abstractmethod def item_config(self) -> ItemConfig: """ Create the Configuration. Responsible for the representation in the application. :return: the menu item configuration :rtype: ItemConfig """ raise NotImplementedError @abstractmethod def setupContent(self, content_widget): """ Internal basic UI setup function. Use the UI setup file here. :param content_widget: The Qt parent widget. :type: QWidget """ raise NotImplementedError @abstractmethod def onDataChanged(self, viewer_ctrl: ViewerController): """ Triggered when a new viewer is selected. Only supported data/viewer types need to be handled. :param viewer_ctrl: The new viewer controller. :type viewer_ctrl: ViewerController """ raise NotImplementedError @abstractmethod def modifyData(self, data_model: DataModel) -> DataModel: """ Triggered action on dialog accept. Execute action on data model. :param data_model: The data model to modify. :type data_model: DataModel :return: The modified data model. :rtype: DataModel """ raise NotImplementedError @property def view(self) -> QtWidgets.QDialog: """ Returns the dialog view. :return: The dialog widget. :rtype: QDialog """ viewer_ctrl = self.content_ctrl.getViewerController() self.onDataChanged(viewer_ctrl) return self._dlg_view def _onOk(self): """Triggered ok action""" viewer_ctrl = self.content_ctrl.getViewerController() model = self.modifyData(copy.deepcopy(viewer_ctrl.model)) self.content_ctrl.setDataModel(model) def _onCancel(self): """Triggered cancel action""" pass
class AppController(QtWidgets.QMainWindow): content_ctrl: ContentController = RequiredFeature(content_ctrl_name) status_bar_ctrl: StatusBarController = RequiredFeature( StatusBarController.name) viewers = RequiredFeature(viewers_name) tool_ctrls: List[ToolController] = MatchingFeatures( IsInstanceOf(ToolController)) dlg_ctrls: List[DialogController] = MatchingFeatures( IsInstanceOf(DialogController)) action_ctrls: List[ActionController] = MatchingFeatures( IsInstanceOf(ActionController)) toolbar_ctrls: List[ToolbarController] = MatchingFeatures( IsInstanceOf(ToolbarController)) def __init__(self, parent=None): QtWidgets.QMainWindow.__init__(self, parent) self.ui = Ui_MainWindow() self.ui.setupUi(self) self.active_tools = {} self.active_toolbars = {} self.ui.horizontalLayout.addWidget(self.content_ctrl.view) self._initIcon() self._initViewers() self._initTools() self._initDialogs() self._initActions() self._initToolbars() self.setStatusBar(self.status_bar_ctrl.view) self.ui.default_toolbar.trigger() self.ui.actionQuit.triggered.connect(lambda evt: self.close()) def openController(self, controller_name: str): """ Opens the controller of the given name in the application :param controller_name: the class name of the controller to open (tool, dialog, action or toolbar) :return: """ if controller_name in self.active_toolbars or controller_name in self.active_tools: return for ctrl in self.tool_ctrls: if ctrl.name == controller_name: self._toggleTool(ctrl) return for ctrl in self.dlg_ctrls: if ctrl.name == controller_name: self._openDialog(ctrl) return for ctrl in self.action_ctrls: if ctrl.name == controller_name: ctrl.onAction() return for ctrl in self.toolbar_ctrls: if ctrl.name == controller_name: self._toggleToolbar(ctrl) return def _openViewer(self, ctrl: ViewerController): extensions = getExtensionString(ctrl.viewer_config.file_types) files, _ = QFileDialog.getOpenFileNames(None, "Open File", "", extensions) if not files: return if ctrl.viewer_config.multi_file: viewer = ctrl.fromFile(files) self.content_ctrl.addViewerController(viewer) return for f in files: viewer = ctrl.fromFile(f) self.content_ctrl.addViewerController(viewer) def _toggleTool(self, ctrl: ToolController, action=None): if ctrl.name not in self.active_tools: dock = QtWidgets.QDockWidget(ctrl.item_config.title) content = ctrl.view dock.setWidget(content) self._setCloseAction(action, content, dock, ctrl.name) self.addDockWidget(ctrl.item_config.orientation, dock) self.active_tools[ctrl.name] = dock else: self.active_tools.pop(ctrl.name).close() def _setCloseAction(self, action, content, dock, name): def f(evt, a=action, c=content, n=name): if a: a.setChecked(False) self.active_tools.pop(n, None) c.close() dock.closeEvent = f def _toggleToolbar(self, ctrl: ToolbarController): if ctrl.name not in self.active_toolbars: tool_bar = ctrl.view self.addToolBar(QtCore.Qt.RightToolBarArea, tool_bar) self.active_toolbars[ctrl.name] = tool_bar else: self.active_toolbars.pop(ctrl.name).close() def _openDialog(self, dlg_ctrl: DialogController): dlg = dlg_ctrl.view dlg.exec_() def _initIcon(self): icon = QtGui.QIcon() icon.addPixmap(QtGui.QPixmap(":/image/icon.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.setWindowIcon(icon) def _initViewers(self): for v_ctrl in self.viewers: tree = v_ctrl.viewer_config.menu_path.split("/") if len(tree) == 1: continue action = InitUtil.getAction(tree, self.ui.menubar) action.triggered.connect( lambda evt, c=v_ctrl: installMissingAndExecute( c.viewer_config.required_pkg, self._openViewer, [c])) def _initTools(self): for ctrl in self.tool_ctrls: tree = ctrl.item_config.menu_path.split("/") if len(tree) == 1: continue action = InitUtil.getAction(tree, self.ui.menubar, True) action.triggered.connect( lambda evt, c=ctrl, a=action: self._toggleTool(c, a)) if ctrl.item_config.shortcut: QShortcut( ctrl.item_config.shortcut, self, lambda: action.trigger if action.isEnabled() else None) def _initDialogs(self): for ctrl in self.dlg_ctrls: tree = ctrl.item_config.menu_path.split("/") if len(tree) == 1: continue action = InitUtil.getAction(tree, self.ui.menubar) action.triggered.connect(lambda evt, c=ctrl: self._openDialog(c)) self._subscribeItemSupportCheck(action, ctrl) if ctrl.item_config.shortcut: QShortcut(ctrl.item_config.shortcut, self, lambda a=action: a.trigger() if action.isEnabled() else None) def _initActions(self): for ctrl in self.action_ctrls: tree = ctrl.item_config.menu_path.split("/") if len(tree) == 1: continue action = InitUtil.getAction(tree, self.ui.menubar) action.triggered.connect(ctrl.onAction) self._subscribeItemSupportCheck(action, ctrl) if ctrl.item_config.shortcut: QShortcut(ctrl.item_config.shortcut, self, lambda a=action: a.trigger() if action.isEnabled() else None) def _initToolbars(self): for ctrl in self.toolbar_ctrls: tree = ctrl.item_config.menu_path.split("/") if len(tree) == 1: continue action = InitUtil.getAction(tree, self.ui.menubar, checkable=True) action.triggered.connect( lambda checked, c=ctrl: self._toggleToolbar(c)) def _subscribeItemSupportCheck(self, action, ctrl): def f(vc: ViewerController, a=action, c=ctrl): dt = vc.data_type if vc else None vt = vc.viewer_type if vc else None enabled = supported(dt, vt, c.item_config.supported_data_types, c.item_config.supported_viewer_types) a.setEnabled(enabled) self.content_ctrl.subscribeViewerChanged(f) f(None) # initial
class DataManagerController(ToolController): content_ctrl: ContentController = RequiredFeature(ContentController.name) def __init__(self): self._view = QtWidgets.QWidget() self._ui = Ui_DataManager() self._ui.setupUi(self._view) self._ui.refresh_button.setIcon(self._view.style().standardIcon( QStyle.SP_BrowserReload)) self._ui.remove_button.setIcon(self._view.style().standardIcon( QStyle.SP_DialogNoButton)) self._ui.add_button.setIcon(self._view.style().standardIcon( QStyle.SP_DialogYesButton)) self.dlg = DataManagerFilterDialog() db = QtSql.QSqlDatabase.addDatabase('QSQLITE') db.setDatabaseName( sunpy.config.get("database", "url").replace("sqlite:///", "")) model = QtSql.QSqlTableModel() model.setTable("data") model.setEditStrategy(QtSql.QSqlTableModel.OnFieldChange) model.select() self._ui.data_table.setModel(model) self.initTableHeader(model) self._ui.add_button.clicked.connect(lambda x: self.onAdd()) self._ui.remove_button.clicked.connect(lambda x: self.onRemove()) self._ui.open_button.clicked.connect(lambda x: self.onOpen()) self._ui.refresh_button.clicked.connect(lambda x: self.model.select()) self._ui.filter_button.clicked.connect(lambda x: self.onFilter()) self.sunpy_db = Database() self.model = model def initTableHeader(self, model): header = self._ui.data_table.horizontalHeader() for i in range(model.columnCount()): header.setSectionResizeMode(i, QtWidgets.QHeaderView.ResizeToContents) header.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) header.customContextMenuRequested.connect(self.onHeaderMenu) self._ui.data_table.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self._ui.data_table.customContextMenuRequested.connect( self.onHeaderMenu) self._ui.data_table.hideColumn(0) self._ui.data_table.hideColumn(2) self._ui.data_table.hideColumn(3) self._ui.data_table.hideColumn(4) self._ui.data_table.hideColumn(8) self._ui.data_table.hideColumn(11) self._ui.data_table.hideColumn(12) self._ui.data_table.hideColumn(13) self._ui.data_table.hideColumn(14) def onAdd(self): paths, _ = QtWidgets.QFileDialog.getOpenFileNames( None, filter="FITS files (*.fits; *.fit; *.fts)") for p in paths: self.sunpy_db.add_from_file(p) self.sunpy_db.commit() self.model.select() def onRemove(self): rows = set([i.row() for i in self._ui.data_table.selectedIndexes()]) for r in rows: self.model.removeRow(r) self.model.submitAll() self.model.select() def onOpen(self): rows = set([i.row() for i in self._ui.data_table.selectedIndexes()]) paths = [ self.model.index(row, self.model.fieldIndex("path")).data() for row in rows ] for path in paths: viewer_ctrl = MapViewerController.fromFile(path) self.content_ctrl.addViewerController(viewer_ctrl) def onFilter(self): if self.dlg.exec_(): self.model.setFilter(self.dlg.getFilter()) self.model.select() def onHeaderMenu(self, point): menu = QMenu(self._view) actions = [] for column in range(self.model.columnCount()): label = self.model.headerData(column, QtCore.Qt.Horizontal) action = QtWidgets.QAction(label) action.setCheckable(True) action.setChecked(not self._ui.data_table.isColumnHidden(column)) event = lambda checked, c=column: self._ui.data_table.showColumn( c) if checked else self._ui.data_table.hideColumn(c) action.triggered.connect(event) actions.append(action) menu.addActions(actions) menu.exec_(QCursor.pos()) @property def item_config(self) -> ItemConfig: return ItemConfig().setMenuPath("File/Data Manager").setTitle( "Data Manager") @property def view(self) -> QWidget: self.model.select() return self._view
class EventController(ToolController): # result_ctrl: DownloadResultController = RequiredFeature(DownloadResultController.name) def __init__(self): self.client = HEKClient() self.query_id = 0 ToolController.__init__(self) self._view = QtWidgets.QWidget() self._ui = Ui_DownloadEvent() self._ui.setupUi(self._view) self._ui.message_box.hide() self.table = self._ui.table self._ui.event_type.addItems([c().item.upper() for c in hek.attrs.EventType.__subclasses__()]) header = self.table.horizontalHeader() for i in range(self.table.columnCount()): header.setSectionResizeMode(i, QtWidgets.QHeaderView.ResizeToContents) now = QDateTime.currentDateTimeUtc() self._ui.from_date.setDateTime(now.addSecs(-2 * 60 * 60)) self._ui.to_date.setDateTime(now.addSecs(-1.50 * 60 * 60)) self._ui.search_button.clicked.connect(self._onSearch) self._ui.query_button.clicked.connect(self._onQuery) @property def item_config(self) -> ItemConfig: return ItemConfig().setTitle("Event Download Tool").setMenuPath("File/HEK") @property def view(self) -> QtWidgets: return self._view def _onSearch(self): self._ui.message_box.hide() try: self._ui.search_button.setEnabled(False) self._ui.search_button.setText("Loading...") self.table.setRowCount(0) start = self._ui.from_date.dateTime().toString(QtCore.Qt.ISODate) end = self._ui.to_date.dateTime().toString(QtCore.Qt.ISODate) event = self._ui.event_type.currentText() attrs = [hek.attrs.Time(start=start, end=end), hek.attrs.EventType(event)] executeTask(self.client.search, attrs, self._onSearchResult) except Exception as ex: self._ui.message_label.setText("Invalid Query: " + str(ex)) self._ui.message_box.show() def _onQuery(self): indexes = self.table.selectedIndexes() if len(indexes) == 0: return vso_query = hek2vso.translate_results_to_query(self.query[indexes[0].row()]) self.result_ctrl.query(vso_query[0]) def _onSearchResult(self, query): self.query = query self.table.setRowCount(len(query)) for index, item in enumerate(query): location = self._createLocation(item) self.table.setItem(index, 0, QtWidgets.QTableWidgetItem(item["event_type"])) self.table.setItem(index, 1, QtWidgets.QTableWidgetItem(item["event_starttime"])) self.table.setItem(index, 2, QtWidgets.QTableWidgetItem(item["event_endtime"])) self.table.setItem(index, 3, QtWidgets.QTableWidgetItem(location)) self.table.setItem(index, 4, QtWidgets.QTableWidgetItem(item["obs_observatory"])) self.table.setItem(index, 5, QtWidgets.QTableWidgetItem(item["obs_instrument"])) self.table.setItem(index, 6, QtWidgets.QTableWidgetItem(item["obs_channelid"])) self.table.setItem(index, 7, QtWidgets.QTableWidgetItem(item["frm_name"])) self._ui.search_button.setEnabled(True) self._ui.search_button.setText("Search") def _createLocation(self, item): event_coordunit = item["event_coordunit"] if not event_coordunit: return "" location = "( " if item["event_coord1"] is not None: location += str(item["event_coord1"]) if item["event_coord2"] is not None: location += ", " + str(item["event_coord2"]) if item["event_coord3"] is not None: location += ", " + str(item["event_coord3"]) location += " ) " + event_coordunit return location
class DownloadResultController(ToolController): queries = {} app_ctrl: AppController = RequiredFeature(AppController.__name__) content_ctrl: ContentController = RequiredFeature(ContentController.name) def __init__(self): self._view = QtWidgets.QWidget() self._ui = Ui_DownloadResult() self._ui.setupUi(self._view) self._ui.tabs.clear() self._ui.tabs.tabCloseRequested.connect(self._onRemoveTab) self.database = Database() self.tabs = {} self.queries = {} self.query_id = 0 self.loading = [] self.loaded = { entry.fileid: entry.path for entry in list(self.database) } self._ui.download_button.clicked.connect( lambda evt: self._onDownloadSelected()) self._ui.open_button.clicked.connect( lambda evt: self._onOpenSelected()) def query(self, attrs): # open tool if not already opened self.app_ctrl.openController(self.name) self.query_id += 1 # add pending tab tab = ResultTab(self.query_id) self.tabs[self.query_id] = tab index = self._ui.tabs.addTab(tab, "Query " + str(self.query_id)) self._ui.tabs.setCurrentIndex(index) # start query executeTask(Fido.search, attrs, self._onQueryResult, [self.query_id]) # register events tab.download.connect( lambda f_id, q_id=self.query_id: self.download(q_id, f_id)) tab.open.connect(lambda f_id: self._onOpen(f_id)) def _onQueryResult(self, query, id): if id not in self.tabs: return query_model = self._convertQuery(query) self.tabs[id].loadQuery(query_model) self.tabs[id].setLoading(self.loading) self.tabs[id].setLoaded(self.loaded.keys()) self.queries[id] = query def download(self, q_id, f_id): req = copy.copy(self.queries[q_id]) req._list = [copy.copy(r) for r in req] for resp in req: resp[:] = [item for item in resp if item.fileid == f_id] self._addLoading([f_id]) executeTask(Fido.fetch, [req], self._onDownloadResult, [f_id, req]) def _onDownloadResult(self, paths, f_id, request): path = paths[0] entry = list( tables.entries_from_fido_search_result( request, self.database.default_waveunit))[0] entry.path = path self.database.add(entry) self.database.commit() self._addLoaded({f_id: path}) def _onOpen(self, f_id): viewer = MapViewerController.fromFile(self.loaded[f_id]) self.content_ctrl.addViewerController(viewer) def _onRemoveTab(self, index): tab = self._ui.tabs.widget(index) self._ui.tabs.removeTab(index) self.tabs.pop(tab.q_id) def _onDownloadSelected(self): tab = self._ui.tabs.currentWidget() f_ids = tab.getSelectedFIds() for f_id in f_ids: if f_id in self.loading or f_id in self.loaded: continue self.download(tab.q_id, f_id) def _onOpenSelected(self): tab = self._ui.tabs.currentWidget() f_ids = tab.getSelectedFIds() for f_id in f_ids: if f_id not in self.loaded: continue self._onOpen(f_id) def _convertQuery(self, query): items = [item for response in query for item in response] return [[c[1](item) for c in columns] for item in items] def _addLoading(self, f_ids): self.loading.extend(f_ids) for tab in self.tabs.values(): tab.setLoading(f_ids) def _addLoaded(self, dict): self.loading = [ f_id for f_id in self.loading if f_id not in dict.keys() ] self.loaded.update(dict) for tab in self.tabs.values(): tab.setLoaded(dict.keys()) @property def item_config(self) -> ItemConfig: return ItemConfig().setTitle("Download Results").setOrientation( QtCore.Qt.BottomDockWidgetArea) @property def view(self) -> QtWidgets: return self._view
class DownloadController(ToolController): result_ctrl: DownloadResultController = RequiredFeature( DownloadResultController.name) def __init__(self): self.query_id = 0 ToolController.__init__(self) self._view = QtWidgets.QWidget() self._ui = Ui_Download() self._ui.setupUi(self._view) self._ui.content.resizeEvent = lambda evt: self._ui.scrollArea.setMinimumWidth( self._ui.content.sizeHint().width( ) + self._ui.scrollArea.verticalScrollBar().sizeHint().width()) self._ui.content_layout.setAlignment(QtCore.Qt.AlignTop) self.possible_filters = [e.value for e in Filter] self.refreshActiveFilters() self.active_filters = [] self.addMandatoryFilters() self._ui.add_filter_button.clicked.connect(self.onAddFilter) self._ui.query_button.clicked.connect(self.onQuery) def refreshActiveFilters(self): self._ui.filter_combo.clear() self._ui.filter_combo.addItems( [f["label"] for f in self.possible_filters]) def addMandatoryFilters(self): mandatory_filters = [ e for e in self.possible_filters if e["mandatory"] ] for f in mandatory_filters: self.addFilter(f) def addFilter(self, f): filter_box = _FilterBox(f) filter_box.closeEvent = lambda evt, fi=filter_box: self.onFilterDestroyed( fi) self._ui.content_layout.addWidget(filter_box) self.possible_filters.remove(f) self.active_filters.append(f) self.refreshActiveFilters() def onAddFilter(self, event): selection = self._ui.filter_combo.currentText() selected_filter = [ f for f in self.possible_filters if f["label"] == selection ] if len(selected_filter) == 1: self.addFilter(selected_filter[0]) def onQuery(self, *args): self._ui.message_box.hide() try: attrs = [] filters = [ f for f in self._ui.content.children() if isinstance(f, _FilterBox) ] for f in filters: attrs.append(f.value()) self.result_ctrl.query(attrs) except Exception as ex: self._ui.message_box.showMessage("Invalid Query: " + str(ex)) def onFilterDestroyed(self, filter_panel): filter = filter_panel.filter self.possible_filters.append(filter) self.active_filters.remove(filter) self.refreshActiveFilters() @property def item_config(self) -> ItemConfig: return ItemConfig().setTitle("Download Tool").setMenuPath( "File/Download Data") @property def view(self) -> QtWidgets: return self._view