Sat, 26 Oct 2024 17:13:16 +0200
- changed to the new style header
- ensured proper parent relationship of modal dialogs
- included compiled form files
# -*- coding: utf-8 -*- # Copyright (c) 2012 - 2024 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the time tracker widget. """ import os from PyQt6.QtCore import QDate, QFileInfo, QPoint, Qt, QTime, pyqtSlot from PyQt6.QtGui import QCursor from PyQt6.QtWidgets import QDialog, QMenu, QTreeWidgetItem, QWidget from eric7 import Preferences from eric7.EricWidgets import EricFileDialog, EricMessageBox try: from eric7.SystemUtilities.FileSystemUtilities import toNativeSeparators except ImportError: # imports for eric < 23.1 from eric7.Utilities import toNativeSeparators try: from eric7.SystemUtilities.OSUtilities import getHomeDir except ImportError: # imports for eric < 23.1 from eric7.Utilities import getHomeDir from .Ui_TimeTrackerWidget import Ui_TimeTrackerWidget 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 @type TimeTracker @param parent reference to the parent widget @type QWidget """ super().__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 @type str """ 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 @type str """ 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 @type bool """ 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.ItemDataRole.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): # noqa: U100 """ Private slot to create the context menu and show it. @param pos position the menu should be shown at @type 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.ItemDataRole.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.ItemDataRole.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 = [ self.taskCombo.itemText(index) for index in range(self.taskCombo.count()) ] comments = [ self.commentCombo.itemText(index) for index in range(self.commentCombo.count()) ] dlg = TimeTrackerEntryDialog(self.__tracker, None, tasks, comments, parent=self) if dlg.exec() == QDialog.DialogCode.Accepted: self.__tracker.addTrackerEntry(*dlg.getData()) def __editEntry(self): """ Private slot to edit the selected tracker entry. """ from .TimeTrackerEntryDialog import TimeTrackerEntryDialog itm = self.entriesList.selectedItems()[0] eid = itm.data(0, Qt.ItemDataRole.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: tasks = [ self.taskCombo.itemText(index) for index in range(self.taskCombo.count()) ] comments = [ self.commentCombo.itemText(index) for index in range(self.commentCombo.count()) ] dlg = TimeTrackerEntryDialog( self.__tracker, entry, tasks, comments, parent=self ) if dlg.exec() == QDialog.DialogCode.Accepted: start, duration, task, comment = dlg.getData() entry.setStartDateTime(start) entry.setDuration(duration) entry.setTask(task) entry.setComment(comment) self.__tracker.entryChanged() data = entry.getEntryData() itm.setText( 0, self.tr("{0}, {1}", "date, time").format( data["start_date"], data["start_time"] ), ) itm.setText(1, self.tr("{0} min").format(data["duration"])) itm.setText(2, data["task"]) itm.setText(3, data["comment"]) self.__resizeColumns() def __deleteSelectedEntries(self): """ Private slot to delete the selected tracker entries. """ res = EricMessageBox.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.ItemDataRole.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 getHomeDir() fname = EricFileDialog.getOpenFileName( None, self.tr("Import Time Tracker Entries"), path, self.tr("Time Tracker Files (*.ttj);;All Files (*)"), ) if fname: fname = toNativeSeparators(fname) if not os.path.exists(fname): EricMessageBox.critical( self, self.tr("Import Time 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. @param ids list of IDs to export or all if empty @type list of int """ path = Preferences.getMultiProject("Workspace") or getHomeDir() fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( self, self.tr("Export Time Tracker Entries"), path, self.tr("Time Tracker Files (*.ttj);;All Files (*)"), None, EricFileDialog.Options(EricFileDialog.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 = EricMessageBox.yesNo( self, self.tr("Export Time Tracker Entries"), self.tr( "<p>The file <b>{0}</b> already exists. Overwrite it?</p>" ).format(fname), icon=EricMessageBox.Warning, ) if not res: return fname = 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.ItemDataRole.UserRole) if eid > -1: ids.append(eid) if ids: self.__exportEntries(ids=ids) def __removeDuplicates(self): """ Private slot to remove duplicate entries. """ res = EricMessageBox.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 = EricMessageBox.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 @type TimeTrackEntry @param index index the entry is to be inserted; -1 for at the end @type int """ data = entry.getEntryData() itm = QTreeWidgetItem( [ self.tr("{0}, {1}", "date, time").format( data["start_date"], data["start_time"] ), self.tr("{0} min").format(data["duration"]), data["task"], data["comment"], ] ) itm.setTextAlignment(1, Qt.AlignmentFlag.AlignRight) itm.setData(0, Qt.ItemDataRole.UserRole, data["id"]) 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 @type 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 @type TimeTrackEntry """ self.__insertEntry(entry, 0) self.__resizeColumns() data = entry.getEntryData() self.startDateTimeEdit.setDateTime(entry.getStartDateTime()) self.durationSpinBox.setValue(data["duration"]) self.taskCombo.setEditText(data["task"]) self.commentCombo.setEditText(data["comment"]) def clear(self): """ Public method to clear all the data. """ self.entriesList.clear() self.startDateTimeEdit.setDate(QDate(2020, 1, 1)) self.startDateTimeEdit.setTime(QTime(0, 0, 0)) self.durationSpinBox.setValue(0) self.taskCombo.clear() self.commentCombo.clear()