def __init__(self, parent=None): super(MainWindowBase, self).__init__(parent) # Do the import here to update the splash import hyperspyui.mdi_mpl_backend from hyperspyui.settings import Settings # Setup settings: self.settings = Settings(self, 'General') # Default setting values: self.settings.set_default('toolbar_button_size', 24) self.settings.set_default('default_widget_floating', False) self.settings.set_default('working_directory', "") self.settings.set_default('low_process_priority', False) # Override any possible invalid stored values, which could prevent load if 'toolbar_button_size' not in self.settings or \ not isinstance(self.settings['toolbar_button_size'], int): self.settings['toolbar_button_size'] = 24 if self.low_process_priority: lowpriority() # State varaibles self.should_capture_traits = None self.active_tool = None # Collections self.widgets = [] # Widgets in widget bar self.figures = [] # Matplotlib figures self.editors = [] # EditorWidgets self.traits_dialogs = [] self.actions = {} self._action_selection_cbs = {} self.toolbars = {} self.menus = {} self.tools = [] self.plugin_manager = None # MPL backend bindings hyperspyui.mdi_mpl_backend.connect_on_new_figure(self.on_new_figure) hyperspyui.mdi_mpl_backend.connect_on_destroy(self.on_destroy_figure) # Create UI self.windowmenu = None self.create_ui() # Connect figure management functions self.main_frame.subWindowActivated.connect(self.on_subwin_activated) # Save standard layout/state self.settings.set_default('_geometry', self.saveGeometry()) self.settings.set_default('_windowState', self.saveState()) # Restore layout/state if saved geometry = self.settings['_geometry'] state = self.settings['_windowState'] if geometry: self.restoreGeometry(geometry) if state: self.restoreState(state)
def main(): from python_qt_binding import QtGui, QtCore, QT_BINDING import hyperspyui.info from hyperspyui.singleapplication import get_app from hyperspyui.settings import Settings from hyperspyui.util import dummy_context_manager # Need to set early to make QSettings accessible QtCore.QCoreApplication.setApplicationName("HyperSpyUI") QtCore.QCoreApplication.setOrganizationName("Hyperspy") QtCore.QCoreApplication.setApplicationVersion(hyperspyui.info.version) # First, clear all default settings! Settings.clear_defaults() # Setup default for single/multi-instance settings = Settings(group="General") settings.set_default('allow_multiple_instances', False) if settings['allow_multiple_instances', bool]: # Using multiple instances, get a new application app = QtGui.QApplication(sys.argv) else: # Make sure we only have a single instance app = get_app('hyperspyui') log_file = _get_logfile() if log_file: sys.stdout = sys.stderr = log_file else: log_file = dummy_context_manager() with log_file: # Create and display the splash screen splash_pix = QtGui.QPixmap( os.path.dirname(__file__) + './images/splash.png') splash = QtGui.QSplashScreen(splash_pix, QtCore.Qt.WindowStaysOnTopHint) splash.setMask(splash_pix.mask()) splash.show() app.processEvents() # Need to have import here, as it can take some time, so should happen # after displaying splash from hyperspyui.mainwindow import MainWindow form = MainWindow() if not settings['allow_multiple_instances', bool]: if QT_BINDING == 'pyqt': app.messageAvailable.connect(form.handleSecondInstance) elif QT_BINDING == 'pyside': app.messageReceived.connect(form.handleSecondInstance) form.showMaximized() splash.finish(form) form.load_complete.emit() # Ensure logging is OK import hyperspy.api as hs hs.set_log_level(LOGLEVEL) app.exec_()
def main(): from qtpy.QtCore import Qt, QCoreApplication from qtpy.QtWidgets import QApplication from qtpy import API import hyperspyui.info from hyperspyui.settings import Settings QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts) # Need to set early to make QSettings accessible QCoreApplication.setApplicationName("HyperSpyUI") QCoreApplication.setOrganizationName("Hyperspy") QCoreApplication.setApplicationVersion(hyperspyui.info.__version__) # First, clear all default settings! # TODO: This will cause a concurrency issue with multiple launch Settings.clear_defaults() # Setup default for single/multi-instance settings = Settings(group="General") settings.set_default('allow_multiple_instances', False) if settings['allow_multiple_instances', bool]: # Using multiple instances, get a new application app = QApplication(sys.argv) else: # Make sure we only have a single instance from hyperspyui.singleapplication import get_app app = get_app('hyperspyui') splash = get_splash() log_file = _get_logfile() if log_file: sys.stdout = sys.stderr = log_file else: @contextmanager def dummy_context_manager(*args, **kwargs): yield log_file = dummy_context_manager() with log_file: # Need to have import here, since QApplication needs to be called first from hyperspyui.mainwindow import MainWindow form = MainWindow(splash=splash) if not settings['allow_multiple_instances', bool]: if "pyqt" in API: app.messageAvailable.connect(form.handleSecondInstance) elif API == 'pyside': app.messageReceived.connect(form.handleSecondInstance) form.showMaximized() form.splash.hide() form.load_complete.emit() # Ensure logging is OK import hyperspy.api as hs hs.set_log_level(LOGLEVEL) app.exec_()
def _create_settings_widgets(self, settings): """ Create a widget for a settings instance, containing label/editor pairs for each setting in the current level of the passed QSettings instance. The key of the setting is used as the label, but it's capitalized and underscores are replaced by spaces. """ wrap = QWidget(self) form = QFormLayout() hint_lookup = Settings() for k in settings.allKeys(): if k.startswith("_"): continue # Ignore hidden keys v = settings.value(k) # Read value label = k.capitalize().replace('_', ' ') abs_key = settings.group() + '/' + k self._initial_values[abs_key] = v # Store initial value hints = hint_lookup.get_enum_hint(abs_key) # Check for enum hints # Create a fitting editor widget based on value type: if hints is not None: w = QComboBox() w.addItems(hints) w.setEditable(True) w.setEditText(v) w.editTextChanged.connect( partial(self._on_setting_changed, abs_key, w)) elif isinstance(v, str): if v.lower() in ('true', 'false'): w = QCheckBox() w.setChecked(v.lower() == 'true') w.toggled.connect( partial(self._on_setting_changed, abs_key, w)) else: w = QLineEdit(v) w.textChanged.connect( partial(self._on_setting_changed, abs_key, w)) elif isinstance(v, int): w = QSpinBox() w.setRange(np.iinfo(np.int32).min, np.iinfo(np.int32).max) w.setValue(v) w.valueChanged.connect( partial(self._on_setting_changed, abs_key, w)) elif isinstance(v, float): w = QDoubleSpinBox() w.setRange(np.finfo(np.float32).min, np.finfo(np.float32).max) w.setValue(v) w.valueChanged.connect( partial(self._on_setting_changed, abs_key, w)) else: w = QLineEdit(str(v)) w.textChanged.connect( partial(self._on_setting_changed, abs_key, w)) self._lut[abs_key] = w form.addRow(label, w) wrap.setLayout(form) return wrap
def _create_settings_widgets(self, settings): """ Create a widget for a settings instance, containing label/editor pairs for each setting in the current level of the passed QSettings instance. The key of the setting is used as the label, but it's capitalized and underscores are replaced by spaces. """ wrap = QWidget(self) form = QFormLayout() hint_lookup = Settings() for k in settings.allKeys(): if k.startswith("_"): continue # Ignore hidden keys v = settings.value(k) # Read value label = k.capitalize().replace('_', ' ') abs_key = settings.group() + '/' + k self._initial_values[abs_key] = v # Store initial value hints = hint_lookup.get_enum_hint(abs_key) # Check for enum hints # Create a fitting editor widget based on value type: if hints is not None: w = QComboBox() w.addItems(hints) w.setEditable(True) w.setEditText(v) w.editTextChanged.connect(partial(self._on_setting_changed, abs_key, w)) elif isinstance(v, str): if v.lower() in ('true', 'false'): w = QCheckBox() w.setChecked(v.lower() == 'true') w.toggled.connect(partial(self._on_setting_changed, abs_key, w)) else: w = QLineEdit(v) w.textChanged.connect(partial(self._on_setting_changed, abs_key, w)) elif isinstance(v, int): w = QSpinBox() w.setRange(np.iinfo(np.int32).min, np.iinfo(np.int32).max) w.setValue(v) w.valueChanged.connect(partial(self._on_setting_changed, abs_key, w)) elif isinstance(v, float): w = QDoubleSpinBox() w.setRange(np.finfo(np.float32).min, np.finfo(np.float32).max) w.setValue(v) w.valueChanged.connect(partial(self._on_setting_changed, abs_key, w)) else: w = QLineEdit(str(v)) w.textChanged.connect(partial(self._on_setting_changed, abs_key, w)) self._lut[abs_key] = w form.addRow(label, w) wrap.setLayout(form) return wrap
def __init__(self, main_window): """ Initializates the manager, and performs discovery of plugins """ self.plugins = AttributeDict() self.ui = main_window self._enabled = {} self.settings = Settings(self.ui, group="General") self.settings.set_default("extra_plugin_directories", "") self.enabled_store = Settings(self.ui, group="PluginManager/enabled") self.discover()
def _on_reset(self): """ Callback for reset button. Prompts user for confirmation, then proceeds to reset settings to default values if confirmed, before updating controls and applying any changes (emits change signal if any changes). """ mb = QMessageBox( QMessageBox.Warning, tr("Reset all settings"), tr("This will reset all settings to their default " + "values. Are you sure you want to continue?"), QMessageBox.Yes | QMessageBox.No) mb.setDefaultButton(QMessageBox.No) dr = mb.exec_() if dr == QMessageBox.Yes: # This clears all settings, and recreates only those values # initialized with set_default this session. Settings.restore_from_defaults() # Now we update controls: s = QSettings(self.ui) keys = list( self._initial_values.keys()) # Use copy, as we may modify for k in keys: # Check if setting is still present if s.contains(k): # Present, update to new value (triggers _on_change) v = s.value(k) w = self._lut[k] if isinstance(w, QLineEdit): w.setText(v) elif isinstance(w, QCheckBox): w.setChecked(v.lower() == "true") elif isinstance(w, (QSpinBox, QDoubleSpinBox)): w.setValue(v) else: # Setting was removed, remove editor w = self._lut[k] layout = w.parent().layout() label = layout.labelForField(w) layout.removeWidget(w) w.close() if label is not None: layout.removeWidget(label) label.close() del self._lut[k] del self._initial_values[k] self._changes[k] = None # Check whether all editors for tab was removed if layout.count() == 0: wrap = w.parent() self.tabs.removeTab(self.tabs.indexOf(wrap)) # Finally apply changes (update _initial_values, and emit signal) self.apply_changes()
def _on_reset(self): """ Callback for reset button. Prompts user for confirmation, then proceeds to reset settings to default values if confirmed, before updating controls and applying any changes (emits change signal if any changes). """ mb = QMessageBox(QMessageBox.Warning, tr("Reset all settings"), tr("This will reset all settings to their default " + "values. Are you sure you want to continue?"), QMessageBox.Yes | QMessageBox.No) mb.setDefaultButton(QMessageBox.No) dr = mb.exec_() if dr == QMessageBox.Yes: # This clears all settings, and recreates only those values # initialized with set_default this session. Settings.restore_from_defaults() # Now we update controls: s = QSettings(self.ui) keys = list(self._initial_values.keys()) # Use copy, as we may modify for k in keys: # Check if setting is still present if s.contains(k): # Present, update to new value (triggers _on_change) v = s.value(k) w = self._lut[k] if isinstance(w, QLineEdit): w.setText(v) elif isinstance(w, QCheckBox): w.setChecked(v.lower() == "true") elif isinstance(w, (QSpinBox, QDoubleSpinBox)): w.setValue(v) else: # Setting was removed, remove editor w = self._lut[k] layout = w.parent().layout() label = layout.labelForField(w) layout.removeWidget(w) w.close() if label is not None: layout.removeWidget(label) label.close() del self._lut[k] del self._initial_values[k] self._changes[k] = None # Check whether all editors for tab was removed if layout.count() == 0: wrap = w.parent() self.tabs.removeTab(self.tabs.indexOf(wrap)) # Finally apply changes (update _initial_values, and emit signal) self.apply_changes()
def main(): from python_qt_binding import QtGui, QtCore, QT_BINDING import hyperspyui.info from hyperspyui.singleapplication import get_app from hyperspyui.settings import Settings from hyperspyui.util import dummy_context_manager # Need to set early to make QSettings accessible QtCore.QCoreApplication.setApplicationName("HyperSpyUI") QtCore.QCoreApplication.setOrganizationName("Hyperspy") QtCore.QCoreApplication.setApplicationVersion(hyperspyui.info.version) # First, clear all default settings! # TODO: This will cause a concurrency issue with multiple launch Settings.clear_defaults() # Setup default for single/multi-instance settings = Settings(group="General") settings.set_default('allow_multiple_instances', False) if settings['allow_multiple_instances', bool]: # Using multiple instances, get a new application app = QtGui.QApplication(sys.argv) else: # Make sure we only have a single instance app = get_app('hyperspyui') log_file = _get_logfile() if log_file: sys.stdout = sys.stderr = log_file else: log_file = dummy_context_manager() with log_file: # Create and display the splash screen splash_pix = QtGui.QPixmap( os.path.dirname(__file__) + './images/splash.png') splash = QtGui.QSplashScreen(splash_pix, QtCore.Qt.WindowStaysOnTopHint) splash.setMask(splash_pix.mask()) splash.show() app.processEvents() # Need to have import here, as it can take some time, so should happen # after displaying splash from hyperspyui.mainwindow import MainWindow form = MainWindow() if not settings['allow_multiple_instances', bool]: if QT_BINDING == 'pyqt': app.messageAvailable.connect(form.handleSecondInstance) elif QT_BINDING == 'pyside': app.messageReceived.connect(form.handleSecondInstance) form.showMaximized() splash.finish(form) form.load_complete.emit() # Ensure logging is OK import hyperspy.api as hs hs.set_log_level(LOGLEVEL) app.exec_()
def main(): from qtpy.QtCore import QCoreApplication from qtpy.QtWidgets import QApplication from qtpy import API import hyperspyui.info from hyperspyui.settings import Settings # Need to set early to make QSettings accessible QCoreApplication.setApplicationName("HyperSpyUI") QCoreApplication.setOrganizationName("Hyperspy") QCoreApplication.setApplicationVersion(hyperspyui.info.version) # First, clear all default settings! # TODO: This will cause a concurrency issue with multiple launch Settings.clear_defaults() # Setup default for single/multi-instance settings = Settings(group="General") settings.set_default('allow_multiple_instances', False) if settings['allow_multiple_instances', bool]: # Using multiple instances, get a new application app = QApplication(sys.argv) else: # Make sure we only have a single instance from hyperspyui.singleapplication import get_app app = get_app('hyperspyui') splash = get_splash() log_file = _get_logfile() if log_file: sys.stdout = sys.stderr = log_file else: @contextmanager def dummy_context_manager(*args, **kwargs): yield log_file = dummy_context_manager() with log_file: # Need to have import here, since QApplication needs to be called first from hyperspyui.mainwindow import MainWindow form = MainWindow(splash=splash) if not settings['allow_multiple_instances', bool]: if "pyqt" in API: app.messageAvailable.connect(form.handleSecondInstance) elif API == 'pyside': app.messageReceived.connect(form.handleSecondInstance) form.showMaximized() form.splash.hide() form.load_complete.emit() # Ensure logging is OK import hyperspy.api as hs hs.set_log_level(LOGLEVEL) app.exec_()
def __init__(self, main_window): super(Plugin, self).__init__() self.ui = main_window if self.name is None: set_group = 'plugins/' + str.lower(self.__class__.__name__) set_group = set_group.replace('.', '') else: set_group = 'plugins/' + self.name.replace('/', '-').replace( '\\', '-') self.settings = Settings(self.ui, group=set_group) self.actions = {} self.menu_actions = {} self.toolbar_actions = {} self.tools = [] self.widgets = set() self.dialogs = []
class MainWindowBase(QtWidgets.QMainWindow): """ Base layer in application stack. Should handle the connection to our custom matplotlib backend, and manage the Figures. As such, it does not know anything about hyperspy, and can readily be reused for other projects. Should also set up basic UI with utilities, and relevant functions for inhereting classes to override. """ def __init__(self, parent=None): super(MainWindowBase, self).__init__(parent) # Do the import here to update the splash import hyperspyui.mdi_mpl_backend from hyperspyui.settings import Settings # Setup settings: self.settings = Settings(self, 'General') # Default setting values: self.settings.set_default('toolbar_button_size', 24) self.settings.set_default('default_widget_floating', False) self.settings.set_default('working_directory', "") self.settings.set_default('low_process_priority', False) # Override any possible invalid stored values, which could prevent load if 'toolbar_button_size' not in self.settings or \ not isinstance(self.settings['toolbar_button_size'], int): self.settings['toolbar_button_size'] = 24 if self.low_process_priority: lowpriority() # State varaibles self.should_capture_traits = None self.active_tool = None # Collections self.widgets = [] # Widgets in widget bar self.figures = [] # Matplotlib figures self.editors = [] # EditorWidgets self.traits_dialogs = [] self.actions = {} self._action_selection_cbs = {} self.toolbars = {} self.menus = {} self.tools = [] self.plugin_manager = None # MPL backend bindings hyperspyui.mdi_mpl_backend.connect_on_new_figure(self.on_new_figure) hyperspyui.mdi_mpl_backend.connect_on_destroy(self.on_destroy_figure) # Create UI self.windowmenu = None self.create_ui() # Connect figure management functions self.main_frame.subWindowActivated.connect(self.on_subwin_activated) # Save standard layout/state self.settings.set_default('_geometry', self.saveGeometry()) self.settings.set_default('_windowState', self.saveState()) # Restore layout/state if saved geometry = self.settings['_geometry'] state = self.settings['_windowState'] if geometry: self.restoreGeometry(geometry) if state: self.restoreState(state) @property def toolbar_button_size(self): return self.settings['toolbar_button_size', int] @toolbar_button_size.setter def toolbar_button_size(self, value): self.settings['toolbar_button_size'] = value self.setIconSize( QtCore.QSize(self.toolbar_button_size, self.toolbar_button_size)) @property def cur_dir(self): return self.settings['working_directory'] or '' @cur_dir.setter def cur_dir(self, value): self.settings['working_directory'] = value @property def low_process_priority(self): return self.settings['low_process_priority', bool] @low_process_priority.setter def low_process_priority(self, value): self.settings['low_process_priority'] = value if value: lowpriority() else: normalpriority() @property def plugins(self): return self.plugin_manager.plugins def handleSecondInstance(self, argv): # overload if needed self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) self.activateWindow() def closeEvent(self, event): self.settings['_geometry'] = self.saveGeometry() self.settings['_windowState'] = self.saveState() return super(MainWindowBase, self).closeEvent(event) def reset_geometry(self): self.settings.restore_key_default('_geometry') self.settings.restore_key_default('_windowState') geometry = self.settings['_geometry'] state = self.settings['_windowState'] if geometry: self.restoreGeometry(geometry) if state: self.restoreState(state) self.setWindowState(Qt.WindowMaximized) def create_ui(self): self.setIconSize( QtCore.QSize(self.toolbar_button_size, self.toolbar_button_size)) self.main_frame = QtWidgets.QMdiArea() self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea) self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea) self.set_splash("Initializing plugins") self.init_plugins() self.set_splash("Creating default actions") self.create_default_actions() # Goes before menu/toolbar/widgetbar # Needs to go before menu, so console can be in menu self.set_splash("Creating console") self.create_console() # This needs to happen before the widgetbar and toolbar self.set_splash("Creating menus") self.create_menu() self.set_splash("Creating toolbars") self.create_toolbars() self.set_splash("Creating widgets") self.create_widgetbar() self.setCentralWidget(self.main_frame) def init_plugins(self): from .pluginmanager import PluginManager self.plugin_manager = PluginManager(self) # Disable Version selector plugin until it is fixed self.plugin_manager.enabled_store['Version selector'] = False self.plugin_manager.init_plugins() def create_default_actions(self): """ Create default actions that can be used for e.g. toolbars and menus, or triggered manually. """ self.set_splash("Creating plugin actions") self.plugin_manager.create_actions() self.selectable_tools = QtWidgets.QActionGroup(self) self.selectable_tools.setExclusive(True) # Nested docking action ac_nested = QtWidgets.QAction(tr("Nested docking"), self) ac_nested.setStatusTip(tr("Allow nested widget docking")) ac_nested.setCheckable(True) ac_nested.setChecked(self.isDockNestingEnabled()) ac_nested.triggered[bool].connect(self.setDockNestingEnabled) self.actions['nested_docking'] = ac_nested # Tile windows action ac_tile = QtWidgets.QAction(tr("Tile"), self) ac_tile.setStatusTip(tr("Arranges all figures in a tile pattern")) ac_tile.triggered.connect(self.main_frame.tileSubWindows) self.actions['tile_windows'] = ac_tile # Cascade windows action ac_cascade = QtWidgets.QAction(tr("Cascade"), self) ac_cascade.setStatusTip( tr("Arranges all figures in a cascade pattern")) ac_cascade.triggered.connect(self.main_frame.cascadeSubWindows) self.actions['cascade_windows'] = ac_cascade # Close all figures action ac_close_figs = QtWidgets.QAction(tr("Close all"), self) ac_close_figs.setStatusTip(tr("Closes all matplotlib figures")) ac_close_figs.triggered.connect(lambda: matplotlib.pyplot.close("all")) self.actions['close_all_windows'] = ac_close_figs # Reset geometry action ac_reset_layout = QtWidgets.QAction(tr("Reset layout"), self) ac_reset_layout.setStatusTip(tr("Resets layout of toolbars and " "widgets")) ac_reset_layout.triggered.connect(self.reset_geometry) self.actions['reset_layout'] = ac_reset_layout def create_menu(self): mb = self.menuBar() # Window menu is filled in add_widget and add_figure self.windowmenu = mb.addMenu(tr("&Windows")) self.windowmenu.addAction(self._console_dock.toggleViewAction()) self.windowmenu.addAction(self.actions['nested_docking']) # Figure windows go below this separator. Other windows can be added # above it with insertAction(self.windowmenu_sep, QAction) self.windowmenu_sep = self.windowmenu.addSeparator() self.windowmenu.addAction(self.actions['tile_windows']) self.windowmenu.addAction(self.actions['cascade_windows']) self.windowmenu.addSeparator() self.windowmenu.addAction(self.actions['close_all_windows']) self.windowmenu_actions_sep = self.windowmenu.addSeparator() self.plugin_manager.create_menu() def create_tools(self): """Override to create tools on UI construction. """ self.plugin_manager.create_tools() def create_toolbars(self): """ Override to create toolbars and toolbar buttons on UI construction. It is called after create_default_action(), so add_toolbar_button() can be used to add previously defined acctions. """ self.create_tools() self.plugin_manager.create_toolbars() def create_widgetbar(self): """ The widget bar itself is created and managed implicitly by Qt. Override this function to add widgets on UI construction. """ self.plugin_manager.create_widgets() def edit_settings(self): """ Shows a dialog for editing the application and plugins settings. """ from hyperspyui.widgets.settingsdialog import SettingsDialog d = SettingsDialog(self, self) d.settings_changed.connect(self.on_settings_changed) d.exec_() def on_settings_changed(self): """ Callback for SettingsDialog, or anything else that updates settings and need to apply the change. """ # Due to the way the property is defined, this updates the UI: self.toolbar_button_size = self.toolbar_button_size self.low_process_priority = self.low_process_priority def select_tool(self, tool): if self.active_tool is not None: try: self.active_tool.disconnect_windows(self.figures) except Exception as e: warnings.warn("Exception disabling tool %s: %s" % ( self.active_tool.get_name(), e)) self.active_tool = tool tool.connect_windows(self.figures) # --------- Figure management --------- # --------- MPL Events --------- def on_new_figure(self, figure, userdata=None): """ Callback for MPL backend. """ self.main_frame.addSubWindow(figure) self.figures.append(figure) self.windowmenu.addAction(figure.activateAction()) for tool in self.tools: if tool.single_action() is not None: tool.connect_windows(figure) if self.active_tool is not None: self.active_tool.connect_windows(figure) def on_destroy_figure(self, figure, userdata=None): """ Callback for MPL backend. """ if figure in self.figures: self.figures.remove(figure) self.windowmenu.removeAction(figure.activateAction()) for tool in self.tools: if tool.single_action() is not None: tool.disconnect_windows(figure) if self.active_tool is not None: self.active_tool.disconnect_windows(figure) self.main_frame.removeSubWindow(figure) # --------- End MPL Events --------- def on_subwin_activated(self, mdi_figure): if mdi_figure and API == 'pyside': mdi_figure.activateAction().setChecked(True) self.check_action_selections(mdi_figure) def check_action_selections(self, mdi_figure=None): if mdi_figure is None: mdi_figure = self.main_frame.activeSubWindow() for key, cb in self._action_selection_cbs.items(): cb(mdi_figure, self.actions[key]) # --------- End figure management --------- # --------- Console functions --------- def _get_console_exec(self): return "" def _get_console_exports(self): return {'ui': self} def _get_console_config(self): return None def on_console_executing(self, source): """ Override when inherited to perform actions before exectuing 'source'. """ pass def on_console_executed(self, response): """ Override when inherited to perform actions after executing, given the 'response' returned. """ pass def create_console(self): # We could inherit QAction, and have it reroute when it triggers, # and then drop route when it finishes, however this will not catch # interactive dialogs and such. c = self._get_console_config() self.settings.set_default('console_completion_type', 'droplist') valid_completions = ConsoleWidget.gui_completion.values self.settings.set_enum_hint('console_completion_type', valid_completions) gui_completion = self.settings['console_completion_type'] if gui_completion not in valid_completions: gui_completion = 'droplist' control = ConsoleWidget(config=c, gui_completion=gui_completion) control.executing.connect(self.on_console_executing) control.executed.connect(self.on_console_executed) # This is where we push variables to the console ex = self._get_console_exec() push = self._get_console_exports() control.ex(ex) control.push(push) self.console = control self._console_dock = QtWidgets.QDockWidget("Console") self._console_dock.setObjectName('console_widget') self._console_dock.setWidget(control) self.addDockWidget(Qt.BottomDockWidgetArea, self._console_dock)
class PluginManager(object): def __init__(self, main_window): """ Initializates the manager, and performs discovery of plugins """ self.plugins = AttributeDict() self.ui = main_window self._enabled = {} self.settings = Settings(self.ui, group="General") self.settings.set_default("extra_plugin_directories", "") self.enabled_store = Settings(self.ui, group="PluginManager/enabled") self.discover() @property def enabled(self): """Returns a read-only dictionary showing the enabled/disabled state of all plugins. """ d = ReadOnlyDict() for name, (enabled, _) in d.items(): d[name] = enabled d._readonly = True return d def enable_plugin(self, name, value=True): """Enable/disable plugin functionality. Also loads/unloads plugin. If enabling and the plugin is already loaded, this will reload the plugin. """ self.enabled_store[name] = value ptype = self._enabled[name][1] self._enabled[name] = (value, ptype) if name in self.plugins: self.unload(self.plugins[name]) if value: self.load(ptype) def disable_plugin(self, name): """Disable plugin functionality. Also unloads plugin. """ self.enable_plugin(name, False) def _import_plugin_from_path(self, name, path): try: mname = "hyperspyui.plugins." + name if sys.version_info >= (3, 5): import importlib.util spec = importlib.util.spec_from_file_location(mname, path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) else: from importlib.machinery import SourceFileLoader loader = SourceFileLoader(mname, path) loader.load_module() except Exception: self.warn("import", path) def discover(self): """Auto-discover all plugins defined in plugin directory. """ import hyperspyui.plugins as plugins for plug in plugins.__all__: try: __import__('hyperspyui.plugins.' + plug, globals()) except Exception: self.warn("import", plug) # Import any plugins in extra dirs. extra_paths = self.settings['extra_plugin_directories'] if extra_paths: extra_paths = extra_paths.split(os.path.pathsep) for path in extra_paths: if not os.path.isdir(path): path = os.path.dirname(path) modules = glob.glob(path + "/*.py") # TODO: In release form, we should consider supporting # compiled plugins in pyc/pyo format # modules.extend(glob.glob(os.path.dirname(__file__)+"/*.py?")) # If so, ensure that duplicates are removed (picks py over pyc) modules = [ m for m in modules if not os.path.basename(m).startswith('_') ] for m in modules: name = os.path.splitext(os.path.basename(m))[0] self._import_plugin_from_path(name, m) master = Plugin self.implementors = sorted(self._inheritors(master), key=lambda x: x.name) logger.debug("Found plugins: %s", self.implementors) @staticmethod def _inheritors(klass): """Return all defined classes that inherit from 'klass'. """ subclasses = set() work = [klass] while work: parent = work.pop() for child in parent.__subclasses__(): if child not in subclasses: subclasses.add(child) work.append(child) return subclasses def warn(self, f_name, p_name, category=RuntimeWarning): tbf = ''.join(traceback.format_exception(*sys.exc_info())[2:]) warnings.warn(("Exception in {0} of hyperspyui plugin " + "\"{1}\" error:\n{2}").format(f_name, p_name, tbf), RuntimeWarning, 2) def init_plugins(self): self.plugins.clear() for plug_type in self.implementors: try: self._load_if_enabled(plug_type) except Exception: self.warn("initialization", plug_type.name) def create_actions(self): for p in self.plugins.values(): try: p.create_actions() except Exception: self.warn(sys._getframe().f_code.co_name, p.name) def create_menu(self): for p in self.plugins.values(): try: p.create_menu() except Exception: self.warn(sys._getframe().f_code.co_name, p.name) def create_tools(self): for p in self.plugins.values(): try: p.create_tools() except Exception: self.warn(sys._getframe().f_code.co_name, p.name) def create_toolbars(self): for p in self.plugins.values(): try: p.create_toolbars() except Exception: self.warn(sys._getframe().f_code.co_name, p.name) def create_widgets(self): for p in self.plugins.values(): try: p.create_widgets() except Exception: self.warn(sys._getframe().f_code.co_name, p.name) def _load_if_enabled(self, p_type): if p_type is None or p_type.name is None: return if self.enabled_store[p_type.name] is None: # Init setting to True on first encounter self.enabled_store[p_type.name] = True enabled = self.enabled_store[p_type.name, bool] self._enabled[p_type.name] = (enabled, p_type) if enabled: # Init logger.debug("Initializing plugin: %s", p_type) p = p_type(self.ui) self.plugins[p.name] = p logger.debug("Plugin loaded: %s", p.name) return p return None def load(self, plugin_type): try: p = self._load_if_enabled(plugin_type) if p is not None: # Order of execution is significant! p.create_actions() p.create_menu() p.create_tools() p.create_toolbars() p.create_widgets() except Exception: self.warn(sys._getframe().f_code.co_name, plugin_type) def load_from_file(self, path): master = Plugin prev = self._inheritors(master) name = os.path.splitext(os.path.basename(path))[0] mod_name = 'hyperspyui.plugins.' + name reload_plugins = mod_name in sys.modules try: if sys.version_info >= (3, 5): spec = importlib.util.spec_from_file_location(mod_name, path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) else: importlib.machinery.SourceFileLoader(mod_name, path).load_module() except Exception: self.warn("import", name) loaded = self._inheritors(master).difference(prev) new_ps = [] for plug_type in loaded: if reload_plugins and plug_type.name in self.plugins: # Unload any plugins with same name self.unload(self.plugins[plug_type.name]) try: p = self._load_if_enabled(plug_type) except Exception: self.warn('load', plug_type.name) if p is not None: new_ps.append(p) for p in new_ps: try: p.create_actions() except Exception: self.warn('create_actions', p.name) for p in new_ps: try: p.create_menu() except Exception: self.warn('create_menu', p.name) for p in new_ps: try: p.create_tools() except Exception: self.warn('create_tools', p.name) for p in new_ps: try: p.create_toolbars() except Exception: self.warn('create_toolbars', p.name) for p in new_ps: try: p.create_widgets() except Exception: self.warn('create_widgets', p.name) return new_ps def unload(self, plugin): plugin.unload() self.plugins.pop(plugin.name) def reload(self, plugin): new_module = importlib.reload(sys.modules[plugin.__module__]) new_ptype = new_module[plugin.__class__.__name__] if new_ptype is not None: self.unload(plugin) self.load(new_ptype)
class PluginManager(object): def __init__(self, main_window): """ Initializates the manager, and performs discovery of plugins """ self.plugins = AttributeDict() self.ui = main_window self._enabled = {} self.settings = Settings(self.ui, group="General") self.settings.set_default("extra_plugin_directories", "") self.enabled_store = Settings(self.ui, group="PluginManager/enabled") self.discover() @property def enabled(self): """Returns a read-only dictionary showing the enabled/disabled state of all plugins. """ d = ReadOnlyDict() for name, (enabled, _) in d.items(): d[name] = enabled d._readonly = True return d def enable_plugin(self, name, value=True): """Enable/disable plugin functionality. Also loads/unloads plugin. If enabling and the plugin is already loaded, this will reload the plugin. """ self.enabled_store[name] = value ptype = self._enabled[name][1] self._enabled[name] = (value, ptype) if name in self.plugins: self.unload(self.plugins[name]) if value: self.load(ptype) def disable_plugin(self, name): """Disable plugin functionality. Also unloads plugin. """ self.enable_plugin(name, False) def _import_plugin_from_path(self, name, path): try: mname = "hyperspyui.plugins." + name if sys.version_info >= (3, 5): import importlib.util spec = importlib.util.spec_from_file_location(mname, path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) else: from importlib.machinery import SourceFileLoader loader = SourceFileLoader(mname, path) loader.load_module() except Exception: self.warn("import", path) def discover(self): """Auto-discover all plugins defined in plugin directory. """ import hyperspyui.plugins as plugins for plug in plugins.__all__: try: __import__('hyperspyui.plugins.' + plug, globals()) except Exception: self.warn("import", plug) # Import any plugins in extra dirs. extra_paths = self.settings['extra_plugin_directories'] if extra_paths: extra_paths = extra_paths.split(os.path.pathsep) for path in extra_paths: if not os.path.isdir(path): path = os.path.dirname(path) modules = glob.glob(path + "/*.py") # TODO: In release form, we should consider supporting # compiled plugins in pyc/pyo format # modules.extend(glob.glob(os.path.dirname(__file__)+"/*.py?")) # If so, ensure that duplicates are removed (picks py over pyc) modules = [m for m in modules if not os.path.basename(m).startswith('_')] for m in modules: name = os.path.splitext(os.path.basename(m))[0] self._import_plugin_from_path(name, m) master = Plugin self.implementors = sorted(self._inheritors(master), key=lambda x: x.name) logger.debug("Found plugins: %s", self.implementors) @staticmethod def _inheritors(klass): """Return all defined classes that inherit from 'klass'. """ subclasses = set() work = [klass] while work: parent = work.pop() for child in parent.__subclasses__(): if child not in subclasses: subclasses.add(child) work.append(child) return subclasses def warn(self, f_name, p_name, category=RuntimeWarning): tbf = ''.join(traceback.format_exception(*sys.exc_info())[2:]) warnings.warn(("Exception in {0} of hyperspyui plugin " + "\"{1}\" error:\n{2}").format(f_name, p_name, tbf), RuntimeWarning, 2) def init_plugins(self): self.plugins.clear() for plug_type in self.implementors: try: self._load_if_enabled(plug_type) except Exception: self.warn("initialization", plug_type.name) def create_actions(self): for p in self.plugins.values(): try: p.create_actions() except Exception: self.warn(sys._getframe().f_code.co_name, p.name) def create_menu(self): for p in self.plugins.values(): try: p.create_menu() except Exception: self.warn(sys._getframe().f_code.co_name, p.name) def create_tools(self): for p in self.plugins.values(): try: p.create_tools() except Exception: self.warn(sys._getframe().f_code.co_name, p.name) def create_toolbars(self): for p in self.plugins.values(): try: p.create_toolbars() except Exception: self.warn(sys._getframe().f_code.co_name, p.name) def create_widgets(self): for p in self.plugins.values(): try: p.create_widgets() except Exception: self.warn(sys._getframe().f_code.co_name, p.name) def _load_if_enabled(self, p_type): if p_type is None or p_type.name is None: return if self.enabled_store[p_type.name] is None: # Init setting to True on first encounter self.enabled_store[p_type.name] = True enabled = self.enabled_store[p_type.name, bool] self._enabled[p_type.name] = (enabled, p_type) if enabled: # Init logger.debug("Initializing plugin: %s", p_type) p = p_type(self.ui) self.plugins[p.name] = p logger.debug("Plugin loaded: %s", p.name) return p return None def load(self, plugin_type): try: p = self._load_if_enabled(plugin_type) if p is not None: # Order of execution is significant! p.create_actions() p.create_menu() p.create_tools() p.create_toolbars() p.create_widgets() except Exception: self.warn(sys._getframe().f_code.co_name, p.name) def load_from_file(self, path): master = Plugin prev = self._inheritors(master) name = os.path.splitext(os.path.basename(path))[0] mod_name = 'hyperspyui.plugins.' + name reload_plugins = mod_name in sys.modules try: if sys.version_info >= (3, 5): spec = importlib.util.spec_from_file_location( mod_name, path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) else: importlib.machinery.SourceFileLoader( mod_name, path).load_module() except Exception: self.warn("import", name) loaded = self._inheritors(master).difference(prev) new_ps = [] for plug_type in loaded: if reload_plugins and plug_type.name in self.plugins: # Unload any plugins with same name self.unload(self.plugins[plug_type.name]) try: p = self._load_if_enabled(plug_type) except Exception: self.warn('load', plug_type.name) if p is not None: new_ps.append(p) for p in new_ps: try: p.create_actions() except Exception: self.warn('create_actions', p.name) for p in new_ps: try: p.create_menu() except Exception: self.warn('create_menu', p.name) for p in new_ps: try: p.create_tools() except Exception: self.warn('create_tools', p.name) for p in new_ps: try: p.create_toolbars() except Exception: self.warn('create_toolbars', p.name) for p in new_ps: try: p.create_widgets() except Exception: self.warn('create_widgets', p.name) return new_ps def unload(self, plugin): plugin.unload() self.plugins.pop(plugin.name) def reload(self, plugin): new_module = importlib.reload(sys.modules[plugin.__module__]) new_ptype = new_module[plugin.__class__.__name__] if new_ptype is not None: self.unload(plugin) self.load(new_ptype)
def main(): from qtpy.QtCore import Qt, QCoreApplication from qtpy.QtWidgets import QApplication from qtpy import API import hyperspyui.info from hyperspyui.settings import Settings # QtWebEngineWidgets must be imported before a QCoreApplication instance # is created (used in eelsdb plugin) # Avoid a bug in Qt: https://bugreports.qt.io/browse/QTBUG-46720 from qtpy import QtWebEngineWidgets # Need to set early to make QSettings accessible QCoreApplication.setApplicationName("HyperSpyUI") QCoreApplication.setOrganizationName("Hyperspy") QCoreApplication.setApplicationVersion(hyperspyui.info.__version__) # To avoid the warning: # Qt WebEngine seems to be initialized from a plugin. Please set # Qt::AA_ShareOpenGLContexts using QCoreApplication::setAttribute before # constructing QGuiApplication. # Only available for pyqt>=5.4 if hasattr(Qt, "AA_ShareOpenGLContexts"): QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts) # First, clear all default settings! # TODO: This will cause a concurrency issue with multiple launch Settings.clear_defaults() # Setup default for single/multi-instance settings = Settings(group="General") settings.set_default('allow_multiple_instances', False) if settings['allow_multiple_instances', bool]: # Using multiple instances, get a new application app = QApplication(sys.argv) else: # Make sure we only have a single instance from hyperspyui.singleapplication import get_app app = get_app('hyperspyui') splash = get_splash() log_file = _get_logfile() if log_file: sys.stdout = sys.stderr = log_file else: @contextmanager def dummy_context_manager(*args, **kwargs): yield log_file = dummy_context_manager() with log_file: # Need to have import here, since QApplication needs to be called first from hyperspyui.mainwindow import MainWindow form = MainWindow(splash=splash) if not settings['allow_multiple_instances', bool]: if "pyqt" in API: app.messageAvailable.connect(form.handleSecondInstance) elif API == 'pyside': app.messageReceived.connect(form.handleSecondInstance) form.showMaximized() form.splash.hide() form.load_complete.emit() # Ensure logging is OK import hyperspy.api as hs hs.set_log_level(LOGLEVEL) app.exec_()