From 464d516537ee72cb2ea1ddf67d9830de6e768bf8 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 31 Jul 2026 09:52:32 +1200 Subject: [PATCH] Add a local, per-project time-spent tracker (#576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a ProjectUsageTracker (sources/project/) that accumulates how long a project has been the active tab, using QElapsedTimer so the value is computed on demand rather than via polling. It's hosted on ProjectPropertiesHandler per that class's own stated design intent ("all new properties should be managed by this class"). The accumulated time is persisted as a new element, a sibling of in the project XML, written/read by new QETProject::writeUsageXml()/ readUsageXml(). It rides along on the existing autosave path for free, since writeBackup() already serializes the full project via toXml(). QETDiagramEditor::subWindowActivated() now pauses every open project's tracker except the one whose tab just became current, so switching between several open projects keeps each project's tracked time isolated. Surfaced in the existing Project Properties "Général" page: a "Temps passé sur ce projet" display, a "Réinitialiser" button, and an opt-out checkbox ("uniquement enregistré localement dans ce fichier" - this is local-only, never transmitted anywhere). Verified beyond compiling: full CMake build, then an actual runtime session confirming the saved XML's time_spent value, that closing and reopening the project round-trips and resumes timing, that the reset button works, and - the key correctness check - that with two projects open, the inactive one's time_spent stays frozen while the active one accumulates real elapsed time, over the same interval. See discussion #576. Co-Authored-By: Claude Sonnet 5 --- cmake/qet_compilation_vars.cmake | 2 + sources/project/projectpropertieshandler.cpp | 5 + sources/project/projectpropertieshandler.h | 3 + sources/project/projectusagetracker.cpp | 121 +++++++++++++++++++ sources/project/projectusagetracker.h | 58 +++++++++ sources/qetdiagrameditor.cpp | 27 +++++ sources/qetdiagrameditor.h | 1 + sources/qetproject.cpp | 26 ++++ sources/qetproject.h | 2 + sources/ui/configpage/projectconfigpages.cpp | 47 +++++++ sources/ui/configpage/projectconfigpages.h | 11 +- 11 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 sources/project/projectusagetracker.cpp create mode 100644 sources/project/projectusagetracker.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 233f08d7a..5c994a25a 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -438,6 +438,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/project/projectpropertieshandler.cpp ${QET_DIR}/sources/project/projectpropertieshandler.h + ${QET_DIR}/sources/project/projectusagetracker.cpp + ${QET_DIR}/sources/project/projectusagetracker.h ${QET_DIR}/sources/properties/elementdata.cpp ${QET_DIR}/sources/properties/elementdata.h diff --git a/sources/project/projectpropertieshandler.cpp b/sources/project/projectpropertieshandler.cpp index 4d924de37..10b021148 100644 --- a/sources/project/projectpropertieshandler.cpp +++ b/sources/project/projectpropertieshandler.cpp @@ -28,3 +28,8 @@ TerminalStripLayoutsHandler &ProjectPropertiesHandler::terminalStripLayoutHandle { return m_terminal_strip_layout_handler; } + +ProjectUsageTracker &ProjectPropertiesHandler::usageTracker() +{ + return m_usage_tracker; +} diff --git a/sources/project/projectpropertieshandler.h b/sources/project/projectpropertieshandler.h index 29413d3f0..b78153fbb 100644 --- a/sources/project/projectpropertieshandler.h +++ b/sources/project/projectpropertieshandler.h @@ -21,6 +21,7 @@ #include #include "../TerminalStrip/GraphicsItem/properties/terminalstriplayoutshandler.h" +#include "projectusagetracker.h" class QETProject; @@ -43,11 +44,13 @@ class ProjectPropertiesHandler ProjectPropertiesHandler(QETProject *project); TerminalStripLayoutsHandler& terminalStripLayoutHandler(); + ProjectUsageTracker& usageTracker(); private: QPointer m_project; TerminalStripLayoutsHandler m_terminal_strip_layout_handler; + ProjectUsageTracker m_usage_tracker; }; #endif // PROJECTPROPERTIESHANDLER_H diff --git a/sources/project/projectusagetracker.cpp b/sources/project/projectusagetracker.cpp new file mode 100644 index 000000000..1e2a5743a --- /dev/null +++ b/sources/project/projectusagetracker.cpp @@ -0,0 +1,121 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "projectusagetracker.h" + +#include + +/** + @brief ProjectUsageTracker::secondsSpent + @return the total accumulated time, in seconds, including the time + elapsed since the tracker was last made active (if it currently is). +*/ +qint64 ProjectUsageTracker::secondsSpent() const +{ + qint64 total = m_seconds_spent; + if (m_elapsed_timer.isValid()) { + total += m_elapsed_timer.elapsed() / 1000; + } + return total; +} + +/** + @brief ProjectUsageTracker::resetSecondsSpent + Zero the accumulated time. If the tracker is currently timing, it keeps + timing from this point on. +*/ +void ProjectUsageTracker::resetSecondsSpent() +{ + m_seconds_spent = 0; + if (m_elapsed_timer.isValid()) { + m_elapsed_timer.restart(); + } +} + +/** + @brief ProjectUsageTracker::setEnabled + Enable or disable time tracking for this project. Disabling while + currently timing flushes the elapsed time first; re-enabling while + the project is the active tab resumes timing immediately. + @param enabled +*/ +void ProjectUsageTracker::setEnabled(bool enabled) +{ + if (m_enabled == enabled) { + return; + } + + if (!enabled && m_elapsed_timer.isValid()) { + m_seconds_spent += m_elapsed_timer.elapsed() / 1000; + m_elapsed_timer.invalidate(); + } else if (enabled && m_active) { + m_elapsed_timer.start(); + } + m_enabled = enabled; +} + +/** + @brief ProjectUsageTracker::setActive + Tell the tracker whether this project's tab is currently the active + one. No-op if tracking is disabled or the state doesn't change. + @param active +*/ +void ProjectUsageTracker::setActive(bool active) +{ + if (m_active == active) { + return; + } + + if (!active && m_elapsed_timer.isValid()) { + m_seconds_spent += m_elapsed_timer.elapsed() / 1000; + m_elapsed_timer.invalidate(); + } else if (active && m_enabled) { + m_elapsed_timer.start(); + } + m_active = active; +} + +/** + @brief ProjectUsageTracker::toXml + Append a child element to @p parent_element with the current + accumulated time and enabled state. + @param parent_element +*/ +void ProjectUsageTracker::toXml(QDomElement &parent_element) const +{ + QDomElement usage_element = + parent_element.ownerDocument().createElement(QStringLiteral("usage")); + usage_element.setAttribute(QStringLiteral("time_spent"), QString::number(secondsSpent())); + usage_element.setAttribute(QStringLiteral("enabled"), m_enabled ? QStringLiteral("true") : QStringLiteral("false")); + parent_element.appendChild(usage_element); +} + +/** + @brief ProjectUsageTracker::fromXml + Read the child element of @p parent_element, if any. + @param parent_element +*/ +void ProjectUsageTracker::fromXml(const QDomElement &parent_element) +{ + const QDomElement usage_element = parent_element.firstChildElement(QStringLiteral("usage")); + if (usage_element.isNull()) { + return; + } + + m_seconds_spent = usage_element.attribute(QStringLiteral("time_spent"), QStringLiteral("0")).toLongLong(); + m_enabled = usage_element.attribute(QStringLiteral("enabled"), QStringLiteral("true")) != QStringLiteral("false"); +} diff --git a/sources/project/projectusagetracker.h b/sources/project/projectusagetracker.h new file mode 100644 index 000000000..fb805a30b --- /dev/null +++ b/sources/project/projectusagetracker.h @@ -0,0 +1,58 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef PROJECTUSAGETRACKER_H +#define PROJECTUSAGETRACKER_H + +#include + +class QDomElement; + +/** + @brief The ProjectUsageTracker class + Accumulates how long a project has been the active tab, purely locally: + the value lives only in the project's own file (a element next + to ) and is never transmitted anywhere. + + The owning code (QETDiagramEditor) is expected to call setActive(true) + when this project's tab becomes the current one, and setActive(false) + when it stops being current (tab switch, project closed, ...). +*/ +class ProjectUsageTracker +{ + public: + ProjectUsageTracker() = default; + + qint64 secondsSpent() const; + void resetSecondsSpent(); + + bool isEnabled() const {return m_enabled;} + void setEnabled(bool enabled); + + void setActive(bool active); + + void toXml(QDomElement &parent_element) const; + void fromXml(const QDomElement &parent_element); + + private: + qint64 m_seconds_spent = 0; + bool m_enabled = true; + bool m_active = false; + QElapsedTimer m_elapsed_timer; +}; + +#endif // PROJECTUSAGETRACKER_H diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index da9c09083..88c239d6e 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -29,6 +29,9 @@ #include "elementspanelwidget.h" #include "factory/qetgraphicstablefactory.h" #include "print/projectprintwindow.h" +#include "project/projectpropertieshandler.h" +#include "projectview.h" +#include "qetproject.h" #include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h" #include "qetgraphicsitem/conductortextitem.h" #include "qetgraphicsitem/dynamicelementtextitem.h" @@ -2466,6 +2469,30 @@ void QETDiagramEditor::subWindowActivated(QMdiSubWindow *subWindows) slot_updateActions(); slot_updateWindowsMenu(); emit syncElementsPanel(); + updateUsageTrackersActiveState(); +} + +/** + @brief QETDiagramEditor::updateUsageTrackersActiveState + Mark the currently active project's usage tracker (time spent on this + project) as active, and every other opened project's tracker as + inactive. Called whenever the current MDI subwindow changes. + + Known limitation: this only accounts for tab switches within this + QETDiagramEditor window. If the same project were ever shown as the + active tab in two different windows at once, its tracked time could be + double-counted -- today QETApp only ever gives a project one ProjectView, + so this doesn't happen in practice. +*/ +void QETDiagramEditor::updateUsageTrackersActiveState() +{ + QETProject *active_project = currentProject(); + const QList project_views = openedProjects(); + for (ProjectView *project_view : project_views) { + if (QETProject *project = project_view->project()) { + project->projectPropertiesHandler().usageTracker().setActive(project == active_project); + } + } } /** diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index 9daf6a122..98a60a967 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -97,6 +97,7 @@ class QETDiagramEditor : public QETMainWindow ProjectView *findProject(QETProject *) const; ProjectView *findProject(const QString &) const; QMdiSubWindow *subWindowForWidget(QWidget *) const; + void updateUsageTrackersActiveState(); signals: void syncElementsPanel(); diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index cf7dfc799..f64440a09 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -996,6 +996,9 @@ QDomDocument QETProject::toXml() writeProjectPropertiesXml(project_properties); project_root.appendChild(project_properties); + // local, non-transmitted usage tracking (time spent on this project) + writeUsageXml(project_root); + // Properties for news diagrams QDomElement new_diagrams_properties = xml_doc.createElement("newdiagrams"); writeDefaultPropertiesXml(new_diagrams_properties); @@ -1496,6 +1499,9 @@ void QETProject::readProjectXml(QDomDocument &xml_project) //Load the project-wide properties readProjectPropertiesXml(xml_project); + //Load the local, non-transmitted usage tracking + readUsageXml(xml_project); + //Load the default properties for the new diagrams readDefaultPropertiesXml(xml_project); @@ -1649,6 +1655,17 @@ void QETProject::readProjectPropertiesXml(QDomDocument &xml_project) m_project_properties.fromXml(dom_elmt); } +/** + @brief QETProject::readUsageXml + Load the local, non-transmitted usage tracking (time spent on this + project) from the XML description of the project. + @param xml_project : the xml description of the project +*/ +void QETProject::readUsageXml(QDomDocument &xml_project) +{ + m_project_properties_handler.usageTracker().fromXml(xml_project.documentElement()); +} + /** @brief QETProject::readDefaultPropertiesXml load default properties for new diagram, found in the xml of this project @@ -1784,6 +1801,15 @@ void QETProject::writeProjectPropertiesXml(QDomElement &xml_element) { m_project_properties.toXml(xml_element); } +/** + @brief QETProject::writeUsageXml + Export the local, non-transmitted usage tracking (time spent on this + project) as a child of \a xml_element. +*/ +void QETProject::writeUsageXml(QDomElement &xml_element) { + m_project_properties_handler.usageTracker().toXml(xml_element); +} + /** @brief QETProject::writeDefaultPropertiesXml Export all defaults properties used by a new diagram and his content diff --git a/sources/qetproject.h b/sources/qetproject.h index 6e96c1e6a..5e1a1a368 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -251,9 +251,11 @@ class QETProject : public QObject void readProjectPropertiesXml(QDomDocument &xml_project); void readDefaultPropertiesXml(QDomDocument &xml_project); void readTerminalStripXml(const QDomDocument &xml_project); + void readUsageXml(QDomDocument &xml_project); void writeProjectPropertiesXml(QDomElement &); void writeDefaultPropertiesXml(QDomElement &); + void writeUsageXml(QDomElement &); void addDiagram(Diagram *diagram, int pos = -1); void writeBackup(); void init(); diff --git a/sources/ui/configpage/projectconfigpages.cpp b/sources/ui/configpage/projectconfigpages.cpp index 422e27109..e4fcb69ef 100644 --- a/sources/ui/configpage/projectconfigpages.cpp +++ b/sources/ui/configpage/projectconfigpages.cpp @@ -22,6 +22,7 @@ #include "../autoNum/ui/folioautonumbering.h" #include "../autoNum/ui/formulaautonumberingw.h" #include "../autoNum/ui/selectautonumw.h" +#include "../project/projectpropertieshandler.h" #include "../qeticons.h" #include "../qetproject.h" #include "../borderpropertieswidget.h" @@ -161,6 +162,13 @@ void ProjectMainConfigPage::applyProjectConf() m_project -> setProjectProperties(new_properties); modified_project = true; } + + ProjectUsageTracker &usage_tracker = m_project -> projectPropertiesHandler().usageTracker(); + if (usage_tracker.isEnabled() != usage_enabled_cb_ -> isChecked()) { + usage_tracker.setEnabled(usage_enabled_cb_ -> isChecked()); + modified_project = true; + } + if (modified_project) { m_project -> setModified(true); } @@ -191,6 +199,12 @@ void ProjectMainConfigPage::initWidgets() project_variables_label_ -> setWordWrap(true); project_variables_ = new DiagramContextWidget(); project_variables_ -> setContext(DiagramContext()); + + usage_label_ = new QLabel(tr("Temps passé sur ce projet :", "label when configuring")); + usage_value_ = new QLabel(); + usage_enabled_cb_ = new QCheckBox(tr("Suivre le temps passé sur ce projet (uniquement enregistré localement dans ce fichier)", "checkbox label")); + usage_reset_pb_ = new QPushButton(tr("Réinitialiser", "button label")); + connect(usage_reset_pb_, &QPushButton::clicked, this, &ProjectMainConfigPage::resetUsageTracker); } /** @@ -207,6 +221,16 @@ void ProjectMainConfigPage::initLayout() main_layout0 -> addSpacing(10); main_layout0 -> addWidget(project_variables_label_); main_layout0 -> addWidget(project_variables_); + main_layout0 -> addSpacing(10); + + QHBoxLayout *usage_layout0 = new QHBoxLayout(); + usage_layout0 -> addWidget(usage_label_); + usage_layout0 -> addWidget(usage_value_); + usage_layout0 -> addStretch(); + usage_layout0 -> addWidget(usage_reset_pb_); + main_layout0 -> addLayout(usage_layout0); + main_layout0 -> addWidget(usage_enabled_cb_); + setLayout(main_layout0); this -> setMinimumWidth(680); @@ -219,6 +243,27 @@ void ProjectMainConfigPage::readValuesFromProject() { title_value_ -> setText(m_project -> title()); project_variables_ -> setContext(m_project -> projectProperties()); + + const ProjectUsageTracker &usage_tracker = m_project -> projectPropertiesHandler().usageTracker(); + const qint64 total_seconds = usage_tracker.secondsSpent(); + usage_value_ -> setText(tr("%1 h %2 min", "hours and minutes of time spent on a project") + .arg(total_seconds / 3600) + .arg((total_seconds % 3600) / 60)); + usage_enabled_cb_ -> setChecked(usage_tracker.isEnabled()); +} + +/** + @brief ProjectMainConfigPage::resetUsageTracker + Reset the accumulated "time spent on this project" counter to zero and + refresh its displayed value. Applies immediately (not staged behind + OK/Cancel like the other fields on this page), since it isn't + destructive to any actual project content. +*/ +void ProjectMainConfigPage::resetUsageTracker() +{ + m_project -> projectPropertiesHandler().usageTracker().resetSecondsSpent(); + m_project -> setModified(true); + usage_value_ -> setText(tr("%1 h %2 min", "hours and minutes of time spent on a project").arg(0).arg(0)); } /** @@ -229,6 +274,8 @@ void ProjectMainConfigPage::adjustReadOnly() { bool is_read_only = m_project -> isReadOnly(); title_value_ -> setReadOnly(is_read_only); + usage_enabled_cb_ -> setDisabled(is_read_only); + usage_reset_pb_ -> setDisabled(is_read_only); } //######################################################################################// diff --git a/sources/ui/configpage/projectconfigpages.h b/sources/ui/configpage/projectconfigpages.h index 7fd7f19f3..79649689a 100644 --- a/sources/ui/configpage/projectconfigpages.h +++ b/sources/ui/configpage/projectconfigpages.h @@ -21,6 +21,8 @@ class QLabel; class QLineEdit; +class QCheckBox; +class QPushButton; class QETProject; class BorderPropertiesWidget; class ConductorPropertiesWidget; @@ -108,7 +110,10 @@ class ProjectMainConfigPage : public ProjectConfigPage { void initLayout() override; void readValuesFromProject() override; void adjustReadOnly() override; - + + private slots: + void resetUsageTracker(); + // attributes protected: QLabel *title_label_; @@ -116,6 +121,10 @@ class ProjectMainConfigPage : public ProjectConfigPage { QLabel *title_information_; QLabel *project_variables_label_; DiagramContextWidget *project_variables_; + QLabel *usage_label_; + QLabel *usage_value_; + QCheckBox *usage_enabled_cb_; + QPushButton *usage_reset_pb_; }; class ProjectAutoNumConfigPage : public ProjectConfigPage {