5 |
5 |
6 """ |
6 """ |
7 Module implementing the time tracker object. |
7 Module implementing the time tracker object. |
8 """ |
8 """ |
9 |
9 |
|
10 import json |
10 import os |
11 import os |
11 |
12 |
12 from PyQt5.QtCore import Qt, QObject |
13 from PyQt6.QtCore import Qt, QObject |
13 from PyQt5.QtGui import QKeySequence |
14 from PyQt6.QtGui import QKeySequence |
14 |
15 |
15 from E5Gui.E5Application import e5App |
16 from EricWidgets.EricApplication import ericApp |
16 from E5Gui import E5MessageBox |
17 from EricWidgets import EricMessageBox |
17 from E5Gui.E5Action import E5Action |
18 from EricGui.EricAction import EricAction |
18 |
19 |
19 import UI.PixmapCache |
20 import UI.PixmapCache |
20 |
21 |
21 |
22 |
22 class TimeTracker(QObject): |
23 class TimeTracker(QObject): |
23 """ |
24 """ |
24 Class implementing the time tracker object. |
25 Class implementing the time tracker object. |
25 """ |
26 """ |
26 FileName = "TimeTracker.txt" |
27 FileName = "TimeTracker.ttj" |
27 |
28 |
28 def __init__(self, plugin, iconSuffix, parent=None): |
29 def __init__(self, plugin, iconSuffix, parent=None): |
29 """ |
30 """ |
30 Constructor |
31 Constructor |
31 |
32 |
40 |
41 |
41 self.__plugin = plugin |
42 self.__plugin = plugin |
42 self.__iconSuffix = iconSuffix |
43 self.__iconSuffix = iconSuffix |
43 self.__ui = parent |
44 self.__ui = parent |
44 |
45 |
45 self.__e5project = e5App().getObject("Project") |
46 self.__ericProject = ericApp().getObject("Project") |
46 |
47 |
47 def __initialize(self): |
48 def __initialize(self): |
48 """ |
49 """ |
49 Private slot to initialize some member variables. |
50 Private slot to initialize some member variables. |
50 """ |
51 """ |
71 os.path.join("TimeTracker", "icons", |
72 os.path.join("TimeTracker", "icons", |
72 "clock-{0}".format(self.__iconSuffix)) |
73 "clock-{0}".format(self.__iconSuffix)) |
73 ), |
74 ), |
74 self.tr("Time Tracker")) |
75 self.tr("Time Tracker")) |
75 |
76 |
76 self.__activateAct = E5Action( |
77 self.__activateAct = EricAction( |
77 self.tr('Time Tracker'), |
78 self.tr('Time Tracker'), |
78 self.tr('T&ime Tracker'), |
79 self.tr('T&ime Tracker'), |
79 QKeySequence(self.tr("Alt+Shift+I")), |
80 QKeySequence(self.tr("Alt+Shift+I")), |
80 0, self, |
81 0, self, |
81 'time_tracker_activate') |
82 'time_tracker_activate') |
86 """<p>This switches the input focus to the Time Tracker""" |
87 """<p>This switches the input focus to the Time Tracker""" |
87 """ window.</p>""" |
88 """ window.</p>""" |
88 )) |
89 )) |
89 self.__activateAct.triggered.connect(self.__activateWidget) |
90 self.__activateAct.triggered.connect(self.__activateWidget) |
90 |
91 |
91 self.__ui.addE5Actions([self.__activateAct], 'ui') |
92 self.__ui.addEricActions([self.__activateAct], 'ui') |
92 menu = self.__ui.getMenu("subwindow") |
93 menu = self.__ui.getMenu("subwindow") |
93 menu.addAction(self.__activateAct) |
94 menu.addAction(self.__activateAct) |
94 |
95 |
95 self.__initialize() |
96 self.__initialize() |
96 |
97 |
98 """ |
99 """ |
99 Public method to deactivate the time tracker. |
100 Public method to deactivate the time tracker. |
100 """ |
101 """ |
101 menu = self.__ui.getMenu("subwindow") |
102 menu = self.__ui.getMenu("subwindow") |
102 menu.removeAction(self.__activateAct) |
103 menu.removeAction(self.__activateAct) |
103 self.__ui.removeE5Actions([self.__activateAct], 'ui') |
104 self.__ui.removeEricActions([self.__activateAct], 'ui') |
104 self.__ui.removeSideWidget(self.__widget) |
105 self.__ui.removeSideWidget(self.__widget) |
105 |
106 |
106 def projectOpened(self): |
107 def projectOpened(self): |
107 """ |
108 """ |
108 Public slot to handle the projectOpened signal. |
109 Public slot to handle the projectOpened signal. |
109 """ |
110 """ |
110 if self.__projectOpen: |
111 if self.__projectOpen: |
111 self.projectClosed() |
112 self.projectClosed() |
112 |
113 |
113 self.__projectOpen = True |
114 self.__projectOpen = True |
114 self.__projectPath = self.__e5project.getProjectPath() |
115 self.__projectPath = self.__ericProject.getProjectPath() |
115 self.__trackerFilePath = os.path.join( |
116 self.__trackerFilePath = os.path.join( |
116 self.__e5project.getProjectManagementDir(), |
117 self.__ericProject.getProjectManagementDir(), |
117 TimeTracker.FileName) |
118 TimeTracker.FileName) |
118 |
119 |
119 self.__readTrackerEntries() |
120 self.__readTrackerEntries() |
120 self.__widget.showTrackerEntries(sorted(self.__entries.values(), |
121 self.__widget.showTrackerEntries(sorted(self.__entries.values(), |
121 reverse=True)) |
122 reverse=True)) |
136 """ |
137 """ |
137 Private slot to read the time tracker entries from a file. |
138 Private slot to read the time tracker entries from a file. |
138 """ |
139 """ |
139 if os.path.exists(self.__trackerFilePath): |
140 if os.path.exists(self.__trackerFilePath): |
140 try: |
141 try: |
141 with open(self.__trackerFilePath, "r", encoding="utf-8") as f: |
142 with open(self.__trackerFilePath, "r") as f: |
142 data = f.read() |
143 jsonString = f.read() |
143 except OSError as err: |
144 entriesDataList = json.loads(jsonString) |
144 E5MessageBox.critical( |
145 except (OSError, json.JSONDecodeError) as err: |
|
146 EricMessageBox.critical( |
145 self.__ui, |
147 self.__ui, |
146 self.tr("Read Time Tracker File"), |
148 self.tr("Read Time Tracker File"), |
147 self.tr("""<p>The time tracker file <b>{0}</b> could""" |
149 self.tr("""<p>The time tracker file <b>{0}</b> could""" |
148 """ not be read.</p><p>Reason: {1}</p>""") |
150 """ not be read.</p><p>Reason: {1}</p>""") |
149 .format(self.__trackerFilePath, str(err))) |
151 .format(self.__trackerFilePath, str(err))) |
150 return |
152 return |
151 |
153 |
152 from .TimeTrackEntry import TimeTrackEntry |
154 from .TimeTrackEntry import TimeTrackEntry |
153 |
155 |
154 invalidCount = 0 |
156 invalidCount = 0 |
155 for line in data.splitlines(): |
157 for data in entriesDataList: |
156 entry = TimeTrackEntry(self.__plugin) |
158 entry = TimeTrackEntry(self.__plugin) |
157 eid = entry.fromString(line.strip()) |
159 eid = entry.fromDict(data) |
158 if eid > -1: |
160 if eid > -1: |
159 self.__entries[eid] = entry |
161 self.__entries[eid] = entry |
160 else: |
162 else: |
161 invalidCount += 1 |
163 invalidCount += 1 |
162 |
164 |
163 if invalidCount: |
165 if invalidCount: |
164 E5MessageBox.information( |
166 EricMessageBox.information( |
165 self.__ui, |
167 self.__ui, |
166 self.tr("Read Time Tracker File"), |
168 self.tr("Read Time Tracker File"), |
167 self.tr("""<p>The time tracker file <b>{0}</b>""" |
169 self.tr("""<p>The time tracker file <b>{0}</b>""" |
168 """ contained %n invalid entries. These""" |
170 """ contained %n invalid entries. These""" |
169 """ have been discarded.</p>""", "", |
171 """ have been discarded.</p>""", "", |
171 |
173 |
172 def saveTrackerEntries(self, filePath="", ids=None): |
174 def saveTrackerEntries(self, filePath="", ids=None): |
173 """ |
175 """ |
174 Public slot to save the tracker entries to a file. |
176 Public slot to save the tracker entries to a file. |
175 |
177 |
176 @keyparam filePath path and name of the file to write the entries to |
178 @param filePath path and name of the file to write the entries to |
177 (string) |
179 @type str |
178 @keyparam ids list of entry IDs to be written (list of integer) |
180 @param ids list of entry IDs to be written |
|
181 @type list of int |
179 """ |
182 """ |
180 if not filePath: |
183 if not filePath: |
181 filePath = self.__trackerFilePath |
184 filePath = self.__trackerFilePath |
182 entriesList = ( |
185 entriesDataList = ( |
183 [self.__entries[eid] for eid in ids if eid in self.__entries] |
186 [self.__entries[eid].toDict() |
|
187 for eid in ids if eid in self.__entries] |
184 if ids else |
188 if ids else |
185 self.__entries.values() |
189 [e.toDict() for e in self.__entries.values()] |
186 ) |
190 ) |
187 try: |
191 try: |
188 with open(filePath, "w", encoding="utf-8") as f: |
192 jsonString = json.dumps(entriesDataList, indent=2) |
189 for entry in entriesList: |
193 with open(filePath, "w") as f: |
190 if entry.isValid(): |
194 f.write(jsonString) |
191 f.write(entry.toString() + "\n") |
195 except (TypeError, OSError) as err: |
192 except OSError as err: |
196 EricMessageBox.critical( |
193 E5MessageBox.critical( |
|
194 self.__ui, |
197 self.__ui, |
195 self.tr("Save Time Tracker File"), |
198 self.tr("Save Time Tracker File"), |
196 self.tr("""<p>The time tracker file <b>{0}</b> could""" |
199 self.tr("""<p>The time tracker file <b>{0}</b> could""" |
197 """ not be saved.</p><p>Reason: {1}</p>""") |
200 """ not be saved.</p><p>Reason: {1}</p>""") |
198 .format(self.__trackerFilePath, str(err))) |
201 .format(self.__trackerFilePath, str(err))) |
203 |
206 |
204 @param fname name of the file to import (string) |
207 @param fname name of the file to import (string) |
205 """ |
208 """ |
206 try: |
209 try: |
207 with open(fname, "r", encoding="utf-8") as f: |
210 with open(fname, "r", encoding="utf-8") as f: |
208 data = f.read() |
211 jsonString = f.read() |
209 except OSError as err: |
212 entriesDataList = json.loads(jsonString) |
210 E5MessageBox.critical( |
213 except (OSError, json.JSONDecodeError) as err: |
|
214 EricMessageBox.critical( |
211 self.__ui, |
215 self.__ui, |
212 self.tr("Import Time Tracker File"), |
216 self.tr("Import Time Tracker File"), |
213 self.tr("""<p>The time tracker file <b>{0}</b> could""" |
217 self.tr("""<p>The time tracker file <b>{0}</b> could""" |
214 """ not be read.</p><p>Reason: {1}</p>""") |
218 """ not be read.</p><p>Reason: {1}</p>""") |
215 .format(fname, str(err))) |
219 .format(fname, str(err))) |
218 from .TimeTrackEntry import TimeTrackEntry |
222 from .TimeTrackEntry import TimeTrackEntry |
219 |
223 |
220 invalidCount = 0 |
224 invalidCount = 0 |
221 duplicateCount = 0 |
225 duplicateCount = 0 |
222 entries = [] |
226 entries = [] |
223 for line in data.splitlines(): |
227 |
|
228 for data in entriesDataList: |
224 entry = TimeTrackEntry(self.__plugin) |
229 entry = TimeTrackEntry(self.__plugin) |
225 eid = entry.fromString(line.strip()) |
230 eid = entry.fromDict(data) |
226 if eid > -1: |
231 if eid > -1: |
227 entries.append(entry) |
232 entries.append(entry) |
228 else: |
233 else: |
229 invalidCount += 1 |
234 invalidCount += 1 |
230 |
235 |
268 """ %n invalid entries.""", |
273 """ %n invalid entries.""", |
269 "", invalidCount).format(fname) |
274 "", invalidCount).format(fname) |
270 msg += " " + self.tr( |
275 msg += " " + self.tr( |
271 """ %n entries have been ignored.</p>""", |
276 """ %n entries have been ignored.</p>""", |
272 "", invalidCount + duplicateCount) |
277 "", invalidCount + duplicateCount) |
273 E5MessageBox.information( |
278 EricMessageBox.information( |
274 self.__ui, |
279 self.__ui, |
275 self.tr("Import Time Tracker File"), |
280 self.tr("Import Time Tracker File"), |
276 msg) |
281 msg) |
277 |
282 |
278 self.__widget.clear() |
283 self.__widget.clear() |
282 |
287 |
283 def addTrackerEntry(self, startDateTime, duration, task, comment): |
288 def addTrackerEntry(self, startDateTime, duration, task, comment): |
284 """ |
289 """ |
285 Public method to add a new tracker entry based on the given data. |
290 Public method to add a new tracker entry based on the given data. |
286 |
291 |
287 @param startDateTime start date and time (QDateTime) |
292 @param startDateTime start date and time |
288 @param duration duration in minutes (integer) |
293 @type QDateTime |
289 @param task task description (string) |
294 @param duration duration in minutes |
290 @param comment comment (string) |
295 @type int |
|
296 @param task task description |
|
297 @type str |
|
298 @param comment comment |
|
299 @type str |
291 """ |
300 """ |
292 if not self.__plugin.getPreferences("AllowDuplicates"): |
301 if not self.__plugin.getPreferences("AllowDuplicates"): |
293 startDateTimes = [ |
302 startDateTimes = [ |
294 entry.getStartDateTime() for entry in self.__entries.values()] |
303 entry.getStartDateTime() for entry in self.__entries.values()] |
295 if startDateTime in startDateTimes: |
304 if startDateTime in startDateTimes: |
313 entry.setTask(task) |
322 entry.setTask(task) |
314 entry.setComment(comment) |
323 entry.setComment(comment) |
315 self.__entries[nextID] = entry |
324 self.__entries[nextID] = entry |
316 |
325 |
317 self.__widget.clear() |
326 self.__widget.clear() |
318 self.__widget.showTrackerEntries(sorted(self.__entries.values(), |
327 self.__widget.showTrackerEntries( |
319 reverse=True)) |
328 sorted(self.__entries.values(), reverse=True)) |
320 self.__widget.setCurrentEntry(self.__currentEntry) |
329 self.__widget.setCurrentEntry(self.__currentEntry) |
321 |
330 |
322 def pauseTrackerEntry(self): |
331 def pauseTrackerEntry(self): |
323 """ |
332 """ |
324 Public method to pause the current tracker entry. |
333 Public method to pause the current tracker entry. |
334 def stopTrackerEntry(self): |
343 def stopTrackerEntry(self): |
335 """ |
344 """ |
336 Public method to stop the current tracker entry. |
345 Public method to stop the current tracker entry. |
337 |
346 |
338 @return tuple of the ID assigned to the stopped tracker entry and |
347 @return tuple of the ID assigned to the stopped tracker entry and |
339 the duration (integer, integer) |
348 the duration |
|
349 @rtype tuple of (int, int) |
340 """ |
350 """ |
341 duration = 0 |
351 duration = 0 |
342 nextID = -1 |
352 nextID = -1 |
343 if self.__currentEntry is not None: |
353 if self.__currentEntry is not None: |
344 self.__currentEntry.stop() |
354 self.__currentEntry.stop() |
345 if self.__currentEntry.isValid(): |
355 if self.__currentEntry.isValid(): |
346 if len(self.__entries.keys()): |
356 nextID = ( |
347 nextID = max(self.__entries.keys()) + 1 |
357 max(self.__entries.keys()) + 1 |
348 else: |
358 if len(self.__entries.keys()) else |
349 nextID = 0 |
359 0 |
|
360 ) |
350 self.__currentEntry.setID(nextID) |
361 self.__currentEntry.setID(nextID) |
351 self.__entries[nextID] = self.__currentEntry |
362 self.__entries[nextID] = self.__currentEntry |
352 if self.__plugin.getPreferences("AutoSave"): |
363 if self.__plugin.getPreferences("AutoSave"): |
353 self.saveTrackerEntries() |
364 self.saveTrackerEntries() |
354 duration = self.__currentEntry.getDuration() |
365 duration = self.__currentEntry.getDuration() |
368 |
379 |
369 def getCurrentEntry(self): |
380 def getCurrentEntry(self): |
370 """ |
381 """ |
371 Public method to get a reference to the current tracker entry. |
382 Public method to get a reference to the current tracker entry. |
372 |
383 |
373 @return reference to the current entry (TimeTrackEntry) |
384 @return reference to the current entry |
|
385 @rtype TimeTrackEntry |
374 """ |
386 """ |
375 return self.__currentEntry |
387 return self.__currentEntry |
376 |
388 |
377 def getEntry(self, eid): |
389 def getEntry(self, eid): |
378 """ |
390 """ |
379 Public method to get a tracker entry given its ID. |
391 Public method to get a tracker entry given its ID. |
380 |
392 |
381 @param eid ID of the tracker entry (integer) |
393 @param eid ID of the tracker entry |
382 @return entry for the given ID (TimeTrackEntry) or None |
394 @type int |
|
395 @return entry for the given ID or None |
|
396 @rtype TimeTrackEntry |
383 """ |
397 """ |
384 if eid in self.__entries: |
398 if eid in self.__entries: |
385 return self.__entries[eid] |
399 return self.__entries[eid] |
386 else: |
400 else: |
387 return None |
401 return None |
388 |
402 |
389 def deleteTrackerEntry(self, eid): |
403 def deleteTrackerEntry(self, eid): |
390 """ |
404 """ |
391 Public method to delete a tracker entry given its ID. |
405 Public method to delete a tracker entry given its ID. |
392 |
406 |
393 @param eid ID of the tracker entry (integer) |
407 @param eid ID of the tracker entry |
|
408 @type int |
394 """ |
409 """ |
395 if eid in self.__entries: |
410 if eid in self.__entries: |
396 del self.__entries[eid] |
411 del self.__entries[eid] |
397 |
412 |
398 def removeDuplicateTrackerEntries(self): |
413 def removeDuplicateTrackerEntries(self): |
418 |
433 |
419 if self.__plugin.getPreferences("AutoSave"): |
434 if self.__plugin.getPreferences("AutoSave"): |
420 self.saveTrackerEntries() |
435 self.saveTrackerEntries() |
421 |
436 |
422 self.__widget.clear() |
437 self.__widget.clear() |
423 self.__widget.showTrackerEntries(sorted(self.__entries.values(), |
438 self.__widget.showTrackerEntries( |
424 reverse=True)) |
439 sorted(self.__entries.values(), reverse=True)) |
425 self.__widget.setCurrentEntry(self.__currentEntry) |
440 self.__widget.setCurrentEntry(self.__currentEntry) |
426 |
441 |
427 def mergeDuplicateTrackerEntries(self): |
442 def mergeDuplicateTrackerEntries(self): |
428 """ |
443 """ |
429 Public slot to merge duplicate time tracker entries. |
444 Public slot to merge duplicate time tracker entries. |
446 |
461 |
447 if self.__plugin.getPreferences("AutoSave"): |
462 if self.__plugin.getPreferences("AutoSave"): |
448 self.saveTrackerEntries() |
463 self.saveTrackerEntries() |
449 |
464 |
450 self.__widget.clear() |
465 self.__widget.clear() |
451 self.__widget.showTrackerEntries(sorted(self.__entries.values(), |
466 self.__widget.showTrackerEntries( |
452 reverse=True)) |
467 sorted(self.__entries.values(), reverse=True)) |
453 self.__widget.setCurrentEntry(self.__currentEntry) |
468 self.__widget.setCurrentEntry(self.__currentEntry) |
454 |
469 |
455 def entryChanged(self): |
470 def entryChanged(self): |
456 """ |
471 """ |
457 Public method to indicate an external change to any of the entries. |
472 Public method to indicate an external change to any of the entries. |
461 |
476 |
462 def getPreferences(self, key): |
477 def getPreferences(self, key): |
463 """ |
478 """ |
464 Public method to retrieve the various settings. |
479 Public method to retrieve the various settings. |
465 |
480 |
466 @param key the key of the value to get |
481 @param key key of the value to get |
467 @return the requested setting |
482 @type str |
|
483 @return value of the requested setting |
|
484 @rtype Any |
468 """ |
485 """ |
469 return self.__plugin.getPreferences(key) |
486 return self.__plugin.getPreferences(key) |
470 |
487 |
471 def __activateWidget(self): |
488 def __activateWidget(self): |
472 """ |
489 """ |
473 Private slot to handle the activation of the project browser. |
490 Private slot to handle the activation of the time tracker widget. |
474 """ |
491 """ |
475 uiLayoutType = self.__ui.getLayoutType() |
492 uiLayoutType = self.__ui.getLayoutType() |
476 |
493 |
477 if uiLayoutType == "Toolboxes": |
494 if uiLayoutType == "Toolboxes": |
478 self.__ui.hToolboxDock.show() |
495 self.__ui.hToolboxDock.show() |