Sat, 23 Dec 2023 19:35:14 +0100
Corrected some code style issues and converted some source code documentation to the new style.
# -*- coding: utf-8 -*- # Copyright (c) 2012 - 2024 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the time tracker object. """ import json import os from PyQt6.QtCore import QObject, Qt from PyQt6.QtGui import QKeySequence try: from eric7.EricGui import EricPixmapCache except ImportError: from UI import PixmapCache as EricPixmapCache from eric7.EricGui.EricAction import EricAction from eric7.EricWidgets import EricMessageBox from eric7.EricWidgets.EricApplication import ericApp class TimeTracker(QObject): """ Class implementing the time tracker object. """ FileName = "TimeTracker.ttj" 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.__ericProject = ericApp().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) iconName = ( "sbTimeTracker96" if self.__ui.getLayoutType() == "Sidebars" else "clock-{0}".format(self.__iconSuffix) ) self.__ui.addSideWidget( self.__ui.BottomSide, self.__widget, EricPixmapCache.getIcon(os.path.join("TimeTracker", "icons", iconName)), self.tr("Time Tracker"), ) self.__activateAct = EricAction( 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.addEricActions([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.removeEricActions([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.__ericProject.getProjectPath() self.__trackerFilePath = os.path.join( self.__ericProject.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. """ from .TimeTrackEntry import TimeTrackEntry if os.path.exists(self.__trackerFilePath): try: with open(self.__trackerFilePath, "r") as f: jsonString = f.read() entriesDataList = json.loads(jsonString) except (OSError, json.JSONDecodeError) as err: EricMessageBox.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 invalidCount = 0 for data in entriesDataList: entry = TimeTrackEntry(self.__plugin) eid = entry.fromDict(data) if eid > -1: self.__entries[eid] = entry else: invalidCount += 1 if invalidCount: EricMessageBox.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. @param filePath path and name of the file to write the entries to @type str @param ids list of entry IDs to be written @type list of int """ if not filePath: filePath = self.__trackerFilePath entriesDataList = ( [self.__entries[eid].toDict() for eid in ids if eid in self.__entries] if ids else [e.toDict() for e in self.__entries.values()] ) try: jsonString = json.dumps(entriesDataList, indent=2) with open(filePath, "w") as f: f.write(jsonString) except (OSError, TypeError) as err: EricMessageBox.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 @type str """ from .TimeTrackEntry import TimeTrackEntry try: with open(fname, "r", encoding="utf-8") as f: jsonString = f.read() entriesDataList = json.loads(jsonString) except (OSError, json.JSONDecodeError) as err: EricMessageBox.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 invalidCount = 0 duplicateCount = 0 entries = [] for data in entriesDataList: entry = TimeTrackEntry(self.__plugin) eid = entry.fromDict(data) 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, ) EricMessageBox.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 @type QDateTime @param duration duration in minutes @type int @param task task description @type str @param comment comment @type str """ from .TimeTrackEntry import TimeTrackEntry 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 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 @rtype tuple of (int, int) """ duration = 0 nextID = -1 if self.__currentEntry is not None: self.__currentEntry.stop() if self.__currentEntry.isValid(): nextID = ( max(self.__entries.keys()) + 1 if len(self.__entries.keys()) else 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 @rtype 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 @type int @return entry for the given ID or None @rtype TimeTrackEntry """ 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 @type int """ 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 key of the value to get @type str @return value of the requested setting @rtype Any """ return self.__plugin.getPreferences(key) def __activateWidget(self): """ Private slot to handle the activation of the time tracker widget. """ 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.FocusReason.ActiveWindowFocusReason)