TimeTracker/TimeTracker.py

Sat, 23 Dec 2023 15:48:55 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 23 Dec 2023 15:48:55 +0100
branch
eric7
changeset 114
f58b64382e67
parent 113
a9002f9b14d5
child 115
859d59103f9f
permissions
-rw-r--r--

Updated copyright for 2024.

# -*- 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.
        """
        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

            from .TimeTrackEntry import TimeTrackEntry

            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 (TypeError, OSError) 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 (string)
        """
        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

        from .TimeTrackEntry import TimeTrackEntry

        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
        """
        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
        @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)

eric ide

mercurial