TimeTracker/TimeTrackerWidget.py

Wed, 30 Dec 2020 11:02:12 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 30 Dec 2020 11:02:12 +0100
changeset 92
81b63c2dc40c
parent 89
e38b60832145
child 94
a3d936304e51
permissions
-rw-r--r--

Updated copyright for 2021.

# -*- coding: utf-8 -*-

# Copyright (c) 2012 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the time tracker widget.
"""

import os

from PyQt5.QtCore import pyqtSlot, QPoint, Qt, QDate, QTime, QFileInfo
from PyQt5.QtGui import QCursor
from PyQt5.QtWidgets import QWidget, QMenu, QTreeWidgetItem, QDialog

from E5Gui import E5MessageBox, E5FileDialog

from .Ui_TimeTrackerWidget import Ui_TimeTrackerWidget

import Preferences
import Utilities


class TimeTrackerWidget(QWidget, Ui_TimeTrackerWidget):
    """
    Class implementing the time tracker widget.
    """
    DurationColumn = 1
    TaskColumn = 2
    CommentColumn = 3
    
    def __init__(self, tracker, parent=None):
        """
        Constructor
        
        @param tracker reference to the time tracker (TimeTracker)
        @param parent reference to the parent widget (QWidget)
        """
        super(TimeTrackerWidget, self).__init__(parent)
        self.setupUi(self)
        
        self.__tracker = tracker
    
    @pyqtSlot(str)
    def on_taskCombo_editTextChanged(self, txt):
        """
        Private slot handling changes of the task description of the current
        entry.
        
        @param txt new task description (string)
        """
        itm = self.entriesList.topLevelItem(0)
        if itm:
            itm.setText(self.TaskColumn, txt)
            self.entriesList.resizeColumnToContents(self.TaskColumn)
        
        entry = self.__tracker.getCurrentEntry()
        if entry:
            entry.setTask(txt)
    
    @pyqtSlot(str)
    def on_commentCombo_editTextChanged(self, txt):
        """
        Private slot handling changes of the comment of the current entry.
        
        @param txt new comment (string)
        """
        itm = self.entriesList.topLevelItem(0)
        if itm:
            itm.setText(self.CommentColumn, txt)
            self.entriesList.resizeColumnToContents(self.CommentColumn)
        
        entry = self.__tracker.getCurrentEntry()
        if entry:
            entry.setComment(txt)
    
    @pyqtSlot(bool)
    def on_pauseButton_toggled(self, checked):
        """
        Private slot to pause the current timing.
        
        @param checked flag indicating the checked status of the button
            (boolean)
        """
        if checked:
            self.__tracker.pauseTrackerEntry()
            
            entry = self.__tracker.getCurrentEntry()
            duration = entry.getDuration()
            
            itm = self.entriesList.topLevelItem(0)
            itm.setText(self.DurationColumn,
                        self.tr("{0} min").format(duration))
            self.entriesList.resizeColumnToContents(self.DurationColumn)
            
            self.durationSpinBox.setValue(duration)
        else:
            self.__tracker.continueTrackerEntry()
    
    @pyqtSlot()
    def on_newButton_clicked(self):
        """
        Private slot to end the current timer and start a new one.
        """
        # stop the current tracker
        eid, duration = self.__tracker.stopTrackerEntry()
        if eid > -1:
            itm = self.entriesList.topLevelItem(0)
            itm.setText(self.DurationColumn,
                        self.tr("{0} min").format(duration))
            itm.setData(0, Qt.UserRole, eid)
        else:
            itm = self.entriesList.takeTopLevelItem(0)
            del itm
        self.__resizeColumns()
        
        # start a new one
        self.__tracker.startTrackerEntry()
    
    @pyqtSlot(QPoint)
    def on_entriesList_customContextMenuRequested(self, pos):
        """
        Private slot to create the context menu and show it.
        
        @param pos position the menu should be shown at (QPoint)
        """
        menu = QMenu()
        
        act = menu.addAction(self.tr("Edit"), self.__editEntry)
        act.setEnabled(
            len(self.entriesList.selectedItems()) == 1 and
            self.entriesList.selectedItems()[0].data(0, Qt.UserRole) > -1
        )
        menu.addAction(self.tr("Add"), self.__addEntry)
        act = menu.addAction(self.tr("Delete"), self.__deleteSelectedEntries)
        act.setEnabled(
            (len(self.entriesList.selectedItems()) == 1 and
             self.entriesList.selectedItems()[0].data(0, Qt.UserRole) > -1) or
            len(self.entriesList.selectedItems()) > 1
        )
        menu.addSeparator()
        menu.addAction(self.tr("Save"), self.__saveEntries)
        menu.addSeparator()
        menu.addAction(self.tr("Import"), self.__importEntries)
        act = menu.addAction(self.tr("Export Selected"),
                             self.__exportSelectedEntries)
        act.setEnabled(len(self.entriesList.selectedItems()) != 0)
        menu.addAction(self.tr("Export All"), self.__exportEntries)
        menu.addSeparator()
        menu.addAction(self.tr("Remove duplicates"), self.__removeDuplicates)
        menu.addAction(self.tr("Merge duplicates"), self.__mergeDuplicates)
        menu.exec(QCursor.pos())
    
    def __addEntry(self):
        """
        Private slot to manually add an entry.
        """
        from .TimeTrackerEntryDialog import TimeTrackerEntryDialog
        
        tasks = []
        for index in range(self.taskCombo.count()):
            tasks.append(self.taskCombo.itemText(index))
        comments = []
        for index in range(self.commentCombo.count()):
            comments.append(self.commentCombo.itemText(index))
        dlg = TimeTrackerEntryDialog(self.__tracker, None, tasks, comments)
        if dlg.exec() == QDialog.Accepted:
            self.__tracker.addTrackerEntry(*dlg.getData())
    
    def __editEntry(self):
        """
        Private slot to edit the selected tracker entry.
        """
        itm = self.entriesList.selectedItems()[0]
        eid = itm.data(0, Qt.UserRole)
        if eid > -1:
            # the current entry is edited via the elements of this widget
            entry = self.__tracker.getEntry(eid)
            if entry is not None:
                from .TimeTrackerEntryDialog import TimeTrackerEntryDialog
                
                tasks = []
                for index in range(self.taskCombo.count()):
                    tasks.append(self.taskCombo.itemText(index))
                comments = []
                for index in range(self.commentCombo.count()):
                    comments.append(self.commentCombo.itemText(index))
                dlg = TimeTrackerEntryDialog(self.__tracker, entry, tasks,
                                             comments)
                if dlg.exec() == QDialog.Accepted:
                    start, duration, task, comment = dlg.getData()
                    
                    entry.setStartDateTime(start)
                    entry.setDuration(duration)
                    entry.setTask(task)
                    entry.setComment(comment)
                    self.__tracker.entryChanged()
                    
                    (date, time, duration, task,
                     comment) = entry.getEntryData()[1:-1]
                    itm.setText(0, self.tr("{0}, {1}", "date, time")
                                .format(date, time))
                    itm.setText(1, self.tr("{0} min").format(duration))
                    itm.setText(2, task)
                    itm.setText(3, comment)
                    self.__resizeColumns()
    
    def __deleteSelectedEntries(self):
        """
        Private slot to delete the selected tracker entries.
        """
        res = E5MessageBox.yesNo(
            self,
            self.tr("Delete Selected Entries"),
            self.tr("""Do you really want to delete the selected"""
                    """ entries?"""))
        if res:
            for item in self.entriesList.selectedItems():
                eid = item.data(0, Qt.UserRole)
                if eid > -1:
                    # the current entry must not be deleted
                    self.entriesList.takeTopLevelItem(
                        self.entriesList.indexOfTopLevelItem(item))
                    self.__tracker.deleteTrackerEntry(eid)
                    del item
    
    def __saveEntries(self):
        """
        Private slot to save the tracker entries.
        """
        self.__tracker.saveTrackerEntries()
    
    def __importEntries(self):
        """
        Private slot to import tracker entries.
        """
        path = (
            Preferences.getMultiProject("Workspace") or
            Utilities.getHomeDir()
        )
        fname = E5FileDialog.getOpenFileName(
            None,
            self.tr("Import Tracker Entries"),
            path,
            self.tr("Text Files (*.txt);;All Files (*)"))
        if fname:
            fname = Utilities.toNativeSeparators(fname)
            if not os.path.exists(fname):
                E5MessageBox.critical(
                    self,
                    self.tr("Import Tracker Entries"),
                    self.tr("<p>The file <b>{0}</b> does not exist.</p>")
                    .format(fname))
                return
            
            self.__tracker.importTrackerEntries(fname)
    
    def __exportEntries(self, ids=None):
        """
        Private method to export all or selected entries.
        
        @keyparam ids list of IDs to export or all if empty (list of integer)
        """
        path = (
            Preferences.getMultiProject("Workspace") or
            Utilities.getHomeDir()
        )
        fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
            self,
            self.tr("Export Tracker Entries"),
            path,
            self.tr("Text Files (*.txt);;All Files (*)"),
            None,
            E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
        if fname:
            ext = QFileInfo(fname).suffix()
            if not ext:
                ex = selectedFilter.split("(*")[1].split(")")[0]
                if ex:
                    fname += ex
            if QFileInfo(fname).exists():
                res = E5MessageBox.yesNo(
                    self,
                    self.tr("Export Tracker Entries"),
                    self.tr("<p>The file <b>{0}</b> already exists."
                            " Overwrite it?</p>").format(fname),
                    icon=E5MessageBox.Warning)
                if not res:
                    return
                fname = Utilities.toNativeSeparators(fname)
            self.__tracker.saveTrackerEntries(filePath=fname, ids=ids)
    
    def __exportSelectedEntries(self):
        """
        Private slot to export the selected tracker entries.
        """
        ids = []
        for itm in self.entriesList.selectedItems():
            eid = itm.data(0, Qt.UserRole)
            if eid > -1:
                ids.append(eid)
        
        if ids:
            self.__exportEntries(ids=ids)
    
    def __removeDuplicates(self):
        """
        Private slot to remove duplicate entries.
        """
        res = E5MessageBox.yesNo(
            self,
            self.tr("Remove Duplicate Tracker Entries"),
            self.tr("""Are you sure you want to remove duplicate"""
                    """ tracker entries? Only the one with the longest"""
                    """ duration will be kept."""))
        if res:
            self.__tracker.removeDuplicateTrackerEntries()
    
    def __mergeDuplicates(self):
        """
        Private slot to merge duplicate entries.
        """
        res = E5MessageBox.yesNo(
            self,
            self.tr("Merge Duplicate Tracker Entries"),
            self.tr("""Are you sure you want to merge duplicate"""
                    """ tracker entries? The durations of duplicate"""
                    """ ones will be added."""))
        if res:
            self.__tracker.mergeDuplicateTrackerEntries()
    
    def __insertEntry(self, entry, index=-1):
        """
        Private method to insert a tracker entry into the list.
        
        @param entry reference to the tracker entry (TimeTrackEntry)
        @param index index the entry is to be inserted; -1 for at the end
            (integer)
        """
        eid, date, time, duration, task, comment, paused = entry.getEntryData()
        itm = QTreeWidgetItem(
            [self.tr("{0}, {1}", "date, time").format(date, time),
             self.tr("{0} min").format(duration), task, comment])
        itm.setTextAlignment(1, Qt.AlignRight)
        itm.setData(0, Qt.UserRole, eid)
        if index == -1:
            self.entriesList.addTopLevelItem(itm)
        else:
            self.entriesList.insertTopLevelItem(index, itm)
    
    def __resizeColumns(self):
        """
        Private slot to resize the columns of the entries list.
        """
        for column in range(self.entriesList.columnCount()):
            self.entriesList.resizeColumnToContents(column)
    
    def showTrackerEntries(self, entries):
        """
        Public method to show the tracker entries of the current project.
        
        @param entries list of tracker entries (list of TimeTrackEntry)
        """
        self.taskCombo.addItem("")
        self.commentCombo.addItem("")
        
        tasks = []
        comments = []
        
        for entry in entries:
            self.__insertEntry(entry)
            task = entry.getTask()
            if task and task not in tasks:
                tasks.append(task)
            comment = entry.getComment()
            if comment and comment not in comments:
                comments.append(comment)
        
        self.__resizeColumns()
        if tasks:
            self.taskCombo.addItems(sorted(tasks))
        if comments:
            self.commentCombo.addItems(sorted(comments))
    
    def setCurrentEntry(self, entry):
        """
        Public method to set the current entry.
        
        @param entry current entry (TimeTrackEntry)
        """
        self.__insertEntry(entry, 0)
        self.__resizeColumns()
        
        eid, date, time, duration, task, comment, paused = entry.getEntryData()
        self.startDateTimeEdit.setDateTime(entry.getStartDateTime())
        self.durationSpinBox.setValue(duration)
        self.taskCombo.setEditText(task)
        self.commentCombo.setEditText(comment)
    
    def clear(self):
        """
        Public method to clear all the data.
        """
        self.entriesList.clear()
        self.startDateTimeEdit.setDate(QDate(2000, 1, 1))
        self.startDateTimeEdit.setTime(QTime(0, 0, 0))
        self.durationSpinBox.setValue(0)
        self.taskCombo.clear()
        self.commentCombo.clear()

eric ide

mercurial