class GlueApplication(Application, QtWidgets.QMainWindow): """ The main GUI application for the Qt frontend""" def __init__(self, data_collection=None, session=None): # At this point we need to check if a Qt application already exists - # this happens for example if using the %gui qt/qt5 mode in Jupyter. We # should keep a reference to the original icon so that we can restore it # later self._original_app = QtWidgets.QApplication.instance() if self._original_app is not None: self._original_icon = self._original_app.windowIcon() self._export_helper = ExportHelper(self) # Now we can get the application instance, which involves setting it # up if it doesn't already exist. self.app = get_qapp() QtWidgets.QMainWindow.__init__(self) Application.__init__(self, data_collection=data_collection, session=session) # Pull in any keybindings from an external file self.keybindings = keyboard_shortcut icon = get_icon('app_icon') self.app.setWindowIcon(icon) # Even though we loaded the plugins in start_glue, we re-load them here # in case glue was started directly by initializing this class. load_plugins() self.setWindowTitle("Glue") self.setWindowIcon(icon) self.setAttribute(Qt.WA_DeleteOnClose) self._actions = {} self._terminal = None self._setup_ui() self.tab_widget.setMovable(True) self.tab_widget.setTabsClosable(True) # The following is a counter that never goes down, even if tabs are # deleted (this is by design, to avoid having two tabs called the # same if a tab is removed then a new one added again) self._total_tab_count = 0 lwidget = self._layer_widget a = PlotAction(lwidget, self) lwidget.ui.layerTree.addAction(a) self._tweak_geometry() self._create_actions() self._create_menu() self._connect() self.new_tab() self._update_viewer_in_focus() def _update_viewer_in_focus(self, *args): if not hasattr(self, '_viewer_in_focus'): self._viewer_in_focus = None mdi_area = self.current_tab active = mdi_area.activeSubWindow() # Disable any active tool in the viewer that was previously in focus. # Note that we want to do this even if active is None, which means that # the user may have switched application. if (self._viewer_in_focus is not None and (active is None or active.widget() is not self._viewer_in_focus)): try: self._viewer_in_focus.toolbar.active_tool = None except AttributeError: pass # not all viewers have toolbars if active is None: first_viewer = None for win in mdi_area.subWindowList(): if self._viewer_in_focus is win.widget(): break elif isinstance(win.widget(), DataViewer): first_viewer = win.widget() else: self._viewer_in_focus = first_viewer self._update_focus_decoration() self._update_plot_dashboard() else: self._viewer_in_focus = active.widget() self._update_focus_decoration() self._update_plot_dashboard() def run_startup_action(self, name): if name in startup_action.members: startup_action.members[name](self.session, self.data_collection) else: raise Exception("Unknown startup action: {0}".format(name)) def _setup_ui(self): self._ui = load_ui('application.ui', None, directory=os.path.dirname(__file__)) self.setCentralWidget(self._ui) self._ui.tabWidget.setTabBar(GlueTabBar()) lw = LayerTreeWidget(session=self._session) lw.set_checkable(False) self._vb = QtWidgets.QVBoxLayout() self._vb.setContentsMargins(0, 0, 0, 0) self._vb.addWidget(lw) self._ui.data_layers.setLayout(self._vb) self._layer_widget = lw # Data toolbar self._data_toolbar = QtWidgets.QToolBar() self._data_toolbar.setIconSize(QtCore.QSize(16, 16)) self._button_open_data = QtWidgets.QToolButton() self._button_open_data.setText("Open Data") self._button_open_data.setIcon(get_icon('glue_open')) self._button_open_data.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_open_data.clicked.connect(self._choose_load_data_wizard) self._data_toolbar.addWidget(self._button_open_data) self._button_save_data = QtWidgets.QToolButton() self._button_save_data.setText("Export Data/Subsets") self._button_save_data.setIcon(get_icon('glue_filesave')) self._button_save_data.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_save_data.clicked.connect(self._choose_save_data) self._data_toolbar.addWidget(self._button_save_data) self._button_link_data = QtWidgets.QToolButton() self._button_link_data.setText("Link Data") self._button_link_data.setIcon(get_icon('glue_link')) self._button_link_data.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_link_data.clicked.connect(self._set_up_links) self._on_data_collection_change() self._data_toolbar.addWidget(self._button_link_data) self._button_ipython = QtWidgets.QToolButton() self._button_ipython.setCheckable(True) self._button_ipython.setText("IPython Terminal") self._button_ipython.setIcon(get_icon('IPythonConsole')) self._button_ipython.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_ipython.clicked.connect(self._toggle_terminal) self._data_toolbar.addWidget(self._button_ipython) self._button_open_session = QtWidgets.QToolButton() self._button_open_session.setText("Open Session") self._button_open_session.setIcon(get_icon('glue_open')) self._button_open_session.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_open_session.clicked.connect(self._restore_session) self._data_toolbar.addWidget(self._button_open_session) self._button_save_session = QtWidgets.QToolButton() self._button_save_session.setText("Export Session") self._button_save_session.setIcon(get_icon('glue_filesave')) self._button_save_session.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_save_session.clicked.connect(self._choose_save_session) self._data_toolbar.addWidget(self._button_save_session) self._button_edit_components = QtWidgets.QToolButton() self._button_edit_components.setText("Add/edit arithmetic attributes") self._button_edit_components.setIcon(get_icon('pencil')) self._button_edit_components.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_edit_components.clicked.connect(nonpartial(self._layer_widget._create_component)) self._data_toolbar.addWidget(self._button_edit_components) spacer = QtWidgets.QWidget() spacer.setMinimumSize(20, 10) spacer.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) self._data_toolbar.addWidget(spacer) self.addToolBar(self._data_toolbar) # Selection mode toolbar tbar = EditSubsetModeToolBar(parent=self) self._mode_toolbar = tbar self.addToolBar(self._mode_toolbar) # Error console toolbar self._console_toolbar = QtWidgets.QToolBar() self._console_toolbar.setIconSize(QtCore.QSize(16, 16)) spacer = QtWidgets.QWidget() spacer.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) self._console_toolbar.addWidget(spacer) self._button_preferences = QtWidgets.QToolButton() self._button_preferences.setText("Preferences") self._button_preferences.setIcon(get_icon('glue_settings')) self._button_preferences.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_preferences.clicked.connect(self._edit_settings) self._console_toolbar.addWidget(self._button_preferences) self._button_console = QtWidgets.QToolButton() self._button_console.setText("View Error Console") self._button_console.setToolButtonStyle(Qt.ToolButtonTextOnly) self._console_toolbar.addWidget(self._button_console) self.addToolBar(self._console_toolbar) self._log = GlueLogger(button_console=self._button_console) self._log.window().setWindowTitle("Console Log") self._log.resize(550, 550) self._log.hide() self._hub.subscribe(self, DataCollectionMessage, handler=self._on_data_collection_change) def _on_data_collection_change(self, *event): self._button_link_data.setEnabled(len(self.data_collection) > 1) def keyPressEvent(self, event): if self.current_tab.activeSubWindow() and self.current_tab.activeSubWindow().widget(): active_window = self.current_tab.activeSubWindow().widget() else: active_window = None # keybindings is a data structure in the form of dict(dict) # which uses the DataViewer as the first key, the key pressed # as the second key, and the function associated with those two # as the value. if type(active_window) in self.keybindings: for k, func in self.keybindings.members[type(active_window)].items(): if event.key() == k: func(self.session) return # If key does not correspond with viewers, it might correspond # with the global application, thus, None if None in self.keybindings: for k, func in self.keybindings.members[None].items(): if event.key() == k: func(self.session) return return super(GlueApplication, self).keyPressEvent(event) def _set_up_links(self, event): LinkEditor.update_links(self.data_collection) def _tweak_geometry(self): """Maximize window by default.""" self._ui.main_splitter.setStretchFactor(0, 0.1) self._ui.main_splitter.setStretchFactor(1, 0.9) self._ui.data_plot_splitter.setStretchFactor(0, 0.25) self._ui.data_plot_splitter.setStretchFactor(1, 0.5) self._ui.data_plot_splitter.setStretchFactor(2, 0.25) @property def tab_widget(self): return self._ui.tabWidget @property def tab_bar(self): return self._ui.tabWidget.tabBar() @property def tab_count(self): """ The number of open tabs """ return self._ui.tabWidget.count() @property def current_tab(self): return self._ui.tabWidget.currentWidget() def get_tab_index(self, widget): for idx in range(self.tab_count): if self.tab(idx) == widget: return idx raise Exception("Tab not found") def tab(self, index=None): if index is None: return self.current_tab return self._ui.tabWidget.widget(index) def new_tab(self, *args): """Spawn a new tab page""" layout = QtWidgets.QGridLayout() layout.setSpacing(1) layout.setContentsMargins(0, 0, 0, 0) widget = GlueMdiArea(self) widget.setLayout(layout) tab = self.tab_widget self._total_tab_count += 1 tab.addTab(widget, str("Tab %i" % self._total_tab_count)) tab.setCurrentWidget(widget) widget.subWindowActivated.connect(self._update_viewer_in_focus) def close_tab(self, index, warn=True): """ Close a tab window and all associated data viewers """ # do not delete the last tab if self.tab_widget.count() == 1: return if warn and not os.environ.get('GLUE_TESTING'): buttons = QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel dialog = QtWidgets.QMessageBox.warning( self, "Confirm Close", "Are you sure you want to close this tab? " "This will close all data viewers in the tab.", buttons=buttons, defaultButton=QtWidgets.QMessageBox.Cancel) if not dialog == QtWidgets.QMessageBox.Ok: return w = self.tab_widget.widget(index) for window in w.subWindowList(): widget = window.widget() if isinstance(widget, DataViewer): widget.close(warn=False) w.close() self.tab_widget.removeTab(index) def add_widget(self, new_widget, label=None, tab=None, hold_position=False): """ Add a widget to one of the tabs. Returns the window that this widget is wrapped in. :param new_widget: new QtWidgets.QWidget to add :param label: label for the new window. Optional :type label: str :param tab: Tab to add to. Optional (default: current tab) :type tab: int :param hold_position: If True, then override Qt's default placement and retain the original position of new_widget :type hold_position: bool """ # Find first tab that supports addSubWindow if tab is None: if hasattr(self.current_tab, 'addSubWindow'): pass else: for tab in range(self.tab_count): page = self.tab(tab) if hasattr(page, 'addSubWindow'): break else: self.new_tab() tab = self.tab_count - 1 page = self.tab(tab) pos = getattr(new_widget, 'position', None) sub = new_widget.mdi_wrap() sub.closed.connect(self._update_viewer_in_focus) if label: sub.setWindowTitle(label) page.addSubWindow(sub) page.setActiveSubWindow(sub) if hold_position and pos is not None: new_widget.move(pos[0], pos[1]) self.tab_widget.setCurrentWidget(page) return sub def _edit_settings(self, *args): self._editor = PreferencesDialog(self, parent=self) self._editor.show() def gather_current_tab(self, *args): """Arrange windows in current tab via tiling""" self.current_tab.tileSubWindows() def _get_plot_dashboards(self, widget): if not isinstance(widget, DataViewer): return QtWidgets.QWidget(), QtWidgets.QWidget(), "" layer_view = widget.layer_view() options_widget = widget.options_widget() return layer_view, options_widget, str(widget) def _clear_dashboard(self): for widget, title in [(self._ui.plot_layers, "Plot Layers"), (self._ui.plot_options, "Plot Options")]: layout = widget.layout() if layout is None: layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(4, 4, 4, 4) widget.setLayout(layout) while layout.count(): layout.takeAt(0).widget().hide() self._ui.plot_options_label.setText("Plot Options") self._ui.plot_layers_label.setText("Plot Layers") def _update_plot_dashboard(self, *args): self._clear_dashboard() if self._viewer_in_focus is None: return layer_view, options_widget, title = self._get_plot_dashboards(self._viewer_in_focus) layout = self._ui.plot_layers.layout() layout.addWidget(layer_view) layout = self._ui.plot_options.layout() layout.addWidget(options_widget) layer_view.show() options_widget.show() if title: self._ui.plot_options_label.setText("Plot Options - %s" % title) self._ui.plot_layers_label.setText("Plot Layers - %s" % title) else: self._ui.plot_options_label.setText("Plot Options") self._ui.plot_layers_label.setText("Plot Layers") self._update_focus_decoration() def _update_focus_decoration(self): mdi_area = self.current_tab for win in mdi_area.subWindowList(): widget = win.widget() if isinstance(widget, DataViewer): widget.set_focus(widget is self._viewer_in_focus) def _connect(self): self.setAcceptDrops(True) self._layer_widget.setup(self._data) self.tab_widget.tabCloseRequested.connect(self.close_tab) self.tab_widget.currentChanged.connect(self._update_viewer_in_focus) def _create_menu(self): mbar = self.menuBar() menu = QtWidgets.QMenu(mbar) menu.setTitle("&File") menu.addAction(self._actions['data_new']) if 'data_importers' in self._actions: submenu = menu.addMenu("I&mport data") for a in self._actions['data_importers']: submenu.addAction(a) # menu.addAction(self._actions['data_save']) # XXX add this menu.addAction(self._actions['session_reset']) menu.addAction(self._actions['session_restore']) menu.addAction(self._actions['session_save']) menu.addAction(self._actions['export_data']) if 'session_export' in self._actions: submenu = menu.addMenu("Advanced E&xporters") for a in self._actions['session_export']: submenu.addAction(a) menu.addSeparator() menu.addAction("Edit &Preferences", self._edit_settings) # Here we use close instead of self.app.quit because if we are launching # glue from an environment with a Qt event loop already existing, we # don't want to quit this. Using close here is safer, though it does # mean that any dialog we launch from glue has to be either modal (to # prevent quitting) or correctly define its parent so that it gets # closed too. menu.addAction("&Quit", self.close) mbar.addMenu(menu) menu = QtWidgets.QMenu(mbar) menu.setTitle("&Edit ") menu.addAction(self._actions['undo']) menu.addAction(self._actions['redo']) mbar.addMenu(menu) menu = QtWidgets.QMenu(mbar) menu.setTitle("&View ") a = QtWidgets.QAction("&Console Log", menu) a.triggered.connect(self._log._show) menu.addAction(a) mbar.addMenu(menu) menu = QtWidgets.QMenu(mbar) menu.setTitle("&Canvas") menu.addAction(self._actions['tab_new']) menu.addAction(self._actions['viewer_new']) menu.addAction(self._actions['fixed_layout_tab_new']) menu.addSeparator() menu.addAction(self._actions['gather']) menu.addAction(self._actions['tab_rename']) mbar.addMenu(menu) menu = QtWidgets.QMenu(mbar) menu.setTitle("Data &Manager") menu.addActions(self._layer_widget.actions()) mbar.addMenu(menu) menu = QtWidgets.QMenu(mbar) menu.setTitle("&Plugins") menu.addAction(self._actions['plugin_manager']) menu.addSeparator() if 'plugins' in self._actions: for plugin in self._actions['plugins']: menu.addAction(plugin) mbar.addMenu(menu) # trigger inclusion of Mac Native "Help" tool menu = mbar.addMenu("&Help") a = QtWidgets.QAction("&Online Documentation", menu) a.triggered.connect(nonpartial(webbrowser.open, DOCS_URL)) menu.addAction(a) a = QtWidgets.QAction("Send &Feedback", menu) a.triggered.connect(nonpartial(submit_feedback)) menu.addAction(a) menu.addSeparator() menu.addAction("Version information", self._show_glue_info) def _show_glue_info(self): window = QVersionsDialog(parent=self) window.show() window.exec_() def _choose_load_data_wizard(self, *args): self._choose_load_data(data_importer=data_wizard) def _choose_load_data(self, data_importer=None): if data_importer is None: self.add_datasets(self.data_collection, data_wizard()) else: data = data_importer() if not isinstance(data, list): raise TypeError("Data loader should return list of " "Data objects") for item in data: if not isinstance(item, Data): raise TypeError("Data loader should return list of " "Data objects") self.add_datasets(self.data_collection, data) def _choose_save_data(self, *args): dialog = SaveDataDialog(data_collection=self.data_collection, parent=self) dialog.exec_() def _create_actions(self): """ Create and connect actions, store in _actions dict """ self._actions = {} a = action("&New Data Viewer", self, tip="Open a new visualization window in the current tab", shortcut=QtGui.QKeySequence.New) a.triggered.connect(self._choose_new_data_viewer_nodata) self._actions['viewer_new'] = a if len(qt_client.members) == 0: a.setEnabled(False) a = action("New Fixed Layout Tab", self, tip="Create a new tab with a fixed layout") a.triggered.connect(self.choose_new_fixed_layout_tab) self._actions['fixed_layout_tab_new'] = a if len(qt_fixed_layout_tab.members) == 0: a.setEnabled(False) a = action('New &Tab', self, shortcut=QtGui.QKeySequence.AddTab, tip='Add a new tab') a.triggered.connect(self.new_tab) self._actions['tab_new'] = a a = action('&Rename Tab', self, shortcut="Ctrl+R", tip='Set a new label for the current tab') a.triggered.connect(nonpartial(self.tab_bar.choose_rename_tab)) self._actions['tab_rename'] = a a = action('&Gather Windows', self, tip='Gather plot windows side-by-side', shortcut='Ctrl+G') a.triggered.connect(self.gather_current_tab) self._actions['gather'] = a a = action('&Export Session', self, tip='Save the current session') a.triggered.connect(self._choose_save_session) self._actions['session_save'] = a # Add file loader as first item in File menu for convenience. We then # also add it again below in the Import menu for consistency. a = action("&Open Data Set", self, tip="Open a new data set", shortcut=QtGui.QKeySequence.Open) a.triggered.connect(self._choose_load_data_wizard) self._actions['data_new'] = a # We now populate the "Import data" menu from glue.config import importer acts = [] # Add default file loader (later we can add this to the registry) a = action("Import from file", self, tip="Import from file") a.triggered.connect(self._choose_load_data_wizard) acts.append(a) for i in importer: label, data_importer = i a = action(label, self, tip=label) a.triggered.connect(self._choose_load_data_wizard) acts.append(a) self._actions['data_importers'] = acts from glue.config import exporters if len(exporters) > 0: acts = [] for e in exporters: label, saver, checker, mode = e a = action(label, self, tip='Export the current session to %s format' % label) a.triggered.connect(nonpartial(self._export_helper._choose_export_session, saver, checker, mode)) acts.append(a) self._actions['session_export'] = acts a = action('Open S&ession', self, tip='Restore a saved session') a.triggered.connect(self._restore_session) self._actions['session_restore'] = a a = action('Reset S&ession', self, tip='Reset session to clean state') a.triggered.connect(self._reset_session) self._actions['session_reset'] = a a = action('Export D&ata/Subsets', self, tip='Export data to a file') a.triggered.connect(self._choose_save_data) self._actions['export_data'] = a a = action("Undo", self, tip='Undo last action', shortcut=QtGui.QKeySequence.Undo) a.triggered.connect(self.undo) a.setEnabled(False) self._actions['undo'] = a a = action("Redo", self, tip='Redo last action', shortcut=QtGui.QKeySequence.Redo) a.triggered.connect(self.redo) a.setEnabled(False) self._actions['redo'] = a # Create actions for menubar plugins from glue.config import menubar_plugin acts = [] for label, function in menubar_plugin: a = action(label, self, tip=label) a.triggered.connect(nonpartial(function, self.session, self.data_collection)) acts.append(a) self._actions['plugins'] = acts a = action('&Plugin Manager', self, tip='Open plugin manager') a.triggered.connect(self.plugin_manager) self._actions['plugin_manager'] = a def undo(self, *args): super(GlueApplication, self).undo() def redo(self, *args): super(GlueApplication, self).redo() def choose_new_fixed_layout_tab(self, *args): """ Creates a new tab with a fixed layout """ tab_cls = pick_class(list(qt_fixed_layout_tab.members), title='Fixed layout tab', label="Choose a new fixed layout tab", sort=True) return self.add_fixed_layout_tab(tab_cls) def add_fixed_layout_tab(self, tab_cls): tab = tab_cls(session=self.session) self._total_tab_count += 1 name = 'Tab {0}'.format(self._total_tab_count) if hasattr(tab, 'LABEL'): name += ': ' + tab.LABEL self.tab_widget.addTab(tab, name) self.tab_widget.setCurrentWidget(tab) tab.subWindowActivated.connect(self._update_viewer_in_focus) return tab def _choose_new_data_viewer_nodata(self): self.choose_new_data_viewer() def choose_new_data_viewer(self, data=None): """ Create a new visualization window in the current tab """ if data and data.ndim == 1 and ScatterViewer in qt_client.members: default = ScatterViewer elif data and data.ndim > 1 and ImageViewer in qt_client.members: default = ImageViewer else: default = None client = pick_class(list(qt_client.members), title='Data Viewer', label="Choose a new data viewer", default=default, sort=True) if client is None: return cmd = command.NewDataViewer(viewer=client, data=data) return self.do(cmd) new_data_viewer = defer_draw(Application.new_data_viewer) def _choose_save_session(self, *args): """ Save the data collection and hub to file. Can be restored via restore_session """ # include file filter twice, so it shows up in Dialog outfile, file_filter = compat.getsavefilename( parent=self, basedir=getattr(self, '_last_session_name', 'session.glu'), filters=("Glue Session with absolute paths to data(*.glu);; " "Glue Session with relative paths to data(*.glu);; " "Glue Session including data (*.glu)"), selectedfilter=getattr(self, '_last_session_filter', '')) # This indicates that the user cancelled if not outfile: return # Add extension if not specified if '.' not in outfile: outfile += '.glu' self._last_session_name = outfile self._last_session_filter = file_filter with set_cursor_cm(Qt.WaitCursor): self.save_session(outfile, include_data="including data" in file_filter, absolute_paths="absolute" in file_filter) @messagebox_on_error("Failed to restore session") def _restore_session(self, *args): """ Load a previously-saved state, and restart the session """ fltr = "Glue sessions (*.glu)" file_name, file_filter = compat.getopenfilename( parent=self, filters=fltr) if not file_name: return ga = self.restore_session_and_close(file_name) return ga @property def is_empty(self): """ Returns `True` if there are no viewers and no data. """ return (len([viewer for tab in self.viewers for viewer in tab]) == 0 and len(self.data_collection) == 0) def _reset_session(self, *args, **kwargs): """ Reset session to clean state. """ warn = kwargs.pop('warn', False) if not os.environ.get('GLUE_TESTING') and warn and not self.is_empty: buttons = QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel dialog = QtWidgets.QMessageBox.warning( self, "Confirm Close", "Are you sure you want to reset the session? " "This will close all datasets, subsets, and data viewers", buttons=buttons, defaultButton=QtWidgets.QMessageBox.Cancel) if not dialog == QtWidgets.QMessageBox.Ok: return # Make sure the closeEvent gets executed to close the GlueLogger self._log.close() if self.app is not None: self.app.processEvents() ga = GlueApplication() ga.start(block=False) # NOTE: we need to keep a reference to this new application object # otherwise it will immediately garbage collect - this is a hack and # we should find a better solution in future. self._new_application = ga # We need to close this after we open the next application otherwise # Qt will quit since there are no actively open windows. self.close() return ga @staticmethod def restore_session(path, show=True): """ Reload a previously-saved session Parameters ---------- path : str Path to the file to load show : bool, optional If True (the default), immediately show the widget Returns ------- app : :class:`glue.app.qt.application.GlueApplication` The loaded application """ ga = Application.restore_session(path) if show: ga.start(block=False) return ga def has_terminal(self, create_if_not=True): """ Returns True if the IPython terminal is present. """ if self._terminal is None and create_if_not: self._create_terminal() return self._terminal is not None def _create_terminal(self): if self._terminal is not None: # already set up return try: widget = glue_terminal(data_collection=self._data, dc=self._data, hub=self._hub, session=self.session, application=self, **vars(env)) except IPythonTerminalError: self._button_ipython.setEnabled(False) else: self._terminal = self.add_widget(widget) self._terminal.closed.connect(self._on_terminal_close) self._hide_terminal() def _toggle_terminal(self): if self._terminal is None: self._create_terminal() if self._terminal.isVisible(): self._hide_terminal() if self._terminal.isVisible(): warnings.warn("An unexpected error occurred while " "trying to hide the terminal") else: self._show_terminal() if not self._terminal.isVisible(): warnings.warn("An unexpected error occurred while " "trying to show the terminal") def _on_terminal_close(self): if self._button_ipython.isChecked(): self._button_ipython.blockSignals(True) self._button_ipython.setChecked(False) self._button_ipython.blockSignals(False) def _hide_terminal(self): self._terminal.hide() def _show_terminal(self): self._terminal.show() self._terminal.widget().show() def start(self, size=None, position=None, block=True, maximized=True): """ Show the GUI and start the application. Parameters ---------- size : (int, int) Optional The default width/height of the application. If not provided, uses the full screen position : (int, int) Optional The default position of the application """ if maximized: self.showMaximized() else: self.show() if size is not None: self.resize(*size) if position is not None: self.move(*position) self.raise_() # bring window to front # at some point during all this, the MPL backend # switches. This call restores things, so # figures are still inlined in the notebook. # XXX find out a better place for this _fix_ipython_pylab() if block: return self.app.exec_() exec_ = start def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.accept() else: event.ignore() @messagebox_on_error("Failed to load files") def dropEvent(self, event): urls = event.mimeData().urls() paths = [qurl_to_path(url) for url in urls] if any(path.endswith('.glu') for path in paths): if len(paths) != 1: raise Exception("When dragging and dropping files onto glue, " "only a single .glu session file can be " "dropped at a time, or multiple datasets, but " "not a mix of both.") else: self.restore_session_and_close(paths[0]) else: self.load_data(paths) event.accept() @messagebox_on_error("Failed to restore session") def restore_session_and_close(self, path, warn=True): if warn and not self.is_empty: buttons = QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel dialog = QtWidgets.QMessageBox.warning( self, "Confirm Close", "Loading a session file will close the existing session. Are you " "sure you want to continue?", buttons=buttons, defaultButton=QtWidgets.QMessageBox.Cancel) if not dialog == QtWidgets.QMessageBox.Ok: return with set_cursor_cm(Qt.WaitCursor): app = self.restore_session(path) app.setGeometry(self.geometry()) # NOTE: we need to keep a reference to this new application object # otherwise it will immediately garbage collect - this is a hack and # we should find a better solution in future. self._new_application = app self.close() def closeEvent(self, event): """Emit a message to hub before closing.""" # Clear the namespace in the terminal to avoid cicular references if self._terminal is not None: self._terminal.widget().clear_ns(['data_collection', 'dc', 'hub', 'session', 'application']) for tab in self.viewers: for viewer in tab: viewer.close(warn=False) self._viewer_in_focus = None self._clear_dashboard() self._log.close() self._hub.broadcast(ApplicationClosedMessage(None)) event.accept() if self._original_app is not None: self._original_app.setWindowIcon(self._original_icon) self._original_app = None self.app = None def report_error(self, message, detail): """ Display an error in a modal :param message: A short description of the error :type message: str :param detail: A longer description :type detail: str """ qmb = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Critical, "Error", message) qmb.setDetailedText(detail) qmb.resize(400, qmb.size().height()) qmb.exec_() def plugin_manager(self, *args): from glue.main import _installed_plugins pm = QtPluginManager(installed=_installed_plugins) pm.ui.exec_() def _update_undo_redo_enabled(self, *args): undo, redo = self._cmds.can_undo_redo() self._actions['undo'].setEnabled(undo) self._actions['redo'].setEnabled(redo) self._actions['undo'].setText('Undo ' + self._cmds.undo_label) self._actions['redo'].setText('Redo ' + self._cmds.redo_label) @property def viewers(self): """ A list of lists of open Data Viewers. Each inner list contains the viewers open on a particular tab. """ result = [] for t in range(self.tab_count): tab = self.tab(t) item = [] for subwindow in tab.subWindowList(): widget = subwindow.widget() if isinstance(widget, DataViewer): item.append(widget) result.append(tuple(item)) return tuple(result) @property def tab_names(self): """ The name of each tab A list of strings """ return [self.tab_bar.tabText(i) for i in range(self.tab_count)] @tab_names.setter def tab_names(self, values): for index, value in enumerate(values): self.tab_bar.setTabText(index, value) @staticmethod def _choose_merge(data, others, suggested_label): w = load_ui('merge.ui', None, directory=os.path.dirname(__file__)) w.button_yes.clicked.connect(w.accept) w.button_no.clicked.connect(w.reject) w.show() w.raise_() # Add the main dataset to the list. Some of the 'others' may also be # new ones, so it doesn't really make sense to distinguish between # the two here. The main point is that some datasets, including at # least one new one, have a common shape. others.append(data) others.sort(key=lambda x: x.label) for i, d in enumerate(others): if isinstance(d.coords, WCSCoordinates): if i == 0: break else: others[0], others[i] = others[i], others[0] break w.merged_label.setText(suggested_label) entries = [QtWidgets.QListWidgetItem(other.label) for other in others] for e in entries: e.setCheckState(Qt.Checked) for d, item in zip(others, entries): w.choices.addItem(item) if not w.exec_(): return None, None result = [layer for layer, entry in zip(others, entries) if entry.checkState() == Qt.Checked] if result: return result, str(w.merged_label.text()) return None, None def __gluestate__(self, context): state = super(GlueApplication, self).__gluestate__(context) state['tab_names'] = self.tab_names return state @classmethod def __setgluestate__(cls, rec, context): self = super(GlueApplication, cls).__setgluestate__(rec, context) if 'tab_names' in rec: self.tab_names = rec['tab_names'] return self
class GlueApplication(Application, QtWidgets.QMainWindow): """ The main GUI application for the Qt frontend""" def __init__(self, data_collection=None, session=None, maximized=True): self.app = get_qapp() QtWidgets.QMainWindow.__init__(self) Application.__init__(self, data_collection=data_collection, session=session) self.app.setQuitOnLastWindowClosed(True) icon = get_icon('app_icon') self.app.setWindowIcon(icon) # Even though we loaded the plugins in start_glue, we re-load them here # in case glue was started directly by initializing this class. load_plugins() self.setWindowTitle("Glue") self.setWindowIcon(icon) self.setAttribute(Qt.WA_DeleteOnClose) self._actions = {} self._terminal = None self._setup_ui() self.tab_widget.setMovable(True) self.tab_widget.setTabsClosable(True) # The following is a counter that never goes down, even if tabs are # deleted (this is by design, to avoid having two tabs called the # same if a tab is removed then a new one added again) self._total_tab_count = 0 lwidget = self._layer_widget a = PlotAction(lwidget, self) lwidget.ui.layerTree.addAction(a) lwidget.bind_selection_to_edit_subset() self._tweak_geometry(maximized=maximized) self._create_actions() self._create_menu() self._connect() self.new_tab() self._update_plot_dashboard(None) def _setup_ui(self): self._ui = load_ui('application.ui', None, directory=os.path.dirname(__file__)) self.setCentralWidget(self._ui) self._ui.tabWidget.setTabBar(GlueTabBar()) lw = LayerTreeWidget() lw.set_checkable(False) self._vb = QtWidgets.QVBoxLayout() self._vb.setContentsMargins(0, 0, 0, 0) self._vb.addWidget(lw) self._ui.data_layers.setLayout(self._vb) self._layer_widget = lw # Data toolbar self._data_toolbar = QtWidgets.QToolBar() self._data_toolbar.setIconSize(QtCore.QSize(16, 16)) self._button_open_data = QtWidgets.QToolButton() self._button_open_data.setText("Open Data") self._button_open_data.setIcon(get_icon('glue_open')) self._button_open_data.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_open_data.clicked.connect( nonpartial(self._choose_load_data)) self._data_toolbar.addWidget(self._button_open_data) self._button_link_data = QtWidgets.QToolButton() self._button_link_data.setText("Link Data") self._button_link_data.setIcon(get_icon('glue_link')) self._button_link_data.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_link_data.clicked.connect(self._set_up_links) self._data_toolbar.addWidget(self._button_link_data) self._button_ipython = QtWidgets.QToolButton() self._button_ipython.setCheckable(True) self._button_ipython.setText("IPython Terminal") self._button_ipython.setIcon(get_icon('IPythonConsole')) self._button_ipython.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_ipython.clicked.connect(self._toggle_terminal) self._data_toolbar.addWidget(self._button_ipython) self._button_open_data = QtWidgets.QToolButton() self._button_open_data.setText("Open Session") self._button_open_data.setIcon(get_icon('glue_open')) self._button_open_data.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_open_data.clicked.connect( nonpartial(self._restore_session)) self._data_toolbar.addWidget(self._button_open_data) self._button_open_data = QtWidgets.QToolButton() self._button_open_data.setText("Save Session") self._button_open_data.setIcon(get_icon('glue_filesave')) self._button_open_data.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._button_open_data.clicked.connect( nonpartial(self._choose_save_session)) self._data_toolbar.addWidget(self._button_open_data) spacer = QtWidgets.QWidget() spacer.setMinimumSize(20, 10) spacer.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) self._data_toolbar.addWidget(spacer) self.addToolBar(self._data_toolbar) # Selection mode toolbar tbar = EditSubsetModeToolBar() self._mode_toolbar = tbar self.addToolBar(self._mode_toolbar) # Error console toolbar self._console_toolbar = QtWidgets.QToolBar() self._console_toolbar.setIconSize(QtCore.QSize(16, 16)) spacer = QtWidgets.QWidget() spacer.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) self._console_toolbar.addWidget(spacer) self._button_preferences = QtWidgets.QToolButton() self._button_preferences.setText("Preferences") self._button_preferences.setIcon(get_icon('glue_settings')) self._button_preferences.setToolButtonStyle( Qt.ToolButtonTextBesideIcon) self._button_preferences.clicked.connect( nonpartial(self._edit_settings)) self._console_toolbar.addWidget(self._button_preferences) self._button_console = QtWidgets.QToolButton() self._button_console.setText("View Error Console") self._button_console.setToolButtonStyle(Qt.ToolButtonTextOnly) self._console_toolbar.addWidget(self._button_console) self.addToolBar(self._console_toolbar) self._log = GlueLogger(button_console=self._button_console) self._log.window().setWindowTitle("Console Log") self._log.resize(550, 550) self._log.hide() def _set_up_links(self, event): LinkEditor.update_links(self.data_collection) def _tweak_geometry(self, maximized=True): """Maximize window by default.""" if maximized: self.setWindowState(Qt.WindowMaximized) self._ui.main_splitter.setStretchFactor(0, 0.1) self._ui.main_splitter.setStretchFactor(1, 0.9) self._ui.data_plot_splitter.setStretchFactor(0, 0.25) self._ui.data_plot_splitter.setStretchFactor(1, 0.5) self._ui.data_plot_splitter.setStretchFactor(2, 0.25) @property def tab_widget(self): return self._ui.tabWidget @property def tab_bar(self): return self._ui.tabWidget.tabBar() @property def tab_count(self): """ The number of open tabs """ return self._ui.tabWidget.count() @property def current_tab(self): return self._ui.tabWidget.currentWidget() def tab(self, index=None): if index is None: return self.current_tab return self._ui.tabWidget.widget(index) def new_tab(self): """Spawn a new tab page""" layout = QtWidgets.QGridLayout() layout.setSpacing(1) layout.setContentsMargins(0, 0, 0, 0) widget = GlueMdiArea(self) widget.setLayout(layout) tab = self.tab_widget self._total_tab_count += 1 tab.addTab(widget, str("Tab %i" % self._total_tab_count)) tab.setCurrentWidget(widget) widget.subWindowActivated.connect(self._update_plot_dashboard) def close_tab(self, index): """ Close a tab window and all associated data viewers """ # do not delete the last tab if self.tab_widget.count() == 1: return if not os.environ.get('GLUE_TESTING'): buttons = QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel dialog = QtWidgets.QMessageBox.warning( self, "Confirm Close", "Are you sure you want to close this tab? " "This will close all data viewers in the tab.", buttons=buttons, defaultButton=QtWidgets.QMessageBox.Cancel) if not dialog == QtWidgets.QMessageBox.Ok: return w = self.tab_widget.widget(index) for window in w.subWindowList(): widget = window.widget() if isinstance(widget, DataViewer): widget.close(warn=False) w.close() self.tab_widget.removeTab(index) def add_widget(self, new_widget, label=None, tab=None, hold_position=False): """ Add a widget to one of the tabs. Returns the window that this widget is wrapped in. :param new_widget: new QtWidgets.QWidget to add :param label: label for the new window. Optional :type label: str :param tab: Tab to add to. Optional (default: current tab) :type tab: int :param hold_position: If True, then override Qt's default placement and retain the original position of new_widget :type hold_position: bool """ page = self.tab(tab) pos = getattr(new_widget, 'position', None) sub = new_widget.mdi_wrap() sub.closed.connect(self._clear_dashboard) if label: sub.setWindowTitle(label) page.addSubWindow(sub) page.setActiveSubWindow(sub) if hold_position and pos is not None: new_widget.move(pos[0], pos[1]) return sub def _edit_settings(self): self._editor = PreferencesDialog(self) self._editor.show() def gather_current_tab(self): """Arrange windows in current tab via tiling""" self.current_tab.tileSubWindows() def _get_plot_dashboards(self, sub_window): if not isinstance(sub_window, GlueMdiSubWindow): return QtWidgets.QWidget(), QtWidgets.QWidget(), "" widget = sub_window.widget() if not isinstance(widget, DataViewer): return QtWidgets.QWidget(), QtWidgets.QWidget(), "" layer_view = widget.layer_view() options_widget = widget.options_widget() return layer_view, options_widget, str(widget) def _clear_dashboard(self): for widget, title in [(self._ui.plot_layers, "Plot Layers"), (self._ui.plot_options, "Plot Options")]: layout = widget.layout() if layout is None: layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(4, 4, 4, 4) widget.setLayout(layout) while layout.count(): layout.takeAt(0).widget().hide() self._ui.plot_options_label.setText("Plot Options") self._ui.plot_layers_label.setText("Plot Layers") def _update_plot_dashboard(self, sub_window): self._clear_dashboard() if sub_window is None: return layer_view, options_widget, title = self._get_plot_dashboards( sub_window) layout = self._ui.plot_layers.layout() layout.addWidget(layer_view) layout = self._ui.plot_options.layout() layout.addWidget(options_widget) layer_view.show() options_widget.show() if title: self._ui.plot_options_label.setText("Plot Options - %s" % title) self._ui.plot_layers_label.setText("Plot Layers - %s" % title) else: self._ui.plot_options_label.setText("Plot Options") self._ui.plot_layers_label.setText("Plot Layers") self._update_focus_decoration() def _update_focus_decoration(self): mdi_area = self.current_tab active = mdi_area.activeSubWindow() for win in mdi_area.subWindowList(): widget = win.widget() if isinstance(widget, DataViewer): widget.set_focus(win is active) def _connect(self): self.setAcceptDrops(True) self._layer_widget.setup(self._data) self.tab_widget.tabCloseRequested.connect(self.close_tab) def _create_menu(self): mbar = self.menuBar() menu = QtWidgets.QMenu(mbar) menu.setTitle("&File") menu.addAction(self._actions['data_new']) if 'data_importers' in self._actions: submenu = menu.addMenu("I&mport data") for a in self._actions['data_importers']: submenu.addAction(a) # menu.addAction(self._actions['data_save']) # XXX add this menu.addAction(self._actions['session_reset']) menu.addAction(self._actions['session_restore']) menu.addAction(self._actions['session_save']) if 'session_export' in self._actions: submenu = menu.addMenu("E&xport") for a in self._actions['session_export']: submenu.addAction(a) menu.addSeparator() menu.addAction("Edit &Preferences", self._edit_settings) menu.addAction("&Quit", self.app.quit) mbar.addMenu(menu) menu = QtWidgets.QMenu(mbar) menu.setTitle("&Edit ") menu.addAction(self._actions['undo']) menu.addAction(self._actions['redo']) mbar.addMenu(menu) menu = QtWidgets.QMenu(mbar) menu.setTitle("&View ") a = QtWidgets.QAction("&Console Log", menu) a.triggered.connect(self._log._show) menu.addAction(a) mbar.addMenu(menu) menu = QtWidgets.QMenu(mbar) menu.setTitle("&Canvas") menu.addAction(self._actions['tab_new']) menu.addAction(self._actions['viewer_new']) menu.addSeparator() menu.addAction(self._actions['gather']) menu.addAction(self._actions['tab_rename']) mbar.addMenu(menu) menu = QtWidgets.QMenu(mbar) menu.setTitle("Data &Manager") menu.addActions(self._layer_widget.actions()) mbar.addMenu(menu) menu = QtWidgets.QMenu(mbar) menu.setTitle("&Plugins") menu.addAction(self._actions['plugin_manager']) menu.addSeparator() if 'plugins' in self._actions: for plugin in self._actions['plugins']: menu.addAction(plugin) mbar.addMenu(menu) # trigger inclusion of Mac Native "Help" tool menu = mbar.addMenu("&Help") a = QtWidgets.QAction("&Online Documentation", menu) a.triggered.connect(nonpartial(webbrowser.open, DOCS_URL)) menu.addAction(a) a = QtWidgets.QAction("Send &Feedback", menu) a.triggered.connect(nonpartial(submit_feedback)) menu.addAction(a) menu.addSeparator() menu.addAction("Version information", show_glue_info) def _choose_load_data(self, data_importer=None): if data_importer is None: self.add_datasets(self.data_collection, data_wizard()) else: data = data_importer() if not isinstance(data, list): raise TypeError("Data loader should return list of " "Data objects") for item in data: if not isinstance(item, Data): raise TypeError("Data loader should return list of " "Data objects") self.add_datasets(self.data_collection, data) def _create_actions(self): """ Create and connect actions, store in _actions dict """ self._actions = {} a = action("&New Data Viewer", self, tip="Open a new visualization window in the current tab", shortcut=QtGui.QKeySequence.New) a.triggered.connect(nonpartial(self.choose_new_data_viewer)) self._actions['viewer_new'] = a a = action('New &Tab', self, shortcut=QtGui.QKeySequence.AddTab, tip='Add a new tab') a.triggered.connect(nonpartial(self.new_tab)) self._actions['tab_new'] = a a = action('&Rename Tab', self, shortcut="Ctrl+R", tip='Set a new label for the current tab') a.triggered.connect(nonpartial(self.tab_bar.rename_tab)) self._actions['tab_rename'] = a a = action('&Gather Windows', self, tip='Gather plot windows side-by-side', shortcut='Ctrl+G') a.triggered.connect(nonpartial(self.gather_current_tab)) self._actions['gather'] = a a = action('&Save Session', self, tip='Save the current session') a.triggered.connect(nonpartial(self._choose_save_session)) self._actions['session_save'] = a # Add file loader as first item in File menu for convenience. We then # also add it again below in the Import menu for consistency. a = action("&Open Data Set", self, tip="Open a new data set", shortcut=QtGui.QKeySequence.Open) a.triggered.connect(nonpartial(self._choose_load_data, data_wizard)) self._actions['data_new'] = a # We now populate the "Import data" menu from glue.config import importer acts = [] # Add default file loader (later we can add this to the registry) a = action("Import from file", self, tip="Import from file") a.triggered.connect(nonpartial(self._choose_load_data, data_wizard)) acts.append(a) for i in importer: label, data_importer = i a = action(label, self, tip=label) a.triggered.connect( nonpartial(self._choose_load_data, data_importer)) acts.append(a) self._actions['data_importers'] = acts from glue.config import exporters if len(exporters) > 0: acts = [] for e in exporters: label, saver, checker, mode = e a = action(label, self, tip='Export the current session to %s format' % label) a.triggered.connect( nonpartial(self._choose_export_session, saver, checker, mode)) acts.append(a) self._actions['session_export'] = acts a = action('Open S&ession', self, tip='Restore a saved session') a.triggered.connect(nonpartial(self._restore_session)) self._actions['session_restore'] = a a = action('Reset S&ession', self, tip='Reset session to clean state') a.triggered.connect(nonpartial(self._reset_session)) self._actions['session_reset'] = a a = action("Undo", self, tip='Undo last action', shortcut=QtGui.QKeySequence.Undo) a.triggered.connect(nonpartial(self.undo)) a.setEnabled(False) self._actions['undo'] = a a = action("Redo", self, tip='Redo last action', shortcut=QtGui.QKeySequence.Redo) a.triggered.connect(nonpartial(self.redo)) a.setEnabled(False) self._actions['redo'] = a # Create actions for menubar plugins from glue.config import menubar_plugin acts = [] for label, function in menubar_plugin: a = action(label, self, tip=label) a.triggered.connect( nonpartial(function, self.session, self.data_collection)) acts.append(a) self._actions['plugins'] = acts a = action('&Plugin Manager', self, tip='Open plugin manager') a.triggered.connect(nonpartial(self.plugin_manager)) self._actions['plugin_manager'] = a def choose_new_data_viewer(self, data=None): """ Create a new visualization window in the current tab """ from glue.config import qt_client if data and data.ndim == 1 and ScatterViewer in qt_client.members: default = ScatterViewer elif data and data.ndim > 1 and ImageViewer in qt_client.members: default = ImageViewer else: default = None client = pick_class(list(qt_client.members), title='Data Viewer', label="Choose a new data viewer", default=default, sort=True) cmd = command.NewDataViewer(viewer=client, data=data) return self.do(cmd) new_data_viewer = defer_draw(Application.new_data_viewer) def _choose_save_session(self): """ Save the data collection and hub to file. Can be restored via restore_session """ # include file filter twice, so it shows up in Dialog outfile, file_filter = compat.getsavefilename( parent=self, filters=("Glue Session (*.glu);; " "Glue Session including data (*.glu)")) # This indicates that the user cancelled if not outfile: return # Add extension if not specified if '.' not in outfile: outfile += '.glu' with set_cursor_cm(Qt.WaitCursor): self.save_session(outfile, include_data="including data" in file_filter) @messagebox_on_error("Failed to export session") def _choose_export_session(self, saver, checker, outmode): checker(self) if outmode is None: return saver(self) elif outmode in ['file', 'directory']: outfile, file_filter = compat.getsavefilename(parent=self) if not outfile: return return saver(self, outfile) else: assert outmode == 'label' label, ok = QtWidgets.QInputDialog.getText(self, 'Choose a label:', 'Choose a label:') if not ok: return return saver(self, label) @messagebox_on_error("Failed to restore session") def _restore_session(self, show=True): """ Load a previously-saved state, and restart the session """ fltr = "Glue sessions (*.glu)" file_name, file_filter = compat.getopenfilename(parent=self, filters=fltr) if not file_name: return with set_cursor_cm(Qt.WaitCursor): ga = self.restore_session(file_name) self.close() return ga def _reset_session(self, show=True, warn=True): """ Reset session to clean state. """ if not os.environ.get('GLUE_TESTING') and warn: buttons = QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel dialog = QtWidgets.QMessageBox.warning( self, "Confirm Close", "Are you sure you want to reset the session? " "This will close all datasets, subsets, and data viewers", buttons=buttons, defaultButton=QtWidgets.QMessageBox.Cancel) if not dialog == QtWidgets.QMessageBox.Ok: return # Make sure the closeEvent gets executed to close the GlueLogger self._log.close() self.app.processEvents() ga = GlueApplication() ga.start(block=False) # We need to close this after we open the next application otherwise # Qt will quit since there are no actively open windows. self.close() return ga @staticmethod def restore_session(path, show=True): """ Reload a previously-saved session Parameters ---------- path : str Path to the file to load show : bool, optional If True (the default), immediately show the widget Returns ------- app : :class:`glue.app.qt.application.GlueApplication` The loaded application """ ga = Application.restore_session(path) if show: ga.start(block=False) return ga def has_terminal(self, create_if_not=True): """ Returns True if the IPython terminal is present. """ if self._terminal is None and create_if_not: self._create_terminal() return self._terminal is not None def _create_terminal(self): if self._terminal is not None: # already set up return widget = glue_terminal(data_collection=self._data, dc=self._data, hub=self._hub, session=self.session, application=self, **vars(env)) self._terminal = self.add_widget(widget) self._terminal.closed.connect(self._on_terminal_close) self._hide_terminal() def _toggle_terminal(self): if self._terminal.isVisible(): self._hide_terminal() if self._terminal.isVisible(): warnings.warn("An unexpected error occurred while " "trying to hide the terminal") else: self._show_terminal() if not self._terminal.isVisible(): warnings.warn("An unexpected error occurred while " "trying to show the terminal") def _on_terminal_close(self): if self._button_ipython.isChecked(): self._button_ipython.blockSignals(True) self._button_ipython.setChecked(False) self._button_ipython.blockSignals(False) def _hide_terminal(self): self._terminal.hide() def _show_terminal(self): self._terminal.show() self._terminal.widget().show() def start(self, size=None, position=None, block=True): """ Show the GUI and start the application. Parameters ---------- size : (int, int) Optional The default width/height of the application. If not provided, uses the full screen position : (int, int) Optional The default position of the application """ self._create_terminal() self.show() if size is not None: self.resize(*size) if position is not None: self.move(*position) self.raise_() # bring window to front # at some point during all this, the MPL backend # switches. This call restores things, so # figures are still inlined in the notebook. # XXX find out a better place for this _fix_ipython_pylab() if block: return self.app.exec_() exec_ = start def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.accept() else: event.ignore() def dropEvent(self, event): urls = event.mimeData().urls() for url in urls: # Get path to file path = url.path() # Workaround for a Qt bug that causes paths to start with a / # on Windows: https://bugreports.qt.io/browse/QTBUG-46417 if sys.platform.startswith('win'): if path.startswith('/') and path[2] == ':': path = path[1:] self.load_data(path) event.accept() def closeEvent(self, event): """Emit a message to hub before closing.""" self._log.close() self._hub.broadcast(ApplicationClosedMessage(None)) event.accept() def report_error(self, message, detail): """ Display an error in a modal :param message: A short description of the error :type message: str :param detail: A longer description :type detail: str """ qmb = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Critical, "Error", message) qmb.setDetailedText(detail) qmb.resize(400, qmb.size().height()) qmb.exec_() def plugin_manager(self): from glue.main import _installed_plugins pm = QtPluginManager(installed=_installed_plugins) pm.ui.exec_() def _update_undo_redo_enabled(self): undo, redo = self._cmds.can_undo_redo() self._actions['undo'].setEnabled(undo) self._actions['redo'].setEnabled(redo) self._actions['undo'].setText('Undo ' + self._cmds.undo_label) self._actions['redo'].setText('Redo ' + self._cmds.redo_label) @property def viewers(self): """ A list of lists of open Data Viewers. Each inner list contains the viewers open on a particular tab. """ result = [] for t in range(self.tab_count): tab = self.tab(t) item = [] for subwindow in tab.subWindowList(): widget = subwindow.widget() if isinstance(widget, DataViewer): item.append(widget) result.append(tuple(item)) return tuple(result) @property def tab_names(self): """ The name of each tab A list of strings """ return [self.tab_bar.tabText(i) for i in range(self.tab_count)] @tab_names.setter def tab_names(self, values): for index, value in enumerate(values): self.tab_bar.setTabText(index, value) @staticmethod def _choose_merge(data, others): w = load_ui('merge.ui', None, directory=os.path.dirname(__file__)) w.button_yes.clicked.connect(w.accept) w.button_no.clicked.connect(w.reject) w.show() w.raise_() # Add the main dataset to the list. Some of the 'others' may also be # new ones, so it doesn't really make sense to distinguish between # the two here. The main point is that some datasets, including at # least one new one, have a common shape. others.append(data) others.sort(key=lambda x: x.label) label = others[0].label w.merged_label.setText(label) entries = [QtWidgets.QListWidgetItem(other.label) for other in others] for e in entries: e.setCheckState(Qt.Checked) for d, item in zip(others, entries): w.choices.addItem(item) if not w.exec_(): return None, None result = [ layer for layer, entry in zip(others, entries) if entry.checkState() == Qt.Checked ] if result: return result, str(w.merged_label.text()) return None, None def __gluestate__(self, context): state = super(GlueApplication, self).__gluestate__(context) state['tab_names'] = self.tab_names return state @classmethod def __setgluestate__(cls, rec, context): self = super(GlueApplication, cls).__setgluestate__(rec, context) if 'tab_names' in rec: self.tab_names = rec['tab_names'] return self