def __init__(self, parent, label, extra_widgets=[], style=QFrame.HLine + QFrame.Raised, offset=16): QWidget.__init__(self, parent) lo = QHBoxLayout(self) lo.setContentsMargins(0, 0, 0, 0) lo.setSpacing(4) if offset: frame = QFrame(self) frame.setFrameStyle(style) frame.setMinimumWidth(offset) lo.addWidget(frame, 0) lo.addWidget(QLabel(label, self), 0) frame = QFrame(self) frame.setFrameStyle(style) lo.addWidget(frame, 1) for w in extra_widgets: lo.addWidget(w, 0)
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()
def VLine(self): line = QFrame() line.setFrameStyle(QFrame.VLine) line.setFrameShape(QFrame.VLine) line.setFrameShadow(QFrame.Sunken) return line
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()