diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f24c7fd..bc4ef01 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,94 +1,97 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ) set(ktimetracker_SRCS export/totalsastext.cpp file/filecalendar.cpp file/icalformatkio.cpp + model/event.cpp + model/eventsmodel.cpp + model/projectmodel.cpp model/task.cpp model/tasksmodel.cpp model/tasksmodelitem.cpp settings/ktimetrackerconfigdialog.cpp widgets/searchline.cpp csvexportdialog.cpp desktoptracker.cpp edittaskdialog.cpp focusdetector.cpp historydialog.cpp idletimedetector.cpp ktimetrackerutility.cpp mainwindow.cpp plannerparser.cpp taskview.cpp timetrackerstorage.cpp timetrackerwidget.cpp tray.cpp treeviewheadercontextmenu.cpp $ $ ) ecm_qt_declare_logging_category(ktimetracker_SRCS HEADER ktt_debug.h IDENTIFIER KTT_LOG CATEGORY_NAME log_ktt ) qt5_add_dbus_adaptor(ktimetracker_SRCS org.kde.ktimetracker.ktimetracker.xml timetrackerwidget.h TimeTrackerWidget mainadaptor MainAdaptor ) ki18n_wrap_ui(ktimetracker_SRCS csvexportdialog.ui historydialog.ui edittaskdialog.ui settings/cfgbehavior.ui settings/cfgdisplay.ui settings/cfgstorage.ui ) kconfig_add_kcfg_files(ktimetracker_SRCS settings/ktimetracker.kcfgc) qt5_add_resources(ktimetracker_SRCS ktimetracker.qrc) add_library(libktimetracker STATIC ${ktimetracker_SRCS}) target_link_libraries(libktimetracker KF5::KCMUtils KF5::WindowSystem KF5::Notifications KF5::IconThemes KF5::I18n KF5::XmlGui KF5::JobWidgets KF5::KIOCore KF5::KIOWidgets KF5::IdleTime KF5::DBusAddons KF5::CalendarCore ${X11_X11_LIB} ) if(X11_Xscreensaver_LIB) target_link_libraries(libktimetracker ${X11_Xscreensaver_LIB}) endif() add_executable(ktimetracker main.cpp) target_link_libraries(ktimetracker libktimetracker) install(TARGETS ktimetracker ${INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES org.kde.ktimetracker.ktimetracker.xml DESTINATION ${DBUS_INTERFACES_INSTALL_DIR}) install(PROGRAMS ktimetracker.desktop DESTINATION ${KDE_INSTALL_APPDIR}) if(BUILD_TESTING) add_subdirectory(tests) endif() diff --git a/src/historydialog.cpp b/src/historydialog.cpp index 9081d56..c6c25ab 100644 --- a/src/historydialog.cpp +++ b/src/historydialog.cpp @@ -1,263 +1,260 @@ /* * Copyright (C) 2007 by Thorsten Staerk and Mathias Soeken * * This program 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. * * This program 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 this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ #include "historydialog.h" #include #include #include #include "file/filecalendar.h" +#include "model/event.h" +#include "model/eventsmodel.h" +#include "model/tasksmodel.h" +#include "model/task.h" #include "taskview.h" #include "ktt_debug.h" static const QString dateTimeFormat = QStringLiteral("yyyy-MM-dd HH:mm:ss"); class HistoryWidgetDelegate : public QItemDelegate { public: explicit HistoryWidgetDelegate(QObject *parent = nullptr) : QItemDelegate(parent) { } QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &) const override { auto* editor = new QDateTimeEdit(parent); editor->setAutoFillBackground(true); editor->setPalette(option.palette); editor->setBackgroundRole(QPalette::Background); return editor; } void setEditorData(QWidget *editor, const QModelIndex &index) const override { QDateTime dateTime = QDateTime::fromString(index.model()->data(index, Qt::DisplayRole).toString(), dateTimeFormat); auto *dateTimeWidget = dynamic_cast(editor); if (dateTimeWidget) { dateTimeWidget->setDateTime(dateTime); } else { qCWarning(KTT_LOG) << "Cast to QDateTimeEdit failed"; } } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { auto *dateTimeWidget = dynamic_cast(editor); if (dateTimeWidget) { QDateTime dateTime = dateTimeWidget->dateTime(); model->setData(index, dateTime.toString(dateTimeFormat), Qt::EditRole); } else { qCWarning(KTT_LOG) << "Cast to QDateTimeEdit failed"; } } void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &) const override { editor->setGeometry(option.rect); } }; HistoryDialog::HistoryDialog(QWidget *parent, TimeTrackerStorage *storage) : QDialog(parent) , m_storage(storage) , m_ui() { m_ui.setupUi(this); /* Item Delegate for displaying KDateTimeWidget instead of QLineEdit */ auto *historyWidgetDelegate = new HistoryWidgetDelegate(m_ui.historytablewidget); m_ui.historytablewidget->setItemDelegateForColumn(1, historyWidgetDelegate); m_ui.historytablewidget->setItemDelegateForColumn(2, historyWidgetDelegate); m_ui.historytablewidget->setEditTriggers(QAbstractItemView::AllEditTriggers); m_ui.historytablewidget->setColumnCount(5); m_ui.historytablewidget->setHorizontalHeaderLabels( QStringList{i18n("Task"), i18n("StartTime"), i18n("EndTime"), i18n("Comment"), QStringLiteral("event UID")}); m_ui.historytablewidget->horizontalHeader()->setStretchLastSection(true); m_ui.historytablewidget->setColumnHidden(4, true); // hide the "UID" column listAllEvents(); m_ui.historytablewidget->setSortingEnabled(true); m_ui.historytablewidget->sortItems(1, Qt::DescendingOrder); m_ui.historytablewidget->resizeColumnsToContents(); } QString HistoryDialog::listAllEvents() { QString err = QString(); // if sorting is enabled and we write to row x, we cannot be sure row x will be in row x some lines later bool old_sortingenabled = m_ui.historytablewidget->isSortingEnabled(); m_ui.historytablewidget->setSortingEnabled(false); connect(m_ui.historytablewidget, &QTableWidget::cellChanged, this, &HistoryDialog::onCellChanged); - FileCalendar::Ptr calendar = m_storage->calendar(); - for (const auto &event : m_storage->rawevents()) { + for (const auto *event : m_storage->eventsModel()->events()) { int row = m_ui.historytablewidget->rowCount(); m_ui.historytablewidget->insertRow(row); // maybe the file is corrupt and (*i)->relatedTo is NULL if (event->relatedTo().isEmpty()) { qCDebug(KTT_LOG) << "There is no 'relatedTo' entry for " << event->summary(); err = "NoRelatedToForEvent"; continue; } - KCalCore::Incidence::Ptr parent = calendar ? calendar->incidence(event->relatedTo()) : KCalCore::Incidence::Ptr(); - auto *item = new QTableWidgetItem(parent ? parent->summary() : event->summary()); + const Task *parent = dynamic_cast(m_storage->tasksModel()->taskByUID(event->relatedTo())); + if (!parent) { + qFatal("orphan event"); + } + + auto *item = new QTableWidgetItem(parent->name()); item->setFlags(Qt::ItemIsEnabled); item->setWhatsThis(i18n("You can change this task's comment, start time and end time.")); m_ui.historytablewidget->setItem(row, 0, item); QDateTime start = event->dtStart(); QDateTime end = event->dtEnd(); m_ui.historytablewidget->setItem(row, 1, new QTableWidgetItem(start.toString(dateTimeFormat))); m_ui.historytablewidget->setItem(row, 2, new QTableWidgetItem(end.toString(dateTimeFormat))); m_ui.historytablewidget->setItem(row, 4, new QTableWidgetItem(event->uid())); qDebug() << "event->comments.count() =" << event->comments().count(); if (event->comments().count() > 0) { m_ui.historytablewidget->setItem(row, 3, new QTableWidgetItem(event->comments().last())); } } m_ui.historytablewidget->resizeColumnsToContents(); m_ui.historytablewidget->setColumnWidth(1, 300); m_ui.historytablewidget->setColumnWidth(2, 300); setMinimumSize( m_ui.historytablewidget->columnWidth(0) + m_ui.historytablewidget->columnWidth(1) + m_ui.historytablewidget->columnWidth(2) + m_ui.historytablewidget->columnWidth(3), height()); m_ui.historytablewidget->setSortingEnabled(old_sortingenabled); return err; } void HistoryDialog::changeEvent(QEvent *e) { QDialog::changeEvent(e); switch (e->type()) { case QEvent::LanguageChange: m_ui.retranslateUi(this); break; default: break; } } void HistoryDialog::onCellChanged(int row, int col) { qCDebug(KTT_LOG) << "row =" << row << " col =" << col; if (!m_ui.historytablewidget->item(row, 4)) { // the program did the change, not the user return; } switch (col) { case 1: { // StartDate changed qCDebug(KTT_LOG) << "user changed StartDate to" << m_ui.historytablewidget->item(row, col)->text(); QDateTime datetime = QDateTime::fromString(m_ui.historytablewidget->item(row, col)->text(), dateTimeFormat); if (!datetime.isValid()) { KMessageBox::information(nullptr, i18n("This is not a valid Date/Time.")); break; } QString uid = m_ui.historytablewidget->item(row, 4)->text(); - for (const auto &event : m_storage->rawevents()) { - if (event->uid() == uid) { - event->setDtStart(datetime); - emit timesChanged(); - qCDebug(KTT_LOG) << "Program SetDtStart to" << m_ui.historytablewidget->item(row, col)->text(); - } - } + Event *event = m_storage->eventsModel()->eventByUID(uid); + event->setDtStart(datetime); + emit timesChanged(); + qCDebug(KTT_LOG) << "Program SetDtStart to" << m_ui.historytablewidget->item(row, col)->text(); break; } case 2: { // EndDate changed qCDebug(KTT_LOG) << "user changed EndDate to" << m_ui.historytablewidget->item(row,col)->text(); QDateTime datetime = QDateTime::fromString(m_ui.historytablewidget->item(row, col)->text(), dateTimeFormat); if (!datetime.isValid()) { KMessageBox::information(nullptr, i18n("This is not a valid Date/Time.")); break; } QString uid = m_ui.historytablewidget->item(row, 4)->text(); - for (const auto &event : m_storage->rawevents()) { - if (event->uid() == uid) { - event->setDtEnd(datetime); - emit timesChanged(); - qCDebug(KTT_LOG) << "Program SetDtEnd to" << m_ui.historytablewidget->item(row, col)->text(); - } - } + Event *event = m_storage->eventsModel()->eventByUID(uid); + event->setDtEnd(datetime); + emit timesChanged(); + qCDebug(KTT_LOG) << "Program SetDtEnd to" << m_ui.historytablewidget->item(row, col)->text(); break; } case 3: { // Comment changed qCDebug(KTT_LOG) << "user changed Comment to" << m_ui.historytablewidget->item(row, col)->text(); QString uid = m_ui.historytablewidget->item(row, 4)->text(); + Event *event = m_storage->eventsModel()->eventByUID(uid); qCDebug(KTT_LOG) << "uid =" << uid; - for (const auto &event : m_storage->rawevents()) { - if (event->uid() == uid) { - event->addComment(m_ui.historytablewidget->item(row, col)->text()); - qCDebug(KTT_LOG) << "added" << m_ui.historytablewidget->item(row, col)->text(); - } - } + event->addComment(m_ui.historytablewidget->item(row, col)->text()); + qCDebug(KTT_LOG) << "added" << m_ui.historytablewidget->item(row, col)->text(); break; } default: break; } } QString HistoryDialog::refresh() { while (m_ui.historytablewidget->rowCount() > 0) { m_ui.historytablewidget->removeRow(0); } listAllEvents(); return QString(); } void HistoryDialog::on_deletepushbutton_clicked() { if (m_ui.historytablewidget->item(m_ui.historytablewidget->currentRow(), 4)) { // if an item is current QString uid = m_ui.historytablewidget->item(m_ui.historytablewidget->currentRow(), 4)->text(); qDebug() << "uid =" << uid; - for (const auto &event : m_storage->rawevents()) { - if (event->uid() == uid) { - qCDebug(KTT_LOG) << "removing uid " << event->uid(); - m_storage->removeEvent(event->uid()); - emit timesChanged(); - this->refresh(); - } + const Event *event = m_storage->eventsModel()->eventByUID(uid); + if (event) { + qCDebug(KTT_LOG) << "removing uid " << event->uid(); + m_storage->removeEvent(event->uid()); + emit timesChanged(); + this->refresh(); } } else { KMessageBox::information(this, i18n("Please select a task to delete.")); } } void HistoryDialog::on_okpushbutton_clicked() { m_ui.historytablewidget->setCurrentCell(0, 0); // you need to change the cell to store the value close(); } diff --git a/src/model/event.cpp b/src/model/event.cpp new file mode 100644 index 0000000..08bc91e --- /dev/null +++ b/src/model/event.cpp @@ -0,0 +1,112 @@ +#include "event.h" + +#include + +#include "eventsmodel.h" +#include "ktt_debug.h" +#include "ktimetrackerutility.h" + +static const QByteArray eventAppName = QByteArray("ktimetracker"); + +Event::Event(const KCalCore::Event::Ptr &event, EventsModel *model) + : m_summary(event->summary()) + , m_dtStart(event->dtStart()) + , m_dtEnd(event->hasEndDate() ? event->dtEnd() : QDateTime()) + , m_uid(event->uid()) + , m_relatedTo(event->relatedTo()) + , m_comments(event->comments()) + , m_duration(0) + , m_model(model) +{ + if (!m_dtStart.isValid()) { + qFatal("1"); + } + + bool ok = false; + m_duration = getCustomProperty(event, QStringLiteral("duration")).toInt(&ok); + if (!ok) { + m_duration = 0; + } +} + +QString Event::summary() const +{ + return m_summary; +} + +void Event::setDtStart(const QDateTime &dtStart) +{ + m_dtStart = dtStart; +// m_model->updateInStorage(this); +} + +QDateTime Event::dtStart() const +{ + return m_dtStart; +} + +void Event::setDtEnd(const QDateTime &dtEnd) +{ + m_dtEnd = dtEnd; +// m_model->updateInStorage(this); +} + +bool Event::hasEndDate() const +{ + return m_dtEnd.isValid(); +} + +QDateTime Event::dtEnd() const +{ + return m_dtEnd; +} + +QString Event::uid() const +{ + return m_uid; +} + +QString Event::relatedTo() const +{ + return m_relatedTo; +} + +void Event::addComment(const QString &comment) +{ + m_comments.append(comment); +} + +QStringList Event::comments() const +{ + return m_comments; +} + +void Event::setDuration(long duration) +{ + m_duration = duration; +} + +KCalCore::Event::Ptr Event::asCalendarEvent(const KCalCore::Event::Ptr &event) const +{ + Q_ASSERT(event != nullptr); + + qCDebug(KTT_LOG) << "Task::asTodo: uid" << m_uid << ", summary" << m_summary; + + event->setSummary(summary()); + event->setDtStart(m_dtStart); + if (hasEndDate()) { + event->setDtEnd(m_dtEnd); + } + event->setUid(uid()); + event->setRelatedTo(relatedTo()); + for (auto comment : comments()) { + event->addComment(comment); + } + + // Use a custom property to keep a record of negative durations + event->setCustomProperty(eventAppName, QByteArray("duration"), QString::number(m_duration)); + + event->setCategories({i18n("KTimeTracker")}); + + return event; +} diff --git a/src/model/event.h b/src/model/event.h new file mode 100644 index 0000000..2c4f237 --- /dev/null +++ b/src/model/event.h @@ -0,0 +1,48 @@ +#ifndef KTIMETRACKER_EVENT_H +#define KTIMETRACKER_EVENT_H + +#include + +class EventsModel; + +class Event +{ +public: + explicit Event(const KCalCore::Event::Ptr &event, EventsModel *model); + + QString summary() const; + + void setDtStart(const QDateTime &dtStart); + QDateTime dtStart() const; + + void setDtEnd(const QDateTime &dtEnd); + bool hasEndDate() const; + QDateTime dtEnd() const; + + QString uid() const; + + QString relatedTo() const; + + void addComment(const QString &comment); + QStringList comments() const; + + void setDuration(long duration); + + /** + * Load the event passed in with this event's info. + */ + KCalCore::Event::Ptr asCalendarEvent(const KCalCore::Event::Ptr &event) const; + +private: + QString m_summary; + QDateTime m_dtStart; + QDateTime m_dtEnd; + QString m_uid; + QString m_relatedTo; + QStringList m_comments; + long m_duration; + + EventsModel *m_model; +}; + +#endif // KTIMETRACKER_EVENT_H diff --git a/src/model/eventsmodel.cpp b/src/model/eventsmodel.cpp new file mode 100644 index 0000000..08ff11b --- /dev/null +++ b/src/model/eventsmodel.cpp @@ -0,0 +1,88 @@ +#include "eventsmodel.h" + +#include "task.h" + +EventsModel::EventsModel() + : m_events() +{ +} + +void EventsModel::load(const KCalCore::Calendar &calendar) +{ + clear(); + + for (const auto& event : calendar.rawEvents()) { + m_events.append(new Event(event, this)); + } +} + +EventsModel::~EventsModel() +{ + clear(); +} + +QList EventsModel::events() const +{ + return m_events; +} + +QList EventsModel::eventsForTask(Task *task) const +{ + QList res; + for (auto *event : events()) { + if (event->relatedTo() == task->uid()) { + res.append(event); + } + } + + return res; +} + +Event *EventsModel::eventByUID(const QString &uid) const +{ + for (auto *event : events()) { + if (event->uid() == uid) { + return event; + } + } + + return nullptr; +} + +void EventsModel::clear() +{ + for (auto *event : m_events) { + delete event; + } + m_events.clear(); +} + +void EventsModel::removeAllForTask(const Task *task) +{ + for (auto it = m_events.begin(); it != m_events.end();) { + Event *event = *it; + if (event->relatedTo() == task->uid()) { + delete event; + it = m_events.erase(it); + } else { + ++it; + } + } +} + +void EventsModel::removeByUID(const QString &uid) +{ + for (auto it = m_events.begin(); it != m_events.end(); ++it) { + Event *event = *it; + if (event->uid() == uid) { + delete event; + m_events.erase(it); + return; + } + } +} + +void EventsModel::addEvent(Event *event) +{ + m_events.append(event); +} diff --git a/src/model/eventsmodel.h b/src/model/eventsmodel.h new file mode 100644 index 0000000..33a7fa3 --- /dev/null +++ b/src/model/eventsmodel.h @@ -0,0 +1,33 @@ +#ifndef KTIMETRACKER_EVENTSMODEL_H +#define KTIMETRACKER_EVENTSMODEL_H + +#include + +#include "event.h" + +class Task; + +class EventsModel +{ +public: + EventsModel(); + virtual ~EventsModel(); + + void load(const KCalCore::Calendar &calendar); + QList events() const; + QList eventsForTask(Task *task) const; + Event *eventByUID(const QString &uid) const; + + // Delete all events + void clear(); + + void removeAllForTask(const Task *task); + void removeByUID(const QString &uid); + + void addEvent(Event *event); + +private: + QList m_events; +}; + +#endif // KTIMETRACKER_EVENTSMODEL_H diff --git a/src/model/projectmodel.cpp b/src/model/projectmodel.cpp new file mode 100644 index 0000000..fa3740c --- /dev/null +++ b/src/model/projectmodel.cpp @@ -0,0 +1,47 @@ +#include "projectmodel.h" + +#include "tasksmodel.h" +#include "task.h" +#include "eventsmodel.h" +#include "event.h" + +ProjectModel::ProjectModel() + : m_tasksModel(new TasksModel()) + , m_eventsModel(new EventsModel()) +{ +} + +TasksModel *ProjectModel::tasksModel() +{ + return m_tasksModel; +} + +EventsModel *ProjectModel::eventsModel() +{ + return m_eventsModel; +} + +FileCalendar::Ptr ProjectModel::asCalendar(const QUrl &url) const +{ + auto calendar = FileCalendar::Ptr(new FileCalendar(url)); + for (auto *item : m_tasksModel->getAllItems()) { + Task *task = dynamic_cast(item); + + KCalCore::Todo::Ptr todo(new KCalCore::Todo()); + calendar->addTodo(task->asTodo(todo)); + } + + + // TODO: Use libkcalcore comments + // todo->addComment(comment); + // temporary +// todo->setDescription(task->comment()); + + + for (Event *event : m_eventsModel->events()) { + KCalCore::Event::Ptr calEvent(new KCalCore::Event()); + calendar->addEvent(event->asCalendarEvent(calEvent)); + } + + return calendar; +} diff --git a/src/model/projectmodel.h b/src/model/projectmodel.h new file mode 100644 index 0000000..6bc0c63 --- /dev/null +++ b/src/model/projectmodel.h @@ -0,0 +1,24 @@ +#ifndef KTIMETRACKER_PROJECTMODEL_H +#define KTIMETRACKER_PROJECTMODEL_H + +#include "file/filecalendar.h" + +class TasksModel; +class EventsModel; + +class ProjectModel +{ +public: + ProjectModel(); + + TasksModel *tasksModel(); + EventsModel *eventsModel(); + + FileCalendar::Ptr asCalendar(const QUrl &url) const; + +private: + TasksModel *m_tasksModel; + EventsModel *m_eventsModel; +}; + +#endif // KTIMETRACKER_PROJECTMODEL_H diff --git a/src/model/task.cpp b/src/model/task.cpp index 20a563f..922325f 100644 --- a/src/model/task.cpp +++ b/src/model/task.cpp @@ -1,611 +1,624 @@ /* * Copyright (C) 1997 by Stephan Kulow * 2007 the ktimetracker developers * * This program 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. * * This program 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 this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ #include "task.h" #include #include #include #include +#include + +#include "model/eventsmodel.h" #include "ktimetrackerutility.h" #include "ktimetracker.h" #include "timetrackerstorage.h" #include "taskview.h" #include "ktt_debug.h" -const QByteArray eventAppName = QByteArray("ktimetracker"); +static const QByteArray eventAppName = QByteArray("ktimetracker"); Task::Task( const QString& taskName, const QString& taskDescription, long minutes, long sessionTime, - DesktopList desktops, TaskView* taskView, Task *parentTask) + DesktopList desktops, TaskView* taskView, EventsModel *eventsModel, Task *parentTask) : QObject() , TasksModelItem(taskView->tasksModel(), parentTask) , m_taskView(taskView) + , m_eventsModel(eventsModel) { if (parentTask) { parentTask->addChild(this); } else { taskView->tasksModel()->addChild(this); } init( taskName, taskDescription, minutes, sessionTime, 0, desktops, 0, 0 ); + + mUid = KCalCore::CalFormat::createUniqueId(); } -Task::Task(const KCalCore::Todo::Ptr &todo, TaskView* taskView) +Task::Task(const KCalCore::Todo::Ptr &todo, TaskView* taskView, EventsModel *eventsModel) : QObject() , TasksModelItem(taskView->tasksModel(), nullptr) , m_taskView(taskView) + , m_eventsModel(eventsModel) { taskView->tasksModel()->addChild(this); long minutes = 0; QString name; QString description; long sessionTime = 0; QString sessionStartTiMe; int percent_complete = 0; int priority = 0; DesktopList desktops; parseIncidence( todo, minutes, sessionTime, sessionStartTiMe, name, description, desktops, percent_complete, priority ); init( name, description, minutes, sessionTime, sessionStartTiMe, desktops, percent_complete, priority); } int Task::depth() // Deliver the depth of a task, i.e. how many tasks are supertasks to it. // A toplevel task has the depth 0. { qCDebug(KTT_LOG) << "Entering function"; int res = 0; Task* t = this; while (t = t->parentTask()) { res++; } qCDebug(KTT_LOG) << "Leaving function. depth is:" << res; return res; } void Task::init( const QString& taskName, const QString& taskDescription, long minutes, long sessionTime, QString sessionStartTiMe, const DesktopList& desktops, int percent_complete, int priority) { // If our parent is the taskview then connect our totalTimesChanged // signal to its receiver if (!parentTask()) { connect(this, &Task::totalTimesChanged, m_taskView, &TaskView::taskTotalTimesChanged); } connect(this, &Task::deletingTask, m_taskView, &TaskView::deletingTask); m_isRunning = false; mName = taskName.trimmed(); mDescription = taskDescription.trimmed(); mLastStart = QDateTime::currentDateTime(); mTotalTime = mTime = minutes; mTotalSessionTime = mSessionTime = sessionTime; mDesktops = desktops; mPercentComplete = percent_complete; mPriority = priority; mSessionStartTiMe = QDateTime::fromString(sessionStartTiMe); update(); changeParentTotalTimes(mSessionTime, mTime); } Task::~Task() { emit deletingTask(this); } void Task::delete_recursive() { while (this->child(0)) { Task* t = (Task*) this->child(0); t->delete_recursive(); } delete this; } // This is the back-end, the front-end is StartTimerFor() void Task::setRunning(bool on, TimeTrackerStorage* storage, const QDateTime& when) { if (on != m_isRunning) { m_isRunning = on; invalidateRunningState(); if (on) { mLastStart = when; qCDebug(KTT_LOG) << "task has been started for " << when; } } } // setRunning is the back-end, the front-end is StartTimerFor(). // resumeRunning does the same as setRunning, but not add a new // start date to the storage. void Task::resumeRunning() { qCDebug(KTT_LOG) << "Entering function"; if (!isRunning()) { m_isRunning = true; invalidateRunningState(); } } -void Task::setUid(const QString& uid) -{ - mUid = uid; -} - bool Task::isRunning() const { return m_isRunning; } void Task::setName(const QString& name, TimeTrackerStorage* storage) { qCDebug(KTT_LOG) << "Entering function, name=" << name; QString oldname = mName; if (oldname != name) { mName = name; storage->setName(this, oldname); update(); } } void Task::setDescription(const QString& description) { qCDebug(KTT_LOG) << "Entering function, description=" << description; QString olddescription = mDescription; if (olddescription != description) { mDescription = description; update(); } } void Task::setPercentComplete(int percent) { qCDebug(KTT_LOG) << "Entering function(" << percent << "):" << mUid; if (!percent) { mPercentComplete = 0; } else if (percent > 100) { mPercentComplete = 100; } else if (percent < 0) { mPercentComplete = 0; } else { mPercentComplete = percent; } if (isRunning() && mPercentComplete == 100) { m_taskView->stopTimerFor(this); } invalidateCompletedState(); // When parent marked as complete, mark all children as complete as well. // This behavior is consistent with KOrganizer (as of 2003-09-24). if (mPercentComplete == 100) { for (int i = 0; i < childCount(); ++i) { Task *task = dynamic_cast(child(i)); task->setPercentComplete(mPercentComplete); } } // maybe there is a column "percent completed", so do a ... update(); } void Task::setPriority(int priority) { if (priority < 0) { priority = 0; } else if (priority > 9) { priority = 9; } mPriority = priority; update(); } bool Task::isComplete() { return mPercentComplete == 100; } void Task::setDesktopList(const DesktopList& desktopList) { mDesktops = desktopList; } QString Task::addTime(long minutes) { mTime += minutes; this->addTotalTime(minutes); return QString(); } QString Task::addTotalTime(long minutes) { mTotalTime += minutes; if (parentTask()) { parentTask()->addTotalTime(minutes); } return QString(); } QString Task::addSessionTime(long minutes) { mSessionTime += minutes; this->addTotalSessionTime(minutes); return QString(); } QString Task::addTotalSessionTime(long minutes) { mTotalSessionTime += minutes; if (parentTask()) { parentTask()->addTotalSessionTime(minutes); } return QString(); } QString Task::setTime(long minutes) { mTime = minutes; mTotalTime += minutes; return QString(); } QString Task::recalculatetotaltime() { QString result; setTotalTime(0); // FIXME: this code is obviously broken Task* child; for (int i = 0; i < this->childCount(); ++i) { child = (Task*)this->child(i); } addTotalTime(time()); return result; } QString Task::recalculatetotalsessiontime() { QString result; setTotalSessionTime(0); // FIXME: this code is obviously broken Task* child; for (int i = 0; i < this->childCount(); ++i) { child = (Task*)this->child(i); } addTotalSessionTime(time()); return result; } QString Task::setSessionTime(long minutes) { mSessionTime = minutes; mTotalSessionTime += minutes; return QString(); } void Task::changeTimes(long minutesSession, long minutes, TimeTrackerStorage* storage) { qDebug() << "Task's sessionStartTiMe is " << mSessionStartTiMe; if (minutesSession != 0 || minutes != 0) { mSessionTime += minutesSession; mTime += minutes; if (storage) { storage->changeTime(this, minutes * secsPerMinute); } changeTotalTimes(minutesSession, minutes); } } void Task::changeTime(long minutes, TimeTrackerStorage* storage) { changeTimes(minutes, minutes, storage); } void Task::changeTotalTimes(long minutesSession, long minutes) { qCDebug(KTT_LOG) << "Task::changeTotalTimes(" << minutesSession << "," << minutes << ") for" << name(); mTotalSessionTime += minutesSession; mTotalTime += minutes; update(); changeParentTotalTimes(minutesSession, minutes); } void Task::resetTimes() { mTotalSessionTime -= mSessionTime; mTotalTime -= mTime; changeParentTotalTimes(-mSessionTime, -mTime); mSessionTime = 0; mTime = 0; update(); } void Task::changeParentTotalTimes(long minutesSession, long minutes) { if (parentTask()) { parentTask()->changeTotalTimes(minutesSession, minutes); } else { emit totalTimesChanged(minutesSession, minutes); } } bool Task::remove(TimeTrackerStorage* storage) { qCDebug(KTT_LOG) << "entering function" << mName; bool ok = true; - storage->removeTask(this); - if (isRunning()) { - setRunning(false, storage); - } - for (int i = 0; i < childCount(); ++i) { Task* task = static_cast(child(i)); - if (task->isRunning()) { - task->setRunning(false, storage); - } task->remove(storage); } + if (isRunning()) { + setRunning(false, storage); + } + + m_eventsModel->removeAllForTask(this); + changeParentTotalTimes(-mSessionTime, -mTime); + + storage->save(m_taskView); + return ok; } QString Task::fullName() const { if (isRoot()) { return name(); } else { return parentTask()->fullName() + QString::fromLatin1("/") + name(); } } KCalCore::Todo::Ptr Task::asTodo(const KCalCore::Todo::Ptr& todo) const { Q_ASSERT(todo != nullptr); qCDebug(KTT_LOG) <<"Task::asTodo: name() = '" << name() << "'"; + todo->setUid(uid()); todo->setSummary(name()); todo->setDescription(description()); // Note: if the date start is empty, the KOrganizer GUI will have the // checkbox blank, but will prefill the todo's starting datetime to the // time the file is opened. // todo->setDtStart( current ); todo->setCustomProperty(eventAppName, QByteArray("totalTaskTime"), QString::number(mTime)); todo->setCustomProperty(eventAppName, QByteArray("totalSessionTime"), QString::number(mSessionTime)); todo->setCustomProperty(eventAppName, QByteArray("sessionStartTiMe"), mSessionStartTiMe.toString()); qDebug() << "mSessionStartTiMe=" << mSessionStartTiMe.toString(); if (getDesktopStr().isEmpty()) { todo->removeCustomProperty(eventAppName, QByteArray("desktopList")); } else { todo->setCustomProperty(eventAppName, QByteArray("desktopList"), getDesktopStr()); } todo->setPercentComplete(mPercentComplete); todo->setPriority( mPriority ); + + if (parentTask()) { + todo->setRelatedTo(parentTask()->uid()); + } + return todo; } bool Task::parseIncidence( const KCalCore::Incidence::Ptr &incident, long& minutes, long& sessionMinutes, QString& sessionStartTiMe, QString& name, QString& description, DesktopList& desktops, int& percent_complete, int& priority ) { qCDebug(KTT_LOG) << "Entering function"; bool ok; name = incident->summary(); description = incident->description(); mUid = incident->uid(); mComment = incident->description(); ok = false; minutes = getCustomProperty(incident, QStringLiteral("totalTaskTime")).toInt(&ok); if (!ok) { minutes = 0; } ok = false; sessionMinutes = getCustomProperty(incident, QStringLiteral("totalSessionTime")).toInt(&ok); if (!ok) { sessionMinutes = 0; } sessionStartTiMe = getCustomProperty(incident, QStringLiteral("sessionStartTiMe")); QString desktopList = getCustomProperty(incident, QStringLiteral("desktopList")); QStringList desktopStrList = desktopList.split(QStringLiteral(","), QString::SkipEmptyParts); desktops.clear(); for (const QString& desktopStr : desktopStrList) { int desktopInt = desktopStr.toInt(&ok); if (ok) { desktops.push_back(desktopInt); } } percent_complete = incident.staticCast()->percentComplete(); priority = incident->priority(); return true; } QString Task::getDesktopStr() const { if (mDesktops.empty()) { return QString(); } QString desktopsStr; for (const int desktop : mDesktops) { desktopsStr += QString::number(desktop) + QString::fromLatin1(","); } desktopsStr.remove(desktopsStr.length() - 1, 1); return desktopsStr; } // This is needed e.g. to move a task under its parent when loading. void Task::cut() { changeParentTotalTimes(-mTotalSessionTime, -mTotalTime); if (!parentTask()) { m_taskView->tasksModel()->takeTopLevelItem(m_taskView->tasksModel()->indexOfTopLevelItem(this)); } else { parentTask()->takeChild(parentTask()->indexOfChild(this)); } } // This is needed e.g. to move a task under its parent when loading. void Task::paste(TasksModelItem* destination) { destination->insertChild(0, this); changeParentTotalTimes(mTotalSessionTime, mTotalTime); } // This is used e.g. to move each task under its parent after loading. void Task::move(TasksModelItem* destination) { cut(); paste(destination); } QVariant Task::data(int column, int role) const { if (role != Qt::DisplayRole) { return {}; } bool b = KTimeTrackerSettings::decimalFormat(); switch (column) { case 0: return mName; case 1: return formatTime(mSessionTime, b); case 2: return formatTime(mTime, b); case 3: return formatTime(mTotalSessionTime, b); case 4: return formatTime(mTotalTime, b); case 5: return mPriority > 0 ? QString::number(mPriority) : QStringLiteral("--"); case 6: return QString::number(mPercentComplete); default: return {}; } } // Update a row, containing one task void Task::update() { QModelIndex first = taskView()->tasksModel()->index(this, 0); QModelIndex last = taskView()->tasksModel()->index(this, 6); emit taskView()->tasksModel()->dataChanged(first, last, QVector{Qt::DisplayRole}); } void Task::addComment(const QString& comment, TimeTrackerStorage* storage) { mComment = mComment + QString::fromLatin1("\n") + comment; - storage->addComment(this, comment); + + // TODO: Use libkcalcore comments + // todo->addComment(comment); + + storage->save(m_taskView); } void Task::startNewSession() { changeTimes(-mSessionTime, 0); mSessionStartTiMe = QDateTime::currentDateTime(); } /* Overriding the < operator in order to sort the names case insensitive and * the progress percentage [coloumn 6] numerically. */ bool Task::operator<(const TasksModelItem &other) const { const auto& otherTask = dynamic_cast(other); const int column = m_taskView->sortColumn(); if (column == 6) { return mPercentComplete < otherTask.mPercentComplete; } else if (column == 0) { //task name return mName.toLower() < otherTask.mName.toLower(); } else { return data(column, Qt::DisplayRole) < other.data(column, Qt::DisplayRole); } } //BEGIN Properties QString Task::uid() const { return mUid; } QString Task::comment() const { return mComment; } int Task::percentComplete() const { return mPercentComplete; } int Task::priority() const { return mPriority; } QString Task::name() const { return mName; } QString Task::description() const { return mDescription; } QDateTime Task::startTime() const { return mLastStart; } QDateTime Task::sessionStartTiMe() const { return mSessionStartTiMe; } DesktopList Task::desktops() const { return mDesktops; } //END diff --git a/src/model/task.h b/src/model/task.h index 8a3bd86..e40aa48 100644 --- a/src/model/task.h +++ b/src/model/task.h @@ -1,390 +1,391 @@ /* * Copyright (C) 1997 by Stephan Kulow * * This program 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. * * This program 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 this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ #ifndef KTIMETRACKER_TASK_H #define KTIMETRACKER_TASK_H #include #include #include #include #include "desktoplist.h" // Required b/c DesktopList is a typedef not a class. #include "model/tasksmodel.h" #include "model/tasksmodelitem.h" QT_BEGIN_NAMESPACE class QObject; class QPixmap; class QString; QT_END_NAMESPACE class TimeTrackerStorage; class TaskView; +class EventsModel; /** \brief A class representing a task * * A "Task" object stores information about a task such as it's name, * total and session times. * * It can log when the task is started, stoped or deleted. * * If a task is associated with some desktop's activity it can remember that * too. * * It can also contain subtasks - these are managed using the * QListViewItem class. */ class Task : public QObject, public TasksModelItem { Q_OBJECT public: Task(const QString& taskname, const QString& taskdescription, long minutes, long sessionTime, - DesktopList desktops, TaskView* taskView, Task* parentTask); - Task(const KCalCore::Todo::Ptr &incident, TaskView* taskView); + DesktopList desktops, TaskView* taskView, EventsModel *eventsModel, Task* parentTask); + Task(const KCalCore::Todo::Ptr &incident, TaskView* taskView, EventsModel *eventsModel); /* destructor */ ~Task() override; Task* parentTask() const { return dynamic_cast(TasksModelItem::parent()); } /** Return task view for this task */ TaskView* taskView() const { return m_taskView; } /** Return unique iCalendar Todo ID for this task. */ QString uid() const; int depth(); void delete_recursive(); - /** - * Set unique id for the task. - * - * The uid is the key used to update the storage. - * - * @param uid The new unique id. - */ - void setUid(const QString &uid); - /** cut Task out of parent Task or the TaskView */ void cut(); /** cut Task out of parent Task or the TaskView and into the * destination Task */ void move(TasksModelItem* destination); /** insert Task into the destination Task */ void paste(TasksModelItem* destination); //@{ timing related functions /** * Change task time. Adds minutes to both total time and session time by adding an event. * * @param minutes minutes to add to - may be negative * @param storage Pointer to TimeTrackerStorage instance. * If zero, don't save changes. */ void changeTime(long minutes, TimeTrackerStorage* storage); /** * Add minutes to time and session time by adding an event, and write to storage. * * @param minutesSession minutes to add to task session time * @param minutes minutes to add to task time * @param storage Pointer to TimeTrackerStorage instance. * If zero, don't save changes. */ void changeTimes(long minutesSession, long minutes, TimeTrackerStorage* storage = nullptr); /** adds minutes to total and session time by adding an event * * @param minutesSession minutes to add to task total session time * @param minutes minutes to add to task total time */ void changeTotalTimes(long minutesSession, long minutes); /** Adds minutes to the time of the task and the total time of its supertasks. This does not add an event. * * @param minutes minutes to add to the time * @returns A QString with the error message, in case of no error an empty QString. * */ QString addTime(long minutes); /** Adds minutes to the total time of the task and its supertasks. This does not add an event. * * @param minutes minutes to add to the time * @returns A QString with the error message, in case of no error an empty QString. * */ QString addTotalTime(long minutes); /** Adds minutes to the task's session time and its supertasks' total session time. This does not add an event. * * @param minutes minutes to add to the session time * @returns A QString with the error message, in case of no error an empty QString. * */ QString addSessionTime(long minutes); /** Adds minutes to the task's and its supertasks' total session time. This does not add an event. * * @param minutes minutes to add to the session time * @returns A QString with the error message, in case of no error an empty QString. * */ QString addTotalSessionTime(long minutes); /** Sets the time (not session time). This does not add an event. * * @param minutes minutes to set time to * */ QString setTime(long minutes); /** Sets the total time, does not change the parent's total time. This means the parent's total time can run out of sync. */ void setTotalTime(long minutes) { mTotalTime = minutes; } /** Sets the total session time, does not change the parent's total session time. This means the parent's total session time can run out of sync. */ void setTotalSessionTime(long minutes) { mTotalSessionTime = minutes; } /** A recursive function to calculate the total time of a task. */ QString recalculatetotaltime(); /** A recursive function to calculate the total session time of a task. */ QString recalculatetotalsessiontime(); /** Sets the session time. * Set the session time without changing totalTime nor sessionTime. * Do not change the parent's totalTime. * Do not add an event. * See also: changeTimes(long, long) and resetTimes * * @param minutes minutes to set session time to * */ QString setSessionTime(long minutes); /** * Reset all times to 0 and adjust parent task's totalTiMes. */ void resetTimes(); /** @return time in minutes */ long time() const { return mTime; } /** @return total time in minutes */ long totalTime() const { return mTotalTime; } long sessionTime() const { return mSessionTime; } long totalSessionTime() const { return mTotalSessionTime; } QDateTime sessionStartTiMe() const; /** * Return time the task was started. */ QDateTime startTime() const; /** sets session time to zero. */ void startNewSession(); //@} //@{ desktop related functions void setDesktopList(const DesktopList& desktopList); DesktopList desktops() const; QString getDesktopStr() const; //@} //@{ name related functions /** sets the name of the task * @param name a pointer to the name. A deep copy will be made. * @param storage a pointer to a TimeTrackerStorage object. */ void setName(const QString& name, TimeTrackerStorage* storage); /** sets the description of the task */ void setDescription(const QString& description); /** returns the name of this task. * @return a pointer to the name. */ QString name() const; /** returns the description of this task. * @return a pointer to the description. */ QString description() const; /** * Returns that task name, prefixed by parent tree up to root. * * Task names are separated by a forward slash: / */ QString fullName() const; //@} /** Update the display of the task (all columns) in the UI. */ void update(); //@{ the state of a Task - stopped, running /** starts or stops a task * @param on true or false for starting or stopping a task * @param storage a pointer to a TimeTrackerStorage object. * @param when time when the task was started or stopped. Normally QDateTime::currentDateTime, but if calendar has been changed by another program and being reloaded the task is set to running with another start date */ void setRunning( bool on, TimeTrackerStorage* storage, const QDateTime &when = QDateTime::currentDateTime() ); /** Resume the running state of a task. * This is the same as setrunning, but the storage is not modified. */ void resumeRunning(); /** return the state of a task - if it's running or not * @return true or false depending on whether the task is running */ bool isRunning() const; //@} /** * Parses an incidence. This is needed e.g. when you create a task out of a todo. * You read the todo, extract its custom properties (like session time) * and use these data to initialize the task. */ bool parseIncidence( const KCalCore::Incidence::Ptr &, long& minutes, long& sessionMinutes, QString& sessionStartTiMe, QString& name, QString& description, DesktopList& desktops, int& percent_complete, int& priority ); /** * Load the todo passed in with this tasks info. */ KCalCore::Todo::Ptr asTodo(const KCalCore::Todo::Ptr &calendar) const; /** * Add a comment to this task. * A comment is called "description" in the context of KCalCore::ToDo + * + * iCal allows multiple comment tags. So we just add a new comment to the + * todo for this task and write the calendar. + * + * @param task The task that gets the comment + * @param comment The comment */ void addComment(const QString& comment, TimeTrackerStorage* storage); /** Retrieve the entire comment for the task. */ QString comment() const; /** tells you whether this task is the root of the task tree */ bool isRoot() const { return !parentTask(); } /** remove Task with all it's children + * Removes task as well as all event history for this task. + * * @param storage a pointer to a TimeTrackerStorage object. */ bool remove(TimeTrackerStorage* storage); /** * Update percent complete for this task. * * Tasks that are complete (i.e., percent = 100) do not show up in * taskview. If percent NULL, set to zero. If greater than 100, set to * 100. If less than zero, set to zero. */ void setPercentComplete(int percent); int percentComplete() const; /** * Update priority for this task. * * Priority is allowed from 0 to 9. 0 unspecified, 1 highest and 9 lowest. */ void setPriority(int priority); int priority() const; /** Return true if task is complete (percent complete equals 100). */ bool isComplete(); QVariant data(int column, int role) const override; protected: void changeParentTotalTimes(long minutesSession, long minutes); Q_SIGNALS: void totalTimesChanged(long minutesSession, long minutes); /** signal that we're about to delete a task */ void deletingTask(Task* thisTask); private: /** initialize a task */ void init( const QString& taskname, const QString& taskdescription, long minutes, long sessionTime, QString sessionStartTiMe, const DesktopList& desktops, int percent_complete, int priority); bool operator<(const TasksModelItem &other) const override; bool m_isRunning; /** The iCal unique ID of the Todo for this task. */ QString mUid; /** The comment associated with this Task. */ QString mComment; int mPercentComplete; /** task name */ QString mName; /** task description */ QString mDescription; /** Last time this task was started. */ QDateTime mLastStart; /** totals of the whole subtree including self */ long mTotalTime; long mTotalSessionTime; /** times spend on the task itself */ long mTime; long mSessionTime; /** time when the session was started */ QDateTime mSessionStartTiMe; DesktopList mDesktops; /** Priority of the task. */ int mPriority; TaskView* m_taskView; + EventsModel *m_eventsModel; }; #endif // KTIMETRACKER_TASK_H diff --git a/src/model/tasksmodel.cpp b/src/model/tasksmodel.cpp index 1341d22..7d6a735 100644 --- a/src/model/tasksmodel.cpp +++ b/src/model/tasksmodel.cpp @@ -1,371 +1,382 @@ #include "tasksmodel.h" #include #include #include #include #include "task.h" TasksModel::TasksModel() : m_rootItem(new TasksModelItem(this, nullptr)) , m_headerLabels{ i18n("Task Name"), i18n("Session Time"), i18n("Time"), i18n("Total Session Time"), i18n("Total Time"), i18n("Priority"), i18n("Percent Complete")} , m_clockAnimation(nullptr) , m_dragCutTask(nullptr) { Q_INIT_RESOURCE(pics); m_clockAnimation = new QMovie(":/pics/watch.gif", QByteArray(), this); // Prepare animated icon connect(m_clockAnimation, &QMovie::frameChanged, this, &TasksModel::setActiveIcon); // TODO: stop animation when no tasks are active m_clockAnimation->start(); } void TasksModel::clear() { beginResetModel(); // Empty "m_children", move it to "children". QList children; children.swap(m_rootItem->m_children); for (int i = 0; i < children.count(); ++i) { TasksModelItem *item = children.at(i); item->m_parent = nullptr; delete item; } endResetModel(); } int TasksModel::topLevelItemCount() const { return m_rootItem->childCount(); } int TasksModel::indexOfTopLevelItem(TasksModelItem *item) const { return m_rootItem->m_children.indexOf(item); } TasksModelItem *TasksModel::topLevelItem(int index) const { return m_rootItem->child(index); } TasksModelItem *TasksModel::takeTopLevelItem(int index) { return m_rootItem->takeChild(index); } TasksModelItem *TasksModel::item(const QModelIndex &index) const { if (!index.isValid()) { return nullptr; } return static_cast(index.internalPointer()); } QModelIndex TasksModel::index(TasksModelItem *item, int column) const { if (!item || item == m_rootItem) { return {}; } const TasksModelItem *par = item->parent(); auto *itm = const_cast(item); if (!par) { par = m_rootItem; } int row = par->m_children.lastIndexOf(itm); return createIndex(row, column, itm); } QModelIndex TasksModel::index(int row, int column, const QModelIndex &parent) const { int c = columnCount(parent); if (row < 0 || column < 0 || column >= c) { return {}; } TasksModelItem *parentItem = parent.isValid() ? item(parent) : m_rootItem; if (parentItem && row < parentItem->childCount()) { TasksModelItem *itm = parentItem->child(row); if (itm) { return createIndex(row, column, itm); } return {}; } return {}; } QModelIndex TasksModel::parent(const QModelIndex &child) const { if (!child.isValid()) { return {}; } auto *item = static_cast(child.internalPointer()); if (!item || item == m_rootItem) { return {}; } TasksModelItem *parent = item->parent(); return index(parent, 0); } int TasksModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return m_rootItem->childCount(); } TasksModelItem *parentItem = item(parent); if (parentItem) { return parentItem->childCount(); } return 0; } int TasksModel::columnCount(const QModelIndex &parent) const { return m_headerLabels.size(); } QVariant TasksModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return {}; } switch (role) { case Qt::TextAlignmentRole: if (index.column() == 5) { // Align priority left return Qt::AlignCenter; } else if (index.column() >= 1) { // Align HH:MM right return Qt::AlignRight; } break; case Qt::WhatsThisRole: if (index.column() == 0) { return i18n("The task name is what you call the task, it can be chosen freely."); } else if (index.column() == 1) { return i18n("The session time is the time since you last chose \"start new session.\""); } break; case Qt::DecorationRole: { auto *task = dynamic_cast(item(index)); if (index.column() == 0) { return QPixmap(task->isComplete() ? ":/pics/task-complete.xpm" : ":/pics/task-incomplete.xpm"); } else if (index.column() == 1) { return task->isRunning() ? m_clockAnimation->currentPixmap() : QPixmap(":/pics/empty-watch.xpm"); } break; } default: { TasksModelItem *itm = item(index); if (itm) { return itm->data(index.column(), role); } break; } } return {}; } QList TasksModel::getAllItems() { QList res; QStack stack; stack.push(m_rootItem); while (!stack.isEmpty()) { TasksModelItem *item = stack.pop(); if (item != m_rootItem) { res.append(item); } for (int c = 0; c < item->m_children.count(); ++c) stack.push(item->m_children.at(c)); } return res; } QVariant TasksModel::headerData(int section, Qt::Orientation orientation, int role) const { if (section < 0 || orientation != Qt::Horizontal || section >= m_headerLabels.size()) { return {}; } switch (role) { case Qt::DisplayRole: return m_headerLabels[section]; case Qt::WhatsThisRole: switch (section) { case 0: return i18n("The task name is what you call the task, it can be chosen freely."); case 1: return i18n("The session time is the time since you last chose \"start new session.\""); case 3: return i18n("The total session time is the session time of this task and all its subtasks."); case 4: return i18n("The total time is the time of this task and all its subtasks."); default: break; } break; default: break; } return QAbstractItemModel::headerData(section, orientation, role); } void TasksModel::setActiveIcon(int frameNumber) { for (auto *task : getAllItems()) { task->invalidateRunningState(); } } void TasksModel::sort(int column, Qt::SortOrder order) { if (column < 0 || column >= columnCount(QModelIndex())) { return; } m_rootItem->sortChildren(column, order, true); } // static bool TasksModel::itemLessThan(const QPair &left, const QPair &right) { return *(left.first) < *(right.first); } // static bool TasksModel::itemGreaterThan(const QPair &left, const QPair &right) { return *(right.first) < *(left.first); } void TasksModel::sortItems(QList *items, int column, Qt::SortOrder order) { Q_UNUSED(column); // store the original order of indexes QVector> sorting(items->count()); for (int i = 0; i < sorting.count(); ++i) { sorting[i].first = items->at(i); sorting[i].second = i; } // do the sorting typedef bool(*LessThan)(const QPair&,const QPair&); LessThan compare = (order == Qt::AscendingOrder ? &itemLessThan : &itemGreaterThan); std::stable_sort(sorting.begin(), sorting.end(), compare); QModelIndexList fromList; QModelIndexList toList; int colCount = columnCount(QModelIndex()); for (int r = 0; r < sorting.count(); ++r) { int oldRow = sorting.at(r).second; if (oldRow == r) { continue; } TasksModelItem *item = sorting.at(r).first; items->replace(r, item); for (int c = 0; c < colCount; ++c) { QModelIndex from = createIndex(oldRow, c, item); QModelIndex to = createIndex(r, c, item); fromList << from; toList << to; } } changePersistentIndexList(fromList, toList); } void TasksModel::addChild(TasksModelItem *item) { m_rootItem->addChild(item); } Qt::ItemFlags TasksModel::flags(const QModelIndex &index) const { if (!index.isValid()) { // Allow move to top-level return Qt::ItemIsDropEnabled; } return QAbstractItemModel::flags(index) | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; } Qt::DropActions TasksModel::supportedDropActions() const { return Qt::MoveAction; } bool TasksModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const { if (!m_dragCutTask) { return false; } return QAbstractItemModel::canDropMimeData(data, action, row, column, parent); } bool TasksModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { if (!canDropMimeData(data, action, row, column, parent)) { return false; } if (action == Qt::IgnoreAction) { return true; } Task *task = dynamic_cast(m_dragCutTask); if (!task) { return false; } // If have parent, move to subtasks of the parent task. // If don't have parent, move to top-level tasks. TasksModelItem *newParent = parent.isValid() ? item(parent) : m_rootItem; if (!newParent) { return false; } task->move(newParent); return true; } QMimeData *TasksModel::mimeData(const QModelIndexList &indexes) const { m_dragCutTask = item(indexes[0]); return QAbstractItemModel::mimeData(indexes); } + +TasksModelItem *TasksModel::taskByUID(const QString &uid) +{ + for (auto *item : getAllItems()) { + if (dynamic_cast(item)->uid() == uid) { + return item; + } + } + + return nullptr; +} diff --git a/src/model/tasksmodel.h b/src/model/tasksmodel.h index 179133d..6e104ec 100644 --- a/src/model/tasksmodel.h +++ b/src/model/tasksmodel.h @@ -1,73 +1,75 @@ #ifndef KTIMETRACKER_TASKSMODEL_H #define KTIMETRACKER_TASKSMODEL_H #include QT_BEGIN_NAMESPACE class QMovie; QT_END_NAMESPACE class TasksModelItem; class TasksModel : public QAbstractItemModel { friend class TasksModelItem; public: TasksModel(); ~TasksModel() override = default; // Clears the tree widget by removing all of its items and selections. void clear(); int topLevelItemCount() const; int indexOfTopLevelItem(TasksModelItem *item) const; TasksModelItem *topLevelItem(int index) const; TasksModelItem *takeTopLevelItem(int index); QModelIndex index(int row, int column, const QModelIndex &parent) const override; QModelIndex index(TasksModelItem *item, int column) const; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent) const override; int columnCount(const QModelIndex &parent) const override; QVariant data(const QModelIndex &index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; TasksModelItem *item(const QModelIndex &index) const; void sort(int column, Qt::SortOrder order) override; QList getAllItems(); void addChild(TasksModelItem *item); Qt::DropActions supportedDropActions() const override; bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; QMimeData *mimeData(const QModelIndexList &indexes) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; + TasksModelItem *taskByUID(const QString &uid); + public Q_SLOTS: void setActiveIcon(int frameNumber); private: void sortItems(QList *items, int column, Qt::SortOrder order); static bool itemLessThan(const QPair &left, const QPair &right); static bool itemGreaterThan(const QPair &left, const QPair &right); TasksModelItem *m_rootItem; QStringList m_headerLabels; QMovie* m_clockAnimation; mutable TasksModelItem *m_dragCutTask; }; #endif // KTIMETRACKER_TASKSMODEL_H diff --git a/src/plannerparser.cpp b/src/plannerparser.cpp index 51c295b..624972e 100644 --- a/src/plannerparser.cpp +++ b/src/plannerparser.cpp @@ -1,121 +1,120 @@ /* * Copyright (C) 2004 by Thorsten Staerk * * This program 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. * * This program 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 this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ /* this class is here to import tasks from a planner project file to ktimetracker. the import shall not be limited to ktimetracker (kPlaTo sends greetings) it imports planner's top-level-tasks on the same level-depth as currentItem. if there is no currentItem, planner's top-level-tasks will become top-level-tasks in ktimetracker. it imports as well the level-depth of each task, as its name, as its percent-complete. test cases: - deleting all tasks away, then import! - having started with an empty ics, import! - with currentItem being a top-level-task, import! - with currentItem being a subtask, import! */ #include "plannerparser.h" #include "model/task.h" #include "taskview.h" #include "ktt_debug.h" -PlannerParser::PlannerParser(TaskView* tv) +PlannerParser::PlannerParser(TaskView* tv, EventsModel *eventsModel) : m_withinTasks(false) , m_taskView(tv) , m_task(nullptr) , m_level(0) + , m_eventsModel(eventsModel) { // if there is a task one level above currentItem, make it the father of all imported tasks. Set level accordingly. // import as well if there a no task in the taskview as if there are. // if there are, put the top-level tasks of planner on the same level as currentItem. // So you have the chance as well to have the planner tasks at top-level as at a whatever-so-deep sublevel. qDebug() << "entering constructor to import planner tasks"; if (m_taskView->currentItem() && m_taskView->currentItem()->parentTask()) { m_task = m_taskView->currentItem()->parentTask(); m_level = 1; } } bool PlannerParser::startDocument() { m_withinTasks = false; // becomes true as soon as parsing occurres return true; } bool PlannerParser::startElement(const QString&, const QString&, const QString &qName, const QXmlAttributes &att) { qDebug() << "entering function"; QString taskName; int taskComplete=0; // only s within are processed if (qName == QString::fromLatin1("tasks")) { m_withinTasks = true; } if (qName == QString::fromLatin1("task") && m_withinTasks) { // find out name and percent-complete for (int i = 0; i < att.length(); ++i) { if (att.qName(i) == QStringLiteral("name")) { taskName = att.value(i); } if (att.qName(i) == QStringLiteral("percent-complete")) { taskComplete=att.value(i).toInt(); } } // at the moment, task is still the old task or the old father task (if an endElement occurred) or not existing (if the // new task is a top-level-task). Make task the parenttask, if existing. DesktopList dl; if (m_level++ > 0) { Task* parentTask = m_task; - m_task = new Task(taskName, QString(), 0, 0, dl, m_taskView, parentTask); - m_task->setUid(m_taskView->storage()->addTask(m_task, parentTask)); + m_task = new Task(taskName, QString(), 0, 0, dl, m_taskView, m_eventsModel, parentTask); } else { - m_task = new Task(taskName, QString(), 0, 0, dl, m_taskView, nullptr); + m_task = new Task(taskName, QString(), 0, 0, dl, m_taskView, m_eventsModel, nullptr); qDebug() << "added" << taskName; - m_task->setUid(m_taskView->storage()->addTask(m_task, nullptr)); } m_task->setPercentComplete(taskComplete); } return true; } bool PlannerParser::endElement(const QString&, const QString&, const QString& qName) { // only s within increased level, so only decrease for s within if (m_withinTasks) { if (qName == "task" && m_level-- >= 0) { m_task = m_task->parentTask(); } if (qName == "tasks") { m_withinTasks = false; } } return true; } diff --git a/src/plannerparser.h b/src/plannerparser.h index 0d7b8f4..98d2b5f 100644 --- a/src/plannerparser.h +++ b/src/plannerparser.h @@ -1,68 +1,70 @@ /* * Copyright (C) 2004 by Thorsten Staerk * 2007 the ktimetracker developers * * This program 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. * * This program 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 this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ #ifndef PLANNERPARSER_H #define PLANNERPARSER_H #include class Task; class TaskView; +class EventsModel; /** this class is here to import tasks from a planner project file to ktimetracker. the import shall not be limited to ktimetracker (kPlaTo sends greetings) it imports planner's top-level-tasks on the same level-depth as currentItem. if there is no currentItem, planner's top-level-tasks will become top-level-tasks in ktimetracker. it imports as well the level-depth of each task, as its name, as its percent-complete. test cases: - deleting all tasks away, then import! - having started with an empty ics, import! - with currentItem being a top-level-task, import! - with currentItem being a subtask, import! @author Thorsten Staerk */ class PlannerParser : public QXmlDefaultHandler { public: /** Stores the active TaskView in this parser. */ - explicit PlannerParser(TaskView* tv); + explicit PlannerParser(TaskView* tv, EventsModel *eventsModel); /** Called when parsing the xml-document starts. */ bool startDocument() override; /** Called when the reader occurs an open tag (e.g. \ ) */ bool startElement(const QString&, const QString&, const QString& qName, const QXmlAttributes& att) override; /** Called when the reader occurs a closed tag (e.g. \ )*/ bool endElement(const QString&, const QString&, const QString& qName) override; private: bool m_withinTasks; // within ? TaskView* m_taskView; + EventsModel *m_eventsModel; Task* m_task; int m_level; // m_level=1: task is top-level-task }; #endif diff --git a/src/taskview.cpp b/src/taskview.cpp index b62b7a1..dcd2ed1 100644 --- a/src/taskview.cpp +++ b/src/taskview.cpp @@ -1,1283 +1,1285 @@ /* * Copyright (C) 2003 by Scott Monachello * * This program 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. * * This program 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 this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ #include "taskview.h" #include #include #include #include #include #include #include #include #include "model/task.h" #include "model/tasksmodel.h" +#include "model/eventsmodel.h" #include "csvexportdialog.h" #include "desktoptracker.h" #include "edittaskdialog.h" #include "idletimedetector.h" #include "plannerparser.h" #include "ktimetracker.h" #include "export/totalsastext.h" #include "treeviewheadercontextmenu.h" #include "focusdetector.h" #include "ktimetrackerutility.h" #include "ktt_debug.h" bool readBoolEntry(const QString& key) { return KSharedConfig::openConfig()->group(QString()).readEntry(key, true); } void writeEntry(const QString& key, bool value) { KConfigGroup config = KSharedConfig::openConfig()->group(QString()); config.writeEntry(key, value); config.sync(); } void deleteEntry(const QString& key) { KConfigGroup config = KSharedConfig::openConfig()->group(QString()); config.deleteEntry(key); config.sync(); } //BEGIN TaskViewDelegate (custom painting of the progress column) class TaskViewDelegate : public QStyledItemDelegate { public: explicit TaskViewDelegate(QObject *parent) : QStyledItemDelegate(parent) {} void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.column () == 6) { QApplication::style()->drawControl( QStyle::CE_ItemViewItem, &option, painter ); int rX = option.rect.x() + 2; int rY = option.rect.y() + 2; int rWidth = option.rect.width() - 4; int rHeight = option.rect.height() - 4; int value = index.model()->data( index ).toInt(); int newWidth = (int)(rWidth * (value / 100.)); if(QApplication::isLeftToRight()) { int mid = rY + rHeight / 2; int width = rWidth / 2; QLinearGradient gradient1( rX, mid, rX + width, mid); gradient1.setColorAt( 0, Qt::red ); gradient1.setColorAt( 1, Qt::yellow ); painter->fillRect( rX, rY, (newWidth < width) ? newWidth : width, rHeight, gradient1 ); if (newWidth > width) { QLinearGradient gradient2( rX + width, mid, rX + 2 * width, mid); gradient2.setColorAt( 0, Qt::yellow ); gradient2.setColorAt( 1, Qt::green ); painter->fillRect( rX + width, rY, newWidth - width, rHeight, gradient2 ); } painter->setPen( option.state & QStyle::State_Selected ? option.palette.highlight().color() : option.palette.background().color() ); for (int x = rHeight; x < newWidth; x += rHeight) { painter->drawLine( rX + x, rY, rX + x, rY + rHeight - 1 ); } } else { int mid = option.rect.height() - rHeight / 2; int width = rWidth / 2; QLinearGradient gradient1( rX, mid, rX + width, mid); gradient1.setColorAt( 0, Qt::red ); gradient1.setColorAt( 1, Qt::yellow ); painter->fillRect( option.rect.height(), rY, (newWidth < width) ? newWidth : width, rHeight, gradient1 ); if (newWidth > width) { QLinearGradient gradient2( rX + width, mid, rX + 2 * width, mid); gradient2.setColorAt( 0, Qt::yellow ); gradient2.setColorAt( 1, Qt::green ); painter->fillRect( rX + width, rY, newWidth - width, rHeight, gradient2 ); } painter->setPen( option.state & QStyle::State_Selected ? option.palette.highlight().color() : option.palette.background().color() ); for (int x = rWidth- rHeight; x > newWidth; x -= rHeight) { painter->drawLine( rWidth - x, rY, rWidth - x, rY + rHeight - 1 ); } } painter->setPen( Qt::black ); painter->drawText( option.rect, Qt::AlignCenter | Qt::AlignVCenter, QString::number(value) + " %" ); } else { QStyledItemDelegate::paint( painter, option, index ); } } }; //END TaskView::TaskView(QWidget* parent) : QTreeView(parent) - , m_model(new TasksModel()) , m_filterProxyModel(new QSortFilterProxyModel(this)) , m_isLoading(false) , m_storage(new TimeTrackerStorage()) , m_focusTrackingActive(false) , m_lastTaskWithFocus(nullptr) , m_popupPercentageMenu(nullptr) , m_popupPriorityMenu(nullptr) , m_focusDetector(new FocusDetector()) { - m_filterProxyModel->setSourceModel(tasksModel()); m_filterProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_filterProxyModel->setRecursiveFilteringEnabled(true); setModel(m_filterProxyModel); connect(this, &QTreeView::expanded, this, &TaskView::itemStateChanged); connect(this, &QTreeView::collapsed, this, &TaskView::itemStateChanged); connect(m_focusDetector, &FocusDetector::newFocus, this, &TaskView::newFocusWindowDetected); setWindowFlags(windowFlags() | Qt::WindowContextHelpButtonHint); setAllColumnsShowFocus( true ); setSortingEnabled( true ); setAlternatingRowColors( true ); setDragDropMode( QAbstractItemView::InternalMove ); setItemDelegateForColumn( 6, new TaskViewDelegate(this) ); // set up the minuteTimer m_minuteTimer = new QTimer(this); connect( m_minuteTimer, SIGNAL(timeout()), this, SLOT(minuteUpdate())); m_minuteTimer->start(1000 * secsPerMinute); // Set up the idle detection. m_idleTimeDetector = new IdleTimeDetector(KTimeTrackerSettings::period()); connect(m_idleTimeDetector, &IdleTimeDetector::subtractTime, this, &TaskView::subtractTime); connect(m_idleTimeDetector, &IdleTimeDetector::stopAllTimers, this, &TaskView::stopAllTimers); if (!m_idleTimeDetector->isIdleDetectionPossible()) { KTimeTrackerSettings::setEnabled(false); } // Setup auto save timer m_autoSaveTimer = new QTimer(this); connect( m_autoSaveTimer, SIGNAL(timeout()), this, SLOT(save())); // Setup manual save timer (to save changes a little while after they happen) m_manualSaveTimer = new QTimer(this); m_manualSaveTimer->setSingleShot( true ); connect( m_manualSaveTimer, SIGNAL(timeout()), this, SLOT(save())); // Connect desktop tracker events to task starting/stopping m_desktopTracker = new DesktopTracker(); connect( m_desktopTracker, SIGNAL(reachedActiveDesktop(Task*)), this, SLOT(startTimerFor(Task*))); connect( m_desktopTracker, SIGNAL(leftActiveDesktop(Task*)), this, SLOT(stopTimerFor(Task*))); // Header context menu TreeViewHeaderContextMenu *headerContextMenu = new TreeViewHeaderContextMenu(this, this, QVector{0}); connect(headerContextMenu, &TreeViewHeaderContextMenu::columnToggled, this, &TaskView::slotColumnToggled); // Context Menu m_popupPercentageMenu = new QMenu(this); for (int i = 0; i <= 100; i += 10) { QString label = i18n("%1 %" , i); m_percentage[m_popupPercentageMenu->addAction(label)] = i; } connect(m_popupPercentageMenu, &QMenu::triggered, this, &TaskView::slotSetPercentage); m_popupPriorityMenu = new QMenu(this); for (int i = 0; i <= 9; ++i) { QString label; switch (i) { case 0: label = i18n("unspecified"); break; case 1: label = i18nc("combox entry for highest priority", "1 (highest)"); break; case 5: label = i18nc("combox entry for medium priority", "5 (medium)"); break; case 9: label = i18nc("combox entry for lowest priority", "9 (lowest)"); break; default: label = QString("%1").arg(i); break; } m_priority[m_popupPriorityMenu->addAction(label)] = i; } connect(m_popupPriorityMenu, &QMenu::triggered, this, &TaskView::slotSetPriority); setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &TaskView::customContextMenuRequested, this, &TaskView::slotCustomContextMenuRequested); reconfigure(); sortByColumn(0, Qt::AscendingOrder); - for (int i = 0; i <= tasksModel()->columnCount(QModelIndex()); ++i) { - resizeColumnToContents(i); - } } void TaskView::newFocusWindowDetected(const QString &taskName) { QString newTaskName = taskName; newTaskName.remove( '\n' ); if ( m_focusTrackingActive ) { bool found = false; // has taskName been found in our tasks stopTimerFor( m_lastTaskWithFocus ); for (Task *task : getAllTasks()) { if (task->name() == newTaskName) { found = true; startTimerFor(task); m_lastTaskWithFocus = task; } } if (!found) { QString taskuid = addTask(newTaskName); if (taskuid.isNull()) { KMessageBox::error( nullptr, i18n("Error storing new task. Your changes were not saved. " "Make sure you can edit your iCalendar file. " "Also quit all applications using this file and remove " "any lock file related to its name from ~/.kde/share/apps/kabc/lock/ ")); } for (Task *task : getAllTasks()) { if (task->name() == newTaskName) { startTimerFor(task); m_lastTaskWithFocus = task; } } } emit updateButtons(); } // focustrackingactive } void TaskView::mouseMoveEvent( QMouseEvent *event ) { QModelIndex index = indexAt( event->pos() ); if (index.isValid() && index.column() == 6) { int newValue = (int)((event->pos().x() - visualRect(index).x()) / (double)(visualRect(index).width()) * 101); if (newValue > 100) { newValue = 100; } if ( event->modifiers() & Qt::ShiftModifier ) { int delta = newValue % 10; if ( delta >= 5 ) { newValue += (10 - delta); } else { newValue -= delta; } } if (selectionModel()->isSelected(index)) { Task *task = taskAtViewIndex(index); if (task) { task->setPercentComplete(newValue); emit updateButtons(); } } } else { QTreeView::mouseMoveEvent(event); } } void TaskView::mousePressEvent( QMouseEvent *event ) { qCDebug(KTT_LOG) << "Entering function, event->button()=" << event->button(); QModelIndex index = indexAt( event->pos() ); // if the user toggles a task as complete/incomplete if ( index.isValid() && index.column() == 0 && visualRect( index ).x() <= event->pos().x() && event->pos().x() < visualRect( index ).x() + 19) { Task *task = taskAtViewIndex(index); if (task) { if (task->isComplete()) { task->setPercentComplete(0); } else { task->setPercentComplete(100); } emit updateButtons(); } } else // the user did not mark a task as complete/incomplete { if ( KTimeTrackerSettings::configPDA() ) // if you have a touchscreen, you cannot right-click. So, display context menu on any click. { QPoint newPos = viewport()->mapToGlobal( event->pos() ); emit contextMenuRequested( newPos ); } QTreeView::mousePressEvent(event); } } void TaskView::mouseDoubleClickEvent(QMouseEvent *event) { qCDebug(KTT_LOG) << "Entering function, event->button()=" << event->button(); QModelIndex index = indexAt(event->pos()); // if the user toggles a task as complete/incomplete if (index.isValid()) { Task *task = taskAtViewIndex(index); if (task) { if (task->isRunning()) { // if task is running, stop it stopCurrentTimer(); } else if (!task->isComplete()) { // if task is not running, start it stopAllTimers(); startCurrentTimer(); } } } else { QTreeView::mousePressEvent(event); } } TimeTrackerStorage* TaskView::storage() { return m_storage; } TaskView::~TaskView() { delete m_storage; KTimeTrackerSettings::self()->save(); } Task* TaskView::taskAtViewIndex(QModelIndex viewIndex) { + if (!m_storage->isLoaded()) { + return nullptr; + } + QModelIndex index = m_filterProxyModel->mapToSource(viewIndex); return dynamic_cast(tasksModel()->item(index)); } Task* TaskView::currentItem() { return taskAtViewIndex(QTreeView::currentIndex()); } void TaskView::load(const QUrl &url) { assert( !( url.isEmpty() ) ); // if the program is used as an embedded plugin for konqueror, there may be a need // to load from a file without touching the preferences. qCDebug(KTT_LOG) << "Entering function"; m_isLoading = true; QString err = m_storage->load(this, url); + // Connect to the new model created by TimeTrackerStorage::load() + m_filterProxyModel->setSourceModel(tasksModel()); + for (int i = 0; i <= tasksModel()->columnCount(QModelIndex()); ++i) { + resizeColumnToContents(i); + } if (!err.isEmpty()) { KMessageBox::error(this, err); m_isLoading = false; qCDebug(KTT_LOG) << "Leaving TaskView::load"; return; } // Register tasks with desktop tracker for (Task *task : getAllTasks()) { m_desktopTracker->registerForDesktops(task, task->desktops()); } // Start all tasks that have an event without endtime for (Task *task : getAllTasks()) { if (!m_storage->allEventsHaveEndTiMe(task)) { task->resumeRunning(); m_activeTasks.append(task); emit updateButtons(); if (m_activeTasks.count() == 1) { emit timersActive(); } emit tasksChanged(m_activeTasks); } } if (tasksModel()->topLevelItemCount() > 0) { restoreItemState(); setCurrentIndex(m_filterProxyModel->mapFromSource( tasksModel()->index(tasksModel()->topLevelItem(0), 0))); if (!m_desktopTracker->startTracking().isEmpty()) { KMessageBox::error(nullptr, i18n("Your virtual desktop number is too high, desktop tracking will not work")); } m_isLoading = false; refresh(); } for (int i = 0; i <= tasksModel()->columnCount(QModelIndex()); ++i) { resizeColumnToContents(i); } qCDebug(KTT_LOG) << "Leaving function"; } void TaskView::restoreItemState() /* Restores the item state of every item. An item is a task in the list. Its state is whether it is expanded or not. If a task shall be expanded is stored in the _preferences object. */ { qCDebug(KTT_LOG) << "Entering function"; if (tasksModel()->topLevelItemCount() > 0) { for (Task *task : getAllTasks()) { setExpanded(m_filterProxyModel->mapFromSource( tasksModel()->index(task, 0)), readBoolEntry(task->uid())); } } qCDebug(KTT_LOG) << "Leaving function"; } void TaskView::itemStateChanged(const QModelIndex &index) { Task *task = taskAtViewIndex(index); if (!task || m_isLoading) { return; } qCDebug(KTT_LOG) <<"TaskView::itemStateChanged()" <<" uid=" << task->uid() <<" state=" << isExpanded(index); writeEntry(task->uid(), isExpanded(index)); } void TaskView::closeStorage() { m_storage->closeStorage(); } bool TaskView::allEventsHaveEndTiMe() { return m_storage->allEventsHaveEndTiMe(); } void TaskView::refresh() { qCDebug(KTT_LOG) << "entering function"; for (Task *task : getAllTasks()) { task->invalidateCompletedState(); task->update(); // maybe there was a change in the times's format } // remove root decoration if there is no more child. // int i = 0; // while (itemAt(++i) && itemAt(i)->depth() == 0){}; //setRootIsDecorated( itemAt( i ) && ( itemAt( i )->depth() != 0 ) ); // FIXME workaround? seems that the QItemDelegate for the procent column only // works properly if rootIsDecorated == true. setRootIsDecorated(true); emit updateButtons(); qCDebug(KTT_LOG) << "exiting TaskView::refresh()"; } QString TaskView::reFreshTimes() /** Refresh the times of the tasks, e.g. when the history has been changed by the user */ { qCDebug(KTT_LOG) << "Entering function"; QString err; // re-calculate the time for every task based on events in the history - KCalCore::Event::List eventList = storage()->rawevents(); // get all events (!= tasks) resetDisplayTimeForAllTasks(); emit reSetTimes(); for (Task *task : getAllTasks()) { - KCalCore::Event::List eventsForTask; - for (KCalCore::Event::List::iterator i = eventList.begin(); i != eventList.end(); ++i) { - if ((*i)->relatedTo() == task->uid()) { - // if event i belongs to task - eventsForTask.append(*i); - } - } - - for (auto event : eventsForTask) { + // get all events for task + for (const auto *event : storage()->eventsModel()->eventsForTask(task)) { QDateTime kdatetimestart = event->dtStart(); QDateTime kdatetimeend = event->dtEnd(); QDateTime eventstart = QDateTime::fromString(kdatetimestart.toString().remove("Z")); QDateTime eventend = QDateTime::fromString(kdatetimeend.toString().remove("Z")); int duration = eventstart.secsTo(eventend) / 60; task->addTime(duration); emit totalTimesChanged(0, duration); qCDebug(KTT_LOG) << "duration is" << duration; if (task->sessionStartTiMe().isValid()) { // if there is a session if (task->sessionStartTiMe().secsTo(eventstart) > 0 && task->sessionStartTiMe().secsTo(eventend) > 0) { // if the event is after the session start int sessionTime = eventstart.secsTo(eventend) / 60; task->setSessionTime(task->sessionTime() + sessionTime); } } else { // so there is no session at all task->addSessionTime(duration); emit totalTimesChanged(duration, 0); } } } for (Task *task : getAllTasks()) { task->recalculatetotaltime(); task->recalculatetotalsessiontime(); } refresh(); qCDebug(KTT_LOG) << "Leaving TaskView::reFreshTimes()"; return err; } void TaskView::importPlanner(const QString& fileName) { qCDebug(KTT_LOG) << "entering importPlanner"; - PlannerParser *handler = new PlannerParser(this); + PlannerParser *handler = new PlannerParser(this, storage()->eventsModel()); QString lFileName = fileName; if (lFileName.isEmpty()) { lFileName = QFileDialog::getOpenFileName(); } QFile xmlFile(lFileName); QXmlInputSource source(&xmlFile); QXmlSimpleReader reader; reader.setContentHandler(handler); reader.parse(source); refresh(); } QString TaskView::report(const ReportCriteria& rc) { QString err; if (rc.reportType == ReportCriteria::CSVHistoryExport) { err = m_storage->exportCSVHistory(this, rc.from, rc.to, rc); } else { // rc.reportType == ReportCriteria::CSVTotalsExport if (!rc.bExPortToClipBoard) { err = exportcsvFile(rc); } else { err = clipTotals(rc); } } return err; } void TaskView::exportCSVFileDialog() { qCDebug(KTT_LOG) << "TaskView::exportCSVFileDialog()"; CSVExportDialog dialog(ReportCriteria::CSVTotalsExport, this); if (currentItem() && currentItem()->isRoot()) { dialog.enableTasksToExportQuestion(); } if (dialog.exec()) { QString err = report(dialog.reportCriteria()); if (!err.isEmpty()) { KMessageBox::error(this, i18n(err.toLatin1())); } } } QString TaskView::exportCSVHistoryDialog() { qCDebug(KTT_LOG) << "TaskView::exportCSVHistoryDialog()"; QString err; CSVExportDialog dialog(ReportCriteria::CSVHistoryExport, this); if (currentItem() && currentItem()->isRoot()) { dialog.enableTasksToExportQuestion(); } if (dialog.exec()) { err = report(dialog.reportCriteria()); } return err; } long TaskView::count() { long n = 0; for (Task *task : getAllTasks()) { ++n; } return n; } QStringList TaskView::tasks() { QStringList result; for (Task *task : getAllTasks()) { result << task->name(); } return result; } Task* TaskView::task(const QString& taskId) { Task* result = nullptr; for (Task *task : getAllTasks()) { if (task->uid() == taskId) { result = task; } } return result; } void TaskView::dropEvent (QDropEvent* event) { QTreeView::dropEvent(event); reFreshTimes(); } void TaskView::scheduleSave() { m_manualSaveTimer->start(10); } void TaskView::save() { qCDebug(KTT_LOG) << "Entering TaskView::save()"; QString err = m_storage->save(this); if (!err.isNull()) { QString errMsg = m_storage->fileUrl().toString() + ":\n"; if (err == QString("Could not save. Could not lock file.")) { errMsg += i18n("Could not save. Disk full?"); } else { errMsg += i18n("Could not save."); } KMessageBox::error(this, errMsg); } } void TaskView::startCurrentTimer() { startTimerFor( currentItem() ); } void TaskView::startTimerFor( Task* task, const QDateTime &startTime ) { qCDebug(KTT_LOG) << "Entering function"; if (task != 0 && m_activeTasks.indexOf(task) == -1) { if (!task->isComplete()) { if ( KTimeTrackerSettings::uniTasking() ) stopAllTimers(); m_idleTimeDetector->startIdleDetection(); task->setRunning(true, m_storage, startTime); m_activeTasks.append(task); emit updateButtons(); if ( m_activeTasks.count() == 1 ) emit timersActive(); emit tasksChanged( m_activeTasks ); } } } void TaskView::clearActiveTasks() { m_activeTasks.clear(); } void TaskView::stopAllTimers(const QDateTime& when) { qCDebug(KTT_LOG) << "Entering function"; QProgressDialog dialog(i18n("Stopping timers..."), i18n("Cancel"), 0, m_activeTasks.count(), this); if (m_activeTasks.count() > 1) { dialog.show(); } for (Task *task : m_activeTasks) { QApplication::processEvents(); task->setRunning(false, m_storage, when); dialog.setValue(dialog.value() + 1); } m_idleTimeDetector->stopIdleDetection(); m_activeTasks.clear(); emit updateButtons(); emit timersInactive(); emit tasksChanged(m_activeTasks); } void TaskView::toggleFocusTracking() { m_focusTrackingActive = !m_focusTrackingActive; if (m_focusTrackingActive) { // FIXME: should get the currently active window and start tracking it? } else { stopTimerFor(m_lastTaskWithFocus); } emit updateButtons(); } void TaskView::startNewSession() /* This procedure starts a new session. We speak of session times, overalltimes (comprising all sessions) and total times (comprising all subtasks). That is why there is also a total session time. */ { qCDebug(KTT_LOG) <<"Entering TaskView::startNewSession"; for (Task *task : getAllTasks()) { task->startNewSession(); } qCDebug(KTT_LOG) << "Leaving TaskView::startNewSession"; } void TaskView::resetTimeForAllTasks() /* This procedure resets all times (session and overall) for all tasks and subtasks. */ { qCDebug(KTT_LOG) << "Entering function"; for (Task *task : getAllTasks()) { task->resetTimes(); } storage()->deleteAllEvents(); qCDebug(KTT_LOG) << "Leaving function"; } void TaskView::resetDisplayTimeForAllTasks() /* This procedure resets all times (session and overall) for all tasks and subtasks. */ { qCDebug(KTT_LOG) << "Entering function"; for (Task *task : getAllTasks()) { task->resetTimes(); } qCDebug(KTT_LOG) << "Leaving function"; } void TaskView::stopTimerFor(Task* task) { qCDebug(KTT_LOG) << "Entering function"; if ( task != 0 && m_activeTasks.indexOf(task) != -1 ) { m_activeTasks.removeAll(task); task->setRunning(false, m_storage); if ( m_activeTasks.count() == 0 ) { m_idleTimeDetector->stopIdleDetection(); emit timersInactive(); } emit updateButtons(); } emit tasksChanged( m_activeTasks ); } void TaskView::stopCurrentTimer() { stopTimerFor( currentItem() ); if ( m_focusTrackingActive && m_lastTaskWithFocus == currentItem() ) { toggleFocusTracking(); } } void TaskView::minuteUpdate() { addTimeToActiveTasks(1, false); } void TaskView::addTimeToActiveTasks(int minutes, bool save_data) { for (Task *task : m_activeTasks) { task->changeTime(minutes, save_data ? m_storage : 0); } } void TaskView::newTask() { newTask(i18n("New Task"), nullptr); } void TaskView::newTask(const QString& caption, Task* parent) { auto *dialog = new EditTaskDialog(this, caption, nullptr); long total, totalDiff, session, sessionDiff; DesktopList desktopList; int result = dialog->exec(); if (result == QDialog::Accepted) { QString taskName = i18n("Unnamed Task"); if (!dialog->taskName().isEmpty()) { taskName = dialog->taskName(); } QString taskDescription = dialog->taskDescription(); total = totalDiff = session = sessionDiff = 0; dialog->status(&desktopList); // If all available desktops are checked, disable auto tracking, // since it makes no sense to track for every desktop. if (desktopList.size() == m_desktopTracker->desktopCount()) { desktopList.clear(); } QString uid = addTask(taskName, taskDescription, total, session, desktopList, parent); if (uid.isNull()) { KMessageBox::error(nullptr, i18n( "Error storing new task. Your changes were not saved. " "Make sure you can edit your iCalendar file. Also quit " "all applications using this file and remove any lock " "file related to its name from ~/.kde/share/apps/kabc/lock/")); } } emit updateButtons(); } QString TaskView::addTask( const QString& taskname, const QString& taskdescription, long total, long session, const DesktopList& desktops, Task* parent) { qCDebug(KTT_LOG) << "Entering function; taskname =" << taskname; setSortingEnabled(false); - Task* task = new Task(taskname, taskdescription, total, session, desktops, this, parent); - - task->setUid(m_storage->addTask(task, parent)); - QString taskuid = task->uid(); - if (!taskuid.isNull()) { - m_desktopTracker->registerForDesktops(task, desktops); - setCurrentIndex(m_filterProxyModel->mapFromSource(tasksModel()->index(task, 0))); - task->invalidateCompletedState(); - save(); - } else { - delete task; + Task *task = new Task( + taskname, taskdescription, total, session, desktops, this, storage()->eventsModel(), parent); + if (task->uid().isNull()) { + qFatal("failed to generate UID"); } + m_desktopTracker->registerForDesktops(task, desktops); + setCurrentIndex(m_filterProxyModel->mapFromSource(tasksModel()->index(task, 0))); + task->invalidateCompletedState(); + save(); + setSortingEnabled(true); - return taskuid; + return task->uid(); } void TaskView::newSubTask() { Task* task = currentItem(); if (!task) { return; } newTask(i18n("New Sub Task"), task); setExpanded(m_filterProxyModel->mapFromSource(tasksModel()->index(task, 0)), true); refresh(); } void TaskView::editTask() { qCDebug(KTT_LOG) <<"Entering editTask"; Task* task = currentItem(); if (!task) { return; } DesktopList desktopList = task->desktops(); DesktopList oldDeskTopList = desktopList; EditTaskDialog* dialog = new EditTaskDialog(this, i18n("Edit Task"), &desktopList); dialog->setTask(task->name()); dialog->setDescription(task->description()); int result = dialog->exec(); if (result == QDialog::Accepted) { QString taskName = i18n("Unnamed Task"); if (!dialog->taskName().isEmpty()) { taskName = dialog->taskName(); } // setName only does something if the new name is different task->setName(taskName, m_storage); task->setDescription(dialog->taskDescription()); // update session time as well if the time was changed if (!dialog->timeChange().isEmpty()) { task->changeTime(dialog->timeChange().toInt(), m_storage); } dialog->status(&desktopList); // If all available desktops are checked, disable auto tracking, // since it makes no sense to track for every desktop. if (desktopList.size() == m_desktopTracker->desktopCount()) { desktopList.clear(); } // only do something for autotracking if the new setting is different if (oldDeskTopList != desktopList) { task->setDesktopList(desktopList); m_desktopTracker->registerForDesktops(task, desktopList); } emit updateButtons(); } } void TaskView::setPerCentComplete(int completion) { Task* task = currentItem(); if (!task) { KMessageBox::information(nullptr, i18n("No task selected.")); return; } if (completion < 0) { completion = 0; } if (completion < 100) { task->setPercentComplete(completion); task->invalidateCompletedState(); save(); emit updateButtons(); } } void TaskView::deleteTaskBatch(Task* task) { QString uid=task->uid(); task->remove(m_storage); deleteEntry(uid); // forget if the item was expanded or collapsed save(); // Stop idle detection if no more counters are running if (m_activeTasks.count() == 0) { m_idleTimeDetector->stopIdleDetection(); emit timersInactive(); } task->delete_recursive(); emit tasksChanged(m_activeTasks); } void TaskView::deleteTask(Task* task) /* Attention when popping up a window asking for confirmation. If you have "Track active applications" on, this window will create a new task and make this task running and selected. */ { if (!task) { task = currentItem(); } if (!currentItem()) { KMessageBox::information(nullptr, i18n("No task selected.")); } else { int response = KMessageBox::Continue; if (KTimeTrackerSettings::promptDelete()) { response = KMessageBox::warningContinueCancel( 0, i18n( "Are you sure you want to delete the selected" " task and its entire history?\n" "NOTE: all subtasks and their history will also " "be deleted."), i18n( "Deleting Task"), KStandardGuiItem::del()); } if (response == KMessageBox::Continue) { deleteTaskBatch(task); } } } void TaskView::markTaskAsComplete() { if (!currentItem()) { KMessageBox::information(nullptr, i18n("No task selected.")); return; } currentItem()->setPercentComplete(100); currentItem()->invalidateCompletedState(); save(); emit updateButtons(); } void TaskView::subtractTime(int minutes) { addTimeToActiveTasks(-minutes, false); // subtract time in memory, but do not store it } void TaskView::deletingTask(Task* deletedTask) { qCDebug(KTT_LOG) << "Entering function"; DesktopList desktopList; m_desktopTracker->registerForDesktops(deletedTask, desktopList); m_activeTasks.removeAll(deletedTask); emit tasksChanged(m_activeTasks); } void TaskView::markTaskAsIncomplete() { setPerCentComplete(50); // if it has been reopened, assume half-done } QString TaskView::clipTotals( const ReportCriteria &rc ) // This function stores the user's tasks into the clipboard. // rc tells how the user wants his report, e.g. all times or session times { QApplication::clipboard()->setText(totalsAsText(tasksModel(), currentItem(), rc)); return QString(); } void TaskView::slotColumnToggled( int column ) { switch (column) { case 1: KTimeTrackerSettings::setDisplaySessionTime( !isColumnHidden( 1 ) ); break; case 2: KTimeTrackerSettings::setDisplayTime( !isColumnHidden( 2 ) ); break; case 3: KTimeTrackerSettings::setDisplayTotalSessionTime( !isColumnHidden( 3 ) ); break; case 4: KTimeTrackerSettings::setDisplayTotalTime( !isColumnHidden( 4 ) ); break; case 5: KTimeTrackerSettings::setDisplayPriority( !isColumnHidden( 5 ) ); break; case 6: KTimeTrackerSettings::setDisplayPercentComplete( !isColumnHidden( 6 ) ); break; } KTimeTrackerSettings::self()->save(); } void TaskView::slotCustomContextMenuRequested(const QPoint& pos) { QPoint newPos = viewport()->mapToGlobal(pos); int column = columnAt(pos.x()); switch (column) { case 6: /* percentage */ m_popupPercentageMenu->popup(newPos); break; case 5: /* priority */ m_popupPriorityMenu->popup(newPos); break; default: emit contextMenuRequested(newPos); break; } } void TaskView::slotSetPercentage(QAction* action) { if (currentItem()) { currentItem()->setPercentComplete(m_percentage[action]); emit updateButtons(); } } void TaskView::slotSetPriority(QAction* action) { if (currentItem()) { currentItem()->setPriority(m_priority[action]); } } bool TaskView::isFocusTrackingActive() const { return m_focusTrackingActive; } QList TaskView::activeTasks() const { return m_activeTasks; } void TaskView::reconfigure() { /* Adapt columns */ setColumnHidden(1, !KTimeTrackerSettings::displaySessionTime()); setColumnHidden(2, !KTimeTrackerSettings::displayTime()); setColumnHidden(3, !KTimeTrackerSettings::displayTotalSessionTime()); setColumnHidden(4, !KTimeTrackerSettings::displayTotalTime()); setColumnHidden(5, !KTimeTrackerSettings::displayPriority()); setColumnHidden(6, !KTimeTrackerSettings::displayPercentComplete()); /* idleness */ m_idleTimeDetector->setMaxIdle(KTimeTrackerSettings::period()); m_idleTimeDetector->toggleOverAllIdleDetection(KTimeTrackerSettings::enabled()); /* auto save */ if (KTimeTrackerSettings::autoSave()) { m_autoSaveTimer->start(KTimeTrackerSettings::autoSavePeriod() * 1000 * secsPerMinute); } else if (m_autoSaveTimer->isActive()) { m_autoSaveTimer->stop(); } refresh(); } QList TaskView::getAllTasks() { QList tasks; - for (TasksModelItem *item : tasksModel()->getAllItems()) { - Task *task = dynamic_cast(item); - if (task) { - tasks.append(task); - } else { - qFatal("dynamic_cast to Task failed"); + if (m_storage->isLoaded()) { + for (TasksModelItem *item : tasksModel()->getAllItems()) { + Task *task = dynamic_cast(item); + if (task) { + tasks.append(task); + } else { + qFatal("dynamic_cast to Task failed"); + } } } return tasks; } int TaskView::sortColumn() const { return header()->sortIndicatorSection(); } void TaskView::setFilterText(const QString &text) { m_filterProxyModel->setFilterFixedString(text); } //---------------------------------------------------------------------------- // Routines that handle Comma-Separated Values export file format. // QString TaskView::exportcsvFile(const ReportCriteria &rc) { QString delim = rc.delimiter; QString dquote = rc.quote; QString double_dquote = dquote + dquote; QString err; QProgressDialog dialog( i18n("Exporting to CSV..."), i18n("Cancel"), 0, static_cast(2 * count()), this, nullptr); dialog.setAutoClose(true); dialog.setWindowTitle(i18nc("@title:window", "Export Progress")); if (count() > 1) { dialog.show(); } QString retval; // Find max task depth int maxdepth = 0; for (Task *task : getAllTasks()) { if (dialog.wasCanceled()) { break; } dialog.setValue(dialog.value() + 1); // if (tasknr % 15 == 0) { // QApplication::processEvents(); // repainting is slow // } QApplication::processEvents(); if (task->depth() > maxdepth) { maxdepth = task->depth(); } } // Export to file for (Task *task : getAllTasks()) { if (dialog.wasCanceled()) { break; } dialog.setValue(dialog.value() + 1); // if (tasknr % 15 == 0) { // QApplication::processEvents(); // repainting is slow // } QApplication::processEvents(); // indent the task in the csv-file: for (int i = 0; i < task->depth(); ++i) { retval += delim; } /* // CSV compliance // Surround the field with quotes if the field contains // a comma (delim) or a double quote if (task->name().contains(delim) || task->name().contains(dquote)) to_quote = true; else to_quote = false; */ bool to_quote = true; if (to_quote) { retval += dquote; } // Double quotes replaced by a pair of consecutive double quotes retval += task->name().replace(dquote, double_dquote); if (to_quote) { retval += dquote; } // maybe other tasks are more indented, so to align the columns: for (int i = 0; i < maxdepth - task->depth(); ++i) { retval += delim; } retval += delim + formatTime(task->sessionTime(), rc.decimalMinutes) + delim + formatTime(task->time(), rc.decimalMinutes) + delim + formatTime(task->totalSessionTime(), rc.decimalMinutes) + delim + formatTime(task->totalTime(), rc.decimalMinutes) + '\n'; } // save, either locally or remote if (rc.url.isLocalFile()) { QString filename = rc.url.toLocalFile(); if (filename.isEmpty()) { filename = rc.url.url(); } QFile f(filename); if (!f.open(QIODevice::WriteOnly)) { err = i18n("Could not open \"%1\".", filename); } if (err.length() == 0) { QTextStream stream(&f); // Export to file stream << retval; f.close(); } } else { // use remote file auto* const job = KIO::storedPut(retval.toUtf8(), rc.url, -1); KJobWidgets::setWindow(job, &dialog); if (!job->exec()) { err=QString::fromLatin1("Could not upload"); } } return err; } + +inline TasksModel *TaskView::tasksModel() +{ + return m_storage->tasksModel(); +} diff --git a/src/taskview.h b/src/taskview.h index 29b4f29..ccf062f 100644 --- a/src/taskview.h +++ b/src/taskview.h @@ -1,297 +1,296 @@ /* * Copyright (C) 2003 by Scott Monachello * 2007 the ktimetracker developers * * This program 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. * * This program 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 this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ #ifndef KTIMETRACKER_TASK_VIEW #define KTIMETRACKER_TASK_VIEW #include #include #include "desktoplist.h" #include "timetrackerstorage.h" #include "reportcriteria.h" QT_BEGIN_NAMESPACE class QMouseEvent; class QString; class QTimer; class QSortFilterProxyModel; QT_END_NAMESPACE class DesktopTracker; class IdleTimeDetector; class Task; class TimeTrackerStorage; class FocusDetector; class TasksModel; /** * Container and interface for the tasks. */ class TaskView : public QTreeView { Q_OBJECT public: explicit TaskView(QWidget* parent = nullptr); ~TaskView() override; Task* taskAtViewIndex(QModelIndex viewIndex); /** Return the current item in the view, cast to a Task pointer. */ Task* currentItem(); //BEGIN model specified /** Load the view from storage. */ void load(const QUrl& url); /** Close the storage and release lock. */ void closeStorage(); /** Reset session time to zero for all tasks. */ void startNewSession(); /** Deliver if all events from the storage have and end time. */ bool allEventsHaveEndTiMe(); /** Reset session and total time to zero for all tasks. */ void resetTimeForAllTasks(); /** Reset session and total time for all tasks - do not touch the storage. */ void resetDisplayTimeForAllTasks(); /** Return the total number of items in the view. */ long count(); /** Schedule that we should save very soon */ void scheduleSave(); /** return all task names, e.g. for batch processing */ QStringList tasks(); /** return the task with the given UID */ Task* task(const QString& uid); /** Add a task to view and storage. */ QString addTask( const QString& taskame, const QString& taskdescription = QString(), long total = 0, long session = 0, const DesktopList& desktops = QVector(0,0), Task* parent = nullptr); /** Returns a list of the current active tasks. */ QList< Task* > activeTasks() const; //END //BEGIN behavior /** Returns whether the focus tracking is currently active. */ bool isFocusTrackingActive() const; //END - inline TasksModel *tasksModel() { return m_model; } + TasksModel *tasksModel(); int sortColumn() const; public Q_SLOTS: /** Save to persistent storage. */ void save(); /** Start the timer on the current item (task) in view. */ void startCurrentTimer(); /** Stop the timer for the current item in the view. */ void stopCurrentTimer(); /** Stop all running timers. * @param when When the timer stopped - this makes sense if the idletime- * detector detects the user stopped working 5 minutes ago. */ void stopAllTimers(const QDateTime& when = QDateTime::currentDateTime()); /** Toggles the automatic tracking of focused windows */ void toggleFocusTracking(); /** Calls newTask dialog with caption "New Task". */ void newTask(); /** Display edit task dialog and create a new task with results. * @param caption Window title of the edit task dialog */ void newTask(const QString& caption, Task* parent); /** Used to refresh (e.g. after import) */ void refresh(); /** used to import tasks from imendio planner */ void importPlanner(const QString& fileName = ""); /** * Call export function for csv totals or history. * Output a report based on contents of ReportCriteria. */ QString report(const ReportCriteria& rc); /** * Writes all tasks and their totals to a Comma-Separated Values file. * * The format of this file is zero or more lines of: * taskName,subtaskName,..,sessionTime,time,totalSessionTime,totalTime * the number of subtasks is determined at runtime. */ QString exportcsvFile(const ReportCriteria &rc); /** Export comma separated values format for task time totals. */ void exportCSVFileDialog(); /** Export comma-separated values format for task history. */ QString exportCSVHistoryDialog(); /** Calls newTask dialog with caption "New Sub Task". */ void newSubTask(); /** Calls editTask dialog for the current task. */ void editTask(); /** * Returns a pointer to storage object. * * This is poor object oriented design--the task view should * expose wrappers around the storage methods we want to access instead of * giving clients full access to objects that we own. * * Hopefully, this will be redesigned as part of the Qt4 migration. */ TimeTrackerStorage* storage(); /** * Deletes the given or the current task (and children) from the view. * It does this in batch mode, no user dialog. * @param task Task to be deleted. If empty, the current task is deleted. * if non-existent, an error message is displayed. */ void deleteTaskBatch(Task* task = nullptr); /** * Deletes the given or the current task (and children) from the view. * Depending on configuration, there may be a user dialog. * @param task Task to be deleted. If empty, the current task is deleted. * if non-existent, an error message is displayed. */ void deleteTask(Task* task = nullptr); /** Sets % completed for the current task. * @param completion The percentage complete to mark the task as. */ void setPerCentComplete(int completion); void markTaskAsComplete(); void markTaskAsIncomplete(); /** Subtracts time from all active tasks, and does not log event. */ void subtractTime(int minutes); void taskTotalTimesChanged(long session, long total) { emit totalTimesChanged( session, total); } /** receiving signal that a task is being deleted */ void deletingTask(Task* deletedTask); /** starts timer for task. * @param task task to start timer of * @param startTime if taskview has been modified by another program, we have to set the starting time to not-now. */ void startTimerFor(Task* task, const QDateTime& startTime = QDateTime::currentDateTime()); void stopTimerFor(Task* task); /** clears all active tasks. Needed e.g. if iCal file was modified by another program and taskview is cleared without stopping tasks IF YOU DO NOT KNOW WHAT YOU ARE DOING, CALL stopAllTimers INSTEAD */ void clearActiveTasks(); /** Copy totals for current and all sub tasks to clipboard. */ QString clipTotals(const ReportCriteria& rc); /** Reconfigures taskView depending on current configuration. */ void reconfigure(); /** Refresh the times of the tasks, e.g. when the history has been changed by the user */ QString reFreshTimes(); QList getAllTasks(); void setFilterText(const QString &text); Q_SIGNALS: void totalTimesChanged(long session, long total); void reSetTimes(); void updateButtons(); void timersActive(); void timersInactive(); void tasksChanged(QList activeTasks); void setStatusBarText(QString); void contextMenuRequested(const QPoint&); private: // member variables - TasksModel* m_model; QSortFilterProxyModel* m_filterProxyModel; IdleTimeDetector* m_idleTimeDetector; QTimer *m_minuteTimer; QTimer *m_autoSaveTimer; QTimer *m_manualSaveTimer; DesktopTracker* m_desktopTracker; bool m_isLoading; TimeTrackerStorage *m_storage; bool m_focusTrackingActive; Task* m_lastTaskWithFocus; QList m_activeTasks; QMenu *m_popupPercentageMenu; QMap m_percentage; QMenu *m_popupPriorityMenu; QMap m_priority; FocusDetector *m_focusDetector; private: void addTimeToActiveTasks(int minutes, bool save_data = true); /** item state stores if a task is expanded so you can see the subtasks */ void restoreItemState(); protected: void mouseMoveEvent(QMouseEvent*) override; void mousePressEvent(QMouseEvent*) override; void mouseDoubleClickEvent(QMouseEvent*) override; public Q_SLOTS: void minuteUpdate(); void dropEvent(QDropEvent* event) override; /** item state stores if a task is expanded so you can see the subtasks */ void itemStateChanged(const QModelIndex &index); /** React on the focus having changed to Window QString **/ void newFocusWindowDetected(const QString&); void slotColumnToggled(int); void slotCustomContextMenuRequested(const QPoint&); void slotSetPercentage(QAction*); void slotSetPriority(QAction*); }; #endif // KTIMETRACKER_TASK_VIEW diff --git a/src/tests/exportcsvtest.cpp b/src/tests/exportcsvtest.cpp index bd27ee2..3a3c091 100644 --- a/src/tests/exportcsvtest.cpp +++ b/src/tests/exportcsvtest.cpp @@ -1,119 +1,119 @@ #include #include #include "taskview.h" #include "model/task.h" #include "export/totalsastext.h" #include "helpers.h" class ExportCSVTest : public QObject { Q_OBJECT private: ReportCriteria createRC(ReportCriteria::REPORTTYPE type, bool toClipboard); private Q_SLOTS: void testTotalsEmpty(); void testTotalsSimpleTree(); void testTimesSimpleTree(); void testHistorySimpleTree(); }; ReportCriteria ExportCSVTest::createRC(ReportCriteria::REPORTTYPE type, bool toClipboard) { QTemporaryFile *file = new QTemporaryFile(this); if (!file->open()) { delete file; throw std::runtime_error("1"); } ReportCriteria rc; rc.reportType = type; rc.url = QUrl::fromLocalFile(file->fileName()); rc.from = QDate::currentDate(); rc.to = QDate::currentDate(); rc.decimalMinutes = false; rc.sessionTimes = false; rc.allTasks = true; rc.bExPortToClipBoard = toClipboard; rc.delimiter = ";"; rc.quote = "\""; return rc; } void ExportCSVTest::testTotalsEmpty() { QLocale::setDefault(QLocale(QLocale::C)); - auto *taskView = new TaskView(); + auto *taskView = createTaskView(this, false); const QString &timeString = QLocale().toString(QDateTime::currentDateTime()); const QString &expected = QStringLiteral( "Task Totals\n%1\n\n" " Time Task\n----------------------------------------------\nNo tasks.").arg(timeString); QCOMPARE( totalsAsText(taskView->tasksModel(), taskView->currentItem(), createRC(ReportCriteria::CSVTotalsExport, true)), expected); } void ExportCSVTest::testTotalsSimpleTree() { QLocale::setDefault(QLocale(QLocale::C)); auto *taskView = createTaskView(this, true); const QString &timeString = QLocale().toString(QDateTime::currentDateTime()); const QString &expected = QStringLiteral( "Task Totals\n" "%1\n" "\n" " Time Task\n" "----------------------------------------------\n" " 0:08 1\n" " 0:03 2\n" " 0:07 3\n" "----------------------------------------------\n" " 0:15 Total").arg(timeString); QCOMPARE( totalsAsText(taskView->tasksModel(), taskView->currentItem(), createRC(ReportCriteria::CSVTotalsExport, true)), expected); } void ExportCSVTest::testTimesSimpleTree() { QLocale::setDefault(QLocale(QLocale::C)); auto *taskView = createTaskView(this, true); const auto &rc = createRC(ReportCriteria::CSVTotalsExport, false); QCOMPARE(taskView->report(rc), ""); const QString &expected = QStringLiteral( "\"3\";;0:07;0:07;0:07;0:07\n" "\"1\";;0:05;0:05;0:08;0:08\n" ";\"2\";0:03;0:03;0:03;0:03\n"); QCOMPARE(readTextFile(rc.url.path()), expected); } void ExportCSVTest::testHistorySimpleTree() { QLocale::setDefault(QLocale(QLocale::C)); auto *taskView = createTaskView(this, true); const auto &rc = createRC(ReportCriteria::CSVHistoryExport, false); QCOMPARE(taskView->report(rc), ""); const QString &expected = QStringLiteral( "\"Task name\";%1\n" "\"1\";0:05\n" "\"1->2\";0:03\n" "\"3\";0:07\n").arg(QDate::currentDate().toString()); QCOMPARE(readTextFile(rc.url.path()), expected); } QTEST_MAIN(ExportCSVTest) #include "exportcsvtest.moc" diff --git a/src/tests/helpers.cpp b/src/tests/helpers.cpp index cbf8ce5..588062e 100644 --- a/src/tests/helpers.cpp +++ b/src/tests/helpers.cpp @@ -1,43 +1,43 @@ #include "helpers.h" #include #include #include "taskview.h" #include "model/task.h" TaskView *createTaskView(QObject *parent, bool simpleTree) { auto *taskView = new TaskView(); QTemporaryFile *icsFile = new QTemporaryFile(parent); if (!icsFile->open()) { delete taskView; delete icsFile; return nullptr; } - taskView->storage()->load(taskView, QUrl::fromLocalFile(icsFile->fileName())); + taskView->load(QUrl::fromLocalFile(icsFile->fileName())); if (simpleTree) { Task* task1 = taskView->task(taskView->addTask("1")); Task* task2 = taskView->task(taskView->addTask("2", QString(), 0, 0, QVector(0, 0), task1)); Task* task3 = taskView->task(taskView->addTask("3")); task1->changeTime(5, taskView->storage()); // add 5 minutes task2->changeTime(3, taskView->storage()); // add 3 minutes task3->changeTime(7, taskView->storage()); // add 7 minutes } return taskView; } QString readTextFile(const QString &path) { QFile file(path); if (!file.open(QFile::ReadOnly | QFile::Text)) { qFatal(QStringLiteral("failed to open file: %1").arg(path).toStdString().c_str()); } QTextStream in(&file); return in.readAll(); } diff --git a/src/tests/plannerparsertest.cpp b/src/tests/plannerparsertest.cpp index 24035f8..b76fb31 100644 --- a/src/tests/plannerparsertest.cpp +++ b/src/tests/plannerparsertest.cpp @@ -1,83 +1,83 @@ #include #include "taskview.h" #include "model/task.h" #include "helpers.h" class PlannerParserTest : public QObject { Q_OBJECT private Q_SLOTS: void testEmpty(); void testAtTopLevel(); void testAtSubTask(); }; void PlannerParserTest::testEmpty() { - auto *taskView = new TaskView(); + auto *taskView = createTaskView(this, false); taskView->importPlanner(QFINDTESTDATA("data/kitchen.planner")); auto *model = taskView->tasksModel(); QCOMPARE(model->getAllItems().size(), 21); QCOMPARE(model->topLevelItemCount(), 4); QCOMPARE(dynamic_cast(model->topLevelItem(0))->name(), "Initiation"); auto *closureTask = dynamic_cast(model->topLevelItem(3)); QCOMPARE(closureTask->name(), "Closure"); QCOMPARE(closureTask->childCount(), 3); } void PlannerParserTest::testAtTopLevel() { auto *taskView = createTaskView(this, false); Task* task1 = taskView->task(taskView->addTask("1")); QVERIFY(task1); taskView->setCurrentIndex(taskView->tasksModel()->index(task1, 0)); taskView->importPlanner(QFINDTESTDATA("data/kitchen.planner")); auto *model = taskView->tasksModel(); QCOMPARE(model->getAllItems().size(), 22); QCOMPARE(model->topLevelItemCount(), 5); QCOMPARE(dynamic_cast(model->topLevelItem(0))->name(), "1"); QCOMPARE(dynamic_cast(model->topLevelItem(1))->name(), "Initiation"); auto *closureTask = dynamic_cast(model->topLevelItem(4)); QCOMPARE(closureTask->name(), "Closure"); QCOMPARE(closureTask->childCount(), 3); } void PlannerParserTest::testAtSubTask() { auto *taskView = createTaskView(this, false); Task* task1 = taskView->task(taskView->addTask("1")); QVERIFY(task1); Task* task2 = taskView->task(taskView->addTask("2", QString(), 0, 0, QVector(0, 0), task1)); QVERIFY(task2); taskView->setCurrentIndex(taskView->tasksModel()->index(task2, 0)); taskView->importPlanner(QFINDTESTDATA("data/kitchen.planner")); auto *model = taskView->tasksModel(); QCOMPARE(model->getAllItems().size(), 23); QCOMPARE(model->topLevelItemCount(), 1); QCOMPARE(task1->childCount(), 5); QCOMPARE(dynamic_cast(task1->child(0))->name(), "2"); QCOMPARE(dynamic_cast(task1->child(1))->name(), "Initiation"); auto *closureTask = dynamic_cast(task1->child(4)); QCOMPARE(closureTask->name(), "Closure"); QCOMPARE(closureTask->childCount(), 3); } QTEST_MAIN(PlannerParserTest) #include "plannerparsertest.moc" diff --git a/src/timetrackerstorage.cpp b/src/timetrackerstorage.cpp index 68a3d00..d4f92b7 100644 --- a/src/timetrackerstorage.cpp +++ b/src/timetrackerstorage.cpp @@ -1,671 +1,617 @@ /* * Copyright (C) 2003, 2004 by Mark Bucciarelli * * This program 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. * * This program 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 this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ /** TimeTrackerStorage * This class cares for the storage of ktimetracker's data. * ktimetracker's data is * - tasks like "programming for customer foo" * - events like "from 2009-09-07, 8pm till 10pm programming for customer foo" * tasks are like the items on your todo list, events are dates when you worked on them. * ktimetracker's data is stored in a ResourceCalendar object hold by mCalendar. */ #include "timetrackerstorage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ktimetrackerutility.h" #include "ktimetracker.h" #include "model/task.h" +#include "model/eventsmodel.h" +#include "model/projectmodel.h" #include "taskview.h" #include "export/totalsastext.h" #include "ktt_debug.h" const QByteArray eventAppName = QByteArray("ktimetracker"); TimeTrackerStorage::TimeTrackerStorage() - : m_calendar() + : m_model(nullptr) , m_url() , m_fileLock(new QLockFile(QStringLiteral("ktimetrackerics.lock"))) , m_taskView(nullptr) { } TimeTrackerStorage::~TimeTrackerStorage() { delete m_fileLock; } // Loads data from filename into view. QString TimeTrackerStorage::load(TaskView *view, const QUrl &url) { if (url.isEmpty()) { return QStringLiteral("TimeTrackerStorage::load() callled with an empty URL"); } // loading might create the file bool removedFromDirWatch = false; if (KDirWatch::self()->contains(m_url.path())) { KDirWatch::self()->removeFile(m_url.path()); removedFromDirWatch = true; } // If same file, don't reload if (url == m_url) { if (removedFromDirWatch) { KDirWatch::self()->addFile(m_url.path()); } return QString(); } const bool fileIsLocal = url.isLocalFile(); // If file doesn't exist, create a blank one to avoid ResourceLocal load // error. We make it user and group read/write, others read. This is // masked by the users umask. (See man creat) if (fileIsLocal) { int handle = open( QFile::encodeName(url.path()), O_CREAT | O_EXCL | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH); if (handle != -1) { close(handle); } } - if (m_calendar) { + if (m_model) { closeStorage(); } + m_model = new ProjectModel(); + if (fileIsLocal) { connect(KDirWatch::self(), &KDirWatch::dirty, this, &TimeTrackerStorage::onFileModified); if (!KDirWatch::self()->contains(url.path())) { KDirWatch::self()->addFile(url.path()); } } // Create local file resource and add to resources m_url = url; - m_calendar = FileCalendar::Ptr(new FileCalendar(m_url)); + auto m_calendar = FileCalendar::Ptr(new FileCalendar(m_url)); m_calendar->setWeakPointer(m_calendar); m_taskView = view; // m_calendar->setTimeSpec( KSystemTimeZones::local() ); m_calendar->reload(); // Build task view from iCal data QString err; + eventsModel()->load(*m_calendar); err = buildTaskView(m_calendar, view); if (removedFromDirWatch) { KDirWatch::self()->addFile(m_url.path()); } return err; } QUrl TimeTrackerStorage::fileUrl() { return m_url; } QString TimeTrackerStorage::buildTaskView(const FileCalendar::Ptr& calendar, TaskView* view) // makes *view contain the tasks out of *rc. { // remember tasks that are running and their start times QVector runningTasks; QVector startTimes; for (Task *task : view->getAllTasks()) { if (task->isRunning()) { runningTasks.append(task->uid()); startTimes.append(task->startTime()); } } view->tasksModel()->clear(); QMultiHash map; auto todoList = calendar->rawTodos(); for (auto todo : todoList) { - Task* task = new Task(todo, view); + Task* task = new Task(todo, view, eventsModel()); map.insert(todo->uid(), task); view->setRootIsDecorated(true); task->invalidateCompletedState(); } // 1.1. Load each task under its parent task. QString err; for (auto todo : todoList) { Task* task = map.value(todo->uid()); // No relatedTo incident just means this is a top-level task. if (!todo->relatedTo().isEmpty()) { Task* newParent = map.value(todo->relatedTo()); // Complete the loading but return a message if (!newParent) { err = i18n("Error loading \"%1\": could not find parent (uid=%2)", task->name(), todo->relatedTo()); } else { task->move(newParent); } } } view->clearActiveTasks(); // restart tasks that have been running with their start times for (Task *task : view->getAllTasks()) { for (int n = 0; n < runningTasks.count(); ++n) { if (runningTasks[n] == task->uid()) { view->startTimerFor(task, startTimes[n]); } } } view->refresh(); return err; } void TimeTrackerStorage::closeStorage() { - if (m_calendar) { - m_calendar->close(); - m_calendar = FileCalendar::Ptr(); + if (m_model) { + delete m_model; + m_model = nullptr; } } -KCalCore::Event::List TimeTrackerStorage::rawevents() +EventsModel *TimeTrackerStorage::eventsModel() { - return m_calendar->rawEvents(); + if (!m_model) { + qFatal("TimeTrackerStorage::eventsModel is nullptr"); + } + + return m_model->eventsModel(); } -bool TimeTrackerStorage::allEventsHaveEndTiMe(Task* task) +TasksModel *TimeTrackerStorage::tasksModel() { - qCDebug(KTT_LOG) << "Entering function"; - KCalCore::Event::List eventList = m_calendar->rawEvents(); - for(KCalCore::Event::List::iterator i = eventList.begin(); - i != eventList.end(); ++i) - { - if ( (*i)->relatedTo() == task->uid() && !(*i)->hasEndDate() ) { - return false; + if (!m_model) { + qFatal("TimeTrackerStorage::tasksModel is nullptr"); + } + + return m_model->tasksModel(); +} + +bool TimeTrackerStorage::allEventsHaveEndTiMe(Task *task) +{ + for (const auto *event : m_model->eventsModel()->eventsForTask(task)) { + if (!event->hasEndDate()) { + return false; } } + return true; } bool TimeTrackerStorage::allEventsHaveEndTiMe() { - qCDebug(KTT_LOG) << "Entering function"; - KCalCore::Event::List eventList = m_calendar->rawEvents(); - for(KCalCore::Event::List::iterator i = eventList.begin(); - i != eventList.end(); ++i) - { - if ( !(*i)->hasEndDate() ) return false; + for (const auto *event : m_model->eventsModel()->events()) { + if (!event->hasEndDate()) { + return false; + } } + return true; } QString TimeTrackerStorage::deleteAllEvents() { - qCDebug(KTT_LOG) << "Entering function"; - QString err; - KCalCore::Event::List eventList = m_calendar->rawEvents(); -// m_calendar->deleteAllEvents(); // FIXME - return err; + m_model->eventsModel()->clear(); + return QString(); } QString TimeTrackerStorage::save(TaskView* taskview) { qCDebug(KTT_LOG) << "Entering function"; QString errorString; for (int i = 0; i < taskview->tasksModel()->topLevelItemCount(); ++i) { Task *task = dynamic_cast(taskview->tasksModel()->topLevelItem(i)); qCDebug(KTT_LOG) << "write task" << task->name(); errorString = writeTaskAsTodo(task, KCalCore::Todo::Ptr()); } errorString = saveCalendar(); if (errorString.isEmpty()) { qCDebug(KTT_LOG) << "TimeTrackerStorage::save : wrote tasks to" << m_url; } else { qCWarning(KTT_LOG) << "TimeTrackerStorage::save :" << errorString; } return errorString; } QString TimeTrackerStorage::writeTaskAsTodo(Task *task, KCalCore::Todo::Ptr parent) { QString err; - KCalCore::Todo::Ptr todo = m_calendar->todo(task->uid()); - if (!todo) { - qCDebug(KTT_LOG) << "Could not get todo from calendar"; - return "Could not get todo from calendar"; - } + KCalCore::Todo::Ptr todo(new KCalCore::Todo()); task->asTodo(todo); if (parent) { todo->setRelatedTo(parent->uid()); } for (int i = 0; i < task->childCount(); ++i) { Task *nextTask = dynamic_cast(task->child(i)); err = writeTaskAsTodo(nextTask, todo); } return err; } //---------------------------------------------------------------------------- // Routines that handle logging ktimetracker history - -QString TimeTrackerStorage::addTask(const Task* task, const Task* parent) -{ - qCDebug(KTT_LOG) << "Entering function"; - KCalCore::Todo::Ptr todo; - QString uid; - - if (!m_calendar) { - qCDebug(KTT_LOG) << "m_calendar is not set"; - return uid; - } - todo = KCalCore::Todo::Ptr(new KCalCore::Todo()); - if (m_calendar->addTodo(todo)) { - task->asTodo(todo); - if (parent) { - todo->setRelatedTo( parent->uid() ); - } - uid = todo->uid(); - } else { - // Most likely a lock could not be pulled, although there are other - // possiblities (like a really confused resource manager). - uid.clear(); - } - return uid; -} - QString TimeTrackerStorage::removeEvent(QString uid) { - KCalCore::Event::List eventList = m_calendar->rawEvents(); - for(KCalCore::Event::List::iterator i = eventList.begin(); - i != eventList.end(); ++i) { - if ((*i)->uid() == uid) { - m_calendar->deleteEvent(*i); - } - } + m_model->eventsModel()->removeByUID(uid); return QString(); } -bool TimeTrackerStorage::removeTask(Task* task) -{ - qCDebug(KTT_LOG) << "Entering function"; - // delete history - KCalCore::Event::List eventList = m_calendar->rawEvents(); - for(KCalCore::Event::List::iterator i = eventList.begin(); - i != eventList.end(); ++i) { - if ((*i)->relatedTo() == task->uid()) { - m_calendar->deleteEvent(*i); - } - } - - // delete todo - KCalCore::Todo::Ptr todo = m_calendar->todo(task->uid()); - m_calendar->deleteTodo(todo); - // Save entire file - saveCalendar(); - - return true; -} - -void TimeTrackerStorage::addComment(const Task* task, const QString& comment) -{ - KCalCore::Todo::Ptr todo = m_calendar->todo(task->uid()); - - // Do this to avoid compiler warnings about comment not being used. once we - // transition to using the addComment method, we need this second param. - QString s = comment; - - // TODO: Use libkcalcore comments - // todo->addComment(comment); - // temporary - todo->setDescription(task->comment()); - - saveCalendar(); -} - int todaySeconds(const QDate &date, const KCalCore::Event::Ptr &event) { if (!event) { return 0; } qCDebug(KTT_LOG) << "found an event for task, event=" << event->uid(); QDateTime startTime = event->dtStart(); QDateTime endTime = event->dtEnd(); QDateTime NextMidNight = startTime; NextMidNight.setTime(QTime(0, 0)); NextMidNight = NextMidNight.addDays(1); // LastMidNight := mdate.setTime(0:00) as it would read in a decent programming language QDateTime LastMidNight = QDateTime::currentDateTime(); LastMidNight.setDate(date); LastMidNight.setTime(QTime(0, 0)); int secsstartTillMidNight = startTime.secsTo(NextMidNight); int secondsToAdd = 0; // seconds that need to be added to the actual cell if (startTime.date() == date && event->dtEnd().date() == date) { // all the event occurred today secondsToAdd = startTime.secsTo(endTime); } if (startTime.date() == date && endTime.date()>date) { // the event started today, but ended later secondsToAdd = secsstartTillMidNight; } if (startTime.date() < date && endTime.date() == date) { // the event started before today and ended today secondsToAdd = LastMidNight.secsTo(event->dtEnd()); } if (startTime.date() < date && endTime.date() > date) { // the event started before today and ended after secondsToAdd = 86400; } return secondsToAdd; } // export history report as csv, all tasks X all dates in one block QString TimeTrackerStorage::exportCSVHistory( TaskView *taskview, const QDate &from, const QDate &to, const ReportCriteria &rc) { qCDebug(KTT_LOG) << "Entering function"; QString delim = rc.delimiter; const QString cr = QStringLiteral("\n"); QString err = QString::null; QString retval; const int intervalLength = from.daysTo(to) + 1; QMap> secsForUid; QMap uidForName; // Step 1: Prepare two hashmaps: // * "uid -> seconds each day": used while traversing events, as uid is their id // "seconds each day" are stored in a vector // * "name -> uid", ordered by name: used when creating the csv file at the end qCDebug(KTT_LOG) << "Taskview Count: " << taskview->count(); for (Task *task : taskview->getAllTasks()) { qCDebug(KTT_LOG) << ", Task Name: " << task->name() << ", UID: " << task->uid(); // uid -> seconds each day // * Init each element to zero QVector vector(intervalLength, 0); secsForUid[task->uid()] = vector; // name -> uid // * Create task fullname concatenating each parent's name QString fullName; Task* parentTask; parentTask = task; fullName += parentTask->name(); parentTask = parentTask->parentTask(); while (parentTask) { fullName = parentTask->name() + "->" + fullName; qCDebug(KTT_LOG) << "Fullname(inside): " << fullName; parentTask = parentTask->parentTask(); qCDebug(KTT_LOG) << "Parent task: " << parentTask; } uidForName[fullName] = task->uid(); qCDebug(KTT_LOG) << "Fullname(end): " << fullName; } qCDebug(KTT_LOG) << "secsForUid" << secsForUid; qCDebug(KTT_LOG) << "uidForName" << uidForName; + auto calendar = m_model->asCalendar(QUrl()); // Step 2: For each date, get the events and calculate the seconds // Store the seconds using secsForUid hashmap, so we don't need to translate uids // We rely on rawEventsForDate to get the events qCDebug(KTT_LOG) << "Let's iterate for each date: "; int dayCount = 0; for ( QDate mdate=from; mdate.daysTo(to)>=0; mdate=mdate.addDays(1) ) { if (dayCount++ > 365 * 100) { return QStringLiteral("too many days to process"); } qCDebug(KTT_LOG) << mdate.toString(); - KCalCore::Event::List dateEvents = m_calendar->rawEventsForDate(mdate); + KCalCore::Event::List dateEvents = calendar->rawEventsForDate(mdate); for(KCalCore::Event::List::iterator i = dateEvents.begin();i != dateEvents.end(); ++i) { qCDebug(KTT_LOG) << "Summary: " << (*i)->summary() << ", Related to uid: " << (*i)->relatedTo(); qCDebug(KTT_LOG) << "Today's seconds: " << todaySeconds(mdate, *i); secsForUid[(*i)->relatedTo()][from.daysTo(mdate)] += todaySeconds(mdate, *i); } } // Step 3: For each task, generate the matching row for the CSV file // We use the two hashmaps to have direct access using the task name // First CSV file line // FIXME: localize strings and date formats retval.append("\"Task name\""); for (int i=0; i nameUid(uidForName); double time; while (nameUid.hasNext()) { nameUid.next(); retval.append("\"" + nameUid.key() + "\""); qCDebug(KTT_LOG) << nameUid.key() << ": " << nameUid.value() << endl; for (int day=0; daysetText(retval); } else { // store the file locally or remote if (rc.url.isLocalFile()) { qCDebug(KTT_LOG) << "storing a local file"; QString filename=rc.url.toLocalFile(); if (filename.isEmpty()) filename=rc.url.url(); QFile f( filename ); if( !f.open( QIODevice::WriteOnly ) ) { err = i18n( "Could not open \"%1\".", filename ); qCDebug(KTT_LOG) << "Could not open file"; } qDebug() << "Err is " << err; if (err.length()==0) { QTextStream stream(&f); qCDebug(KTT_LOG) << "Writing to file: " << retval; // Export to file stream << retval; f.close(); } } else // use remote file { auto* const job = KIO::storedPut(retval.toUtf8(), rc.url, -1); //KJobWidgets::setWindow(job, &dialog); // TODO: add progress dialog if (!job->exec()) { err=QString::fromLatin1("Could not upload"); } } } return err; } void TimeTrackerStorage::changeTime(const Task* task, long deltaSeconds) { qCDebug(KTT_LOG) << "Entering function; deltaSeconds=" << deltaSeconds; - KCalCore::Event::Ptr e; QDateTime end; - e = baseEvent(task); + auto kcalEvent = baseEvent(task); + Event *e = new Event(kcalEvent, eventsModel()); // Don't use duration, as ICalFormatImpl::writeIncidence never writes a // duration, even though it looks like it's used in event.cpp. end = task->startTime(); if ( deltaSeconds > 0 ) end = task->startTime().addSecs(deltaSeconds); e->setDtEnd(end); // Use a custom property to keep a record of negative durations - e->setCustomProperty(eventAppName, QByteArray("duration"), QString::number(deltaSeconds)); + e->setDuration(deltaSeconds); - m_calendar->addEvent(e); + eventsModel()->addEvent(e); task->taskView()->scheduleSave(); } bool TimeTrackerStorage::bookTime(const Task* task, const QDateTime& startDateTime, long durationInSeconds) { qCDebug(KTT_LOG) << "Entering function"; - auto e = baseEvent(task); + auto kcalEvent = baseEvent(task); + Event *e = new Event(kcalEvent, eventsModel()); + e->setDtStart(startDateTime); e->setDtEnd(startDateTime.addSecs( durationInSeconds)); // Use a custom property to keep a record of negative durations - e->setCustomProperty(eventAppName, QByteArray("duration"), QString::number(durationInSeconds)); - return m_calendar->addEvent(e); + e->setDuration(durationInSeconds); + + eventsModel()->addEvent(e); + return true; } KCalCore::Event::Ptr TimeTrackerStorage::baseEvent(const Task *task) { qCDebug(KTT_LOG) << "Entering function"; KCalCore::Event::Ptr e( new KCalCore::Event() ); QStringList categories; e->setSummary(task->name()); // Can't use setRelatedToUid()--no error, but no RelatedTo written to disk e->setRelatedTo( task->uid() ); // Debugging: some events where not getting a related-to field written. Q_ASSERT(e->relatedTo() == task->uid()); // Have to turn this off to get datetimes in date fields. e->setAllDay(false); // e->setDtStart(KDateTime(task->startTime(), KDateTime::Spec::LocalZone())); e->setDtStart(task->startTime()); // So someone can filter this mess out of their calendar display categories.append(i18n("KTimeTracker")); e->setCategories(categories); return e; } QString TimeTrackerStorage::saveCalendar() { qCDebug(KTT_LOG) << "Entering function"; bool removedFromDirWatch = false; if (KDirWatch::self()->contains(m_url.path())) { KDirWatch::self()->removeFile(m_url.path()); removedFromDirWatch = true; } - if (!m_calendar) { - qDebug() << "m_calendar not set"; - return QString("m_calendar not set"); + if (!m_model) { + qDebug() << "m_model not set"; + return QString("m_model not set"); } if (!m_fileLock->lock()) { return QString("Could not save. Could not lock file."); } QString errorMessage; - if (!m_calendar->save()) { + auto calendar = m_model->asCalendar(m_url); + calendar->setWeakPointer(calendar); + if (!calendar->save()) { errorMessage = QString("Could not save. Could lock file."); } m_fileLock->unlock(); if (removedFromDirWatch) { KDirWatch::self()->addFile(m_url.path()); } return errorMessage; } -FileCalendar::Ptr TimeTrackerStorage::calendar() const -{ - return m_calendar; -} - void TimeTrackerStorage::onFileModified() { - if (m_calendar.isNull()) { - qCWarning(KTT_LOG) << "TaskView::onFileModified(): calendar is null"; - } else { - qCDebug(KTT_LOG) << "entering function"; - m_calendar->reload(); - this->buildTaskView(m_calendar, m_taskView); - qCDebug(KTT_LOG) << "exiting onFileModified"; + if (!m_model) { + qCWarning(KTT_LOG) << "TaskView::onFileModified(): model is null"; + return; } + + qCDebug(KTT_LOG) << "entering function"; + + auto m_calendar = FileCalendar::Ptr(new FileCalendar(m_url)); + m_calendar->setWeakPointer(m_calendar); +// m_calendar->setTimeSpec( KSystemTimeZones::local() ); + m_calendar->reload(); + buildTaskView(m_calendar, m_taskView); + + qCDebug(KTT_LOG) << "exiting onFileModified"; } diff --git a/src/timetrackerstorage.h b/src/timetrackerstorage.h index 113b40f..98e84f6 100644 --- a/src/timetrackerstorage.h +++ b/src/timetrackerstorage.h @@ -1,257 +1,241 @@ /* * Copyright (C) 2003 by Mark Bucciarelli * * This program 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. * * This program 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 this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ #ifndef KTIMETRACKER_STORAGE_H #define KTIMETRACKER_STORAGE_H #include #include #include "reportcriteria.h" #include "desktoplist.h" #include "file/filecalendar.h" QT_BEGIN_NAMESPACE class QDateTime; class QLockFile; QT_END_NAMESPACE +class ProjectModel; +class Event; class Task; class TaskView; +class TasksModel; +class EventsModel; /** * Class to store/retrieve KTimeTracker data to/from persistent storage. * * The storage is an iCalendar file. Its name is contained in this class * in the variable _icalfile and can be read using the function icalfile(). * The name gets set by the load() operation. * * All logic that deals with getting and saving data should go here. * * This program uses iCalendar to store its data. There are tasks and * events. Events have a start and a end date and an associated task. * * @short Logic that gets and stores KTimeTracker data to disk. * @author Mark Bucciarelli */ class TimeTrackerStorage : public QObject { Q_OBJECT public: TimeTrackerStorage(); ~TimeTrackerStorage() override; /** Load the list view with tasks read from iCalendar file. Parses iCalendar file, builds list view items in the proper hierarchy, and loads them into the list view widget. If the file name passed in is the same as the last file name that was loaded, this method does nothing. This method considers any of the following conditions errors: @li the iCalendar file does not exist @li the iCalendar file is not readable @li the list group currently has list items @li an iCalendar todo has no related to attribute @li a todo is related to another todo which does not exist @param taskview The list group used in the TaskView. Must not be nullptr. @param url Override preferences' filename @return empty string if success, error message if error. */ QString load(TaskView *taskview, const QUrl &url); /** * Return the name of the iCal file */ QUrl fileUrl(); /** * Build up the taskview. * * This is needed if the iCal file has been modified. */ QString buildTaskView(const FileCalendar::Ptr& calendar, TaskView* view); /** Close calendar and clear view. Release lock if holding one. */ void closeStorage(); + bool isLoaded() const { return m_model; } + /** list of all events */ - KCalCore::Event::List rawevents(); + EventsModel *eventsModel(); - QString removeEvent(QString uid); + TasksModel *tasksModel(); - FileCalendar::Ptr calendar() const; + QString removeEvent(QString uid); /** * Deliver if all events of a task have an endtime * * If ktimetracker has been quitted with one task running, it needs to resumeRunning(). * This function delivers if an enddate of an event has not yet been stored. * * @param task The task to be examined */ bool allEventsHaveEndTiMe(Task* task); /** * Deliver if all events of the actual calendar have an endtime * * If ktimetracker has been quitted with one task running, it needs to resumeRunning(). * This function delivers if an enddate of an event has not yet been stored. * */ bool allEventsHaveEndTiMe(); QString deleteAllEvents(); /** * Save all tasks and their totals to an iCalendar file. * * All tasks must have an associated VTODO object already created in the * calendar file; that is, the task->uid() must refer to a valid VTODO in * the calendar. * Delivers empty string if successful, else error msg. * * @param taskview The list group used in the TaskView */ QString save(TaskView* taskview); /** * Log the change in a task's time. * * This is also called when a timer is stopped. * We create an iCalendar event to store each change. The event start * date is set to the current datetime. If time is added to the task, the * task end date is set to start time + delta. If the time is negative, * the end date is set to the start time. * * In both cases (postive or negative delta), we create a custom iCalendar * property that stores the delta (in seconds). This property is called * X-KDE-ktimetracker-duration. * * Note that the ktimetracker UI allows the user to change both the session and * the total task time, and this routine does not account for all posibile * cases. For example, it is possible for the user to do something crazy * like add 10 minutes to the session time and subtract 50 minutes from * the total time. Although this change violates a basic law of physics, * it is allowed. * * For now, you should pass in the change to the total task time. * * @param task The task the change is for. * @param delta Change in task time, in seconds. Can be negative. */ void changeTime(const Task* task, long deltaSeconds); /** * Book time to a task. * * Creates an iCalendar event and adds it to the calendar. Does not write * calendar to disk, just adds event to calendar in memory. However, the * resource framework does try to get a lock on the file. After a * successful lock, the calendar marks this incidence as modified and then * releases the lock. * * @param task Task * @param startDateTime Date and time the booking starts. * @param durationInSeconds Duration of time to book, in seconds. * * @return true if event was added, false if not (if, for example, the * attempted file lock failed). */ bool bookTime(const Task* task, const QDateTime& startDateTime, const long durationInSeconds); /** * Log a change to a task name. * * For iCalendar storage, there is no need to log an Event for this event, * since unique id's are used to link Events to Todos. No matter how many * times you change a task's name, the uid stays the same. * * @param task The task * @param oldname The old name of the task. The new name is in the task * object already. */ void setName(const Task* task, const QString& oldname) { Q_UNUSED(task); Q_UNUSED(oldname); } - /** - * Log a new comment for this task. - * - * iCal allows multiple comment tags. So we just add a new comment to the - * todo for this task and write the calendar. - * - * @param task The task that gets the comment - * @param comment The comment - */ - void addComment(const Task* task, const QString& comment); - - - /** - * Remove this task from iCalendar file. - * - * Removes task as well as all event history for this task. - * - * @param task The task to be removed. - * @return true if change was saved, false otherwise - */ - bool removeTask(Task* task); - /** * Add this task from iCalendar file. * * Create a new KCalCore::Todo object and load with task information. If * parent is not zero, then set the RELATED-TO attribute for this Todo. * * @param task The task to be removed. * @param parent The parent of this task. Must have a uid() that is in * the existing calendar. If zero, this task is considered a root task. * @return The unique ID for the new VTODO. Return an null QString if * there was an error creating the new calendar object. */ QString addTask(const Task* task, const Task* parent = nullptr); /** * Write task history to file as comma-delimited data. */ QString exportCSVHistory(TaskView *taskview, const QDate &from, const QDate &to, const ReportCriteria &rc); private Q_SLOTS: void onFileModified(); private: - FileCalendar::Ptr m_calendar; + ProjectModel *m_model; QUrl m_url; QLockFile *m_fileLock; TaskView* m_taskView; QString writeTaskAsTodo(Task *task, KCalCore::Todo::Ptr parent); QString saveCalendar(); KCalCore::Event::Ptr baseEvent(const Task*); }; #endif // KTIMETRACKER_STORAGE_H diff --git a/src/timetrackerwidget.cpp b/src/timetrackerwidget.cpp index 99fc981..b1e60be 100644 --- a/src/timetrackerwidget.cpp +++ b/src/timetrackerwidget.cpp @@ -1,933 +1,934 @@ /* * Copyright (C) 2007 by Mathias Soeken * * This program 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. * * This program 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 this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ // TODO: what is the sense of tasksChanged()? #include "timetrackerwidget.h" #include #include #include #include #include #include "model/task.h" +#include "model/eventsmodel.h" #include "settings/ktimetrackerconfigdialog.h" #include "widgets/searchline.h" #include "historydialog.h" #include "idletimedetector.h" #include "ktimetrackerutility.h" #include "ktimetracker.h" #include "mainadaptor.h" #include "reportcriteria.h" #include "taskview.h" #include "ktimetracker-version.h" #include "mainwindow.h" #include "ktt_debug.h" TimeTrackerWidget::TimeTrackerWidget(QWidget *parent) : QWidget(parent) , m_searchLine(nullptr) , m_taskView(nullptr) , m_actionCollection(nullptr) { qCDebug(KTT_LOG) << "Entering function"; new MainAdaptor(this); QDBusConnection::sessionBus().registerObject("/KTimeTracker", this); QLayout* layout = new QVBoxLayout; layout->setMargin(0); layout->setSpacing(0); m_searchLine = new SearchLine(this); connect(m_searchLine, &SearchLine::textSubmitted, this, &TimeTrackerWidget::slotAddTask); m_taskView = new TaskView(this); connect(m_searchLine, &SearchLine::searchUpdated, m_taskView, &TaskView::setFilterText); layout->addWidget(m_searchLine); layout->addWidget(m_taskView); setLayout(layout); showSearchBar(!KTimeTrackerSettings::configPDA() && KTimeTrackerSettings::showSearchBar()); } bool TimeTrackerWidget::allEventsHaveEndTiMe() { return currentTaskView()->allEventsHaveEndTiMe(); } int TimeTrackerWidget::focusSearchBar() { if (m_searchLine->isEnabled()) { m_searchLine->setFocus(); } return 0; } void TimeTrackerWidget::addTaskView(const QUrl &url) { qCDebug(KTT_LOG) << "Entering function (url=" << url << ")"; bool isNew = url.isEmpty(); QUrl lFileName = url; if (isNew) { QTemporaryFile tempFile; tempFile.setAutoRemove(false); if (tempFile.open()) { lFileName = tempFile.fileName(); tempFile.close(); } else { KMessageBox::error(this, i18n("Cannot create new file.")); return; } } TaskView *taskView = m_taskView; connect(taskView, &TaskView::contextMenuRequested, this, &TimeTrackerWidget::contextMenuRequested); connect(taskView, &TaskView::timersActive, this, &TimeTrackerWidget::timersActive); connect(taskView, &TaskView::timersInactive, this, &TimeTrackerWidget::timersInactive); connect(taskView, &TaskView::tasksChanged, this, &TimeTrackerWidget::tasksChanged); emit setCaption(url.toString()); taskView->load(lFileName); // When adding the first tab currentChanged is not emitted, so... if (!m_taskView) { emit currentTaskViewChanged(); slotCurrentChanged(); } } TaskView* TimeTrackerWidget::currentTaskView() const { return m_taskView; } Task* TimeTrackerWidget::currentTask() { TaskView* taskView = currentTaskView(); return taskView ? taskView->currentItem() : nullptr; } void TimeTrackerWidget::setupActions(KActionCollection* actionCollection) { Q_INIT_RESOURCE(pics); m_actionCollection = actionCollection; KStandardAction::open(this, SLOT(openFile()), actionCollection); KStandardAction::save(this, &TimeTrackerWidget::saveFile, actionCollection); KStandardAction::preferences(this, &TimeTrackerWidget::showSettingsDialog, actionCollection); QAction* startNewSession = actionCollection->addAction(QStringLiteral("start_new_session")); startNewSession->setText(i18n("Start &New Session")); startNewSession->setToolTip(i18n("Starts a new session")); startNewSession->setWhatsThis(i18n("This will reset the " "session time to 0 for all tasks, to start a new session, without " "affecting the totals.")); connect(startNewSession, &QAction::triggered, this, &TimeTrackerWidget::startNewSession); QAction* editHistory = actionCollection->addAction(QStringLiteral("edit_history")); editHistory->setText(i18n("Edit History...")); editHistory->setToolTip(i18n("Edits history of all tasks of the current document")); editHistory->setWhatsThis(i18n("A window will " "be opened where you can change start and stop times of tasks or add a " "comment to them.")); editHistory->setIcon(QIcon::fromTheme("view-history")); connect(editHistory, &QAction::triggered, this, &TimeTrackerWidget::editHistory); QAction* resetAllTimes = actionCollection->addAction(QStringLiteral("reset_all_times")); resetAllTimes->setText(i18n("&Reset All Times")); resetAllTimes->setToolTip(i18n("Resets all times")); resetAllTimes->setWhatsThis(i18n("This will reset the session " "and total time to 0 for all tasks, to restart from scratch.")); connect(resetAllTimes, &QAction::triggered, this, &TimeTrackerWidget::resetAllTimes); QAction* startCurrentTimer = actionCollection->addAction(QStringLiteral("start")); startCurrentTimer->setText(i18n("&Start")); startCurrentTimer->setToolTip(i18n("Starts timing for selected task")); startCurrentTimer->setWhatsThis(i18n("This will start timing for the " "selected task.\nIt is even possible to time several tasks " "simultanously.\n\nYou may also start timing of tasks by double clicking " "the left mouse button on a given task. This will, however, stop timing " "of other tasks.")); startCurrentTimer->setIcon(QIcon::fromTheme("media-playback-start")); actionCollection->setDefaultShortcut(startCurrentTimer, QKeySequence(Qt::Key_G)); connect(startCurrentTimer, &QAction::triggered, this, &TimeTrackerWidget::startCurrentTimer); QAction* stopCurrentTimer = actionCollection->addAction(QStringLiteral("stop")); stopCurrentTimer->setText(i18n("S&top")); stopCurrentTimer->setToolTip(i18n("Stops timing of the selected task")); stopCurrentTimer->setWhatsThis(i18n("Stops timing of the selected task")); stopCurrentTimer->setIcon(QIcon::fromTheme("media-playback-stop")); connect(stopCurrentTimer, &QAction::triggered, this, &TimeTrackerWidget::stopCurrentTimer); QAction* focusSearchBar = actionCollection->addAction(QStringLiteral("focusSearchBar")); focusSearchBar->setText(i18n("Focus on Searchbar")); focusSearchBar->setToolTip(i18n("Sets the focus on the searchbar")); focusSearchBar->setWhatsThis(i18n("Sets the focus on the searchbar")); actionCollection->setDefaultShortcut(focusSearchBar, QKeySequence(Qt::Key_S)); connect(focusSearchBar, &QAction::triggered, this, &TimeTrackerWidget::focusSearchBar); QAction* stopAllTimers = actionCollection->addAction(QStringLiteral("stopAll")); stopAllTimers->setText(i18n("Stop &All Timers")); stopAllTimers->setToolTip(i18n("Stops all of the active timers")); stopAllTimers->setWhatsThis(i18n("Stops all of the active timers")); actionCollection->setDefaultShortcut(stopAllTimers, QKeySequence(Qt::Key_Escape)); connect(stopAllTimers, SIGNAL(triggered()), this, SLOT(stopAllTimers())); QAction* focusTracking = actionCollection->addAction(QStringLiteral("focustracking")); focusTracking->setCheckable(true); focusTracking->setText(i18n("Track Active Applications")); focusTracking->setToolTip(i18n("Auto-creates and updates tasks when the focus of the " "current window has changed")); focusTracking->setWhatsThis(i18n("If the focus of a window changes for the " "first time when this action is enabled, a new task will be created " "with the title of the window as its name and will be started. If there " "already exists such an task it will be started.")); connect(focusTracking, &QAction::triggered, this, &TimeTrackerWidget::focusTracking); QAction* newTask = actionCollection->addAction(QStringLiteral("new_task")); newTask->setText(i18n("&New Task...")); newTask->setToolTip(i18n("Creates new top level task")); newTask->setWhatsThis(i18n("This will create a new top level task.")); newTask->setIcon(QIcon::fromTheme("document-new")); actionCollection->setDefaultShortcut(newTask, QKeySequence(Qt::CTRL + Qt::Key_T)); connect(newTask, &QAction::triggered, this, &TimeTrackerWidget::newTask); QAction* newSubTask = actionCollection->addAction(QStringLiteral("new_sub_task")); newSubTask->setText(i18n("New &Subtask...")); newSubTask->setToolTip(i18n("Creates a new subtask to the current selected task")); newSubTask->setWhatsThis(i18n("This will create a new subtask to the current selected task.")); newSubTask->setIcon(QIcon::fromTheme("subtask-new-ktimetracker")); actionCollection->setDefaultShortcut(newSubTask, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_N)); connect(newSubTask, &QAction::triggered, this, &TimeTrackerWidget::newSubTask); QAction* deleteTask = actionCollection->addAction(QStringLiteral("delete_task")); deleteTask->setText(i18n("&Delete")); deleteTask->setToolTip(i18n("Deletes selected task")); deleteTask->setWhatsThis(i18n("This will delete the selected task(s) and all subtasks.")); deleteTask->setIcon(QIcon::fromTheme("edit-delete")); actionCollection->setDefaultShortcut(deleteTask, QKeySequence(Qt::Key_Delete)); connect(deleteTask, &QAction::triggered, this, QOverload<>::of(&TimeTrackerWidget::deleteTask)); QAction* editTask = actionCollection->addAction(QStringLiteral("edit_task")); editTask->setText(i18n("&Edit...")); editTask->setToolTip(i18n("Edits name or times for selected task")); editTask->setWhatsThis(i18n("This will bring up a dialog " "box where you may edit the parameters for the selected task.")); editTask->setIcon(QIcon::fromTheme("document-properties")); actionCollection->setDefaultShortcut(editTask, QKeySequence(Qt::CTRL + Qt::Key_E)); connect(editTask, &QAction::triggered, this, &TimeTrackerWidget::editTask); QAction* markTaskAsComplete = actionCollection->addAction(QStringLiteral("mark_as_complete")); markTaskAsComplete->setText(i18n("&Mark as Complete")); markTaskAsComplete->setIcon(QPixmap(":/pics/task-complete.xpm")); actionCollection->setDefaultShortcut(markTaskAsComplete, QKeySequence(Qt::CTRL + Qt::Key_M)); connect(markTaskAsComplete, &QAction::triggered, this, &TimeTrackerWidget::markTaskAsComplete); QAction* markTaskAsIncomplete = actionCollection->addAction(QStringLiteral("mark_as_incomplete")); markTaskAsIncomplete->setText(i18n("&Mark as Incomplete")); markTaskAsIncomplete->setIcon(QPixmap(":/pics/task-incomplete.xpm")); actionCollection->setDefaultShortcut(markTaskAsIncomplete, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_M)); connect(markTaskAsIncomplete, &QAction::triggered, this, &TimeTrackerWidget::markTaskAsIncomplete); QAction* exportTimes = actionCollection->addAction(QStringLiteral("export_times")); exportTimes->setText(i18n("&Export Times...")); connect(exportTimes, &QAction::triggered, this, &TimeTrackerWidget::exportCSVFileDialog); QAction* exportHistory = actionCollection->addAction(QStringLiteral("export_history")); exportHistory->setText(i18n("Export &History...")); connect(exportHistory, &QAction::triggered, this, &TimeTrackerWidget::exportCSVHistoryDialog); QAction* importPlanner = actionCollection->addAction(QStringLiteral("import_planner")); importPlanner->setText(i18n("Import Tasks From &Planner...")); connect(importPlanner, SIGNAL(triggered(bool)), this, SLOT(importPlanner())); QAction* showSearchBar = actionCollection->addAction(QStringLiteral("searchbar")); showSearchBar->setCheckable(true); showSearchBar->setChecked(KTimeTrackerSettings::showSearchBar()); showSearchBar->setText(i18n("Show Searchbar")); connect(showSearchBar, &QAction::triggered, this, &TimeTrackerWidget::slotSearchBar); connect(this, &TimeTrackerWidget::currentTaskChanged, this, &TimeTrackerWidget::slotUpdateButtons); connect(this, &TimeTrackerWidget::currentTaskViewChanged, this, &TimeTrackerWidget::slotUpdateButtons); connect(this, &TimeTrackerWidget::updateButtons, this, &TimeTrackerWidget::slotUpdateButtons); } QAction *TimeTrackerWidget::action(const QString &name) const { return m_actionCollection->action(name); } void TimeTrackerWidget::openFile(const QUrl &url) { qCDebug(KTT_LOG) << "Entering function, url is " << url; QUrl newUrl = url; if (newUrl.isEmpty()) { const QString &path = QFileDialog::getOpenFileName(this); if (path.isEmpty()) { return; } else { newUrl = QUrl::fromLocalFile(path); } } addTaskView(newUrl); } bool TimeTrackerWidget::closeFile() { qCDebug(KTT_LOG) << "Entering TimeTrackerWidget::closeFile"; TaskView* taskView = currentTaskView(); if (taskView) { taskView->save(); taskView->closeStorage(); } emit currentTaskViewChanged(); emit setCaption(QString()); slotCurrentChanged(); delete taskView; // removeTab does not delete its widget. m_taskView = nullptr; return true; } void TimeTrackerWidget::saveFile() { currentTaskView()->save(); } void TimeTrackerWidget::showSearchBar(bool visible) { m_searchLine->setVisible(visible); } bool TimeTrackerWidget::closeAllFiles() { qCDebug(KTT_LOG) << "Entering TimeTrackerWidget::closeAllFiles"; bool err = true; if (m_taskView) { m_taskView->stopAllTimers(); err = closeFile(); } return err; } void TimeTrackerWidget::slotCurrentChanged() { qDebug() << "entering KTimeTrackerWidget::slotCurrentChanged"; if (m_taskView) { disconnect(m_taskView, SLOT(totalTimesChanged(long, long))); disconnect(m_taskView, SLOT(reSetTimes())); disconnect(m_taskView, SLOT(updateButtons())); disconnect(m_taskView, SLOT(setStatusBarText(QString))); disconnect(m_taskView, SLOT(timersActive())); disconnect(m_taskView, SLOT(timersInactive())); disconnect(m_taskView, &TaskView::tasksChanged, this, &TimeTrackerWidget::tasksChanged); connect( m_taskView, SIGNAL(totalTimesChanged(long,long)), this, SIGNAL(totalTimesChanged(long,long)) ); connect( m_taskView, SIGNAL(reSetTimes()), this, SIGNAL(reSetTimes()) ); connect( m_taskView, SIGNAL(updateButtons()), this, SIGNAL(updateButtons()) ); connect( m_taskView, SIGNAL(setStatusBarText(QString)), // FIXME signature this, SIGNAL(statusBarTextChangeRequested(QString)) ); connect( m_taskView, SIGNAL(timersActive()), this, SIGNAL(timersActive()) ); connect( m_taskView, SIGNAL(timersInactive()), this, SIGNAL(timersInactive()) ); connect( m_taskView, SIGNAL(tasksChanged(QList)), // FIXME signature this, SIGNAL(tasksChanged(QList)) ); emit setCaption(m_taskView->storage()->fileUrl().toString()); } m_searchLine->setEnabled(m_taskView); } void TimeTrackerWidget::slotAddTask(const QString &taskName) { TaskView *taskView = currentTaskView(); taskView->addTask(taskName, QString(), 0, 0, DesktopList(), 0); } void TimeTrackerWidget::slotUpdateButtons() { Task *item = currentTask(); action(QStringLiteral("start"))->setEnabled(item && !item->isRunning() && !item->isComplete()); action(QStringLiteral("stop"))->setEnabled(item && item->isRunning()); action(QStringLiteral("delete_task"))->setEnabled(item); action(QStringLiteral("edit_task"))->setEnabled(item); action(QStringLiteral("mark_as_complete"))->setEnabled(item && !item->isComplete()); action(QStringLiteral("mark_as_incomplete"))->setEnabled(item && item->isComplete()); action(QStringLiteral("new_task"))->setEnabled(currentTaskView()); action(QStringLiteral("new_sub_task"))->setEnabled(currentTaskView() && currentTaskView()->count()); action(QStringLiteral("focustracking"))->setEnabled(currentTaskView()); action(QStringLiteral("focustracking"))->setChecked(currentTaskView() && currentTaskView()->isFocusTrackingActive()); action(QStringLiteral("start_new_session"))->setEnabled(currentTaskView()); action(QStringLiteral("edit_history"))->setEnabled(currentTaskView()); action(QStringLiteral("reset_all_times"))->setEnabled(currentTaskView()); action(QStringLiteral("export_times"))->setEnabled(currentTaskView()); action(QStringLiteral("export_history"))->setEnabled(currentTaskView()); action(QStringLiteral("import_planner"))->setEnabled(currentTaskView()); action(QStringLiteral("file_save"))->setEnabled(currentTaskView()); } void TimeTrackerWidget::showSettingsDialog() { if (KConfigDialog::showDialog("settings")) { return; } KConfigDialog *dialog = new KConfigDialog(this, "settings", KTimeTrackerSettings::self()); dialog->setFaceType(KPageDialog::List); dialog->addPage(new KTimeTrackerBehaviorConfig(dialog), i18nc("@title:tab", "Behavior"), QStringLiteral("preferences-other")); dialog->addPage(new KTimeTrackerDisplayConfig(dialog), i18nc("@title:tab", "Appearance"), QStringLiteral("preferences-desktop-theme")); dialog->addPage(new KTimeTrackerStorageConfig(dialog), i18nc("@title:tab", "Storage"), QStringLiteral("system-file-manager")); connect(dialog, SIGNAL(settingsChanged(const QString&)), this, SLOT(loadSettings())); dialog->show(); } void TimeTrackerWidget::loadSettings() { KTimeTrackerSettings::self()->load(); showSearchBar(!KTimeTrackerSettings::configPDA() && KTimeTrackerSettings::showSearchBar()); currentTaskView()->reconfigure(); } //BEGIN wrapper slots void TimeTrackerWidget::startCurrentTimer() { currentTaskView()->startCurrentTimer(); } void TimeTrackerWidget::stopCurrentTimer() { currentTaskView()->stopCurrentTimer(); } void TimeTrackerWidget::stopAllTimers(const QDateTime& when) { currentTaskView()->stopAllTimers(when); } void TimeTrackerWidget::newTask() { currentTaskView()->newTask(); } void TimeTrackerWidget::newSubTask() { currentTaskView()->newSubTask(); } void TimeTrackerWidget::editTask() { currentTaskView()->editTask(); } void TimeTrackerWidget::deleteTask() { currentTaskView()->deleteTask(); } void TimeTrackerWidget::markTaskAsComplete() { currentTaskView()->markTaskAsComplete(); } void TimeTrackerWidget::markTaskAsIncomplete() { currentTaskView()->markTaskAsIncomplete(); } void TimeTrackerWidget::exportCSVFileDialog() { currentTaskView()->exportCSVFileDialog(); } void TimeTrackerWidget::exportCSVHistoryDialog() { currentTaskView()->exportCSVHistoryDialog(); } void TimeTrackerWidget::importPlanner(const QString &fileName) { currentTaskView()->importPlanner(fileName); } void TimeTrackerWidget::startNewSession() { currentTaskView()->startNewSession(); } void TimeTrackerWidget::editHistory() { // HistoryDialog is the new HistoryDialog, but the EditHiStoryDiaLog exists as well. // HistoryDialog can be edited with qtcreator and qtdesigner, EditHiStoryDiaLog cannot. if (currentTaskView()) { auto *dialog = new HistoryDialog(currentTaskView(), currentTaskView()->storage()); connect(dialog, &HistoryDialog::timesChanged, currentTaskView(), &TaskView::reFreshTimes); - if (currentTaskView()->storage()->rawevents().count() != 0) { + if (currentTaskView()->storage()->eventsModel()->events().count() != 0) { dialog->exec(); } else { KMessageBox::information(nullptr, i18nc("@info in message box", "There is no history yet. Start and stop a task and you will have an entry in your history.")); } } } void TimeTrackerWidget::resetAllTimes() { if (currentTaskView()) { if (KMessageBox::warningContinueCancel( this, i18n("Do you really want to reset the time to zero for all tasks? This will delete the entire history."), i18n("Confirmation Required"), KGuiItem(i18n("Reset All Times"))) == KMessageBox::Continue) { currentTaskView()->resetTimeForAllTasks(); } } } void TimeTrackerWidget::focusTracking() { currentTaskView()->toggleFocusTracking(); action(QStringLiteral("focustracking"))->setChecked(currentTaskView()->isFocusTrackingActive()); } void TimeTrackerWidget::slotSearchBar() { bool currentVisible = KTimeTrackerSettings::showSearchBar(); KTimeTrackerSettings::setShowSearchBar(!currentVisible); action(QStringLiteral("searchbar"))->setChecked(!currentVisible); showSearchBar(!currentVisible); } //END /** \defgroup dbus slots ‘‘dbus slots’’ */ /* @{ */ QString TimeTrackerWidget::version() const { return KTIMETRACKER_VERSION_STRING; } QStringList TimeTrackerWidget::taskIdsFromName( const QString &taskName ) const { QStringList result; TaskView *taskView = currentTaskView(); if (!taskView) { return result; } for (Task *task : taskView->getAllTasks()) { if (task->name() == taskName) { result << task->uid(); } } return result; } void TimeTrackerWidget::addTask( const QString &taskName ) { TaskView *taskView = currentTaskView(); if ( taskView ) { taskView->addTask( taskName, QString(), 0, 0, DesktopList(), 0 ); } } void TimeTrackerWidget::addSubTask(const QString &taskName, const QString &taskId) { TaskView *taskView = currentTaskView(); if (taskView) { taskView->addTask(taskName, QString(), 0, 0, DesktopList(), taskView->task(taskId)); taskView->refresh(); } } void TimeTrackerWidget::deleteTask(const QString &taskId) { TaskView *taskView = currentTaskView(); if (!taskView) { return; } for (Task *task : taskView->getAllTasks()) { if (task->uid() == taskId) { taskView->deleteTaskBatch(task); } } } void TimeTrackerWidget::setPercentComplete(const QString &taskId, int percent) { TaskView *taskView = currentTaskView(); if (!taskView) { return; } for (Task *task : taskView->getAllTasks()) { if (task->uid() == taskId) { task->setPercentComplete(percent); } } } int TimeTrackerWidget::bookTime(const QString &taskId, const QString &dateTime, int minutes) { QDate startDate; QTime startTime; QDateTime startDateTime; if (minutes <= 0) { return KTIMETRACKER_ERR_INVALID_DURATION; } Task *task = nullptr; TaskView *taskView = currentTaskView(); if (taskView) { for (Task *t : taskView->getAllTasks()) { if (t->uid() == taskId) { task = t; break; } } } if (!task) { return KTIMETRACKER_ERR_UID_NOT_FOUND; } // Parse datetime startDate = QDate::fromString(dateTime, Qt::ISODate); if (dateTime.length() > 10) { // "YYYY-MM-DD".length() = 10 startTime = QTime::fromString(dateTime, Qt::ISODate); } else { startTime = QTime(12, 0); } if (startDate.isValid() && startTime.isValid()) { startDateTime = QDateTime(startDate, startTime); } else { return KTIMETRACKER_ERR_INVALID_DATE; } // Update task totals (session and total) and save to disk task->changeTotalTimes(task->sessionTime() + minutes, task->totalTime() + minutes); if (!task->taskView()->storage()->bookTime(task, startDateTime, minutes * 60)) { return KTIMETRACKER_ERR_GENERIC_SAVE_FAILED; } return 0; } int TimeTrackerWidget::changeTime(const QString &taskId, int minutes) { if (minutes <= 0) { return KTIMETRACKER_ERR_INVALID_DURATION; } // Find task TaskView *taskView = currentTaskView(); if (!taskView) { //FIXME: it mimics the behaviour with the for loop, but I am not sure semantics were right. Maybe a new error code must be defined? return KTIMETRACKER_ERR_UID_NOT_FOUND; } Task *task = nullptr; for (Task *t : taskView->getAllTasks()) { if (t->uid() == taskId) { task = t; break; } } if (!task) { return KTIMETRACKER_ERR_UID_NOT_FOUND; } task->changeTime(minutes, task->taskView()->storage()); return 0; } QString TimeTrackerWidget::error( int errorCode ) const { switch ( errorCode ) { case KTIMETRACKER_ERR_GENERIC_SAVE_FAILED: return i18n( "Save failed, most likely because the file could not be locked." ); case KTIMETRACKER_ERR_COULD_NOT_MODIFY_RESOURCE: return i18n( "Could not modify calendar resource." ); case KTIMETRACKER_ERR_MEMORY_EXHAUSTED: return i18n( "Out of memory--could not create object." ); case KTIMETRACKER_ERR_UID_NOT_FOUND: return i18n( "UID not found." ); case KTIMETRACKER_ERR_INVALID_DATE: return i18n( "Invalidate date--format is YYYY-MM-DD." ); case KTIMETRACKER_ERR_INVALID_TIME: return i18n( "Invalid time--format is YYYY-MM-DDTHH:MM:SS." ); case KTIMETRACKER_ERR_INVALID_DURATION: return i18n( "Invalid task duration--must be greater than zero." ); default: return i18n( "Invalid error number: %1", errorCode ); } } bool TimeTrackerWidget::isIdleDetectionPossible() const { bool result; auto *idletimedetector1=new IdleTimeDetector(50); result = idletimedetector1->isIdleDetectionPossible(); delete idletimedetector1; return result; } int TimeTrackerWidget::totalMinutesForTaskId( const QString &taskId ) const { TaskView *taskView = currentTaskView(); if (!taskView) { return -1; } for (Task *task : taskView->getAllTasks()) { if (task->uid() == taskId) { return task->totalTime(); } } return -1; } void TimeTrackerWidget::startTimerFor(const QString &taskId) { qDebug(); TaskView *taskView = currentTaskView(); if (!taskView) { return; } for (Task *task : taskView->getAllTasks()) { if (task->uid() == taskId) { taskView->startTimerFor(task); return; } } } bool TimeTrackerWidget::startTimerForTaskName( const QString &taskName ) { TaskView *taskView = currentTaskView(); if (!taskView) { return false; } for (Task *task : taskView->getAllTasks()) { if (task->name() == taskName ) { taskView->startTimerFor(task); return true; } } return false; } bool TimeTrackerWidget::stopTimerForTaskName(const QString &taskName) { TaskView *taskView = currentTaskView(); if (!taskView) { return false; } for (Task *task : taskView->getAllTasks()) { if (task->name() == taskName) { taskView->stopTimerFor(task); return true; } } return false; } void TimeTrackerWidget::stopTimerFor(const QString &taskId) { TaskView *taskView = currentTaskView(); if (!taskView) { return; } for (Task *task : taskView->getAllTasks()) { if (task->uid() == taskId) { taskView->stopTimerFor(task); return; } } } void TimeTrackerWidget::stopAllTimersDBUS() { TaskView *taskView = currentTaskView(); if (taskView) taskView->stopAllTimers(); } QString TimeTrackerWidget::exportCSVFile( const QString &filename, const QString &from, const QString &to, int type, bool decimalMinutes, bool allTasks, const QString &delimiter, const QString "e) { TaskView *taskView = currentTaskView(); if ( !taskView ) return ""; ReportCriteria rc; rc.url = filename; rc.from = QDate::fromString( from ); if ( rc.from.isNull() ) rc.from = QDate::fromString( from, Qt::ISODate ); rc.to = QDate::fromString( to ); if ( rc.to.isNull() ) rc.to = QDate::fromString( to, Qt::ISODate ); rc.reportType = ( ReportCriteria::REPORTTYPE )type; rc.decimalMinutes = decimalMinutes; rc.allTasks = allTasks; rc.delimiter = delimiter; rc.quote = quote; return taskView->report(rc); } void TimeTrackerWidget::importPlannerFile(const QString &filename) { TaskView *taskView = currentTaskView(); if (!taskView) { return; } taskView->importPlanner(filename); } bool TimeTrackerWidget::isActive(const QString &taskId) const { TaskView *taskView = currentTaskView(); if (!taskView) { return false; } for (Task *task : taskView->getAllTasks()) { if (task->uid() == taskId) { return task->isRunning(); } } return false; } bool TimeTrackerWidget::isTaskNameActive(const QString &taskName) const { TaskView *taskView = currentTaskView(); if (!taskView) { return false; } for (Task *task : taskView->getAllTasks()) { if (task->name() == taskName) { return task->isRunning(); } } return false; } QStringList TimeTrackerWidget::tasks() const { QStringList result; TaskView *taskView = currentTaskView(); if (!taskView) { return result; } for (Task *task : taskView->getAllTasks()) { result << task->name(); } return result; } QStringList TimeTrackerWidget::activeTasks() const { QStringList result; TaskView* taskView = currentTaskView(); if (!taskView) { return result; } for (Task *task : taskView->getAllTasks()) { if (task->isRunning()) { result << task->name(); } } return result; } void TimeTrackerWidget::saveAll() { currentTaskView()->save(); } void TimeTrackerWidget::quit() { auto* mainWindow = dynamic_cast(parent()->parent()); if (mainWindow) { mainWindow->quit(); } else { qCWarning(KTT_LOG) << "Cast to MainWindow failed"; } } bool TimeTrackerWidget::event(QEvent* event) // inherited from QWidget { if (event->type() == QEvent::QueryWhatsThis) { if ( m_taskView->count() == 0 ) { setWhatsThis(i18n("This is ktimetracker, KDE's program to help you track your time. Best, start with creating your first task - enter it into the field where you see \"search or add task\".")); } else { setWhatsThis(i18n("You have already created a task. You can now start and stop timing")); } } return QWidget::event(event); } // END of dbus slots group /* @} */