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 __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 __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 __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 __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 __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 __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;
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: </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;
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");
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 ]
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;
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();
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");
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;
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"]);
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: </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
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")
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"])
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 "")));
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 ];
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
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());
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 "")));