Sun, 25 Apr 2021 17:29:38 +0200
Implemented some code simplifications.
# -*- coding: utf-8 -*- # Copyright (c) 2012 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the time tracker object. """ import os from PyQt5.QtCore import Qt, QObject from PyQt5.QtGui import QKeySequence from E5Gui.E5Application import e5App from E5Gui import E5MessageBox from E5Gui.E5Action import E5Action import UI.PixmapCache class TimeTracker(QObject): """ Class implementing the time tracker object. """ FileName = "TimeTracker.txt" def __init__(self, plugin, iconSuffix, parent=None): """ Constructor @param plugin reference to the plugin object @type TimeTrackerPlugin @param iconSuffix suffix for the icons @type str @param parent parent @type QObject """ QObject.__init__(self, parent) self.__plugin = plugin self.__iconSuffix = iconSuffix self.__ui = parent self.__e5project = e5App().getObject("Project") def __initialize(self): """ Private slot to initialize some member variables. """ self.__projectPath = '' self.__trackerFilePath = '' self.__projectOpen = False self.__entries = {} # key: entry ID, value tracker entry self.__currentEntry = None self.__widget.clear() self.__widget.setEnabled(False) def activate(self): """ Public method to activate the time tracker. """ from .TimeTrackerWidget import TimeTrackerWidget self.__widget = TimeTrackerWidget(self) self.__ui.addSideWidget( self.__ui.BottomSide, self.__widget, UI.PixmapCache.getIcon( os.path.join("TimeTracker", "icons", "clock-{0}".format(self.__iconSuffix)) ), self.tr("Time Tracker")) self.__activateAct = E5Action( self.tr('Time Tracker'), self.tr('T&ime Tracker'), QKeySequence(self.tr("Alt+Shift+I")), 0, self, 'time_tracker_activate') self.__activateAct.setStatusTip(self.tr( "Switch the input focus to the Time Tracker window.")) self.__activateAct.setWhatsThis(self.tr( """<b>Activate Time Tracker</b>""" """<p>This switches the input focus to the Time Tracker""" """ window.</p>""" )) self.__activateAct.triggered.connect(self.__activateWidget) self.__ui.addE5Actions([self.__activateAct], 'ui') menu = self.__ui.getMenu("subwindow") menu.addAction(self.__activateAct) self.__initialize() def deactivate(self): """ Public method to deactivate the time tracker. """ menu = self.__ui.getMenu("subwindow") menu.removeAction(self.__activateAct) self.__ui.removeE5Actions([self.__activateAct], 'ui') self.__ui.removeSideWidget(self.__widget) def projectOpened(self): """ Public slot to handle the projectOpened signal. """ if self.__projectOpen: self.projectClosed() self.__projectOpen = True self.__projectPath = self.__e5project.getProjectPath() self.__trackerFilePath = os.path.join( self.__e5project.getProjectManagementDir(), TimeTracker.FileName) self.__readTrackerEntries() self.__widget.showTrackerEntries(sorted(self.__entries.values(), reverse=True)) self.__widget.setEnabled(True) self.startTrackerEntry() def projectClosed(self): """ Public slot to handle the projectClosed signal. """ if self.__projectOpen: self.stopTrackerEntry() self.saveTrackerEntries() self.__initialize() def __readTrackerEntries(self): """ Private slot to read the time tracker entries from a file. """ if os.path.exists(self.__trackerFilePath): try: with open(self.__trackerFilePath, "r", encoding="utf-8") as f: data = f.read() except OSError as err: E5MessageBox.critical( self.__ui, self.tr("Read Time Tracker File"), self.tr("""<p>The time tracker file <b>{0}</b> could""" """ not be read.</p><p>Reason: {1}</p>""") .format(self.__trackerFilePath, str(err))) return from .TimeTrackEntry import TimeTrackEntry invalidCount = 0 for line in data.splitlines(): entry = TimeTrackEntry(self.__plugin) eid = entry.fromString(line.strip()) if eid > -1: self.__entries[eid] = entry else: invalidCount += 1 if invalidCount: E5MessageBox.information( self.__ui, self.tr("Read Time Tracker File"), self.tr("""<p>The time tracker file <b>{0}</b>""" """ contained %n invalid entries. These""" """ have been discarded.</p>""", "", invalidCount).format(self.__trackerFilePath)) def saveTrackerEntries(self, filePath="", ids=None): """ Public slot to save the tracker entries to a file. @keyparam filePath path and name of the file to write the entries to (string) @keyparam ids list of entry IDs to be written (list of integer) """ if not filePath: filePath = self.__trackerFilePath entriesList = ( [self.__entries[eid] for eid in ids if eid in self.__entries] if ids else self.__entries.values() ) try: with open(filePath, "w", encoding="utf-8") as f: for entry in entriesList: if entry.isValid(): f.write(entry.toString() + "\n") except OSError as err: E5MessageBox.critical( self.__ui, self.tr("Save Time Tracker File"), self.tr("""<p>The time tracker file <b>{0}</b> could""" """ not be saved.</p><p>Reason: {1}</p>""") .format(self.__trackerFilePath, str(err))) def importTrackerEntries(self, fname): """ Public slot to import tracker entries from a file. @param fname name of the file to import (string) """ try: with open(fname, "r", encoding="utf-8") as f: data = f.read() except OSError as err: E5MessageBox.critical( self.__ui, self.tr("Import Time Tracker File"), self.tr("""<p>The time tracker file <b>{0}</b> could""" """ not be read.</p><p>Reason: {1}</p>""") .format(fname, str(err))) return from .TimeTrackEntry import TimeTrackEntry invalidCount = 0 duplicateCount = 0 entries = [] for line in data.splitlines(): entry = TimeTrackEntry(self.__plugin) eid = entry.fromString(line.strip()) if eid > -1: entries.append(entry) else: invalidCount += 1 if not self.__plugin.getPreferences("AllowDuplicates"): startDateTimes = [ e.getStartDateTime() for e in self.__entries.values()] for entry in entries[:]: if entry.getStartDateTime() in startDateTimes: entries.remove(entry) duplicateCount += 1 start = ( max(self.__entries.keys()) + 1 if len(self.__entries.keys()) else 0 ) for nextID, entry in enumerate(entries, start=start): entry.setID(nextID) self.__entries[nextID] = entry if self.__plugin.getPreferences("AutoSave"): self.saveTrackerEntries() if invalidCount != 0 or duplicateCount != 0: if invalidCount != 0 and duplicateCount != 0: msg = self.tr( """<p>The time tracker file <b>{0}</b> contained""" """ %n invalid entries.""", "", invalidCount).format(fname) msg += " " + self.tr( """ %n duplicate entries were detected.""", "", duplicateCount) elif duplicateCount != 0: msg = self.tr( """<p>The time tracker file <b>{0}</b> contained""" """ %n duplicate entries.""", "", duplicateCount).format(fname) elif invalidCount != 0: msg = self.tr( """<p>The time tracker file <b>{0}</b> contained""" """ %n invalid entries.""", "", invalidCount).format(fname) msg += " " + self.tr( """ %n entries have been ignored.</p>""", "", invalidCount + duplicateCount) E5MessageBox.information( self.__ui, self.tr("Import Time Tracker File"), msg) self.__widget.clear() self.__widget.showTrackerEntries(sorted(self.__entries.values(), reverse=True)) self.__widget.setCurrentEntry(self.__currentEntry) def addTrackerEntry(self, startDateTime, duration, task, comment): """ Public method to add a new tracker entry based on the given data. @param startDateTime start date and time (QDateTime) @param duration duration in minutes (integer) @param task task description (string) @param comment comment (string) """ if not self.__plugin.getPreferences("AllowDuplicates"): startDateTimes = [ entry.getStartDateTime() for entry in self.__entries.values()] if startDateTime in startDateTimes: return if duration < self.__plugin.getPreferences("MinimumDuration"): return nextID = ( max(self.__entries.keys()) + 1 if len(self.__entries.keys()) else 0 ) from .TimeTrackEntry import TimeTrackEntry entry = TimeTrackEntry(self.__plugin) entry.setID(nextID) entry.setStartDateTime(startDateTime) entry.setDuration(duration) entry.setTask(task) entry.setComment(comment) self.__entries[nextID] = entry self.__widget.clear() self.__widget.showTrackerEntries(sorted(self.__entries.values(), reverse=True)) self.__widget.setCurrentEntry(self.__currentEntry) def pauseTrackerEntry(self): """ Public method to pause the current tracker entry. """ self.__currentEntry.pause() def continueTrackerEntry(self): """ Public method to continue the current tracker entry. """ self.__currentEntry.continue_() def stopTrackerEntry(self): """ Public method to stop the current tracker entry. @return tuple of the ID assigned to the stopped tracker entry and the duration (integer, integer) """ duration = 0 nextID = -1 if self.__currentEntry is not None: self.__currentEntry.stop() if self.__currentEntry.isValid(): if len(self.__entries.keys()): nextID = max(self.__entries.keys()) + 1 else: nextID = 0 self.__currentEntry.setID(nextID) self.__entries[nextID] = self.__currentEntry if self.__plugin.getPreferences("AutoSave"): self.saveTrackerEntries() duration = self.__currentEntry.getDuration() self.__currentEntry = None return nextID, duration def startTrackerEntry(self): """ Public method to start a new tracker entry. """ from .TimeTrackEntry import TimeTrackEntry self.__currentEntry = TimeTrackEntry(self.__plugin) self.__currentEntry.start() self.__widget.setCurrentEntry(self.__currentEntry) def getCurrentEntry(self): """ Public method to get a reference to the current tracker entry. @return reference to the current entry (TimeTrackEntry) """ return self.__currentEntry def getEntry(self, eid): """ Public method to get a tracker entry given its ID. @param eid ID of the tracker entry (integer) @return entry for the given ID (TimeTrackEntry) or None """ if eid in self.__entries: return self.__entries[eid] else: return None def deleteTrackerEntry(self, eid): """ Public method to delete a tracker entry given its ID. @param eid ID of the tracker entry (integer) """ if eid in self.__entries: del self.__entries[eid] def removeDuplicateTrackerEntries(self): """ Public slot to remove duplicate time tracker entries. If entries with the identical start date and time are found, the one with the longest duration is kept. """ entries = {} for entry in self.__entries.values(): dt = entry.getStartDateTime() if dt in entries: if entry.getDuration() > entries[dt].getDuration(): entries[dt] = entry else: entries[dt] = entry self.__entries = {} for nextID, entry in enumerate(sorted(entries.values())): entry.setID(nextID) self.__entries[nextID] = entry if self.__plugin.getPreferences("AutoSave"): self.saveTrackerEntries() self.__widget.clear() self.__widget.showTrackerEntries(sorted(self.__entries.values(), reverse=True)) self.__widget.setCurrentEntry(self.__currentEntry) def mergeDuplicateTrackerEntries(self): """ Public slot to merge duplicate time tracker entries. If entries with the identical start date and time are found, the durations of these entries are added. """ entries = {} for entry in self.__entries.values(): dt = entry.getStartDateTime() if dt in entries: entries[dt].addDuration(entry.getDuration()) else: entries[dt] = entry self.__entries = {} for nextID, entry in enumerate(sorted(entries.values())): entry.setID(nextID) self.__entries[nextID] = entry if self.__plugin.getPreferences("AutoSave"): self.saveTrackerEntries() self.__widget.clear() self.__widget.showTrackerEntries(sorted(self.__entries.values(), reverse=True)) self.__widget.setCurrentEntry(self.__currentEntry) def entryChanged(self): """ Public method to indicate an external change to any of the entries. """ if self.__plugin.getPreferences("AutoSave"): self.saveTrackerEntries() def getPreferences(self, key): """ Public method to retrieve the various settings. @param key the key of the value to get @return the requested setting """ return self.__plugin.getPreferences(key) def __activateWidget(self): """ Private slot to handle the activation of the project browser. """ uiLayoutType = self.__ui.getLayoutType() if uiLayoutType == "Toolboxes": self.__ui.hToolboxDock.show() self.__ui.hToolbox.setCurrentWidget(self.__widget) elif uiLayoutType == "Sidebars": self.__ui.bottomSidebar.show() self.__ui.bottomSidebar.setCurrentWidget(self.__widget) else: self.__widget.show() self.__widget.setFocus(Qt.ActiveWindowFocusReason)