Ejemplo n.º 1
0
 def __init__(self, *args):
     Kittens.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])
Ejemplo n.º 2
0
 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);
Ejemplo n.º 3
0
 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.iteritems():
         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
Ejemplo n.º 4
0
 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)
Ejemplo n.º 5
0
 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();
Ejemplo n.º 6
0
 def __init__ (self,*args):
   Kittens.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]);
Ejemplo n.º 7
0
 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.iteritems():
     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;
Ejemplo n.º 8
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 = Kittens.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;
Ejemplo n.º 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.iteritems():
      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.iteritems ()]);

  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.iteritems():
        # 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(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 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.iterkeys():
        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.iterkeys():
        self.table.showColumn(col);
      self._showattrbtn.setText("<< Hide plot styles");
Ejemplo n.º 10
0
    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, u"\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
        ]
Ejemplo n.º 11
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,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,exc:
      busy = None;
      traceback.print_exc();
      self.showErrorMessage("""Error evaluating "%s": %s."""%(expression,str(exc)));
      return None;
Ejemplo n.º 12
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(u"\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(u"\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(u"\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((u"\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((u"\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();
Ejemplo n.º 13
0
  def __init__ (self,parent,hide_on_close=False):
    QMainWindow.__init__(self,parent);
    self.setWindowIcon(pixmaps.tigger_starface.icon());
    self._currier = PersistentCurrier();
    self.hide();
    # init column constants
    for icol,col in enumerate(self.ViewModelColumns):
        setattr(self,"Column%s"%col.capitalize(),icol);
    # init GUI
    self.setWindowTitle("Tigger");
    # self.setIcon(pixmaps.purr_logo.pm());
    cw = QWidget(self);
    self.setCentralWidget(cw);
    cwlo = QVBoxLayout(cw);
    cwlo.setMargin(5);
    # make splitter
    spl1 = self._splitter1 = QSplitter(Qt.Vertical,cw);
    spl1.setOpaqueResize(False);
    cwlo.addWidget(spl1);
    # Create listview of LSM entries
    self.tw = SkyModelTreeWidget(spl1);
    self.tw.hide();

    # split bottom pane
    spl2 = self._splitter2 = QSplitter(Qt.Horizontal,spl1);
    spl2.setOpaqueResize(False);
    self._skyplot_stack = QWidget(spl2);
    self._skyplot_stack_lo = QVBoxLayout(self._skyplot_stack);
    self._skyplot_stack_lo.setContentsMargins(0,0,0,0);

    # add plot
    self.skyplot = SkyModelPlotter(self._skyplot_stack,self);
    self.skyplot.resize(128,128);
    self.skyplot.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Preferred);
    self._skyplot_stack_lo.addWidget(self.skyplot,1000);
    self.skyplot.hide();
    QObject.connect(self.skyplot,SIGNAL("imagesChanged"),self._imagesChanged);
    QObject.connect(self.skyplot,SIGNAL("showMessage"),self.showMessage);
    QObject.connect(self.skyplot,SIGNAL("showErrorMessage"),self.showErrorMessage);

    self._grouptab_stack = QWidget(spl2);
    self._grouptab_stack_lo = lo =QVBoxLayout(self._grouptab_stack);
    self._grouptab_stack_lo.setContentsMargins(0,0,0,0);
    # add groupings table
    self.grouptab = ModelGroupsTable(self._grouptab_stack);
    self.grouptab.setSizePolicy(QSizePolicy.Preferred,QSizePolicy.Preferred);
    QObject.connect(self,SIGNAL("hasSkyModel"),self.grouptab.setEnabled);
    lo.addWidget(self.grouptab,1000);
    lo.addStretch(1);
    self.grouptab.hide();

    # add image controls -- parentless for now (setLayout will reparent them anyway)
    self.imgman = ImageManager();
    self.skyplot.setImageManager(self.imgman);
    QObject.connect(self.imgman,SIGNAL("imagesChanged"),self._imagesChanged);
    QObject.connect(self.imgman,SIGNAL("showMessage"),self.showMessage);
    QObject.connect(self.imgman,SIGNAL("showErrorMessage"),self.showErrorMessage);

    # enable status line
    self.statusBar().show();
    # Create and populate main menu
    menubar = self.menuBar();
    # File menu
    file_menu = menubar.addMenu("&File");
    qa_open = file_menu.addAction("&Open model...",self._openFileCallback,Qt.CTRL+Qt.Key_O);
    qa_merge = file_menu.addAction("&Merge in model...",self._mergeFileCallback,Qt.CTRL+Qt.SHIFT+Qt.Key_O);
    QObject.connect(self,SIGNAL("hasSkyModel"),qa_merge.setEnabled);
    file_menu.addSeparator();
    qa_save = file_menu.addAction("&Save model",self.saveFile,Qt.CTRL+Qt.Key_S);
    QObject.connect(self,SIGNAL("isUpdated"),qa_save.setEnabled);
    qa_save_as = file_menu.addAction("Save model &as...",self.saveFileAs);
    QObject.connect(self,SIGNAL("hasSkyModel"),qa_save_as.setEnabled);
    qa_save_selection_as = file_menu.addAction("Save selection as...",self.saveSelectionAs);
    QObject.connect(self,SIGNAL("hasSelection"),qa_save_selection_as.setEnabled);
    file_menu.addSeparator();
    qa_close = file_menu.addAction("&Close model",self.closeFile,Qt.CTRL+Qt.Key_W);
    QObject.connect(self,SIGNAL("hasSkyModel"),qa_close.setEnabled);
    qa_quit = file_menu.addAction("Quit",self.close,Qt.CTRL+Qt.Key_Q);

    # Image menu
    menubar.addMenu(self.imgman.getMenu());
    # Plot menu
    menubar.addMenu(self.skyplot.getMenu());

    # LSM Menu
    em = QMenu("&LSM",self);
    self._qa_em = menubar.addMenu(em);
    self._qa_em.setVisible(False);
    QObject.connect(self,SIGNAL("hasSkyModel"),self._qa_em.setVisible);
    self._column_view_menu = QMenu("&Show columns",self);
    self._qa_cv_menu = em.addMenu(self._column_view_menu);
    em.addSeparator();
    em.addAction("Select &all",self._selectAll,Qt.CTRL+Qt.Key_A);
    em.addAction("&Invert selection",self._selectInvert,Qt.CTRL+Qt.Key_I);
    em.addAction("Select b&y attribute...",self._showSourceSelector,Qt.CTRL+Qt.Key_Y);
    em.addSeparator();
    qa_add_tag = em.addAction("&Tag selection...",self.addTagToSelection,Qt.CTRL+Qt.Key_T);
    QObject.connect(self,SIGNAL("hasSelection"),qa_add_tag.setEnabled);
    qa_del_tag = em.addAction("&Untag selection...",self.removeTagsFromSelection,Qt.CTRL+Qt.Key_U);
    QObject.connect(self,SIGNAL("hasSelection"),qa_del_tag.setEnabled);
    qa_del_sel = em.addAction("&Delete selection",self._deleteSelection);
    QObject.connect(self,SIGNAL("hasSelection"),qa_del_sel.setEnabled);

   # Tools menu
    tm = self._tools_menu = QMenu("&Tools",self);
    self._qa_tm = menubar.addMenu(tm);
    self._qa_tm.setVisible(False);
    QObject.connect(self,SIGNAL("hasSkyModel"),self._qa_tm.setVisible);

   # Help menu
    menubar.addSeparator();
    hm = self._help_menu = menubar.addMenu("&Help");
    hm.addAction("&About...",self._showAboutDialog);
    self._about_dialog = None;

    # message handlers
    self.qerrmsg = QErrorMessage(self);

    # set initial state
    self.setAcceptDrops(True);
    self.model = None;
    self.filename = None;
    self._display_filename = None;
    self._open_file_dialog = self._merge_file_dialog = self._save_as_dialog = self._save_sel_as_dialog = self._open_image_dialog = None;
    self.emit(SIGNAL("isUpdated"),False);
    self.emit(SIGNAL("hasSkyModel"),False);
    self.emit(SIGNAL("hasSelection"),False);
    self._exiting = False;

    # set initial layout
    self._current_layout = None;
    self.setLayout(self.LayoutEmpty);
    dprint(1,"init complete");
Ejemplo n.º 14
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,u"\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,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;
Ejemplo n.º 15
0
class SkyModelTreeWidget (Kittens.widgets.ClickableTreeWidget):
  """This implements a QTreeWidget for sky models""";
  def __init__ (self,*args):
    Kittens.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):
    Kittens.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");
    Kittens.widgets.ClickableTreeWidget.clear(self);
    dprint(2,"creating model items");
    items = [ SkyModelTreeWidgetItem(src) for src in self.model.sources ];
    self._itemdict = dict(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"]);
Ejemplo n.º 16
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 = Kittens.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
Ejemplo n.º 17
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.iteritems():
            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.iteritems()
    ])

    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.iteritems():
                # 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(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 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.iterkeys():
                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.iterkeys():
                self.table.showColumn(col)
            self._showattrbtn.setText("<< Hide plot styles")
Ejemplo n.º 18
0
class SkyModelTreeWidget(Kittens.widgets.ClickableTreeWidget):
    """This implements a QTreeWidget for sky models"""
    def __init__(self, *args):
        Kittens.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):
        Kittens.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")
        Kittens.widgets.ClickableTreeWidget.clear(self)
        dprint(2, "creating model items")
        items = [SkyModelTreeWidgetItem(src) for src in self.model.sources]
        self._itemdict = dict(
            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"])
Ejemplo n.º 19
0
class MainWindow (QMainWindow):
  ViewModelColumns = [ "name","RA","Dec","type","Iapp","I","Q","U","V","RM","spi","shape" ];
  def __init__ (self,parent,hide_on_close=False):
    QMainWindow.__init__(self,parent);
    self.setWindowIcon(pixmaps.tigger_starface.icon());
    self._currier = PersistentCurrier();
    self.hide();
    # init column constants
    for icol,col in enumerate(self.ViewModelColumns):
        setattr(self,"Column%s"%col.capitalize(),icol);
    # init GUI
    self.setWindowTitle("Tigger");
    # self.setIcon(pixmaps.purr_logo.pm());
    cw = QWidget(self);
    self.setCentralWidget(cw);
    cwlo = QVBoxLayout(cw);
    cwlo.setMargin(5);
    # make splitter
    spl1 = self._splitter1 = QSplitter(Qt.Vertical,cw);
    spl1.setOpaqueResize(False);
    cwlo.addWidget(spl1);
    # Create listview of LSM entries
    self.tw = SkyModelTreeWidget(spl1);
    self.tw.hide();

    # split bottom pane
    spl2 = self._splitter2 = QSplitter(Qt.Horizontal,spl1);
    spl2.setOpaqueResize(False);
    self._skyplot_stack = QWidget(spl2);
    self._skyplot_stack_lo = QVBoxLayout(self._skyplot_stack);
    self._skyplot_stack_lo.setContentsMargins(0,0,0,0);

    # add plot
    self.skyplot = SkyModelPlotter(self._skyplot_stack,self);
    self.skyplot.resize(128,128);
    self.skyplot.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Preferred);
    self._skyplot_stack_lo.addWidget(self.skyplot,1000);
    self.skyplot.hide();
    QObject.connect(self.skyplot,SIGNAL("imagesChanged"),self._imagesChanged);
    QObject.connect(self.skyplot,SIGNAL("showMessage"),self.showMessage);
    QObject.connect(self.skyplot,SIGNAL("showErrorMessage"),self.showErrorMessage);

    self._grouptab_stack = QWidget(spl2);
    self._grouptab_stack_lo = lo =QVBoxLayout(self._grouptab_stack);
    self._grouptab_stack_lo.setContentsMargins(0,0,0,0);
    # add groupings table
    self.grouptab = ModelGroupsTable(self._grouptab_stack);
    self.grouptab.setSizePolicy(QSizePolicy.Preferred,QSizePolicy.Preferred);
    QObject.connect(self,SIGNAL("hasSkyModel"),self.grouptab.setEnabled);
    lo.addWidget(self.grouptab,1000);
    lo.addStretch(1);
    self.grouptab.hide();

    # add image controls -- parentless for now (setLayout will reparent them anyway)
    self.imgman = ImageManager();
    self.skyplot.setImageManager(self.imgman);
    QObject.connect(self.imgman,SIGNAL("imagesChanged"),self._imagesChanged);
    QObject.connect(self.imgman,SIGNAL("showMessage"),self.showMessage);
    QObject.connect(self.imgman,SIGNAL("showErrorMessage"),self.showErrorMessage);

    # enable status line
    self.statusBar().show();
    # Create and populate main menu
    menubar = self.menuBar();
    # File menu
    file_menu = menubar.addMenu("&File");
    qa_open = file_menu.addAction("&Open model...",self._openFileCallback,Qt.CTRL+Qt.Key_O);
    qa_merge = file_menu.addAction("&Merge in model...",self._mergeFileCallback,Qt.CTRL+Qt.SHIFT+Qt.Key_O);
    QObject.connect(self,SIGNAL("hasSkyModel"),qa_merge.setEnabled);
    file_menu.addSeparator();
    qa_save = file_menu.addAction("&Save model",self.saveFile,Qt.CTRL+Qt.Key_S);
    QObject.connect(self,SIGNAL("isUpdated"),qa_save.setEnabled);
    qa_save_as = file_menu.addAction("Save model &as...",self.saveFileAs);
    QObject.connect(self,SIGNAL("hasSkyModel"),qa_save_as.setEnabled);
    qa_save_selection_as = file_menu.addAction("Save selection as...",self.saveSelectionAs);
    QObject.connect(self,SIGNAL("hasSelection"),qa_save_selection_as.setEnabled);
    file_menu.addSeparator();
    qa_close = file_menu.addAction("&Close model",self.closeFile,Qt.CTRL+Qt.Key_W);
    QObject.connect(self,SIGNAL("hasSkyModel"),qa_close.setEnabled);
    qa_quit = file_menu.addAction("Quit",self.close,Qt.CTRL+Qt.Key_Q);

    # Image menu
    menubar.addMenu(self.imgman.getMenu());
    # Plot menu
    menubar.addMenu(self.skyplot.getMenu());

    # LSM Menu
    em = QMenu("&LSM",self);
    self._qa_em = menubar.addMenu(em);
    self._qa_em.setVisible(False);
    QObject.connect(self,SIGNAL("hasSkyModel"),self._qa_em.setVisible);
    self._column_view_menu = QMenu("&Show columns",self);
    self._qa_cv_menu = em.addMenu(self._column_view_menu);
    em.addSeparator();
    em.addAction("Select &all",self._selectAll,Qt.CTRL+Qt.Key_A);
    em.addAction("&Invert selection",self._selectInvert,Qt.CTRL+Qt.Key_I);
    em.addAction("Select b&y attribute...",self._showSourceSelector,Qt.CTRL+Qt.Key_Y);
    em.addSeparator();
    qa_add_tag = em.addAction("&Tag selection...",self.addTagToSelection,Qt.CTRL+Qt.Key_T);
    QObject.connect(self,SIGNAL("hasSelection"),qa_add_tag.setEnabled);
    qa_del_tag = em.addAction("&Untag selection...",self.removeTagsFromSelection,Qt.CTRL+Qt.Key_U);
    QObject.connect(self,SIGNAL("hasSelection"),qa_del_tag.setEnabled);
    qa_del_sel = em.addAction("&Delete selection",self._deleteSelection);
    QObject.connect(self,SIGNAL("hasSelection"),qa_del_sel.setEnabled);

   # Tools menu
    tm = self._tools_menu = QMenu("&Tools",self);
    self._qa_tm = menubar.addMenu(tm);
    self._qa_tm.setVisible(False);
    QObject.connect(self,SIGNAL("hasSkyModel"),self._qa_tm.setVisible);

   # Help menu
    menubar.addSeparator();
    hm = self._help_menu = menubar.addMenu("&Help");
    hm.addAction("&About...",self._showAboutDialog);
    self._about_dialog = None;

    # message handlers
    self.qerrmsg = QErrorMessage(self);

    # set initial state
    self.setAcceptDrops(True);
    self.model = None;
    self.filename = None;
    self._display_filename = None;
    self._open_file_dialog = self._merge_file_dialog = self._save_as_dialog = self._save_sel_as_dialog = self._open_image_dialog = None;
    self.emit(SIGNAL("isUpdated"),False);
    self.emit(SIGNAL("hasSkyModel"),False);
    self.emit(SIGNAL("hasSelection"),False);
    self._exiting = False;

    # set initial layout
    self._current_layout = None;
    self.setLayout(self.LayoutEmpty);
    dprint(1,"init complete");

  # layout identifiers
  LayoutEmpty = "empty";
  LayoutImage = "image";
  LayoutImageModel = "model";
  LayoutSplit = "split";

  def _getFilenamesFromDropEvent (self,event):
    """Checks if drop event is valid (i.e. contains a local URL to a FITS file), and returns list of filenames contained therein.""";
    dprint(1,"drop event:",event.mimeData().text());
    if not event.mimeData().hasUrls():
      dprint(1,"drop event: no urls");
      return None;
    filenames = [];
    for url in event.mimeData().urls():
      name = str(url.toLocalFile());
      dprint(2,"drop event: name is",name);
      if name and Images.isFITS(name):
        filenames.append(name);
    dprint(2,"drop event: filenames are",filenames);
    return filenames;

  def dragEnterEvent (self,event):
    if self._getFilenamesFromDropEvent(event):
      dprint(1,"drag-enter accepted");
      event.acceptProposedAction();
    else:
      dprint(1,"drag-enter rejected");

  def dropEvent (self,event):
    filenames = self._getFilenamesFromDropEvent(event);
    dprint(1,"dropping",filenames);
    if filenames:
      event.acceptProposedAction();
      busy = BusyIndicator();
      for name in filenames:
        self.imgman.loadImage(name);

  def saveSizes (self):
    if self._current_layout is not None:
      dprint(1,"saving sizes for layout",self._current_layout);
      # save main window size and splitter dimensions
      sz = self.size();
      Config.set('%s-main-window-width'%self._current_layout,sz.width());
      Config.set('%s-main-window-height'%self._current_layout,sz.height());
      for spl,name in ((self._splitter1,"splitter1"),(self._splitter2,"splitter2")):
        ssz = spl.sizes();
        for i,sz in enumerate(ssz):
          Config.set('%s-%s-size%d'%(self._current_layout,name,i),sz);

  def loadSizes (self):
    if self._current_layout is not None:
      dprint(1,"loading sizes for layout",self._current_layout);
      # get main window size and splitter dimensions
      w = Config.getint('%s-main-window-width'%self._current_layout,0);
      h = Config.getint('%s-main-window-height'%self._current_layout,0);
      dprint(2,"window size is",w,h);
      if not (w and h):
        return None;
      self.resize(QSize(w,h));
      for spl,name in (self._splitter1,"splitter1"),(self._splitter2,"splitter2"):
        ssz = [ Config.getint('%s-%s-size%d'%(self._current_layout,name,i),-1) for i in 0,1 ];
        dprint(2,"splitter",name,"sizes",ssz);
        if all([ sz >=0 for sz in ssz ]):
          spl.setSizes(ssz);
        else:
          return None;
    return True;

  def setLayout (self,layout):
    """Changes the current window layout. Restores sizes etc. from config file.""";
    if self._current_layout is layout:
      return;
    dprint(1,"switching to layout",layout);
    # save sizes to config file
    self.saveSizes();
    # remove imgman widget from all layouts
    for lo in self._skyplot_stack_lo,self._grouptab_stack_lo:
      if lo.indexOf(self.imgman) >= 0:
        lo.removeWidget(self.imgman);
    # assign it to appropriate parent and parent's layout
    if layout is self.LayoutImage or layout is self.LayoutEmpty:
      lo = self._skyplot_stack_lo;
    else:
      lo = self._grouptab_stack_lo;
    self.imgman.setParent(lo.parentWidget());
    lo.addWidget(self.imgman,0);
    # show/hide panels
    if layout is self.LayoutEmpty:
      self.tw.hide();
      self.grouptab.hide();
      self.skyplot.show();
    elif layout is self.LayoutImage:
      self.tw.hide();
      self.grouptab.hide();
      self.skyplot.show();
    elif layout is self.LayoutImageModel:
      self.tw.show();
      self.grouptab.show();
      self.skyplot.show();
    # reload sizes
    self._current_layout = layout;
    if not self.loadSizes():
      dprint(1,"no sizes loaded, setting defaults");
      if layout is self.LayoutEmpty:
        self.resize(QSize(512,256));
      elif layout is self.LayoutImage:
        self.resize(QSize(512,512));
        self._splitter2.setSizes([512,0]);
      elif layout is self.LayoutImageModel:
        self.resize(QSize(1024,512));
        self._splitter1.setSizes([256,256]);
        self._splitter2.setSizes([256,256]);

  def enableUpdates (self,enable=True):
    """Enables updates of the child widgets. Usually called after startup is completed (i.e. all data loaded)""";
    self.skyplot.enableUpdates(enable);
    if enable:
      if self.model:
        self.setLayout(self.LayoutImageModel);
      elif self.imgman.getImages():
        self.setLayout(self.LayoutImage);
      else:
        self.setLayout(self.LayoutEmpty);
      self.show();

  def _showAboutDialog (self):
    if not self._about_dialog:
      self._about_dialog = AboutDialog.AboutDialog(self);
    self._about_dialog.show();

  def addTool (self,name,callback):
    """Adds a tool to the Tools menu""";
    self._tools_menu.addAction(name,self._currier.curry(self._callTool,callback));

  def _callTool (self,callback):
    callback(self,self.model);

  def _imagesChanged (self):
    """Called when the set of loaded images has changed""";
    if self.imgman.getImages():
      if self._current_layout  is self.LayoutEmpty:
       self.setLayout(self.LayoutImage);
    else:
      if not self.model:
       self.setLayout(self.LayoutEmpty);

  def _selectAll (self):
    if not self.model:
      return;
    busy = BusyIndicator();
    for src in self.model.sources:
      src.selected = True;
    self.model.emitSelection(self);

  def _selectInvert (self):
    if not self.model:
      return;
    busy = BusyIndicator();
    for src in self.model.sources:
      src.selected = not src.selected;
    self.model.emitSelection(self);

  def _deleteSelection (self):
    unselected = [ src for src in self.model.sources if not src.selected ];
    nsel = len(self.model.sources) - len(unselected);
    if QMessageBox.question(self,"Delete selection","""<P>Really deleted %d selected source(s)?
        %d unselected sources will remain in the model.</P>"""%(nsel,len(unselected)),
        QMessageBox.Ok|QMessageBox.Cancel,QMessageBox.Cancel) != QMessageBox.Ok:
      return;
    self.model.setSources(unselected);
    self.showMessage("""Deleted %d sources"""%nsel);
    self.model.emitUpdate(SkyModel.UpdateAll,origin=self);

  def _showSourceSelector (self):
    TigGUI.Tools.source_selector.show_source_selector(self, self.model);

  def _updateModelSelection (self,num,origin=None):
    """Called when the model selection has been updated.""";
    self.emit(SIGNAL("hasSelection"),bool(num));

  import Tigger.Models.Formats
  _formats = [f[1] for f in Tigger.Models.Formats.listFormatsFull()];

  _load_file_types = [ (doc,["*"+ext for ext in extensions],load) for load,save,doc,extensions in _formats if load ];
  _save_file_types = [ (doc,["*"+ext for ext in extensions],save) for load,save,doc,extensions in _formats if save ];

  def showMessage (self,msg,time=3000):
    self.statusBar().showMessage(msg,3000);

  def showErrorMessage (self,msg,time=3000):
    self.qerrmsg.showMessage(msg);

  def loadImage (self,filename):
    return self.imgman.loadImage(filename);

  def setModel (self,model):
    self.emit(SIGNAL("modelChanged"),model);
    if model:
      self.model = model;
      self.emit(SIGNAL("hasSkyModel"),True);
      self.emit(SIGNAL("hasSelection"),False);
      self.emit(SIGNAL("isUpdated"),False);
      self.model.enableSignals();
      self.model.connect("updated",self._indicateModelUpdated);
      self.model.connect("selected",self._updateModelSelection);
      # pass to children
      self.tw.setModel(self.model);
      self.grouptab.setModel(self.model);
      self.skyplot.setModel(self.model);
      # add items to View menu
      self._column_view_menu.clear();
      self.tw.addColumnViewActionsTo(self._column_view_menu);
    else:
      self.model = None;
      self.setWindowTitle("Tigger");
      self.emit(SIGNAL("hasSelection"),False);
      self.emit(SIGNAL("isUpdated"),False);
      self.emit(SIGNAL("hasSkyModel"),False);
      self.tw.clear();
      self.grouptab.clear();
      self.skyplot.setModel(None);

  def _openFileCallback (self):
    if not self._open_file_dialog:
      filters = ";;".join([  "%s (%s)"%(name," ".join(patterns)) for name,patterns,func in self._load_file_types ]);
      dialog = self._open_file_dialog = QFileDialog(self,"Open sky model",".",filters);
      dialog.setFileMode(QFileDialog.ExistingFile);
      dialog.setModal(True);
      QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self.openFile);
    self._open_file_dialog.exec_();
    return;

  def _mergeFileCallback (self):
    if not self._merge_file_dialog:
      filters = ";;".join([  "%s (%s)"%(name," ".join(patterns)) for name,patterns,func in self._load_file_types ]);
      dialog = self._merge_file_dialog = QFileDialog(self,"Merge in sky model",".",filters);
      dialog.setFileMode(QFileDialog.ExistingFile);
      dialog.setModal(True);
      QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),
        self._currier.curry(self.openFile,merge=True));
    self._merge_file_dialog.exec_();
    return;

  def openFile (self,filename=None,format=None,merge=False,show=True):
    # check that we can close existing model
    if not merge and not self._canCloseExistingModel():
      return False;
    if isinstance(filename,QStringList):
      filename = filename[0];
    filename = str(filename);
    # try to determine the file type
    filetype,import_func,export_func,doc = Tigger.Models.Formats.resolveFormat(filename, format);
    if import_func is None:
      self.showErrorMessage("""Error loading model file %s: unknown file format"""%filename);
      return;
    # try to load the specified file
    busy = BusyIndicator();
    self.showMessage("""Reading %s file %s"""%(filetype,filename),3000);
    QApplication.flush();
    try:
      model = import_func(filename);
      model.setFilename(filename);
    except:
      busy = None;
      self.showErrorMessage("""Error loading '%s' file %s: %s"""%(filetype,filename,str(sys.exc_info()[1])));
      return;
    # set the layout
    if show:
      self.setLayout(self.LayoutImageModel);
    # add to content
    if merge and self.model:
      self.model.addSources(model.sources);
      self.showMessage("""Merged in %d sources from '%s' file %s"""%(len(model.sources),filetype,filename),3000);
      self.model.emitUpdate(SkyModel.UpdateAll);
    else:
      self.showMessage("""Loaded %d sources from '%s' file %s"""%(len(model.sources),filetype,filename),3000);
      self._display_filename = os.path.basename(filename);
      self.setModel(model);
      self._indicateModelUpdated(updated=False);
      # only set self.filename if an export function is available for this format. Otherwise set it to None, so that trying to save
      # the file results in a save-as operation (so that we don't save to a file in an unsupported format).
      self.filename = filename if export_func else None;

  def closeEvent (self,event):
    dprint(1,"closing");
    self._exiting = True;
    self.saveSizes();
    if not self.closeFile():
      self._exiting = False;
      event.ignore();
      return;
    self.skyplot.close();
    self.imgman.close();
    self.emit(SIGNAL("closing"));
    dprint(1,"invoking os._exit(0)");
    os._exit(0);
    QMainWindow.closeEvent(self,event);

  def _canCloseExistingModel (self):
    # save model if modified
    if self.model and self._model_updated:
      res = QMessageBox.question(self,"Closing sky model","<P>Model has been modified, would you like to save the changes?</P>",
                    QMessageBox.Save|QMessageBox.Discard|QMessageBox.Cancel,QMessageBox.Save);
      if res == QMessageBox.Cancel:
        return False;
      elif res == QMessageBox.Save:
        if not self.saveFile(confirm=False,overwrite=True):
          return False;
    # unload model images, unless we are already exiting anyway
    if not self._exiting:
      self.imgman.unloadModelImages();
    return True;

  def closeFile (self):
    if not self._canCloseExistingModel():
      return False;
    # close model
    self._display_filename = None;
    self.setModel(None);
    # set the layout
    self.setLayout(self.LayoutImage if self.imgman.getTopImage() else self.LayoutEmpty);
    return True;

  def saveFile (self,filename=None,confirm=False,overwrite=True,non_native=False):
    """Saves file using the specified 'filename'. If filename is None, uses current filename, if
    that is not set, goes to saveFileAs() to open dialog and get a filename.
    If overwrite=False, will ask for confirmation before overwriting an existing file.
    If non_native=False, will ask for confirmation before exporting in non-native format.
    If confirm=True, will ask for confirmation regardless.
    Returns True if saving succeeded, False on error (or if cancelled by user).
    """;
    if isinstance(filename,QStringList):
      filename = filename[0];
    filename = ( filename and str(filename) ) or self.filename;
    if filename is None:
      return self.saveFileAs();
    else:
      warning = '';
      # try to determine the file type
      filetype,import_func,export_func,doc = Tigger.Models.Formats.resolveFormat(filename, None);
      if export_func is None:
        self.showErrorMessage("""Error saving model file %s: unsupported output format"""%filename);
        return;
      if os.path.exists(filename) and not overwrite:
        warning += "<P>The file already exists and will be overwritten.</P>";
      if filetype != 'Tigger' and not non_native:
        warning += """<P>Please note that you are exporting the model using the external format '%s'.
              Source types, tags and other model features not supported by this
              format will be omitted during the export.</P>"""%filetype;
      # get confirmation
      if confirm or warning:
        dialog = QMessageBox.warning if warning else QMessageBox.question;
        if dialog(self,"Saving sky model","<P>Save model to %s?</P>%s"%(filename,warning),
                  QMessageBox.Save|QMessageBox.Cancel,QMessageBox.Save) != QMessageBox.Save:
          return False;
      busy = BusyIndicator();
      try:
        export_func(self.model,filename);
        self.model.setFilename(filename);
      except:
          busy = None;
          self.showErrorMessage("""Error saving model file %s: %s"""%(filename,str(sys.exc_info()[1])));
          return False;
      self.showMessage("""Saved model to file %s"""%filename,3000);
      self._display_filename = os.path.basename(filename);
      self._indicateModelUpdated(updated=False);
      self.filename = filename;
      return True;

  def saveFileAs (self,filename=None):
    """Saves file using the specified 'filename'. If filename is None, opens dialog to get a filename.
    Returns True if saving succeeded, False on error (or if cancelled by user).
    """;
    if filename is None:
      if not self._save_as_dialog:
          filters = ";;".join([  "%s (%s)"%(name," ".join(patterns)) for name,patterns,func in self._save_file_types ]);
          dialog = self._save_as_dialog = QFileDialog(self,"Save sky model",".",filters);
          dialog.setDefaultSuffix(ModelHTML.DefaultExtension);
          dialog.setFileMode(QFileDialog.AnyFile);
          dialog.setAcceptMode(QFileDialog.AcceptSave);
          dialog.setConfirmOverwrite(False);
          dialog.setModal(True);
          QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self.saveFileAs);
      return self._save_as_dialog.exec_() == QDialog.Accepted;
    # filename supplied, so save
    return self.saveFile(filename,confirm=False);

  def saveSelectionAs (self,filename=None,force=False):
    if not self.model:
      return;
    if filename is None:
      if not self._save_sel_as_dialog:
          filters = ";;".join([  "%s (%s)"%(name," ".join(patterns)) for name,patterns,func in self._save_file_types ]);
          dialog = self._save_sel_as_dialog = QFileDialog(self,"Save sky model",".",filters);
          dialog.setDefaultSuffix(ModelHTML.DefaultExtension);
          dialog.setFileMode(QFileDialog.AnyFile);
          dialog.setAcceptMode(QFileDialog.AcceptSave);
          dialog.setConfirmOverwrite(True);
          dialog.setModal(True);
          QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self.saveSelectionAs);
      return self._save_sel_as_dialog.exec_() == QDialog.Accepted;
    # save selection
    if isinstance(filename,QStringList):
      filename = filename[0];
    filename= str(filename);
    selmodel = self.model.copy();
    sources = [ src for src in self.model.sources if src.selected ];
    if not sources:
      self.showErrorMessage("""You have not selected any sources to save.""");
      return;
    # try to determine the file type
    filetype,import_func,export_func,doc = Tigger.Models.Formats.resolveFormat(filename, None);
    if export_func is None:
      self.showErrorMessage("""Error saving model file %s: unsupported output format"""%filename);
      return;
    busy = BusyIndicator();
    try:
      export_func(self.model,filename,sources=sources);
    except:
      busy = None;
      self.showErrorMessage("""Error saving selection to model file %s: %s"""%(filename,str(sys.exc_info()[1])));
      return False;
    self.showMessage("""Wrote %d selected source%s to file %s"""%(len(selmodel.sources),"" if len(selmodel.sources)==1 else "s",filename),3000);
    pass;

  def addTagToSelection (self):
    if not hasattr(self,'_add_tag_dialog'):
      self._add_tag_dialog = Widgets.AddTagDialog(self,modal=True);
    self._add_tag_dialog.setTags(self.model.tagnames);
    self._add_tag_dialog.setValue(True);
    if self._add_tag_dialog.exec_() != QDialog.Accepted:
      return;
    tagname,value = self._add_tag_dialog.getTag();
    if tagname is None or value is None:
      return None;
    dprint(1,"tagging selected sources with",tagname,value);
    # tag selected sources
    for src in self.model.sources:
      if src.selected:
        src.setAttribute(tagname,value);
    # If tag is not new, set a UpdateSelectionOnly flag on the signal
    dprint(1,"adding tag to model");
    self.model.addTag(tagname);
    dprint(1,"recomputing totals");
    self.model.getTagGrouping(tagname).computeTotal(self.model.sources);
    dprint(1,"emitting update signal");
    what = SkyModel.UpdateSourceContent+SkyModel.UpdateTags+SkyModel.UpdateSelectionOnly;
    self.model.emitUpdate(what,origin=self);

  def removeTagsFromSelection (self):
    if not hasattr(self,'_remove_tag_dialog'):
      self._remove_tag_dialog = Widgets.SelectTagsDialog(self,modal=True,caption="Remove Tags",ok_button="Remove");
    # get set of all tags in selected sources
    tags = set();
    for src in self.model.sources:
      if src.selected:
        tags.update(src.getTagNames());
    if not tags:
      return;
    tags = list(tags);
    tags.sort();
    # show dialog
    self._remove_tag_dialog.setTags(tags);
    if self._remove_tag_dialog.exec_() != QDialog.Accepted:
      return;
    tags = self._remove_tag_dialog.getSelectedTags();
    if not tags:
      return;
    # ask for confirmation
    plural = (len(tags)>1 and "s") or "";
    if QMessageBox.question(self,"Removing tags","<P>Really remove the tag%s '%s' from selected sources?</P>"%(plural,"', '".join(tags)),
            QMessageBox.Yes|QMessageBox.No,QMessageBox.Yes) != QMessageBox.Yes:
      return;
    # remove the tags
    for src in self.model.sources:
      if src.selected:
        for tag in tags:
          src.removeAttribute(tag);
    # update model
    self.model.scanTags();
    self.model.initGroupings();
    # emit signal
    what = SkyModel.UpdateSourceContent+SkyModel.UpdateTags+SkyModel.UpdateSelectionOnly;
    self.model.emitUpdate(what,origin=self);

  def _indicateModelUpdated (self,what=None,origin=None,updated=True):
    """Marks model as updated.""";
    self._model_updated = updated;
    self.emit(SIGNAL("isUpdated"),updated);
    if self.model:
      self.setWindowTitle("Tigger - %s%s"%((self._display_filename or "(unnamed)"," (modified)" if updated else "")));
Ejemplo n.º 20
0
  def __init__ (self,parent,hide_on_close=False):
    QMainWindow.__init__(self,parent);
    self.setWindowIcon(pixmaps.tigger_starface.icon());
    self._currier = PersistentCurrier();
    self.hide();
    # init column constants
    for icol,col in enumerate(self.ViewModelColumns):
        setattr(self,"Column%s"%col.capitalize(),icol);
    # init GUI
    self.setWindowTitle("Tigger");
    # self.setIcon(pixmaps.purr_logo.pm());
    cw = QWidget(self);
    self.setCentralWidget(cw);
    cwlo = QVBoxLayout(cw);
    cwlo.setMargin(5);
    # make splitter
    spl1 = self._splitter1 = QSplitter(Qt.Vertical,cw);
    spl1.setOpaqueResize(False);
    cwlo.addWidget(spl1);
    # Create listview of LSM entries
    self.tw = SkyModelTreeWidget(spl1);
    self.tw.hide();

    # split bottom pane
    spl2 = self._splitter2 = QSplitter(Qt.Horizontal,spl1);
    spl2.setOpaqueResize(False);
    self._skyplot_stack = QWidget(spl2);
    self._skyplot_stack_lo = QVBoxLayout(self._skyplot_stack);
    self._skyplot_stack_lo.setContentsMargins(0,0,0,0);

    # add plot
    self.skyplot = SkyModelPlotter(self._skyplot_stack,self);
    self.skyplot.resize(128,128);
    self.skyplot.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Preferred);
    self._skyplot_stack_lo.addWidget(self.skyplot,1000);
    self.skyplot.hide();
    QObject.connect(self.skyplot,SIGNAL("imagesChanged"),self._imagesChanged);
    QObject.connect(self.skyplot,SIGNAL("showMessage"),self.showMessage);
    QObject.connect(self.skyplot,SIGNAL("showErrorMessage"),self.showErrorMessage);

    self._grouptab_stack = QWidget(spl2);
    self._grouptab_stack_lo = lo =QVBoxLayout(self._grouptab_stack);
    self._grouptab_stack_lo.setContentsMargins(0,0,0,0);
    # add groupings table
    self.grouptab = ModelGroupsTable(self._grouptab_stack);
    self.grouptab.setSizePolicy(QSizePolicy.Preferred,QSizePolicy.Preferred);
    QObject.connect(self,SIGNAL("hasSkyModel"),self.grouptab.setEnabled);
    lo.addWidget(self.grouptab,1000);
    lo.addStretch(1);
    self.grouptab.hide();

    # add image controls -- parentless for now (setLayout will reparent them anyway)
    self.imgman = ImageManager();
    self.skyplot.setImageManager(self.imgman);
    QObject.connect(self.imgman,SIGNAL("imagesChanged"),self._imagesChanged);
    QObject.connect(self.imgman,SIGNAL("showMessage"),self.showMessage);
    QObject.connect(self.imgman,SIGNAL("showErrorMessage"),self.showErrorMessage);

    # enable status line
    self.statusBar().show();
    # Create and populate main menu
    menubar = self.menuBar();
    # File menu
    file_menu = menubar.addMenu("&File");
    qa_open = file_menu.addAction("&Open model...",self._openFileCallback,Qt.CTRL+Qt.Key_O);
    qa_merge = file_menu.addAction("&Merge in model...",self._mergeFileCallback,Qt.CTRL+Qt.SHIFT+Qt.Key_O);
    QObject.connect(self,SIGNAL("hasSkyModel"),qa_merge.setEnabled);
    file_menu.addSeparator();
    qa_save = file_menu.addAction("&Save model",self.saveFile,Qt.CTRL+Qt.Key_S);
    QObject.connect(self,SIGNAL("isUpdated"),qa_save.setEnabled);
    qa_save_as = file_menu.addAction("Save model &as...",self.saveFileAs);
    QObject.connect(self,SIGNAL("hasSkyModel"),qa_save_as.setEnabled);
    qa_save_selection_as = file_menu.addAction("Save selection as...",self.saveSelectionAs);
    QObject.connect(self,SIGNAL("hasSelection"),qa_save_selection_as.setEnabled);
    file_menu.addSeparator();
    qa_close = file_menu.addAction("&Close model",self.closeFile,Qt.CTRL+Qt.Key_W);
    QObject.connect(self,SIGNAL("hasSkyModel"),qa_close.setEnabled);
    qa_quit = file_menu.addAction("Quit",self.close,Qt.CTRL+Qt.Key_Q);

    # Image menu
    menubar.addMenu(self.imgman.getMenu());
    # Plot menu
    menubar.addMenu(self.skyplot.getMenu());

    # LSM Menu
    em = QMenu("&LSM",self);
    self._qa_em = menubar.addMenu(em);
    self._qa_em.setVisible(False);
    QObject.connect(self,SIGNAL("hasSkyModel"),self._qa_em.setVisible);
    self._column_view_menu = QMenu("&Show columns",self);
    self._qa_cv_menu = em.addMenu(self._column_view_menu);
    em.addSeparator();
    em.addAction("Select &all",self._selectAll,Qt.CTRL+Qt.Key_A);
    em.addAction("&Invert selection",self._selectInvert,Qt.CTRL+Qt.Key_I);
    em.addAction("Select b&y attribute...",self._showSourceSelector,Qt.CTRL+Qt.Key_Y);
    em.addSeparator();
    qa_add_tag = em.addAction("&Tag selection...",self.addTagToSelection,Qt.CTRL+Qt.Key_T);
    QObject.connect(self,SIGNAL("hasSelection"),qa_add_tag.setEnabled);
    qa_del_tag = em.addAction("&Untag selection...",self.removeTagsFromSelection,Qt.CTRL+Qt.Key_U);
    QObject.connect(self,SIGNAL("hasSelection"),qa_del_tag.setEnabled);
    qa_del_sel = em.addAction("&Delete selection",self._deleteSelection);
    QObject.connect(self,SIGNAL("hasSelection"),qa_del_sel.setEnabled);

   # Tools menu
    tm = self._tools_menu = QMenu("&Tools",self);
    self._qa_tm = menubar.addMenu(tm);
    self._qa_tm.setVisible(False);
    QObject.connect(self,SIGNAL("hasSkyModel"),self._qa_tm.setVisible);

   # Help menu
    menubar.addSeparator();
    hm = self._help_menu = menubar.addMenu("&Help");
    hm.addAction("&About...",self._showAboutDialog);
    self._about_dialog = None;

    # message handlers
    self.qerrmsg = QErrorMessage(self);

    # set initial state
    self.setAcceptDrops(True);
    self.model = None;
    self.filename = None;
    self._display_filename = None;
    self._open_file_dialog = self._merge_file_dialog = self._save_as_dialog = self._save_sel_as_dialog = self._open_image_dialog = None;
    self.emit(SIGNAL("isUpdated"),False);
    self.emit(SIGNAL("hasSkyModel"),False);
    self.emit(SIGNAL("hasSelection"),False);
    self._exiting = False;

    # set initial layout
    self._current_layout = None;
    self.setLayout(self.LayoutEmpty);
    dprint(1,"init complete");
Ejemplo n.º 21
0
  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,u"\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 ];
Ejemplo n.º 22
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, u"\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, 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
Ejemplo n.º 23
0
  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(u"\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(u"\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(u"\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());
Ejemplo n.º 24
0
class MainWindow (QMainWindow):
  ViewModelColumns = [ "name","RA","Dec","type","Iapp","I","Q","U","V","RM","spi","shape" ];
  def __init__ (self,parent,hide_on_close=False):
    QMainWindow.__init__(self,parent);
    self.setWindowIcon(pixmaps.tigger_starface.icon());
    self._currier = PersistentCurrier();
    self.hide();
    # init column constants
    for icol,col in enumerate(self.ViewModelColumns):
        setattr(self,"Column%s"%col.capitalize(),icol);
    # init GUI
    self.setWindowTitle("Tigger");
    # self.setIcon(pixmaps.purr_logo.pm());
    cw = QWidget(self);
    self.setCentralWidget(cw);
    cwlo = QVBoxLayout(cw);
    cwlo.setMargin(5);
    # make splitter
    spl1 = self._splitter1 = QSplitter(Qt.Vertical,cw);
    spl1.setOpaqueResize(False);
    cwlo.addWidget(spl1);
    # Create listview of LSM entries
    self.tw = SkyModelTreeWidget(spl1);
    self.tw.hide();

    # split bottom pane
    spl2 = self._splitter2 = QSplitter(Qt.Horizontal,spl1);
    spl2.setOpaqueResize(False);
    self._skyplot_stack = QWidget(spl2);
    self._skyplot_stack_lo = QVBoxLayout(self._skyplot_stack);
    self._skyplot_stack_lo.setContentsMargins(0,0,0,0);

    # add plot
    self.skyplot = SkyModelPlotter(self._skyplot_stack,self);
    self.skyplot.resize(128,128);
    self.skyplot.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Preferred);
    self._skyplot_stack_lo.addWidget(self.skyplot,1000);
    self.skyplot.hide();
    QObject.connect(self.skyplot,SIGNAL("imagesChanged"),self._imagesChanged);
    QObject.connect(self.skyplot,SIGNAL("showMessage"),self.showMessage);
    QObject.connect(self.skyplot,SIGNAL("showErrorMessage"),self.showErrorMessage);

    self._grouptab_stack = QWidget(spl2);
    self._grouptab_stack_lo = lo =QVBoxLayout(self._grouptab_stack);
    self._grouptab_stack_lo.setContentsMargins(0,0,0,0);
    # add groupings table
    self.grouptab = ModelGroupsTable(self._grouptab_stack);
    self.grouptab.setSizePolicy(QSizePolicy.Preferred,QSizePolicy.Preferred);
    QObject.connect(self,SIGNAL("hasSkyModel"),self.grouptab.setEnabled);
    lo.addWidget(self.grouptab,1000);
    lo.addStretch(1);
    self.grouptab.hide();

    # add image controls -- parentless for now (setLayout will reparent them anyway)
    self.imgman = ImageManager();
    self.skyplot.setImageManager(self.imgman);
    QObject.connect(self.imgman,SIGNAL("imagesChanged"),self._imagesChanged);
    QObject.connect(self.imgman,SIGNAL("showMessage"),self.showMessage);
    QObject.connect(self.imgman,SIGNAL("showErrorMessage"),self.showErrorMessage);

    # enable status line
    self.statusBar().show();
    # Create and populate main menu
    menubar = self.menuBar();
    # File menu
    file_menu = menubar.addMenu("&File");
    qa_open = file_menu.addAction("&Open model...",self._openFileCallback,Qt.CTRL+Qt.Key_O);
    qa_merge = file_menu.addAction("&Merge in model...",self._mergeFileCallback,Qt.CTRL+Qt.SHIFT+Qt.Key_O);
    QObject.connect(self,SIGNAL("hasSkyModel"),qa_merge.setEnabled);
    file_menu.addSeparator();
    qa_save = file_menu.addAction("&Save model",self.saveFile,Qt.CTRL+Qt.Key_S);
    QObject.connect(self,SIGNAL("isUpdated"),qa_save.setEnabled);
    qa_save_as = file_menu.addAction("Save model &as...",self.saveFileAs);
    QObject.connect(self,SIGNAL("hasSkyModel"),qa_save_as.setEnabled);
    qa_save_selection_as = file_menu.addAction("Save selection as...",self.saveSelectionAs);
    QObject.connect(self,SIGNAL("hasSelection"),qa_save_selection_as.setEnabled);
    file_menu.addSeparator();
    qa_close = file_menu.addAction("&Close model",self.closeFile,Qt.CTRL+Qt.Key_W);
    QObject.connect(self,SIGNAL("hasSkyModel"),qa_close.setEnabled);
    qa_quit = file_menu.addAction("Quit",self.close,Qt.CTRL+Qt.Key_Q);

    # Image menu
    menubar.addMenu(self.imgman.getMenu());
    # Plot menu
    menubar.addMenu(self.skyplot.getMenu());

    # LSM Menu
    em = QMenu("&LSM",self);
    self._qa_em = menubar.addMenu(em);
    self._qa_em.setVisible(False);
    QObject.connect(self,SIGNAL("hasSkyModel"),self._qa_em.setVisible);
    self._column_view_menu = QMenu("&Show columns",self);
    self._qa_cv_menu = em.addMenu(self._column_view_menu);
    em.addSeparator();
    em.addAction("Select &all",self._selectAll,Qt.CTRL+Qt.Key_A);
    em.addAction("&Invert selection",self._selectInvert,Qt.CTRL+Qt.Key_I);
    em.addAction("Select b&y attribute...",self._showSourceSelector,Qt.CTRL+Qt.Key_Y);
    em.addSeparator();
    qa_add_tag = em.addAction("&Tag selection...",self.addTagToSelection,Qt.CTRL+Qt.Key_T);
    QObject.connect(self,SIGNAL("hasSelection"),qa_add_tag.setEnabled);
    qa_del_tag = em.addAction("&Untag selection...",self.removeTagsFromSelection,Qt.CTRL+Qt.Key_U);
    QObject.connect(self,SIGNAL("hasSelection"),qa_del_tag.setEnabled);
    qa_del_sel = em.addAction("&Delete selection",self._deleteSelection);
    QObject.connect(self,SIGNAL("hasSelection"),qa_del_sel.setEnabled);

   # Tools menu
    tm = self._tools_menu = QMenu("&Tools",self);
    self._qa_tm = menubar.addMenu(tm);
    self._qa_tm.setVisible(False);
    QObject.connect(self,SIGNAL("hasSkyModel"),self._qa_tm.setVisible);

   # Help menu
    menubar.addSeparator();
    hm = self._help_menu = menubar.addMenu("&Help");
    hm.addAction("&About...",self._showAboutDialog);
    self._about_dialog = None;

    # message handlers
    self.qerrmsg = QErrorMessage(self);

    # set initial state
    self.setAcceptDrops(True);
    self.model = None;
    self.filename = None;
    self._display_filename = None;
    self._open_file_dialog = self._merge_file_dialog = self._save_as_dialog = self._save_sel_as_dialog = self._open_image_dialog = None;
    self.emit(SIGNAL("isUpdated"),False);
    self.emit(SIGNAL("hasSkyModel"),False);
    self.emit(SIGNAL("hasSelection"),False);
    self._exiting = False;

    # set initial layout
    self._current_layout = None;
    self.setLayout(self.LayoutEmpty);
    dprint(1,"init complete");

  # layout identifiers
  LayoutEmpty = "empty";
  LayoutImage = "image";
  LayoutImageModel = "model";
  LayoutSplit = "split";

  def _getFilenamesFromDropEvent (self,event):
    """Checks if drop event is valid (i.e. contains a local URL to a FITS file), and returns list of filenames contained therein.""";
    dprint(1,"drop event:",event.mimeData().text());
    if not event.mimeData().hasUrls():
      dprint(1,"drop event: no urls");
      return None;
    filenames = [];
    for url in event.mimeData().urls():
      name = str(url.toLocalFile());
      dprint(2,"drop event: name is",name);
      if name and Images.isFITS(name):
        filenames.append(name);
    dprint(2,"drop event: filenames are",filenames);
    return filenames;

  def dragEnterEvent (self,event):
    if self._getFilenamesFromDropEvent(event):
      dprint(1,"drag-enter accepted");
      event.acceptProposedAction();
    else:
      dprint(1,"drag-enter rejected");

  def dropEvent (self,event):
    filenames = self._getFilenamesFromDropEvent(event);
    dprint(1,"dropping",filenames);
    if filenames:
      event.acceptProposedAction();
      busy = BusyIndicator();
      for name in filenames:
        self.imgman.loadImage(name);

  def saveSizes (self):
    if self._current_layout is not None:
      dprint(1,"saving sizes for layout",self._current_layout);
      # save main window size and splitter dimensions
      sz = self.size();
      Config.set('%s-main-window-width'%self._current_layout,sz.width());
      Config.set('%s-main-window-height'%self._current_layout,sz.height());
      for spl,name in ((self._splitter1,"splitter1"),(self._splitter2,"splitter2")):
        ssz = spl.sizes();
        for i,sz in enumerate(ssz):
          Config.set('%s-%s-size%d'%(self._current_layout,name,i),sz);

  def loadSizes (self):
    if self._current_layout is not None:
      dprint(1,"loading sizes for layout",self._current_layout);
      # get main window size and splitter dimensions
      w = Config.getint('%s-main-window-width'%self._current_layout,0);
      h = Config.getint('%s-main-window-height'%self._current_layout,0);
      dprint(2,"window size is",w,h);
      if not (w and h):
        return None;
      self.resize(QSize(w,h));
      for spl,name in (self._splitter1,"splitter1"),(self._splitter2,"splitter2"):
        ssz = [ Config.getint('%s-%s-size%d'%(self._current_layout,name,i),-1) for i in 0,1 ];
        dprint(2,"splitter",name,"sizes",ssz);
        if all([ sz >=0 for sz in ssz ]):
          spl.setSizes(ssz);
        else:
          return None;
    return True;

  def setLayout (self,layout):
    """Changes the current window layout. Restores sizes etc. from config file.""";
    if self._current_layout is layout:
      return;
    dprint(1,"switching to layout",layout);
    # save sizes to config file
    self.saveSizes();
    # remove imgman widget from all layouts
    for lo in self._skyplot_stack_lo,self._grouptab_stack_lo:
      if lo.indexOf(self.imgman) >= 0:
        lo.removeWidget(self.imgman);
    # assign it to appropriate parent and parent's layout
    if layout is self.LayoutImage or layout is self.LayoutEmpty:
      lo = self._skyplot_stack_lo;
    else:
      lo = self._grouptab_stack_lo;
    self.imgman.setParent(lo.parentWidget());
    lo.addWidget(self.imgman,0);
    # show/hide panels
    if layout is self.LayoutEmpty:
      self.tw.hide();
      self.grouptab.hide();
      self.skyplot.show();
    elif layout is self.LayoutImage:
      self.tw.hide();
      self.grouptab.hide();
      self.skyplot.show();
    elif layout is self.LayoutImageModel:
      self.tw.show();
      self.grouptab.show();
      self.skyplot.show();
    # reload sizes
    self._current_layout = layout;
    if not self.loadSizes():
      dprint(1,"no sizes loaded, setting defaults");
      if layout is self.LayoutEmpty:
        self.resize(QSize(512,256));
      elif layout is self.LayoutImage:
        self.resize(QSize(512,512));
        self._splitter2.setSizes([512,0]);
      elif layout is self.LayoutImageModel:
        self.resize(QSize(1024,512));
        self._splitter1.setSizes([256,256]);
        self._splitter2.setSizes([256,256]);

  def enableUpdates (self,enable=True):
    """Enables updates of the child widgets. Usually called after startup is completed (i.e. all data loaded)""";
    self.skyplot.enableUpdates(enable);
    if enable:
      if self.model:
        self.setLayout(self.LayoutImageModel);
      elif self.imgman.getImages():
        self.setLayout(self.LayoutImage);
      else:
        self.setLayout(self.LayoutEmpty);
      self.show();

  def _showAboutDialog (self):
    if not self._about_dialog:
      self._about_dialog = AboutDialog.AboutDialog(self);
    self._about_dialog.show();

  def addTool (self,name,callback):
    """Adds a tool to the Tools menu""";
    self._tools_menu.addAction(name,self._currier.curry(self._callTool,callback));

  def _callTool (self,callback):
    callback(self,self.model);

  def _imagesChanged (self):
    """Called when the set of loaded images has changed""";
    if self.imgman.getImages():
      if self._current_layout  is self.LayoutEmpty:
       self.setLayout(self.LayoutImage);
    else:
      if not self.model:
       self.setLayout(self.LayoutEmpty);

  def _selectAll (self):
    if not self.model:
      return;
    busy = BusyIndicator();
    for src in self.model.sources:
      src.selected = True;
    self.model.emitSelection(self);

  def _selectInvert (self):
    if not self.model:
      return;
    busy = BusyIndicator();
    for src in self.model.sources:
      src.selected = not src.selected;
    self.model.emitSelection(self);

  def _deleteSelection (self):
    unselected = [ src for src in self.model.sources if not src.selected ];
    nsel = len(self.model.sources) - len(unselected);
    if QMessageBox.question(self,"Delete selection","""<P>Really deleted %d selected source(s)?
        %d unselected sources will remain in the model.</P>"""%(nsel,len(unselected)),
        QMessageBox.Ok|QMessageBox.Cancel,QMessageBox.Cancel) != QMessageBox.Ok:
      return;
    self.model.setSources(unselected);
    self.showMessage("""Deleted %d sources"""%nsel);
    self.model.emitUpdate(SkyModel.UpdateAll,origin=self);

  def _showSourceSelector (self):
    Tigger.Tools.source_selector.show_source_selector(self,self.model);

  def _updateModelSelection (self,num,origin=None):
    """Called when the model selection has been updated.""";
    self.emit(SIGNAL("hasSelection"),bool(num));

  import Tigger.Models.Formats
  _formats = [ f[1] for f in Tigger.Models.Formats.listFormatsFull() ];

  _load_file_types = [ (doc,["*"+ext for ext in extensions],load) for load,save,doc,extensions in _formats if load ];
  _save_file_types = [ (doc,["*"+ext for ext in extensions],save) for load,save,doc,extensions in _formats if save ];

  def showMessage (self,msg,time=3000):
    self.statusBar().showMessage(msg,3000);

  def showErrorMessage (self,msg,time=3000):
    self.qerrmsg.showMessage(msg);

  def loadImage (self,filename):
    return self.imgman.loadImage(filename);

  def setModel (self,model):
    self.emit(SIGNAL("modelChanged"),model);
    if model:
      self.model = model;
      self.emit(SIGNAL("hasSkyModel"),True);
      self.emit(SIGNAL("hasSelection"),False);
      self.emit(SIGNAL("isUpdated"),False);
      self.model.enableSignals();
      self.model.connect("updated",self._indicateModelUpdated);
      self.model.connect("selected",self._updateModelSelection);
      # pass to children
      self.tw.setModel(self.model);
      self.grouptab.setModel(self.model);
      self.skyplot.setModel(self.model);
      # add items to View menu
      self._column_view_menu.clear();
      self.tw.addColumnViewActionsTo(self._column_view_menu);
    else:
      self.model = None;
      self.setWindowTitle("Tigger");
      self.emit(SIGNAL("hasSelection"),False);
      self.emit(SIGNAL("isUpdated"),False);
      self.emit(SIGNAL("hasSkyModel"),False);
      self.tw.clear();
      self.grouptab.clear();
      self.skyplot.setModel(None);

  def _openFileCallback (self):
    if not self._open_file_dialog:
      filters = ";;".join([  "%s (%s)"%(name," ".join(patterns)) for name,patterns,func in self._load_file_types ]);
      dialog = self._open_file_dialog = QFileDialog(self,"Open sky model",".",filters);
      dialog.setFileMode(QFileDialog.ExistingFile);
      dialog.setModal(True);
      QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self.openFile);
    self._open_file_dialog.exec_();
    return;

  def _mergeFileCallback (self):
    if not self._merge_file_dialog:
      filters = ";;".join([  "%s (%s)"%(name," ".join(patterns)) for name,patterns,func in self._load_file_types ]);
      dialog = self._merge_file_dialog = QFileDialog(self,"Merge in sky model",".",filters);
      dialog.setFileMode(QFileDialog.ExistingFile);
      dialog.setModal(True);
      QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),
        self._currier.curry(self.openFile,merge=True));
    self._merge_file_dialog.exec_();
    return;

  def openFile (self,filename=None,format=None,merge=False,show=True):
    from Models import ModelClasses
    # check that we can close existing model
    if not merge and not self._canCloseExistingModel():
      return False;
    if isinstance(filename,QStringList):
      filename = filename[0];
    filename = str(filename);
    # try to determine the file type
    filetype,import_func,export_func,doc = Tigger.Models.Formats.resolveFormat(filename,format);
    if import_func is None:
      self.showErrorMessage("""Error loading model file %s: unknown file format"""%filename);
      return;
    # try to load the specified file
    busy = BusyIndicator();
    self.showMessage("""Reading %s file %s"""%(filetype,filename),3000);
    QApplication.flush();
    try:
      model = import_func(filename);
      model.setFilename(filename);
    except:
      busy = None;
      self.showErrorMessage("""Error loading '%s' file %s: %s"""%(filetype,filename,str(sys.exc_info()[1])));
      return;
    # set the layout
    if show:
      self.setLayout(self.LayoutImageModel);
    # add to content
    if merge and self.model:
      self.model.addSources(model.sources);
      self.showMessage("""Merged in %d sources from '%s' file %s"""%(len(model.sources),filetype,filename),3000);
      self.model.emitUpdate(SkyModel.UpdateAll);
    else:
      self.showMessage("""Loaded %d sources from '%s' file %s"""%(len(model.sources),filetype,filename),3000);
      self._display_filename = os.path.basename(filename);
      self.setModel(model);
      self._indicateModelUpdated(updated=False);
      # only set self.filename if an export function is available for this format. Otherwise set it to None, so that trying to save
      # the file results in a save-as operation (so that we don't save to a file in an unsupported format).
      self.filename = filename if export_func else None;

  def closeEvent (self,event):
    dprint(1,"closing");
    self._exiting = True;
    self.saveSizes();
    if not self.closeFile():
      self._exiting = False;
      event.ignore();
      return;
    self.skyplot.close();
    self.imgman.close();
    self.emit(SIGNAL("closing"));
    dprint(1,"invoking os._exit(0)");
    os._exit(0);
    QMainWindow.closeEvent(self,event);

  def _canCloseExistingModel (self):
    # save model if modified
    if self.model and self._model_updated:
      res = QMessageBox.question(self,"Closing sky model","<P>Model has been modified, would you like to save the changes?</P>",
                    QMessageBox.Save|QMessageBox.Discard|QMessageBox.Cancel,QMessageBox.Save);
      if res == QMessageBox.Cancel:
        return False;
      elif res == QMessageBox.Save:
        if not self.saveFile(confirm=False,overwrite=True):
          return False;
    # unload model images, unless we are already exiting anyway
    if not self._exiting:
      self.imgman.unloadModelImages();
    return True;

  def closeFile (self):
    if not self._canCloseExistingModel():
      return False;
    # close model
    self._display_filename = None;
    self.setModel(None);
    # set the layout
    self.setLayout(self.LayoutImage if self.imgman.getTopImage() else self.LayoutEmpty);
    return True;

  def saveFile (self,filename=None,confirm=False,overwrite=True,non_native=False):
    """Saves file using the specified 'filename'. If filename is None, uses current filename, if
    that is not set, goes to saveFileAs() to open dialog and get a filename.
    If overwrite=False, will ask for confirmation before overwriting an existing file.
    If non_native=False, will ask for confirmation before exporting in non-native format.
    If confirm=True, will ask for confirmation regardless.
    Returns True if saving succeeded, False on error (or if cancelled by user).
    """;
    if isinstance(filename,QStringList):
      filename = filename[0];
    filename = ( filename and str(filename) ) or self.filename;
    if filename is None:
      return self.saveFileAs();
    else:
      warning = '';
      # try to determine the file type
      filetype,import_func,export_func,doc = Tigger.Models.Formats.resolveFormat(filename,None);
      if export_func is None:
        self.showErrorMessage("""Error saving model file %s: unsupported output format"""%filename);
        return;
      if os.path.exists(filename) and not overwrite:
        warning += "<P>The file already exists and will be overwritten.</P>";
      if filetype != 'Tigger' and not non_native:
        warning += """<P>Please note that you are exporting the model using the external format '%s'.
              Source types, tags and other model features not supported by this
              format will be omitted during the export.</P>"""%filetype;
      # get confirmation
      if confirm or warning:
        dialog = QMessageBox.warning if warning else QMessageBox.question;
        if dialog(self,"Saving sky model","<P>Save model to %s?</P>%s"%(filename,warning),
                  QMessageBox.Save|QMessageBox.Cancel,QMessageBox.Save) != QMessageBox.Save:
          return False;
      busy = BusyIndicator();
      try:
        export_func(self.model,filename);
        self.model.setFilename(filename);
      except:
          busy = None;
          self.showErrorMessage("""Error saving model file %s: %s"""%(filename,str(sys.exc_info()[1])));
          return False;
      self.showMessage("""Saved model to file %s"""%filename,3000);
      self._display_filename = os.path.basename(filename);
      self._indicateModelUpdated(updated=False);
      self.filename = filename;
      return True;

  def saveFileAs (self,filename=None):
    """Saves file using the specified 'filename'. If filename is None, opens dialog to get a filename.
    Returns True if saving succeeded, False on error (or if cancelled by user).
    """;
    if filename is None:
      if not self._save_as_dialog:
          filters = ";;".join([  "%s (%s)"%(name," ".join(patterns)) for name,patterns,func in self._save_file_types ]);
          dialog = self._save_as_dialog = QFileDialog(self,"Save sky model",".",filters);
          dialog.setDefaultSuffix(ModelHTML.DefaultExtension);
          dialog.setFileMode(QFileDialog.AnyFile);
          dialog.setAcceptMode(QFileDialog.AcceptSave);
          dialog.setConfirmOverwrite(False);
          dialog.setModal(True);
          QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self.saveFileAs);
      return self._save_as_dialog.exec_() == QDialog.Accepted;
    # filename supplied, so save
    return self.saveFile(filename,confirm=False);

  def saveSelectionAs (self,filename=None,force=False):
    if not self.model:
      return;
    if filename is None:
      if not self._save_sel_as_dialog:
          filters = ";;".join([  "%s (%s)"%(name," ".join(patterns)) for name,patterns,func in self._save_file_types ]);
          dialog = self._save_sel_as_dialog = QFileDialog(self,"Save sky model",".",filters);
          dialog.setDefaultSuffix(ModelHTML.DefaultExtension);
          dialog.setFileMode(QFileDialog.AnyFile);
          dialog.setAcceptMode(QFileDialog.AcceptSave);
          dialog.setConfirmOverwrite(True);
          dialog.setModal(True);
          QObject.connect(dialog,SIGNAL("filesSelected(const QStringList &)"),self.saveSelectionAs);
      return self._save_sel_as_dialog.exec_() == QDialog.Accepted;
    # save selection
    if isinstance(filename,QStringList):
      filename = filename[0];
    filename= str(filename);
    selmodel = self.model.copy();
    sources = [ src for src in self.model.sources if src.selected ];
    if not sources:
      self.showErrorMessage("""You have not selected any sources to save.""");
      return;
    # try to determine the file type
    filetype,import_func,export_func,doc = Tigger.Models.Formats.resolveFormat(filename,None);
    if export_func is None:
      self.showErrorMessage("""Error saving model file %s: unsupported output format"""%filename);
      return;
    busy = BusyIndicator();
    try:
      export_func(self.model,filename,sources=sources);
    except:
      busy = None;
      self.showErrorMessage("""Error saving selection to model file %s: %s"""%(filename,str(sys.exc_info()[1])));
      return False;
    self.showMessage("""Wrote %d selected source%s to file %s"""%(len(selmodel.sources),"" if len(selmodel.sources)==1 else "s",filename),3000);
    pass;

  def addTagToSelection (self):
    if not hasattr(self,'_add_tag_dialog'):
      self._add_tag_dialog = Widgets.AddTagDialog(self,modal=True);
    self._add_tag_dialog.setTags(self.model.tagnames);
    self._add_tag_dialog.setValue(True);
    if self._add_tag_dialog.exec_() != QDialog.Accepted:
      return;
    tagname,value = self._add_tag_dialog.getTag();
    if tagname is None or value is None:
      return None;
    dprint(1,"tagging selected sources with",tagname,value);
    # tag selected sources
    for src in self.model.sources:
      if src.selected:
        src.setAttribute(tagname,value);
    # If tag is not new, set a UpdateSelectionOnly flag on the signal
    dprint(1,"adding tag to model");
    self.model.addTag(tagname);
    dprint(1,"recomputing totals");
    self.model.getTagGrouping(tagname).computeTotal(self.model.sources);
    dprint(1,"emitting update signal");
    what = SkyModel.UpdateSourceContent+SkyModel.UpdateTags+SkyModel.UpdateSelectionOnly;
    self.model.emitUpdate(what,origin=self);

  def removeTagsFromSelection (self):
    if not hasattr(self,'_remove_tag_dialog'):
      self._remove_tag_dialog = Widgets.SelectTagsDialog(self,modal=True,caption="Remove Tags",ok_button="Remove");
    # get set of all tags in selected sources
    tags = set();
    for src in self.model.sources:
      if src.selected:
        tags.update(src.getTagNames());
    if not tags:
      return;
    tags = list(tags);
    tags.sort();
    # show dialog
    self._remove_tag_dialog.setTags(tags);
    if self._remove_tag_dialog.exec_() != QDialog.Accepted:
      return;
    tags = self._remove_tag_dialog.getSelectedTags();
    if not tags:
      return;
    # ask for confirmation
    plural = (len(tags)>1 and "s") or "";
    if QMessageBox.question(self,"Removing tags","<P>Really remove the tag%s '%s' from selected sources?</P>"%(plural,"', '".join(tags)),
            QMessageBox.Yes|QMessageBox.No,QMessageBox.Yes) != QMessageBox.Yes:
      return;
    # remove the tags
    for src in self.model.sources:
      if src.selected:
        for tag in tags:
          src.removeAttribute(tag);
    # update model
    self.model.scanTags();
    self.model.initGroupings();
    # emit signal
    what = SkyModel.UpdateSourceContent+SkyModel.UpdateTags+SkyModel.UpdateSelectionOnly;
    self.model.emitUpdate(what,origin=self);

  def _indicateModelUpdated (self,what=None,origin=None,updated=True):
    """Marks model as updated.""";
    self._model_updated = updated;
    self.emit(SIGNAL("isUpdated"),updated);
    if self.model:
      self.setWindowTitle("Tigger - %s%s"%((self._display_filename or "(unnamed)"," (modified)" if updated else "")));