Пример #1
0
class MouseModeManager(QObject):
    class MouseMode:
        def __init__(self, mid):
            self.id = mid
            self.name = self.icon = self.tooltip = None
            self.contexts = []
            self.submodes = []
            self.patterns = {}
            self.qa = None

        def addAction(self, menu, qag, callback, toolbar=None):
            self.qa = menu.addAction(self.name, callback)
            icon = self.icon and getattr(pixmaps, self.icon, None)
            icon and self.qa.setIcon(icon.icon())
            self.qa.setCheckable(True)
            qag.addAction(self.qa)
            toolbar and toolbar.addAction(self.qa)

    setMouseMode = pyqtSignal(MouseMode)

    def __init__(self, parent, menu, toolbar):
        QObject.__init__(self, parent)
        self._currier = PersistentCurrier()
        # get list of mouse modes from config
        modelist = []
        for mid in Config.get("mouse-modes", _DefaultModes).split(","):
            if not ConfigFile.has_section(mid):
                print(
                    "ERROR: unknown mouse-mode '%s', skipping. Check your %s."
                    % (mid, ConfigFileName))
            else:
                modelist.append(self._readModeConfig(mid))
        self._modes = dict([(mode.id, mode) for mode in modelist])
        self._qag_mode = QActionGroup(self)
        self._qag_submode = QActionGroup(self)
        self._all_submodes = []
        # make entries for main modes
        for mode in modelist:
            mode.addAction(menu,
                           self._qag_mode,
                           callback=self._currier.curry(
                               self._setMode, mode.id))
            if mode.submodes:
                self._all_submodes += list(mode.submodes)
        # make entries for submodes
        self._qa_submode_sep = menu.addSeparator()
        self._modes.update([(mode.id, mode) for mode in self._all_submodes])
        for mode in self._all_submodes:
            mode.addAction(menu,
                           self._qag_submode,
                           toolbar=toolbar,
                           callback=self._currier.curry(
                               self._setSubmode, mode.id))
        # other init
        self._current_context = None
        self._available_submodes = []
        # set initial mode
        initmode = Config.get("current-mouse-mode", _DefaultInitialMode)
        if initmode not in self._modes:
            initmode = modelist[0].id
        self._modes[initmode].qa.setChecked(True)
        self._setMode(initmode, write_config=False)

    def currentMode(self):
        return self._current_submode or self._current_mode

    def setContext(self, has_image, has_model):
        self._current_context = (has_image and _Contexts['image']) | (
            has_model and _Contexts['model'])
        self._ensureValidSubmodes()

    def _ensureValidSubmodes(self):
        current = None
        self._valid_submodes = []
        # accumulate list of valid submodes, and find the checked-on one
        for mode in self._available_submodes:
            if not mode.contexts or not self._current_context or self._current_context & mode.contexts:
                self._valid_submodes.append(mode)
                mode.qa.setVisible(True)
                if mode.qa.isChecked():
                    current = mode.id
            else:
                mode.qa.setVisible(False)
        if self._valid_submodes:
            self._setSubmode(current or self._valid_submodes[0].id)

    def _setMode(self, mid, write_config=True):
        """Called when the mouse mode changes"""
        if write_config:
            Config.set("current-mouse-mode", mid)
        self._current_mode = mode = self._modes[mid]
        # hide submodes if any
        for mm in self._all_submodes:
            mm.qa.setVisible(False)
        self._qa_submode_sep.setVisible(bool(mode.submodes))
        self._current_submode = None
        self._available_submodes = mode.submodes
        # make relevant submodes visible, and make sure one is enabled
        if mode.submodes:
            self._ensureValidSubmodes()
        else:
            self.setMouseMode.emit(mode)

    def _setSubmode(self, mid):
        """Called when the mouse submode changes"""
        self._current_submode = mode = self._modes[mid]
        mode.qa.setChecked(True)
        # hide submodes if any
        for mm in self._all_submodes:
            mm.qa.setShortcuts([])
        # set F4 shortcut to next submode
        if len(self._valid_submodes) > 1:
            for i, mm in enumerate(self._valid_submodes):
                if mm is mode:
                    self._valid_submodes[(i + 1) % len(
                        self._valid_submodes)].qa.setShortcut(Qt.Key_F4)
                    break
        self.setMouseMode.emit(mode)

    def _readModeConfig(self, section, main_tooltip=None):
        """Reads the given config section (and uses the supplied defaults dict)
        and returns a dict of mouse_patterns,key_patterns per each function."""
        # read basic stuff
        mode = self.MouseMode(section)
        config = TigGUI.kitties.config.SectionParser(ConfigFile, section)
        mode.name = config.get("name", section)
        mode.icon = config.get("icon", "") or None
        mode.contexts = sum([
            _Contexts.get(x, 0) for x in config.get("contexts", "").split(",")
        ])
        submodes = config.get("submodes", "") or None
        # eiher a mode with submodes, or a main mode
        if submodes:
            mode.tooltip = "<P>Your current mouse scheme is \"%s\".</P>" % mode.name
            for mid in submodes.split(","):
                if ConfigFile.has_section(mid):
                    mode.submodes.append(
                        self._readModeConfig(mid, main_tooltip=mode.tooltip))
                else:
                    print(
                        "ERROR: unknown submode '%s' in mode config section '%s', skipping/ Check your %s."
                        % (mid, section, ConfigFileName))
        else:
            if main_tooltip:
                mode.tooltip = main_tooltip + """<P>In this scheme, available mouse functions depend on the selected mode.
        The current mode is %s. Use F4 to cycle through other modes.</P>""" % mode.name
            else:
                mode.tooltip = "<P>Your current mouse scheme is: \"%s\".</P>" % mode.name
            mode.tooltip += """<P>The following mouse functions are available:</P><BR><TABLE>\n"""
            patterns = {}
            # get basic patterns
            for func in _AllFuncs:
                # get pattern
                pattern = config.get(func, "")
                if not pattern:
                    continue
                mouse_pattern = key_pattern = None
                for pat in pattern.split(";"):
                    pat = pat.strip()
                    if pat and pat.lower() != "none":
                        # split by "+" and lookup each identifier in the Qt namespace
                        scomps = pat.split("+")
                        try:
                            comps = [
                                x if x in (WHEELUP,
                                           WHEELDOWN) else getattr(Qt, x)
                                for x in scomps
                            ]
                        except AttributeError:
                            print(
                                "WARNING: can't parse '%s' for function '%s' in mode config section '%s', disabling. Check your %s."
                                % (pat, func, section, ConfigFileName))
                            continue
                        # append key/button code and sum of modifiers to the key or keyboard pattern list
                        if scomps[-1].startswith("Key_"):
                            if key_pattern:
                                print(
                                    "WARNING: more than one key pattern for function '%s' in mode config section '%s', ignoring. Check your %s."
                                    % (func, section, ConfigFileName))
                            else:
                                key_pattern = comps[-1], sum(comps[:-1])
                        else:
                            if mouse_pattern:
                                print(
                                    "WARNING: more than one mouse pattern for function '%s' in mode config section '%s', ignoring. Check your %s."
                                    % (func, section, ConfigFileName))
                            else:
                                mouse_pattern = comps[-1], sum(comps[:-1])
                mode.tooltip += "<TR><TD>%s:&nbsp;&nbsp;</TD><TD>%s</TD></TR>\n" % (
                    pattern, FuncDoc[func])
                mode.patterns[func] = (mouse_pattern or (0, 0), key_pattern
                                       or (0, 0))
            mode.tooltip += "</TABLE><BR>"
        return mode
Пример #2
0
class ImageController(QFrame):
    """An ImageController is a widget for controlling the display of one image.
    It can emit the following signals from the image:
    raise                     raise button was clicked
    center                  center-on-image option was selected
    unload                  unload option was selected
    slice                     image slice has changed, need to redraw (emitted by SkyImage automatically)
    repaint                 image display range or colormap has changed, need to redraw (emitted by SkyImage automatically)
    """

    # image signals
    imageSignalRepaint = pyqtSignal()
    imageSignalSlice = pyqtSignal(tuple)
    imageSignalRaise = pyqtSignal([FITSImagePlotItem])
    imageSignalUnload = pyqtSignal(object)
    imageSignalCenter = pyqtSignal(object)

    def __init__(self, image, parent, imgman, name=None, save=False):
        QFrame.__init__(self, parent)
        self.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
        # init state
        self._border_pen = None
        self._image_label_text = None
        self._subset = None
        self.image = image
        self._imgman = imgman
        self._currier = PersistentCurrier()
        self._control_dialog = None
        # create widgets
        self._lo = lo = QHBoxLayout(self)
        lo.setContentsMargins(0, 0, 0, 0)
        lo.setSpacing(2)
        # raise button
        self._wraise = QToolButton(self)
        lo.addWidget(self._wraise)
        self._wraise.setIcon(pixmaps.raise_up.icon())
        self._wraise.setAutoRaise(True)
        self._can_raise = False
        self._wraise.clicked.connect(self._raiseButtonPressed)
        self._wraise.setToolTip(
            """<P>Click here to raise this image above other images. Hold the button down briefly to
      show a menu of image operations.</P>""")
        # center label
        self._wcenter = QLabel(self)
        self._wcenter.setPixmap(pixmaps.center_image.pm())
        self._wcenter.setToolTip(
            "<P>The plot is currently centered on (the reference pixel %d,%d) of this image.</P>"
            % self.image.referencePixel())
        lo.addWidget(self._wcenter)
        # name/filename label
        self.name = image.name
        self._wlabel = QLabel(self.name, self)
        self._number = 0
        self.setName(self.name)
        self._wlabel.setToolTip(
            "%s %s" %
            (image.filename, "\u00D7".join(map(str,
                                               image.data().shape))))
        lo.addWidget(self._wlabel, 1)
        # if 'save' is specified, create a "save" button
        if save:
            self._wsave = QToolButton(self)
            lo.addWidget(self._wsave)
            self._wsave.setText("save")
            self._wsave.setAutoRaise(True)
            self._save_dir = save if isinstance(save, str) else "."
            self._wsave.clicked.connect(self._saveImage)
            self._wsave.setToolTip(
                """<P>Click here to write this image to a FITS file.</P>""")
        # render control
        self.image.connectRepaint(self.imageSignalRepaint)
        self.image.connectSlice(self.imageSignalSlice)
        self.image.connectRaise(self.imageSignalRaise)
        self.image.connectUnload(self.imageSignalUnload)
        self.image.connectCenter(self.imageSignalCenter)
        dprint(2, "creating RenderControl")
        self._rc = RenderControl(image, self)
        dprint(2, "done")
        # selectors for extra axes
        self._wslicers = []
        curslice = self._rc.currentSlice(
        )  # this may be loaded from config, so not necessarily 0
        for iextra, axisname, labels in self._rc.slicedAxes():
            if axisname.upper() not in ["STOKES", "COMPLEX"]:
                lbl = QLabel("%s:" % axisname, self)
                lo.addWidget(lbl)
            else:
                lbl = None
            slicer = QComboBox(self)
            self._wslicers.append(slicer)
            lo.addWidget(slicer)
            slicer.addItems(labels)
            slicer.setToolTip(
                """<P>Selects current slice along the %s axis.</P>""" %
                axisname)
            slicer.setCurrentIndex(curslice[iextra])
            slicer.activated[int].connect(
                self._currier.curry(self._rc.changeSlice, iextra))
        # min/max display ranges
        lo.addSpacing(5)
        self._wrangelbl = QLabel(self)
        lo.addWidget(self._wrangelbl)
        self._minmaxvalidator = FloatValidator(self)
        self._wmin = QLineEdit(self)
        self._wmax = QLineEdit(self)
        width = self._wmin.fontMetrics().width("1.234567e-05")
        for w in self._wmin, self._wmax:
            lo.addWidget(w, 0)
            w.setValidator(self._minmaxvalidator)
            w.setMaximumWidth(width)
            w.setMinimumWidth(width)
            w.editingFinished.connect(self._changeDisplayRange)
        # full-range button
        self._wfullrange = QToolButton(self)
        lo.addWidget(self._wfullrange, 0)
        self._wfullrange.setIcon(pixmaps.zoom_range.icon())
        self._wfullrange.setAutoRaise(True)
        self._wfullrange.clicked.connect(
            self.renderControl().resetSubsetDisplayRange)
        rangemenu = QMenu(self)
        rangemenu.addAction(pixmaps.full_range.icon(), "Full subset",
                            self.renderControl().resetSubsetDisplayRange)
        for percent in (99.99, 99.9, 99.5, 99, 98, 95):
            rangemenu.addAction(
                "%g%%" % percent,
                self._currier.curry(self._changeDisplayRangeToPercent,
                                    percent))
        self._wfullrange.setPopupMode(QToolButton.DelayedPopup)
        self._wfullrange.setMenu(rangemenu)
        # update widgets from current display range
        self._updateDisplayRange(*self._rc.displayRange())
        # lock button
        self._wlock = QToolButton(self)
        self._wlock.setIcon(pixmaps.unlocked.icon())
        self._wlock.setAutoRaise(True)
        self._wlock.setToolTip(
            """<P>Click to lock or unlock the intensity range. When the intensity range is locked across multiple images, any changes in the intensity
          range of one are propagated to the others. Hold the button down briefly for additional options.</P>"""
        )
        lo.addWidget(self._wlock)
        self._wlock.clicked.connect(self._toggleDisplayRangeLock)
        self.renderControl().displayRangeLocked.connect(
            self._setDisplayRangeLock)
        self.renderControl().dataSubsetChanged.connect(self._dataSubsetChanged)
        lockmenu = QMenu(self)
        lockmenu.addAction(
            pixmaps.locked.icon(), "Lock all to this",
            self._currier.curry(imgman.lockAllDisplayRanges,
                                self.renderControl()))
        lockmenu.addAction(pixmaps.unlocked.icon(), "Unlock all",
                           imgman.unlockAllDisplayRanges)
        self._wlock.setPopupMode(QToolButton.DelayedPopup)
        self._wlock.setMenu(lockmenu)
        self._setDisplayRangeLock(self.renderControl().isDisplayRangeLocked())
        # dialog button
        self._wshowdialog = QToolButton(self)
        lo.addWidget(self._wshowdialog)
        self._wshowdialog.setIcon(pixmaps.colours.icon())
        self._wshowdialog.setAutoRaise(True)
        self._wshowdialog.setToolTip(
            """<P>Click for colourmap and intensity policy options.</P>""")
        self._wshowdialog.clicked.connect(self.showRenderControls)
        tooltip = """<P>You can change the currently displayed intensity range by entering low and high limits here.</P>
            <TABLE>
            <TR><TD><NOBR>Image min:</NOBR></TD><TD>%g</TD><TD>max:</TD><TD>%g</TD></TR>
            </TABLE>""" % self.image.imageMinMax()
        for w in self._wmin, self._wmax, self._wrangelbl:
            w.setToolTip(tooltip)
        # create image operations menu
        self._menu = QMenu(self.name, self)
        self._qa_raise = self._menu.addAction(
            pixmaps.raise_up.icon(), "Raise image",
            self._currier.curry(self.image.signalRaise.emit, None))
        self._qa_center = self._menu.addAction(
            pixmaps.center_image.icon(), "Center plot on image",
            self._currier.curry(self.image.signalCenter.emit, True))
        self._qa_show_rc = self._menu.addAction(pixmaps.colours.icon(),
                                                "Colours && Intensities...",
                                                self.showRenderControls)
        if save:
            self._qa_save = self._menu.addAction("Save image...",
                                                 self._saveImage)
        self._menu.addAction("Export image to PNG file...",
                             self._exportImageToPNG)
        self._export_png_dialog = None
        self._menu.addAction(
            "Unload image",
            self._currier.curry(self.image.signalUnload.emit, None))
        self._wraise.setMenu(self._menu)
        self._wraise.setPopupMode(QToolButton.DelayedPopup)
        # connect updates from renderControl and image
        self.image.signalSlice.connect(self._updateImageSlice)
        self._rc.displayRangeChanged.connect(self._updateDisplayRange)
        # default plot depth of image markers
        self._z_markers = None
        # and the markers themselves
        self._image_border = QwtPlotCurve()
        self._image_border.setRenderHint(QwtPlotItem.RenderAntialiased)
        self._image_label = QwtPlotMarker()
        self._image_label.setRenderHint(QwtPlotItem.RenderAntialiased)
        # subset markers
        self._subset_pen = QPen(QColor("Light Blue"))
        self._subset_border = QwtPlotCurve()
        self._subset_border.setRenderHint(QwtPlotItem.RenderAntialiased)
        self._subset_border.setPen(self._subset_pen)
        self._subset_border.setVisible(False)
        self._subset_label = QwtPlotMarker()
        self._subset_label.setRenderHint(QwtPlotItem.RenderAntialiased)
        text = QwtText("subset")
        text.setColor(self._subset_pen.color())
        self._subset_label.setLabel(text)
        self._subset_label.setLabelAlignment(Qt.AlignRight | Qt.AlignBottom)
        self._subset_label.setVisible(False)
        self._setting_lmrect = False
        self._all_markers = [
            self._image_border, self._image_label, self._subset_border,
            self._subset_label
        ]
        self._exportMaxRes = False
        self._dockable_colour_ctrl = None

    def close(self):
        if self._control_dialog:
            self._control_dialog.close()
            self._control_dialog = None

    def __del__(self):
        self.close()

    def __eq__(self, other):
        return self is other

    def renderControl(self):
        return self._rc

    def getMenu(self):
        return self._menu

    def getFilename(self):
        return self.image.filename

    def setName(self, name):
        self.name = name
        self._wlabel.setText("%s: %s" %
                             (chr(ord('a') + self._number), self.name))

    def setNumber(self, num):
        self._number = num
        self._menu.menuAction().setText(
            "%s: %s" % (chr(ord('a') + self._number), self.name))
        self._qa_raise.setShortcut(QKeySequence("Alt+" + chr(ord('A') + num)))
        self.setName(self.name)

    def getNumber(self):
        return self._number

    def setPlotProjection(self, proj):
        self.image.setPlotProjection(proj)
        sameproj = proj == self.image.projection
        self._wcenter.setVisible(sameproj)
        self._qa_center.setVisible(not sameproj)
        if self._image_border:
            (l0, l1), (m0, m1) = self.image.getExtents()
            path = numpy.array([l0, l0, l1, l1,
                                l0]), numpy.array([m0, m1, m1, m0, m0])
            self._image_border.setSamples(*path)
            if self._image_label:
                self._image_label.setValue(path[0][2], path[1][2])

    def addPlotBorder(self,
                      border_pen,
                      label,
                      label_color=None,
                      bg_brush=None):
        # make plot items for image frame
        # make curve for image borders
        (l0, l1), (m0, m1) = self.image.getExtents()
        self._border_pen = QPen(border_pen)
        self._image_border.show()
        self._image_border.setSamples([l0, l0, l1, l1, l0],
                                      [m0, m1, m1, m0, m0])
        self._image_border.setPen(self._border_pen)
        self._image_border.setZ(
            self.image.z() + 1 if self._z_markers is None else self._z_markers)
        if label:
            self._image_label.show()
            self._image_label_text = text = QwtText(" %s " % label)
            text.setColor(label_color)
            text.setBackgroundBrush(bg_brush)
            self._image_label.setValue(l1, m1)
            self._image_label.setLabel(text)
            self._image_label.setLabelAlignment(Qt.AlignRight
                                                | Qt.AlignVCenter)
            self._image_label.setZ(
                self.image.z() +
                2 if self._z_markers is None else self._z_markers)

    def setPlotBorderStyle(self, border_color=None, label_color=None):
        if border_color:
            self._border_pen.setColor(border_color)
            self._image_border.setPen(self._border_pen)
        if label_color:
            self._image_label_text.setColor(label_color)
            self._image_label.setLabel(self._image_label_text)

    def showPlotBorder(self, show=True):
        self._image_border.setVisible(show)
        self._image_label.setVisible(show)

    def attachToPlot(self, plot, z_markers=None):
        for item in [self.image] + self._all_markers:
            if item.plot() != plot:
                item.attach(plot)

    def setImageVisible(self, visible):
        self.image.setVisible(visible)

    def showRenderControls(self):
        if not self._control_dialog:
            dprint(1, "creating control dialog")
            self._control_dialog = ImageControlDialog(self, self._rc,
                                                      self._imgman)
            # line below allows window to be resized by the user
            self._control_dialog.setSizeGripEnabled(True)
            # get and set sizing
            self._control_dialog.setMinimumWidth(396)
            # create size policy for control dialog
            colour_ctrl_policy = QSizePolicy()
            colour_ctrl_policy.setHorizontalPolicy(QSizePolicy.Minimum)
            self._control_dialog.setSizePolicy(colour_ctrl_policy)
            # setup dockable colour control dialog
            self._dockable_colour_ctrl = TDockWidget(
                title=f"{self._rc.image.name}",
                parent=self.parent().mainwin,
                bind_widget=self._control_dialog,
                close_slot=self.colourctrl_dockwidget_closed,
                toggle_slot=self.colourctrl_dockwidget_toggled)
            self.addDockWidgetToTab()
            dprint(1, "done")
        # set dockable widget visibility in sync with control dialog
        if not self._control_dialog.isVisible():
            dprint(1, "showing control dialog")
            self._control_dialog.show()
            self._dockable_colour_ctrl.setVisible(True)
            self.addDockWidgetToTab()
            self._dockable_colour_ctrl.show()
            self._dockable_colour_ctrl.raise_()
        else:
            self._control_dialog.hide()
            self._dockable_colour_ctrl.setVisible(False)
            self.parent().mainwin.setMaximumWidth(
                self.parent().mainwin.width() +
                self._dockable_colour_ctrl.width())

    def addDockWidgetToTab(self):
        # Add dockable widget to main window.
        # This needs to itterate through the widgets to find DockWidgets already in the right side area,
        # then tabifydockwidget when adding, or add to the right area if empty
        widget_list = self.parent().mainwin.findChildren(QDockWidget)
        for widget in widget_list:
            if self.parent().mainwin.dockWidgetArea(
                    widget) == 2:  # if in right dock area
                if widget.isVisible() and not widget.isFloating(
                ):  # if widget active and not a window
                    if self._dockable_colour_ctrl is not widget:  # check not itself
                        # add dock widget in tab on top of current widget in right area
                        self.parent().mainwin.tabifyDockWidget(
                            widget, self._dockable_colour_ctrl)
                        self.parent().mainwin.resizeDocks(
                            [widget], [widget.bind_widget.width()],
                            Qt.Horizontal)
            elif self.parent().mainwin.dockWidgetArea(
                    widget
            ) == 0:  # if not in any dock area assume we have new dock widget
                # no previous widget in this area then add
                self.parent().mainwin.addDockWidget(Qt.RightDockWidgetArea,
                                                    self._dockable_colour_ctrl)
                self.parent().mainwin.resizeDocks([widget],
                                                  [widget.bind_widget.width()],
                                                  Qt.Horizontal)

    def removeDockWidget(self):
        # remove image control dock widget
        self.parent().mainwin.removeDockWidget(self._dockable_colour_ctrl)
        # get widgets to resize
        widget_list = self.parent().mainwin.findChildren(QDockWidget)
        size_list = []
        result = []
        for widget in widget_list:
            if not isinstance(widget.bind_widget, ImageControlDialog):
                size_list.append(widget.bind_widget.width())
                result.append(widget)
                dprint(2, f"{widget} width {widget.width()}")
                dprint(
                    2,
                    f"{widget} bind_widget width {widget.bind_widget.width()}")
                if isinstance(widget.bind_widget, LiveImageZoom):
                    widget.bind_widget.setMinimumWidth(widget.width())
        widget_list = result
        # resize dock areas
        self.parent().mainwin.resizeDocks(widget_list, size_list,
                                          Qt.Horizontal)

    def colourctrl_dockwidget_closed(self):
        self._dockable_colour_ctrl.setVisible(False)
        self.parent().mainwin.setMaximumWidth(
            self.parent().mainwin.width() - self._dockable_colour_ctrl.width())

    def colourctrl_dockwidget_toggled(self):
        if self._dockable_colour_ctrl.isVisible():
            if self._dockable_colour_ctrl.isWindow():
                self._dockable_colour_ctrl.setFloating(False)
            else:
                self._dockable_colour_ctrl.setFloating(True)
                self.parent().mainwin.setMaximumWidth(
                    self.parent().mainwin.width() +
                    self._dockable_colour_ctrl.width())

    def _changeDisplayRangeToPercent(self, percent):
        if not self._control_dialog:
            self._control_dialog = ImageControlDialog(self, self._rc,
                                                      self._imgman)
        self._control_dialog._changeDisplayRangeToPercent(percent)

    def _updateDisplayRange(self, dmin, dmax):
        """Updates display range widgets."""
        self._wmin.setText("%.4g" % dmin)
        self._wmax.setText("%.4g" % dmax)
        self._updateFullRangeIcon()

    def _changeDisplayRange(self):
        """Gets display range from widgets and updates the image with it."""
        try:
            newrange = float(str(self._wmin.text())), float(
                str(self._wmax.text()))
        except ValueError:
            return
        self._rc.setDisplayRange(*newrange)

    def _dataSubsetChanged(self, subset, minmax, desc, subset_type):
        """Called when the data subset changes (or is reset)"""
        # hide the subset indicator -- unless we're invoked while we're actually setting the subset itself
        if not self._setting_lmrect:
            self._subset = None
            self._subset_border.setVisible(False)
            self._subset_label.setVisible(False)

    def setLMRectSubset(self, rect):
        self._subset = rect
        l0, m0, l1, m1 = rect.getCoords()
        self._subset_border.setSamples([l0, l0, l1, l1, l0],
                                       [m0, m1, m1, m0, m0])
        self._subset_border.setVisible(True)
        self._subset_label.setValue(max(l0, l1), max(m0, m1))
        self._subset_label.setVisible(True)
        self._setting_lmrect = True
        self.renderControl().setLMRectSubset(rect)
        self._setting_lmrect = False

    def currentSlice(self):
        return self._rc.currentSlice()

    def _updateImageSlice(self, _slice):
        dprint(2, _slice)
        for i, (iextra, name, labels) in enumerate(self._rc.slicedAxes()):
            slicer = self._wslicers[i]
            if slicer.currentIndex() != _slice[iextra]:
                dprint(3, "setting widget", i, "to", _slice[iextra])
                slicer.setCurrentIndex(_slice[iextra])

    def setMarkersZ(self, z):
        self._z_markers = z
        for i, elem in enumerate(self._all_markers):
            elem.setZ(z + i)

    def setZ(self, z, top=False, depthlabel=None, can_raise=True):
        self.image.setZ(z)
        if self._z_markers is None:
            for i, elem in enumerate(self._all_markers):
                elem.setZ(z + i + i)
        # set the depth label, if any
        label = "%s: %s" % (chr(ord('a') + self._number), self.name)
        # label = "%s %s"%(depthlabel,self.name) if depthlabel else self.name
        if top:
            label = "%s: <B>%s</B>" % (chr(ord('a') + self._number), self.name)
        self._wlabel.setText(label)
        # set hotkey
        self._qa_show_rc.setShortcut(Qt.Key_F9 if top else QKeySequence())
        # set raise control
        self._can_raise = can_raise
        self._qa_raise.setVisible(can_raise)
        self._wlock.setVisible(can_raise)
        if can_raise:
            self._wraise.setToolTip(
                "<P>Click here to raise this image to the top. Click on the down-arrow to access the image menu.</P>"
            )
        else:
            self._wraise.setToolTip("<P>Click to access the image menu.</P>")

    def _raiseButtonPressed(self):
        if self._can_raise:
            self.image.signalRaise.emit(self.image)
        else:
            self._wraise.showMenu()

    def _saveImage(self):
        filename = QFileDialog.getSaveFileName(
            self,
            "Save FITS file",
            self._save_dir,
            "FITS files(*.fits *.FITS *fts *FTS)",
            options=QFileDialog.DontUseNativeDialog)
        filename = str(filename[0])
        if not filename:
            return
        busy = BusyIndicator()
        self._imgman.signalShowMessage.emit(
            """Writing FITS image %s""" % filename, 3000)
        QApplication.flush()
        try:
            self.image.save(filename)
        except Exception as exc:
            busy.reset_cursor()
            traceback.print_exc()
            self._imgman.signalShowErrorMessage.emit(
                """Error writing FITS image %s: %s""" %
                (filename, str(sys.exc_info()[1])))
            return None
        self.renderControl().startSavingConfig(filename)
        self.setName(self.image.name)
        self._qa_save.setVisible(False)
        self._wsave.hide()
        busy.reset_cursor()

    def _exportImageResolution(self):
        sender = self.sender()
        if isinstance(sender, QCheckBox):
            if sender.isChecked():
                self._exportMaxRes = True
            else:
                self._exportMaxRes = False

    def _exportImageToPNG(self, filename=None):
        if not filename:
            if not self._export_png_dialog:
                dialog = self._export_png_dialog = QFileDialog(
                    self, "Export image to PNG", ".", "*.png")
                dialog.setDefaultSuffix("png")
                dialog.setFileMode(QFileDialog.AnyFile)
                dialog.setAcceptMode(QFileDialog.AcceptSave)
                dialog.setModal(True)
                dialog.filesSelected['QStringList'].connect(
                    self._exportImageToPNG)
                # attempt to add limit 4K option - not available on Ubuntu Unity
                layout = dialog.layout()
                if layout is not None:
                    checkbox = QCheckBox("Limit to 4K image")
                    checkbox.setChecked(False)
                    checkbox.setToolTip("Limits the image output to 4K")
                    checkbox.toggled.connect(self._exportImageResolution)
                    layout.addWidget(checkbox)
                    dialog.setLayout(layout)
            return self._export_png_dialog.exec_() == QDialog.Accepted
        busy = BusyIndicator()
        if isinstance(filename, QStringList):
            filename = filename[0]
        filename = str(filename)
        # get image dimensions
        nx, ny = self.image.imageDims()
        # export either max resolution possible or default to 4K. If image is small then no scaling occurs.
        if not self._exportMaxRes:
            # get free memory. Note: Linux only!
            import os
            total_memory, used_memory, free_memory = map(
                int,
                os.popen('free -t -m').readlines()[-1].split()[1:])
            # use 90% of free memory available
            free_memory = free_memory * 0.9
            # use an approximation to find the max image size that can be generated
            if nx >= ny and nx > free_memory:
                scale_factor = round(free_memory / nx, 1)
            elif ny > nx and ny > free_memory:
                scale_factor = round(free_memory / ny, 1)
            else:
                scale_factor = 1
        else:
            # default to 4K
            if nx > 4000:
                scale_factor = 4000 / nx
            elif ny > nx and ny > 4000:
                scale_factor = 4000 / ny
            else:
                scale_factor = 1

        # make QPixmap
        nx = nx * scale_factor
        ny = ny * scale_factor
        (l0, l1), (m0, m1) = self.image.getExtents()
        pixmap = QPixmap(nx, ny)
        painter = QPainter(pixmap)
        # use QwtPlot implementation of draw canvas, since we want to avoid caching
        xmap = QwtScaleMap()
        xmap.setPaintInterval(0, nx)
        xmap.setScaleInterval(l1, l0)
        ymap = QwtScaleMap()
        ymap.setPaintInterval(ny, 0)
        ymap.setScaleInterval(m0, m1)
        # call painter with clear cache option for consistent file size output.
        self.image.draw(painter, xmap, ymap, pixmap.rect(), use_cache=False)
        painter.end()
        # save to file
        try:
            pixmap.save(filename, "PNG")
            # clean up export items
            pixmap.detach()
            del xmap
            del ymap
            del pixmap
            del painter
        except Exception as exc:
            self._imgman.signalShowErrorMessage[str, int].emit(
                "Error writing %s: %s" % (filename, str(exc)), 3000)
            busy.reset_cursor()
        else:
            busy.reset_cursor()
            self._imgman.signalShowMessage[str, int].emit(
                "Exported image to file %s" % filename, 3000)

    def _toggleDisplayRangeLock(self):
        self.renderControl().lockDisplayRange(
            not self.renderControl().isDisplayRangeLocked())

    def _setDisplayRangeLock(self, locked):
        self._wlock.setIcon(
            pixmaps.locked.icon() if locked else pixmaps.unlocked.icon())

    def _updateFullRangeIcon(self):
        if self._rc.isSubsetDisplayRange():
            self._wfullrange.setIcon(pixmaps.zoom_range.icon())
            self._wfullrange.setToolTip(
                """<P>The current intensity range is the full range. Hold this button down briefly for additional options.</P>"""
            )
        else:
            self._wfullrange.setIcon(pixmaps.full_range.icon())
            self._wfullrange.setToolTip(
                """<P>Click to reset to a full intensity range. Hold the button down briefly for additional options.</P>"""
            )
Пример #3
0
class ModelGroupsTable(QWidget):
    EditableAttrs = [
        attr for attr in PlotStyles.StyleAttributes
        if attr in PlotStyles.StyleAttributeOptions
    ]
    ColList = 3
    ColPlot = 4
    ColApply = 5
    AttrByCol = dict([(i + 6, attr) for i, attr in enumerate(EditableAttrs)])

    def __init__(self, parent, *args):
        QWidget.__init__(self, parent, *args)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        lo = QVBoxLayout(self)
        lo.setContentsMargins(0, 0, 0, 0)
        lo1 = QHBoxLayout()
        lo.addLayout(lo1)
        lo1.setContentsMargins(0, 0, 0, 0)
        lbl = QLabel(QString("<nobr><b>Source groupings:</b></nobr>"), self)
        lo1.addWidget(lbl, 0)
        lo1.addStretch(1)
        # add show/hide button
        self._showattrbtn = QPushButton(self)
        self._showattrbtn.setMinimumWidth(256)
        lo1.addWidget(self._showattrbtn, 0)
        lo1.addStretch()
        self._showattrbtn.clicked.connect(self._togglePlotControlsVisibility)
        # add table
        self.table = QTableWidget(self)
        lo.addWidget(self.table)
        self.table.cellChanged[int, int].connect(self._valueChanged)
        self.table.setSelectionMode(QTableWidget.NoSelection)
        # setup basic columns
        self.table.setColumnCount(6 + len(self.EditableAttrs))
        for i, label in enumerate(
            ("grouping", "total", "selection", "list", "plot", "style")):
            self.table.setHorizontalHeaderItem(i, QTableWidgetItem(label))
        self.table.horizontalHeader().setSectionHidden(self.ColApply, True)
        # setup columns for editable grouping attributes
        for i, attr in self.AttrByCol.items():
            self.table.setHorizontalHeaderItem(
                i, QTableWidgetItem(PlotStyles.StyleAttributeLabels[attr]))
            self.table.horizontalHeader().setSectionHidden(i, True)
        self.table.verticalHeader().hide()
        # other internal init
        self._attrs_shown = False
        self._togglePlotControlsVisibility()
        self.model = None
        self._setting_model = False
        self._currier = PersistentCurrier()
        # row of 'selected' grouping
        self._irow_selgroup = 0

    def clear(self):
        self.table.setRowCount(0)
        self.model = None

    # setup mappings from the group.show_plot attribute to check state
    ShowAttrToCheckState = {
        PlotStyles.ShowNot: Qt.Unchecked,
        PlotStyles.ShowDefault: Qt.PartiallyChecked,
        PlotStyles.ShowAlways: Qt.Checked
    }
    CheckStateToShowAttr = dict([(val, key)
                                 for key, val in ShowAttrToCheckState.items()])

    def _makeCheckItem(self, name, group, attr):
        item = QTableWidgetItem(name)
        if group is self.model.defgroup:
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
            item.setCheckState(
                Qt.Checked if getattr(group.style, attr) else Qt.Unchecked)
        else:
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable
                          | Qt.ItemIsTristate)
            item.setCheckState(self.ShowAttrToCheckState[getattr(
                group.style, attr)])
        return item

    def _updateModel(self, what=SkyModel.UpdateAll, origin=None):
        if origin is self or not what & (SkyModel.UpdateTags
                                         | SkyModel.UpdateGroupStyle):
            return
        model = self.model
        self._setting_model = True  # to ignore cellChanged() signals (in valueChanged())
        # _item_cb is a dict (with row,col keys) containing the widgets (CheckBoxes ComboBoxes) per each cell
        self._item_cb = {}
        # lists of "list" and "plot" checkboxes per each grouping (excepting the default grouping); each entry is an (row,col,item) tuple.
        # used as argument to self._showControls()
        self._list_controls = []
        self._plot_controls = []
        # list of selection callbacks (to which signals are connected)
        self._callbacks = []
        # set requisite number of rows,and start filling
        self.table.setRowCount(len(model.groupings))
        for irow, group in enumerate(model.groupings):
            self.table.setItem(irow, 0, QTableWidgetItem(group.name))
            if group is model.selgroup:
                self._irow_selgroup = irow
            # total # source in group: skip for "current"
            if group is not model.curgroup:
                self.table.setItem(irow, 1, QTableWidgetItem(str(group.total)))
            # selection controls: skip for current and selection
            if group not in (model.curgroup, model.selgroup):
                btns = QWidget()
                lo = QHBoxLayout(btns)
                lo.setContentsMargins(0, 0, 0, 0)
                lo.setSpacing(0)
                # make selector buttons (depending on which group we're in)
                if group is model.defgroup:
                    Buttons = (("+", lambda src, grp=group: True,
                                "select all sources"),
                               ("-", lambda src, grp=group: False,
                                "unselect all sources"))
                else:
                    Buttons = (
                        ("=", lambda src, grp=group: grp.func(src),
                         "select only this grouping"),
                        ("+",
                         lambda src, grp=group: src.selected or grp.func(src),
                         "add grouping to selection"),
                        ("-", lambda src, grp=group: src.selected and not grp.
                         func(src), "remove grouping from selection"),
                        ("&&",
                         lambda src, grp=group: src.selected and grp.func(src),
                         "intersect selection with grouping"))
                lo.addStretch(1)
                for label, predicate, tooltip in Buttons:
                    btn = QToolButton(btns)
                    btn.setText(label)
                    btn.setMinimumWidth(24)
                    btn.setMaximumWidth(24)
                    btn.setToolTip(tooltip)
                    lo.addWidget(btn)
                    # add callback
                    btn.clicked.connect(
                        self._currier.curry(self.selectSources, predicate))
                lo.addStretch(1)
                self.table.setCellWidget(irow, 2, btns)
            # "list" checkbox (not for current and selected groupings: these are always listed)
            if group not in (model.curgroup, model.selgroup):
                item = self._makeCheckItem("", group, "show_list")
                self.table.setItem(irow, self.ColList, item)
                item.setToolTip(
                    """<P>If checked, sources in this grouping will be listed in the source table. If un-checked, sources will be
            excluded from the table. If partially checked, then the default list/no list setting of "all sources" will be in effect.
            </P>""")
            # "plot" checkbox (not for the current grouping, since that's always plotted)
            if group is not model.curgroup:
                item = self._makeCheckItem("", group, "show_plot")
                self.table.setItem(irow, self.ColPlot, item)
                item.setToolTip(
                    """<P>If checked, sources in this grouping will be included in the plot. If un-checked, sources will be
            excluded from the plot. If partially checked, then the default plot/no plot setting of "all sources" will be in effect.
            </P>""")
            # custom style control
            # for default, current and selected, this is just a text label
            if group is model.defgroup:
                item = QTableWidgetItem("default:")
                item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
                item.setToolTip(
                    """<P>This is the default plot style used for all sources for which a custom grouping style is not selected.</P>"""
                )
                self.table.setItem(irow, self.ColApply, item)
            elif group is model.curgroup:
                item = QTableWidgetItem("")
                item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
                item.setToolTip(
                    """<P>This is the plot style used for the highlighted source, if any.</P>"""
                )
                self.table.setItem(irow, self.ColApply, item)
            elif group is model.selgroup:
                item = QTableWidgetItem("")
                item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
                item.setToolTip(
                    """<P>This is the plot style used for the currently selected sources.</P>"""
                )
                self.table.setItem(irow, self.ColApply, item)
            # for the rest, a combobox with custom priorities
            else:
                cb = QComboBox()
                cb.addItems(["default"] +
                            ["custom %d" % p for p in range(1, 10)])
                index = max(0, min(group.style.apply, 9))
                #        dprint(0,group.name,"apply",index)
                cb.setCurrentIndex(index)
                cb.activated[int].connect(
                    self._currier.xcurry(self._valueChanged,
                                         (irow, self.ColApply)))
                self.table.setCellWidget(irow, self.ColApply, cb)
                cb.setToolTip(
                    """<P>This controls whether sources within this group are plotted with a customized
            plot style. Customized styles have numeric priority; if a source belongs to multiple groups, then
            the style with the lowest priority takes precedence.<P>""")
            # attribute comboboxes
            for icol, attr in self.AttrByCol.items():
                # get list of options for this style attribute. If dealing with first grouping (i==0), which is
                # the "all sources" grouping, then remove the "default" option (which is always first in the list)
                options = PlotStyles.StyleAttributeOptions[attr]
                if irow == 0:
                    options = options[1:]
                # make combobox
                cb = QComboBox()
                cb.addItems(list(map(str, options)))
                # the "label" option is also editable
                if attr == "label":
                    cb.setEditable(True)
                try:
                    index = options.index(getattr(group.style, attr))
                    cb.setCurrentIndex(index)
                except ValueError:
                    cb.setEditText(str(getattr(group.style, attr)))
                slot = self._currier.xcurry(self._valueChanged, (irow, icol))
                cb.activated[int].connect(slot)
                cb.editTextChanged['QString'].connect(slot)
                cb.setEnabled(group is model.defgroup or group.style.apply)
                self.table.setCellWidget(irow, icol, cb)
                label = attr
                if irow:
                    cb.setToolTip(
                        """<P>This is the %s used to plot sources in this group, when a "custom" style for the group
          is enabled via the style control.<P>""" % label)
                else:
                    cb.setToolTip(
                        "<P>This is the default %s used for all sources for which a custom style is not specified below.<P>"
                        % label)
        self.table.resizeColumnsToContents()
        # re-enable processing of cellChanged() signals
        self._setting_model = False

    def setModel(self, model):
        self.model = model
        self.model.connect("updated", self._updateModel)
        self.model.connect("selected", self.updateModelSelection)
        self._updateModel(SkyModel.UpdateAll)

    def _valueChanged(self, row, col):
        """Called when a cell has been edited"""
        if self._setting_model:
            return
        group = self.model.groupings[row]
        item = self.table.item(row, col)
        if col == self.ColList:
            if group is not self.model.defgroup:
                # tri-state items go from unchecked to checked when user clicks them. Make them partially checked instead.
                if group.style.show_list == PlotStyles.ShowNot and item.checkState(
                ) == Qt.Checked:
                    item.setCheckState(Qt.PartiallyChecked)
            group.style.show_list = self.CheckStateToShowAttr[
                item.checkState()]
            self.model.emitChangeGroupingVisibility(group, origin=self)
            return
        elif col == self.ColPlot:
            if group is not self.model.defgroup:
                # tri-state items go from unchecked to checked by default. Make them partially checked instead.
                if group.style.show_plot == PlotStyles.ShowNot and item.checkState(
                ) == Qt.Checked:
                    item.setCheckState(Qt.PartiallyChecked)
            group.style.show_plot = self.CheckStateToShowAttr[
                item.checkState()]
        elif col == self.ColApply:
            group.style.apply = self.table.cellWidget(row, col).currentIndex()
            # enable/disable editable cells
            for j in list(self.AttrByCol.keys()):
                item1 = self.table.item(row, j)
                if item1:
                    fl = item1.flags() & ~Qt.ItemIsEnabled
                    if group.style.apply:
                        fl |= Qt.ItemIsEnabled
                    item1.setFlags(fl)
                cw = self.table.cellWidget(row, j)
                cw and cw.setEnabled(group.style.apply)
        elif col in self.AttrByCol:
            cb = self.table.cellWidget(row, col)
            txt = str(cb.currentText())
            attr = self.AttrByCol[col]
            if txt == "default":
                setattr(group.style, attr, PlotStyles.DefaultValue)
            else:
                setattr(group.style, attr,
                        PlotStyles.StyleAttributeTypes.get(attr, str)(txt))
        # all other columns: return so we don't emit a signal
        else:
            return
        # in all cases emit a signal
        self.model.emitChangeGroupingStyle(group, origin=self)

    def selectSources(self, predicate, curry=False):
        """Selects sources according to predicate(src)"""
        busy = BusyIndicator()
        for src in self.model.sources:
            src.selected = predicate(src)
        self.model.emitSelection(origin=self)
        busy.reset_cursor()

    def updateModelSelection(self, nsel, origin=None):
        """This is called when some other widget changes the set of selected model sources"""
        self.table.clearSelection()
        if self.model:
            self.table.item(self._irow_selgroup, 1).setText(str(nsel))

    def _togglePlotControlsVisibility(self):
        if self._attrs_shown:
            self._attrs_shown = False
            self.table.hideColumn(self.ColApply)
            for col in self.AttrByCol.keys():
                self.table.hideColumn(col)
            self._showattrbtn.setText("Show plot styles >>")
        else:
            self._attrs_shown = True
            self.table.showColumn(self.ColApply)
            for col in self.AttrByCol.keys():
                self.table.showColumn(col)
            self._showattrbtn.setText("<< Hide plot styles")
Пример #4
0
class SkyModelTreeWidget(TigGUI.kitties.widgets.ClickableTreeWidget):
    """This implements a QTreeWidget for sky models"""
    def __init__(self, *args):
        TigGUI.kitties.widgets.ClickableTreeWidget.__init__(self, *args)
        self._currier = PersistentCurrier()
        self.model = None
        # insert columns
        self.setHeaderLabels(ViewColumns)
        self.headerItem().setText(ColumnIapp, "I(app)")
        self.header().setSectionsMovable(False)
        self.header().setSectionsClickable(True)
        self.setSortingEnabled(True)
        self.setRootIsDecorated(False)
        self.setEditTriggers(QAbstractItemView.AllEditTriggers)
        self.setMouseTracking(True)
        # set column width modes
        self.header().setStretchLastSection(False)
        self.header().setSectionResizeMode(QHeaderView.ResizeToContents)
        ## self.setTextAlignment(ColumnR,Qt.AlignRight)
        ## self.setTextAlignment(ColumnType,Qt.AlignHCenter)
        # _column_enabled[i] is True if column is available in the model.
        # _column_show[i] is True if column is currently being shown (via a view control)
        self._column_enabled = [True] * NumColumns
        self._column_shown = [True] * NumColumns
        # other listview init
        self.header().show()
        self.setSelectionMode(QTreeWidget.ExtendedSelection)
        self.setAllColumnsShowFocus(True)
        ## self.setShowToolTips(True)
        self._updating_selection = False
        self.setRootIsDecorated(False)
        # connect signals to track selected sources
        self.itemSelectionChanged.connect(self._selectionChanged)
        self.itemEntered[QTreeWidgetItem, int].connect(self._itemHighlighted)
        # add "View" controls for different column categories
        self._column_views = []
        self._column_widths = {}
        self.addColumnCategory("Position", [ColumnRa, ColumnDec])
        self.addColumnCategory("Position errors",
                               [ColumnRa_err, ColumnDec_err], False)
        self.addColumnCategory("Type", [ColumnType])
        self.addColumnCategory("Flux", [ColumnIapp, ColumnI])
        self.addColumnCategory("Flux errors", [ColumnI_err], False)
        self.addColumnCategory("Polarization",
                               [ColumnQ, ColumnU, ColumnV, ColumnRm])
        self.addColumnCategory(
            "Polarization errors",
            [ColumnQ_err, ColumnU_err, ColumnV_err, ColumnRm_err], False)
        self.addColumnCategory("Spectrum", [ColumnSpi])
        self.addColumnCategory("Spectrum errors", [ColumnSpi_err], False)
        self.addColumnCategory("Shape", [ColumnShape])
        self.addColumnCategory("Shape errors", [ColumnShape_err], False)
        self.addColumnCategory("Tags", [ColumnTags])

    def _showColumn(self, col, show=True):
        """Shows or hides the specified column.
        (When hiding, saves width of column to internal array so that it can be restored properly.)"""
        hdr = self.header()
        hdr.setSectionHidden(col, not show)
        if show:
            if not hdr.sectionSize(col):
                hdr.resizeSection(col, self._column_widths[col])
                hdr.setResizeMode(col, QHeaderView.ResizeToContents)
        else:
            if hdr.sectionSize(col):
                self._column_widths[col] = hdr.sectionSize(col)

    def _enableColumn(self, column, enable=True):
        busy = BusyIndicator()
        self._column_enabled[column] = enable
        self._showColumn(column, enable and self._column_shown[column])
        busy.reset_cursor()

    def _showColumnCategory(self, columns, show):
        busy = BusyIndicator()
        for col in columns:
            self._column_shown[col] = show
            self._showColumn(col, self._column_enabled[col] and show)
        busy.reset_cursor()

    def _selectionChanged(self):
        if self._updating_selection:
            return
        for item in self.iterator():
            item._src.select(item.isSelected())
        self.model.emitSelection(origin=self)

    def _itemHighlighted(self, item, col):
        dprint(3, "highlighting", item._src.name)
        self.model.setCurrentSource(item._src, origin=self)

    def viewportEvent(self, event):
        if event.type() in (QEvent.Leave, QEvent.FocusOut) and self.model:
            self.model.setCurrentSource(None, origin=self)
        return QTreeWidget.viewportEvent(self, event)

    def addColumnCategory(self, name, columns, visible=True):
        qa = QAction(name, self)
        qa.setCheckable(True)
        qa.setChecked(visible)
        if not visible:
            self._showColumnCategory(columns, False)
        qa.toggled[bool].connect(
            self._currier.curry(self._showColumnCategory, columns))
        self._column_views.append((name, qa, columns))

    def clear(self):
        TigGUI.kitties.widgets.ClickableTreeWidget.clear(self)
        self.model = None
        self._itemdict = {}

    def setModel(self, model):
        self.model = model
        self._refreshModel(SkyModel.UpdateAll)
        self.model.connect("changeCurrentSource", self._updateCurrentSource)
        self.model.connect("changeGroupingVisibility",
                           self.changeGroupingVisibility)
        self.model.connect("selected", self._updateModelSelection)
        self.model.connect("updated", self._refreshModel)

    def _refreshModel(self, what=SkyModel.UpdateAll, origin=None):
        if origin is self or not what & (SkyModel.UpdateSourceList
                                         | SkyModel.UpdateSourceContent):
            return
        # if only selection was changed, take shortcut
        if what & SkyModel.UpdateSelectionOnly:
            dprint(2, "model update -- selection only")
            return self._refreshSelectedItems(origin)
        busy = BusyIndicator()
        # else repopulate widget completely
        dprint(2, "model update -- complete")
        TigGUI.kitties.widgets.ClickableTreeWidget.clear(self)
        dprint(2, "creating model items")
        items = [SkyModelTreeWidgetItem(src) for src in self.model.sources]
        self._itemdict = dict(
            list(zip([src.name for src in self.model.sources], items)))
        dprint(2, "adding to tree widget")
        self.addTopLevelItems(items)
        self.header().updateGeometry()
        # show/hide columns based on tag availability
        self._enableColumn(ColumnIapp, 'Iapp' in self.model.tagnames)
        self._enableColumn(ColumnR, 'r' in self.model.tagnames)
        dprint(2, "re-sorting")
        self.sortItems(('Iapp' in self.model.tagnames and ColumnIapp)
                       or ColumnI, Qt.DescendingOrder)
        busy.reset_cursor()

    def addColumnViewActionsTo(self, menu):
        for name, qa, columns in self._column_views:
            menu.addAction(qa)

    def _updateCurrentSource(self, src, src0=None, origin=None):
        # if origin is self:
        # return
        # dehighlight old item
        item = src0 and self._itemdict.get(src0.name)
        if item:
            item.setHighlighted(False)
        # scroll to new item, if found
        item = src and self._itemdict.get(src.name)
        if item:
            item.setHighlighted(True, origin is not self)
            if origin is not self:
                self.scrollToItem(item)

    def _updateModelSelection(self, nsel, origin=None):
        """This is called when some other widget (origin!=self) changes the set of selected model sources"""
        if origin is self:
            return
        self._updating_selection = True
        ## this is very slow because of setSelected()
        #    for item in self.iterator():
        #     item.setSelected(item._src.selected)
        selection = QItemSelection()
        for item in self.iterator():
            if item._src.selected:
                selection.append(
                    QItemSelectionRange(
                        self.indexFromItem(item, 0),
                        self.indexFromItem(item,
                                           self.columnCount() - 1)))
        self.selectionModel().select(selection,
                                     QItemSelectionModel.ClearAndSelect)
        self.changeGroupingVisibility(None, origin=origin)
        self._updating_selection = False

    def _refreshSelectedItems(self, origin=None):
        busy = BusyIndicator()
        dprint(3, "refreshing selected items")
        for item in self.iterator():
            if item.isSelected():
                dprint(4, "resetting item", item._src.name)
                item.setSource(item._src)
        dprint(3, "refreshing selected items done")
        busy.reset_cursor()

    def changeGroupingVisibility(self, group, origin=None):
        if origin is self:
            return
        for item in self.iterator():
            # collect show_list values from groupings to which this source belongs (default group excepted)
            show = [
                group.style.show_list for group in self.model.groupings
                if group is not self.model.defgroup and group.func(item._src)
            ]
            # if at least one group is showing explicitly, show
            # else if at least one group is hiding explicitly, hide
            # else use default setting
            if max(show) == PlotStyles.ShowAlways:
                visible = True
            elif min(show) == PlotStyles.ShowNot:
                visible = False
            else:
                visible = bool(self.model.defgroup.style.show_list)
            # set visibility accordingly
            item.setHidden(not visible)

    TagsWithOwnColumn = set(["Iapp", "r"])
Пример #5
0
class ImageManager(QWidget):
    """An ImageManager manages a stack of images (and associated ImageControllers)"""

    def __init__(self, *args):
        QWidget.__init__(self, *args)
        # init layout
        self._lo = QVBoxLayout(self)
        self._lo.setContentsMargins(0, 0, 0, 0)
        self._lo.setSpacing(0)
        # init internal state
        self._currier = PersistentCurrier()
        self._z0 = 0;  # z-depth of first image, the rest count down from it
        self._updating_imap = False
        self._locked_display_range = False
        self._imagecons = []
        self._imagecon_loadorder = []
        self._center_image = None
        self._plot = None
        self._border_pen = None
        self._drawing_key = None
        self._load_image_dialog = None
        self._model_imagecons = set()
        # init menu and standard actions
        self._menu = QMenu("&Image", self)
        qag = QActionGroup(self)
        # exclusive controls for plotting topmost or all images
        self._qa_plot_top = qag.addAction("Display topmost image only")
        self._qa_plot_all = qag.addAction("Display all images")
        self._qa_plot_top.setCheckable(True)
        self._qa_plot_all.setCheckable(True)
        self._qa_plot_top.setChecked(True)
        QObject.connect(self._qa_plot_all, SIGNAL("toggled(bool)"), self._displayAllImages)
        self._closing = False

        self._qa_load_clipboard = None
        self._clipboard_mode = QClipboard.Clipboard
        QObject.connect(QApplication.clipboard(), SIGNAL("changed(QClipboard::Mode)"), self._checkClipboardPath)
        # populate the menu
        self._repopulateMenu()

    def close(self):
        dprint(1, "closing Manager")
        self._closing = True
        for ic in self._imagecons:
            ic.close()

    def loadImage(self, filename=None, duplicate=True, to_top=True, model=None):
        """Loads image. Returns ImageControlBar object.
        If image is already loaded: returns old ICB if duplicate=False (raises to top if to_top=True),
        or else makes a new control bar.
        If model is set to a source name, marks the image as associated with a model source. These can be unloaded en masse by calling
        unloadModelImages().
        """
        if filename is None:
            if not self._load_image_dialog:
                dialog = self._load_image_dialog = QFileDialog(self, "Load FITS image", ".",
                                                               "FITS images (%s);;All files (*)" % (" ".join(
                                                                   ["*" + ext for ext in FITS_ExtensionList])))
                dialog.setFileMode(QFileDialog.ExistingFile)
                dialog.setModal(True)
                QObject.connect(dialog, SIGNAL("filesSelected(const QStringList &)"), self.loadImage)
            self._load_image_dialog.exec_()
            return None
        if isinstance(filename, QStringList):
            filename = filename[0]
        filename = str(filename)
        # report error if image does not exist
        if not os.path.exists(filename):
            self.showErrorMessage("""FITS image %s does not exist.""" % filename)
            return None
        # see if image is already loaded
        if not duplicate:
            for ic in self._imagecons:
                if ic.getFilename() and os.path.samefile(filename, ic.getFilename()):
                    if to_top:
                        self.raiseImage(ic)
                    if model:
                        self._model_imagecons.add(id(ic))
                    return ic
        # load the FITS image
        busy = BusyIndicator()
        dprint(2, "reading FITS image", filename)
        self.showMessage("""Reading FITS image %s""" % filename, 3000)
        QApplication.flush()
        try:
            image = SkyImage.FITSImagePlotItem(str(filename))
        except KeyboardInterrupt:
            raise
        except:
            busy = None
            traceback.print_exc()
            self.showErrorMessage("""<P>Error loading FITS image %s: %s. This may be due to a bug in Tigger; if the FITS file loads fine in another viewer,
          please send the FITS file, along with a copy of any error messages from the text console, to [email protected].</P>""" % (
            filename, str(sys.exc_info()[1])))
            return None
        # create control bar, add to widget stack
        ic = self._createImageController(image, "model source '%s'" % model if model else filename, model or image.name,
                                         model=model)
        self.showMessage("""Loaded FITS image %s""" % filename, 3000)
        dprint(2, "image loaded")
        return ic

    def showMessage(self, message, time=None):
        self.emit(SIGNAL("showMessage"), message, time)

    def showErrorMessage(self, message, time=None):
        self.emit(SIGNAL("showErrorMessage"), message, time)

    def setZ0(self, z0):
        self._z0 = z0
        if self._imagecons:
            self.raiseImage(self._imagecons[0])

    def enableImageBorders(self, border_pen, label_color, label_bg_brush):
        self._border_pen, self._label_color, self._label_bg_brush = \
            border_pen, label_color, label_bg_brush

    def lockAllDisplayRanges(self, rc0):
        """Locks all display ranges, and sets the intensity from rc0"""
        if not self._updating_imap:
            self._updating_imap = True
            rc0.lockDisplayRange()
            try:
                for ic in self._imagecons:
                    rc1 = ic.renderControl()
                    if rc1 is not rc0:
                        rc1.setDisplayRange(*rc0.displayRange())
                        rc1.lockDisplayRange()
            finally:
                self._updating_imap = False

    def unlockAllDisplayRanges(self):
        """Unlocks all display range."""
        for ic in self._imagecons:
            ic.renderControl().lockDisplayRange(False)

    def _lockDisplayRange(self, rc0, lock):
        """Locks or unlocks the display range of a specific controller."""
        if lock and not self._updating_imap:
            self._updating_imap = True
            try:
                # if something is already locked, copy display range from it
                for ic in self._imagecons:
                    rc1 = ic.renderControl()
                    if rc1 is not rc0 and rc1.isDisplayRangeLocked():
                        rc0.setDisplayRange(*rc1.displayRange())
            finally:
                self._updating_imap = False

    def _updateDisplayRange(self, rc, dmin, dmax):
        """This is called whenever one of the images (or rather, its associated RenderControl object) changes its display range."""
        if not rc.isDisplayRangeLocked():
            return
        # If the display range is locked, propagate it to all images.
        # but don't do it if we're already propagating (otherwise we may get called in an infinte loop)
        if not self._updating_imap:
            self._updating_imap = True
            try:
                for ic in self._imagecons:
                    rc1 = ic.renderControl()
                    if rc1 is not rc and rc1.isDisplayRangeLocked():
                        rc1.setDisplayRange(dmin, dmax)
            finally:
                self._updating_imap = False

    def getImages(self):
        return [ic.image for ic in self._imagecons]

    def getTopImage(self):
        return (self._imagecons or None) and self._imagecons[0].image

    def cycleImages(self):
        index = self._imagecon_loadorder.index(self._imagecons[0])
        index = (index + 1) % len(self._imagecon_loadorder)
        self.raiseImage(self._imagecon_loadorder[index])

    def blinkImages(self):
        if len(self._imagecons) > 1:
            self.raiseImage(self._imagecons[1])

    def incrementSlice(self, extra_axis, incr):
        if self._imagecons:
            rc = self._imagecons[0].renderControl()
            sliced_axes = rc.slicedAxes()
            if extra_axis < len(sliced_axes):
                rc.incrementSlice(sliced_axes[extra_axis][0], incr)

    def setLMRectSubset(self, rect):
        if self._imagecons:
            self._imagecons[0].setLMRectSubset(rect)

    def getLMRectStats(self, rect):
        if self._imagecons:
            return self._imagecons[0].renderControl().getLMRectStats(rect)

    def unloadModelImages(self):
        """Unloads images associated with model (i.e. loaded with the model=True flag)"""
        for ic in [ic for ic in self._imagecons if id(ic) in self._model_imagecons]:
            self.unloadImage(ic)

    def unloadImage(self, imagecon):
        """Unloads the given imagecon object."""
        if imagecon not in self._imagecons:
            return
        # recenter if needed
        self._imagecons.remove(imagecon)
        self._imagecon_loadorder.remove(imagecon)
        self._model_imagecons.discard(id(imagecon))
        # reparent widget and release it
        imagecon.setParent(None)
        imagecon.close()
        # recenter image, if unloaded the center image
        if self._center_image is imagecon.image:
            self.centerImage(self._imagecons[0] if self._imagecons else None, emit=False)
        # emit signal
        self._repopulateMenu()
        self.emit(SIGNAL("imagesChanged"))
        if self._imagecons:
            self.raiseImage(self._imagecons[0])

    def getCenterImage(self):
        return self._center_image

    def centerImage(self, imagecon, emit=True):
        self._center_image = imagecon and imagecon.image
        for ic in self._imagecons:
            ic.setPlotProjection(self._center_image.projection)
        if emit:
            self.emit(SIGNAL("imagesChanged"))

    def raiseImage(self, imagecon):
        # reshuffle image stack, if more than one image image
        if len(self._imagecons) > 1:
            busy = BusyIndicator()
            # reshuffle image stack
            self._imagecons.remove(imagecon)
            self._imagecons.insert(0, imagecon)
            # notify imagecons
            for i, ic in enumerate(self._imagecons):
                label = "%d" % (i + 1) if i else "<B>1</B>"
                ic.setZ(self._z0 - i * 10, top=not i, depthlabel=label, can_raise=True)
            # adjust visibility
            for j, ic in enumerate(self._imagecons):
                ic.setImageVisible(not j or bool(self._qa_plot_all.isChecked()))
            # issue replot signal
            self.emit(SIGNAL("imageRaised"))
            self.fastReplot()
        # else simply update labels
        else:
            self._imagecons[0].setZ(self._z0, top=True, depthlabel=None, can_raise=False)
            self._imagecons[0].setImageVisible(True)
        # update slice menus
        img = imagecon.image
        axes = imagecon.renderControl().slicedAxes()
        for i, (next, prev) in enumerate(self._qa_slices):
            next.setVisible(False)
            prev.setVisible(False)
            if i < len(axes):
                iaxis, name, labels = axes[i]
                next.setVisible(True)
                prev.setVisible(True)
                next.setText("Show next slice along %s axis" % name)
                prev.setText("Show previous slice along %s axis" % name)
        # emit signasl
        self.emit(SIGNAL("imageRaised"), img)

    def resetDrawKey(self):
        """Makes and sets the current plot's drawing key"""
        if self._plot:
            key = []
            for ic in self._imagecons:
                key.append(id(ic))
                key += ic.currentSlice()
                self._plot.setDrawingKey(tuple(key))

    def fastReplot(self, *dum):
        """Fast replot -- called when flipping images or slices. Uses the plot cache, if possible."""
        if self._plot:
            self.resetDrawKey()
            dprint(2, "calling replot", time.time() % 60)
            self._plot.replot()
            dprint(2, "replot done", time.time() % 60)

    def replot(self, *dum):
        """Proper replot -- called when an image needs to be properly redrawn. Cleares the plot's drawing cache."""
        if self._plot:
            self._plot.clearDrawCache()
            self.resetDrawKey()
            self._plot.replot()

    def attachImagesToPlot(self, plot):
        self._plot = plot
        self.resetDrawKey()
        for ic in self._imagecons:
            ic.attachToPlot(plot)

    def getMenu(self):
        return self._menu

    def _displayAllImages(self, enabled):
        busy = BusyIndicator()
        if enabled:
            for ic in self._imagecons:
                ic.setImageVisible(True)
        else:
            self._imagecons[0].setImageVisible(True)
            for ic in self._imagecons[1:]:
                ic.setImageVisible(False)
        self.replot()

    def _checkClipboardPath(self, mode=QClipboard.Clipboard):
        if self._qa_load_clipboard:
            self._clipboard_mode = mode
            try:
                path = str(QApplication.clipboard().text(mode))
            except:
                path = None
            self._qa_load_clipboard.setEnabled(bool(path and os.path.isfile(path)))

    def _loadClipboardPath(self):
        try:
            path = QApplication.clipboard().text(self._clipboard_mode)
        except:
            return
        self.loadImage(path)

    def _repopulateMenu(self):
        self._menu.clear()
        self._menu.addAction("&Load image...", self.loadImage, Qt.CTRL + Qt.Key_L)
        self._menu.addAction("&Compute image...", self.computeImage, Qt.CTRL + Qt.Key_M)
        self._qa_load_clipboard = self._menu.addAction("Load from clipboard &path", self._loadClipboardPath,
                                                       Qt.CTRL + Qt.Key_P)
        self._checkClipboardPath()
        if self._imagecons:
            self._menu.addSeparator()
            # add controls to cycle images and planes
            for i, imgcon in enumerate(self._imagecons[::-1]):
                self._menu.addMenu(imgcon.getMenu())
            self._menu.addSeparator()
            if len(self._imagecons) > 1:
                self._menu.addAction("Cycle images", self.cycleImages, Qt.Key_F5)
                self._menu.addAction("Blink images", self.blinkImages, Qt.Key_F6)
            self._qa_slices = (
            (self._menu.addAction("Next slice along axis 1", self._currier.curry(self.incrementSlice, 0, 1), Qt.Key_F7),
             self._menu.addAction("Previous slice along axis 1", self._currier.curry(self.incrementSlice, 0, -1),
                                  Qt.SHIFT + Qt.Key_F7)),
            (self._menu.addAction("Next slice along axis 2", self._currier.curry(self.incrementSlice, 1, 1), Qt.Key_F8),
             self._menu.addAction("Previous slice along axis 2", self._currier.curry(self.incrementSlice, 1, -1),
                                  Qt.SHIFT + Qt.Key_F8)))
            self._menu.addSeparator()
            self._menu.addAction(self._qa_plot_top)
            self._menu.addAction(self._qa_plot_all)

    def computeImage(self, expression=None):
        """Computes image from expression (if expression is None, pops up dialog)"""
        if expression is None:
            (expression, ok) = QInputDialog.getText(self, "Compute image",
                                                    """Enter an image expression to compute.
                                              Any valid numpy expression is supported, and
                                              all functions from the numpy module are available (including sub-modules such as fft).
                                              Use 'a', 'b', 'c' to refer to images.
                                              Examples:  "(a+b)/2", "cos(a)+sin(b)", "a-a.mean()", "fft.fft2(a)", etc.""")
            #      (expression,ok) = QInputDialog.getText(self,"Compute image","""<P>Enter an expression to compute.
            #        Use 'a', 'b', etc. to refer to loaded images. Any valid numpy expression is supported, and all the
            #       functions from the numpy module are available. Examples of valid expressions include "(a+b)/2",
            #       "cos(a)+sin(b)", "a-a.mean()", etc.
            #        </P>
            #      """)
            expression = str(expression)
            if not ok or not expression:
                return
        # try to parse expression
        arglist = [(chr(ord('a') + ic.getNumber()), ic.image) for ic in self._imagecons]
        try:
            exprfunc = eval("lambda " + (",".join([x[0] for x in arglist])) + ":" + expression,
                            numpy.__dict__, {})
        except Exception as exc:
            self.showErrorMessage("""Error parsing expression "%s": %s.""" % (expression, str(exc)))
            return None
        # try to evaluate expression
        self.showMessage("Computing expression \"%s\"" % expression, 10000)
        busy = BusyIndicator()
        QApplication.flush()

        # trim trivial trailing dimensions. This avoids the problem of when an NxMx1 and an NxMx1x1 arrays are added,
        # the result is promoted to NxMxMx1 following the numpy rules.
        def trimshape(shape):
            out = shape
            while out and out[-1] == 1:
                out = out[:-1]
            return out

        def trimarray(array):
            return array.reshape(trimshape(array.shape))

        try:
            result = exprfunc(*[trimarray(x[1].data()) for x in arglist])
        except Exception as exc:
            busy = None
            traceback.print_exc()
            self.showErrorMessage("""Error evaluating "%s": %s.""" % (expression, str(exc)))
            return None
        busy = None
        if type(result) != numpy.ma.masked_array and type(result) != numpy.ndarray:
            self.showErrorMessage(
                """Result of "%s" is of invalid type "%s" (array expected).""" % (expression, type(result).__name__))
            return None
        # convert coomplex results to real
        if numpy.iscomplexobj(result):
            self.showErrorMessage("""Result of "%s" is complex. Complex images are currently
      not fully supported, so we'll implicitly use the absolute value instead.""" % (expression))
            expression = "abs(%s)" % expression
            result = abs(result)
        # determine which image this expression can be associated with
        res_shape = trimshape(result.shape)
        arglist = [x for x in arglist if hasattr(x[1], 'fits_header') and trimshape(x[1].data().shape) == res_shape]
        if not arglist:
            self.showErrorMessage("""Result of "%s" has shape %s, which does not match any loaded FITS image.""" % (
            expression, "x".join(map(str, result.shape))))
            return None
        # look for an image in the arglist with the same projection, and with a valid dirname
        # (for the where-to-save hint)
        template = arglist[0][1]
        # if all images in arglist have the same projection, then it doesn't matter what we use
        # else ask
        if len([x for x in arglist[1:] if x[1].projection == template.projection]) != len(arglist) - 1:
            options = [x[0] for x in arglist]
            (which, ok) = QInputDialog.getItem(self, "Compute image",
                                               "Coordinate system to use for the result of \"%s\":" % expression,
                                               options, 0, False)
            if not ok:
                return None
            try:
                template = arglist[options.index(which)][1]
            except:
                pass
        # create a FITS image
        busy = BusyIndicator()
        dprint(2, "creating FITS image", expression)
        self.showMessage("""Creating image for %s""" % expression, 3000)
        QApplication.flush()
        try:
            hdu = pyfits.PrimaryHDU(result.transpose(), template.fits_header)
            skyimage = SkyImage.FITSImagePlotItem(name=expression, filename=None, hdu=hdu)
        except:
            busy = None
            traceback.print_exc()
            self.showErrorMessage("""Error creating FITS image %s: %s""" % (expression, str(sys.exc_info()[1])))
            return None
        # get directory name for save-to hint
        dirname = getattr(template, 'filename', None)
        if not dirname:
            dirnames = [getattr(img, 'filename') for x, img in arglist if hasattr(img, 'filename')]
            dirname = dirnames[0] if dirnames else None
        # create control bar, add to widget stack
        self._createImageController(skyimage, expression, expression,
                                    save=((dirname and os.path.dirname(dirname)) or "."))
        self.showMessage("Created new image for %s" % expression, 3000)
        dprint(2, "image created")

    def _createImageController(self, image, name, basename, model=False, save=False):
        dprint(2, "creating ImageController for", name)
        ic = ImageController(image, self, self, name, save=save)
        ic.setNumber(len(self._imagecons))
        self._imagecons.insert(0, ic)
        self._imagecon_loadorder.append(ic)
        if model:
            self._model_imagecons.add(id(ic))
        self._lo.addWidget(ic)
        if self._border_pen:
            ic.addPlotBorder(self._border_pen, basename, self._label_color, self._label_bg_brush)
        # attach appropriate signals
        image.connect(SIGNAL("slice"), self.fastReplot)
        image.connect(SIGNAL("repaint"), self.replot)
        image.connect(SIGNAL("raise"), self._currier.curry(self.raiseImage, ic))
        image.connect(SIGNAL("unload"), self._currier.curry(self.unloadImage, ic))
        image.connect(SIGNAL("center"), self._currier.curry(self.centerImage, ic))
        QObject.connect(ic.renderControl(), SIGNAL("displayRangeChanged"),
                        self._currier.curry(self._updateDisplayRange, ic.renderControl()))
        QObject.connect(ic.renderControl(), SIGNAL("displayRangeLocked"),
                        self._currier.curry(self._lockDisplayRange, ic.renderControl()))
        self._plot = None
        # add to menus
        dprint(2, "repopulating menus")
        self._repopulateMenu()
        # center and raise to top of stack
        self.raiseImage(ic)
        if not self._center_image:
            self.centerImage(ic, emit=False)
        else:
            ic.setPlotProjection(self._center_image.projection)
        # signal
        self.emit(SIGNAL("imagesChanged"))
        return ic
Пример #6
0
class ImageControlDialog(QDialog):
    def __init__(self, parent, rc, imgman):
        """An ImageControlDialog is initialized with a parent widget, a RenderControl object,
        and an ImageManager object"""
        QDialog.__init__(self, parent)
        image = rc.image
        self.setWindowTitle("%s: Colour Controls" % image.name)
        self.setWindowIcon(pixmaps.colours.icon())
        self.setModal(False)
        self.image = image
        self._rc = rc
        self._imgman = imgman
        self._currier = PersistentCurrier()

        # init internal state
        self._prev_range = self._display_range = None, None
        self._hist = None
        self._geometry = None

        # create layouts
        lo0 = QVBoxLayout(self)
        #    lo0.setContentsMargins(0,0,0,0)

        # histogram plot
        whide = self.makeButton("Hide", self.hide, width=128)
        whide.setShortcut(Qt.Key_F9)
        lo0.addWidget(Separator(self, "Histogram and ITF", extra_widgets=[whide]))
        lo1 = QHBoxLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        self._histplot = QwtPlot(self)
        self._histplot.setAutoDelete(False)
        lo1.addWidget(self._histplot, 1)
        lo2 = QHBoxLayout()
        lo2.setContentsMargins(0, 0, 0, 0)
        lo2.setSpacing(2)
        lo0.addLayout(lo2)
        lo0.addLayout(lo1)
        self._wautozoom = QCheckBox("autozoom", self)
        self._wautozoom.setChecked(True)
        self._wautozoom.setToolTip("""<P>If checked, then the histrogram plot will zoom in automatically when
      you narrow the current intensity range.</P>""")
        self._wlogy = QCheckBox("log Y", self)
        self._wlogy.setChecked(True)
        self._ylogscale = True
        self._wlogy.setToolTip(
            """<P>If checked, a log-scale Y axis is used for the histogram plot instead of a linear one.""")
        QObject.connect(self._wlogy, SIGNAL("toggled(bool)"), self._setHistLogScale)
        self._whistunzoom = self.makeButton("", self._unzoomHistogram, icon=pixmaps.full_range.icon())
        self._whistzoomout = self.makeButton("-", self._currier.curry(self._zoomHistogramByFactor, math.sqrt(.1)))
        self._whistzoomin = self.makeButton("+", self._currier.curry(self._zoomHistogramByFactor, math.sqrt(10)))
        self._whistzoomin.setToolTip("""<P>Click to zoom into the histogram plot by one step. This does not
      change the current intensity range.</P>""")
        self._whistzoomout.setToolTip("""<P>Click to zoom out of the histogram plot by one step. This does not
      change the current intensity range.</P>""")
        self._whistunzoom.setToolTip("""<P>Click to reset the histogram plot back to its full extent.
      This does not change the current intensity range.</P>""")
        self._whistzoom = QwtWheel(self)
        self._whistzoom.setOrientation(Qt.Horizontal)
        self._whistzoom.setMaximumWidth(80)
        self._whistzoom.setRange(10, 0)
        self._whistzoom.setStep(0.1)
        self._whistzoom.setTickCnt(30)
        self._whistzoom.setTracking(False)
        QObject.connect(self._whistzoom, SIGNAL("valueChanged(double)"), self._zoomHistogramFinalize)
        QObject.connect(self._whistzoom, SIGNAL("sliderMoved(double)"), self._zoomHistogramPreview)
        self._whistzoom.setToolTip("""<P>Use this wheel control to zoom in/out of the histogram plot.
      This does not change the current intensity range.
      Note that the zoom wheel should also respond to your mouse wheel, if you have one.</P>""")
        # This works around a stupid bug in QwtSliders -- when using the mousewheel, only sliderMoved() signals are emitted,
        # with no final  valueChanged(). If we want to do a fast preview of something on sliderMoved(), and a "slow" final
        # step on valueChanged(), we're in trouble. So we start a timer on sliderMoved(), and if the timer expires without
        # anything else happening, do a valueChanged().
        # Here we use a timer to call zoomHistogramFinalize() w/o an argument.
        self._whistzoom_timer = QTimer(self)
        self._whistzoom_timer.setSingleShot(True)
        self._whistzoom_timer.setInterval(500)
        QObject.connect(self._whistzoom_timer, SIGNAL("timeout()"), self._zoomHistogramFinalize)
        # set same size for all buttons and controls
        width = 24
        for w in self._whistunzoom, self._whistzoomin, self._whistzoomout:
            w.setMinimumSize(width, width)
            w.setMaximumSize(width, width)
        self._whistzoom.setMinimumSize(80, width)
        self._wlab_histpos_text = "(hover here for help)"
        self._wlab_histpos = QLabel(self._wlab_histpos_text, self)
        self._wlab_histpos.setToolTip("""
      <P>The plot shows a histogram of either the full image or its selected subset
      (as per the "Data subset" section below).</P>
      <P>The current intensity range is indicated by the grey box
      in the plot.</P>
      <P>Use the left mouse button to change the low intensity limit, and the right
      button (on Macs, use Ctrl-click) to change the high limit.</P>
      <P>Use Shift with the left mouse button to zoom into an area of the histogram,
      or else use the "zoom wheel" control or the plus/minus toolbuttons above the histogram to zoom in or out.
      To zoom back out to the full extent of the histogram, click on the rightmost button above the histogram.</P>
      """)
        lo2.addWidget(self._wlab_histpos, 1)
        lo2.addWidget(self._wautozoom)
        lo2.addWidget(self._wlogy, 0)
        lo2.addWidget(self._whistzoomin, 0)
        lo2.addWidget(self._whistzoom, 0)
        lo2.addWidget(self._whistzoomout, 0)
        lo2.addWidget(self._whistunzoom, 0)
        self._zooming_histogram = False

        sliced_axes = rc.slicedAxes()
        dprint(1, "sliced axes are", sliced_axes)
        self._stokes_axis = None

        # subset indication
        lo0.addWidget(Separator(self, "Data subset"))
        # sliced axis selectors
        self._wslicers = []
        if sliced_axes:
            lo1 = QHBoxLayout()
            lo1.setContentsMargins(0, 0, 0, 0)
            lo1.setSpacing(2)
            lo0.addLayout(lo1)
            lo1.addWidget(QLabel("Current slice:  ", self))
            for i, (iextra, name, labels) in enumerate(sliced_axes):
                lo1.addWidget(QLabel("%s:" % name, self))
                if name == "STOKES":
                    self._stokes_axis = iextra
                # add controls
                wslicer = QComboBox(self)
                self._wslicers.append(wslicer)
                wslicer.addItems(labels)
                wslicer.setToolTip("""<P>Selects current slice along the %s axis.</P>""" % name)
                wslicer.setCurrentIndex(self._rc.currentSlice()[iextra])
                QObject.connect(wslicer, SIGNAL("activated(int)"), self._currier.curry(self._rc.changeSlice, iextra))
                lo2 = QVBoxLayout()
                lo1.addLayout(lo2)
                lo2.setContentsMargins(0, 0, 0, 0)
                lo2.setSpacing(0)
                wminus = QToolButton(self)
                wminus.setArrowType(Qt.UpArrow)
                QObject.connect(wminus, SIGNAL("clicked()"), self._currier.curry(self._rc.incrementSlice, iextra, 1))
                if i == 0:
                    wminus.setShortcut(Qt.SHIFT + Qt.Key_F7)
                elif i == 1:
                    wminus.setShortcut(Qt.SHIFT + Qt.Key_F8)
                wplus = QToolButton(self)
                wplus.setArrowType(Qt.DownArrow)
                QObject.connect(wplus, SIGNAL("clicked()"), self._currier.curry(self._rc.incrementSlice, iextra, -1))
                if i == 0:
                    wplus.setShortcut(Qt.Key_F7)
                elif i == 1:
                    wplus.setShortcut(Qt.Key_F8)
                wminus.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
                wplus.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
                sz = QSize(12, 8)
                wminus.setMinimumSize(sz)
                wplus.setMinimumSize(sz)
                wminus.resize(sz)
                wplus.resize(sz)
                lo2.addWidget(wminus)
                lo2.addWidget(wplus)
                lo1.addWidget(wslicer)
                lo1.addSpacing(5)
            lo1.addStretch(1)
        # subset indicator
        lo1 = QHBoxLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        lo1.setSpacing(2)
        lo0.addLayout(lo1)
        self._wlab_subset = QLabel("Subset: xxx", self)
        self._wlab_subset.setToolTip("""<P>This indicates the current data subset to which the histogram
      and the stats given here apply. Use the "Reset to" control on the right to change the
      current subset and recompute the histogram and stats.</P>""")
        lo1.addWidget(self._wlab_subset, 1)

        self._wreset_full = self.makeButton("\u2192 full", self._rc.setFullSubset)
        lo1.addWidget(self._wreset_full)
        if sliced_axes:
            #      if self._stokes_axis is not None and len(sliced_axes)>1:
            #        self._wreset_stokes = self.makeButton(u"\u21920Stokes",self._rc.setFullSubset)
            self._wreset_slice = self.makeButton("\u2192 slice", self._rc.setSliceSubset)
            lo1.addWidget(self._wreset_slice)
        else:
            self._wreset_slice = None

        # min/max controls
        lo1 = QHBoxLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        lo0.addLayout(lo1, 0)
        self._wlab_stats = QLabel(self)
        lo1.addWidget(self._wlab_stats, 0)
        self._wmore_stats = self.makeButton("more...", self._showMeanStd)
        self._wlab_stats.setMinimumHeight(self._wmore_stats.height())
        lo1.addWidget(self._wmore_stats, 0)
        lo1.addStretch(1)

        # intensity controls
        lo0.addWidget(Separator(self, "Intensity mapping"))
        lo1 = QHBoxLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        lo1.setSpacing(2)
        lo0.addLayout(lo1, 0)
        self._range_validator = FloatValidator(self)
        self._wrange = QLineEdit(self), QLineEdit(self)
        self._wrange[0].setToolTip("""<P>This is the low end of the intensity range.</P>""")
        self._wrange[1].setToolTip("""<P>This is the high end of the intensity range.</P>""")
        for w in self._wrange:
            w.setValidator(self._range_validator)
            QObject.connect(w, SIGNAL("editingFinished()"), self._changeDisplayRange)
        lo1.addWidget(QLabel("low:", self), 0)
        lo1.addWidget(self._wrange[0], 1)
        self._wrangeleft0 = self.makeButton("\u21920", self._setZeroLeftLimit, width=32)
        self._wrangeleft0.setToolTip("""<P>Click this to set the low end of the intensity range to 0.</P>""")
        lo1.addWidget(self._wrangeleft0, 0)
        lo1.addSpacing(8)
        lo1.addWidget(QLabel("high:", self), 0)
        lo1.addWidget(self._wrange[1], 1)
        lo1.addSpacing(8)
        self._wrange_full = self.makeButton(None, self._setHistDisplayRange, icon=pixmaps.intensity_graph.icon())
        lo1.addWidget(self._wrange_full)
        self._wrange_full.setToolTip(
            """<P>Click this to reset the intensity range to the current extent of the histogram plot.</P>""")
        # add menu for display range
        range_menu = QMenu(self)
        wrange_menu = QToolButton(self)
        wrange_menu.setText("Reset to")
        wrange_menu.setToolTip("""<P>Use this to reset the intensity range to various pre-defined settings.</P>""")
        lo1.addWidget(wrange_menu)
        self._qa_range_full = range_menu.addAction(pixmaps.full_range.icon(), "Full subset",
                                                   self._rc.resetSubsetDisplayRange)
        self._qa_range_hist = range_menu.addAction(pixmaps.intensity_graph.icon(), "Current histogram limits",
                                                   self._setHistDisplayRange)
        for percent in (99.99, 99.9, 99.5, 99, 98, 95):
            range_menu.addAction("%g%%" % percent, self._currier.curry(self._changeDisplayRangeToPercent, percent))
        wrange_menu.setMenu(range_menu)
        wrange_menu.setPopupMode(QToolButton.InstantPopup)

        lo1 = QGridLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        lo0.addLayout(lo1, 0)
        self._wimap = QComboBox(self)
        lo1.addWidget(QLabel("Intensity policy:", self), 0, 0)
        lo1.addWidget(self._wimap, 1, 0)
        self._wimap.addItems(rc.getIntensityMapNames())
        QObject.connect(self._wimap, SIGNAL("currentIndexChanged(int)"), self._rc.setIntensityMapNumber)
        self._wimap.setToolTip("""<P>Use this to change the type of the intensity transfer function (ITF).</P>""")

        # log cycles control
        lo1.setColumnStretch(1, 1)
        self._wlogcycles_label = QLabel("Log cycles: ", self)
        lo1.addWidget(self._wlogcycles_label, 0, 1)
        #    self._wlogcycles = QwtWheel(self)
        #    self._wlogcycles.setTotalAngle(360)
        self._wlogcycles = QwtSlider(self)
        self._wlogcycles.setToolTip(
            """<P>Use this to change the log-base for the logarithmic intensity transfer function (ITF).</P>""")
        # This works around a stupid bug in QwtSliders -- see comments on histogram zoom wheel above
        self._wlogcycles_timer = QTimer(self)
        self._wlogcycles_timer.setSingleShot(True)
        self._wlogcycles_timer.setInterval(500)
        QObject.connect(self._wlogcycles_timer, SIGNAL("timeout()"), self._setIntensityLogCycles)
        lo1.addWidget(self._wlogcycles, 1, 1)
        self._wlogcycles.setRange(1., 10)
        self._wlogcycles.setStep(0.1)
        self._wlogcycles.setTracking(False)
        QObject.connect(self._wlogcycles, SIGNAL("valueChanged(double)"), self._setIntensityLogCycles)
        QObject.connect(self._wlogcycles, SIGNAL("sliderMoved(double)"), self._previewIntensityLogCycles)
        self._updating_imap = False

        # lock intensity map
        lo1 = QHBoxLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        lo0.addLayout(lo1, 0)
        #    lo1.addWidget(QLabel("Lock range accross",self))
        wlock = QCheckBox("Lock display range", self)
        wlock.setToolTip("""<P>If checked, then the intensity range will be locked. The ranges of all locked images
      change simultaneously.</P>""")
        lo1.addWidget(wlock)
        wlockall = QToolButton(self)
        wlockall.setIcon(pixmaps.locked.icon())
        wlockall.setText("Lock all to this")
        wlockall.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        wlockall.setAutoRaise(True)
        wlockall.setToolTip("""<P>Click this to lock together the intensity ranges of all images.</P>""")
        lo1.addWidget(wlockall)
        wunlockall = QToolButton(self)
        wunlockall.setIcon(pixmaps.unlocked.icon())
        wunlockall.setText("Unlock all")
        wunlockall.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        wunlockall.setAutoRaise(True)
        wunlockall.setToolTip("""<P>Click this to unlock the intensity ranges of all images.</P>""")
        lo1.addWidget(wunlockall)
        wlock.setChecked(self._rc.isDisplayRangeLocked())
        QObject.connect(wlock, SIGNAL("clicked(bool)"), self._rc.lockDisplayRange)
        QObject.connect(wlockall, SIGNAL("clicked()"),
                        self._currier.curry(self._imgman.lockAllDisplayRanges, self._rc))
        QObject.connect(wunlockall, SIGNAL("clicked()"), self._imgman.unlockAllDisplayRanges)
        QObject.connect(self._rc, SIGNAL("displayRangeLocked"), wlock.setChecked)

        #    self._wlock_imap_axis = [ QCheckBox(name,self) for iaxis,name,labels in sliced_axes ]
        #    for iw,w in enumerate(self._wlock_imap_axis):
        #      QObject.connect(w,SIGNAL("toggled(bool)"),self._currier.curry(self._rc.lockDisplayRangeForAxis,iw))
        #      lo1.addWidget(w,0)
        lo1.addStretch(1)

        # lo0.addWidget(Separator(self,"Colourmap"))
        # color bar
        self._colorbar = QwtPlot(self)
        lo0.addWidget(self._colorbar)
        self._colorbar.setAutoDelete(False)
        self._colorbar.setMinimumHeight(32)
        self._colorbar.enableAxis(QwtPlot.yLeft, False)
        self._colorbar.enableAxis(QwtPlot.xBottom, False)
        # color plot
        self._colorplot = QwtPlot(self)
        lo0.addWidget(self._colorplot)
        self._colorplot.setAutoDelete(False)
        self._colorplot.setMinimumHeight(64)
        self._colorplot.enableAxis(QwtPlot.yLeft, False)
        self._colorplot.enableAxis(QwtPlot.xBottom, False)
        # self._colorplot.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Preferred)
        self._colorbar.hide()
        self._colorplot.hide()
        # color controls
        lo1 = QHBoxLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        lo0.addLayout(lo1, 1)
        lo1.addWidget(QLabel("Colourmap:", self))
        # colormap list
        ### NB: use setIconSize() and icons in QComboBox!!!
        self._wcolmaps = QComboBox(self)
        self._wcolmaps.setIconSize(QSize(128, 16))
        self._wcolmaps.setToolTip("""<P>Use this to select a different colourmap.</P>""")
        for cmap in self._rc.getColormapList():
            self._wcolmaps.addItem(QIcon(cmap.makeQPixmap(128, 16)), cmap.name)
        lo1.addWidget(self._wcolmaps)
        QObject.connect(self._wcolmaps, SIGNAL("activated(int)"), self._rc.setColorMapNumber)
        # add widgetstack for colormap controls
        self._wcolmap_control_stack = QStackedWidget(self)
        self._wcolmap_control_blank = QWidget(self._wcolmap_control_stack)
        self._wcolmap_control_stack.addWidget(self._wcolmap_control_blank)
        lo0.addWidget(self._wcolmap_control_stack)
        self._colmap_controls = []
        # add controls to stack
        for index, cmap in enumerate(self._rc.getColormapList()):
            if isinstance(cmap, Colormaps.ColormapWithControls):
                controls = cmap.makeControlWidgets(self._wcolmap_control_stack)
                self._wcolmap_control_stack.addWidget(controls)
                QObject.connect(cmap, SIGNAL("colormapChanged"),
                                self._currier.curry(self._previewColormapParameters, index, cmap))
                QObject.connect(cmap, SIGNAL("colormapPreviewed"),
                                self._currier.curry(self._previewColormapParameters, index, cmap))
                self._colmap_controls.append(controls)
            else:
                self._colmap_controls.append(self._wcolmap_control_blank)

        # connect updates from renderControl and image
        self.image.connect(SIGNAL("slice"), self._updateImageSlice)
        QObject.connect(self._rc, SIGNAL("intensityMapChanged"), self._updateIntensityMap)
        QObject.connect(self._rc, SIGNAL("colorMapChanged"), self._updateColorMap)
        QObject.connect(self._rc, SIGNAL("dataSubsetChanged"), self._updateDataSubset)
        QObject.connect(self._rc, SIGNAL("displayRangeChanged"), self._updateDisplayRange)

        # update widgets
        self._setupHistogramPlot()
        self._updateDataSubset(*self._rc.currentSubset())
        self._updateColorMap(image.colorMap())
        self._updateIntensityMap(rc.currentIntensityMap(), rc.currentIntensityMapNumber())
        self._updateDisplayRange(*self._rc.displayRange())

    def makeButton(self, label, callback=None, width=None, icon=None):
        btn = QToolButton(self)
        #    btn.setAutoRaise(True)
        label and btn.setText(label)
        icon and btn.setIcon(icon)
        #    btn = QPushButton(label,self)
        #   btn.setFlat(True)
        if width:
            btn.setMinimumWidth(width)
            btn.setMaximumWidth(width)
        if icon:
            btn.setIcon(icon)
        if callback:
            QObject.connect(btn, SIGNAL("clicked()"), callback)
        return btn

    #  def closeEvent (self,ev):
    #    ev.ignore()
    #    self.hide()

    def hide(self):
        self._geometry = self.geometry()
        QDialog.hide(self)

    def show(self):
        dprint(4, "show entrypoint")
        if self._geometry:
            dprint(4, "setting geometry")
            self.setGeometry(self._geometry)
        if self._hist is None:
            busy = BusyIndicator()
            dprint(4, "updating histogram")
            self._updateHistogram()
            dprint(4, "updating stats")
            self._updateStats(self._subset, self._subset_range)
            busy = None
        dprint(4, "calling QDialog.show")
        QDialog.show(self)

    # number of bins used to compute intensity transfer function
    NumItfBins = 1000
    # number of bins used for displaying histograms
    NumHistBins = 500
    # number of bins used for high-res histograms
    NumHistBinsHi = 10000
    # colorbar height, as fraction of plot area
    ColorBarHeight = 0.1

    class HistLimitPicker(QwtPlotPicker):
        """Auguments QwtPlotPicker with functions for selecting hist min/max values"""

        def __init__(self, plot, label, color="green", mode=QwtPicker.PointSelection,
                     rubber_band=QwtPicker.VLineRubberBand, tracker_mode=QwtPicker.ActiveOnly, track=None):
            QwtPlotPicker.__init__(self, QwtPlot.xBottom, QwtPlot.yRight, mode, rubber_band, tracker_mode,
                                   plot.canvas())
            self.plot = plot
            self.label = label
            self.track = track
            self.color = QColor(color)
            self.setRubberBandPen(QPen(self.color))

        def trackerText(self, pos):
            x, y = self.plot.invTransform(QwtPlot.xBottom, pos.x()), self.plot.invTransform(QwtPlot.yLeft, pos.y())
            if self.track:
                text = self.track(x, y)
                if text is not None:
                    return text
            if self.label:
                text = QwtText(self.label % dict(x=x, y=y))
                text.setColor(self.color)
                return text
            return QwtText()

        def widgetLeaveEvent(self, ev):
            if self.track:
                self.track(None, None)
            QwtPlotPicker.widgetLeaveEvent(self, ev)

    class ColorBarPlotItem(QwtPlotItem):
        def __init__(self, y0, y1, *args):
            QwtPlotItem.__init__(self, *args)
            self._y0 = y1
            self._dy = y1 - y0

        def setIntensityMap(self, imap):
            self.imap = imap

        def setColorMap(self, cmap):
            self.cmap = cmap

        def draw(self, painter, xmap, ymap, rect):
            """Implements QwtPlotItem.draw(), to render the colorbar on the given painter."""
            xp1, xp2, xdp, xs1, xs2, xds = xinfo = xmap.p1(), xmap.p2(), xmap.pDist(), xmap.s1(), xmap.s2(), xmap.sDist()
            yp1, yp2, ydp, ys1, ys2, yds = yinfo = ymap.p1(), ymap.p2(), ymap.pDist(), ymap.s1(), ymap.s2(), ymap.sDist()
            # xp: coordinates of pixels xp1...xp2 in data units
            xp = xs1 + (xds / xdp) * (0.5 + numpy.arange(int(xdp)))
            # convert y0 and y1 into pixel coordinates
            y0 = yp1 - (self._y0 - ys1) * (ydp / yds)
            dy = self._dy * (ydp / yds)
            # remap into an Nx1 image
            qimg = self.cmap.colorize(self.imap.remap(xp.reshape((len(xp), 1))))
            # plot image
            painter.drawImage(QRect(xp1, y0, xdp, dy), qimg)

    class HistogramLineMarker(object):
        """Helper class implementing a line marker for a histogram plot"""

        def __init__(self, plot, color="black", linestyle=Qt.DotLine, align=Qt.AlignBottom | Qt.AlignRight, z=90,
                     label="", zlabel=None, linewidth=1, spacing=2,
                     yaxis=QwtPlot.yRight):
            self.line = TiggerPlotCurve()
            self.color = color = color if isinstance(color, QColor) else QColor(color)
            self.line.setPen(QPen(color, linewidth, linestyle))
            self.marker = TiggerPlotMarker()
            self.marker.setLabelAlignment(align)
            try:
                self.marker.setSpacing(spacing)
            except AttributeError:
                pass
            self.setText(label)
            self.line.setZ(z)
            self.marker.setZ(zlabel if zlabel is not None else z)
            # set axes -- using yRight, since that is the "markup" z-axis
            self.line.setAxis(QwtPlot.xBottom, yaxis)
            self.marker.setAxis(QwtPlot.xBottom, yaxis)
            # attach to plot
            self.line.attach(plot)
            self.marker.attach(plot)

        def show(self):
            self.line.show()
            self.marker.show()

        def hide(self):
            self.line.hide()
            self.marker.hide()

        def setText(self, text):
            label = QwtText(text)
            label.setColor(self.color)
            self.marker.setLabel(label)

    def _setupHistogramPlot(self):
        self._histplot.setCanvasBackground(QColor("lightgray"))
        self._histplot.setAxisFont(QwtPlot.yLeft, QApplication.font())
        self._histplot.setAxisFont(QwtPlot.xBottom, QApplication.font())
        # add histogram curves
        self._histcurve1 = TiggerPlotCurve()
        self._histcurve2 = TiggerPlotCurve()
        self._histcurve1.setStyle(QwtPlotCurve.Steps)
        self._histcurve2.setStyle(QwtPlotCurve.Steps)
        self._histcurve1.setPen(QPen(Qt.NoPen))
        self._histcurve1.setBrush(QBrush(QColor("slategrey")))
        pen = QPen(QColor("red"))
        pen.setWidth(1)
        self._histcurve2.setPen(pen)
        self._histcurve1.setZ(0)
        self._histcurve2.setZ(100)
        #    self._histcurve1.attach(self._histplot)
        self._histcurve2.attach(self._histplot)
        # add maxbin and half-max curves
        self._line_0 = self.HistogramLineMarker(self._histplot, color="grey50", linestyle=Qt.SolidLine,
                                                align=Qt.AlignTop | Qt.AlignLeft, z=90)
        self._line_mean = self.HistogramLineMarker(self._histplot, color="black", linestyle=Qt.SolidLine,
                                                   align=Qt.AlignBottom | Qt.AlignRight, z=91,
                                                   label="mean", zlabel=151)
        self._line_std = self.HistogramLineMarker(self._histplot, color="black", linestyle=Qt.SolidLine,
                                                  align=Qt.AlignTop | Qt.AlignRight, z=91,
                                                  label="std", zlabel=151)
        sym = QwtSymbol()
        sym.setStyle(QwtSymbol.VLine)
        sym.setSize(8)
        self._line_std.line.setSymbol(sym)
        self._line_maxbin = self.HistogramLineMarker(self._histplot, color="green", linestyle=Qt.DotLine,
                                                     align=Qt.AlignTop | Qt.AlignRight, z=92,
                                                     label="max bin", zlabel=150)
        self._line_halfmax = self.HistogramLineMarker(self._histplot, color="green", linestyle=Qt.DotLine,
                                                      align=Qt.AlignBottom | Qt.AlignRight, z=90,
                                                      label="half-max", yaxis=QwtPlot.yLeft)
        # add current range
        self._rangebox = TiggerPlotCurve()
        self._rangebox.setStyle(QwtPlotCurve.Steps)
        self._rangebox.setYAxis(QwtPlot.yRight)
        self._rangebox.setPen(QPen(Qt.NoPen))
        self._rangebox.setBrush(QBrush(QColor("darkgray")))
        self._rangebox.setZ(50)
        self._rangebox.attach(self._histplot)
        self._rangebox2 = TiggerPlotCurve()
        self._rangebox2.setStyle(QwtPlotCurve.Sticks)
        self._rangebox2.setYAxis(QwtPlot.yRight)
        self._rangebox2.setZ(60)
        #  self._rangebox2.attach(self._histplot)
        # add intensity transfer function
        self._itfcurve = TiggerPlotCurve()
        self._itfcurve.setStyle(QwtPlotCurve.Lines)
        self._itfcurve.setPen(QPen(QColor("blue")))
        self._itfcurve.setYAxis(QwtPlot.yRight)
        self._itfcurve.setZ(120)
        self._itfcurve.attach(self._histplot)
        self._itfmarker = TiggerPlotMarker()
        label = QwtText("ITF")
        label.setColor(QColor("blue"))
        self._itfmarker.setLabel(label)
        try:
            self._itfmarker.setSpacing(0)
        except AttributeError:
            pass
        self._itfmarker.setLabelAlignment(Qt.AlignTop | Qt.AlignRight)
        self._itfmarker.setZ(120)
        self._itfmarker.attach(self._histplot)
        # add colorbar
        self._cb_item = self.ColorBarPlotItem(1, 1 + self.ColorBarHeight)
        self._cb_item.setYAxis(QwtPlot.yRight)
        self._cb_item.attach(self._histplot)
        # add pickers
        self._hist_minpicker = self.HistLimitPicker(self._histplot, "low: %(x).4g")
        self._hist_minpicker.setMousePattern(QwtEventPattern.MouseSelect1, Qt.LeftButton)
        QObject.connect(self._hist_minpicker, SIGNAL("selected(const QwtDoublePoint &)"), self._selectLowLimit)
        self._hist_maxpicker = self.HistLimitPicker(self._histplot, "high: %(x).4g")
        self._hist_maxpicker.setMousePattern(QwtEventPattern.MouseSelect1, Qt.RightButton)
        QObject.connect(self._hist_maxpicker, SIGNAL("selected(const QwtDoublePoint &)"), self._selectHighLimit)
        self._hist_maxpicker1 = self.HistLimitPicker(self._histplot, "high: %(x).4g")
        self._hist_maxpicker1.setMousePattern(QwtEventPattern.MouseSelect1, Qt.LeftButton, Qt.CTRL)
        QObject.connect(self._hist_maxpicker1, SIGNAL("selected(const QwtDoublePoint &)"), self._selectHighLimit)
        self._hist_zoompicker = self.HistLimitPicker(self._histplot, label="zoom",
                                                     tracker_mode=QwtPicker.AlwaysOn, track=self._trackHistCoordinates,
                                                     color="black",
                                                     mode=QwtPicker.RectSelection,
                                                     rubber_band=QwtPicker.RectRubberBand)
        self._hist_zoompicker.setMousePattern(QwtEventPattern.MouseSelect1, Qt.LeftButton, Qt.SHIFT)
        QObject.connect(self._hist_zoompicker, SIGNAL("selected(const QwtDoubleRect &)"), self._zoomHistogramIntoRect)

    def _trackHistCoordinates(self, x, y):
        self._wlab_histpos.setText((DataValueFormat + " %d") % (x, y) if x is not None else self._wlab_histpos_text)
        return QwtText()

    def _updateITF(self):
        """Updates current ITF array."""
        # do nothing if no histogram -- means we're not visible
        if self._hist is not None:
            xdata = self._itf_bins
            ydata = self.image.intensityMap().remap(xdata)
            self._rangebox.setData(self._rc.displayRange(), [1, 1])
            self._rangebox2.setData(self._rc.displayRange(), [1, 1])
            self._itfcurve.setData(xdata, ydata)
            self._itfmarker.setValue(xdata[0], 1)

    def _updateHistogram(self, hmin=None, hmax=None):
        """Recomputes histogram. If no arguments, computes full histogram for
        data subset. If hmin/hmax is specified, computes zoomed-in histogram."""
        busy = BusyIndicator()
        self._prev_range = self._display_range
        dmin, dmax = self._subset_range
        hmin0, hmax0 = dmin, dmax
        if hmin0 >= hmax0:
            hmax0 = hmin0 + 1
        subset, mask = self.image.optimalRavel(self._subset)
        # compute full-subset hi-res histogram, if we don't have one (for percentile stats)
        if self._hist_hires is None:
            dprint(1, "computing histogram for full subset range", hmin0, hmax0)
            self._hist_hires = measurements.histogram(subset, hmin0, hmax0, self.NumHistBinsHi, labels=mask,
                                                      index=None if mask is None else False)
            self._hist_bins_hires = hmin0 + (hmax0 - hmin0) * (numpy.arange(self.NumHistBinsHi) + 0.5) / float(
                self.NumHistBinsHi)
            self._hist_binsize_hires = (hmax0 - hmin0) / self.NumHistBins
        # if hist limits not specified, then compute lo-res histogram based on the hi-res one
        if hmin is None:
            hmin, hmax = hmin0, hmax0
            # downsample to low-res histogram
            self._hist = self._hist_hires.reshape((self.NumHistBins, self.NumHistBinsHi / self.NumHistBins)).sum(1)
        else:
            # zoomed-in low-res histogram
            # bracket limits at subset range
            hmin, hmax = max(hmin, dmin), min(hmax, dmax)
            if hmin >= hmax:
                hmax = hmin + 1
            dprint(1, "computing histogram for", self._subset.shape, self._subset.dtype, hmin, hmax)
            self._hist = measurements.histogram(subset, hmin, hmax, self.NumHistBins, labels=mask,
                                                index=None if mask is None else False)
        dprint(1, "histogram computed")
        # compute bins
        self._itf_bins = hmin + (hmax - hmin) * (numpy.arange(self.NumItfBins)) / (float(self.NumItfBins) - 1)
        self._hist_bins = hmin + (hmax - hmin) * (numpy.arange(self.NumHistBins) + 0.5) / float(self.NumHistBins)
        # histogram range and position of peak
        self._hist_range = hmin, hmax
        self._hist_min, self._hist_max, self._hist_imin, self._hist_imax = measurements.extrema(self._hist)
        self._hist_peak = self._hist_bins[self._hist_imax]
        # set controls accordingly
        if dmin >= dmax:
            dmax = dmin + 1
        zoom = math.log10((dmax - dmin) / (hmax - hmin))
        self._whistzoom.setValue(zoom)
        self._whistunzoom.setEnabled(zoom > 0)
        self._whistzoomout.setEnabled(zoom > 0)
        # reset scales
        self._histplot.setAxisScale(QwtPlot.xBottom, hmin, hmax)
        self._histplot.setAxisScale(QwtPlot.yRight, 0, 1 + self.ColorBarHeight)
        # update curves
        # call _setHistLogScale() (with current setting) to update axis scales and set data
        self._setHistLogScale(self._ylogscale, replot=False)
        # set plot lines
        self._line_0.line.setData([0, 0], [0, 1])
        self._line_0.marker.setValue(0, 0)
        self._line_maxbin.line.setData([self._hist_peak, self._hist_peak], [0, 1])
        self._line_maxbin.marker.setValue(self._hist_peak, 0)
        self._line_maxbin.setText(("max bin:" + DataValueFormat) % self._hist_peak)
        # set half-max line
        self._line_halfmax.line.setData(self._hist_range, [self._hist_max / 2, self._hist_max / 2])
        self._line_halfmax.marker.setValue(hmin, self._hist_max / 2)
        # update ITF
        self._updateITF()

    def _updateStats(self, subset, minmax):
        """Recomputes subset statistics."""
        if subset.size <= (2048 * 2048):
            self._showMeanStd(busy=False)
        else:
            self._wlab_stats.setText(
                ("min: %s  max: %s  np: %d" % (DataValueFormat, DataValueFormat, self._subset.size)) % minmax)
            self._wmore_stats.show()

    def _updateDataSubset(self, subset, minmax, desc, subset_type):
        """Called when the displayed data subset is changed. Updates the histogram."""
        self._subset = subset
        self._subset_range = minmax
        self._wlab_subset.setText("Subset: %s" % desc)
        self._hist = self._hist_hires = None
        self._wreset_full.setVisible(subset_type is not RenderControl.SUBSET_FULL)
        self._wreset_slice and self._wreset_slice.setVisible(subset_type is not RenderControl.SUBSET_SLICE)
        # hide the mean/std markers, they will only be shown when _showMeanStd() is called
        self._line_mean.hide()
        self._line_std.hide()
        # if we're visibile, recompute histograms and stats
        if self.isVisible():
            # if subset is sufficiently small, compute extended stats on-the-fly. Else show the "more" button to compute them later
            self._updateHistogram()
            self._updateStats(subset, minmax)
            self._histplot.replot()

    def _showMeanStd(self, busy=True):
        if busy:
            busy = BusyIndicator()
        dmin, dmax = self._subset_range
        subset, mask = self.image.optimalRavel(self._subset)
        dprint(5, "computing mean")
        mean = measurements.mean(subset, labels=mask, index=None if mask is None else False)
        dprint(5, "computing std")
        std = measurements.standard_deviation(subset, labels=mask, index=None if mask is None else False)
        dprint(5, "done")
        text = "  ".join([("%s: " + DataValueFormat) % (name, value) for name, value in
                          ("min", dmin), ("max", dmax), ("mean", mean), ("std", std)] + ["np: %d" % self._subset.size])
        self._wlab_stats.setText(text)
        self._wmore_stats.hide()
        # update markers
        ypos = 0.3
        self._line_mean.line.setData([mean, mean], [0, 1])
        self._line_mean.marker.setValue(mean, ypos)
        self._line_mean.setText(("\u03BC=" + DataValueFormat) % mean)
        self._line_mean.show()
        self._line_std.line.setData([mean - std, mean + std], [ypos, ypos])
        self._line_std.marker.setValue(mean, ypos)
        self._line_std.setText(("\u03C3=" + DataValueFormat) % std)
        self._line_std.show()
        self._histplot.replot()

    def _setIntensityLogCyclesLabel(self, value):
        self._wlogcycles_label.setText("Log cycles: %4.1f" % value)

    def _previewIntensityLogCycles(self, value):
        self._setIntensityLogCycles(value, notify_image=False, write_config=False)
        self._wlogcycles_timer.start(500)

    def _setIntensityLogCycles(self, value=None, notify_image=True, write_config=True):
        if value is None:
            value = self._wlogcycles.value()
        # stop timer if being called to finalize the change in value
        if notify_image:
            self._wlogcycles_timer.stop()
        if not self._updating_imap:
            self._setIntensityLogCyclesLabel(value)
            self._rc.setIntensityMapLogCycles(value, notify_image=notify_image, write_config=write_config)
            self._updateITF()
            self._histplot.replot()

    def _updateDisplayRange(self, dmin, dmax):
        self._rangebox.setData([dmin, dmax], [.9, .9])
        self._wrange[0].setText(DataValueFormat % dmin)
        self._wrange[1].setText(DataValueFormat % dmax)
        self._wrangeleft0.setEnabled(dmin != 0)
        self._display_range = dmin, dmax
        # if auto-zoom is on, zoom the histogram
        # try to be a little clever about this. Zoom only if (a) both limits have changed (so that adjusting one end of the range
        # does not cause endless rezooms), or (b) display range is < 1/10 of the histogram range
        if self._wautozoom.isChecked() and self._hist is not None:
            if (dmax - dmin) / (self._hist_range[1] - self._hist_range[0]) < .1 or (
                    dmin != self._prev_range[0] and dmax != self._prev_range[1]):
                margin = (dmax - dmin) / 8
                self._updateHistogram(dmin - margin, dmax + margin)
        self._updateITF()
        self._histplot.replot()

    def _updateIntensityMap(self, imap, index):
        self._updating_imap = True
        try:
            self._cb_item.setIntensityMap(imap)
            self._updateITF()
            self._histplot.replot()
            self._wimap.setCurrentIndex(index)
            if isinstance(imap, Colormaps.LogIntensityMap):
                self._wlogcycles.setValue(imap.log_cycles)
                self._setIntensityLogCyclesLabel(imap.log_cycles)
                self._wlogcycles.show()
                self._wlogcycles_label.show()
            else:
                self._wlogcycles.hide()
                self._wlogcycles_label.hide()
        finally:
            self._updating_imap = False

    def _updateColorMap(self, cmap):
        self._cb_item.setColorMap(cmap)
        self._histplot.replot()
        try:
            index = self._rc.getColormapList().index(cmap)
        except:
            return
        self._setCurrentColormapNumber(index, cmap)

    def _previewColormapParameters(self, index, cmap):
        """Called to preview a new colormap parameter value"""
        self._histplot.replot()
        self._wcolmaps.setItemIcon(index, QIcon(cmap.makeQPixmap(128, 16)))

    def _setCurrentColormapNumber(self, index, cmap):
        self._wcolmaps.setCurrentIndex(index)
        # show controls for colormap
        self._wcolmap_control_stack.setCurrentWidget(self._colmap_controls[index])

    def _changeDisplayRange(self):
        """Gets display range from widgets and updates the image with it."""
        try:
            newrange = [float(str(w.text())) for w in self._wrange]
        except ValueError:
            return
        self._rc.setDisplayRange(*newrange)

    def _setHistDisplayRange(self):
        self._rc.setDisplayRange(*self._hist_range)

    def _updateImageSlice(self, slice):
        for i, (iextra, name, labels) in enumerate(self._rc.slicedAxes()):
            self._wslicers[i].setCurrentIndex(slice[iextra])

    def _changeDisplayRangeToPercent(self, percent):
        busy = BusyIndicator()
        if self._hist is None:
            self._updateHistogram()
            self._updateStats(self._subset, self._subset_range)
        # delta: we need the [delta,100-delta] interval of the total distribution
        delta = self._subset.size * ((100. - percent) / 200.)
        # get F(x): cumulative sum
        cumsum = numpy.zeros(len(self._hist_hires) + 1, dtype=int)
        cumsum[1:] = numpy.cumsum(self._hist_hires)
        bins = numpy.zeros(len(self._hist_hires) + 1, dtype=float)
        bins[0] = self._subset_range[0]
        bins[1:] = self._hist_bins_hires + self._hist_binsize_hires / 2
        # use interpolation to find value interval corresponding to [delta,100-delta] of the distribution
        dprint(2, self._subset.size, delta, self._subset.size - delta)
        dprint(2, cumsum, self._hist_bins_hires)
        # if first bin is already > delta, then set colour range to first bin
        x0, x1 = numpy.interp([delta, self._subset.size - delta], cumsum, bins)
        # and change the display range (this will also cause a histplot.replot() via _updateDisplayRange above)
        self._rc.setDisplayRange(x0, x1)

    def _setZeroLeftLimit(self):
        self._rc.setDisplayRange(0., self._rc.displayRange()[1])

    def _selectLowLimit(self, pos):
        self._rc.setDisplayRange(pos.x(), self._rc.displayRange()[1])

    def _selectHighLimit(self, pos):
        self._rc.setDisplayRange(self._rc.displayRange()[0], pos.x())

    def _unzoomHistogram(self):
        self._updateHistogram()
        self._histplot.replot()

    def _zoomHistogramByFactor(self, factor):
        """Changes histogram limits by specified factor"""
        # get max distance of plot limit from peak
        dprint(1, "zooming histogram by", factor)
        halfdist = (self._hist_range[1] - self._hist_range[0]) / (factor * 2)
        self._updateHistogram(self._hist_peak - halfdist, self._hist_peak + halfdist)
        self._histplot.replot()

    def _zoomHistogramIntoRect(self, rect):
        hmin, hmax = rect.bottomLeft().x(), rect.bottomRight().x()
        if hmax > hmin:
            self._updateHistogram(rect.bottomLeft().x(), rect.bottomRight().x())
            self._histplot.replot()

    def _zoomHistogramPreview(self, value):
        dprint(2, "wheel moved to", value)
        self._zoomHistogramFinalize(value, preview=True)
        self._whistzoom_timer.start()

    def _zoomHistogramFinalize(self, value=None, preview=False):
        if self._zooming_histogram:
            return
        self._zooming_histogram = True
        try:
            if value is not None:
                dmin, dmax = self._subset_range
                dist = max(dmax - self._hist_peak, self._hist_peak - dmin) / 10 ** value
                self._preview_hist_range = max(self._hist_peak - dist, dmin), min(self._hist_peak + dist, dmax)
            if preview:
                self._histplot.setAxisScale(QwtPlot.xBottom, *self._preview_hist_range)
            else:
                dprint(2, "wheel finalized at", value)
                self._whistzoom_timer.stop()
                self._updateHistogram(*self._preview_hist_range)
            self._histplot.replot()
        finally:
            self._zooming_histogram = False

    def _setHistLogScale(self, logscale, replot=True):
        self._ylogscale = logscale
        if logscale:
            self._histplot.setAxisScaleEngine(QwtPlot.yLeft, QwtLog10ScaleEngine())
            ymax = max(1, self._hist_max)
            self._histplot.setAxisScale(QwtPlot.yLeft, 1, 10 ** (math.log10(ymax) * (1 + self.ColorBarHeight)))
            y = self._hist.copy()
            y[y == 0] = 1
            self._histcurve1.setData(self._hist_bins, y)
            self._histcurve2.setData(self._hist_bins, y)
        else:
            self._histplot.setAxisScaleEngine(QwtPlot.yLeft, QwtLinearScaleEngine())
            self._histplot.setAxisScale(QwtPlot.yLeft, 0, self._hist_max * (1 + self.ColorBarHeight))
            self._histcurve1.setData(self._hist_bins, self._hist)
            self._histcurve2.setData(self._hist_bins, self._hist)
        if replot:
            self._histplot.replot()
Пример #7
0
class ImageManager(QWidget):
    """An ImageManager manages a stack of images (and associated ImageControllers)"""
    def __init__(self, *args):
        QWidget.__init__(self, *args)
        # init layout
        self._lo = QVBoxLayout(self)
        self._lo.setContentsMargins(0, 0, 0, 0)
        self._lo.setSpacing(0)
        # init internal state
        self._currier = PersistentCurrier()
        self._z0 = 0
        # z-depth of first image, the rest count down from it
        self._updating_imap = False
        self._locked_display_range = False
        self._imagecons = []
        self._imagecon_loadorder = []
        self._center_image = None
        self._plot = None
        self._border_pen = None
        self._drawing_key = None
        self._load_image_dialog = None
        self._model_imagecons = set()
        # init menu and standard actions
        self._menu = QMenu("&Image", self)
        qag = QActionGroup(self)
        # exclusive controls for plotting topmost or all images
        self._qa_plot_top = qag.addAction("Display topmost image only")
        self._qa_plot_all = qag.addAction("Display all images")
        self._qa_plot_top.setCheckable(True)
        self._qa_plot_all.setCheckable(True)
        self._qa_plot_top.setChecked(True)
        QObject.connect(self._qa_plot_all, SIGNAL("toggled(bool)"),
                        self._displayAllImages)
        self._closing = False

        self._qa_load_clipboard = None
        self._clipboard_mode = QClipboard.Clipboard
        QObject.connect(QApplication.clipboard(),
                        SIGNAL("changed(QClipboard::Mode)"),
                        self._checkClipboardPath)
        # populate the menu
        self._repopulateMenu()

    def close(self):
        dprint(1, "closing Manager")
        self._closing = True
        for ic in self._imagecons:
            ic.close()

    def loadImage(self,
                  filename=None,
                  duplicate=True,
                  to_top=True,
                  model=None):
        """Loads image. Returns ImageControlBar object.
        If image is already loaded: returns old ICB if duplicate=False (raises to top if to_top=True),
        or else makes a new control bar.
        If model is set to a source name, marks the image as associated with a model source. These can be unloaded en masse by calling
        unloadModelImages().
        """
        if filename is None:
            if not self._load_image_dialog:
                dialog = self._load_image_dialog = QFileDialog(
                    self, "Load FITS image", ".",
                    "FITS images (%s);;All files (*)" %
                    (" ".join(["*" + ext for ext in FITS_ExtensionList])))
                dialog.setFileMode(QFileDialog.ExistingFile)
                dialog.setModal(True)
                QObject.connect(dialog,
                                SIGNAL("filesSelected(const QStringList &)"),
                                self.loadImage)
            self._load_image_dialog.exec_()
            return None
        if isinstance(filename, QStringList):
            filename = filename[0]
        filename = str(filename)
        # report error if image does not exist
        if not os.path.exists(filename):
            self.showErrorMessage("""FITS image %s does not exist.""" %
                                  filename)
            return None
        # see if image is already loaded
        if not duplicate:
            for ic in self._imagecons:
                if ic.getFilename() and os.path.samefile(
                        filename, ic.getFilename()):
                    if to_top:
                        self.raiseImage(ic)
                    if model:
                        self._model_imagecons.add(id(ic))
                    return ic
        # load the FITS image
        busy = BusyIndicator()
        dprint(2, "reading FITS image", filename)
        self.showMessage("""Reading FITS image %s""" % filename, 3000)
        QApplication.flush()
        try:
            image = SkyImage.FITSImagePlotItem(str(filename))
        except KeyboardInterrupt:
            raise
        except:
            busy = None
            traceback.print_exc()
            self.showErrorMessage(
                """<P>Error loading FITS image %s: %s. This may be due to a bug in Tigger; if the FITS file loads fine in another viewer,
          please send the FITS file, along with a copy of any error messages from the text console, to [email protected].</P>"""
                % (filename, str(sys.exc_info()[1])))
            return None
        # create control bar, add to widget stack
        ic = self._createImageController(image,
                                         "model source '%s'" %
                                         model if model else filename,
                                         model or image.name,
                                         model=model)
        self.showMessage("""Loaded FITS image %s""" % filename, 3000)
        dprint(2, "image loaded")
        return ic

    def showMessage(self, message, time=None):
        self.emit(SIGNAL("showMessage"), message, time)

    def showErrorMessage(self, message, time=None):
        self.emit(SIGNAL("showErrorMessage"), message, time)

    def setZ0(self, z0):
        self._z0 = z0
        if self._imagecons:
            self.raiseImage(self._imagecons[0])

    def enableImageBorders(self, border_pen, label_color, label_bg_brush):
        self._border_pen, self._label_color, self._label_bg_brush = \
            border_pen, label_color, label_bg_brush

    def lockAllDisplayRanges(self, rc0):
        """Locks all display ranges, and sets the intensity from rc0"""
        if not self._updating_imap:
            self._updating_imap = True
            rc0.lockDisplayRange()
            try:
                for ic in self._imagecons:
                    rc1 = ic.renderControl()
                    if rc1 is not rc0:
                        rc1.setDisplayRange(*rc0.displayRange())
                        rc1.lockDisplayRange()
            finally:
                self._updating_imap = False

    def unlockAllDisplayRanges(self):
        """Unlocks all display range."""
        for ic in self._imagecons:
            ic.renderControl().lockDisplayRange(False)

    def _lockDisplayRange(self, rc0, lock):
        """Locks or unlocks the display range of a specific controller."""
        if lock and not self._updating_imap:
            self._updating_imap = True
            try:
                # if something is already locked, copy display range from it
                for ic in self._imagecons:
                    rc1 = ic.renderControl()
                    if rc1 is not rc0 and rc1.isDisplayRangeLocked():
                        rc0.setDisplayRange(*rc1.displayRange())
            finally:
                self._updating_imap = False

    def _updateDisplayRange(self, rc, dmin, dmax):
        """This is called whenever one of the images (or rather, its associated RenderControl object) changes its display range."""
        if not rc.isDisplayRangeLocked():
            return
        # If the display range is locked, propagate it to all images.
        # but don't do it if we're already propagating (otherwise we may get called in an infinte loop)
        if not self._updating_imap:
            self._updating_imap = True
            try:
                for ic in self._imagecons:
                    rc1 = ic.renderControl()
                    if rc1 is not rc and rc1.isDisplayRangeLocked():
                        rc1.setDisplayRange(dmin, dmax)
            finally:
                self._updating_imap = False

    def getImages(self):
        return [ic.image for ic in self._imagecons]

    def getTopImage(self):
        return (self._imagecons or None) and self._imagecons[0].image

    def cycleImages(self):
        index = self._imagecon_loadorder.index(self._imagecons[0])
        index = (index + 1) % len(self._imagecon_loadorder)
        self.raiseImage(self._imagecon_loadorder[index])

    def blinkImages(self):
        if len(self._imagecons) > 1:
            self.raiseImage(self._imagecons[1])

    def incrementSlice(self, extra_axis, incr):
        if self._imagecons:
            rc = self._imagecons[0].renderControl()
            sliced_axes = rc.slicedAxes()
            if extra_axis < len(sliced_axes):
                rc.incrementSlice(sliced_axes[extra_axis][0], incr)

    def setLMRectSubset(self, rect):
        if self._imagecons:
            self._imagecons[0].setLMRectSubset(rect)

    def getLMRectStats(self, rect):
        if self._imagecons:
            return self._imagecons[0].renderControl().getLMRectStats(rect)

    def unloadModelImages(self):
        """Unloads images associated with model (i.e. loaded with the model=True flag)"""
        for ic in [
                ic for ic in self._imagecons if id(ic) in self._model_imagecons
        ]:
            self.unloadImage(ic)

    def unloadImage(self, imagecon):
        """Unloads the given imagecon object."""
        if imagecon not in self._imagecons:
            return
        # recenter if needed
        self._imagecons.remove(imagecon)
        self._imagecon_loadorder.remove(imagecon)
        self._model_imagecons.discard(id(imagecon))
        # reparent widget and release it
        imagecon.setParent(None)
        imagecon.close()
        # recenter image, if unloaded the center image
        if self._center_image is imagecon.image:
            self.centerImage(self._imagecons[0] if self._imagecons else None,
                             emit=False)
        # emit signal
        self._repopulateMenu()
        self.emit(SIGNAL("imagesChanged"))
        if self._imagecons:
            self.raiseImage(self._imagecons[0])

    def getCenterImage(self):
        return self._center_image

    def centerImage(self, imagecon, emit=True):
        self._center_image = imagecon and imagecon.image
        for ic in self._imagecons:
            ic.setPlotProjection(self._center_image.projection)
        if emit:
            self.emit(SIGNAL("imagesChanged"))

    def raiseImage(self, imagecon):
        # reshuffle image stack, if more than one image image
        if len(self._imagecons) > 1:
            busy = BusyIndicator()
            # reshuffle image stack
            self._imagecons.remove(imagecon)
            self._imagecons.insert(0, imagecon)
            # notify imagecons
            for i, ic in enumerate(self._imagecons):
                label = "%d" % (i + 1) if i else "<B>1</B>"
                ic.setZ(self._z0 - i * 10,
                        top=not i,
                        depthlabel=label,
                        can_raise=True)
            # adjust visibility
            for j, ic in enumerate(self._imagecons):
                ic.setImageVisible(not j
                                   or bool(self._qa_plot_all.isChecked()))
            # issue replot signal
            self.emit(SIGNAL("imageRaised"))
            self.fastReplot()
        # else simply update labels
        else:
            self._imagecons[0].setZ(self._z0,
                                    top=True,
                                    depthlabel=None,
                                    can_raise=False)
            self._imagecons[0].setImageVisible(True)
        # update slice menus
        img = imagecon.image
        axes = imagecon.renderControl().slicedAxes()
        for i, (next, prev) in enumerate(self._qa_slices):
            next.setVisible(False)
            prev.setVisible(False)
            if i < len(axes):
                iaxis, name, labels = axes[i]
                next.setVisible(True)
                prev.setVisible(True)
                next.setText("Show next slice along %s axis" % name)
                prev.setText("Show previous slice along %s axis" % name)
        # emit signasl
        self.emit(SIGNAL("imageRaised"), img)

    def resetDrawKey(self):
        """Makes and sets the current plot's drawing key"""
        if self._plot:
            key = []
            for ic in self._imagecons:
                key.append(id(ic))
                key += ic.currentSlice()
                self._plot.setDrawingKey(tuple(key))

    def fastReplot(self, *dum):
        """Fast replot -- called when flipping images or slices. Uses the plot cache, if possible."""
        if self._plot:
            self.resetDrawKey()
            dprint(2, "calling replot", time.time() % 60)
            self._plot.replot()
            dprint(2, "replot done", time.time() % 60)

    def replot(self, *dum):
        """Proper replot -- called when an image needs to be properly redrawn. Cleares the plot's drawing cache."""
        if self._plot:
            self._plot.clearDrawCache()
            self.resetDrawKey()
            self._plot.replot()

    def attachImagesToPlot(self, plot):
        self._plot = plot
        self.resetDrawKey()
        for ic in self._imagecons:
            ic.attachToPlot(plot)

    def getMenu(self):
        return self._menu

    def _displayAllImages(self, enabled):
        busy = BusyIndicator()
        if enabled:
            for ic in self._imagecons:
                ic.setImageVisible(True)
        else:
            self._imagecons[0].setImageVisible(True)
            for ic in self._imagecons[1:]:
                ic.setImageVisible(False)
        self.replot()

    def _checkClipboardPath(self, mode=QClipboard.Clipboard):
        if self._qa_load_clipboard:
            self._clipboard_mode = mode
            try:
                path = str(QApplication.clipboard().text(mode))
            except:
                path = None
            self._qa_load_clipboard.setEnabled(
                bool(path and os.path.isfile(path)))

    def _loadClipboardPath(self):
        try:
            path = QApplication.clipboard().text(self._clipboard_mode)
        except:
            return
        self.loadImage(path)

    def _repopulateMenu(self):
        self._menu.clear()
        self._menu.addAction("&Load image...", self.loadImage,
                             Qt.CTRL + Qt.Key_L)
        self._menu.addAction("&Compute image...", self.computeImage,
                             Qt.CTRL + Qt.Key_M)
        self._qa_load_clipboard = self._menu.addAction(
            "Load from clipboard &path", self._loadClipboardPath,
            Qt.CTRL + Qt.Key_P)
        self._checkClipboardPath()
        if self._imagecons:
            self._menu.addSeparator()
            # add controls to cycle images and planes
            for i, imgcon in enumerate(self._imagecons[::-1]):
                self._menu.addMenu(imgcon.getMenu())
            self._menu.addSeparator()
            if len(self._imagecons) > 1:
                self._menu.addAction("Cycle images", self.cycleImages,
                                     Qt.Key_F5)
                self._menu.addAction("Blink images", self.blinkImages,
                                     Qt.Key_F6)
            self._qa_slices = ((self._menu.addAction(
                "Next slice along axis 1",
                self._currier.curry(self.incrementSlice, 0, 1), Qt.Key_F7),
                                self._menu.addAction(
                                    "Previous slice along axis 1",
                                    self._currier.curry(
                                        self.incrementSlice, 0, -1),
                                    Qt.SHIFT + Qt.Key_F7)),
                               (self._menu.addAction(
                                   "Next slice along axis 2",
                                   self._currier.curry(self.incrementSlice, 1,
                                                       1), Qt.Key_F8),
                                self._menu.addAction(
                                    "Previous slice along axis 2",
                                    self._currier.curry(
                                        self.incrementSlice, 1, -1),
                                    Qt.SHIFT + Qt.Key_F8)))
            self._menu.addSeparator()
            self._menu.addAction(self._qa_plot_top)
            self._menu.addAction(self._qa_plot_all)

    def computeImage(self, expression=None):
        """Computes image from expression (if expression is None, pops up dialog)"""
        if expression is None:
            (expression, ok) = QInputDialog.getText(
                self, "Compute image", """Enter an image expression to compute.
                                              Any valid numpy expression is supported, and
                                              all functions from the numpy module are available (including sub-modules such as fft).
                                              Use 'a', 'b', 'c' to refer to images.
                                              Examples:  "(a+b)/2", "cos(a)+sin(b)", "a-a.mean()", "fft.fft2(a)", etc."""
            )
            #      (expression,ok) = QInputDialog.getText(self,"Compute image","""<P>Enter an expression to compute.
            #        Use 'a', 'b', etc. to refer to loaded images. Any valid numpy expression is supported, and all the
            #       functions from the numpy module are available. Examples of valid expressions include "(a+b)/2",
            #       "cos(a)+sin(b)", "a-a.mean()", etc.
            #        </P>
            #      """)
            expression = str(expression)
            if not ok or not expression:
                return
        # try to parse expression
        arglist = [(chr(ord('a') + ic.getNumber()), ic.image)
                   for ic in self._imagecons]
        try:
            exprfunc = eval(
                "lambda " + (",".join([x[0]
                                       for x in arglist])) + ":" + expression,
                numpy.__dict__, {})
        except Exception as exc:
            self.showErrorMessage("""Error parsing expression "%s": %s.""" %
                                  (expression, str(exc)))
            return None
        # try to evaluate expression
        self.showMessage("Computing expression \"%s\"" % expression, 10000)
        busy = BusyIndicator()
        QApplication.flush()

        # trim trivial trailing dimensions. This avoids the problem of when an NxMx1 and an NxMx1x1 arrays are added,
        # the result is promoted to NxMxMx1 following the numpy rules.
        def trimshape(shape):
            out = shape
            while out and out[-1] == 1:
                out = out[:-1]
            return out

        def trimarray(array):
            return array.reshape(trimshape(array.shape))

        try:
            result = exprfunc(*[trimarray(x[1].data()) for x in arglist])
        except Exception as exc:
            busy = None
            traceback.print_exc()
            self.showErrorMessage("""Error evaluating "%s": %s.""" %
                                  (expression, str(exc)))
            return None
        busy = None
        if type(result) != numpy.ma.masked_array and type(
                result) != numpy.ndarray:
            self.showErrorMessage(
                """Result of "%s" is of invalid type "%s" (array expected)."""
                % (expression, type(result).__name__))
            return None
        # convert coomplex results to real
        if numpy.iscomplexobj(result):
            self.showErrorMessage(
                """Result of "%s" is complex. Complex images are currently
      not fully supported, so we'll implicitly use the absolute value instead."""
                % (expression))
            expression = "abs(%s)" % expression
            result = abs(result)
        # determine which image this expression can be associated with
        res_shape = trimshape(result.shape)
        arglist = [
            x for x in arglist if hasattr(x[1], 'fits_header')
            and trimshape(x[1].data().shape) == res_shape
        ]
        if not arglist:
            self.showErrorMessage(
                """Result of "%s" has shape %s, which does not match any loaded FITS image."""
                % (expression, "x".join(map(str, result.shape))))
            return None
        # look for an image in the arglist with the same projection, and with a valid dirname
        # (for the where-to-save hint)
        template = arglist[0][1]
        # if all images in arglist have the same projection, then it doesn't matter what we use
        # else ask
        if len(
            [x for x in arglist[1:] if x[1].projection == template.projection
             ]) != len(arglist) - 1:
            options = [x[0] for x in arglist]
            (which, ok) = QInputDialog.getItem(
                self, "Compute image",
                "Coordinate system to use for the result of \"%s\":" %
                expression, options, 0, False)
            if not ok:
                return None
            try:
                template = arglist[options.index(which)][1]
            except:
                pass
        # create a FITS image
        busy = BusyIndicator()
        dprint(2, "creating FITS image", expression)
        self.showMessage("""Creating image for %s""" % expression, 3000)
        QApplication.flush()
        try:
            hdu = pyfits.PrimaryHDU(result.transpose(), template.fits_header)
            skyimage = SkyImage.FITSImagePlotItem(name=expression,
                                                  filename=None,
                                                  hdu=hdu)
        except:
            busy = None
            traceback.print_exc()
            self.showErrorMessage("""Error creating FITS image %s: %s""" %
                                  (expression, str(sys.exc_info()[1])))
            return None
        # get directory name for save-to hint
        dirname = getattr(template, 'filename', None)
        if not dirname:
            dirnames = [
                getattr(img, 'filename') for x, img in arglist
                if hasattr(img, 'filename')
            ]
            dirname = dirnames[0] if dirnames else None
        # create control bar, add to widget stack
        self._createImageController(
            skyimage,
            expression,
            expression,
            save=((dirname and os.path.dirname(dirname)) or "."))
        self.showMessage("Created new image for %s" % expression, 3000)
        dprint(2, "image created")

    def _createImageController(self,
                               image,
                               name,
                               basename,
                               model=False,
                               save=False):
        dprint(2, "creating ImageController for", name)
        ic = ImageController(image, self, self, name, save=save)
        ic.setNumber(len(self._imagecons))
        self._imagecons.insert(0, ic)
        self._imagecon_loadorder.append(ic)
        if model:
            self._model_imagecons.add(id(ic))
        self._lo.addWidget(ic)
        if self._border_pen:
            ic.addPlotBorder(self._border_pen, basename, self._label_color,
                             self._label_bg_brush)
        # attach appropriate signals
        image.connect(SIGNAL("slice"), self.fastReplot)
        image.connect(SIGNAL("repaint"), self.replot)
        image.connect(SIGNAL("raise"),
                      self._currier.curry(self.raiseImage, ic))
        image.connect(SIGNAL("unload"),
                      self._currier.curry(self.unloadImage, ic))
        image.connect(SIGNAL("center"),
                      self._currier.curry(self.centerImage, ic))
        QObject.connect(
            ic.renderControl(), SIGNAL("displayRangeChanged"),
            self._currier.curry(self._updateDisplayRange, ic.renderControl()))
        QObject.connect(
            ic.renderControl(), SIGNAL("displayRangeLocked"),
            self._currier.curry(self._lockDisplayRange, ic.renderControl()))
        self._plot = None
        # add to menus
        dprint(2, "repopulating menus")
        self._repopulateMenu()
        # center and raise to top of stack
        self.raiseImage(ic)
        if not self._center_image:
            self.centerImage(ic, emit=False)
        else:
            ic.setPlotProjection(self._center_image.projection)
        # signal
        self.emit(SIGNAL("imagesChanged"))
        return ic
Пример #8
0
class SkyModelTreeWidget(TigGUI.kitties.widgets.ClickableTreeWidget):
    """This implements a QTreeWidget for sky models"""

    def __init__(self, *args):
        TigGUI.kitties.widgets.ClickableTreeWidget.__init__(self, *args)
        self._currier = PersistentCurrier()
        self.model = None
        # insert columns
        self.setHeaderLabels(ViewColumns)
        self.headerItem().setText(ColumnIapp, "I(app)")
        self.header().setMovable(False)
        self.header().setClickable(True)
        self.setSortingEnabled(True)
        self.setRootIsDecorated(False)
        self.setEditTriggers(QAbstractItemView.AllEditTriggers)
        self.setMouseTracking(True)
        # set column width modes
        for icol in range(NumColumns - 1):
            self.header().setResizeMode(icol, QHeaderView.ResizeToContents)
        self.header().setStretchLastSection(True)
        ## self.setTextAlignment(ColumnR,Qt.AlignRight)
        ## self.setTextAlignment(ColumnType,Qt.AlignHCenter)
        # _column_enabled[i] is True if column is available in the model.
        # _column_show[i] is True if column is currently being shown (via a view control)
        self._column_enabled = [True] * NumColumns
        self._column_shown = [True] * NumColumns
        # other listview init
        self.header().show()
        self.setSelectionMode(QTreeWidget.ExtendedSelection)
        self.setAllColumnsShowFocus(True)
        ## self.setShowToolTips(True)
        self._updating_selection = False
        self.setRootIsDecorated(False)
        # connect signals to track selected sources
        QObject.connect(self, SIGNAL("itemSelectionChanged()"), self._selectionChanged)
        QObject.connect(self, SIGNAL("itemEntered(QTreeWidgetItem*,int)"), self._itemHighlighted)
        # add "View" controls for different column categories
        self._column_views = []
        self._column_widths = {}
        self.addColumnCategory("Position", [ColumnRa, ColumnDec])
        self.addColumnCategory("Position errors", [ColumnRa_err, ColumnDec_err], False)
        self.addColumnCategory("Type", [ColumnType])
        self.addColumnCategory("Flux", [ColumnIapp, ColumnI])
        self.addColumnCategory("Flux errors", [ColumnI_err], False)
        self.addColumnCategory("Polarization", [ColumnQ, ColumnU, ColumnV, ColumnRm])
        self.addColumnCategory("Polarization errors", [ColumnQ_err, ColumnU_err, ColumnV_err, ColumnRm_err], False)
        self.addColumnCategory("Spectrum", [ColumnSpi])
        self.addColumnCategory("Spectrum errors", [ColumnSpi_err], False)
        self.addColumnCategory("Shape", [ColumnShape])
        self.addColumnCategory("Shape errors", [ColumnShape_err], False)
        self.addColumnCategory("Tags", [ColumnTags])

    def _showColumn(self, col, show=True):
        """Shows or hides the specified column.
        (When hiding, saves width of column to internal array so that it can be restored properly.)"""
        hdr = self.header()
        hdr.setSectionHidden(col, not show)
        if show:
            if not hdr.sectionSize(col):
                hdr.resizeSection(col, self._column_widths[col])
                hdr.setResizeMode(col, QHeaderView.ResizeToContents)
        else:
            if hdr.sectionSize(col):
                self._column_widths[col] = hdr.sectionSize(col)

    def _enableColumn(self, column, enable=True):
        busy = BusyIndicator()
        self._column_enabled[column] = enable
        self._showColumn(column, enable and self._column_shown[column])

    def _showColumnCategory(self, columns, show):
        busy = BusyIndicator()
        for col in columns:
            self._column_shown[col] = show
            self._showColumn(col, self._column_enabled[col] and show)

    def _selectionChanged(self):
        if self._updating_selection:
            return
        for item in self.iterator():
            item._src.select(item.isSelected())
        self.model.emitSelection(origin=self)

    def _itemHighlighted(self, item, col):
        dprint(3, "highlighting", item._src.name)
        self.model.setCurrentSource(item._src, origin=self)

    def viewportEvent(self, event):
        if event.type() in (QEvent.Leave, QEvent.FocusOut) and self.model:
            self.model.setCurrentSource(None, origin=self)
        return QTreeWidget.viewportEvent(self, event)

    def addColumnCategory(self, name, columns, visible=True):
        qa = QAction(name, self)
        qa.setCheckable(True)
        qa.setChecked(visible)
        if not visible:
            self._showColumnCategory(columns, False)
        QObject.connect(qa, SIGNAL("toggled(bool)"), self._currier.curry(self._showColumnCategory, columns))
        self._column_views.append((name, qa, columns))

    def clear(self):
        TigGUI.kitties.widgets.ClickableTreeWidget.clear(self)
        self.model = None
        self._itemdict = {}

    def setModel(self, model):
        self.model = model
        self._refreshModel(SkyModel.UpdateAll)
        self.model.connect("changeCurrentSource", self._updateCurrentSource)
        self.model.connect("changeGroupingVisibility", self.changeGroupingVisibility)
        self.model.connect("selected", self._updateModelSelection)
        self.model.connect("updated", self._refreshModel)

    def _refreshModel(self, what=SkyModel.UpdateAll, origin=None):
        if origin is self or not what & (SkyModel.UpdateSourceList | SkyModel.UpdateSourceContent):
            return
        # if only selection was changed, take shortcut
        if what & SkyModel.UpdateSelectionOnly:
            dprint(2, "model update -- selection only")
            return self._refreshSelectedItems(origin)
        busy = BusyIndicator()
        # else repopulate widget completely
        dprint(2, "model update -- complete")
        TigGUI.kitties.widgets.ClickableTreeWidget.clear(self)
        dprint(2, "creating model items")
        items = [SkyModelTreeWidgetItem(src) for src in self.model.sources]
        self._itemdict = dict(list(zip([src.name for src in self.model.sources], items)))
        dprint(2, "adding to tree widget")
        self.addTopLevelItems(items)
        self.header().updateGeometry()
        # show/hide columns based on tag availability
        self._enableColumn(ColumnIapp, 'Iapp' in self.model.tagnames)
        self._enableColumn(ColumnR, 'r' in self.model.tagnames)
        dprint(2, "re-sorting")
        self.sortItems(('Iapp' in self.model.tagnames and ColumnIapp) or ColumnI, Qt.DescendingOrder)
        busy = None

    def addColumnViewActionsTo(self, menu):
        for name, qa, columns in self._column_views:
            menu.addAction(qa)

    def _updateCurrentSource(self, src, src0=None, origin=None):
        # if origin is self:
        # return
        # dehighlight old item
        item = src0 and self._itemdict.get(src0.name)
        if item:
            item.setHighlighted(False)
        # scroll to new item, if found
        item = src and self._itemdict.get(src.name)
        if item:
            item.setHighlighted(True, origin is not self)
            if origin is not self:
                self.scrollToItem(item)

    def _updateModelSelection(self, nsel, origin=None):
        """This is called when some other widget (origin!=self) changes the set of selected model sources"""
        if origin is self:
            return
        self._updating_selection = True
        ## this is very slow because of setSelected()
        #    for item in self.iterator():
        #     item.setSelected(item._src.selected)
        selection = QItemSelection()
        for item in self.iterator():
            if item._src.selected:
                selection.append(
                    QItemSelectionRange(self.indexFromItem(item, 0), self.indexFromItem(item, self.columnCount() - 1)))
        self.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect)
        self.changeGroupingVisibility(None, origin=origin)
        self._updating_selection = False

    def _refreshSelectedItems(self, origin=None):
        busy = BusyIndicator()
        dprint(3, "refreshing selected items")
        for item in self.iterator():
            if item.isSelected():
                dprint(4, "resetting item", item._src.name)
                item.setSource(item._src)
        dprint(3, "refreshing selected items done")
        busy = None

    def changeGroupingVisibility(self, group, origin=None):
        if origin is self:
            return
        for item in self.iterator():
            # collect show_list values from groupings to which this source belongs (default group excepted)
            show = [group.style.show_list for group in self.model.groupings if
                    group is not self.model.defgroup and group.func(item._src)]
            # if at least one group is showing explicitly, show
            # else if at least one group is hiding explicitly, hide
            # else use default setting
            if max(show) == PlotStyles.ShowAlways:
                visible = True
            elif min(show) == PlotStyles.ShowNot:
                visible = False
            else:
                visible = bool(self.model.defgroup.style.show_list)
            # set visibility accordingly
            item.setHidden(not visible)

    TagsWithOwnColumn = set(["Iapp", "r"])
Пример #9
0
class ModelGroupsTable(QWidget):
    EditableAttrs = [attr for attr in PlotStyles.StyleAttributes if attr in PlotStyles.StyleAttributeOptions]
    ColList = 3
    ColPlot = 4
    ColApply = 5
    AttrByCol = dict([(i + 6, attr) for i, attr in enumerate(EditableAttrs)])

    def __init__(self, parent, *args):
        QWidget.__init__(self, parent, *args)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        lo = QVBoxLayout(self)
        lo.setContentsMargins(0, 0, 0, 0)
        lo1 = QHBoxLayout()
        lo.addLayout(lo1)
        lo1.setContentsMargins(0, 0, 0, 0)
        lbl = QLabel(QString("<nobr><b>Source groupings:</b></nobr>"), self)
        lo1.addWidget(lbl, 0)
        lo1.addStretch(1)
        # add show/hide button
        self._showattrbtn = QPushButton(self)
        self._showattrbtn.setMinimumWidth(256)
        lo1.addWidget(self._showattrbtn, 0)
        lo1.addStretch()
        QObject.connect(self._showattrbtn, SIGNAL("clicked()"), self._togglePlotControlsVisibility)
        # add table
        self.table = QTableWidget(self)
        lo.addWidget(self.table)
        QObject.connect(self.table, SIGNAL("cellChanged(int,int)"), self._valueChanged)
        self.table.setSelectionMode(QTableWidget.NoSelection)
        # setup basic columns
        self.table.setColumnCount(6 + len(self.EditableAttrs))
        for i, label in enumerate(("grouping", "total", "selection", "list", "plot", "style")):
            self.table.setHorizontalHeaderItem(i, QTableWidgetItem(label))
        self.table.horizontalHeader().setSectionHidden(self.ColApply, True)
        # setup columns for editable grouping attributes
        for i, attr in self.AttrByCol.items():
            self.table.setHorizontalHeaderItem(i, QTableWidgetItem(PlotStyles.StyleAttributeLabels[attr]))
            self.table.horizontalHeader().setSectionHidden(i, True)
        self.table.verticalHeader().hide()
        # other internal init
        self._attrs_shown = False
        self._togglePlotControlsVisibility()
        self.model = None
        self._setting_model = False
        self._currier = PersistentCurrier()
        # row of 'selected' grouping
        self._irow_selgroup = 0

    def clear(self):
        self.table.setRowCount(0)
        self.model = None

    # setup mappings from the group.show_plot attribute to check state
    ShowAttrToCheckState = {PlotStyles.ShowNot: Qt.Unchecked,
                            PlotStyles.ShowDefault: Qt.PartiallyChecked,
                            PlotStyles.ShowAlways: Qt.Checked}
    CheckStateToShowAttr = dict([(val, key) for key, val in ShowAttrToCheckState.items()])

    def _makeCheckItem(self, name, group, attr):
        item = QTableWidgetItem(name)
        if group is self.model.defgroup:
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Checked if getattr(group.style, attr) else Qt.Unchecked)
        else:
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsTristate)
            item.setCheckState(self.ShowAttrToCheckState[getattr(group.style, attr)])
        return item

    def _updateModel(self, what=SkyModel.UpdateAll, origin=None):
        if origin is self or not what & (SkyModel.UpdateTags | SkyModel.UpdateGroupStyle):
            return
        model = self.model
        self._setting_model = True;  # to ignore cellChanged() signals (in valueChanged())
        # _item_cb is a dict (with row,col keys) containing the widgets (CheckBoxes ComboBoxes) per each cell
        self._item_cb = {}
        # lists of "list" and "plot" checkboxes per each grouping (excepting the default grouping); each entry is an (row,col,item) tuple.
        # used as argument to self._showControls()
        self._list_controls = []
        self._plot_controls = []
        # list of selection callbacks (to which signals are connected)
        self._callbacks = []
        # set requisite number of rows,and start filling
        self.table.setRowCount(len(model.groupings))
        for irow, group in enumerate(model.groupings):
            self.table.setItem(irow, 0, QTableWidgetItem(group.name))
            if group is model.selgroup:
                self._irow_selgroup = irow
            # total # source in group: skip for "current"
            if group is not model.curgroup:
                self.table.setItem(irow, 1, QTableWidgetItem(str(group.total)))
            # selection controls: skip for current and selection
            if group not in (model.curgroup, model.selgroup):
                btns = QWidget()
                lo = QHBoxLayout(btns)
                lo.setContentsMargins(0, 0, 0, 0)
                lo.setSpacing(0)
                # make selector buttons (depending on which group we're in)
                if group is model.defgroup:
                    Buttons = (
                        ("+", lambda src, grp=group: True, "select all sources"),
                        ("-", lambda src, grp=group: False, "unselect all sources"))
                else:
                    Buttons = (
                        ("=", lambda src, grp=group: grp.func(src), "select only this grouping"),
                        ("+", lambda src, grp=group: src.selected or grp.func(src), "add grouping to selection"),
                        ("-", lambda src, grp=group: src.selected and not grp.func(src),
                         "remove grouping from selection"),
                        ("&&", lambda src, grp=group: src.selected and grp.func(src),
                         "intersect selection with grouping"))
                lo.addStretch(1)
                for label, predicate, tooltip in Buttons:
                    btn = QToolButton(btns)
                    btn.setText(label)
                    btn.setMinimumWidth(24)
                    btn.setMaximumWidth(24)
                    btn.setToolTip(tooltip)
                    lo.addWidget(btn)
                    # add callback
                    QObject.connect(btn, SIGNAL("clicked()"), self._currier.curry(self.selectSources, predicate))
                lo.addStretch(1)
                self.table.setCellWidget(irow, 2, btns)
            # "list" checkbox (not for current and selected groupings: these are always listed)
            if group not in (model.curgroup, model.selgroup):
                item = self._makeCheckItem("", group, "show_list")
                self.table.setItem(irow, self.ColList, item)
                item.setToolTip("""<P>If checked, sources in this grouping will be listed in the source table. If un-checked, sources will be
            excluded from the table. If partially checked, then the default list/no list setting of "all sources" will be in effect.
            </P>""")
            # "plot" checkbox (not for the current grouping, since that's always plotted)
            if group is not model.curgroup:
                item = self._makeCheckItem("", group, "show_plot")
                self.table.setItem(irow, self.ColPlot, item)
                item.setToolTip("""<P>If checked, sources in this grouping will be included in the plot. If un-checked, sources will be
            excluded from the plot. If partially checked, then the default plot/no plot setting of "all sources" will be in effect.
            </P>""")
            # custom style control
            # for default, current and selected, this is just a text label
            if group is model.defgroup:
                item = QTableWidgetItem("default:")
                item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
                item.setToolTip(
                    """<P>This is the default plot style used for all sources for which a custom grouping style is not selected.</P>""")
                self.table.setItem(irow, self.ColApply, item)
            elif group is model.curgroup:
                item = QTableWidgetItem("")
                item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
                item.setToolTip("""<P>This is the plot style used for the highlighted source, if any.</P>""")
                self.table.setItem(irow, self.ColApply, item)
            elif group is model.selgroup:
                item = QTableWidgetItem("")
                item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
                item.setToolTip("""<P>This is the plot style used for the currently selected sources.</P>""")
                self.table.setItem(irow, self.ColApply, item)
            # for the rest, a combobox with custom priorities
            else:
                cb = QComboBox()
                cb.addItems(["default"] + ["custom %d" % p for p in range(1, 10)])
                index = max(0, min(group.style.apply, 9))
                #        dprint(0,group.name,"apply",index)
                cb.setCurrentIndex(index)
                QObject.connect(cb, SIGNAL("activated(int)"),
                                self._currier.xcurry(self._valueChanged, (irow, self.ColApply)))
                self.table.setCellWidget(irow, self.ColApply, cb)
                cb.setToolTip("""<P>This controls whether sources within this group are plotted with a customized
            plot style. Customized styles have numeric priority; if a source belongs to multiple groups, then
            the style with the lowest priority takes precedence.<P>""")
            # attribute comboboxes
            for icol, attr in self.AttrByCol.items():
                # get list of options for this style attribute. If dealing with first grouping (i==0), which is
                # the "all sources" grouping, then remove the "default" option (which is always first in the list)
                options = PlotStyles.StyleAttributeOptions[attr]
                if irow == 0:
                    options = options[1:]
                # make combobox
                cb = QComboBox()
                cb.addItems(list(map(str, options)))
                # the "label" option is also editable
                if attr == "label":
                    cb.setEditable(True)
                try:
                    index = options.index(getattr(group.style, attr))
                    cb.setCurrentIndex(index)
                except ValueError:
                    cb.setEditText(str(getattr(group.style, attr)))
                slot = self._currier.xcurry(self._valueChanged, (irow, icol))
                QObject.connect(cb, SIGNAL("activated(int)"), slot)
                QObject.connect(cb, SIGNAL("editTextChanged(const QString &)"), slot)
                cb.setEnabled(group is model.defgroup or group.style.apply)
                self.table.setCellWidget(irow, icol, cb)
                label = attr
                if irow:
                    cb.setToolTip("""<P>This is the %s used to plot sources in this group, when a "custom" style for the group
          is enabled via the style control.<P>""" % label)
                else:
                    cb.setToolTip(
                        "<P>This is the default %s used for all sources for which a custom style is not specified below.<P>" % label)
        self.table.resizeColumnsToContents()
        # re-enable processing of cellChanged() signals
        self._setting_model = False

    def setModel(self, model):
        self.model = model
        self.model.connect("updated", self._updateModel)
        self.model.connect("selected", self.updateModelSelection)
        self._updateModel(SkyModel.UpdateAll)

    def _valueChanged(self, row, col):
        """Called when a cell has been edited"""
        if self._setting_model:
            return
        group = self.model.groupings[row]
        item = self.table.item(row, col)
        if col == self.ColList:
            if group is not self.model.defgroup:
                # tri-state items go from unchecked to checked when user clicks them. Make them partially checked instead.
                if group.style.show_list == PlotStyles.ShowNot and item.checkState() == Qt.Checked:
                    item.setCheckState(Qt.PartiallyChecked)
            group.style.show_list = self.CheckStateToShowAttr[item.checkState()]
            self.model.emitChangeGroupingVisibility(group, origin=self)
            return
        elif col == self.ColPlot:
            if group is not self.model.defgroup:
                # tri-state items go from unchecked to checked by default. Make them partially checked instead.
                if group.style.show_plot == PlotStyles.ShowNot and item.checkState() == Qt.Checked:
                    item.setCheckState(Qt.PartiallyChecked)
            group.style.show_plot = self.CheckStateToShowAttr[item.checkState()]
        elif col == self.ColApply:
            group.style.apply = self.table.cellWidget(row, col).currentIndex()
            # enable/disable editable cells
            for j in list(self.AttrByCol.keys()):
                item1 = self.table.item(row, j)
                if item1:
                    fl = item1.flags() & ~Qt.ItemIsEnabled
                    if group.style.apply:
                        fl |= Qt.ItemIsEnabled
                    item1.setFlags(fl)
                cw = self.table.cellWidget(row, j)
                cw and cw.setEnabled(group.style.apply)
        elif col in self.AttrByCol:
            cb = self.table.cellWidget(row, col)
            txt = str(cb.currentText())
            attr = self.AttrByCol[col]
            if txt == "default":
                setattr(group.style, attr, PlotStyles.DefaultValue)
            else:
                setattr(group.style, attr, PlotStyles.StyleAttributeTypes.get(attr, str)(txt))
        # all other columns: return so we don't emit a signal
        else:
            return
        # in all cases emit a signal
        self.model.emitChangeGroupingStyle(group, origin=self)

    def selectSources(self, predicate):
        """Selects sources according to predicate(src)"""
        busy = BusyIndicator()
        for src in self.model.sources:
            src.selected = predicate(src)
        self.model.emitSelection(origin=self)
        busy = None

    def updateModelSelection(self, nsel, origin=None):
        """This is called when some other widget changes the set of selected model sources"""
        self.table.clearSelection()
        if self.model:
            self.table.item(self._irow_selgroup, 1).setText(str(nsel))

    def _togglePlotControlsVisibility(self):
        if self._attrs_shown:
            self._attrs_shown = False
            self.table.hideColumn(self.ColApply)
            for col in self.AttrByCol.keys():
                self.table.hideColumn(col)
            self._showattrbtn.setText("Show plot styles >>")
        else:
            self._attrs_shown = True
            self.table.showColumn(self.ColApply)
            for col in self.AttrByCol.keys():
                self.table.showColumn(col)
            self._showattrbtn.setText("<< Hide plot styles")
Пример #10
0
class ImageController(QFrame):
    """An ImageController is a widget for controlling the display of one image.
    It can emit the following signals from the image:
    raise                     raise button was clicked
    center                  center-on-image option was selected
    unload                  unload option was selected
    slice                     image slice has changed, need to redraw (emitted by SkyImage automatically)
    repaint                 image display range or colormap has changed, need to redraw (emitted by SkyImage automatically)
    """

    def __init__(self, image, parent, imgman, name=None, save=False):
        QFrame.__init__(self, parent)
        self.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
        # init state
        self.image = image
        self._imgman = imgman
        self._currier = PersistentCurrier()
        self._control_dialog = None
        # create widgets
        self._lo = lo = QHBoxLayout(self)
        lo.setContentsMargins(0, 0, 0, 0)
        lo.setSpacing(2)
        # raise button
        self._wraise = QToolButton(self)
        lo.addWidget(self._wraise)
        self._wraise.setIcon(pixmaps.raise_up.icon())
        self._wraise.setAutoRaise(True)
        self._can_raise = False
        QObject.connect(self._wraise, SIGNAL("clicked()"), self._raiseButtonPressed)
        self._wraise.setToolTip("""<P>Click here to raise this image above other images. Hold the button down briefly to
      show a menu of image operations.</P>""")
        # center label
        self._wcenter = QLabel(self)
        self._wcenter.setPixmap(pixmaps.center_image.pm())
        self._wcenter.setToolTip(
            "<P>The plot is currently centered on (the reference pixel %d,%d) of this image.</P>" % self.image.referencePixel())
        lo.addWidget(self._wcenter)
        # name/filename label
        self.name = image.name
        self._wlabel = QLabel(self.name, self)
        self._number = 0
        self.setName(self.name)
        self._wlabel.setToolTip("%s %s" % (image.filename, "\u00D7".join(map(str, image.data().shape))))
        lo.addWidget(self._wlabel, 1)
        # if 'save' is specified, create a "save" button
        if save:
            self._wsave = QToolButton(self)
            lo.addWidget(self._wsave)
            self._wsave.setText("save")
            self._wsave.setAutoRaise(True)
            self._save_dir = save if isinstance(save, str) else "."
            QObject.connect(self._wsave, SIGNAL("clicked()"), self._saveImage)
            self._wsave.setToolTip("""<P>Click here to write this image to a FITS file.</P>""")
        # render control
        dprint(2, "creating RenderControl")
        self._rc = RenderControl(image, self)
        dprint(2, "done")
        # selectors for extra axes
        self._wslicers = []
        curslice = self._rc.currentSlice();  # this may be loaded from config, so not necessarily 0
        for iextra, axisname, labels in self._rc.slicedAxes():
            if axisname.upper() not in ["STOKES", "COMPLEX"]:
                lbl = QLabel("%s:" % axisname, self)
                lo.addWidget(lbl)
            else:
                lbl = None
            slicer = QComboBox(self)
            self._wslicers.append(slicer)
            lo.addWidget(slicer)
            slicer.addItems(labels)
            slicer.setToolTip("""<P>Selects current slice along the %s axis.</P>""" % axisname)
            slicer.setCurrentIndex(curslice[iextra])
            QObject.connect(slicer, SIGNAL("activated(int)"), self._currier.curry(self._rc.changeSlice, iextra))
        # min/max display ranges
        lo.addSpacing(5)
        self._wrangelbl = QLabel(self)
        lo.addWidget(self._wrangelbl)
        self._minmaxvalidator = FloatValidator(self)
        self._wmin = QLineEdit(self)
        self._wmax = QLineEdit(self)
        width = self._wmin.fontMetrics().width("1.234567e-05")
        for w in self._wmin, self._wmax:
            lo.addWidget(w, 0)
            w.setValidator(self._minmaxvalidator)
            w.setMaximumWidth(width)
            w.setMinimumWidth(width)
            QObject.connect(w, SIGNAL("editingFinished()"), self._changeDisplayRange)
        # full-range button
        self._wfullrange = QToolButton(self)
        lo.addWidget(self._wfullrange, 0)
        self._wfullrange.setIcon(pixmaps.zoom_range.icon())
        self._wfullrange.setAutoRaise(True)
        QObject.connect(self._wfullrange, SIGNAL("clicked()"), self.renderControl().resetSubsetDisplayRange)
        rangemenu = QMenu(self)
        rangemenu.addAction(pixmaps.full_range.icon(), "Full subset", self.renderControl().resetSubsetDisplayRange)
        for percent in (99.99, 99.9, 99.5, 99, 98, 95):
            rangemenu.addAction("%g%%" % percent, self._currier.curry(self._changeDisplayRangeToPercent, percent))
        self._wfullrange.setPopupMode(QToolButton.DelayedPopup)
        self._wfullrange.setMenu(rangemenu)
        # update widgets from current display range
        self._updateDisplayRange(*self._rc.displayRange())
        # lock button
        self._wlock = QToolButton(self)
        self._wlock.setIcon(pixmaps.unlocked.icon())
        self._wlock.setAutoRaise(True)
        self._wlock.setToolTip("""<P>Click to lock or unlock the intensity range. When the intensity range is locked across multiple images, any changes in the intensity
          range of one are propagated to the others. Hold the button down briefly for additional options.</P>""")
        lo.addWidget(self._wlock)
        QObject.connect(self._wlock, SIGNAL("clicked()"), self._toggleDisplayRangeLock)
        QObject.connect(self.renderControl(), SIGNAL("displayRangeLocked"), self._setDisplayRangeLock)
        QObject.connect(self.renderControl(), SIGNAL("dataSubsetChanged"), self._dataSubsetChanged)
        lockmenu = QMenu(self)
        lockmenu.addAction(pixmaps.locked.icon(), "Lock all to this",
                           self._currier.curry(imgman.lockAllDisplayRanges, self.renderControl()))
        lockmenu.addAction(pixmaps.unlocked.icon(), "Unlock all", imgman.unlockAllDisplayRanges)
        self._wlock.setPopupMode(QToolButton.DelayedPopup)
        self._wlock.setMenu(lockmenu)
        self._setDisplayRangeLock(self.renderControl().isDisplayRangeLocked())
        # dialog button
        self._wshowdialog = QToolButton(self)
        lo.addWidget(self._wshowdialog)
        self._wshowdialog.setIcon(pixmaps.colours.icon())
        self._wshowdialog.setAutoRaise(True)
        self._wshowdialog.setToolTip("""<P>Click for colourmap and intensity policy options.</P>""")
        QObject.connect(self._wshowdialog, SIGNAL("clicked()"), self.showRenderControls)
        tooltip = """<P>You can change the currently displayed intensity range by entering low and high limits here.</P>
    <TABLE>
      <TR><TD><NOBR>Image min:</NOBR></TD><TD>%g</TD><TD>max:</TD><TD>%g</TD></TR>
      </TABLE>""" % self.image.imageMinMax()
        for w in self._wmin, self._wmax, self._wrangelbl:
            w.setToolTip(tooltip)

        # create image operations menu
        self._menu = QMenu(self.name, self)
        self._qa_raise = self._menu.addAction(pixmaps.raise_up.icon(), "Raise image",
                                              self._currier.curry(self.image.emit, SIGNAL("raise")))
        self._qa_center = self._menu.addAction(pixmaps.center_image.icon(), "Center plot on image",
                                               self._currier.curry(self.image.emit, SIGNAL("center")))
        self._qa_show_rc = self._menu.addAction(pixmaps.colours.icon(), "Colours && Intensities...",
                                                self.showRenderControls)
        if save:
            self._qa_save = self._menu.addAction("Save image...", self._saveImage)
        self._menu.addAction("Export image to PNG file...", self._exportImageToPNG)
        self._export_png_dialog = None
        self._menu.addAction("Unload image", self._currier.curry(self.image.emit, SIGNAL("unload")))
        self._wraise.setMenu(self._menu)
        self._wraise.setPopupMode(QToolButton.DelayedPopup)

        # connect updates from renderControl and image
        self.image.connect(SIGNAL("slice"), self._updateImageSlice)
        QObject.connect(self._rc, SIGNAL("displayRangeChanged"), self._updateDisplayRange)

        # default plot depth of image markers
        self._z_markers = None
        # and the markers themselves
        self._image_border = QwtPlotCurve()
        self._image_label = QwtPlotMarker()

        # subset markers
        self._subset_pen = QPen(QColor("Light Blue"))
        self._subset_border = QwtPlotCurve()
        self._subset_border.setPen(self._subset_pen)
        self._subset_border.setVisible(False)
        self._subset_label = QwtPlotMarker()
        text = QwtText("subset")
        text.setColor(self._subset_pen.color())
        self._subset_label.setLabel(text)
        self._subset_label.setLabelAlignment(Qt.AlignRight | Qt.AlignBottom)
        self._subset_label.setVisible(False)
        self._setting_lmrect = False

        self._all_markers = [self._image_border, self._image_label, self._subset_border, self._subset_label]

    def close(self):
        if self._control_dialog:
            self._control_dialog.close()
            self._control_dialog = None

    def __del__(self):
        self.close()

    def __eq__(self, other):
        return self is other

    def renderControl(self):
        return self._rc

    def getMenu(self):
        return self._menu

    def getFilename(self):
        return self.image.filename

    def setName(self, name):
        self.name = name
        self._wlabel.setText("%s: %s" % (chr(ord('a') + self._number), self.name))

    def setNumber(self, num):
        self._number = num
        self._menu.menuAction().setText("%s: %s" % (chr(ord('a') + self._number), self.name))
        self._qa_raise.setShortcut(QKeySequence("Alt+" + chr(ord('A') + num)))
        self.setName(self.name)

    def getNumber(self):
        return self._number

    def setPlotProjection(self, proj):
        self.image.setPlotProjection(proj)
        sameproj = proj == self.image.projection
        self._wcenter.setVisible(sameproj)
        self._qa_center.setVisible(not sameproj)
        if self._image_border:
            (l0, l1), (m0, m1) = self.image.getExtents()
            path = numpy.array([l0, l0, l1, l1, l0]), numpy.array([m0, m1, m1, m0, m0])
            self._image_border.setData(*path)
            if self._image_label:
                self._image_label.setValue(path[0][2], path[1][2])

    def addPlotBorder(self, border_pen, label, label_color=None, bg_brush=None):
        # make plot items for image frame
        # make curve for image borders
        (l0, l1), (m0, m1) = self.image.getExtents()
        self._border_pen = QPen(border_pen)
        self._image_border.show()
        self._image_border.setData([l0, l0, l1, l1, l0], [m0, m1, m1, m0, m0])
        self._image_border.setPen(self._border_pen)
        self._image_border.setZ(self.image.z() + 1 if self._z_markers is None else self._z_markers)
        if label:
            self._image_label.show()
            self._image_label_text = text = QwtText(" %s " % label)
            text.setColor(label_color)
            text.setBackgroundBrush(bg_brush)
            self._image_label.setValue(l1, m1)
            self._image_label.setLabel(text)
            self._image_label.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
            self._image_label.setZ(self.image.z() + 2 if self._z_markers is None else self._z_markers)

    def setPlotBorderStyle(self, border_color=None, label_color=None):
        if border_color:
            self._border_pen.setColor(border_color)
            self._image_border.setPen(self._border_pen)
        if label_color:
            self._image_label_text.setColor(label_color)
            self._image_label.setLabel(self._image_label_text)

    def showPlotBorder(self, show=True):
        self._image_border.setVisible(show)
        self._image_label.setVisible(show)

    def attachToPlot(self, plot, z_markers=None):
        for item in [self.image] + self._all_markers:
            if item.plot() != plot:
                item.attach(plot)

    def setImageVisible(self, visible):
        self.image.setVisible(visible)

    def showRenderControls(self):
        if not self._control_dialog:
            dprint(1, "creating control dialog")
            self._control_dialog = ImageControlDialog(self, self._rc, self._imgman)
            dprint(1, "done")
        if not self._control_dialog.isVisible():
            dprint(1, "showing control dialog")
            self._control_dialog.show()
        else:
            self._control_dialog.hide()

    def _changeDisplayRangeToPercent(self, percent):
        if not self._control_dialog:
            self._control_dialog = ImageControlDialog(self, self._rc, self._imgman)
        self._control_dialog._changeDisplayRangeToPercent(percent)

    def _updateDisplayRange(self, dmin, dmax):
        """Updates display range widgets."""
        self._wmin.setText("%.4g" % dmin)
        self._wmax.setText("%.4g" % dmax)
        self._updateFullRangeIcon()

    def _changeDisplayRange(self):
        """Gets display range from widgets and updates the image with it."""
        try:
            newrange = float(str(self._wmin.text())), float(str(self._wmax.text()))
        except ValueError:
            return
        self._rc.setDisplayRange(*newrange)

    def _dataSubsetChanged(self, subset, minmax, desc, subset_type):
        """Called when the data subset changes (or is reset)"""
        # hide the subset indicator -- unless we're invoked while we're actually setting the subset itself
        if not self._setting_lmrect:
            self._subset = None
            self._subset_border.setVisible(False)
            self._subset_label.setVisible(False)

    def setLMRectSubset(self, rect):
        self._subset = rect
        l0, m0, l1, m1 = rect.getCoords()
        self._subset_border.setData([l0, l0, l1, l1, l0], [m0, m1, m1, m0, m0])
        self._subset_border.setVisible(True)
        self._subset_label.setValue(max(l0, l1), max(m0, m1))
        self._subset_label.setVisible(True)
        self._setting_lmrect = True
        self.renderControl().setLMRectSubset(rect)
        self._setting_lmrect = False

    def currentSlice(self):
        return self._rc.currentSlice()

    def _updateImageSlice(self, slice):
        dprint(2, slice)
        for i, (iextra, name, labels) in enumerate(self._rc.slicedAxes()):
            slicer = self._wslicers[i]
            if slicer.currentIndex() != slice[iextra]:
                dprint(3, "setting widget", i, "to", slice[iextra])
                slicer.setCurrentIndex(slice[iextra])

    def setMarkersZ(self, z):
        self._z_markers = z
        for i, elem in enumerate(self._all_markers):
            elem.setZ(z + i)

    def setZ(self, z, top=False, depthlabel=None, can_raise=True):
        self.image.setZ(z)
        if self._z_markers is None:
            for i, elem in enumerate(self._all_markers):
                elem.setZ(z + i + i)
        # set the depth label, if any
        label = "%s: %s" % (chr(ord('a') + self._number), self.name)
        # label = "%s %s"%(depthlabel,self.name) if depthlabel else self.name
        if top:
            label = "%s: <B>%s</B>" % (chr(ord('a') + self._number), self.name)
        self._wlabel.setText(label)
        # set hotkey
        self._qa_show_rc.setShortcut(Qt.Key_F9 if top else QKeySequence())
        # set raise control
        self._can_raise = can_raise
        self._qa_raise.setVisible(can_raise)
        self._wlock.setVisible(can_raise)
        if can_raise:
            self._wraise.setToolTip(
                "<P>Click here to raise this image to the top. Click on the down-arrow to access the image menu.</P>")
        else:
            self._wraise.setToolTip("<P>Click to access the image menu.</P>")

    def _raiseButtonPressed(self):
        if self._can_raise:
            self.image.emit(SIGNAL("raise"))
        else:
            self._wraise.showMenu()

    def _saveImage(self):
        filename = QFileDialog.getSaveFileName(self, "Save FITS file", self._save_dir,
                                               "FITS files(*.fits *.FITS *fts *FTS)")
        filename = str(filename)
        if not filename:
            return
        busy = BusyIndicator()
        self._imgman.showMessage("""Writing FITS image %s""" % filename, 3000)
        QApplication.flush()
        try:
            self.image.save(filename)
        except Exception as exc:
            busy = None
            traceback.print_exc()
            self._imgman.showErrorMessage("""Error writing FITS image %s: %s""" % (filename, str(sys.exc_info()[1])))
            return None
        self.renderControl().startSavingConfig(filename)
        self.setName(self.image.name)
        self._qa_save.setVisible(False)
        self._wsave.hide()
        busy = None

    def _exportImageToPNG(self, filename=None):
        if not filename:
            if not self._export_png_dialog:
                dialog = self._export_png_dialog = QFileDialog(self, "Export image to PNG", ".", "*.png")
                dialog.setDefaultSuffix("png")
                dialog.setFileMode(QFileDialog.AnyFile)
                dialog.setAcceptMode(QFileDialog.AcceptSave)
                dialog.setModal(True)
                QObject.connect(dialog, SIGNAL("filesSelected(const QStringList &)"), self._exportImageToPNG)
            return self._export_png_dialog.exec_() == QDialog.Accepted
        busy = BusyIndicator()
        if isinstance(filename, QStringList):
            filename = filename[0]
        filename = str(filename)
        # make QPixmap
        nx, ny = self.image.imageDims()
        (l0, l1), (m0, m1) = self.image.getExtents()
        pixmap = QPixmap(nx, ny)
        painter = QPainter(pixmap)
        # use QwtPlot implementation of draw canvas, since we want to avoid caching
        xmap = QwtScaleMap()
        xmap.setPaintInterval(0, nx)
        xmap.setScaleInterval(l1, l0)
        ymap = QwtScaleMap()
        ymap.setPaintInterval(ny, 0)
        ymap.setScaleInterval(m0, m1)
        self.image.draw(painter, xmap, ymap, pixmap.rect())
        painter.end()
        # save to file
        try:
            pixmap.save(filename, "PNG")
        except Exception as exc:
            self.emit(SIGNAL("showErrorMessage"), "Error writing %s: %s" % (filename, str(exc)))
            return
        self.emit(SIGNAL("showMessage"), "Exported image to file %s" % filename)

    def _toggleDisplayRangeLock(self):
        self.renderControl().lockDisplayRange(not self.renderControl().isDisplayRangeLocked())

    def _setDisplayRangeLock(self, locked):
        self._wlock.setIcon(pixmaps.locked.icon() if locked else pixmaps.unlocked.icon())

    def _updateFullRangeIcon(self):
        if self._rc.isSubsetDisplayRange():
            self._wfullrange.setIcon(pixmaps.zoom_range.icon())
            self._wfullrange.setToolTip(
                """<P>The current intensity range is the full range. Hold this button down briefly for additional options.</P>""")
        else:
            self._wfullrange.setIcon(pixmaps.full_range.icon())
            self._wfullrange.setToolTip(
                """<P>Click to reset to a full intensity range. Hold the button down briefly for additional options.</P>""")
Пример #11
0
class MouseModeManager(QObject):
    class MouseMode(object):
        def __init__(self, mid):
            self.id = mid
            self.name = self.icon = self.tooltip = None
            self.contexts = []
            self.submodes = []
            self.patterns = {}
            self.qa = None

        def addAction(self, menu, qag, callback, toolbar=None):
            self.qa = menu.addAction(self.name, callback)
            icon = self.icon and getattr(pixmaps, self.icon, None)
            icon and self.qa.setIcon(icon.icon())
            self.qa.setCheckable(True)
            qag.addAction(self.qa)
            toolbar and toolbar.addAction(self.qa)

    def __init__(self, parent, menu, toolbar):
        QObject.__init__(self, parent)
        self._currier = PersistentCurrier()
        # get list of mouse modes from config
        modelist = []
        for mid in Config.get("mouse-modes", _DefaultModes).split(","):
            if not ConfigFile.has_section(mid):
                print("ERROR: unknown mouse-mode '%s', skipping. Check your %s." % (mid, ConfigFileName))
            else:
                modelist.append(self._readModeConfig(mid))
        self._modes = dict([(mode.id, mode) for mode in modelist])
        self._qag_mode = QActionGroup(self)
        self._qag_submode = QActionGroup(self)
        self._all_submodes = []
        # make entries for main modes
        for mode in modelist:
            mode.addAction(menu, self._qag_mode, callback=self._currier.curry(self._setMode, mode.id))
            if mode.submodes:
                self._all_submodes += list(mode.submodes)
        # make entries for submodes
        self._qa_submode_sep = menu.addSeparator()
        self._modes.update([(mode.id, mode) for mode in self._all_submodes])
        for mode in self._all_submodes:
            mode.addAction(menu, self._qag_submode, toolbar=toolbar,
                           callback=self._currier.curry(self._setSubmode, mode.id))
        # other init
        self._current_context = None
        self._available_submodes = []
        # set initial mode
        initmode = Config.get("current-mouse-mode", _DefaultInitialMode)
        if initmode not in self._modes:
            initmode = modelist[0].id
        self._modes[initmode].qa.setChecked(True)
        self._setMode(initmode, write_config=False)

    def currentMode(self):
        return self._current_submode or self._current_mode

    def setContext(self, has_image, has_model):
        self._current_context = (has_image and _Contexts['image']) | (has_model and _Contexts['model'])
        self._ensureValidSubmodes()

    def _ensureValidSubmodes(self):
        current = None
        self._valid_submodes = []
        # accumulate list of valid submodes, and find the checked-on one
        for mode in self._available_submodes:
            if not mode.contexts or not self._current_context or self._current_context & mode.contexts:
                self._valid_submodes.append(mode)
                mode.qa.setVisible(True)
                if mode.qa.isChecked():
                    current = mode.id
            else:
                mode.qa.setVisible(False)
        if self._valid_submodes:
            self._setSubmode(current or self._valid_submodes[0].id)

    def _setMode(self, mid, write_config=True):
        """Called when the mouse mode changes"""
        if write_config:
            Config.set("current-mouse-mode", mid)
        self._current_mode = mode = self._modes[mid]
        # hide submodes if any
        for mm in self._all_submodes:
            mm.qa.setVisible(False)
        self._qa_submode_sep.setVisible(bool(mode.submodes))
        self._current_submode = None
        self._available_submodes = mode.submodes
        # make relevant submodes visible, and make sure one is enabled
        if mode.submodes:
            self._ensureValidSubmodes()
        else:
            self.emit(SIGNAL("setMouseMode"), mode)

    def _setSubmode(self, mid):
        """Called when the mouse submode changes"""
        self._current_submode = mode = self._modes[mid]
        mode.qa.setChecked(True)
        # hide submodes if any
        for mm in self._all_submodes:
            mm.qa.setShortcuts([])
        # set F4 shortcut to next submode
        if len(self._valid_submodes) > 1:
            for i, mm in enumerate(self._valid_submodes):
                if mm is mode:
                    self._valid_submodes[(i + 1) % len(self._valid_submodes)].qa.setShortcut(Qt.Key_F4)
                    break
        self.emit(SIGNAL("setMouseMode"), mode)

    def _readModeConfig(self, section, main_tooltip=None):
        """Reads the given config section (and uses the supplied defaults dict)
        and returns a dict of mouse_patterns,key_patterns per each function."""
        # read basic stuff
        mode = self.MouseMode(section)
        config = TigGUI.kitties.config.SectionParser(ConfigFile, section)
        mode.name = config.get("name", section)
        mode.icon = config.get("icon", "") or None
        mode.contexts = sum([_Contexts.get(x, 0) for x in config.get("contexts", "").split(",")])
        submodes = config.get("submodes", "") or None
        # eiher a mode with submodes, or a main mode
        if submodes:
            mode.tooltip = "<P>Your current mouse scheme is \"%s\".</P>" % mode.name
            for mid in submodes.split(","):
                if ConfigFile.has_section(mid):
                    mode.submodes.append(self._readModeConfig(mid, main_tooltip=mode.tooltip))
                else:
                    print("ERROR: unknown submode '%s' in mode config section '%s', skipping/ Check your %s." % (
                    mid, section, ConfigFileName))
        else:
            if main_tooltip:
                mode.tooltip = main_tooltip + """<P>In this scheme, available mouse functions depend on the selected mode.
        The current mode is %s. Use F4 to cycle through other modes.</P>""" % mode.name
            else:
                mode.tooltip = "<P>Your current mouse scheme is: \"%s\".</P>" % mode.name
            mode.tooltip += """<P>The following mouse functions are available:</P><BR><TABLE>\n"""
            patterns = {}
            # get basic patterns
            for func in _AllFuncs:
                # get pattern
                pattern = config.get(func, "")
                if not pattern:
                    continue
                mouse_pattern = key_pattern = None
                for pat in pattern.split(";"):
                    pat = pat.strip()
                    if pat and pat.lower() != "none":
                        # split by "+" and lookup each identifier in the Qt namespace
                        scomps = pat.split("+")
                        try:
                            comps = [x if x in (WHEELUP, WHEELDOWN) else getattr(Qt, x) for x in scomps]
                        except AttributeError:
                            print("WARNING: can't parse '%s' for function '%s' in mode config section '%s', disabling. Check your %s." % (
                            pat, func, section, ConfigFileName))
                            continue
                        # append key/button code and sum of modifiers to the key or keyboard pattern list
                        if scomps[-1].startswith("Key_"):
                            if key_pattern:
                                print("WARNING: more than one key pattern for function '%s' in mode config section '%s', ignoring. Check your %s." % (
                                func, section, ConfigFileName))
                            else:
                                key_pattern = comps[-1], sum(comps[:-1])
                        else:
                            if mouse_pattern:
                                print("WARNING: more than one mouse pattern for function '%s' in mode config section '%s', ignoring. Check your %s." % (
                                func, section, ConfigFileName))
                            else:
                                mouse_pattern = comps[-1], sum(comps[:-1])
                mode.tooltip += "<TR><TD>%s:&nbsp;&nbsp;</TD><TD>%s</TD></TR>\n" % (pattern, FuncDoc[func])
                mode.patterns[func] = (mouse_pattern or (0, 0), key_pattern or (0, 0))
            mode.tooltip += "</TABLE><BR>"
        return mode