class DemoApp(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.bottomwidget = QWidget(self) self.bottomwidget.setMaximumHeight(200) self.bottomwidget.setMinimumHeight(0) self.glwidget = DemoWidget(self) self.mlayout = QVBoxLayout() self.setLayout(self.mlayout) self.mlayout.addWidget(self.glwidget) self.mlayout.addWidget(self.bottomwidget) self.mlayout.setSpacing(0) self.mlayout.setContentsMargins(0, 0, 0, 0) self.bottomwidget.hide() self.frame = self.glwidget def appendView(self, view): return self.glwidget.appendView(view) def appendAboutView(self, view): return self.glwidget.appendAboutView(view) def appendInitialView(self, view): return self.glwidget.appendInitialView(view)
def setupWidgets(self): """Override this.""" self.setCentralWidget(QWidget(self)) _layout = QVBoxLayout(self.centralWidget()) self.centralWidget().setLayout(_layout) _layout.setContentsMargins(0,0,0,0) self.centralWidget().layout().addWidget(QLabel("This is normal text", self))
class Dialog(QDialog): def __init__(self, title, widget=None, closeButton=True, keySequence=None, isDialog=False, icon=None): QDialog.__init__(self, ctx.mainScreen) self.setObjectName("dialog") self.isDialog = isDialog self.layout = QVBoxLayout() self.setLayout(self.layout) self.wlayout= QHBoxLayout() if icon: self.setStyleSheet("""QDialog QLabel{ margin-left:16px;margin-right:10px} QDialog#dialog {background-image:url(':/images/%s.png'); background-repeat:no-repeat; background-position: top left; padding-left:500px;} """ % icon) self.windowTitle = windowTitle(self, closeButton) self.setTitle(title) self.layout.setMargin(0) self.layout.addWidget(self.windowTitle) if widget: self.addWidget(widget) QObject.connect(widget, SIGNAL("finished(int)"), self.reject) QObject.connect(widget, SIGNAL("resizeDialog(int,int)"), self.resize) if closeButton: QObject.connect(self.windowTitle.pushButton, SIGNAL("clicked()"), self.reject) if keySequence: shortCut = QShortcut(keySequence, self) QObject.connect(shortCut, SIGNAL("activated()"), self.reject) QMetaObject.connectSlotsByName(self) self.resize(10,10) def setTitle(self, title): self.windowTitle.label.setText(title) def addWidget(self, widget): self.content = widget self.wlayout.addWidget(self.content) if self.isDialog: widget.setStyleSheet("QMessageBox { background:none }") self.layout.addItem(QSpacerItem(10, 10, QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)) self.layout.setContentsMargins(0, 0, 0, 8) self.layout.addLayout(self.wlayout) def setCentered(self): self.move(ctx.mainScreen.width()/2 - self.width()/2, ctx.mainScreen.height()/2 - self.height()/2) def exec_(self): QTimer.singleShot(0, self.setCentered) return QDialog.exec_(self)
def __init__(self, parent, *args): QWidget.__init__(self, parent, *args) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) lo = QVBoxLayout(self) lo.setContentsMargins(0, 0, 0, 0) lo1 = QHBoxLayout() lo.addLayout(lo1) lo1.setContentsMargins(0, 0, 0, 0) lbl = QLabel(QString("<nobr><b>Source groupings:</b></nobr>"), self) lo1.addWidget(lbl, 0) lo1.addStretch(1) # add show/hide button self._showattrbtn = QPushButton(self) self._showattrbtn.setMinimumWidth(256) lo1.addWidget(self._showattrbtn, 0) lo1.addStretch() QObject.connect(self._showattrbtn, SIGNAL("clicked()"), self._togglePlotControlsVisibility) # add table self.table = QTableWidget(self) lo.addWidget(self.table) QObject.connect(self.table, SIGNAL("cellChanged(int,int)"), self._valueChanged) self.table.setSelectionMode(QTableWidget.NoSelection) # setup basic columns self.table.setColumnCount(6 + len(self.EditableAttrs)) for i, label in enumerate( ("grouping", "total", "selection", "list", "plot", "style")): self.table.setHorizontalHeaderItem(i, QTableWidgetItem(label)) self.table.horizontalHeader().setSectionHidden(self.ColApply, True) # setup columns for editable grouping attributes for i, attr in self.AttrByCol.items(): self.table.setHorizontalHeaderItem( i, QTableWidgetItem(PlotStyles.StyleAttributeLabels[attr])) self.table.horizontalHeader().setSectionHidden(i, True) self.table.verticalHeader().hide() # other internal init self._attrs_shown = False self._togglePlotControlsVisibility() self.model = None self._setting_model = False self._currier = PersistentCurrier() # row of 'selected' grouping self._irow_selgroup = 0
def __init__(self, parent, *args): QWidget.__init__(self, parent, *args) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) lo = QVBoxLayout(self) lo.setContentsMargins(0, 0, 0, 0) lo1 = QHBoxLayout() lo.addLayout(lo1) lo1.setContentsMargins(0, 0, 0, 0) lbl = QLabel(QString("<nobr><b>Source groupings:</b></nobr>"), self) lo1.addWidget(lbl, 0) lo1.addStretch(1) # add show/hide button self._showattrbtn = QPushButton(self) self._showattrbtn.setMinimumWidth(256) lo1.addWidget(self._showattrbtn, 0) lo1.addStretch() QObject.connect(self._showattrbtn, SIGNAL("clicked()"), self._togglePlotControlsVisibility) # add table self.table = QTableWidget(self) lo.addWidget(self.table) QObject.connect(self.table, SIGNAL("cellChanged(int,int)"), self._valueChanged) self.table.setSelectionMode(QTableWidget.NoSelection) # setup basic columns self.table.setColumnCount(6 + len(self.EditableAttrs)) for i, label in enumerate(("grouping", "total", "selection", "list", "plot", "style")): self.table.setHorizontalHeaderItem(i, QTableWidgetItem(label)) self.table.horizontalHeader().setSectionHidden(self.ColApply, True) # setup columns for editable grouping attributes for i, attr in self.AttrByCol.items(): self.table.setHorizontalHeaderItem(i, QTableWidgetItem(PlotStyles.StyleAttributeLabels[attr])) self.table.horizontalHeader().setSectionHidden(i, True) self.table.verticalHeader().hide() # other internal init self._attrs_shown = False self._togglePlotControlsVisibility() self.model = None self._setting_model = False self._currier = PersistentCurrier() # row of 'selected' grouping self._irow_selgroup = 0
class MainWidget(QWidget): def __init__(self): QWidget.__init__(self) # define periodic table widget for element selection self.periodicTableWidget = widgets.PeriodicTableDialog() # initial molecule Zmatrix (can be empty) # self.inp = [] self.inp = [['H'], ['O', 1, 0.9], ['O', 2, 1.4, 1, 105.], ['H', 3, 0.9, 2, 105., 1, 120.]] self.atomList = [] self.highList = [] self.labelList = [] self.fast = False # define & initialize ZMatModel that will contain Zmatrix data self.ZMatModel = QStandardItemModel(len(self.inp), 7, self) self.ZMatTable = QTableView(self) self.ZMatTable.setModel(self.ZMatModel) self.ZMatTable.setFixedWidth(325) #self.ZMatTable.installEventFilter(self) #self.ZMatModel.installEventFilter(self) self.ZMatModel.setHorizontalHeaderLabels(['atom','','bond','','angle','','dihedral']) for j, width in enumerate([40, 22, 65, 22, 65, 22, 65]): self.ZMatTable.setColumnWidth(j, width) # populate the ZMatModel self.populateZMatModel() #define Menu bar menus and their actions self.menuBar = QMenuBar(self) fileMenu = self.menuBar.addMenu('&File') editMenu = self.menuBar.addMenu('&Edit') viewMenu = self.menuBar.addMenu('&View') measureMenu = self.menuBar.addMenu('&Measure') helpMenu = self.menuBar.addMenu('&Help') readZmatAction = QAction('&Read &ZMat', self) readZmatAction.setShortcut('Ctrl+O') readZmatAction.setStatusTip('Read Zmat from file') readZmatAction.triggered.connect(self.readZmat) fileMenu.addAction(readZmatAction) readXYZAction = QAction('&Read &XYZ', self) readXYZAction.setShortcut('Ctrl+Shift+O') readXYZAction.setStatusTip('Read XYZ from file') readXYZAction.triggered.connect(self.readXYZ) fileMenu.addAction(readXYZAction) readGaussianAction = QAction('&Read &Gaussian log', self) readGaussianAction.setShortcut('Ctrl+G') readGaussianAction.setStatusTip('Read Gaussian log file') readGaussianAction.triggered.connect(self.readGaussian) fileMenu.addAction(readGaussianAction) writeZmatAction = QAction('&Write &ZMat', self) writeZmatAction.setShortcut('Ctrl+S') writeZmatAction.setStatusTip('Write Zmat to file') writeZmatAction.triggered.connect(self.writeZmat) fileMenu.addAction(writeZmatAction) writeXYZAction = QAction('&Write &XYZ', self) writeXYZAction.setShortcut('Ctrl+Shift+S') writeXYZAction.setStatusTip('Write XYZ from file') writeXYZAction.triggered.connect(self.writeXYZ) fileMenu.addAction(writeXYZAction) exitAction = QAction('&Exit', self) exitAction.setShortcut('Ctrl+Q') exitAction.setStatusTip('Exit application') exitAction.triggered.connect(qApp.quit) fileMenu.addAction(exitAction) addRowAction = QAction('&Add &row', self) addRowAction.setShortcut('Ctrl+R') addRowAction.setStatusTip('Add row to ZMatrix') addRowAction.triggered.connect(self.addRow) editMenu.addAction(addRowAction) deleteRowAction = QAction('&Delete &row', self) deleteRowAction.setShortcut('Ctrl+Shift+R') deleteRowAction.setStatusTip('Delete row from ZMatrix') deleteRowAction.triggered.connect(self.deleteRow) editMenu.addAction(deleteRowAction) addAtomAction = QAction('&Add &atom', self) addAtomAction.setShortcut('Ctrl+A') addAtomAction.setStatusTip('Add atom to ZMatrix') addAtomAction.triggered.connect(self.buildB) editMenu.addAction(addAtomAction) drawModeMenu = QMenu('Draw mode', self) viewMenu.addMenu(drawModeMenu) fastDrawAction = QAction('&Fast draw', self) fastDrawAction.triggered.connect(self.fastDraw) normalDrawAction = QAction('&Normal draw', self) normalDrawAction.triggered.connect(self.normalDraw) drawModeMenu.addAction(normalDrawAction) drawModeMenu.addAction(fastDrawAction) clearHighlightsAction = QAction('&Clear selection', self) clearHighlightsAction.setShortcut('Ctrl+C') clearHighlightsAction.setStatusTip('Clear highlighted atoms') clearHighlightsAction.triggered.connect(self.clearHighlights) viewMenu.addAction(clearHighlightsAction) clearLabelsAction = QAction('&Clear labels', self) clearLabelsAction.setShortcut('Ctrl+Alt+C') clearLabelsAction.setStatusTip('Clear labels') clearLabelsAction.triggered.connect(self.clearLabels) viewMenu.addAction(clearLabelsAction) clearUpdateViewAction = QAction('&Clear selection and labels', self) clearUpdateViewAction.setShortcut('Ctrl+Shift+C') clearUpdateViewAction.setStatusTip('Clear highlighted atoms and labels') clearUpdateViewAction.triggered.connect(self.clearUpdateView) viewMenu.addAction(clearUpdateViewAction) self.showGaussAction = QAction('Show &Gaussian geometry optimization', self) self.showGaussAction.setShortcut('Ctrl+G') self.showGaussAction.setStatusTip('Show Gaussian geometry optimization plots for energy, force and displacement.') self.showGaussAction.setEnabled(False) self.showGaussAction.triggered.connect(self.showGauss) viewMenu.addAction(self.showGaussAction) self.showFreqAction = QAction('Show &IR frequency plot', self) self.showFreqAction.setShortcut('Ctrl+I') self.showFreqAction.setStatusTip('Show Gaussian calculated IR frequency plot.') self.showFreqAction.setEnabled(False) self.showFreqAction.triggered.connect(self.showFreq) viewMenu.addAction(self.showFreqAction) measureDistanceAction = QAction('&Measure &distance', self) measureDistanceAction.setShortcut('Ctrl+D') measureDistanceAction.setStatusTip('Measure distance between two atoms') measureDistanceAction.triggered.connect(self.measureDistanceB) measureMenu.addAction(measureDistanceAction) measureAngleAction = QAction('&Measure &angle', self) measureAngleAction.setShortcut('Ctrl+Shift+D') measureAngleAction.setStatusTip('Measure angle between three atoms') measureAngleAction.triggered.connect(self.measureAngleB) measureMenu.addAction(measureAngleAction) aboutAction = QAction('&About', self) aboutAction.setStatusTip('About this program...') aboutAction.triggered.connect(self.about) helpMenu.addAction(aboutAction) aboutQtAction = QAction('&About Qt', self) aboutQtAction.setStatusTip('About Qt...') aboutQtAction.triggered.connect(self.aboutQt) helpMenu.addAction(aboutQtAction) # define GL widget that displays the 3D molecule model self.window = widgets.MyGLView() self.window.installEventFilter(self) self.window.setMinimumSize(500, 500) #self.window.setBackgroundColor((50, 0, 10)) self.updateView() self.gaussianPlot = GraphicsLayoutWidget() self.gaussianPlot.resize(750, 250) self.gaussianPlot.setWindowTitle('Gaussian geometry optimization') #self.gaussianPlot.setAspectLocked(True) #self.gaussianPlot.addLayout(rowspan=3, colspan=1) self.FreqModel = QStandardItemModel(1, 3, self) self.freqTable = QTableView(self) self.freqTable.setModel(self.FreqModel) self.freqTable.setMinimumWidth(240) self.freqTable.installEventFilter(self) self.FreqModel.installEventFilter(self) self.FreqModel.setHorizontalHeaderLabels(['Frequency','IR Intensity','Raman Intensity']) for j, width in enumerate([80, 80, 80]): self.freqTable.setColumnWidth(j, width) self.freqWidget = QWidget() self.freqWidget.setWindowTitle('IR frequency plot & table') self.freqWidget.resize(800, 400) self.freqWidget.layout = QHBoxLayout(self.freqWidget) self.freqWidget.layout.setSpacing(1) self.freqWidget.layout.setContentsMargins(1, 1, 1, 1) self.freqPlot = GraphicsLayoutWidget() self.freqWidget.layout.addWidget(self.freqPlot) self.freqWidget.layout.addWidget(self.freqTable) self.freqTable.clicked.connect(self.freqCellClicked) # define other application parts self.statusBar = QStatusBar(self) self.fileDialog = QFileDialog(self) # define application layout self.layout = QVBoxLayout(self) self.layout.setSpacing(1) self.layout.setContentsMargins(1, 1, 1, 1) self.layout1 = QHBoxLayout() self.layout1.setSpacing(1) self.layout1.addWidget(self.ZMatTable) self.layout1.addWidget(self.window) self.layout.addWidget(self.menuBar) self.layout.addLayout(self.layout1) self.layout.addWidget(self.statusBar) self.adjustSize() self.setWindowTitle('Moldy') iconPath = 'icon.png' icon = QIcon(iconPath) icon.addFile(iconPath, QSize(16, 16)) icon.addFile(iconPath, QSize(24, 24)) icon.addFile(iconPath, QSize(32, 32)) icon.addFile(iconPath, QSize(48, 48)) icon.addFile(iconPath, QSize(256, 256)) self.setWindowIcon(icon) # start monitoring changes in the ZMatModel self.ZMatModel.dataChanged.connect(self.clearUpdateView) # run and show the application def run(self): self.show() self.ZMatTable.clicked.connect(self.ZMatCellClicked) qt_app.instance().aboutToQuit.connect(self.deleteGLwidget) qt_app.exec_() # fill the ZMatModel with initial data from 'self.inp' def populateZMatModel(self): self.ZMatModel.removeRows(0, self.ZMatModel.rowCount()) for i, row in enumerate(self.inp): for j, cell in enumerate(row): item = QStandardItem(str(cell)) self.ZMatModel.setItem(i, j, item) # some cells should not be editable, they are disabled for i in range(min(len(self.inp), 3)): for j in range(2*i+1, 7): self.ZMatModel.setItem(i, j, QStandardItem()) self.ZMatModel.item(i, j).setBackground(QColor(150,150,150)) self.ZMatModel.item(i, j).setFlags(Qt.ItemIsEnabled) def populateFreqModel(self): self.FreqModel.removeRows(0, self.FreqModel.rowCount()) for i, row in enumerate(zip(self.vibfreqs, self.vibirs, self.vibramans)): for j, cell in enumerate(row): item = QStandardItem(str(cell)) self.FreqModel.setItem(i, j, item) # add a row to the bottom of the ZMatModel def addRow(self): # temporarily stop updating the GL window self.ZMatModel.dataChanged.disconnect(self.clearUpdateView) row = self.ZMatModel.rowCount() self.ZMatModel.insertRow(row) # some cells should not be editable if row < 3: for j in range(2*row+1, 7): self.ZMatModel.setItem(row, j, QStandardItem()) self.ZMatModel.item(row, j).setBackground(QColor(150,150,150)) self.ZMatModel.item(row, j).setFlags(Qt.ItemIsEnabled) # restart GL window updating self.ZMatModel.dataChanged.connect(self.clearUpdateView) self.statusBar.clearMessage() self.statusBar.showMessage('Added 1 row.', 3000) # delete the last row of the ZMatModel def deleteRow(self): xyz = [list(vi) for vi in list(v)] atoms = [str(elements[e]) for e in elems] oldLen = self.ZMatModel.rowCount() idxs = sorted(set(idx.row() for idx in self.ZMatTable.selectedIndexes()), reverse=True) newLen = oldLen - len(idxs) if newLen == oldLen: self.ZMatModel.removeRow(self.ZMatModel.rowCount()-1) else: self.ZMatModel.dataChanged.disconnect(self.clearUpdateView) for idx in idxs: self.ZMatModel.removeRow(idx) if idx < 3: for i in range(idx, min(3, newLen)): for j in range(2*i+1, 7): self.ZMatModel.setItem(i, j, QStandardItem()) self.ZMatModel.item(i, j).setBackground(QColor(150,150,150)) self.ZMatModel.item(i, j).setFlags(Qt.ItemIsEnabled) if len(xyz) > idx: xyz.pop(idx) atoms.pop(idx) self.inp = xyz2zmat(xyz, atoms) self.populateZMatModel() for i in reversed(self.highList): self.window.removeItem(i[1]) self.highList = [] self.ZMatModel.dataChanged.connect(self.clearUpdateView) self.updateView() self.statusBar.clearMessage() if idxs: self.statusBar.showMessage('Deleted row(s): '+str([i+1 for i in idxs]), 3000) else: self.statusBar.showMessage('Deleted last row.', 3000) # show the periodic table widget def periodicTable(self): self.statusBar.clearMessage() self.statusBar.showMessage('Select element from periodic table.') self.periodicTableWidget.exec_() selection = self.periodicTableWidget.selection() return selection # import molecule with zmatrix coordinates def readZmat(self): self.ZMatModel.dataChanged.disconnect(self.clearUpdateView) filename = self.fileDialog.getOpenFileName(self, 'Open file', expanduser('~'), '*.zmat;;*.*') self.inp = [] self.populateZMatModel() if filename: with open(filename, 'r') as f: next(f) next(f) for row in f: self.inp.append(row.split()) f.close() self.populateZMatModel() self.ZMatModel.dataChanged.connect(self.clearUpdateView) self.updateView() self.statusBar.clearMessage() self.statusBar.showMessage('Read molecule from '+filename+'.', 5000) self.showGaussAction.setEnabled(False) self.showFreqAction.setEnabled(False) # import molecule with xyz coordinates def readXYZ(self): self.ZMatModel.dataChanged.disconnect(self.clearUpdateView) filename = self.fileDialog.getOpenFileName(self, 'Open file', expanduser('~'), '*.xyz;;*.*') xyz = [] elems = [] self.inp = [] self.populateZMatModel() if filename: with open(filename, 'r') as f: next(f) next(f) for row in f: rs = row.split() if len(rs) == 4: elems.append(rs[0]) xyz.append([float(f) for f in rs[1:]]) f.close() self.inp = xyz2zmat(xyz, elems) self.populateZMatModel() #print(elems) self.ZMatModel.dataChanged.connect(self.clearUpdateView) self.updateView() self.statusBar.clearMessage() self.statusBar.showMessage('Read molecule from '+filename+'.', 5000) self.showGaussAction.setEnabled(False) self.showFreqAction.setEnabled(False) # import Gaussian log file def readGaussian(self): global vsShifted self.ZMatModel.dataChanged.disconnect(self.clearUpdateView) filename = self.fileDialog.getOpenFileName(self, 'Open file', expanduser('~'), '*.log;;*.*') if filename: self.gaussianPlot.clear() self.inp = [] self.populateZMatModel() file = ccopen(filename) data = file.parse().getattributes() self.natom = data['natom'] self.atomnos = data['atomnos'].tolist() self.atomsymbols = [ str(elements[e]) for e in self.atomnos ] self.atomcoords = data['atomcoords'].tolist() self.scfenergies = data['scfenergies'].tolist() self.geovalues = data['geovalues'].T.tolist() self.geotargets = data['geotargets'].tolist() if 'vibfreqs' in data.keys(): self.vibfreqs = data['vibfreqs'] #print(self.vibfreqs) self.vibirs = data['vibirs'] #print(self.vibirs) #print(data.keys()) if 'vibramans' in data.keys(): self.vibramans = data['vibramans'] else: self.vibramans = [''] * len(self.vibirs) self.vibdisps = data['vibdisps'] #print(self.vibdisps) self.inp = xyz2zmat(self.atomcoords[0], self.atomsymbols) self.populateZMatModel() titles = ['SCF Energies', 'RMS & Max Forces', 'RMS & Max Displacements'] for i in range(3): self.gaussianPlot.addPlot(row=1, col=i+1) plot = self.gaussianPlot.getItem(1, i+1) plot.setTitle(title=titles[i]) if i == 0: c = ['c'] x = [0] y = [self.scfenergies] else: c = ['r', 'y'] x = [0, 0] y = [self.geovalues[2*i-2], self.geovalues[2*i-1]] targety = [self.geotargets[2*i-2], self.geotargets[2*i-1]] plot.clear() plot.maxData = plot.plot(y[0], symbol='o', symbolPen=c[0], symbolBrush=c[0], pen=c[0], symbolSize=5, pxMode=True, antialias=True, autoDownsample=False) plot.highlight=plot.plot(x, [ yy[0] for yy in y ], symbol='o', symbolPen='w', symbolBrush=None, pen=None, symbolSize=15, pxMode=True, antialias=True, autoDownsample=False) plot.maxData.sigPointsClicked.connect(self.gausclicked) if i > 0: for j in range(2): plot.addLine(y=np.log10(targety[j]), pen=mkPen((255, 255*j, 0, int(255/2)), width=1)) plot.RMSData=plot.plot(y[1], symbol='o', symbolPen=c[1], symbolBrush=c[1], pen=c[1], symbolSize=5, pxMode=True, antialias=True, autoDownsample=False) plot.RMSData.sigPointsClicked.connect(self.gausclicked) plot.setLogMode(y=True) self.showGauss() self.updateView() self.statusBar.clearMessage() self.statusBar.showMessage('Read molecule from '+filename+'.', 5000) self.ZMatModel.dataChanged.connect(self.clearUpdateView) if self.natom: self.showGaussAction.setEnabled(True) if 'vibfreqs' in data.keys(): self.showFreqAction.setEnabled(True) # populate the FreqModel self.populateFreqModel() self.freqPlot.clear() irPlot = self.freqPlot.addPlot(row=1, col=1) irPlot.clear() minFreq = np.min(self.vibfreqs) maxFreq = np.max(self.vibfreqs) maxInt = np.max(self.vibirs) x = np.sort(np.concatenate([np.linspace(minFreq-100, maxFreq+100, num=1000), self.vibfreqs])) y = x*0 for f,i in zip(self.vibfreqs, self.vibirs): y += lorentzv(x, f, 2*np.pi, i) #xy = np.array([np.concatenate([x, np.array(self.vibfreqs)]), np.concatenate([y, np.array(self.vibirs)])]).T #xysort = xy[xy[:,0].argsort()] irPlot.maxData = irPlot.plot(x, y, antialias=True) markers = ErrorBarItem(x=self.vibfreqs, y=self.vibirs, top=maxInt/30, bottom=None, pen='r') irPlot.addItem(markers) self.showFreq() #self.vibdisps = np.append(self.vibdisps, [np.mean(self.vibdisps, axis=0)], axis=0) maxt = 100 vsShifted = np.array([ [ vs + self.vibdisps[i]*np.sin(t*2*np.pi/maxt)/3 for t in range(maxt) ] for i in range(len(self.vibfreqs)) ]) else: self.showFreqAction.setEnabled(False) self.freqWidget.hide() def showGauss(self): self.gaussianPlot.show() def showFreq(self): self.freqWidget.show() # export Zmatrix to csv def writeZmat(self): zm = model2list(self.ZMatModel) filename = self.fileDialog.getSaveFileName(self, 'Save file', expanduser('~')+'/'+getFormula(list(list(zip(*zm))[0]))+'.zmat', '*.zmat;;*.*') try: filename except NameError: pass else: if filename: writeOutput(zm, filename) self.statusBar.clearMessage() self.statusBar.showMessage('Wrote molecule to '+filename+'.', 5000) # export XYZ coordinates to csv def writeXYZ(self): xyz = [] zm = model2list(self.ZMatModel) for i in range(len(v)): xyz.append(np.round(v[i], 7).tolist()) xyz[i][:0] = zm[i][0] if len(v) > 0: formula = getFormula(list(list(zip(*xyz))[0])) else: formula = 'moldy_output' filename = self.fileDialog.getSaveFileName(self, 'Save file', expanduser('~')+'/'+formula+'.xyz', '*.xyz;;*.*') try: filename except NameError: pass else: if filename: writeOutput(xyz, filename) self.statusBar.clearMessage() self.statusBar.showMessage('Wrote molecule to '+filename+'.', 5000) # redraw the 3D molecule in GL widget def updateView(self): global r global c global v global vs global elems global nelems data = model2list(self.ZMatModel) try: # create a list with element coordinates v = zmat2xyz(data) except (AssertionError, IndexError, ZMError): pass else: # clear the screen before redraw for item in reversed(self.window.items): self.window.removeItem(item) # create a second coordinate list 'vs' that is centered in the GL view self.atomList = [] if len(v) > 0: shift = np.mean(v, axis=0) vs = np.add(v, -shift) elems = [ 1 + next((i for i, sublist in enumerate(colors) if row[0] in sublist), -1) for row in data ] nelems = len(elems) # define molecule radii and colors r = [] c = [] for i in elems: r.append(elements[i].covalent_radius) c.append(colors[i-1][-1]) # draw atoms for i in range(nelems): addAtom(self.window, i, r, vs, c, fast=self.fast) self.atomList.append([i, self.window.items[-1]]) #print(self.atomList) # draw bonds where appropriate combs = list(itertools.combinations(range(nelems), 2)) bonds = [] for i in combs: bonds.append(addBond(self.window, i[0], i[1], r, vs, c, fast=self.fast)) if self.fast: bondedAtoms = set(filter((None).__ne__, flatten(bonds))) for i in set(range(nelems)) - bondedAtoms: addUnbonded(self.window, i, vs, c) self.atomList[i][1]=self.window.items[-1] #print(self.atomList) for i in self.highList: self.window.addItem(i[1]) for i in self.labelList: self.window.addItem(i) if len(v) > 1: maxDim = float('-inf') for dim in v.T: span = max(dim)-min(dim) if span > maxDim: maxDim = span else: maxDim = 2 self.window.setCameraPosition(distance=maxDim*1.5+1) global index index = 0 def updateFreq(self): global vsShifted, index, r, c index += 1 index = index % len(vsShifted[0]) #print(index) #print(vsShifted[index]) for item in reversed(self.window.items): self.window.removeItem(item) for i in range(nelems): addAtom(self.window, i, r, vsShifted[self.freqIndex, index], c, fast=self.fast) self.atomList.append([i, self.window.items[-1]]) combs = itertools.combinations(range(nelems), 2) bonds = [] for i in combs: bonds.append(addBond(self.window, i[0], i[1], r, vsShifted[self.freqIndex, index], c, fast=self.fast)) if self.fast: bondedAtoms = set(filter((None).__ne__, flatten(bonds))) for i in set(range(nelems)) - bondedAtoms: addUnbonded(self.window, i, vsShifted[self.freqIndex, index], c) self.atomList[i][1]=self.window.items[-1] # detect mouse clicks in GL window and process them def eventFilter(self, obj, event): if obj == self.window: if event.type() == event.MouseButtonPress: itms = obj.itemsAt((event.pos().x()-2, event.pos().y()-2, 4, 4)) if len(itms): self.highlight(obj, [itms[0]]) elif len(self.atomList) == 0: self.build() # also do the default click action return super(MainWidget, self).eventFilter(obj, event) def ZMatCellClicked(self): idxs = sorted(set(idx.row() for idx in self.ZMatTable.selectedIndexes()), reverse=True) itms = [] if self.highList: highIdx = list(np.array(self.highList).T[0]) for idx in idxs: if self.highList and idx in highIdx: itms.append(self.highList[highIdx.index(idx)][1]) elif len(self.atomList) > idx: itms.append(self.atomList[idx][1]) self.highlight(self.window, itms) def freqCellClicked(self): global vsShifted self.timer = QTimer() self.timer.setInterval(30) self.timer.timeout.connect(self.updateFreq) idxs = [ idx.row() for idx in self.freqTable.selectedIndexes() ] if len(idxs) == 1: self.freqIndex = idxs[0] self.timer.stop() self.timer.timeout.connect(self.updateFreq) try: self.ZMatModel.dataChanged.disconnect(self.clearUpdateView) except TypeError: pass self.timer.start() if len(idxs) != 1: self.timer.stop() self.freqTable.clearSelection() self.timer.timeout.disconnect(self.updateFreq) self.ZMatModel.dataChanged.connect(self.clearUpdateView) self.clearUpdateView() def gausclicked(self, item, point): itemdata = item.scatter.data points = [ row[7] for row in itemdata ] idx = points.index(point[0]) for i in range(3): if i == 0: x = [idx] y = [self.scfenergies[idx]] else: x = [idx, idx] y = [self.geovalues[2*i-2][idx], self.geovalues[2*i-1][idx]] plot = self.gaussianPlot.getItem(1, i+1) plot.removeItem(plot.highlight) plot.highlight=plot.plot(x, y, symbol='o', symbolPen='w', symbolBrush=None, pen=None, symbolSize=15, pxMode=True, antialias=True, autoDownsample=False) self.ZMatModel.dataChanged.disconnect(self.clearUpdateView) self.inp = [] self.populateZMatModel() self.inp = xyz2zmat(self.atomcoords[min(idx, len(self.atomcoords)-1)], self.atomsymbols) self.populateZMatModel() self.ZMatModel.dataChanged.connect(self.clearUpdateView) self.updateView() def highlight(self, obj, itms): for itm in itms: idx = next((i for i, sublist in enumerate(self.atomList) if itm in sublist), -1) #print(idx) if idx != -1: addAtom(obj, idx, r, vs, c, opt='highlight', fast=self.fast) self.highList.append([idx, obj.items[-1]]) self.ZMatTable.selectRow(idx) idx = next((i for i, sublist in enumerate(self.highList) if itm in sublist), -1) if idx != -1: obj.removeItem(self.highList[idx][1]) self.highList.pop(idx) self.ZMatTable.clearSelection() self.statusBar.clearMessage() if len(self.highList) > 0: idxs = np.asarray(self.highList).T[0] selected = [] for i in idxs: selected.append(str(i+1)+str(elements[elems[i]])) self.statusBar.showMessage('Selected atoms: '+str(selected), 5000) def buildB(self): try: nelems except NameError: self.build() else: if len(self.highList) <= min(nelems, 3): diff = min(nelems, 3) - len(self.highList) if diff != 0: self.statusBar.clearMessage() self.statusBar.showMessage('Please select '+str(diff)+' more atom(s).') else: self.build() else: self.statusBar.clearMessage() self.statusBar.showMessage('Too many atoms selected.') def build(self): selection = self.periodicTable() row = self.ZMatModel.rowCount() self.addRow() self.ZMatModel.dataChanged.disconnect(self.clearUpdateView) newSymbol = selection[1] newData = [newSymbol] if len(self.highList) >= 1: newBond = round(2.1*gmean([ elements[e].covalent_radius for e in [selection[0], elems[self.highList[0][0]]] ]), 4) newData.append(self.highList[0][0]+1) newData.append(newBond) if len(self.highList) >= 2: newAngle = 109.4712 newData.append(self.highList[1][0]+1) newData.append(newAngle) if len(self.highList) == 3: newDihedral = 120. newData.append(self.highList[2][0]+1) newData.append(newDihedral) for j, cell in enumerate(newData): item = QStandardItem(str(cell)) self.ZMatModel.setItem(row, j, item) self.highList = [] self.ZMatModel.dataChanged.connect(self.clearUpdateView) self.updateView() def measureDistanceB(self): sel = len(self.highList) if sel <= 2: if sel < 2: self.statusBar.clearMessage() self.statusBar.showMessage('Please select '+str(2-sel)+' more atom(s).') else: self.measureDistance() else: self.statusBar.clearMessage() self.statusBar.showMessage('Too many atoms selected.') def measureDistance(self): pts = [] for pt in self.highList: pts.append(vs[pt[0]]) pts = np.array(pts) self.clearHighlights() line = gl.GLLinePlotItem(pos=pts, color=(0., 1., 0., 1.), width=3) self.window.addItem(line) self.labelList.append(line) q = pts[1]-pts[0] dist = round(np.sqrt(np.dot(q, q)), 4) self.window.labelPos.append(np.mean(pts[0:2], axis=0)) self.window.labelText.append(str(dist)) self.statusBar.clearMessage() self.statusBar.showMessage('Measured distance: '+str(dist)+' A.', 3000) def measureAngleB(self): sel = len(self.highList) if sel <= 3: if sel < 3: self.statusBar.clearMessage() self.statusBar.showMessage('Please select '+str(3-sel)+' more atom(s).') else: self.measureAngle() else: self.statusBar.clearMessage() self.statusBar.showMessage('Too many atoms selected.') def measureAngle(self): pts = [] for pt in self.highList: pts.append(vs[pt[0]]) pts = np.array(pts) q = pts[1]-pts[0] r = pts[2]-pts[0] q_u = q / np.sqrt(np.dot(q, q)) r_u = r / np.sqrt(np.dot(r, r)) angle = round(degrees(acos(np.dot(q_u, r_u))), 1) srange = np.array([slerp(q, r, t) for t in np.arange(0.0, 13/12, 1/12)]) self.clearHighlights() for i in range(12): mesh = gl.MeshData(np.array([[0,0,0],srange[i],srange[i+1]])) tri = gl.GLMeshItem(meshdata=mesh, smooth=False, computeNormals=False, color=(0.3, 1., 0.3, 0.5), glOptions=('translucent')) tri.translate(pts[0][0], pts[0][1], pts[0][2]) self.window.addItem(tri) self.labelList.append(tri) self.window.labelPos.append(slerp(q, r, 0.5)+pts[0]) self.window.labelText.append(str(angle)) self.statusBar.clearMessage() self.statusBar.showMessage('Measured angle: '+str(angle)+'°', 3000) def clearLabels(self): self.window.labelPos = [] self.window.labelText = [] self.labelList = [] self.updateView() def clearHighlights(self): for item in reversed(self.highList): self.window.removeItem(item[1]) self.highList = [] self.updateView() def clearUpdateView(self): self.window.labelPos = [] self.window.labelText = [] self.labelList = [] for item in reversed(self.highList): self.window.removeItem(item[1]) self.highList = [] self.updateView() #print(self.highList) def fastDraw(self): if not self.fast: self.fast = True self.updateView() def normalDraw(self): if self.fast: self.fast = False self.updateView() def about(self): QMessageBox.about(self, 'About moldy', 'moldy beta 15. 9. 2015') def aboutQt(self): QMessageBox.aboutQt(self, 'About Qt') def deleteGLwidget(self): self.window.setParent(None) del self.window
class ImageManager(QWidget): """An ImageManager manages a stack of images (and associated ImageControllers)""" def __init__(self, *args): QWidget.__init__(self, *args) # init layout self._lo = QVBoxLayout(self) self._lo.setContentsMargins(0, 0, 0, 0) self._lo.setSpacing(0) # init internal state self._currier = PersistentCurrier() self._z0 = 0; # z-depth of first image, the rest count down from it self._updating_imap = False self._locked_display_range = False self._imagecons = [] self._imagecon_loadorder = [] self._center_image = None self._plot = None self._border_pen = None self._drawing_key = None self._load_image_dialog = None self._model_imagecons = set() # init menu and standard actions self._menu = QMenu("&Image", self) qag = QActionGroup(self) # exclusive controls for plotting topmost or all images self._qa_plot_top = qag.addAction("Display topmost image only") self._qa_plot_all = qag.addAction("Display all images") self._qa_plot_top.setCheckable(True) self._qa_plot_all.setCheckable(True) self._qa_plot_top.setChecked(True) QObject.connect(self._qa_plot_all, SIGNAL("toggled(bool)"), self._displayAllImages) self._closing = False self._qa_load_clipboard = None self._clipboard_mode = QClipboard.Clipboard QObject.connect(QApplication.clipboard(), SIGNAL("changed(QClipboard::Mode)"), self._checkClipboardPath) # populate the menu self._repopulateMenu() def close(self): dprint(1, "closing Manager") self._closing = True for ic in self._imagecons: ic.close() def loadImage(self, filename=None, duplicate=True, to_top=True, model=None): """Loads image. Returns ImageControlBar object. If image is already loaded: returns old ICB if duplicate=False (raises to top if to_top=True), or else makes a new control bar. If model is set to a source name, marks the image as associated with a model source. These can be unloaded en masse by calling unloadModelImages(). """ if filename is None: if not self._load_image_dialog: dialog = self._load_image_dialog = QFileDialog(self, "Load FITS image", ".", "FITS images (%s);;All files (*)" % (" ".join( ["*" + ext for ext in FITS_ExtensionList]))) dialog.setFileMode(QFileDialog.ExistingFile) dialog.setModal(True) QObject.connect(dialog, SIGNAL("filesSelected(const QStringList &)"), self.loadImage) self._load_image_dialog.exec_() return None if isinstance(filename, QStringList): filename = filename[0] filename = str(filename) # report error if image does not exist if not os.path.exists(filename): self.showErrorMessage("""FITS image %s does not exist.""" % filename) return None # see if image is already loaded if not duplicate: for ic in self._imagecons: if ic.getFilename() and os.path.samefile(filename, ic.getFilename()): if to_top: self.raiseImage(ic) if model: self._model_imagecons.add(id(ic)) return ic # load the FITS image busy = BusyIndicator() dprint(2, "reading FITS image", filename) self.showMessage("""Reading FITS image %s""" % filename, 3000) QApplication.flush() try: image = SkyImage.FITSImagePlotItem(str(filename)) except KeyboardInterrupt: raise except: busy = None traceback.print_exc() self.showErrorMessage("""<P>Error loading FITS image %s: %s. This may be due to a bug in Tigger; if the FITS file loads fine in another viewer, please send the FITS file, along with a copy of any error messages from the text console, to [email protected].</P>""" % ( filename, str(sys.exc_info()[1]))) return None # create control bar, add to widget stack ic = self._createImageController(image, "model source '%s'" % model if model else filename, model or image.name, model=model) self.showMessage("""Loaded FITS image %s""" % filename, 3000) dprint(2, "image loaded") return ic def showMessage(self, message, time=None): self.emit(SIGNAL("showMessage"), message, time) def showErrorMessage(self, message, time=None): self.emit(SIGNAL("showErrorMessage"), message, time) def setZ0(self, z0): self._z0 = z0 if self._imagecons: self.raiseImage(self._imagecons[0]) def enableImageBorders(self, border_pen, label_color, label_bg_brush): self._border_pen, self._label_color, self._label_bg_brush = \ border_pen, label_color, label_bg_brush def lockAllDisplayRanges(self, rc0): """Locks all display ranges, and sets the intensity from rc0""" if not self._updating_imap: self._updating_imap = True rc0.lockDisplayRange() try: for ic in self._imagecons: rc1 = ic.renderControl() if rc1 is not rc0: rc1.setDisplayRange(*rc0.displayRange()) rc1.lockDisplayRange() finally: self._updating_imap = False def unlockAllDisplayRanges(self): """Unlocks all display range.""" for ic in self._imagecons: ic.renderControl().lockDisplayRange(False) def _lockDisplayRange(self, rc0, lock): """Locks or unlocks the display range of a specific controller.""" if lock and not self._updating_imap: self._updating_imap = True try: # if something is already locked, copy display range from it for ic in self._imagecons: rc1 = ic.renderControl() if rc1 is not rc0 and rc1.isDisplayRangeLocked(): rc0.setDisplayRange(*rc1.displayRange()) finally: self._updating_imap = False def _updateDisplayRange(self, rc, dmin, dmax): """This is called whenever one of the images (or rather, its associated RenderControl object) changes its display range.""" if not rc.isDisplayRangeLocked(): return # If the display range is locked, propagate it to all images. # but don't do it if we're already propagating (otherwise we may get called in an infinte loop) if not self._updating_imap: self._updating_imap = True try: for ic in self._imagecons: rc1 = ic.renderControl() if rc1 is not rc and rc1.isDisplayRangeLocked(): rc1.setDisplayRange(dmin, dmax) finally: self._updating_imap = False def getImages(self): return [ic.image for ic in self._imagecons] def getTopImage(self): return (self._imagecons or None) and self._imagecons[0].image def cycleImages(self): index = self._imagecon_loadorder.index(self._imagecons[0]) index = (index + 1) % len(self._imagecon_loadorder) self.raiseImage(self._imagecon_loadorder[index]) def blinkImages(self): if len(self._imagecons) > 1: self.raiseImage(self._imagecons[1]) def incrementSlice(self, extra_axis, incr): if self._imagecons: rc = self._imagecons[0].renderControl() sliced_axes = rc.slicedAxes() if extra_axis < len(sliced_axes): rc.incrementSlice(sliced_axes[extra_axis][0], incr) def setLMRectSubset(self, rect): if self._imagecons: self._imagecons[0].setLMRectSubset(rect) def getLMRectStats(self, rect): if self._imagecons: return self._imagecons[0].renderControl().getLMRectStats(rect) def unloadModelImages(self): """Unloads images associated with model (i.e. loaded with the model=True flag)""" for ic in [ic for ic in self._imagecons if id(ic) in self._model_imagecons]: self.unloadImage(ic) def unloadImage(self, imagecon): """Unloads the given imagecon object.""" if imagecon not in self._imagecons: return # recenter if needed self._imagecons.remove(imagecon) self._imagecon_loadorder.remove(imagecon) self._model_imagecons.discard(id(imagecon)) # reparent widget and release it imagecon.setParent(None) imagecon.close() # recenter image, if unloaded the center image if self._center_image is imagecon.image: self.centerImage(self._imagecons[0] if self._imagecons else None, emit=False) # emit signal self._repopulateMenu() self.emit(SIGNAL("imagesChanged")) if self._imagecons: self.raiseImage(self._imagecons[0]) def getCenterImage(self): return self._center_image def centerImage(self, imagecon, emit=True): self._center_image = imagecon and imagecon.image for ic in self._imagecons: ic.setPlotProjection(self._center_image.projection) if emit: self.emit(SIGNAL("imagesChanged")) def raiseImage(self, imagecon): # reshuffle image stack, if more than one image image if len(self._imagecons) > 1: busy = BusyIndicator() # reshuffle image stack self._imagecons.remove(imagecon) self._imagecons.insert(0, imagecon) # notify imagecons for i, ic in enumerate(self._imagecons): label = "%d" % (i + 1) if i else "<B>1</B>" ic.setZ(self._z0 - i * 10, top=not i, depthlabel=label, can_raise=True) # adjust visibility for j, ic in enumerate(self._imagecons): ic.setImageVisible(not j or bool(self._qa_plot_all.isChecked())) # issue replot signal self.emit(SIGNAL("imageRaised")) self.fastReplot() # else simply update labels else: self._imagecons[0].setZ(self._z0, top=True, depthlabel=None, can_raise=False) self._imagecons[0].setImageVisible(True) # update slice menus img = imagecon.image axes = imagecon.renderControl().slicedAxes() for i, (next, prev) in enumerate(self._qa_slices): next.setVisible(False) prev.setVisible(False) if i < len(axes): iaxis, name, labels = axes[i] next.setVisible(True) prev.setVisible(True) next.setText("Show next slice along %s axis" % name) prev.setText("Show previous slice along %s axis" % name) # emit signasl self.emit(SIGNAL("imageRaised"), img) def resetDrawKey(self): """Makes and sets the current plot's drawing key""" if self._plot: key = [] for ic in self._imagecons: key.append(id(ic)) key += ic.currentSlice() self._plot.setDrawingKey(tuple(key)) def fastReplot(self, *dum): """Fast replot -- called when flipping images or slices. Uses the plot cache, if possible.""" if self._plot: self.resetDrawKey() dprint(2, "calling replot", time.time() % 60) self._plot.replot() dprint(2, "replot done", time.time() % 60) def replot(self, *dum): """Proper replot -- called when an image needs to be properly redrawn. Cleares the plot's drawing cache.""" if self._plot: self._plot.clearDrawCache() self.resetDrawKey() self._plot.replot() def attachImagesToPlot(self, plot): self._plot = plot self.resetDrawKey() for ic in self._imagecons: ic.attachToPlot(plot) def getMenu(self): return self._menu def _displayAllImages(self, enabled): busy = BusyIndicator() if enabled: for ic in self._imagecons: ic.setImageVisible(True) else: self._imagecons[0].setImageVisible(True) for ic in self._imagecons[1:]: ic.setImageVisible(False) self.replot() def _checkClipboardPath(self, mode=QClipboard.Clipboard): if self._qa_load_clipboard: self._clipboard_mode = mode try: path = str(QApplication.clipboard().text(mode)) except: path = None self._qa_load_clipboard.setEnabled(bool(path and os.path.isfile(path))) def _loadClipboardPath(self): try: path = QApplication.clipboard().text(self._clipboard_mode) except: return self.loadImage(path) def _repopulateMenu(self): self._menu.clear() self._menu.addAction("&Load image...", self.loadImage, Qt.CTRL + Qt.Key_L) self._menu.addAction("&Compute image...", self.computeImage, Qt.CTRL + Qt.Key_M) self._qa_load_clipboard = self._menu.addAction("Load from clipboard &path", self._loadClipboardPath, Qt.CTRL + Qt.Key_P) self._checkClipboardPath() if self._imagecons: self._menu.addSeparator() # add controls to cycle images and planes for i, imgcon in enumerate(self._imagecons[::-1]): self._menu.addMenu(imgcon.getMenu()) self._menu.addSeparator() if len(self._imagecons) > 1: self._menu.addAction("Cycle images", self.cycleImages, Qt.Key_F5) self._menu.addAction("Blink images", self.blinkImages, Qt.Key_F6) self._qa_slices = ( (self._menu.addAction("Next slice along axis 1", self._currier.curry(self.incrementSlice, 0, 1), Qt.Key_F7), self._menu.addAction("Previous slice along axis 1", self._currier.curry(self.incrementSlice, 0, -1), Qt.SHIFT + Qt.Key_F7)), (self._menu.addAction("Next slice along axis 2", self._currier.curry(self.incrementSlice, 1, 1), Qt.Key_F8), self._menu.addAction("Previous slice along axis 2", self._currier.curry(self.incrementSlice, 1, -1), Qt.SHIFT + Qt.Key_F8))) self._menu.addSeparator() self._menu.addAction(self._qa_plot_top) self._menu.addAction(self._qa_plot_all) def computeImage(self, expression=None): """Computes image from expression (if expression is None, pops up dialog)""" if expression is None: (expression, ok) = QInputDialog.getText(self, "Compute image", """Enter an image expression to compute. Any valid numpy expression is supported, and all functions from the numpy module are available (including sub-modules such as fft). Use 'a', 'b', 'c' to refer to images. Examples: "(a+b)/2", "cos(a)+sin(b)", "a-a.mean()", "fft.fft2(a)", etc.""") # (expression,ok) = QInputDialog.getText(self,"Compute image","""<P>Enter an expression to compute. # Use 'a', 'b', etc. to refer to loaded images. Any valid numpy expression is supported, and all the # functions from the numpy module are available. Examples of valid expressions include "(a+b)/2", # "cos(a)+sin(b)", "a-a.mean()", etc. # </P> # """) expression = str(expression) if not ok or not expression: return # try to parse expression arglist = [(chr(ord('a') + ic.getNumber()), ic.image) for ic in self._imagecons] try: exprfunc = eval("lambda " + (",".join([x[0] for x in arglist])) + ":" + expression, numpy.__dict__, {}) except Exception as exc: self.showErrorMessage("""Error parsing expression "%s": %s.""" % (expression, str(exc))) return None # try to evaluate expression self.showMessage("Computing expression \"%s\"" % expression, 10000) busy = BusyIndicator() QApplication.flush() # trim trivial trailing dimensions. This avoids the problem of when an NxMx1 and an NxMx1x1 arrays are added, # the result is promoted to NxMxMx1 following the numpy rules. def trimshape(shape): out = shape while out and out[-1] == 1: out = out[:-1] return out def trimarray(array): return array.reshape(trimshape(array.shape)) try: result = exprfunc(*[trimarray(x[1].data()) for x in arglist]) except Exception as exc: busy = None traceback.print_exc() self.showErrorMessage("""Error evaluating "%s": %s.""" % (expression, str(exc))) return None busy = None if type(result) != numpy.ma.masked_array and type(result) != numpy.ndarray: self.showErrorMessage( """Result of "%s" is of invalid type "%s" (array expected).""" % (expression, type(result).__name__)) return None # convert coomplex results to real if numpy.iscomplexobj(result): self.showErrorMessage("""Result of "%s" is complex. Complex images are currently not fully supported, so we'll implicitly use the absolute value instead.""" % (expression)) expression = "abs(%s)" % expression result = abs(result) # determine which image this expression can be associated with res_shape = trimshape(result.shape) arglist = [x for x in arglist if hasattr(x[1], 'fits_header') and trimshape(x[1].data().shape) == res_shape] if not arglist: self.showErrorMessage("""Result of "%s" has shape %s, which does not match any loaded FITS image.""" % ( expression, "x".join(map(str, result.shape)))) return None # look for an image in the arglist with the same projection, and with a valid dirname # (for the where-to-save hint) template = arglist[0][1] # if all images in arglist have the same projection, then it doesn't matter what we use # else ask if len([x for x in arglist[1:] if x[1].projection == template.projection]) != len(arglist) - 1: options = [x[0] for x in arglist] (which, ok) = QInputDialog.getItem(self, "Compute image", "Coordinate system to use for the result of \"%s\":" % expression, options, 0, False) if not ok: return None try: template = arglist[options.index(which)][1] except: pass # create a FITS image busy = BusyIndicator() dprint(2, "creating FITS image", expression) self.showMessage("""Creating image for %s""" % expression, 3000) QApplication.flush() try: hdu = pyfits.PrimaryHDU(result.transpose(), template.fits_header) skyimage = SkyImage.FITSImagePlotItem(name=expression, filename=None, hdu=hdu) except: busy = None traceback.print_exc() self.showErrorMessage("""Error creating FITS image %s: %s""" % (expression, str(sys.exc_info()[1]))) return None # get directory name for save-to hint dirname = getattr(template, 'filename', None) if not dirname: dirnames = [getattr(img, 'filename') for x, img in arglist if hasattr(img, 'filename')] dirname = dirnames[0] if dirnames else None # create control bar, add to widget stack self._createImageController(skyimage, expression, expression, save=((dirname and os.path.dirname(dirname)) or ".")) self.showMessage("Created new image for %s" % expression, 3000) dprint(2, "image created") def _createImageController(self, image, name, basename, model=False, save=False): dprint(2, "creating ImageController for", name) ic = ImageController(image, self, self, name, save=save) ic.setNumber(len(self._imagecons)) self._imagecons.insert(0, ic) self._imagecon_loadorder.append(ic) if model: self._model_imagecons.add(id(ic)) self._lo.addWidget(ic) if self._border_pen: ic.addPlotBorder(self._border_pen, basename, self._label_color, self._label_bg_brush) # attach appropriate signals image.connect(SIGNAL("slice"), self.fastReplot) image.connect(SIGNAL("repaint"), self.replot) image.connect(SIGNAL("raise"), self._currier.curry(self.raiseImage, ic)) image.connect(SIGNAL("unload"), self._currier.curry(self.unloadImage, ic)) image.connect(SIGNAL("center"), self._currier.curry(self.centerImage, ic)) QObject.connect(ic.renderControl(), SIGNAL("displayRangeChanged"), self._currier.curry(self._updateDisplayRange, ic.renderControl())) QObject.connect(ic.renderControl(), SIGNAL("displayRangeLocked"), self._currier.curry(self._lockDisplayRange, ic.renderControl())) self._plot = None # add to menus dprint(2, "repopulating menus") self._repopulateMenu() # center and raise to top of stack self.raiseImage(ic) if not self._center_image: self.centerImage(ic, emit=False) else: ic.setPlotProjection(self._center_image.projection) # signal self.emit(SIGNAL("imagesChanged")) return ic
class TagBrowserWidget(QWidget): # {{{ def __init__(self, parent): QWidget.__init__(self, parent) self.parent = parent self._layout = QVBoxLayout() self.setLayout(self._layout) self._layout.setContentsMargins(0,0,0,0) # Set up the find box & button search_layout = QHBoxLayout() self._layout.addLayout(search_layout) self.item_search = HistoryLineEdit(parent) self.item_search.setMinimumContentsLength(5) self.item_search.setSizeAdjustPolicy(self.item_search.AdjustToMinimumContentsLengthWithIcon) try: self.item_search.lineEdit().setPlaceholderText( _('Find item in tag browser')) except: pass # Using Qt < 4.7 self.item_search.setToolTip(_( 'Search for items. This is a "contains" search; items containing the\n' 'text anywhere in the name will be found. You can limit the search\n' 'to particular categories using syntax similar to search. For example,\n' 'tags:foo will find foo in any tag, but not in authors etc. Entering\n' '*foo will filter all categories at once, showing only those items\n' 'containing the text "foo"')) search_layout.addWidget(self.item_search) # Not sure if the shortcut should be translatable ... sc = QShortcut(QKeySequence(_('ALT+f')), parent) sc.activated.connect(self.set_focus_to_find_box) self.search_button = QToolButton() self.search_button.setText(_('F&ind')) self.search_button.setToolTip(_('Find the first/next matching item')) search_layout.addWidget(self.search_button) self.expand_button = QToolButton() self.expand_button.setText('-') self.expand_button.setToolTip(_('Collapse all categories')) search_layout.addWidget(self.expand_button) search_layout.setStretch(0, 10) search_layout.setStretch(1, 1) search_layout.setStretch(2, 1) self.current_find_position = None self.search_button.clicked.connect(self.find) self.item_search.initialize('tag_browser_search') self.item_search.lineEdit().returnPressed.connect(self.do_find) self.item_search.lineEdit().textEdited.connect(self.find_text_changed) self.item_search.activated[QString].connect(self.do_find) self.item_search.completer().setCaseSensitivity(Qt.CaseSensitive) parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self.expand_button.clicked.connect(self.tags_view.collapseAll) self._layout.addWidget(parent.tags_view) # Now the floating 'not found' box l = QLabel(self.tags_view) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText('<p><b>'+_('No More Matches.</b><p> Click Find again to go to first match')) l.setAlignment(Qt.AlignVCenter) l.setWordWrap(True) l.resize(l.sizeHint()) l.move(10,20) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect(self.not_found_label_timer_event, type=Qt.QueuedConnection) parent.alter_tb = l = QPushButton(parent) l.setText(_('Alter Tag Browser')) l.setIcon(QIcon(I('tags.png'))) l.m = QMenu() l.setMenu(l.m) self._layout.addWidget(l) sb = l.m.addAction(_('Sort by')) sb.m = l.sort_menu = QMenu(l.m) sb.setMenu(sb.m) sb.bg = QActionGroup(sb) # Must be in the same order as db2.CATEGORY_SORTS for i, x in enumerate((_('Sort by name'), _('Sort by number of books'), _('Sort by average rating'))): a = sb.m.addAction(x) sb.bg.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) sb.setToolTip( _('Set the sort order for entries in the Tag Browser')) sb.setStatusTip(sb.toolTip()) ma = l.m.addAction(_('Search type when selecting multiple items')) ma.m = l.match_menu = QMenu(l.m) ma.setMenu(ma.m) ma.ag = QActionGroup(ma) # Must be in the same order as db2.MATCH_TYPE for i, x in enumerate((_('Match any of the items'), _('Match all of the items'))): a = ma.m.addAction(x) ma.ag.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) ma.setToolTip( _('When selecting multiple entries in the Tag Browser ' 'match any or all of them')) ma.setStatusTip(ma.toolTip()) mt = l.m.addAction(_('Manage authors, tags, etc')) mt.setToolTip(_('All of these category_managers are available by right-clicking ' 'on items in the tag browser above')) mt.m = l.manage_menu = QMenu(l.m) mt.setMenu(mt.m) # self.leak_test_timer = QTimer(self) # self.leak_test_timer.timeout.connect(self.test_for_leak) # self.leak_test_timer.start(5000) def set_pane_is_visible(self, to_what): self.tags_view.set_pane_is_visible(to_what) def find_text_changed(self, str): self.current_find_position = None def set_focus_to_find_box(self): self.item_search.setFocus() self.item_search.lineEdit().selectAll() def do_find(self, str=None): self.current_find_position = None self.find() def find(self): model = self.tags_view.model() model.clear_boxed() txt = unicode(self.item_search.currentText()).strip() if txt.startswith('*'): model.set_categories_filter(txt[1:]) self.tags_view.recount() self.current_find_position = None return if model.get_categories_filter(): model.set_categories_filter(None) self.tags_view.recount() self.current_find_position = None if not txt: return self.item_search.lineEdit().blockSignals(True) self.search_button.setFocus(True) self.item_search.lineEdit().blockSignals(False) key = None colon = txt.rfind(':') if len(txt) > 2 else 0 if colon > 0: key = self.parent.library_view.model().db.\ field_metadata.search_term_to_field_key(txt[:colon]) txt = txt[colon+1:] self.current_find_position = \ model.find_item_node(key, txt, self.current_find_position) if self.current_find_position: self.tags_view.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): self.not_found_label.setVisible(True) if self.tags_view.verticalScrollBar().isVisible(): sbw = self.tags_view.verticalScrollBar().width() else: sbw = 0 width = self.width() - 8 - sbw height = self.not_found_label.heightForWidth(width) + 20 self.not_found_label.resize(width, height) self.not_found_label.move(4, 10) self.not_found_label_timer.start(2000) def not_found_label_timer_event(self): self.not_found_label.setVisible(False)
class ImageManager(QWidget): """An ImageManager manages a stack of images (and associated ImageControllers)""" def __init__(self, *args): QWidget.__init__(self, *args) # init layout self._lo = QVBoxLayout(self) self._lo.setContentsMargins(0, 0, 0, 0) self._lo.setSpacing(0) # init internal state self._currier = PersistentCurrier() self._z0 = 0 # z-depth of first image, the rest count down from it self._updating_imap = False self._locked_display_range = False self._imagecons = [] self._imagecon_loadorder = [] self._center_image = None self._plot = None self._border_pen = None self._drawing_key = None self._load_image_dialog = None self._model_imagecons = set() # init menu and standard actions self._menu = QMenu("&Image", self) qag = QActionGroup(self) # exclusive controls for plotting topmost or all images self._qa_plot_top = qag.addAction("Display topmost image only") self._qa_plot_all = qag.addAction("Display all images") self._qa_plot_top.setCheckable(True) self._qa_plot_all.setCheckable(True) self._qa_plot_top.setChecked(True) QObject.connect(self._qa_plot_all, SIGNAL("toggled(bool)"), self._displayAllImages) self._closing = False self._qa_load_clipboard = None self._clipboard_mode = QClipboard.Clipboard QObject.connect(QApplication.clipboard(), SIGNAL("changed(QClipboard::Mode)"), self._checkClipboardPath) # populate the menu self._repopulateMenu() def close(self): dprint(1, "closing Manager") self._closing = True for ic in self._imagecons: ic.close() def loadImage(self, filename=None, duplicate=True, to_top=True, model=None): """Loads image. Returns ImageControlBar object. If image is already loaded: returns old ICB if duplicate=False (raises to top if to_top=True), or else makes a new control bar. If model is set to a source name, marks the image as associated with a model source. These can be unloaded en masse by calling unloadModelImages(). """ if filename is None: if not self._load_image_dialog: dialog = self._load_image_dialog = QFileDialog( self, "Load FITS image", ".", "FITS images (%s);;All files (*)" % (" ".join(["*" + ext for ext in FITS_ExtensionList]))) dialog.setFileMode(QFileDialog.ExistingFile) dialog.setModal(True) QObject.connect(dialog, SIGNAL("filesSelected(const QStringList &)"), self.loadImage) self._load_image_dialog.exec_() return None if isinstance(filename, QStringList): filename = filename[0] filename = str(filename) # report error if image does not exist if not os.path.exists(filename): self.showErrorMessage("""FITS image %s does not exist.""" % filename) return None # see if image is already loaded if not duplicate: for ic in self._imagecons: if ic.getFilename() and os.path.samefile( filename, ic.getFilename()): if to_top: self.raiseImage(ic) if model: self._model_imagecons.add(id(ic)) return ic # load the FITS image busy = BusyIndicator() dprint(2, "reading FITS image", filename) self.showMessage("""Reading FITS image %s""" % filename, 3000) QApplication.flush() try: image = SkyImage.FITSImagePlotItem(str(filename)) except KeyboardInterrupt: raise except: busy = None traceback.print_exc() self.showErrorMessage( """<P>Error loading FITS image %s: %s. This may be due to a bug in Tigger; if the FITS file loads fine in another viewer, please send the FITS file, along with a copy of any error messages from the text console, to [email protected].</P>""" % (filename, str(sys.exc_info()[1]))) return None # create control bar, add to widget stack ic = self._createImageController(image, "model source '%s'" % model if model else filename, model or image.name, model=model) self.showMessage("""Loaded FITS image %s""" % filename, 3000) dprint(2, "image loaded") return ic def showMessage(self, message, time=None): self.emit(SIGNAL("showMessage"), message, time) def showErrorMessage(self, message, time=None): self.emit(SIGNAL("showErrorMessage"), message, time) def setZ0(self, z0): self._z0 = z0 if self._imagecons: self.raiseImage(self._imagecons[0]) def enableImageBorders(self, border_pen, label_color, label_bg_brush): self._border_pen, self._label_color, self._label_bg_brush = \ border_pen, label_color, label_bg_brush def lockAllDisplayRanges(self, rc0): """Locks all display ranges, and sets the intensity from rc0""" if not self._updating_imap: self._updating_imap = True rc0.lockDisplayRange() try: for ic in self._imagecons: rc1 = ic.renderControl() if rc1 is not rc0: rc1.setDisplayRange(*rc0.displayRange()) rc1.lockDisplayRange() finally: self._updating_imap = False def unlockAllDisplayRanges(self): """Unlocks all display range.""" for ic in self._imagecons: ic.renderControl().lockDisplayRange(False) def _lockDisplayRange(self, rc0, lock): """Locks or unlocks the display range of a specific controller.""" if lock and not self._updating_imap: self._updating_imap = True try: # if something is already locked, copy display range from it for ic in self._imagecons: rc1 = ic.renderControl() if rc1 is not rc0 and rc1.isDisplayRangeLocked(): rc0.setDisplayRange(*rc1.displayRange()) finally: self._updating_imap = False def _updateDisplayRange(self, rc, dmin, dmax): """This is called whenever one of the images (or rather, its associated RenderControl object) changes its display range.""" if not rc.isDisplayRangeLocked(): return # If the display range is locked, propagate it to all images. # but don't do it if we're already propagating (otherwise we may get called in an infinte loop) if not self._updating_imap: self._updating_imap = True try: for ic in self._imagecons: rc1 = ic.renderControl() if rc1 is not rc and rc1.isDisplayRangeLocked(): rc1.setDisplayRange(dmin, dmax) finally: self._updating_imap = False def getImages(self): return [ic.image for ic in self._imagecons] def getTopImage(self): return (self._imagecons or None) and self._imagecons[0].image def cycleImages(self): index = self._imagecon_loadorder.index(self._imagecons[0]) index = (index + 1) % len(self._imagecon_loadorder) self.raiseImage(self._imagecon_loadorder[index]) def blinkImages(self): if len(self._imagecons) > 1: self.raiseImage(self._imagecons[1]) def incrementSlice(self, extra_axis, incr): if self._imagecons: rc = self._imagecons[0].renderControl() sliced_axes = rc.slicedAxes() if extra_axis < len(sliced_axes): rc.incrementSlice(sliced_axes[extra_axis][0], incr) def setLMRectSubset(self, rect): if self._imagecons: self._imagecons[0].setLMRectSubset(rect) def getLMRectStats(self, rect): if self._imagecons: return self._imagecons[0].renderControl().getLMRectStats(rect) def unloadModelImages(self): """Unloads images associated with model (i.e. loaded with the model=True flag)""" for ic in [ ic for ic in self._imagecons if id(ic) in self._model_imagecons ]: self.unloadImage(ic) def unloadImage(self, imagecon): """Unloads the given imagecon object.""" if imagecon not in self._imagecons: return # recenter if needed self._imagecons.remove(imagecon) self._imagecon_loadorder.remove(imagecon) self._model_imagecons.discard(id(imagecon)) # reparent widget and release it imagecon.setParent(None) imagecon.close() # recenter image, if unloaded the center image if self._center_image is imagecon.image: self.centerImage(self._imagecons[0] if self._imagecons else None, emit=False) # emit signal self._repopulateMenu() self.emit(SIGNAL("imagesChanged")) if self._imagecons: self.raiseImage(self._imagecons[0]) def getCenterImage(self): return self._center_image def centerImage(self, imagecon, emit=True): self._center_image = imagecon and imagecon.image for ic in self._imagecons: ic.setPlotProjection(self._center_image.projection) if emit: self.emit(SIGNAL("imagesChanged")) def raiseImage(self, imagecon): # reshuffle image stack, if more than one image image if len(self._imagecons) > 1: busy = BusyIndicator() # reshuffle image stack self._imagecons.remove(imagecon) self._imagecons.insert(0, imagecon) # notify imagecons for i, ic in enumerate(self._imagecons): label = "%d" % (i + 1) if i else "<B>1</B>" ic.setZ(self._z0 - i * 10, top=not i, depthlabel=label, can_raise=True) # adjust visibility for j, ic in enumerate(self._imagecons): ic.setImageVisible(not j or bool(self._qa_plot_all.isChecked())) # issue replot signal self.emit(SIGNAL("imageRaised")) self.fastReplot() # else simply update labels else: self._imagecons[0].setZ(self._z0, top=True, depthlabel=None, can_raise=False) self._imagecons[0].setImageVisible(True) # update slice menus img = imagecon.image axes = imagecon.renderControl().slicedAxes() for i, (next, prev) in enumerate(self._qa_slices): next.setVisible(False) prev.setVisible(False) if i < len(axes): iaxis, name, labels = axes[i] next.setVisible(True) prev.setVisible(True) next.setText("Show next slice along %s axis" % name) prev.setText("Show previous slice along %s axis" % name) # emit signasl self.emit(SIGNAL("imageRaised"), img) def resetDrawKey(self): """Makes and sets the current plot's drawing key""" if self._plot: key = [] for ic in self._imagecons: key.append(id(ic)) key += ic.currentSlice() self._plot.setDrawingKey(tuple(key)) def fastReplot(self, *dum): """Fast replot -- called when flipping images or slices. Uses the plot cache, if possible.""" if self._plot: self.resetDrawKey() dprint(2, "calling replot", time.time() % 60) self._plot.replot() dprint(2, "replot done", time.time() % 60) def replot(self, *dum): """Proper replot -- called when an image needs to be properly redrawn. Cleares the plot's drawing cache.""" if self._plot: self._plot.clearDrawCache() self.resetDrawKey() self._plot.replot() def attachImagesToPlot(self, plot): self._plot = plot self.resetDrawKey() for ic in self._imagecons: ic.attachToPlot(plot) def getMenu(self): return self._menu def _displayAllImages(self, enabled): busy = BusyIndicator() if enabled: for ic in self._imagecons: ic.setImageVisible(True) else: self._imagecons[0].setImageVisible(True) for ic in self._imagecons[1:]: ic.setImageVisible(False) self.replot() def _checkClipboardPath(self, mode=QClipboard.Clipboard): if self._qa_load_clipboard: self._clipboard_mode = mode try: path = str(QApplication.clipboard().text(mode)) except: path = None self._qa_load_clipboard.setEnabled( bool(path and os.path.isfile(path))) def _loadClipboardPath(self): try: path = QApplication.clipboard().text(self._clipboard_mode) except: return self.loadImage(path) def _repopulateMenu(self): self._menu.clear() self._menu.addAction("&Load image...", self.loadImage, Qt.CTRL + Qt.Key_L) self._menu.addAction("&Compute image...", self.computeImage, Qt.CTRL + Qt.Key_M) self._qa_load_clipboard = self._menu.addAction( "Load from clipboard &path", self._loadClipboardPath, Qt.CTRL + Qt.Key_P) self._checkClipboardPath() if self._imagecons: self._menu.addSeparator() # add controls to cycle images and planes for i, imgcon in enumerate(self._imagecons[::-1]): self._menu.addMenu(imgcon.getMenu()) self._menu.addSeparator() if len(self._imagecons) > 1: self._menu.addAction("Cycle images", self.cycleImages, Qt.Key_F5) self._menu.addAction("Blink images", self.blinkImages, Qt.Key_F6) self._qa_slices = ((self._menu.addAction( "Next slice along axis 1", self._currier.curry(self.incrementSlice, 0, 1), Qt.Key_F7), self._menu.addAction( "Previous slice along axis 1", self._currier.curry( self.incrementSlice, 0, -1), Qt.SHIFT + Qt.Key_F7)), (self._menu.addAction( "Next slice along axis 2", self._currier.curry(self.incrementSlice, 1, 1), Qt.Key_F8), self._menu.addAction( "Previous slice along axis 2", self._currier.curry( self.incrementSlice, 1, -1), Qt.SHIFT + Qt.Key_F8))) self._menu.addSeparator() self._menu.addAction(self._qa_plot_top) self._menu.addAction(self._qa_plot_all) def computeImage(self, expression=None): """Computes image from expression (if expression is None, pops up dialog)""" if expression is None: (expression, ok) = QInputDialog.getText( self, "Compute image", """Enter an image expression to compute. Any valid numpy expression is supported, and all functions from the numpy module are available (including sub-modules such as fft). Use 'a', 'b', 'c' to refer to images. Examples: "(a+b)/2", "cos(a)+sin(b)", "a-a.mean()", "fft.fft2(a)", etc.""" ) # (expression,ok) = QInputDialog.getText(self,"Compute image","""<P>Enter an expression to compute. # Use 'a', 'b', etc. to refer to loaded images. Any valid numpy expression is supported, and all the # functions from the numpy module are available. Examples of valid expressions include "(a+b)/2", # "cos(a)+sin(b)", "a-a.mean()", etc. # </P> # """) expression = str(expression) if not ok or not expression: return # try to parse expression arglist = [(chr(ord('a') + ic.getNumber()), ic.image) for ic in self._imagecons] try: exprfunc = eval( "lambda " + (",".join([x[0] for x in arglist])) + ":" + expression, numpy.__dict__, {}) except Exception as exc: self.showErrorMessage("""Error parsing expression "%s": %s.""" % (expression, str(exc))) return None # try to evaluate expression self.showMessage("Computing expression \"%s\"" % expression, 10000) busy = BusyIndicator() QApplication.flush() # trim trivial trailing dimensions. This avoids the problem of when an NxMx1 and an NxMx1x1 arrays are added, # the result is promoted to NxMxMx1 following the numpy rules. def trimshape(shape): out = shape while out and out[-1] == 1: out = out[:-1] return out def trimarray(array): return array.reshape(trimshape(array.shape)) try: result = exprfunc(*[trimarray(x[1].data()) for x in arglist]) except Exception as exc: busy = None traceback.print_exc() self.showErrorMessage("""Error evaluating "%s": %s.""" % (expression, str(exc))) return None busy = None if type(result) != numpy.ma.masked_array and type( result) != numpy.ndarray: self.showErrorMessage( """Result of "%s" is of invalid type "%s" (array expected).""" % (expression, type(result).__name__)) return None # convert coomplex results to real if numpy.iscomplexobj(result): self.showErrorMessage( """Result of "%s" is complex. Complex images are currently not fully supported, so we'll implicitly use the absolute value instead.""" % (expression)) expression = "abs(%s)" % expression result = abs(result) # determine which image this expression can be associated with res_shape = trimshape(result.shape) arglist = [ x for x in arglist if hasattr(x[1], 'fits_header') and trimshape(x[1].data().shape) == res_shape ] if not arglist: self.showErrorMessage( """Result of "%s" has shape %s, which does not match any loaded FITS image.""" % (expression, "x".join(map(str, result.shape)))) return None # look for an image in the arglist with the same projection, and with a valid dirname # (for the where-to-save hint) template = arglist[0][1] # if all images in arglist have the same projection, then it doesn't matter what we use # else ask if len( [x for x in arglist[1:] if x[1].projection == template.projection ]) != len(arglist) - 1: options = [x[0] for x in arglist] (which, ok) = QInputDialog.getItem( self, "Compute image", "Coordinate system to use for the result of \"%s\":" % expression, options, 0, False) if not ok: return None try: template = arglist[options.index(which)][1] except: pass # create a FITS image busy = BusyIndicator() dprint(2, "creating FITS image", expression) self.showMessage("""Creating image for %s""" % expression, 3000) QApplication.flush() try: hdu = pyfits.PrimaryHDU(result.transpose(), template.fits_header) skyimage = SkyImage.FITSImagePlotItem(name=expression, filename=None, hdu=hdu) except: busy = None traceback.print_exc() self.showErrorMessage("""Error creating FITS image %s: %s""" % (expression, str(sys.exc_info()[1]))) return None # get directory name for save-to hint dirname = getattr(template, 'filename', None) if not dirname: dirnames = [ getattr(img, 'filename') for x, img in arglist if hasattr(img, 'filename') ] dirname = dirnames[0] if dirnames else None # create control bar, add to widget stack self._createImageController( skyimage, expression, expression, save=((dirname and os.path.dirname(dirname)) or ".")) self.showMessage("Created new image for %s" % expression, 3000) dprint(2, "image created") def _createImageController(self, image, name, basename, model=False, save=False): dprint(2, "creating ImageController for", name) ic = ImageController(image, self, self, name, save=save) ic.setNumber(len(self._imagecons)) self._imagecons.insert(0, ic) self._imagecon_loadorder.append(ic) if model: self._model_imagecons.add(id(ic)) self._lo.addWidget(ic) if self._border_pen: ic.addPlotBorder(self._border_pen, basename, self._label_color, self._label_bg_brush) # attach appropriate signals image.connect(SIGNAL("slice"), self.fastReplot) image.connect(SIGNAL("repaint"), self.replot) image.connect(SIGNAL("raise"), self._currier.curry(self.raiseImage, ic)) image.connect(SIGNAL("unload"), self._currier.curry(self.unloadImage, ic)) image.connect(SIGNAL("center"), self._currier.curry(self.centerImage, ic)) QObject.connect( ic.renderControl(), SIGNAL("displayRangeChanged"), self._currier.curry(self._updateDisplayRange, ic.renderControl())) QObject.connect( ic.renderControl(), SIGNAL("displayRangeLocked"), self._currier.curry(self._lockDisplayRange, ic.renderControl())) self._plot = None # add to menus dprint(2, "repopulating menus") self._repopulateMenu() # center and raise to top of stack self.raiseImage(ic) if not self._center_image: self.centerImage(ic, emit=False) else: ic.setPlotProjection(self._center_image.projection) # signal self.emit(SIGNAL("imagesChanged")) return ic
def __init__(self, parent, hide_on_close=False): QMainWindow.__init__(self, parent) self._hide_on_close = hide_on_close # replace the BusyIndicator class with a GUI-aware one Purr.BusyIndicator = BusyIndicator self._pounce = False # we keep a small stack of previously active purrers. This makes directory changes # faster (when going back and forth between dirs) # current purrer self.purrer = None self.purrer_stack = [] # Purr pipes for receiving remote commands self.purrpipes = {} # init GUI self.setWindowTitle("PURR") self.setWindowIcon(pixmaps.purr_logo.icon()) cw = QWidget(self) self.setCentralWidget(cw) cwlo = QVBoxLayout(cw) cwlo.setContentsMargins(0, 0, 0, 0) cwlo.setMargin(5) cwlo.setSpacing(0) toplo = QHBoxLayout(); cwlo.addLayout(toplo) # About dialog self._about_dialog = QMessageBox(self) self._about_dialog.setWindowTitle("About PURR") self._about_dialog.setText(self.about_message + """ <P>PURR is not watching any directories right now. You may need to restart it, and give it some directory names on the command line.</P>""") self._about_dialog.setIconPixmap(pixmaps.purr_logo.pm()) # Log viewer dialog self.viewer_dialog = HTMLViewerDialog(self, config_name="log-viewer", buttons=[(pixmaps.blue_round_reload, "Regenerate", """<P>Regenerates your log's HTML code from scratch. This can be useful if your PURR version has changed, or if there was an error of some kind the last time the files were generated.</P> """)]) self._viewer_timestamp = None self.connect(self.viewer_dialog, SIGNAL("Regenerate"), self._regenerateLog) self.connect(self.viewer_dialog, SIGNAL("viewPath"), self._viewPath) # Log title toolbar title_tb = QToolBar(cw) title_tb.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) title_tb.setIconSize(QSize(16, 16)) cwlo.addWidget(title_tb) title_label = QLabel("Purrlog title:", title_tb) title_tb.addWidget(title_label) self.title_editor = QLineEdit(title_tb) title_tb.addWidget(self.title_editor) self.connect(self.title_editor, SIGNAL("editingFinished()"), self._titleChanged) tip = """<P>This is your current log title. To rename the log, enter new name here and press Enter.</P>""" title_label.setToolTip(tip) self.title_editor.setToolTip(tip) self.wviewlog = title_tb.addAction(pixmaps.openbook.icon(), "View", self._showViewerDialog) self.wviewlog.setToolTip("Click to see an HTML rendering of your current log.") qa = title_tb.addAction(pixmaps.purr_logo.icon(), "About...", self._about_dialog.exec_) qa.setToolTip("<P>Click to see the About... dialog, which will tell you something about PURR.</P>") self.wdirframe = QFrame(cw) cwlo.addWidget(self.wdirframe) self.dirs_lo = QVBoxLayout(self.wdirframe) self.dirs_lo.setMargin(5) self.dirs_lo.setContentsMargins(5, 0, 5, 5) self.dirs_lo.setSpacing(0) self.wdirframe.setFrameStyle(QFrame.Box | QFrame.Raised) self.wdirframe.setLineWidth(1) ## Directories toolbar dirs_tb = QToolBar(self.wdirframe) dirs_tb.setToolButtonStyle(Qt.ToolButtonIconOnly) dirs_tb.setIconSize(QSize(16, 16)) self.dirs_lo.addWidget(dirs_tb) label = QLabel("Monitoring directories:", dirs_tb) self._dirs_tip = """<P>PURR can monitor your working directories for new or updated files. If there's a checkmark next to the directory name in this list, PURR is monitoring it.</P> <P>If the checkmark is grey, PURR is monitoring things unobtrusively. When a new or updated file is detected in he monitored directory, it is quietly added to the list of files in the "New entry" window, even if this window is not currently visible.</P> <P>If the checkmark is black, PURR will be more obtrusive. Whenever a new or updated file is detected, the "New entry" window will pop up automatically. This is called "pouncing", and some people find it annoying.</P> """ label.setToolTip(self._dirs_tip) label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) dirs_tb.addWidget(label) # add directory list widget self.wdirlist = DirectoryListWidget(self.wdirframe) self.wdirlist.setToolTip(self._dirs_tip) QObject.connect(self.wdirlist, SIGNAL("directoryStateChanged"), self._changeWatchedDirState) self.dirs_lo.addWidget(self.wdirlist) # self.wdirlist.setMaximumSize(1000000,64) # add directory button add = dirs_tb.addAction(pixmaps.list_add.icon(), "Add", self._showAddDirectoryDialog) add.setToolTip("<P>Click to add another directory to be monitored.</P>") # remove directory button delbtn = dirs_tb.addAction(pixmaps.list_remove.icon(), "Remove", self.wdirlist.removeCurrent) delbtn.setEnabled(False) delbtn.setToolTip("<P>Click to removed the currently selected directory from the list.</P>") QObject.connect(self.wdirlist, SIGNAL("hasSelection"), delbtn.setEnabled) # # qa = dirs_tb.addAction(pixmaps.blue_round_reload.icon(),"Rescan",self._forceRescan) # # qa.setToolTip("Click to rescan the directories for any new or updated files.") # self.wshownew = QCheckBox("show new files",dirs_tb) # dirs_tb.addWidget(self.wshownew) # self.wshownew.setCheckState(Qt.Checked) # self.wshownew.setToolTip("""<P>If this is checked, the "New entry" window will pop up automatically whenever # new or updated files are detected. If this is unchecked, the files will be added to the window quietly # and unobtrusively; you can show the window manually by clicking on the "New entry..." button below.</P>""") # self._dir_entries = {} cwlo.addSpacing(5) wlogframe = QFrame(cw) cwlo.addWidget(wlogframe) log_lo = QVBoxLayout(wlogframe) log_lo.setMargin(5) log_lo.setContentsMargins(5, 5, 5, 5) log_lo.setSpacing(0) wlogframe.setFrameStyle(QFrame.Box | QFrame.Raised) wlogframe.setLineWidth(1) # listview of log entries self.etw = LogEntryTree(cw) log_lo.addWidget(self.etw, 1) self.etw.header().setDefaultSectionSize(128) self.etw.header().setMovable(False) self.etw.setHeaderLabels(["date", "entry title", "comment"]) if hasattr(QHeaderView, 'ResizeToContents'): self.etw.header().setResizeMode(0, QHeaderView.ResizeToContents) else: self.etw.header().setResizeMode(0, QHeaderView.Custom) self.etw.header().resizeSection(0, 120) self.etw.header().setResizeMode(1, QHeaderView.Interactive) self.etw.header().setResizeMode(2, QHeaderView.Stretch) self.etw.header().show() try: self.etw.setAllColumnsShowFocus(True) except AttributeError: pass; # Qt 4.2+ # self.etw.setShowToolTips(True) self.etw.setSortingEnabled(False) # self.etw.setColumnAlignment(2,Qt.AlignLeft|Qt.AlignTop) self.etw.setSelectionMode(QTreeWidget.ExtendedSelection) self.etw.setRootIsDecorated(True) self.connect(self.etw, SIGNAL("itemSelectionChanged()"), self._entrySelectionChanged) self.connect(self.etw, SIGNAL("itemActivated(QTreeWidgetItem*,int)"), self._viewEntryItem) self.connect(self.etw, SIGNAL("itemContextMenuRequested"), self._showItemContextMenu) # create popup menu for data products self._archived_dp_menu = menu = QMenu(self) self._archived_dp_menu_title = QLabel() self._archived_dp_menu_title.setMargin(5) self._archived_dp_menu_title_wa = wa = QWidgetAction(self) wa.setDefaultWidget(self._archived_dp_menu_title) menu.addAction(wa) menu.addSeparator() menu.addAction(pixmaps.editcopy.icon(), "Restore file(s) from archived copy", self._restoreItemFromArchive) menu.addAction(pixmaps.editpaste.icon(), "Copy pathname of archived copy to clipboard", self._copyItemToClipboard) self._current_item = None # create popup menu for entries self._entry_menu = menu = QMenu(self) self._entry_menu_title = QLabel() self._entry_menu_title.setMargin(5) self._entry_menu_title_wa = wa = QWidgetAction(self) wa.setDefaultWidget(self._entry_menu_title) menu.addAction(wa) menu.addSeparator() menu.addAction(pixmaps.filefind.icon(), "View this log entry", self._viewEntryItem) menu.addAction(pixmaps.editdelete.icon(), "Delete this log entry", self._deleteSelectedEntries) # buttons at bottom log_lo.addSpacing(5) btnlo = QHBoxLayout() log_lo.addLayout(btnlo) self.wnewbtn = QPushButton(pixmaps.filenew.icon(), "New entry...", cw) self.wnewbtn.setToolTip("Click to add a new log entry.") # self.wnewbtn.setFlat(True) self.wnewbtn.setEnabled(False) btnlo.addWidget(self.wnewbtn) btnlo.addSpacing(5) self.weditbtn = QPushButton(pixmaps.filefind.icon(), "View entry...", cw) self.weditbtn.setToolTip("Click to view or edit the selected log entry/") # self.weditbtn.setFlat(True) self.weditbtn.setEnabled(False) self.connect(self.weditbtn, SIGNAL("clicked()"), self._viewEntryItem) btnlo.addWidget(self.weditbtn) btnlo.addSpacing(5) self.wdelbtn = QPushButton(pixmaps.editdelete.icon(), "Delete", cw) self.wdelbtn.setToolTip("Click to delete the selected log entry or entries.") # self.wdelbtn.setFlat(True) self.wdelbtn.setEnabled(False) self.connect(self.wdelbtn, SIGNAL("clicked()"), self._deleteSelectedEntries) btnlo.addWidget(self.wdelbtn) # enable status line self.statusBar().show() Purr.progressMessage = self.message self._prev_msg = None # editor dialog for new entry self.new_entry_dialog = Purr.Editors.NewLogEntryDialog(self) self.connect(self.new_entry_dialog, SIGNAL("newLogEntry"), self._newLogEntry) self.connect(self.new_entry_dialog, SIGNAL("filesSelected"), self._addDPFiles) self.connect(self.wnewbtn, SIGNAL("clicked()"), self.new_entry_dialog.show) self.connect(self.new_entry_dialog, SIGNAL("shown"), self._checkPounceStatus) # entry viewer dialog self.view_entry_dialog = Purr.Editors.ExistingLogEntryDialog(self) self.connect(self.view_entry_dialog, SIGNAL("previous()"), self._viewPrevEntry) self.connect(self.view_entry_dialog, SIGNAL("next()"), self._viewNextEntry) self.connect(self.view_entry_dialog, SIGNAL("viewPath"), self._viewPath) self.connect(self.view_entry_dialog, SIGNAL("filesSelected"), self._addDPFilesToOldEntry) self.connect(self.view_entry_dialog, SIGNAL("entryChanged"), self._entryChanged) # saving a data product to an older entry will automatically drop it from the # new entry dialog self.connect(self.view_entry_dialog, SIGNAL("creatingDataProduct"), self.new_entry_dialog.dropDataProducts) # resize selves width = Config.getint('main-window-width', 512) height = Config.getint('main-window-height', 512) self.resize(QSize(width, height)) # create timer for pouncing self._timer = QTimer(self) self.connect(self._timer, SIGNAL("timeout()"), self._rescan) # create dict mapping index.html paths to entry numbers self._index_paths = {}
class MainWindow(QMainWindow): about_message = """ <P>PURR ("<B>P</B>URR is <B>U</B>seful for <B>R</B>emembering <B>R</B>eductions", for those working with a stable version, or "<B>P</B>URR <B>U</B>sually <B>R</B>emembers <B>R</B>eductions", for those working with a development version, or "<B>P</B>URR <B>U</B>sed to <B>R</B>emember <B>R</B>eductions", for those working with a broken version) is a tool for automatically keeping a log of your data reduction operations. PURR will monitor your working directories for new or updated files (called "data products"), and upon seeing any, it can "pounce" -- that is, offer you the option of saving the files to a log, along with descriptive comments. It will then generate an HTML page with a pretty rendering of your log and data products.</P> """ def __init__(self, parent, hide_on_close=False): QMainWindow.__init__(self, parent) self._hide_on_close = hide_on_close # replace the BusyIndicator class with a GUI-aware one Purr.BusyIndicator = BusyIndicator self._pounce = False # we keep a small stack of previously active purrers. This makes directory changes # faster (when going back and forth between dirs) # current purrer self.purrer = None self.purrer_stack = [] # Purr pipes for receiving remote commands self.purrpipes = {} # init GUI self.setWindowTitle("PURR") self.setWindowIcon(pixmaps.purr_logo.icon()) cw = QWidget(self) self.setCentralWidget(cw) cwlo = QVBoxLayout(cw) cwlo.setContentsMargins(0, 0, 0, 0) cwlo.setMargin(5) cwlo.setSpacing(0) toplo = QHBoxLayout(); cwlo.addLayout(toplo) # About dialog self._about_dialog = QMessageBox(self) self._about_dialog.setWindowTitle("About PURR") self._about_dialog.setText(self.about_message + """ <P>PURR is not watching any directories right now. You may need to restart it, and give it some directory names on the command line.</P>""") self._about_dialog.setIconPixmap(pixmaps.purr_logo.pm()) # Log viewer dialog self.viewer_dialog = HTMLViewerDialog(self, config_name="log-viewer", buttons=[(pixmaps.blue_round_reload, "Regenerate", """<P>Regenerates your log's HTML code from scratch. This can be useful if your PURR version has changed, or if there was an error of some kind the last time the files were generated.</P> """)]) self._viewer_timestamp = None self.connect(self.viewer_dialog, SIGNAL("Regenerate"), self._regenerateLog) self.connect(self.viewer_dialog, SIGNAL("viewPath"), self._viewPath) # Log title toolbar title_tb = QToolBar(cw) title_tb.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) title_tb.setIconSize(QSize(16, 16)) cwlo.addWidget(title_tb) title_label = QLabel("Purrlog title:", title_tb) title_tb.addWidget(title_label) self.title_editor = QLineEdit(title_tb) title_tb.addWidget(self.title_editor) self.connect(self.title_editor, SIGNAL("editingFinished()"), self._titleChanged) tip = """<P>This is your current log title. To rename the log, enter new name here and press Enter.</P>""" title_label.setToolTip(tip) self.title_editor.setToolTip(tip) self.wviewlog = title_tb.addAction(pixmaps.openbook.icon(), "View", self._showViewerDialog) self.wviewlog.setToolTip("Click to see an HTML rendering of your current log.") qa = title_tb.addAction(pixmaps.purr_logo.icon(), "About...", self._about_dialog.exec_) qa.setToolTip("<P>Click to see the About... dialog, which will tell you something about PURR.</P>") self.wdirframe = QFrame(cw) cwlo.addWidget(self.wdirframe) self.dirs_lo = QVBoxLayout(self.wdirframe) self.dirs_lo.setMargin(5) self.dirs_lo.setContentsMargins(5, 0, 5, 5) self.dirs_lo.setSpacing(0) self.wdirframe.setFrameStyle(QFrame.Box | QFrame.Raised) self.wdirframe.setLineWidth(1) ## Directories toolbar dirs_tb = QToolBar(self.wdirframe) dirs_tb.setToolButtonStyle(Qt.ToolButtonIconOnly) dirs_tb.setIconSize(QSize(16, 16)) self.dirs_lo.addWidget(dirs_tb) label = QLabel("Monitoring directories:", dirs_tb) self._dirs_tip = """<P>PURR can monitor your working directories for new or updated files. If there's a checkmark next to the directory name in this list, PURR is monitoring it.</P> <P>If the checkmark is grey, PURR is monitoring things unobtrusively. When a new or updated file is detected in he monitored directory, it is quietly added to the list of files in the "New entry" window, even if this window is not currently visible.</P> <P>If the checkmark is black, PURR will be more obtrusive. Whenever a new or updated file is detected, the "New entry" window will pop up automatically. This is called "pouncing", and some people find it annoying.</P> """ label.setToolTip(self._dirs_tip) label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) dirs_tb.addWidget(label) # add directory list widget self.wdirlist = DirectoryListWidget(self.wdirframe) self.wdirlist.setToolTip(self._dirs_tip) QObject.connect(self.wdirlist, SIGNAL("directoryStateChanged"), self._changeWatchedDirState) self.dirs_lo.addWidget(self.wdirlist) # self.wdirlist.setMaximumSize(1000000,64) # add directory button add = dirs_tb.addAction(pixmaps.list_add.icon(), "Add", self._showAddDirectoryDialog) add.setToolTip("<P>Click to add another directory to be monitored.</P>") # remove directory button delbtn = dirs_tb.addAction(pixmaps.list_remove.icon(), "Remove", self.wdirlist.removeCurrent) delbtn.setEnabled(False) delbtn.setToolTip("<P>Click to removed the currently selected directory from the list.</P>") QObject.connect(self.wdirlist, SIGNAL("hasSelection"), delbtn.setEnabled) # # qa = dirs_tb.addAction(pixmaps.blue_round_reload.icon(),"Rescan",self._forceRescan) # # qa.setToolTip("Click to rescan the directories for any new or updated files.") # self.wshownew = QCheckBox("show new files",dirs_tb) # dirs_tb.addWidget(self.wshownew) # self.wshownew.setCheckState(Qt.Checked) # self.wshownew.setToolTip("""<P>If this is checked, the "New entry" window will pop up automatically whenever # new or updated files are detected. If this is unchecked, the files will be added to the window quietly # and unobtrusively; you can show the window manually by clicking on the "New entry..." button below.</P>""") # self._dir_entries = {} cwlo.addSpacing(5) wlogframe = QFrame(cw) cwlo.addWidget(wlogframe) log_lo = QVBoxLayout(wlogframe) log_lo.setMargin(5) log_lo.setContentsMargins(5, 5, 5, 5) log_lo.setSpacing(0) wlogframe.setFrameStyle(QFrame.Box | QFrame.Raised) wlogframe.setLineWidth(1) # listview of log entries self.etw = LogEntryTree(cw) log_lo.addWidget(self.etw, 1) self.etw.header().setDefaultSectionSize(128) self.etw.header().setMovable(False) self.etw.setHeaderLabels(["date", "entry title", "comment"]) if hasattr(QHeaderView, 'ResizeToContents'): self.etw.header().setResizeMode(0, QHeaderView.ResizeToContents) else: self.etw.header().setResizeMode(0, QHeaderView.Custom) self.etw.header().resizeSection(0, 120) self.etw.header().setResizeMode(1, QHeaderView.Interactive) self.etw.header().setResizeMode(2, QHeaderView.Stretch) self.etw.header().show() try: self.etw.setAllColumnsShowFocus(True) except AttributeError: pass; # Qt 4.2+ # self.etw.setShowToolTips(True) self.etw.setSortingEnabled(False) # self.etw.setColumnAlignment(2,Qt.AlignLeft|Qt.AlignTop) self.etw.setSelectionMode(QTreeWidget.ExtendedSelection) self.etw.setRootIsDecorated(True) self.connect(self.etw, SIGNAL("itemSelectionChanged()"), self._entrySelectionChanged) self.connect(self.etw, SIGNAL("itemActivated(QTreeWidgetItem*,int)"), self._viewEntryItem) self.connect(self.etw, SIGNAL("itemContextMenuRequested"), self._showItemContextMenu) # create popup menu for data products self._archived_dp_menu = menu = QMenu(self) self._archived_dp_menu_title = QLabel() self._archived_dp_menu_title.setMargin(5) self._archived_dp_menu_title_wa = wa = QWidgetAction(self) wa.setDefaultWidget(self._archived_dp_menu_title) menu.addAction(wa) menu.addSeparator() menu.addAction(pixmaps.editcopy.icon(), "Restore file(s) from archived copy", self._restoreItemFromArchive) menu.addAction(pixmaps.editpaste.icon(), "Copy pathname of archived copy to clipboard", self._copyItemToClipboard) self._current_item = None # create popup menu for entries self._entry_menu = menu = QMenu(self) self._entry_menu_title = QLabel() self._entry_menu_title.setMargin(5) self._entry_menu_title_wa = wa = QWidgetAction(self) wa.setDefaultWidget(self._entry_menu_title) menu.addAction(wa) menu.addSeparator() menu.addAction(pixmaps.filefind.icon(), "View this log entry", self._viewEntryItem) menu.addAction(pixmaps.editdelete.icon(), "Delete this log entry", self._deleteSelectedEntries) # buttons at bottom log_lo.addSpacing(5) btnlo = QHBoxLayout() log_lo.addLayout(btnlo) self.wnewbtn = QPushButton(pixmaps.filenew.icon(), "New entry...", cw) self.wnewbtn.setToolTip("Click to add a new log entry.") # self.wnewbtn.setFlat(True) self.wnewbtn.setEnabled(False) btnlo.addWidget(self.wnewbtn) btnlo.addSpacing(5) self.weditbtn = QPushButton(pixmaps.filefind.icon(), "View entry...", cw) self.weditbtn.setToolTip("Click to view or edit the selected log entry/") # self.weditbtn.setFlat(True) self.weditbtn.setEnabled(False) self.connect(self.weditbtn, SIGNAL("clicked()"), self._viewEntryItem) btnlo.addWidget(self.weditbtn) btnlo.addSpacing(5) self.wdelbtn = QPushButton(pixmaps.editdelete.icon(), "Delete", cw) self.wdelbtn.setToolTip("Click to delete the selected log entry or entries.") # self.wdelbtn.setFlat(True) self.wdelbtn.setEnabled(False) self.connect(self.wdelbtn, SIGNAL("clicked()"), self._deleteSelectedEntries) btnlo.addWidget(self.wdelbtn) # enable status line self.statusBar().show() Purr.progressMessage = self.message self._prev_msg = None # editor dialog for new entry self.new_entry_dialog = Purr.Editors.NewLogEntryDialog(self) self.connect(self.new_entry_dialog, SIGNAL("newLogEntry"), self._newLogEntry) self.connect(self.new_entry_dialog, SIGNAL("filesSelected"), self._addDPFiles) self.connect(self.wnewbtn, SIGNAL("clicked()"), self.new_entry_dialog.show) self.connect(self.new_entry_dialog, SIGNAL("shown"), self._checkPounceStatus) # entry viewer dialog self.view_entry_dialog = Purr.Editors.ExistingLogEntryDialog(self) self.connect(self.view_entry_dialog, SIGNAL("previous()"), self._viewPrevEntry) self.connect(self.view_entry_dialog, SIGNAL("next()"), self._viewNextEntry) self.connect(self.view_entry_dialog, SIGNAL("viewPath"), self._viewPath) self.connect(self.view_entry_dialog, SIGNAL("filesSelected"), self._addDPFilesToOldEntry) self.connect(self.view_entry_dialog, SIGNAL("entryChanged"), self._entryChanged) # saving a data product to an older entry will automatically drop it from the # new entry dialog self.connect(self.view_entry_dialog, SIGNAL("creatingDataProduct"), self.new_entry_dialog.dropDataProducts) # resize selves width = Config.getint('main-window-width', 512) height = Config.getint('main-window-height', 512) self.resize(QSize(width, height)) # create timer for pouncing self._timer = QTimer(self) self.connect(self._timer, SIGNAL("timeout()"), self._rescan) # create dict mapping index.html paths to entry numbers self._index_paths = {} def resizeEvent(self, ev): QMainWindow.resizeEvent(self, ev) sz = ev.size() Config.set('main-window-width', sz.width()) Config.set('main-window-height', sz.height()) def closeEvent(self, ev): if self._hide_on_close: ev.ignore() self.hide() self.new_entry_dialog.hide() else: if self.purrer: self.purrer.detach() return QMainWindow.closeEvent(self, ev) def message(self, msg, ms=2000, sub=False): if sub: if self._prev_msg: msg = ": ".join((self._prev_msg, msg)) else: self._prev_msg = msg self.statusBar().showMessage(msg, ms) QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) def _changeWatchedDirState(self, pathname, watching): self.purrer.setWatchingState(pathname, watching) # update dialogs if dir list has changed if watching == Purr.REMOVED: self.purrpipes.pop(pathname) dirs = [path for path, state in self.purrer.watchedDirectories()] self.new_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.setDefaultDirs(*dirs) pass def _showAddDirectoryDialog(self): dd = str(QFileDialog.getExistingDirectory(self, "PURR: Add a directory to monitor")).strip() if dd: # adds a watched directory. Default initial setting of 'watching' is POUNCE if all # directories are in POUNCE state, or WATCHED otherwise. watching = max(Purr.WATCHED, min([state for path, state in self.purrer.watchedDirectories()] or [Purr.WATCHED])) self.purrer.addWatchedDirectory(dd, watching) self.purrpipes[dd] = Purr.Pipe.open(dd) self.wdirlist.add(dd, watching) # update dialogs since dir list has changed dirs = [path for path, state in self.purrer.watchedDirectories()] self.new_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.setDefaultDirs(*dirs) def detachPurrlog(self): self.wdirlist.clear() self.purrer and self.purrer.detach() self.purrer = None def hasPurrlog(self): return bool(self.purrer) def attachPurrlog(self, purrlog, watchdirs=[]): """Attaches Purr to the given purrlog directory. Arguments are passed to Purrer object as is.""" # check purrer stack for a Purrer already watching this directory dprint(1, "attaching to purrlog", purrlog) for i, purrer in enumerate(self.purrer_stack): if os.path.samefile(purrer.logdir, purrlog): dprint(1, "Purrer object found on stack (#%d),reusing\n", i) # found? move to front of stack self.purrer_stack.pop(i) self.purrer_stack.insert(0, purrer) # update purrer with watched directories, in case they have changed for dd in (watchdirs or []): purrer.addWatchedDirectory(dd, watching=None) break # no purrer found, make a new one else: dprint(1, "creating new Purrer object") try: purrer = Purr.Purrer(purrlog, watchdirs) except Purr.Purrer.LockedError as err: # check that we could attach, display message if not QMessageBox.warning(self, "Catfight!", """<P><NOBR>It appears that another PURR process (%s)</NOBR> is already attached to <tt>%s</tt>, so we're not allowed to touch it. You should exit the other PURR process first.</P>""" % (err.args[0], os.path.abspath(purrlog)), QMessageBox.Ok, 0) return False except Purr.Purrer.LockFailError as err: QMessageBox.warning(self, "Failed to obtain lock", """<P><NOBR>PURR was unable to obtain a lock</NOBR> on directory <tt>%s</tt> (error was "%s"). The most likely cause is insufficient permissions.</P>""" % ( os.path.abspath(purrlog), err.args[0]), QMessageBox.Ok, 0) return False self.purrer_stack.insert(0, purrer) # discard end of stack self.purrer_stack = self.purrer_stack[:3] # attach signals self.connect(purrer, SIGNAL("disappearedFile"), self.new_entry_dialog.dropDataProducts) self.connect(purrer, SIGNAL("disappearedFile"), self.view_entry_dialog.dropDataProducts) # have we changed the current purrer? Update our state then # reopen Purr pipes self.purrpipes = {} for dd, state in purrer.watchedDirectories(): self.purrpipes[dd] = Purr.Pipe.open(dd) if purrer is not self.purrer: self.message("Attached to %s" % purrer.logdir, ms=10000) dprint(1, "current Purrer changed, updating state") # set window title path = Kittens.utils.collapseuser(os.path.join(purrer.logdir, '')) self.setWindowTitle("PURR - %s" % path) # other init self.purrer = purrer self.new_entry_dialog.hide() self.new_entry_dialog.reset() dirs = [path for path, state in purrer.watchedDirectories()] self.new_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.hide() self.viewer_dialog.hide() self._viewing_ientry = None self._setEntries(self.purrer.getLogEntries()) # print self._index_paths self._viewer_timestamp = None self._updateViewer() self._updateNames() # update directory widgets self.wdirlist.clear() for pathname, state in purrer.watchedDirectories(): self.wdirlist.add(pathname, state) # Reset _pounce to false -- this will cause checkPounceStatus() into a rescan self._pounce = False self._checkPounceStatus() return True def setLogTitle(self, title): if self.purrer: if title != self.purrer.logtitle: self.purrer.setLogTitle(title) self._updateViewer() self._updateNames() def _updateNames(self): self.wnewbtn.setEnabled(True) self.wviewlog.setEnabled(True) self._about_dialog.setText(self.about_message + """ <P>Your current log resides in:<PRE> <tt>%s</tt></PRE>To see your log in all its HTML-rendered glory, point your browser to <tt>index.html</tt> therein, or use the handy "View" button provided by PURR.</P> <P>Your current working directories are:</P> <P>%s</P> """ % (self.purrer.logdir, "".join(["<PRE> <tt>%s</tt></PRE>" % name for name, state in self.purrer.watchedDirectories()]) )) title = self.purrer.logtitle or "Unnamed log" self.title_editor.setText(title) self.viewer_dialog.setWindowTitle(title) def _showViewerDialog(self): self._updateViewer(True) self.viewer_dialog.show() @staticmethod def fileModTime(path): try: return os.path.getmtime(path) except: return None def _updateViewer(self, force=False): """Updates the viewer dialog. If dialog is not visible and force=False, does nothing. Otherwise, checks the mtime of the current purrer index.html file against self._viewer_timestamp. If it is newer, reloads it. """ if not force and not self.viewer_dialog.isVisible(): return # default text if nothing is found path = self.purrer.indexfile mtime = self.fileModTime(path) # return if file is older than our content if mtime and mtime <= (self._viewer_timestamp or 0): return busy = BusyIndicator() self.viewer_dialog.setDocument(path, empty= "<P>Nothing in the log yet. Try adding some log entries.</P>") self.viewer_dialog.reload() self.viewer_dialog.setLabel("""<P>Below is your full HTML-rendered log. Note that this is only a bare-bones viewer, so only a limited set of links will work. For a fully-functional view, use a proper HTML browser to look at the index file residing here:<BR> <tt>%s</tt></P> """ % self.purrer.indexfile) self._viewer_timestamp = mtime def _setEntries(self, entries): self.etw.clear() item = None self._index_paths = {} self._index_paths[os.path.abspath(self.purrer.indexfile)] = -1 for i, entry in enumerate(entries): item = self._addEntryItem(entry, i, item) self._index_paths[os.path.abspath(entry.index_file)] = i self.etw.resizeColumnToContents(0) def _titleChanged(self): self.setLogTitle(str(self.title_editor.text())) def _checkPounceStatus(self): ## pounce = bool([ entry for entry in self._dir_entries.itervalues() if entry.watching ]) pounce = bool([path for path, state in self.purrer.watchedDirectories() if state >= Purr.WATCHED]) # rescan, if going from not-pounce to pounce if pounce and not self._pounce: self._rescan() self._pounce = pounce # start timer -- we need it running to check the purr pipe, anyway self._timer.start(2000) def _forceRescan(self): if not self.purrer: self.attachDirectory('.') self._rescan(force=True) def _rescan(self, force=False): if not self.purrer: return # if pounce is on, tell the Purrer to rescan directories if self._pounce or force: dps = self.purrer.rescan() if dps: filenames = [dp.filename for dp in dps] dprint(2, "new data products:", filenames) self.message("Pounced on " + ", ".join(filenames)) if self.new_entry_dialog.addDataProducts(dps): dprint(2, "showing dialog") self.new_entry_dialog.show() # else read stuff from pipe for pipe in self.purrpipes.values(): do_show = False for command, show, content in pipe.read(): if command == "title": self.new_entry_dialog.suggestTitle(content) elif command == "comment": self.new_entry_dialog.addComment(content) elif command == "pounce": self.new_entry_dialog.addDataProducts(self.purrer.makeDataProducts( [(content, not show)], unbanish=True)) else: print("Unknown command received from Purr pipe: ", command) continue do_show = do_show or show if do_show: self.new_entry_dialog.show() def _addDPFiles(self, *files): """callback to add DPs corresponding to files.""" # quiet flag is always true self.new_entry_dialog.addDataProducts(self.purrer.makeDataProducts( [(file, True) for file in files], unbanish=True, unignore=True)) def _addDPFilesToOldEntry(self, *files): """callback to add DPs corresponding to files.""" # quiet flag is always true self.view_entry_dialog.addDataProducts(self.purrer.makeDataProducts( [(file, True) for file in files], unbanish=True, unignore=True)) def _entrySelectionChanged(self): selected = [item for item in self.etw.iterator(self.etw.Iterator.Selected) if item._ientry is not None] self.weditbtn.setEnabled(len(selected) == 1) self.wdelbtn.setEnabled(bool(selected)) def _viewEntryItem(self, item=None, *dum): """Pops up the viewer dialog for the entry associated with the given item. If 'item' is None, looks for a selected item in the listview. The dum arguments are for connecting this to QTreeWidget signals such as doubleClicked(). """ # if item not set, look for selected items in listview. Only 1 must be selected. select = True if item is None: selected = [item for item in self.etw.iterator(self.etw.Iterator.Selected) if item._ientry is not None] if len(selected) != 1: return item = selected[0] select = False; # already selected else: # make sure item is open -- the click will cause it to close self.etw.expandItem(item) # show dialog ientry = getattr(item, '_ientry', None) if ientry is not None: self._viewEntryNumber(ientry, select=select) def _viewEntryNumber(self, ientry, select=True): """views entry #ientry. Also selects entry in listview if select=True""" # pass entry to viewer dialog self._viewing_ientry = ientry entry = self.purrer.entries[ientry] busy = BusyIndicator() self.view_entry_dialog.viewEntry(entry, prev=ientry > 0 and self.purrer.entries[ientry - 1], next=ientry < len(self.purrer.entries) - 1 and self.purrer.entries[ientry + 1]) self.view_entry_dialog.show() # select entry in listview if select: self.etw.clearSelection() self.etw.setItemSelected(self.etw.topLevelItem(ientry), True) def _viewPrevEntry(self): if self._viewing_ientry is not None and self._viewing_ientry > 0: self._viewEntryNumber(self._viewing_ientry - 1) def _viewNextEntry(self): if self._viewing_ientry is not None and self._viewing_ientry < len(self.purrer.entries) - 1: self._viewEntryNumber(self._viewing_ientry + 1) def _viewPath(self, path): num = self._index_paths.get(os.path.abspath(path), None) if num is None: return elif num == -1: self.view_entry_dialog.hide() self._showViewerDialog() else: self._viewEntryNumber(num) def _showItemContextMenu(self, item, point, col): """Callback for contextMenuRequested() signal. Pops up item menu, if defined""" menu = getattr(item, '_menu', None) if menu: settitle = getattr(item, '_set_menu_title', None) if settitle: settitle() # self._current_item tells callbacks what item the menu was referring to point = self.etw.mapToGlobal(point) self._current_item = item self.etw.clearSelection() self.etw.setItemSelected(item, True) menu.exec_(point) else: self._current_item = None def _copyItemToClipboard(self): """Callback for item menu.""" if self._current_item is None: return dp = getattr(self._current_item, '_dp', None) if dp and dp.archived: path = dp.fullpath.replace(" ", "\\ ") QApplication.clipboard().setText(path, QClipboard.Clipboard) QApplication.clipboard().setText(path, QClipboard.Selection) def _restoreItemFromArchive(self): """Callback for item menu.""" if self._current_item is None: return dp = getattr(self._current_item, '_dp', None) if dp and dp.archived: dp.restore_from_archive(parent=self) def _deleteSelectedEntries(self): remaining_entries = [] del_entries = list(self.etw.iterator(self.etw.Iterator.Selected)) remaining_entries = list(self.etw.iterator(self.etw.Iterator.Unselected)) if not del_entries: return hide_viewer = bool([item for item in del_entries if self._viewing_ientry == item._ientry]) del_entries = [self.purrer.entries[self.etw.indexOfTopLevelItem(item)] for item in del_entries] remaining_entries = [self.purrer.entries[self.etw.indexOfTopLevelItem(item)] for item in remaining_entries] # ask for confirmation if len(del_entries) == 1: msg = """<P><NOBR>Permanently delete the log entry</NOBR> "%s"?</P>""" % del_entries[0].title if del_entries[0].dps: msg += """<P>%d data product(s) saved with this entry will be deleted as well.</P>""" % len(del_entries[0].dps) else: msg = """<P>Permanently delete the %d selected log entries?</P>""" % len(del_entries) ndp = 0 for entry in del_entries: ndp += len([dp for dp in entry.dps if not dp.ignored]) if ndp: msg += """<P>%d data product(s) saved with these entries will be deleted as well.</P>""" % ndp if QMessageBox.warning(self, "Deleting log entries", msg, QMessageBox.Yes, QMessageBox.No) != QMessageBox.Yes: return if hide_viewer: self.view_entry_dialog.hide() # reset entries in purrer and in our log window self._setEntries(remaining_entries) self.purrer.deleteLogEntries(del_entries) # self.purrer.setLogEntries(remaining_entries) # log will have changed, so update the viewer self._updateViewer() # delete entry files for entry in del_entries: entry.remove_directory() def _addEntryItem(self, entry, number, after): item = entry.tw_item = QTreeWidgetItem(self.etw, after) timelabel = self._make_time_label(entry.timestamp) item.setText(0, timelabel) item.setText(1, " " + (entry.title or "")) item.setToolTip(1, entry.title) if entry.comment: item.setText(2, " " + entry.comment.split('\n')[0]) item.setToolTip(2, "<P>" + entry.comment.replace("<", "<").replace(">", ">"). \ replace("\n\n", "</P><P>").replace("\n", "</P><P>") + "</P>") item._ientry = number item._dp = None item._menu = self._entry_menu item._set_menu_title = lambda: self._entry_menu_title.setText('"%s"' % entry.title) # now make subitems for DPs subitem = None for dp in entry.dps: if not dp.ignored: subitem = self._addDPSubItem(dp, item, subitem) self.etw.collapseItem(item) self.etw.header().headerDataChanged(Qt.Horizontal, 0, 2) return item def _addDPSubItem(self, dp, parent, after): item = QTreeWidgetItem(parent, after) item.setText(1, dp.filename) item.setToolTip(1, dp.filename) item.setText(2, dp.comment or "") item.setToolTip(2, dp.comment or "") item._ientry = None item._dp = dp item._menu = self._archived_dp_menu item._set_menu_title = lambda: self._archived_dp_menu_title.setText(os.path.basename(dp.filename)) return item def _make_time_label(self, timestamp): return time.strftime("%b %d %H:%M", time.localtime(timestamp)) def _newLogEntry(self, entry): """This is called when a new log entry is created""" # add entry to purrer self.purrer.addLogEntry(entry) # add entry to listview if it is not an ignored entry # (ignored entries only carry information about DPs to be ignored) if not entry.ignore: if self.etw.topLevelItemCount(): lastitem = self.etw.topLevelItem(self.etw.topLevelItemCount() - 1) else: lastitem = None self._addEntryItem(entry, len(self.purrer.entries) - 1, lastitem) self._index_paths[os.path.abspath(entry.index_file)] = len(self.purrer.entries) - 1 # log will have changed, so update the viewer if not entry.ignore: self._updateViewer() self.show() def _entryChanged(self, entry): """This is called when a log entry is changed""" # resave the log self.purrer.save() # redo entry item if entry.tw_item: number = entry.tw_item._ientry entry.tw_item = None self.etw.takeTopLevelItem(number) if number: after = self.etw.topLevelItem(number - 1) else: after = None self._addEntryItem(entry, number, after) # log will have changed, so update the viewer self._updateViewer() def _regenerateLog(self): if QMessageBox.question(self.viewer_dialog, "Regenerate log", """<P><NOBR>Do you really want to regenerate the entire</NOBR> log? This can be a time-consuming operation.</P>""", QMessageBox.Yes, QMessageBox.No) != QMessageBox.Yes: return self.purrer.save(refresh=True) self._updateViewer()
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)) _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 "")))
class Dialog(QDialog): def __init__(self, title, widget=None, closeButton=True, keySequence=None, isDialog=False, icon=None): QDialog.__init__(self, ctx.mainScreen) self.setObjectName("dialog") self.isDialog = isDialog self.layout = QVBoxLayout() self.setLayout(self.layout) self.wlayout = QHBoxLayout() if icon: self.setStyleSheet( """QDialog QLabel{ margin-left:16px;margin-right:10px} QDialog#dialog {background-image:url(':/images/%s.png'); background-repeat:no-repeat; background-position: top left; padding-left:500px;} """ % icon) self.windowTitle = windowTitle(self, closeButton) self.setTitle(title) self.layout.setMargin(0) self.layout.addWidget(self.windowTitle) if widget: self.addWidget(widget) QObject.connect(widget, SIGNAL("finished(int)"), self.reject) QObject.connect(widget, SIGNAL("resizeDialog(int,int)"), self.resize) if closeButton: QObject.connect(self.windowTitle.pushButton, SIGNAL("clicked()"), self.reject) if keySequence: shortCut = QShortcut(keySequence, self) QObject.connect(shortCut, SIGNAL("activated()"), self.reject) QMetaObject.connectSlotsByName(self) self.resize(10, 10) def setTitle(self, title): self.windowTitle.label.setText(title) def addWidget(self, widget): self.content = widget self.wlayout.addWidget(self.content) if self.isDialog: widget.setStyleSheet("QMessageBox { background:none }") self.layout.addItem( QSpacerItem(10, 10, QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)) self.layout.setContentsMargins(0, 0, 0, 8) self.layout.addLayout(self.wlayout) def setCentered(self): self.move(ctx.mainScreen.width() / 2 - self.width() / 2, ctx.mainScreen.height() / 2 - self.height() / 2) def exec_(self): QTimer.singleShot(0, self.setCentered) return QDialog.exec_(self)
def __init__(self, parent, hide_on_close=False): QMainWindow.__init__(self, parent) self._hide_on_close = hide_on_close # replace the BusyIndicator class with a GUI-aware one Purr.BusyIndicator = BusyIndicator self._pounce = False # we keep a small stack of previously active purrers. This makes directory changes # faster (when going back and forth between dirs) # current purrer self.purrer = None self.purrer_stack = [] # Purr pipes for receiving remote commands self.purrpipes = {} # init GUI self.setWindowTitle("PURR") self.setWindowIcon(pixmaps.purr_logo.icon()) cw = QWidget(self) self.setCentralWidget(cw) cwlo = QVBoxLayout(cw) cwlo.setContentsMargins(0, 0, 0, 0) cwlo.setMargin(5) cwlo.setSpacing(0) toplo = QHBoxLayout() cwlo.addLayout(toplo) # About dialog self._about_dialog = QMessageBox(self) self._about_dialog.setWindowTitle("About PURR") self._about_dialog.setText(self.about_message + """ <P>PURR is not watching any directories right now. You may need to restart it, and give it some directory names on the command line.</P>""") self._about_dialog.setIconPixmap(pixmaps.purr_logo.pm()) # Log viewer dialog self.viewer_dialog = HTMLViewerDialog( self, config_name="log-viewer", buttons= [(pixmaps.blue_round_reload, "Regenerate", """<P>Regenerates your log's HTML code from scratch. This can be useful if your PURR version has changed, or if there was an error of some kind the last time the files were generated.</P> """)]) self._viewer_timestamp = None self.connect(self.viewer_dialog, SIGNAL("Regenerate"), self._regenerateLog) self.connect(self.viewer_dialog, SIGNAL("viewPath"), self._viewPath) # Log title toolbar title_tb = QToolBar(cw) title_tb.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) title_tb.setIconSize(QSize(16, 16)) cwlo.addWidget(title_tb) title_label = QLabel("Purrlog title:", title_tb) title_tb.addWidget(title_label) self.title_editor = QLineEdit(title_tb) title_tb.addWidget(self.title_editor) self.connect(self.title_editor, SIGNAL("editingFinished()"), self._titleChanged) tip = """<P>This is your current log title. To rename the log, enter new name here and press Enter.</P>""" title_label.setToolTip(tip) self.title_editor.setToolTip(tip) self.wviewlog = title_tb.addAction(pixmaps.openbook.icon(), "View", self._showViewerDialog) self.wviewlog.setToolTip( "Click to see an HTML rendering of your current log.") qa = title_tb.addAction(pixmaps.purr_logo.icon(), "About...", self._about_dialog.exec_) qa.setToolTip( "<P>Click to see the About... dialog, which will tell you something about PURR.</P>" ) self.wdirframe = QFrame(cw) cwlo.addWidget(self.wdirframe) self.dirs_lo = QVBoxLayout(self.wdirframe) self.dirs_lo.setMargin(5) self.dirs_lo.setContentsMargins(5, 0, 5, 5) self.dirs_lo.setSpacing(0) self.wdirframe.setFrameStyle(QFrame.Box | QFrame.Raised) self.wdirframe.setLineWidth(1) ## Directories toolbar dirs_tb = QToolBar(self.wdirframe) dirs_tb.setToolButtonStyle(Qt.ToolButtonIconOnly) dirs_tb.setIconSize(QSize(16, 16)) self.dirs_lo.addWidget(dirs_tb) label = QLabel("Monitoring directories:", dirs_tb) self._dirs_tip = """<P>PURR can monitor your working directories for new or updated files. If there's a checkmark next to the directory name in this list, PURR is monitoring it.</P> <P>If the checkmark is grey, PURR is monitoring things unobtrusively. When a new or updated file is detected in he monitored directory, it is quietly added to the list of files in the "New entry" window, even if this window is not currently visible.</P> <P>If the checkmark is black, PURR will be more obtrusive. Whenever a new or updated file is detected, the "New entry" window will pop up automatically. This is called "pouncing", and some people find it annoying.</P> """ label.setToolTip(self._dirs_tip) label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) dirs_tb.addWidget(label) # add directory list widget self.wdirlist = DirectoryListWidget(self.wdirframe) self.wdirlist.setToolTip(self._dirs_tip) QObject.connect(self.wdirlist, SIGNAL("directoryStateChanged"), self._changeWatchedDirState) self.dirs_lo.addWidget(self.wdirlist) # self.wdirlist.setMaximumSize(1000000,64) # add directory button add = dirs_tb.addAction(pixmaps.list_add.icon(), "Add", self._showAddDirectoryDialog) add.setToolTip( "<P>Click to add another directory to be monitored.</P>") # remove directory button delbtn = dirs_tb.addAction(pixmaps.list_remove.icon(), "Remove", self.wdirlist.removeCurrent) delbtn.setEnabled(False) delbtn.setToolTip( "<P>Click to removed the currently selected directory from the list.</P>" ) QObject.connect(self.wdirlist, SIGNAL("hasSelection"), delbtn.setEnabled) # # qa = dirs_tb.addAction(pixmaps.blue_round_reload.icon(),"Rescan",self._forceRescan) # # qa.setToolTip("Click to rescan the directories for any new or updated files.") # self.wshownew = QCheckBox("show new files",dirs_tb) # dirs_tb.addWidget(self.wshownew) # self.wshownew.setCheckState(Qt.Checked) # self.wshownew.setToolTip("""<P>If this is checked, the "New entry" window will pop up automatically whenever # new or updated files are detected. If this is unchecked, the files will be added to the window quietly # and unobtrusively; you can show the window manually by clicking on the "New entry..." button below.</P>""") # self._dir_entries = {} cwlo.addSpacing(5) wlogframe = QFrame(cw) cwlo.addWidget(wlogframe) log_lo = QVBoxLayout(wlogframe) log_lo.setMargin(5) log_lo.setContentsMargins(5, 5, 5, 5) log_lo.setSpacing(0) wlogframe.setFrameStyle(QFrame.Box | QFrame.Raised) wlogframe.setLineWidth(1) # listview of log entries self.etw = LogEntryTree(cw) log_lo.addWidget(self.etw, 1) self.etw.header().setDefaultSectionSize(128) self.etw.header().setMovable(False) self.etw.setHeaderLabels(["date", "entry title", "comment"]) if hasattr(QHeaderView, 'ResizeToContents'): self.etw.header().setResizeMode(0, QHeaderView.ResizeToContents) else: self.etw.header().setResizeMode(0, QHeaderView.Custom) self.etw.header().resizeSection(0, 120) self.etw.header().setResizeMode(1, QHeaderView.Interactive) self.etw.header().setResizeMode(2, QHeaderView.Stretch) self.etw.header().show() try: self.etw.setAllColumnsShowFocus(True) except AttributeError: pass # Qt 4.2+ # self.etw.setShowToolTips(True) self.etw.setSortingEnabled(False) # self.etw.setColumnAlignment(2,Qt.AlignLeft|Qt.AlignTop) self.etw.setSelectionMode(QTreeWidget.ExtendedSelection) self.etw.setRootIsDecorated(True) self.connect(self.etw, SIGNAL("itemSelectionChanged()"), self._entrySelectionChanged) self.connect(self.etw, SIGNAL("itemActivated(QTreeWidgetItem*,int)"), self._viewEntryItem) self.connect(self.etw, SIGNAL("itemContextMenuRequested"), self._showItemContextMenu) # create popup menu for data products self._archived_dp_menu = menu = QMenu(self) self._archived_dp_menu_title = QLabel() self._archived_dp_menu_title.setMargin(5) self._archived_dp_menu_title_wa = wa = QWidgetAction(self) wa.setDefaultWidget(self._archived_dp_menu_title) menu.addAction(wa) menu.addSeparator() menu.addAction(pixmaps.editcopy.icon(), "Restore file(s) from archived copy", self._restoreItemFromArchive) menu.addAction(pixmaps.editpaste.icon(), "Copy pathname of archived copy to clipboard", self._copyItemToClipboard) self._current_item = None # create popup menu for entries self._entry_menu = menu = QMenu(self) self._entry_menu_title = QLabel() self._entry_menu_title.setMargin(5) self._entry_menu_title_wa = wa = QWidgetAction(self) wa.setDefaultWidget(self._entry_menu_title) menu.addAction(wa) menu.addSeparator() menu.addAction(pixmaps.filefind.icon(), "View this log entry", self._viewEntryItem) menu.addAction(pixmaps.editdelete.icon(), "Delete this log entry", self._deleteSelectedEntries) # buttons at bottom log_lo.addSpacing(5) btnlo = QHBoxLayout() log_lo.addLayout(btnlo) self.wnewbtn = QPushButton(pixmaps.filenew.icon(), "New entry...", cw) self.wnewbtn.setToolTip("Click to add a new log entry.") # self.wnewbtn.setFlat(True) self.wnewbtn.setEnabled(False) btnlo.addWidget(self.wnewbtn) btnlo.addSpacing(5) self.weditbtn = QPushButton(pixmaps.filefind.icon(), "View entry...", cw) self.weditbtn.setToolTip( "Click to view or edit the selected log entry/") # self.weditbtn.setFlat(True) self.weditbtn.setEnabled(False) self.connect(self.weditbtn, SIGNAL("clicked()"), self._viewEntryItem) btnlo.addWidget(self.weditbtn) btnlo.addSpacing(5) self.wdelbtn = QPushButton(pixmaps.editdelete.icon(), "Delete", cw) self.wdelbtn.setToolTip( "Click to delete the selected log entry or entries.") # self.wdelbtn.setFlat(True) self.wdelbtn.setEnabled(False) self.connect(self.wdelbtn, SIGNAL("clicked()"), self._deleteSelectedEntries) btnlo.addWidget(self.wdelbtn) # enable status line self.statusBar().show() Purr.progressMessage = self.message self._prev_msg = None # editor dialog for new entry self.new_entry_dialog = Purr.Editors.NewLogEntryDialog(self) self.connect(self.new_entry_dialog, SIGNAL("newLogEntry"), self._newLogEntry) self.connect(self.new_entry_dialog, SIGNAL("filesSelected"), self._addDPFiles) self.connect(self.wnewbtn, SIGNAL("clicked()"), self.new_entry_dialog.show) self.connect(self.new_entry_dialog, SIGNAL("shown"), self._checkPounceStatus) # entry viewer dialog self.view_entry_dialog = Purr.Editors.ExistingLogEntryDialog(self) self.connect(self.view_entry_dialog, SIGNAL("previous()"), self._viewPrevEntry) self.connect(self.view_entry_dialog, SIGNAL("next()"), self._viewNextEntry) self.connect(self.view_entry_dialog, SIGNAL("viewPath"), self._viewPath) self.connect(self.view_entry_dialog, SIGNAL("filesSelected"), self._addDPFilesToOldEntry) self.connect(self.view_entry_dialog, SIGNAL("entryChanged"), self._entryChanged) # saving a data product to an older entry will automatically drop it from the # new entry dialog self.connect(self.view_entry_dialog, SIGNAL("creatingDataProduct"), self.new_entry_dialog.dropDataProducts) # resize selves width = Config.getint('main-window-width', 512) height = Config.getint('main-window-height', 512) self.resize(QSize(width, height)) # create timer for pouncing self._timer = QTimer(self) self.connect(self._timer, SIGNAL("timeout()"), self._rescan) # create dict mapping index.html paths to entry numbers self._index_paths = {}
class MainWindow(QMainWindow): about_message = """ <P>PURR ("<B>P</B>URR is <B>U</B>seful for <B>R</B>emembering <B>R</B>eductions", for those working with a stable version, or "<B>P</B>URR <B>U</B>sually <B>R</B>emembers <B>R</B>eductions", for those working with a development version, or "<B>P</B>URR <B>U</B>sed to <B>R</B>emember <B>R</B>eductions", for those working with a broken version) is a tool for automatically keeping a log of your data reduction operations. PURR will monitor your working directories for new or updated files (called "data products"), and upon seeing any, it can "pounce" -- that is, offer you the option of saving the files to a log, along with descriptive comments. It will then generate an HTML page with a pretty rendering of your log and data products.</P> """ def __init__(self, parent, hide_on_close=False): QMainWindow.__init__(self, parent) self._hide_on_close = hide_on_close # replace the BusyIndicator class with a GUI-aware one Purr.BusyIndicator = BusyIndicator self._pounce = False # we keep a small stack of previously active purrers. This makes directory changes # faster (when going back and forth between dirs) # current purrer self.purrer = None self.purrer_stack = [] # Purr pipes for receiving remote commands self.purrpipes = {} # init GUI self.setWindowTitle("PURR") self.setWindowIcon(pixmaps.purr_logo.icon()) cw = QWidget(self) self.setCentralWidget(cw) cwlo = QVBoxLayout(cw) cwlo.setContentsMargins(0, 0, 0, 0) cwlo.setMargin(5) cwlo.setSpacing(0) toplo = QHBoxLayout() cwlo.addLayout(toplo) # About dialog self._about_dialog = QMessageBox(self) self._about_dialog.setWindowTitle("About PURR") self._about_dialog.setText(self.about_message + """ <P>PURR is not watching any directories right now. You may need to restart it, and give it some directory names on the command line.</P>""") self._about_dialog.setIconPixmap(pixmaps.purr_logo.pm()) # Log viewer dialog self.viewer_dialog = HTMLViewerDialog( self, config_name="log-viewer", buttons= [(pixmaps.blue_round_reload, "Regenerate", """<P>Regenerates your log's HTML code from scratch. This can be useful if your PURR version has changed, or if there was an error of some kind the last time the files were generated.</P> """)]) self._viewer_timestamp = None self.connect(self.viewer_dialog, SIGNAL("Regenerate"), self._regenerateLog) self.connect(self.viewer_dialog, SIGNAL("viewPath"), self._viewPath) # Log title toolbar title_tb = QToolBar(cw) title_tb.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) title_tb.setIconSize(QSize(16, 16)) cwlo.addWidget(title_tb) title_label = QLabel("Purrlog title:", title_tb) title_tb.addWidget(title_label) self.title_editor = QLineEdit(title_tb) title_tb.addWidget(self.title_editor) self.connect(self.title_editor, SIGNAL("editingFinished()"), self._titleChanged) tip = """<P>This is your current log title. To rename the log, enter new name here and press Enter.</P>""" title_label.setToolTip(tip) self.title_editor.setToolTip(tip) self.wviewlog = title_tb.addAction(pixmaps.openbook.icon(), "View", self._showViewerDialog) self.wviewlog.setToolTip( "Click to see an HTML rendering of your current log.") qa = title_tb.addAction(pixmaps.purr_logo.icon(), "About...", self._about_dialog.exec_) qa.setToolTip( "<P>Click to see the About... dialog, which will tell you something about PURR.</P>" ) self.wdirframe = QFrame(cw) cwlo.addWidget(self.wdirframe) self.dirs_lo = QVBoxLayout(self.wdirframe) self.dirs_lo.setMargin(5) self.dirs_lo.setContentsMargins(5, 0, 5, 5) self.dirs_lo.setSpacing(0) self.wdirframe.setFrameStyle(QFrame.Box | QFrame.Raised) self.wdirframe.setLineWidth(1) ## Directories toolbar dirs_tb = QToolBar(self.wdirframe) dirs_tb.setToolButtonStyle(Qt.ToolButtonIconOnly) dirs_tb.setIconSize(QSize(16, 16)) self.dirs_lo.addWidget(dirs_tb) label = QLabel("Monitoring directories:", dirs_tb) self._dirs_tip = """<P>PURR can monitor your working directories for new or updated files. If there's a checkmark next to the directory name in this list, PURR is monitoring it.</P> <P>If the checkmark is grey, PURR is monitoring things unobtrusively. When a new or updated file is detected in he monitored directory, it is quietly added to the list of files in the "New entry" window, even if this window is not currently visible.</P> <P>If the checkmark is black, PURR will be more obtrusive. Whenever a new or updated file is detected, the "New entry" window will pop up automatically. This is called "pouncing", and some people find it annoying.</P> """ label.setToolTip(self._dirs_tip) label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) dirs_tb.addWidget(label) # add directory list widget self.wdirlist = DirectoryListWidget(self.wdirframe) self.wdirlist.setToolTip(self._dirs_tip) QObject.connect(self.wdirlist, SIGNAL("directoryStateChanged"), self._changeWatchedDirState) self.dirs_lo.addWidget(self.wdirlist) # self.wdirlist.setMaximumSize(1000000,64) # add directory button add = dirs_tb.addAction(pixmaps.list_add.icon(), "Add", self._showAddDirectoryDialog) add.setToolTip( "<P>Click to add another directory to be monitored.</P>") # remove directory button delbtn = dirs_tb.addAction(pixmaps.list_remove.icon(), "Remove", self.wdirlist.removeCurrent) delbtn.setEnabled(False) delbtn.setToolTip( "<P>Click to removed the currently selected directory from the list.</P>" ) QObject.connect(self.wdirlist, SIGNAL("hasSelection"), delbtn.setEnabled) # # qa = dirs_tb.addAction(pixmaps.blue_round_reload.icon(),"Rescan",self._forceRescan) # # qa.setToolTip("Click to rescan the directories for any new or updated files.") # self.wshownew = QCheckBox("show new files",dirs_tb) # dirs_tb.addWidget(self.wshownew) # self.wshownew.setCheckState(Qt.Checked) # self.wshownew.setToolTip("""<P>If this is checked, the "New entry" window will pop up automatically whenever # new or updated files are detected. If this is unchecked, the files will be added to the window quietly # and unobtrusively; you can show the window manually by clicking on the "New entry..." button below.</P>""") # self._dir_entries = {} cwlo.addSpacing(5) wlogframe = QFrame(cw) cwlo.addWidget(wlogframe) log_lo = QVBoxLayout(wlogframe) log_lo.setMargin(5) log_lo.setContentsMargins(5, 5, 5, 5) log_lo.setSpacing(0) wlogframe.setFrameStyle(QFrame.Box | QFrame.Raised) wlogframe.setLineWidth(1) # listview of log entries self.etw = LogEntryTree(cw) log_lo.addWidget(self.etw, 1) self.etw.header().setDefaultSectionSize(128) self.etw.header().setMovable(False) self.etw.setHeaderLabels(["date", "entry title", "comment"]) if hasattr(QHeaderView, 'ResizeToContents'): self.etw.header().setResizeMode(0, QHeaderView.ResizeToContents) else: self.etw.header().setResizeMode(0, QHeaderView.Custom) self.etw.header().resizeSection(0, 120) self.etw.header().setResizeMode(1, QHeaderView.Interactive) self.etw.header().setResizeMode(2, QHeaderView.Stretch) self.etw.header().show() try: self.etw.setAllColumnsShowFocus(True) except AttributeError: pass # Qt 4.2+ # self.etw.setShowToolTips(True) self.etw.setSortingEnabled(False) # self.etw.setColumnAlignment(2,Qt.AlignLeft|Qt.AlignTop) self.etw.setSelectionMode(QTreeWidget.ExtendedSelection) self.etw.setRootIsDecorated(True) self.connect(self.etw, SIGNAL("itemSelectionChanged()"), self._entrySelectionChanged) self.connect(self.etw, SIGNAL("itemActivated(QTreeWidgetItem*,int)"), self._viewEntryItem) self.connect(self.etw, SIGNAL("itemContextMenuRequested"), self._showItemContextMenu) # create popup menu for data products self._archived_dp_menu = menu = QMenu(self) self._archived_dp_menu_title = QLabel() self._archived_dp_menu_title.setMargin(5) self._archived_dp_menu_title_wa = wa = QWidgetAction(self) wa.setDefaultWidget(self._archived_dp_menu_title) menu.addAction(wa) menu.addSeparator() menu.addAction(pixmaps.editcopy.icon(), "Restore file(s) from archived copy", self._restoreItemFromArchive) menu.addAction(pixmaps.editpaste.icon(), "Copy pathname of archived copy to clipboard", self._copyItemToClipboard) self._current_item = None # create popup menu for entries self._entry_menu = menu = QMenu(self) self._entry_menu_title = QLabel() self._entry_menu_title.setMargin(5) self._entry_menu_title_wa = wa = QWidgetAction(self) wa.setDefaultWidget(self._entry_menu_title) menu.addAction(wa) menu.addSeparator() menu.addAction(pixmaps.filefind.icon(), "View this log entry", self._viewEntryItem) menu.addAction(pixmaps.editdelete.icon(), "Delete this log entry", self._deleteSelectedEntries) # buttons at bottom log_lo.addSpacing(5) btnlo = QHBoxLayout() log_lo.addLayout(btnlo) self.wnewbtn = QPushButton(pixmaps.filenew.icon(), "New entry...", cw) self.wnewbtn.setToolTip("Click to add a new log entry.") # self.wnewbtn.setFlat(True) self.wnewbtn.setEnabled(False) btnlo.addWidget(self.wnewbtn) btnlo.addSpacing(5) self.weditbtn = QPushButton(pixmaps.filefind.icon(), "View entry...", cw) self.weditbtn.setToolTip( "Click to view or edit the selected log entry/") # self.weditbtn.setFlat(True) self.weditbtn.setEnabled(False) self.connect(self.weditbtn, SIGNAL("clicked()"), self._viewEntryItem) btnlo.addWidget(self.weditbtn) btnlo.addSpacing(5) self.wdelbtn = QPushButton(pixmaps.editdelete.icon(), "Delete", cw) self.wdelbtn.setToolTip( "Click to delete the selected log entry or entries.") # self.wdelbtn.setFlat(True) self.wdelbtn.setEnabled(False) self.connect(self.wdelbtn, SIGNAL("clicked()"), self._deleteSelectedEntries) btnlo.addWidget(self.wdelbtn) # enable status line self.statusBar().show() Purr.progressMessage = self.message self._prev_msg = None # editor dialog for new entry self.new_entry_dialog = Purr.Editors.NewLogEntryDialog(self) self.connect(self.new_entry_dialog, SIGNAL("newLogEntry"), self._newLogEntry) self.connect(self.new_entry_dialog, SIGNAL("filesSelected"), self._addDPFiles) self.connect(self.wnewbtn, SIGNAL("clicked()"), self.new_entry_dialog.show) self.connect(self.new_entry_dialog, SIGNAL("shown"), self._checkPounceStatus) # entry viewer dialog self.view_entry_dialog = Purr.Editors.ExistingLogEntryDialog(self) self.connect(self.view_entry_dialog, SIGNAL("previous()"), self._viewPrevEntry) self.connect(self.view_entry_dialog, SIGNAL("next()"), self._viewNextEntry) self.connect(self.view_entry_dialog, SIGNAL("viewPath"), self._viewPath) self.connect(self.view_entry_dialog, SIGNAL("filesSelected"), self._addDPFilesToOldEntry) self.connect(self.view_entry_dialog, SIGNAL("entryChanged"), self._entryChanged) # saving a data product to an older entry will automatically drop it from the # new entry dialog self.connect(self.view_entry_dialog, SIGNAL("creatingDataProduct"), self.new_entry_dialog.dropDataProducts) # resize selves width = Config.getint('main-window-width', 512) height = Config.getint('main-window-height', 512) self.resize(QSize(width, height)) # create timer for pouncing self._timer = QTimer(self) self.connect(self._timer, SIGNAL("timeout()"), self._rescan) # create dict mapping index.html paths to entry numbers self._index_paths = {} def resizeEvent(self, ev): QMainWindow.resizeEvent(self, ev) sz = ev.size() Config.set('main-window-width', sz.width()) Config.set('main-window-height', sz.height()) def closeEvent(self, ev): if self._hide_on_close: ev.ignore() self.hide() self.new_entry_dialog.hide() else: if self.purrer: self.purrer.detach() return QMainWindow.closeEvent(self, ev) def message(self, msg, ms=2000, sub=False): if sub: if self._prev_msg: msg = ": ".join((self._prev_msg, msg)) else: self._prev_msg = msg self.statusBar().showMessage(msg, ms) QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) def _changeWatchedDirState(self, pathname, watching): self.purrer.setWatchingState(pathname, watching) # update dialogs if dir list has changed if watching == Purr.REMOVED: self.purrpipes.pop(pathname) dirs = [path for path, state in self.purrer.watchedDirectories()] self.new_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.setDefaultDirs(*dirs) pass def _showAddDirectoryDialog(self): dd = str( QFileDialog.getExistingDirectory( self, "PURR: Add a directory to monitor")).strip() if dd: # adds a watched directory. Default initial setting of 'watching' is POUNCE if all # directories are in POUNCE state, or WATCHED otherwise. watching = max( Purr.WATCHED, min([ state for path, state in self.purrer.watchedDirectories() ] or [Purr.WATCHED])) self.purrer.addWatchedDirectory(dd, watching) self.purrpipes[dd] = Purr.Pipe.open(dd) self.wdirlist.add(dd, watching) # update dialogs since dir list has changed dirs = [path for path, state in self.purrer.watchedDirectories()] self.new_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.setDefaultDirs(*dirs) def detachPurrlog(self): self.wdirlist.clear() self.purrer and self.purrer.detach() self.purrer = None def hasPurrlog(self): return bool(self.purrer) def attachPurrlog(self, purrlog, watchdirs=[]): """Attaches Purr to the given purrlog directory. Arguments are passed to Purrer object as is.""" # check purrer stack for a Purrer already watching this directory dprint(1, "attaching to purrlog", purrlog) for i, purrer in enumerate(self.purrer_stack): if os.path.samefile(purrer.logdir, purrlog): dprint(1, "Purrer object found on stack (#%d),reusing\n", i) # found? move to front of stack self.purrer_stack.pop(i) self.purrer_stack.insert(0, purrer) # update purrer with watched directories, in case they have changed for dd in (watchdirs or []): purrer.addWatchedDirectory(dd, watching=None) break # no purrer found, make a new one else: dprint(1, "creating new Purrer object") try: purrer = Purr.Purrer(purrlog, watchdirs) except Purr.Purrer.LockedError as err: # check that we could attach, display message if not QMessageBox.warning( self, "Catfight!", """<P><NOBR>It appears that another PURR process (%s)</NOBR> is already attached to <tt>%s</tt>, so we're not allowed to touch it. You should exit the other PURR process first.</P>""" % (err.args[0], os.path.abspath(purrlog)), QMessageBox.Ok, 0) return False except Purr.Purrer.LockFailError as err: QMessageBox.warning( self, "Failed to obtain lock", """<P><NOBR>PURR was unable to obtain a lock</NOBR> on directory <tt>%s</tt> (error was "%s"). The most likely cause is insufficient permissions.</P>""" % (os.path.abspath(purrlog), err.args[0]), QMessageBox.Ok, 0) return False self.purrer_stack.insert(0, purrer) # discard end of stack self.purrer_stack = self.purrer_stack[:3] # attach signals self.connect(purrer, SIGNAL("disappearedFile"), self.new_entry_dialog.dropDataProducts) self.connect(purrer, SIGNAL("disappearedFile"), self.view_entry_dialog.dropDataProducts) # have we changed the current purrer? Update our state then # reopen Purr pipes self.purrpipes = {} for dd, state in purrer.watchedDirectories(): self.purrpipes[dd] = Purr.Pipe.open(dd) if purrer is not self.purrer: self.message("Attached to %s" % purrer.logdir, ms=10000) dprint(1, "current Purrer changed, updating state") # set window title path = Kittens.utils.collapseuser(os.path.join(purrer.logdir, '')) self.setWindowTitle("PURR - %s" % path) # other init self.purrer = purrer self.new_entry_dialog.hide() self.new_entry_dialog.reset() dirs = [path for path, state in purrer.watchedDirectories()] self.new_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.setDefaultDirs(*dirs) self.view_entry_dialog.hide() self.viewer_dialog.hide() self._viewing_ientry = None self._setEntries(self.purrer.getLogEntries()) # print self._index_paths self._viewer_timestamp = None self._updateViewer() self._updateNames() # update directory widgets self.wdirlist.clear() for pathname, state in purrer.watchedDirectories(): self.wdirlist.add(pathname, state) # Reset _pounce to false -- this will cause checkPounceStatus() into a rescan self._pounce = False self._checkPounceStatus() return True def setLogTitle(self, title): if self.purrer: if title != self.purrer.logtitle: self.purrer.setLogTitle(title) self._updateViewer() self._updateNames() def _updateNames(self): self.wnewbtn.setEnabled(True) self.wviewlog.setEnabled(True) self._about_dialog.setText(self.about_message + """ <P>Your current log resides in:<PRE> <tt>%s</tt></PRE>To see your log in all its HTML-rendered glory, point your browser to <tt>index.html</tt> therein, or use the handy "View" button provided by PURR.</P> <P>Your current working directories are:</P> <P>%s</P> """ % (self.purrer.logdir, "".join([ "<PRE> <tt>%s</tt></PRE>" % name for name, state in self.purrer.watchedDirectories() ]))) title = self.purrer.logtitle or "Unnamed log" self.title_editor.setText(title) self.viewer_dialog.setWindowTitle(title) def _showViewerDialog(self): self._updateViewer(True) self.viewer_dialog.show() @staticmethod def fileModTime(path): try: return os.path.getmtime(path) except: return None def _updateViewer(self, force=False): """Updates the viewer dialog. If dialog is not visible and force=False, does nothing. Otherwise, checks the mtime of the current purrer index.html file against self._viewer_timestamp. If it is newer, reloads it. """ if not force and not self.viewer_dialog.isVisible(): return # default text if nothing is found path = self.purrer.indexfile mtime = self.fileModTime(path) # return if file is older than our content if mtime and mtime <= (self._viewer_timestamp or 0): return busy = BusyIndicator() self.viewer_dialog.setDocument( path, empty="<P>Nothing in the log yet. Try adding some log entries.</P>" ) self.viewer_dialog.reload() self.viewer_dialog.setLabel( """<P>Below is your full HTML-rendered log. Note that this is only a bare-bones viewer, so only a limited set of links will work. For a fully-functional view, use a proper HTML browser to look at the index file residing here:<BR> <tt>%s</tt></P> """ % self.purrer.indexfile) self._viewer_timestamp = mtime def _setEntries(self, entries): self.etw.clear() item = None self._index_paths = {} self._index_paths[os.path.abspath(self.purrer.indexfile)] = -1 for i, entry in enumerate(entries): item = self._addEntryItem(entry, i, item) self._index_paths[os.path.abspath(entry.index_file)] = i self.etw.resizeColumnToContents(0) def _titleChanged(self): self.setLogTitle(str(self.title_editor.text())) def _checkPounceStatus(self): ## pounce = bool([ entry for entry in self._dir_entries.itervalues() if entry.watching ]) pounce = bool([ path for path, state in self.purrer.watchedDirectories() if state >= Purr.WATCHED ]) # rescan, if going from not-pounce to pounce if pounce and not self._pounce: self._rescan() self._pounce = pounce # start timer -- we need it running to check the purr pipe, anyway self._timer.start(2000) def _forceRescan(self): if not self.purrer: self.attachDirectory('.') self._rescan(force=True) def _rescan(self, force=False): if not self.purrer: return # if pounce is on, tell the Purrer to rescan directories if self._pounce or force: dps = self.purrer.rescan() if dps: filenames = [dp.filename for dp in dps] dprint(2, "new data products:", filenames) self.message("Pounced on " + ", ".join(filenames)) if self.new_entry_dialog.addDataProducts(dps): dprint(2, "showing dialog") self.new_entry_dialog.show() # else read stuff from pipe for pipe in self.purrpipes.values(): do_show = False for command, show, content in pipe.read(): if command == "title": self.new_entry_dialog.suggestTitle(content) elif command == "comment": self.new_entry_dialog.addComment(content) elif command == "pounce": self.new_entry_dialog.addDataProducts( self.purrer.makeDataProducts([(content, not show)], unbanish=True)) else: print("Unknown command received from Purr pipe: ", command) continue do_show = do_show or show if do_show: self.new_entry_dialog.show() def _addDPFiles(self, *files): """callback to add DPs corresponding to files.""" # quiet flag is always true self.new_entry_dialog.addDataProducts( self.purrer.makeDataProducts([(file, True) for file in files], unbanish=True, unignore=True)) def _addDPFilesToOldEntry(self, *files): """callback to add DPs corresponding to files.""" # quiet flag is always true self.view_entry_dialog.addDataProducts( self.purrer.makeDataProducts([(file, True) for file in files], unbanish=True, unignore=True)) def _entrySelectionChanged(self): selected = [ item for item in self.etw.iterator(self.etw.Iterator.Selected) if item._ientry is not None ] self.weditbtn.setEnabled(len(selected) == 1) self.wdelbtn.setEnabled(bool(selected)) def _viewEntryItem(self, item=None, *dum): """Pops up the viewer dialog for the entry associated with the given item. If 'item' is None, looks for a selected item in the listview. The dum arguments are for connecting this to QTreeWidget signals such as doubleClicked(). """ # if item not set, look for selected items in listview. Only 1 must be selected. select = True if item is None: selected = [ item for item in self.etw.iterator(self.etw.Iterator.Selected) if item._ientry is not None ] if len(selected) != 1: return item = selected[0] select = False # already selected else: # make sure item is open -- the click will cause it to close self.etw.expandItem(item) # show dialog ientry = getattr(item, '_ientry', None) if ientry is not None: self._viewEntryNumber(ientry, select=select) def _viewEntryNumber(self, ientry, select=True): """views entry #ientry. Also selects entry in listview if select=True""" # pass entry to viewer dialog self._viewing_ientry = ientry entry = self.purrer.entries[ientry] busy = BusyIndicator() self.view_entry_dialog.viewEntry( entry, prev=ientry > 0 and self.purrer.entries[ientry - 1], next=ientry < len(self.purrer.entries) - 1 and self.purrer.entries[ientry + 1]) self.view_entry_dialog.show() # select entry in listview if select: self.etw.clearSelection() self.etw.setItemSelected(self.etw.topLevelItem(ientry), True) def _viewPrevEntry(self): if self._viewing_ientry is not None and self._viewing_ientry > 0: self._viewEntryNumber(self._viewing_ientry - 1) def _viewNextEntry(self): if self._viewing_ientry is not None and self._viewing_ientry < len( self.purrer.entries) - 1: self._viewEntryNumber(self._viewing_ientry + 1) def _viewPath(self, path): num = self._index_paths.get(os.path.abspath(path), None) if num is None: return elif num == -1: self.view_entry_dialog.hide() self._showViewerDialog() else: self._viewEntryNumber(num) def _showItemContextMenu(self, item, point, col): """Callback for contextMenuRequested() signal. Pops up item menu, if defined""" menu = getattr(item, '_menu', None) if menu: settitle = getattr(item, '_set_menu_title', None) if settitle: settitle() # self._current_item tells callbacks what item the menu was referring to point = self.etw.mapToGlobal(point) self._current_item = item self.etw.clearSelection() self.etw.setItemSelected(item, True) menu.exec_(point) else: self._current_item = None def _copyItemToClipboard(self): """Callback for item menu.""" if self._current_item is None: return dp = getattr(self._current_item, '_dp', None) if dp and dp.archived: path = dp.fullpath.replace(" ", "\\ ") QApplication.clipboard().setText(path, QClipboard.Clipboard) QApplication.clipboard().setText(path, QClipboard.Selection) def _restoreItemFromArchive(self): """Callback for item menu.""" if self._current_item is None: return dp = getattr(self._current_item, '_dp', None) if dp and dp.archived: dp.restore_from_archive(parent=self) def _deleteSelectedEntries(self): remaining_entries = [] del_entries = list(self.etw.iterator(self.etw.Iterator.Selected)) remaining_entries = list( self.etw.iterator(self.etw.Iterator.Unselected)) if not del_entries: return hide_viewer = bool([ item for item in del_entries if self._viewing_ientry == item._ientry ]) del_entries = [ self.purrer.entries[self.etw.indexOfTopLevelItem(item)] for item in del_entries ] remaining_entries = [ self.purrer.entries[self.etw.indexOfTopLevelItem(item)] for item in remaining_entries ] # ask for confirmation if len(del_entries) == 1: msg = """<P><NOBR>Permanently delete the log entry</NOBR> "%s"?</P>""" % del_entries[ 0].title if del_entries[0].dps: msg += """<P>%d data product(s) saved with this entry will be deleted as well.</P>""" % len( del_entries[0].dps) else: msg = """<P>Permanently delete the %d selected log entries?</P>""" % len( del_entries) ndp = 0 for entry in del_entries: ndp += len([dp for dp in entry.dps if not dp.ignored]) if ndp: msg += """<P>%d data product(s) saved with these entries will be deleted as well.</P>""" % ndp if QMessageBox.warning(self, "Deleting log entries", msg, QMessageBox.Yes, QMessageBox.No) != QMessageBox.Yes: return if hide_viewer: self.view_entry_dialog.hide() # reset entries in purrer and in our log window self._setEntries(remaining_entries) self.purrer.deleteLogEntries(del_entries) # self.purrer.setLogEntries(remaining_entries) # log will have changed, so update the viewer self._updateViewer() # delete entry files for entry in del_entries: entry.remove_directory() def _addEntryItem(self, entry, number, after): item = entry.tw_item = QTreeWidgetItem(self.etw, after) timelabel = self._make_time_label(entry.timestamp) item.setText(0, timelabel) item.setText(1, " " + (entry.title or "")) item.setToolTip(1, entry.title) if entry.comment: item.setText(2, " " + entry.comment.split('\n')[0]) item.setToolTip(2, "<P>" + entry.comment.replace("<", "<").replace(">", ">"). \ replace("\n\n", "</P><P>").replace("\n", "</P><P>") + "</P>") item._ientry = number item._dp = None item._menu = self._entry_menu item._set_menu_title = lambda: self._entry_menu_title.setText( '"%s"' % entry.title) # now make subitems for DPs subitem = None for dp in entry.dps: if not dp.ignored: subitem = self._addDPSubItem(dp, item, subitem) self.etw.collapseItem(item) self.etw.header().headerDataChanged(Qt.Horizontal, 0, 2) return item def _addDPSubItem(self, dp, parent, after): item = QTreeWidgetItem(parent, after) item.setText(1, dp.filename) item.setToolTip(1, dp.filename) item.setText(2, dp.comment or "") item.setToolTip(2, dp.comment or "") item._ientry = None item._dp = dp item._menu = self._archived_dp_menu item._set_menu_title = lambda: self._archived_dp_menu_title.setText( os.path.basename(dp.filename)) return item def _make_time_label(self, timestamp): return time.strftime("%b %d %H:%M", time.localtime(timestamp)) def _newLogEntry(self, entry): """This is called when a new log entry is created""" # add entry to purrer self.purrer.addLogEntry(entry) # add entry to listview if it is not an ignored entry # (ignored entries only carry information about DPs to be ignored) if not entry.ignore: if self.etw.topLevelItemCount(): lastitem = self.etw.topLevelItem(self.etw.topLevelItemCount() - 1) else: lastitem = None self._addEntryItem(entry, len(self.purrer.entries) - 1, lastitem) self._index_paths[os.path.abspath( entry.index_file)] = len(self.purrer.entries) - 1 # log will have changed, so update the viewer if not entry.ignore: self._updateViewer() self.show() def _entryChanged(self, entry): """This is called when a log entry is changed""" # resave the log self.purrer.save() # redo entry item if entry.tw_item: number = entry.tw_item._ientry entry.tw_item = None self.etw.takeTopLevelItem(number) if number: after = self.etw.topLevelItem(number - 1) else: after = None self._addEntryItem(entry, number, after) # log will have changed, so update the viewer self._updateViewer() def _regenerateLog(self): if QMessageBox.question( self.viewer_dialog, "Regenerate log", """<P><NOBR>Do you really want to regenerate the entire</NOBR> log? This can be a time-consuming operation.</P>""", QMessageBox.Yes, QMessageBox.No) != QMessageBox.Yes: return self.purrer.save(refresh=True) self._updateViewer()