Exemplo n.º 1
0
class DataViewer(ViewerBase, QtWidgets.QMainWindow):
    """
    Base class for all Qt DataViewer widgets.

    This defines a minimal interface, and implemlements the following::

       * An automatic call to unregister on window close
       * Drag and drop support for adding data
    """

    window_closed = QtCore.Signal()

    _layer_artist_container_cls = QtLayerArtistContainer
    _layer_style_widget_cls = None

    LABEL = 'Override this'

    _toolbar_cls = None
    tools = []

    def __init__(self, session, parent=None):
        """
        :type session: :class:`~glue.core.Session`
        """
        QtWidgets.QMainWindow.__init__(self, parent)
        ViewerBase.__init__(self, session)
        self.setWindowIcon(get_qapp().windowIcon())
        self._view = LayerArtistWidget(layer_style_widget_cls=self._layer_style_widget_cls,
                                       hub=session.hub)
        self._view.layer_list.setModel(self._layer_artist_container.model)
        self._tb_vis = {}  # store whether toolbars are enabled
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setAcceptDrops(True)
        self.setAnimated(False)
        self._toolbars = []
        self._warn_close = True
        self.setContentsMargins(2, 2, 2, 2)
        self._mdi_wrapper = None  # GlueMdiSubWindow that self is embedded in
        self.statusBar().setStyleSheet("QStatusBar{font-size:10px}")

        # close window when last plot layer deleted
        self._layer_artist_container.on_empty(lambda: self.close(warn=False))
        self._layer_artist_container.on_changed(self.update_window_title)

    @property
    def selected_layer(self):
        return self._view.layer_list.current_artist()

    def remove_layer(self, layer):
        self._layer_artist_container.pop(layer)

    def dragEnterEvent(self, event):
        """ Accept the event if it has data layers"""
        if event.mimeData().hasFormat(LAYER_MIME_TYPE):
            event.accept()
        elif event.mimeData().hasFormat(LAYERS_MIME_TYPE):
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        """ Add layers to the viewer if contained in mime data """
        if event.mimeData().hasFormat(LAYER_MIME_TYPE):
            self.request_add_layer(event.mimeData().data(LAYER_MIME_TYPE))

        assert event.mimeData().hasFormat(LAYERS_MIME_TYPE)

        for layer in event.mimeData().data(LAYERS_MIME_TYPE):
            self.request_add_layer(layer)

        event.accept()

    def mousePressEvent(self, event):
        """ Consume mouse press events, and prevent them from propagating
            down to the MDI area """
        event.accept()

    apply_roi = set_cursor(Qt.WaitCursor)(ViewerBase.apply_roi)

    def close(self, warn=True):

        self._warn_close = warn

        if getattr(self, '_mdi_wrapper', None) is not None:
            self._mdi_wrapper.close()
            self._mdi_wrapper = None
        else:
            QtWidgets.QMainWindow.close(self)
            ViewerBase.close(self)

        self._warn_close = True

    def mdi_wrap(self):
        """Wrap this object in a GlueMdiSubWindow"""
        from glue.app.qt.mdi_area import GlueMdiSubWindow
        sub = GlueMdiSubWindow()
        sub.setWidget(self)
        self.destroyed.connect(sub.close)
        sub.resize(self.size())
        self._mdi_wrapper = sub

        return sub

    @property
    def position(self):
        target = self._mdi_wrapper or self
        pos = target.pos()
        return pos.x(), pos.y()

    @position.setter
    def position(self, xy):
        x, y = xy
        self.move(x, y)

    def move(self, x=None, y=None):
        """
        Move the viewer to a new XY pixel location

        You can also set the position attribute to a new tuple directly.

        Parameters
        ----------
        x : int (optional)
           New x position
        y : int (optional)
           New y position
        """
        x0, y0 = self.position
        if x is None:
            x = x0
        if y is None:
            y = y0
        if self._mdi_wrapper is not None:
            self._mdi_wrapper.move(x, y)
        else:
            QtWidgets.QMainWindow.move(self, x, y)

    @property
    def viewer_size(self):
        if self._mdi_wrapper is not None:
            sz = self._mdi_wrapper.size()
        else:
            sz = self.size()
        return sz.width(), sz.height()

    @viewer_size.setter
    def viewer_size(self, value):
        width, height = value
        self.resize(width, height)
        if self._mdi_wrapper is not None:
            self._mdi_wrapper.resize(width, height)

    def closeEvent(self, event):
        """ Call unregister on window close """

        if not self._confirm_close():
            event.ignore()
            return

        if self._hub is not None:
            self.unregister(self._hub)

        self._layer_artist_container.clear_callbacks()
        self._layer_artist_container.clear()

        super(DataViewer, self).closeEvent(event)
        event.accept()

        self.window_closed.emit()

    def _confirm_close(self):
        """Ask for close confirmation

        :rtype: bool. True if user wishes to close. False otherwise
        """
        if self._warn_close and (not os.environ.get('GLUE_TESTING')) and self.isVisible():
            buttons = QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel
            dialog = QtWidgets.QMessageBox.warning(self, "Confirm Close",
                                                   "Do you want to close this window?",
                                                   buttons=buttons,
                                                   defaultButton=QtWidgets.QMessageBox.Cancel)
            return dialog == QtWidgets.QMessageBox.Ok
        return True

    def _confirm_large_data(self, data):
        if not settings.SHOW_LARGE_DATA_WARNING:
            # Ignoring large data warning
            return True
        else:
            warn_msg = ("WARNING: Data set has %i points, and may render slowly."
                        " Continue?" % data.size)
            title = "Add large data set?"
            ok = QtWidgets.QMessageBox.Ok
            cancel = QtWidgets.QMessageBox.Cancel
            buttons = ok | cancel
            result = QtWidgets.QMessageBox.question(self, title, warn_msg,
                                                    buttons=buttons,
                                                    defaultButton=cancel)
            return result == ok

    def layer_view(self):
        return self._view

    def options_widget(self):
        return QtWidgets.QWidget()

    def addToolBar(self, tb):
        super(DataViewer, self).addToolBar(tb)
        self._toolbars.append(tb)
        self._tb_vis[tb] = True

    def initialize_toolbar(self):

        from glue.config import viewer_tool

        self.toolbar = self._toolbar_cls(self)

        for tool_id in self.tools:
            mode_cls = viewer_tool.members[tool_id]
            mode = mode_cls(self)
            self.toolbar.add_tool(mode)

        self.addToolBar(self.toolbar)

    def show_toolbars(self):
        """Re-enable any toolbars that were hidden with `hide_toolbars()`

        Does not re-enable toolbars that were hidden by other means
        """
        for tb in self._toolbars:
            if self._tb_vis.get(tb, False):
                tb.setEnabled(True)

    def hide_toolbars(self):
        """ Disable all the toolbars in the viewer.

        This action can be reversed by calling `show_toolbars()`
        """
        for tb in self._toolbars:
            self._tb_vis[tb] = self._tb_vis.get(tb, False) or tb.isVisible()
            tb.setEnabled(False)

    def set_focus(self, state):
        if state:
            css = """
            DataViewer
            {
            border: 2px solid;
            border-color: rgb(56, 117, 215);
            }
            """
            self.setStyleSheet(css)
            self.show_toolbars()
        else:
            css = """
            DataViewer
            {
            border: none;
            }
            """
            self.setStyleSheet(css)
            self.hide_toolbars()

    def __str__(self):
        return self.LABEL

    def unregister(self, hub):
        """
        Override to perform cleanup operations when disconnecting from hub
        """
        pass

    @property
    def window_title(self):
        return str(self)

    def update_window_title(self):
        self.setWindowTitle(self.window_title)

    def set_status(self, message):
        sb = self.statusBar()
        sb.showMessage(message)
Exemplo n.º 2
0
class DataViewer(ViewerBase, QtWidgets.QMainWindow):
    """
    Base class for all Qt DataViewer widgets.

    This defines a minimal interface, and implemlements the following::

       * An automatic call to unregister on window close
       * Drag and drop support for adding data
    """

    window_closed = QtCore.Signal()
    toolbar_added = QtCore.Signal()

    _layer_artist_container_cls = QtLayerArtistContainer
    _layer_style_widget_cls = None

    LABEL = 'Override this'

    _toolbar_cls = BasicToolbar
    # This defines the mouse mode to be used when no toolbar modes are active
    _default_mouse_mode_cls = None
    _inherit_tools = True
    tools = ['save']
    subtools = {'save': []}

    _close_on_last_layer_removed = True

    _closed = False

    def __init__(self, session, parent=None):
        """
        :type session: :class:`~glue.core.Session`
        """
        QtWidgets.QMainWindow.__init__(self, parent)
        ViewerBase.__init__(self, session)
        self.setWindowIcon(get_qapp().windowIcon())
        self._view = LayerArtistWidget(
            layer_style_widget_cls=self._layer_style_widget_cls,
            hub=session.hub)
        self._view.layer_list.setModel(self._layer_artist_container.model)
        self._tb_vis = {}  # store whether toolbars are enabled
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setAcceptDrops(True)
        self.setAnimated(False)
        self.toolbar = None
        self._toolbars = []
        self._warn_close = True
        self.setContentsMargins(2, 2, 2, 2)
        self._mdi_wrapper = None  # GlueMdiSubWindow that self is embedded in
        self.statusBar().setStyleSheet("QStatusBar{font-size:10px}")

        # close window when last plot layer deleted
        if self._close_on_last_layer_removed:
            self._layer_artist_container.on_empty(
                lambda: self.close(warn=False))
        self._layer_artist_container.on_changed(self.update_window_title)

    @property
    def selected_layer(self):
        return self._view.layer_list.current_artist()

    def remove_layer(self, layer):
        self._layer_artist_container.pop(layer)

    def dragEnterEvent(self, event):
        """ Accept the event if it has data layers"""
        if event.mimeData().hasFormat(LAYER_MIME_TYPE):
            event.accept()
        elif event.mimeData().hasFormat(LAYERS_MIME_TYPE):
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        """ Add layers to the viewer if contained in mime data """
        if event.mimeData().hasFormat(LAYER_MIME_TYPE):
            self.request_add_layer(event.mimeData().data(LAYER_MIME_TYPE))

        assert event.mimeData().hasFormat(LAYERS_MIME_TYPE)

        for layer in event.mimeData().data(LAYERS_MIME_TYPE):
            self.request_add_layer(layer)

        event.accept()

    def mousePressEvent(self, event):
        """ Consume mouse press events, and prevent them from propagating
            down to the MDI area """
        event.accept()

    apply_roi = set_cursor(Qt.WaitCursor)(ViewerBase.apply_roi)

    def close(self, warn=True):

        if self._closed:
            return

        if warn and not self._confirm_close():
            return

        self._warn_close = False

        if getattr(self, '_mdi_wrapper', None) is not None:
            self._mdi_wrapper.close()
            self._mdi_wrapper = None
        else:
            try:
                QtWidgets.QMainWindow.close(self)
            except RuntimeError:
                # In some cases the above can raise a "wrapped C/C++ object of
                # type ... has been deleted" error, in which case we can just
                # ignore and carry on.
                pass
            ViewerBase.close(self)

        # We tell the toolbar to do cleanup to make sure we get rid of any
        # circular references
        if self.toolbar:
            self.toolbar.cleanup()

        self._warn_close = True

        self._closed = True

    def mdi_wrap(self):
        """Wrap this object in a GlueMdiSubWindow"""
        from glue.app.qt.mdi_area import GlueMdiSubWindow
        sub = GlueMdiSubWindow()
        sub.setWidget(self)
        self.destroyed.connect(sub.close)
        sub.resize(self.size())
        self._mdi_wrapper = sub

        return sub

    @property
    def position(self):
        target = self._mdi_wrapper or self
        pos = target.pos()
        return pos.x(), pos.y()

    @position.setter
    def position(self, xy):
        x, y = xy
        self.move(x, y)

    def move(self, x=None, y=None):
        """
        Move the viewer to a new XY pixel location

        You can also set the position attribute to a new tuple directly.

        Parameters
        ----------
        x : int (optional)
           New x position
        y : int (optional)
           New y position
        """
        x0, y0 = self.position
        if x is None:
            x = x0
        if y is None:
            y = y0
        if self._mdi_wrapper is not None:
            self._mdi_wrapper.move(x, y)
        else:
            QtWidgets.QMainWindow.move(self, x, y)

    @property
    def viewer_size(self):
        if self._mdi_wrapper is not None:
            sz = self._mdi_wrapper.size()
        else:
            sz = self.size()
        return sz.width(), sz.height()

    @viewer_size.setter
    def viewer_size(self, value):
        width, height = value
        self.resize(width, height)
        if self._mdi_wrapper is not None:
            self._mdi_wrapper.resize(width, height)

    def closeEvent(self, event):
        """ Call unregister on window close """

        if not self._confirm_close():
            event.ignore()
            return

        if self._hub is not None:
            self.unregister(self._hub)

        self._layer_artist_container.clear_callbacks()
        self._layer_artist_container.clear()

        super(DataViewer, self).closeEvent(event)
        event.accept()

        self.window_closed.emit()

    def isVisible(self):
        # Override this so as to catch RuntimeError: wrapped C/C++ object of
        # type ... has been deleted
        try:
            return self.isVisible()
        except RuntimeError:
            return False

    def _confirm_close(self):
        """Ask for close confirmation

        :rtype: bool. True if user wishes to close. False otherwise
        """
        if self._warn_close and not os.environ.get('GLUE_TESTING'):
            buttons = QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel
            dialog = QtWidgets.QMessageBox.warning(
                self,
                "Confirm Close",
                "Do you want to close this window?",
                buttons=buttons,
                defaultButton=QtWidgets.QMessageBox.Cancel)
            return dialog == QtWidgets.QMessageBox.Ok
        return True

    def layer_view(self):
        return self._view

    def options_widget(self):
        return QtWidgets.QWidget()

    def addToolBar(self, tb):
        super(DataViewer, self).addToolBar(tb)
        self._toolbars.append(tb)
        self._tb_vis[tb] = True

    def initialize_toolbar(self):

        from glue.config import viewer_tool

        self.toolbar = self._toolbar_cls(
            self, default_mouse_mode_cls=self._default_mouse_mode_cls)

        def get_tools_recursive(cls, tools, subtools):
            if not issubclass(cls, DataViewer):
                return
            if cls._inherit_tools and cls is not DataViewer:
                for parent_cls in cls.__bases__:
                    get_tools_recursive(parent_cls, tools, subtools)
            for tool_id in cls.tools:
                if tool_id not in tools:
                    tools.append(tool_id)
            for tool_id in cls.subtools:
                if tool_id not in subtools:
                    subtools[tool_id] = []
                for subtool_id in cls.subtools[tool_id]:
                    if subtool_id not in subtools[tool_id]:
                        subtools[tool_id].append(subtool_id)

        # Need to include tools and subtools declared by parent classes unless
        # specified otherwise
        tool_ids = []
        subtool_ids = {}
        get_tools_recursive(self.__class__, tool_ids, subtool_ids)

        for tool_id in tool_ids:
            mode_cls = viewer_tool.members[tool_id]
            if tool_id in subtool_ids:
                subtools = []
                for subtool_id in subtool_ids[tool_id]:
                    subtools.append(viewer_tool.members[subtool_id](self))
                mode = mode_cls(self, subtools=subtools)
            else:
                mode = mode_cls(self)
            self.toolbar.add_tool(mode)

        self.addToolBar(self.toolbar)

        self.toolbar_added.emit()

    def show_toolbars(self):
        """Re-enable any toolbars that were hidden with `hide_toolbars()`

        Does not re-enable toolbars that were hidden by other means
        """
        for tb in self._toolbars:
            if self._tb_vis.get(tb, False):
                tb.setEnabled(True)

    def hide_toolbars(self):
        """ Disable all the toolbars in the viewer.

        This action can be reversed by calling `show_toolbars()`
        """
        for tb in self._toolbars:
            self._tb_vis[tb] = self._tb_vis.get(tb, False) or tb.isVisible()
            tb.setEnabled(False)

    def set_focus(self, state):
        if state:
            css = """
            DataViewer
            {
            border: 2px solid;
            border-color: rgb(56, 117, 215);
            }
            """
            self.setStyleSheet(css)
            self.show_toolbars()
        else:
            css = """
            DataViewer
            {
            border: none;
            }
            """
            self.setStyleSheet(css)
            self.hide_toolbars()

    def __str__(self):
        return self.LABEL

    def unregister(self, hub):
        """
        Override to perform cleanup operations when disconnecting from hub
        """
        pass

    @property
    def window_title(self):
        return str(self)

    def update_window_title(self):
        self.setWindowTitle(self.window_title)

    def set_status(self, message):
        sb = self.statusBar()
        sb.showMessage(message)

    def export_as_script(self, filename):

        data_filename = os.path.relpath(filename) + '.data'

        save(data_filename, self.session.data_collection)

        imports = ['from glue.core.state import load']

        imports_header, header = self._script_header()
        imports.extend(imports_header)

        layers = ""
        for ilayer, layer in enumerate(self.layers):
            if layer.layer.label:
                layers += '## Layer {0}: {1}\n\n'.format(
                    ilayer + 1, layer.layer.label)
            else:
                layers += '## Layer {0}\n\n'.format(ilayer + 1)
            if layer.visible and layer.enabled:
                if isinstance(layer.layer, Data):
                    index = self.session.data_collection.index(layer.layer)
                    layers += "layer_data = data_collection[{0}]\n\n".format(
                        index)
                else:
                    dindex = self.session.data_collection.index(
                        layer.layer.data)
                    sindex = layer.layer.data.subsets.index(layer.layer)
                    layers += "layer_data = data_collection[{0}].subsets[{1}]\n\n".format(
                        dindex, sindex)
            imports_layer, layer_script = layer._python_exporter(layer)
            if layer_script is None:
                continue
            imports.extend(imports_layer)
            layers += layer_script.strip() + "\n"

        imports_footer, footer = self._script_footer()
        imports.extend(imports_footer)

        imports = os.linesep.join(sorted(set(imports)))

        script = TEMPLATE_SCRIPT.format(data=data_filename,
                                        imports=imports.strip(),
                                        header=header.strip(),
                                        layers=layers.strip(),
                                        footer=footer.strip())

        with open(filename, 'w') as f:
            f.write(script)

    def _script_header(self):
        raise NotImplementedError()

    def _script_footer(self):
        raise NotImplementedError()