diff --git a/CMakeLists.txt b/CMakeLists.txt index f4f5915..5f6f3b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,54 +1,52 @@ cmake_minimum_required(VERSION 3.5) set(APP_VERSION_NUMBER "5.0.1") project(ktimetracker VERSION ${APP_VERSION_NUMBER}) set(KF5_MIN_VERSION "5.54.0") find_package(ECM ${KF5_MIN_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMAddAppIcon) include(ECMInstallIcons) include(ECMQtDeclareLoggingCategory) include(ECMSetupVersion) include(FeatureSummary) ecm_setup_version(${APP_VERSION_NUMBER} VARIABLE_PREFIX KTIMETRACKER VERSION_HEADER src/ktimetracker-version.h ) set(QT_REQUIRED_VERSION "5.10.0") find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED DBus Gui Widgets Xml) # TODO merge these two find_package(KF5) calls when KF 5.63.0 matures find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Config ConfigWidgets DBusAddons DocTools I18n IdleTime JobWidgets KIO Notifications WindowSystem XmlGui TextWidgets ) -find_package(KF5 5.63.0 REQUIRED COMPONENTS - CalendarCore -) +find_package(KF5CalendarCore 4.82.0 CONFIG REQUIRED) add_subdirectory(pics) add_subdirectory(icons) add_subdirectory(doc) add_subdirectory(src) install(FILES org.kde.ktimetracker.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/export/csvhistory.cpp b/src/export/csvhistory.cpp index 2a3f4e7..2b00195 100644 --- a/src/export/csvhistory.cpp +++ b/src/export/csvhistory.cpp @@ -1,174 +1,174 @@ /* * Copyright (C) 2003, 2004 by Mark Bucciarelli * Copyright (C) 2019 Alexander Potashev * * 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 "csvhistory.h" #include "ktimetrackerutility.h" #include "ktt_debug.h" #include "model/projectmodel.h" #include "model/task.h" #include "model/tasksmodel.h" -static int64_t todaySeconds(const QDate &date, const KCalendarCore::Event::Ptr &event) +static int64_t 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)); int64_t secsstartTillMidNight = startTime.secsTo(NextMidNight); int64_t 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; } QString exportCSVHistoryToString(ProjectModel *projectModel, const ReportCriteria &rc) { const QDate &from = rc.from; const QDate &to = rc.to; QString delim = rc.delimiter; const QString cr = QStringLiteral("\n"); const int64_t intervalLength = from.daysTo(to) + 1; QMap> secsForUid; QMap uidForName; QString retval; // 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 auto tasks = projectModel->tasksModel()->getAllTasks(); qCDebug(KTT_LOG) << "Tasks count: " << tasks.size(); for (Task *task : tasks) { 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; std::unique_ptr calendar = projectModel->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(); for (const auto &event : calendar->rawEventsForDate(mdate)) { qCDebug(KTT_LOG) << "Summary: " << event->summary() << ", Related to uid: " << event->relatedTo(); qCDebug(KTT_LOG) << "Today's seconds: " << todaySeconds(mdate, event); secsForUid[event->relatedTo()][from.daysTo(mdate)] += todaySeconds(mdate, event); } } // 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 < intervalLength; ++i) { retval.append(delim); retval.append(from.addDays(i).toString()); } retval.append(cr); // Rest of the CSV file QMapIterator nameUid(uidForName); double time; while (nameUid.hasNext()) { nameUid.next(); retval.append(rc.quote + nameUid.key() + rc.quote); qCDebug(KTT_LOG) << nameUid.key() << ": " << nameUid.value() << endl; for (int day = 0; day < intervalLength; day++) { qCDebug(KTT_LOG) << "Secs for day " << day << ":" << secsForUid[nameUid.value()][day]; retval.append(delim); time = secsForUid[nameUid.value()][day] / 60.0; retval.append(formatTime(time, rc.decimalMinutes)); } retval.append(cr); } qDebug() << "Retval is \n" << retval; return retval; } diff --git a/src/file/filecalendar.cpp b/src/file/filecalendar.cpp index be15003..7ddd6d4 100644 --- a/src/file/filecalendar.cpp +++ b/src/file/filecalendar.cpp @@ -1,81 +1,81 @@ /* * Copyright (C) 2012 by Sérgio Martins * Copyright (C) 2019 Alexander Potashev * * 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 "filecalendar.h" #include -#include +#include #include "icalformatkio.h" #include "ktt_debug.h" FileCalendar::FileCalendar(QUrl url) : m_url(std::move(url)) - , m_calendar(new KCalendarCore::MemoryCalendar(QTimeZone::systemTimeZone())) + , m_calendar(new KCalCore::MemoryCalendar(QTimeZone::systemTimeZone())) { } bool FileCalendar::reload() { m_calendar->close(); // delete all TODOs - KCalendarCore::FileStorage fileStorage(m_calendar, m_url.url(), new ICalFormatKIO()); + KCalCore::FileStorage fileStorage(m_calendar, m_url.url(), new ICalFormatKIO()); const bool result = fileStorage.load(); if (!result) { qCritical() << "FileCalendar::reload: problem loading calendar"; } return result; } bool FileCalendar::save() { - KCalendarCore::FileStorage fileStorage(m_calendar, m_url.url(), new ICalFormatKIO()); + KCalCore::FileStorage fileStorage(m_calendar, m_url.url(), new ICalFormatKIO()); const bool result = fileStorage.save(); if (!result) { qCritical() << "FileCalendar::save: problem saving calendar"; } return result; } -KCalendarCore::Event::List FileCalendar::rawEvents() const +KCalCore::Event::List FileCalendar::rawEvents() const { return m_calendar->rawEvents(); } -KCalendarCore::Event::List FileCalendar::rawEventsForDate(const QDate &date) const +KCalCore::Event::List FileCalendar::rawEventsForDate(const QDate &date) const { return m_calendar->rawEventsForDate(date); } -void FileCalendar::addTodo(const KCalendarCore::Todo::Ptr &todo) +void FileCalendar::addTodo(const KCalCore::Todo::Ptr &todo) { m_calendar->addTodo(todo); } -void FileCalendar::addEvent(const KCalendarCore::Event::Ptr &event) +void FileCalendar::addEvent(const KCalCore::Event::Ptr &event) { m_calendar->addEvent(event); } -KCalendarCore::Todo::List FileCalendar::rawTodos() const { +KCalCore::Todo::List FileCalendar::rawTodos() const { return m_calendar->rawTodos(); } diff --git a/src/file/filecalendar.h b/src/file/filecalendar.h index 14a7e3d..5f0c299 100644 --- a/src/file/filecalendar.h +++ b/src/file/filecalendar.h @@ -1,50 +1,50 @@ /* * Copyright (C) 2012 by Sérgio Martins * Copyright (C) 2019 Alexander Potashev * * 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_FILECALENDAR_H #define KTIMETRACKER_FILECALENDAR_H -#include +#include class FileCalendar { public: explicit FileCalendar(QUrl url); FileCalendar() = delete; ~FileCalendar() = default; bool reload(); bool save(); - void addTodo(const KCalendarCore::Todo::Ptr &todo); - KCalendarCore::Todo::List rawTodos() const; + void addTodo(const KCalCore::Todo::Ptr &todo); + KCalCore::Todo::List rawTodos() const; - void addEvent(const KCalendarCore::Event::Ptr &event); - KCalendarCore::Event::List rawEvents() const; - KCalendarCore::Event::List rawEventsForDate(const QDate &date) const; + void addEvent(const KCalCore::Event::Ptr &event); + KCalCore::Event::List rawEvents() const; + KCalCore::Event::List rawEventsForDate(const QDate &date) const; private: QUrl m_url; - KCalendarCore::MemoryCalendar::Ptr m_calendar; + KCalCore::MemoryCalendar::Ptr m_calendar; }; #endif // KTIMETRACKER_FILECALENDAR_H diff --git a/src/file/icalformatkio.cpp b/src/file/icalformatkio.cpp index 2679576..12e0792 100644 --- a/src/file/icalformatkio.cpp +++ b/src/file/icalformatkio.cpp @@ -1,127 +1,128 @@ /* * Copyright (c) 2019 Alexander Potashev * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . */ #include "icalformatkio.h" #include -#include +#include + #include #include #include "ktt_debug.h" -bool ICalFormatKIO::load(const KCalendarCore::Calendar::Ptr &calendar, const QString &urlString) +bool ICalFormatKIO::load(const KCalCore::Calendar::Ptr &calendar, const QString &urlString) { qCDebug(KTT_LOG) << "ICalFormatKIO::load:" << urlString; clearException(); QUrl url(urlString); if (url.isLocalFile()) { QFile file(url.toLocalFile()); if (!file.exists()) { // Local file does not exist return true; } // Local file exists if (!file.open(QIODevice::ReadOnly)) { qCWarning(KTT_LOG) << "load file open error: " << file.errorString() << ";filename=" << urlString; - setException(new KCalendarCore::Exception(KCalendarCore::Exception::LoadError)); + setException(new KCalCore::Exception(KCalCore::Exception::LoadError)); return false; } const QByteArray text = file.readAll().trimmed(); file.close(); if (text.isEmpty()) { // empty files are valid return true; } else { return fromRawString(calendar, text, false, urlString); } } else { // use remote file auto* const job = KIO::storedGet(url, KIO::Reload); KJobWidgets::setWindow(job, nullptr); // hide progress notification if (!job->exec()) { - setException(new KCalendarCore::Exception(KCalendarCore::Exception::SaveErrorSaveFile, QStringList(url.url()))); + setException(new KCalCore::Exception(KCalCore::Exception::SaveErrorSaveFile, QStringList(url.url()))); return false; } const QByteArray text = job->data(); if (text.isEmpty()) { // empty files are valid return true; } else { return fromRawString(calendar, text, false, urlString); } } } -bool ICalFormatKIO::save(const KCalendarCore::Calendar::Ptr &calendar, const QString &urlString) +bool ICalFormatKIO::save(const KCalCore::Calendar::Ptr &calendar, const QString &urlString) { qCDebug(KTT_LOG) << "ICalFormatKIO::save:" << urlString; clearException(); QString text = toString(calendar); if (text.isEmpty()) { // TODO: save empty file if we have 0 tasks? return false; } QByteArray textUtf8 = text.toUtf8(); // TODO: Write backup file (i.e. backup the existing file somewhere, e.g. to ~/.local/share/apps/ktimetracker/backups/) // const QString backupFile = urlString + QLatin1Char('~'); // QFile::remove(backupFile); // QFile::copy(urlString, backupFile); // save, either locally or remote QUrl url(urlString); if (url.isLocalFile()) { QSaveFile file(url.toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { qCWarning(KTT_LOG) << "save file open error: " << file.errorString() << ";local path" << file.fileName(); - setException(new KCalendarCore::Exception(KCalendarCore::Exception::SaveErrorOpenFile, QStringList(urlString))); + setException(new KCalCore::Exception(KCalCore::Exception::SaveErrorOpenFile, QStringList(urlString))); return false; } file.write(textUtf8.data(), textUtf8.size()); if (!file.commit()) { qCWarning(KTT_LOG) << "file finalize error:" << file.errorString() << ";local path" << file.fileName(); - setException(new KCalendarCore::Exception(KCalendarCore::Exception::SaveErrorSaveFile, QStringList(urlString))); + setException(new KCalCore::Exception(KCalCore::Exception::SaveErrorSaveFile, QStringList(urlString))); return false; } } else { // use remote file auto* const job = KIO::storedPut(textUtf8, url, -1, KIO::Overwrite); KJobWidgets::setWindow(job, nullptr); // hide progress notification if (!job->exec()) { - setException(new KCalendarCore::Exception(KCalendarCore::Exception::SaveErrorSaveFile, QStringList(url.url()))); + setException(new KCalCore::Exception(KCalCore::Exception::SaveErrorSaveFile, QStringList(url.url()))); qCWarning(KTT_LOG) << "save remote error: " << job->errorString(); return false; } } return true; } diff --git a/src/file/icalformatkio.h b/src/file/icalformatkio.h index 1396b41..ad47d8d 100644 --- a/src/file/icalformatkio.h +++ b/src/file/icalformatkio.h @@ -1,51 +1,51 @@ /* * Copyright (c) 2019 Alexander Potashev * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . */ #ifndef KTIMETRACKER_ICALFORMATKIO_H #define KTIMETRACKER_ICALFORMATKIO_H -#include +#include -class ICalFormatKIO : public KCalendarCore::ICalFormat +class ICalFormatKIO : public KCalCore::ICalFormat { public: ICalFormatKIO() = default; ~ICalFormatKIO() override = default; /** * Read calendar from local or remote file. * * @param calendar * @param urlString Must start with a schema, for example "file:///" or "https://" * @return */ - bool load(const KCalendarCore::Calendar::Ptr &calendar, const QString &urlString) override; + bool load(const KCalCore::Calendar::Ptr &calendar, const QString &urlString) override; /** * Write calendar to local or remote file. * * @param calendar * @param urlString Must start with a schema, for example "file:///" or "https://" * @return */ - bool save(const KCalendarCore::Calendar::Ptr &calendar, const QString &urlString) override; + bool save(const KCalCore::Calendar::Ptr &calendar, const QString &urlString) override; }; #endif // KTIMETRACKER_ICALFORMATKIO_H diff --git a/src/ktimetrackerutility.cpp b/src/ktimetrackerutility.cpp index 22fa321..aca00e8 100644 --- a/src/ktimetrackerutility.cpp +++ b/src/ktimetrackerutility.cpp @@ -1,62 +1,62 @@ /* * Copyright (C) 2007 by Thorsten Staerk * Copyright (C) 2019 Alexander Potashev * * 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 "ktimetrackerutility.h" #include #include QString formatTime(double minutes, bool decimal) { QString time; if (decimal) { time.sprintf("%.2f", minutes / 60.0); time.replace('.', QLocale().decimalPoint()); } else { const auto absMinutes = static_cast(std::round(std::fabs(minutes))); time.sprintf( "%s%ld:%02ld", minutes < 0 ? QString(QLocale().negativeSign()).toUtf8().data() : "", absMinutes / 60, absMinutes % 60); } return time; } -QString getCustomProperty(const KCalendarCore::Incidence::Ptr &incident, const QString &name) +QString getCustomProperty(const KCalCore::Incidence::Ptr &incident, const QString &name) { static const QByteArray eventAppName = QByteArray("ktimetracker"); static const QByteArray eventAppNameOld = QByteArray("karm"); const QByteArray nameArray = name.toLatin1(); // If a KDE-karm-* exists and not KDE-ktimetracker-*, change this. if ( incident->customProperty(eventAppName, nameArray).isNull() && !incident->customProperty(eventAppNameOld, nameArray).isNull()) { incident->setCustomProperty( eventAppName, nameArray, incident->customProperty(eventAppNameOld, nameArray)); } return incident->customProperty(eventAppName, nameArray); } diff --git a/src/ktimetrackerutility.h b/src/ktimetrackerutility.h index 03e88df..5118f9f 100644 --- a/src/ktimetrackerutility.h +++ b/src/ktimetrackerutility.h @@ -1,59 +1,59 @@ /* * Copyright (C) 2007 by Thorsten Staerk * Copyright (C) 2019 Alexander Potashev * * 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 KTIMETRACKERUTILITY_H #define KTIMETRACKERUTILITY_H #include -#include +#include /** Format time for output. All times output on screen or report output go through this function. If the second argument is true, the time is output as a two-place decimal. Otherwise the format is hh:mi. Examples: 30 seconds are 0.5 minutes. The output of formatTiMe(0.5,true) is 0.008333, because 0.5 minutes are 0.008333 hours. The output of formatTiMe(0.5,false) is 0:01, because 0.5 minutes are 0:01 hours rounded. */ QString formatTime(double minutes, bool decimal = false); const int secsPerMinute = 60; enum KTIMETRACKER_Errors { KTIMETRACKER_ERR_GENERIC_SAVE_FAILED = 1, KTIMETRACKER_ERR_COULD_NOT_MODIFY_RESOURCE, KTIMETRACKER_ERR_MEMORY_EXHAUSTED, KTIMETRACKER_ERR_UID_NOT_FOUND, KTIMETRACKER_ERR_INVALID_DATE, KTIMETRACKER_ERR_INVALID_TIME, KTIMETRACKER_ERR_INVALID_DURATION, KTIMETRACKER_MAX_ERROR_NO = KTIMETRACKER_ERR_INVALID_DURATION }; -QString getCustomProperty(const KCalendarCore::Incidence::Ptr &incident, const QString &name); +QString getCustomProperty(const KCalCore::Incidence::Ptr &incident, const QString &name); #endif diff --git a/src/model/event.cpp b/src/model/event.cpp index ce51555..9ae5bb9 100644 --- a/src/model/event.cpp +++ b/src/model/event.cpp @@ -1,170 +1,170 @@ /* * Copyright (c) 2019 Alexander Potashev * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . */ #include "event.h" #include #include "ktimetrackerutility.h" #include "ktt_debug.h" static const QByteArray eventAppName = QByteArray("ktimetracker"); -Event::Event(const KCalendarCore::Event::Ptr &event) +Event::Event(const KCalCore::Event::Ptr &event) : 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) { 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; } static int64_t durationInSeconds(const QDateTime &start, const QDateTime &end) { // Ignore milliseconds when calculating duration. // This will look like correct calculation for users who do not // see milliseconds in the UI. QDateTime roundedStart = QDateTime::fromSecsSinceEpoch(start.toSecsSinceEpoch()); QDateTime roundedEnd = QDateTime::fromSecsSinceEpoch(end.toSecsSinceEpoch()); return roundedStart.secsTo(roundedEnd); } void Event::updateDuration(QDateTime &changedDt, const QDateTime &otherDt) { if (m_duration < 0) { // Was a negative duration task if (m_dtEnd > m_dtStart) { // If range is not trivial or inversed, we assume that // event duration now becomes positive, thus we should update it here. m_duration = durationInSeconds(m_dtStart, m_dtEnd); } } else { // Was a regular task, and will stay regular. Calculate duration if possible. if (hasEndDate()) { m_duration = durationInSeconds(m_dtStart, m_dtEnd); if (m_duration < 0) { // We cannot make duration negative. // May be the user tried to make duration zero, let us help him. m_duration = 0; changedDt = otherDt; } } } } void Event::setDtStart(const QDateTime &dtStart) { m_dtStart = dtStart; updateDuration(m_dtStart, m_dtEnd); } QDateTime Event::dtStart() const { return m_dtStart; } void Event::setDtEnd(const QDateTime &dtEnd) { m_dtEnd = dtEnd; updateDuration(m_dtEnd, m_dtStart); } 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) { if (m_comments.isEmpty() || comment != m_comments.last()) { m_comments.append(comment); } } QStringList Event::comments() const { return m_comments; } void Event::setDuration(int64_t seconds) { m_duration = seconds; } int64_t Event::duration() const { return m_duration; } -KCalendarCore::Event::Ptr Event::asCalendarEvent(const KCalendarCore::Event::Ptr &event) const +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 (const 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 index a392883..ff3422e 100644 --- a/src/model/event.h +++ b/src/model/event.h @@ -1,81 +1,81 @@ /* * Copyright (c) 2019 Alexander Potashev * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . */ #ifndef KTIMETRACKER_EVENT_H #define KTIMETRACKER_EVENT_H -#include +#include /** * We have three types of events: * 1. Finished events of positive duration: * - dtStart and dtEnd are defined * - dtEnd >= dtStart * - duration >= 0 * 2. Unfinished events: * - dtStart is defined * - dtEnd is not defined, thus hasEndDate() returns false * 3. Events of negative duration: * - dtStart and dtEnd are defined * - dtEnd <= dtStart (TODO make dtEnd undefined in the case of negative duration?) * - duration < 0 */ class Event { public: - explicit Event(const KCalendarCore::Event::Ptr &event); + explicit Event(const KCalCore::Event::Ptr &event); 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(int64_t seconds); int64_t duration() const; /** * Load the event passed in with this event's info. */ - KCalendarCore::Event::Ptr asCalendarEvent(const KCalendarCore::Event::Ptr &event) const; + KCalCore::Event::Ptr asCalendarEvent(const KCalCore::Event::Ptr &event) const; private: void updateDuration(QDateTime &changedDt, const QDateTime &otherDt); QString m_summary; QDateTime m_dtStart; QDateTime m_dtEnd; QString m_uid; QString m_relatedTo; QStringList m_comments; int64_t m_duration; }; #endif // KTIMETRACKER_EVENT_H diff --git a/src/model/eventsmodel.cpp b/src/model/eventsmodel.cpp index f57de91..43d66af 100644 --- a/src/model/eventsmodel.cpp +++ b/src/model/eventsmodel.cpp @@ -1,200 +1,200 @@ /* * Copyright (c) 2019 Alexander Potashev * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . */ #include "eventsmodel.h" #include #include "ktt_debug.h" #include "task.h" -void EventsModel::load(const KCalendarCore::Event::List &events) +void EventsModel::load(const KCalCore::Event::List &events) { clear(); for (const auto& event : events) { m_events.append(new Event(event)); } } EventsModel::~EventsModel() { clear(); } QList EventsModel::events() const { return m_events; } QList EventsModel::eventsForTask(const 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); } -static KCalendarCore::Event::Ptr baseEvent(const Task *task) +static KCalCore::Event::Ptr baseEvent(const Task *task) { qCDebug(KTT_LOG) << "Entering function"; - KCalendarCore::Event::Ptr e(new KCalendarCore::Event()); + 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; } void EventsModel::changeTime(const Task* task, int64_t deltaSeconds) { qCDebug(KTT_LOG) << "Entering function; deltaSeconds=" << deltaSeconds; QDateTime end; auto kcalEvent = baseEvent(task); auto *e = new Event(kcalEvent); // 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->setDuration(deltaSeconds); addEvent(e); } bool EventsModel::bookTime(const Task* task, const QDateTime& startDateTime, int64_t durationInSeconds) { qCDebug(KTT_LOG) << "Entering function"; auto kcalEvent = baseEvent(task); auto *e = new Event(kcalEvent); e->setDtStart(startDateTime); e->setDtEnd(startDateTime.addSecs( durationInSeconds)); // Use a custom property to keep a record of negative durations e->setDuration(durationInSeconds); addEvent(e); return true; } void EventsModel::startTask(const Task *task) { auto *e = new Event(baseEvent(task)); // TODO support passing of a different start time from TimeTrackerStorage::buildTaskView? e->setDtStart(QDateTime::currentDateTime()); addEvent(e); } void EventsModel::stopTask(const Task *task, const QDateTime &when) { QList openEvents; for (auto *event : eventsForTask(task)) { if (!event->hasEndDate()) { openEvents.append(event); } } if (openEvents.isEmpty()) { qCWarning(KTT_LOG) << "Task to stop has no open events, uid=" << task->uid() << ", name=" << task->name(); } else if (openEvents.size() > 1) { qCWarning(KTT_LOG) << "Task to stop has multiple open events (" << openEvents.size() << "), uid=" << task->uid() << ", name=" << task->name(); } for (auto *event : openEvents) { qCDebug(KTT_LOG) << "found an event for task, event=" << event->uid(); event->setDtEnd(when); } } diff --git a/src/model/eventsmodel.h b/src/model/eventsmodel.h index fe45080..9c5b7b7 100644 --- a/src/model/eventsmodel.h +++ b/src/model/eventsmodel.h @@ -1,106 +1,106 @@ /* * Copyright (c) 2019 Alexander Potashev * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . */ #ifndef KTIMETRACKER_EVENTSMODEL_H #define KTIMETRACKER_EVENTSMODEL_H -#include +#include #include "event.h" class Task; class EventsModel : public QObject { Q_OBJECT public: EventsModel() = default; ~EventsModel() override; - void load(const KCalendarCore::Event::List &events); + void load(const KCalCore::Event::List &events); QList events() const; QList eventsForTask(const 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); /** * 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 (positive 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 possible * 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, int64_t 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, int64_t durationInSeconds); void startTask(const Task *task); void stopTask(const Task *task, const QDateTime &when); Q_SIGNALS: void timesChanged(); private: QList m_events; }; #endif // KTIMETRACKER_EVENTSMODEL_H diff --git a/src/model/projectmodel.cpp b/src/model/projectmodel.cpp index 1cd270f..6b2638d 100644 --- a/src/model/projectmodel.cpp +++ b/src/model/projectmodel.cpp @@ -1,141 +1,141 @@ /* * Copyright (c) 2019 Alexander Potashev * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . */ #include "projectmodel.h" #include "event.h" #include "eventsmodel.h" #include "import/plannerparser.h" #include "ktt_debug.h" #include "task.h" #include "tasksmodel.h" ProjectModel::ProjectModel() : m_tasksModel(new TasksModel()) , m_eventsModel(new EventsModel()) { } TasksModel *ProjectModel::tasksModel() { return m_tasksModel; } EventsModel *ProjectModel::eventsModel() { return m_eventsModel; } std::unique_ptr ProjectModel::asCalendar(const QUrl &url) const { std::unique_ptr calendar(new FileCalendar(url)); for (auto *item : m_tasksModel->getAllItems()) { Task *task = dynamic_cast(item); - KCalendarCore::Todo::Ptr todo(new KCalendarCore::Todo()); + 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()) { - KCalendarCore::Event::Ptr calEvent(new KCalendarCore::Event()); + KCalCore::Event::Ptr calEvent(new KCalCore::Event()); calendar->addEvent(event->asCalendarEvent(calEvent)); } return calendar; } void ProjectModel::resetTimeForAllTasks() { for (Task *task : tasksModel()->getAllTasks()) { task->resetTimes(); } eventsModel()->clear(); } void ProjectModel::importPlanner(const QString &fileName, Task *currentTask) { auto *handler = new PlannerParser(this, currentTask); QFile xmlFile(fileName); QXmlInputSource source(&xmlFile); QXmlSimpleReader reader; reader.setContentHandler(handler); reader.parse(source); refresh(); } void ProjectModel::refresh() { for (Task *task : tasksModel()->getAllTasks()) { task->invalidateCompletedState(); task->update(); // maybe there was a change in the times's format } } void ProjectModel::refreshTimes() { // This procedure resets all times (session and overall) for all tasks and subtasks. // Reset session and total time for all tasks - do not touch the storage. for (Task *task : tasksModel()->getAllTasks()) { task->resetTimes(); } for (Task *task : tasksModel()->getAllTasks()) { // get all events for task for (const auto *event : eventsModel()->eventsForTask(task)) { QDateTime eventStart = event->dtStart(); QDateTime eventEnd = event->dtEnd(); const int64_t duration = event->duration() / 60; task->addTime(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 task->addSessionTime(duration); } } else { // so there is no session at all task->addSessionTime(duration); } } } // Recalculate total times after changing hierarchy by drag&drop for (Task *task : tasksModel()->getAllTasks()) { // Start recursive method recalculateTotalTimesSubtree() for each top-level task. if (task->isRoot()) { task->recalculateTotalTimesSubtree(); } } refresh(); } diff --git a/src/model/task.cpp b/src/model/task.cpp index 36202d7..106e719 100644 --- a/src/model/task.cpp +++ b/src/model/task.cpp @@ -1,586 +1,586 @@ /* * Copyright (C) 1997 by Stephan Kulow * Copyright (C) 2019 Alexander Potashev * * 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 "ktimetracker.h" #include "ktimetrackerutility.h" #include "ktt_debug.h" #include "model/eventsmodel.h" #include "model/projectmodel.h" #include "model/tasksmodel.h" #include "timetrackerstorage.h" static const QByteArray eventAppName = QByteArray("ktimetracker"); Task::Task(const QString& taskName, const QString& taskDescription, int64_t minutes, int64_t sessionTime, const DesktopList& desktops, ProjectModel *projectModel, Task *parentTask) : TasksModelItem(projectModel->tasksModel(), parentTask) , m_projectModel(projectModel) { if (parentTask) { parentTask->addChild(this); } else { m_projectModel->tasksModel()->addChild(this); } init(taskName, taskDescription, minutes, sessionTime, nullptr, desktops, 0, 0); - m_uid = KCalendarCore::CalFormat::createUniqueId(); + m_uid = KCalCore::CalFormat::createUniqueId(); } -Task::Task(const KCalendarCore::Todo::Ptr &todo, ProjectModel *projectModel) +Task::Task(const KCalCore::Todo::Ptr &todo, ProjectModel *projectModel) : TasksModelItem(projectModel->tasksModel(), nullptr) , m_projectModel(projectModel) { projectModel->tasksModel()->addChild(this); int64_t minutes = 0; QString name; QString description; int64_t 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); } Task::~Task() { disconnectFromParent(); } int Task::depth() { int res = 0; for (Task* t = parentTask(); t; t = t->parentTask()) { res++; } qCDebug(KTT_LOG) << "Leaving function. depth is:" << res; return res; } void Task::init( const QString& taskName, const QString& taskDescription, int64_t minutes, int64_t sessionTime, const QString& sessionStartTiMe, const DesktopList& desktops, int percent_complete, int priority) { m_isRunning = false; m_name = taskName.trimmed(); m_description = taskDescription.trimmed(); m_lastStart = QDateTime::currentDateTime(); m_totalTime = m_time = minutes; m_totalSessionTime = m_sessionTime = sessionTime; m_desktops = desktops; m_percentComplete = percent_complete; m_priority = priority; m_sessionStartTime = QDateTime::fromString(sessionStartTiMe); update(); changeParentTotalTimes(m_sessionTime, m_time); } 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, const QDateTime& when) { if (on != m_isRunning) { m_isRunning = on; invalidateRunningState(); if (on) { m_lastStart = when; qCDebug(KTT_LOG) << "task has been started for " << when; // m_projectModel->eventsModel()->startTask(this, when); m_projectModel->eventsModel()->startTask(this); } else { m_projectModel->eventsModel()->stopTask(this, 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(); } } bool Task::isRunning() const { return m_isRunning; } void Task::setName(const QString& name) { qCDebug(KTT_LOG) << "Entering function, name=" << name; QString oldname = m_name; if (oldname != name) { m_name = name; update(); } } void Task::setDescription(const QString& description) { qCDebug(KTT_LOG) << "Entering function, description=" << description; QString olddescription = m_description; if (olddescription != description) { m_description = description; update(); } } void Task::setPercentComplete(int percent) { qCDebug(KTT_LOG) << "Entering function(" << percent << "):" << m_uid; if (!percent) { m_percentComplete = 0; } else if (percent > 100) { m_percentComplete = 100; } else if (percent < 0) { m_percentComplete = 0; } else { m_percentComplete = percent; } if (isRunning() && m_percentComplete == 100) { emit m_projectModel->tasksModel()->taskCompleted(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 (m_percentComplete == 100) { for (int i = 0; i < childCount(); ++i) { Task *task = dynamic_cast(child(i)); task->setPercentComplete(m_percentComplete); } } // 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; } m_priority = priority; update(); } bool Task::isComplete() { return m_percentComplete == 100; } void Task::setDesktopList(const DesktopList& desktopList) { m_desktops = desktopList; } void Task::addTime(int64_t minutes) { m_time += minutes; this->addTotalTime(minutes); } void Task::addTotalTime(int64_t minutes) { m_totalTime += minutes; if (parentTask()) { parentTask()->addTotalTime(minutes); } } void Task::addSessionTime(int64_t minutes) { m_sessionTime += minutes; this->addTotalSessionTime(minutes); } void Task::addTotalSessionTime(int64_t minutes) { m_totalSessionTime += minutes; if (parentTask()) { parentTask()->addTotalSessionTime(minutes); } } QString Task::setTime(int64_t minutes) { m_time = minutes; m_totalTime += minutes; return QString(); } void Task::recalculateTotalTimesSubtree() { int64_t totalMinutes = time(); int64_t totalSessionMinutes = sessionTime(); for (int i = 0; i < this->childCount(); ++i) { Task *subTask = dynamic_cast(child(i)); subTask->recalculateTotalTimesSubtree(); totalMinutes += subTask->totalTime(); totalSessionMinutes += subTask->totalSessionTime(); } setTotalTime(totalMinutes); setTotalSessionTime(totalSessionMinutes); } QString Task::setSessionTime(int64_t minutes) { m_sessionTime = minutes; m_totalSessionTime += minutes; return QString(); } void Task::changeTimes(int64_t minutesSession, int64_t minutes, EventsModel *eventsModel) { qDebug() << "Task's sessionStartTiMe is " << m_sessionStartTime; if (minutesSession != 0 || minutes != 0) { m_sessionTime += minutesSession; m_time += minutes; if (eventsModel) { eventsModel->changeTime(this, minutes * secsPerMinute); } changeTotalTimes(minutesSession, minutes); } } void Task::changeTime(int64_t minutes, EventsModel *eventsModel) { changeTimes(minutes, minutes, eventsModel); } void Task::changeTotalTimes(int64_t minutesSession, int64_t minutes) { qCDebug(KTT_LOG) << "Task::changeTotalTimes(" << minutesSession << "," << minutes << ") for" << name(); m_totalSessionTime += minutesSession; m_totalTime += minutes; update(); changeParentTotalTimes(minutesSession, minutes); } void Task::resetTimes() { m_totalSessionTime -= m_sessionTime; m_totalTime -= m_time; changeParentTotalTimes(-m_sessionTime, -m_time); m_sessionTime = 0; m_time = 0; update(); } void Task::changeParentTotalTimes(int64_t minutesSession, int64_t minutes) { if (parentTask()) { parentTask()->changeTotalTimes(minutesSession, minutes); } } bool Task::remove() { qCDebug(KTT_LOG) << "entering function" << m_name; bool ok = true; for (int i = 0; i < childCount(); ++i) { Task* task = dynamic_cast(child(i)); if (!task) { qFatal("Task::remove: task is nullptr"); } task->remove(); } setRunning(false); m_projectModel->eventsModel()->removeAllForTask(this); changeParentTotalTimes(-m_sessionTime, -m_time); return ok; } QString Task::fullName() const { if (isRoot()) { return name(); } else { return parentTask()->fullName() + QString::fromLatin1("/") + name(); } } -KCalendarCore::Todo::Ptr Task::asTodo(const KCalendarCore::Todo::Ptr& todo) const +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(m_time)); todo->setCustomProperty(eventAppName, QByteArray("totalSessionTime"), QString::number(m_sessionTime)); todo->setCustomProperty(eventAppName, QByteArray("sessionStartTiMe"), m_sessionStartTime.toString()); qDebug() << "m_sessionStartTime=" << m_sessionStartTime.toString(); if (getDesktopStr().isEmpty()) { todo->removeCustomProperty(eventAppName, QByteArray("desktopList")); } else { todo->setCustomProperty(eventAppName, QByteArray("desktopList"), getDesktopStr()); } todo->setPercentComplete(m_percentComplete); todo->setPriority( m_priority ); if (parentTask()) { todo->setRelatedTo(parentTask()->uid()); } return todo; } bool Task::parseIncidence( - const KCalendarCore::Incidence::Ptr &incident, int64_t& minutes, + const KCalCore::Incidence::Ptr &incident, int64_t& minutes, int64_t& 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(); m_uid = incident->uid(); 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(); + percent_complete = incident.staticCast()->percentComplete(); priority = incident->priority(); return true; } QString Task::getDesktopStr() const { if (m_desktops.empty()) { return QString(); } QString desktopsStr; for (const int desktop : m_desktops) { 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(-m_totalSessionTime, -m_totalTime); if (!parentTask()) { m_projectModel->tasksModel()->takeTopLevelItem(m_projectModel->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(m_totalSessionTime, m_totalTime); } // 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 { switch (role) { case Qt::DisplayRole: { bool b = KTimeTrackerSettings::decimalFormat(); switch (column) { case 0: return m_name; case 1: return formatTime(m_sessionTime, b); case 2: return formatTime(m_time, b); case 3: return formatTime(m_totalSessionTime, b); case 4: return formatTime(m_totalTime, b); case 5: return m_priority > 0 ? QString::number(m_priority) : QStringLiteral("--"); case 6: return QString::number(m_percentComplete); default: return {}; } } case SortRole: { // QSortFilterProxyModel::lessThan() supports comparison of a few data types, // here we use some of those: QString, qlonglong, int. switch (column) { case 0: return m_name; case 1: return QVariant::fromValue(m_sessionTime); case 2: return QVariant::fromValue(m_time); case 3: return QVariant::fromValue(m_totalSessionTime); case 4: return QVariant::fromValue(m_totalTime); case 5: return QVariant::fromValue(m_priority); case 6: return QVariant::fromValue(m_percentComplete); default: return {}; } } default: return {}; } } // Update a row, containing one task void Task::update() { QModelIndex first = m_projectModel->tasksModel()->index(this, 0); QModelIndex last = m_projectModel->tasksModel()->index(this, 6); emit m_projectModel->tasksModel()->dataChanged(first, last, QVector{Qt::DisplayRole}); } void Task::startNewSession() { changeTimes(-m_sessionTime, 0, nullptr); m_sessionStartTime = QDateTime::currentDateTime(); } //BEGIN Properties QString Task::uid() const { return m_uid; } int Task::percentComplete() const { return m_percentComplete; } int Task::priority() const { return m_priority; } QString Task::name() const { return m_name; } QString Task::description() const { return m_description; } QDateTime Task::startTime() const { return m_lastStart; } QDateTime Task::sessionStartTiMe() const { return m_sessionStartTime; } DesktopList Task::desktops() const { return m_desktops; } //END diff --git a/src/model/task.h b/src/model/task.h index 5cfe767..6ba00e9 100644 --- a/src/model/task.h +++ b/src/model/task.h @@ -1,348 +1,348 @@ /* * Copyright (C) 1997 by Stephan Kulow * Copyright (C) 2019 Alexander Potashev * * 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 "desktoplist.h" // Required b/c DesktopList is a typedef not a class. #include "model/tasksmodelitem.h" class TimeTrackerStorage; class ProjectModel; 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, stopped 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 TasksModelItem { public: enum { SortRole = Qt::UserRole, }; Task( const QString& taskname, const QString& taskdescription, int64_t minutes, int64_t sessionTime, const DesktopList& desktops, ProjectModel *projectModel, Task* parentTask); - Task(const KCalendarCore::Todo::Ptr &todo, ProjectModel *projectModel); + Task(const KCalCore::Todo::Ptr &todo, ProjectModel *projectModel); /* destructor */ ~Task() override; Task* parentTask() const { return dynamic_cast(TasksModelItem::parent()); } /** Return unique iCalendar Todo ID for this task. */ QString uid() const; /** * Deliver the depth of a task, i.e. how many tasks are supertasks to it. * A toplevel task has the depth 0. */ int depth(); void delete_recursive(); /** 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(int64_t minutes, EventsModel *eventsModel); /** * 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(int64_t minutesSession, int64_t minutes, EventsModel *eventsModel); /** 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(int64_t minutesSession, int64_t 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 */ void addTime(int64_t 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 */ void addTotalTime(int64_t 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 */ void addSessionTime(int64_t 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 */ void addTotalSessionTime(int64_t minutes); /** Sets the time (not session time). This does not add an event. * * @param minutes minutes to set time to * */ QString setTime(int64_t 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(int64_t minutes) { m_totalTime = 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(int64_t minutes) { m_totalSessionTime = minutes; } /** A recursive function to calculate the total time/session time of a task. */ void recalculateTotalTimesSubtree(); /** 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(int64_t minutes); /** * Reset all times to 0 and adjust parent task's totalTiMes. */ void resetTimes(); /** @return time in minutes */ int64_t time() const { return m_time; } /** @return total time in minutes */ int64_t totalTime() const { return m_totalTime; } int64_t sessionTime() const { return m_sessionTime; } int64_t totalSessionTime() const { return m_totalSessionTime; } 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. */ void setName(const QString &name); /** 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 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, 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 KCalendarCore::Incidence::Ptr &, int64_t& minutes, + const KCalCore::Incidence::Ptr &, int64_t& minutes, int64_t& sessionMinutes, QString& sessionStartTiMe, QString& name, QString& description, DesktopList& desktops, int& percent_complete, int& priority); /** * Load the todo passed in with this tasks info. */ - KCalendarCore::Todo::Ptr asTodo(const KCalendarCore::Todo::Ptr& todo) const; + KCalCore::Todo::Ptr asTodo(const KCalCore::Todo::Ptr& todo) 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. */ bool remove(); /** * 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(int64_t minutesSession, int64_t minutes); private: /** initialize a task */ void init( const QString& taskname, const QString& taskdescription, int64_t minutes, int64_t sessionTime, const QString& sessionStartTiMe, const DesktopList& desktops, int percent_complete, int priority); bool m_isRunning; /** The iCal unique ID of the Todo for this task. */ QString m_uid; int m_percentComplete; /** task name */ QString m_name; /** task description */ QString m_description; /** Last time this task was started. */ QDateTime m_lastStart; /** totals of the whole subtree including self */ int64_t m_totalTime; int64_t m_totalSessionTime; /** times spend on the task itself */ int64_t m_time; int64_t m_sessionTime; /** time when the session was started */ QDateTime m_sessionStartTime; DesktopList m_desktops; /** Priority of the task. */ int m_priority; ProjectModel *m_projectModel; }; #endif // KTIMETRACKER_TASK_H diff --git a/src/tests/storagetest.cpp b/src/tests/storagetest.cpp index 7f5bf36..b6dbac1 100644 --- a/src/tests/storagetest.cpp +++ b/src/tests/storagetest.cpp @@ -1,109 +1,109 @@ /* * Copyright (c) 2019 Alexander Potashev * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . */ #include -#include -#include +#include +#include #include "taskview.h" #include "model/task.h" #include "helpers.h" class StorageTest : public QObject { Q_OBJECT private Q_SLOTS: void testSaveEmpty(); void testSaveSimpleTree(); void testLockPath(); }; void StorageTest::testSaveEmpty() { auto *taskView = createTaskView(this, false); QVERIFY(taskView); QCOMPARE(taskView->storage()->save(), ""); QCOMPARE(readTextFile(taskView->storage()->fileUrl().toLocalFile()), readTextFile(QFINDTESTDATA("data/empty.ics"))); } void StorageTest::testSaveSimpleTree() { auto *taskView = createTaskView(this, true); QVERIFY(taskView); Task* negativeTask = taskView->addTask("negative duration"); negativeTask->changeTime(-5, taskView->storage()->eventsModel()); // subtract 5 minutes QCOMPARE(taskView->storage()->save(), ""); - KCalendarCore::MemoryCalendar::Ptr calendar(new KCalendarCore::MemoryCalendar(QTimeZone::systemTimeZone())); - KCalendarCore::FileStorage fileStorage(calendar, taskView->storage()->fileUrl().toLocalFile(), new KCalendarCore::ICalFormat()); + KCalCore::MemoryCalendar::Ptr calendar(new KCalCore::MemoryCalendar(QTimeZone::systemTimeZone())); + KCalCore::FileStorage fileStorage(calendar, taskView->storage()->fileUrl().toLocalFile(), new KCalCore::ICalFormat()); QVERIFY(fileStorage.load()); - auto todos = calendar->rawTodos(KCalendarCore::TodoSortSummary); + auto todos = calendar->rawTodos(KCalCore::TodoSortSummary); QCOMPARE(todos.size(), 4); QCOMPARE(todos[0]->summary(), "1"); QCOMPARE(todos[0]->customProperty("ktimetracker", "totalSessionTime"), "5"); QCOMPARE(todos[0]->customProperty("ktimetracker", "totalTaskTime"), "5"); QCOMPARE(todos[1]->summary(), "2"); QCOMPARE(todos[1]->relatedTo(), todos[0]->uid()); QCOMPARE(todos[3]->summary(), "negative duration"); QCOMPARE(todos[3]->customProperty("ktimetracker", "totalSessionTime"), "-5"); QCOMPARE(todos[3]->customProperty("ktimetracker", "totalTaskTime"), "-5"); - auto events = calendar->rawEvents(KCalendarCore::EventSortSummary); + auto events = calendar->rawEvents(KCalCore::EventSortSummary); QCOMPARE(events.size(), 4); QCOMPARE(events[0]->summary(), "1"); QCOMPARE(events[0]->customProperty("ktimetracker", "duration"), "300"); QCOMPARE(events[0]->relatedTo(), todos[0]->uid()); QCOMPARE(events[0]->categoriesStr(), "KTimeTracker"); QCOMPARE(events[3]->summary(), "negative duration"); QCOMPARE(events[3]->customProperty("ktimetracker", "duration"), "-300"); QCOMPARE(events[3]->relatedTo(), todos[3]->uid()); } void StorageTest::testLockPath() { #ifdef Q_OS_WIN // SHA-1 of "C:/var/hello.ics" const QString expectedSuffix1("/ktimetracker_a1a9ea47d71476676afa881247fcd8ae915788b5_hello.ics.lock"); #else // SHA-1 of "/var/hello.ics" const QString expectedSuffix1("/ktimetracker_290fa23d3a268915fba5b2b2e5818aec381b7044_hello.ics.lock"); #endif QVERIFY(TimeTrackerStorage::createLockFileName(QUrl::fromLocalFile("/tmp/../var/hello.ics")).endsWith( expectedSuffix1)); // SHA-1 of "https://abc/1/привет world" QVERIFY(TimeTrackerStorage::createLockFileName(QUrl("https://abc/1/привет%20world")).endsWith( QDir().absoluteFilePath("/ktimetracker_7048956ad41735da76ae0da13d116b500753ba9e_привет world.lock"))); } QTEST_MAIN(StorageTest) #include "storagetest.moc" diff --git a/src/timetrackerstorage.cpp b/src/timetrackerstorage.cpp index 674ae8c..7fd5715 100644 --- a/src/timetrackerstorage.cpp +++ b/src/timetrackerstorage.cpp @@ -1,309 +1,309 @@ /* * Copyright (C) 2003, 2004 by Mark Bucciarelli * Copyright (C) 2019 Alexander Potashev * * 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 "ktt_debug.h" #include "model/eventsmodel.h" #include "model/projectmodel.h" #include "model/task.h" #include "model/tasksmodel.h" #include "taskview.h" #include "widgets/taskswidget.h" const QByteArray eventAppName = QByteArray("ktimetracker"); TimeTrackerStorage::TimeTrackerStorage() : m_model(nullptr) , m_taskView(nullptr) { } // 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.toLocalFile())) { KDirWatch::self()->removeFile(m_url.toLocalFile()); removedFromDirWatch = true; } // If same file, don't reload if (url == m_url) { if (removedFromDirWatch) { KDirWatch::self()->addFile(m_url.toLocalFile()); } return QString(); } if (m_model) { closeStorage(); } m_model = new ProjectModel(); if (url.isLocalFile()) { connect(KDirWatch::self(), &KDirWatch::dirty, this, &TimeTrackerStorage::onFileModified); if (!KDirWatch::self()->contains(url.toLocalFile())) { KDirWatch::self()->addFile(url.toLocalFile()); } } // Create local file resource and add to resources m_url = url; FileCalendar m_calendar(m_url); m_taskView = view; // m_calendar->setTimeSpec( KSystemTimeZones::local() ); m_calendar.reload(); // Build task view from iCal data QString err; eventsModel()->load(m_calendar.rawEvents()); err = buildTaskView(m_calendar.rawTodos(), view); if (removedFromDirWatch) { KDirWatch::self()->addFile(m_url.toLocalFile()); } return err; } QUrl TimeTrackerStorage::fileUrl() { return m_url; } -QString TimeTrackerStorage::buildTaskView(const KCalendarCore::Todo::List &todos, TaskView *view) +QString TimeTrackerStorage::buildTaskView(const KCalCore::Todo::List &todos, 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 : tasksModel()->getActiveTasks()) { runningTasks.append(task->uid()); startTimes.append(task->startTime()); } tasksModel()->clear(); QMultiHash map; for (const auto &todo : todos) { Task* task = new Task(todo, m_model); map.insert(todo->uid(), task); task->invalidateCompletedState(); } // 1.1. Load each task under its parent task. QString err; for (const auto &todo : todos) { 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); } } } // restart tasks that have been running with their start times for (Task *task : tasksModel()->getAllTasks()) { for (int n = 0; n < runningTasks.count(); ++n) { if (runningTasks[n] == task->uid()) { view->startTimerFor(task, startTimes[n]); } } } return err; } void TimeTrackerStorage::closeStorage() { if (m_model) { delete m_model; m_model = nullptr; } } EventsModel *TimeTrackerStorage::eventsModel() { if (!m_model) { qFatal("TimeTrackerStorage::eventsModel is nullptr"); } return m_model->eventsModel(); } TasksModel *TimeTrackerStorage::tasksModel() { if (!m_model) { qFatal("TimeTrackerStorage::tasksModel is nullptr"); } return m_model->tasksModel(); } ProjectModel *TimeTrackerStorage::projectModel() { if (!m_model) { qFatal("TimeTrackerStorage::projectModel is nullptr"); } return m_model; } bool TimeTrackerStorage::allEventsHaveEndTime(Task *task) { for (const auto *event : m_model->eventsModel()->eventsForTask(task)) { if (!event->hasEndDate()) { return false; } } return true; } // static QString TimeTrackerStorage::createLockFileName(const QUrl& url) { QString canonicalSeedString; QString baseName; if (url.isLocalFile()) { QFileInfo fileInfo(url.toLocalFile()); canonicalSeedString = fileInfo.absoluteFilePath(); baseName = fileInfo.fileName(); } else { canonicalSeedString = url.url(); baseName = QFileInfo(url.path()).completeBaseName(); } // Report failure if canonicalSeedString is empty. if (canonicalSeedString.isEmpty()) { return QString(); } // Remove characters disallowed by operating systems baseName.replace(QRegExp("[" + QRegExp::escape("\\/:*?\"<>|") + "]"), QString()); const QString& hash = QCryptographicHash::hash(canonicalSeedString.toUtf8(), QCryptographicHash::Sha1).toHex(); const QString& lockBaseName = QStringLiteral("ktimetracker_%1_%2.lock").arg(hash).arg(baseName); // Put the lock file in a directory for temporary files return QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).absoluteFilePath(lockBaseName); } QString TimeTrackerStorage::save() { bool removedFromDirWatch = false; if (KDirWatch::self()->contains(m_url.toLocalFile())) { KDirWatch::self()->removeFile(m_url.toLocalFile()); removedFromDirWatch = true; } if (!m_model) { qCWarning(KTT_LOG) << "TimeTrackerStorage::save: m_model is nullptr"; // No i18n() here because it's too technical and unlikely to happen return QStringLiteral("m_model is nullptr"); } const QString& fileLockPath = createLockFileName(m_url); QLockFile fileLock(fileLockPath); if (!fileLock.lock()) { qCWarning(KTT_LOG) << "TimeTrackerStorage::save: m_fileLock->lock() failed"; return i18nc("%1=lock file path", "Could not write lock file \"%1\". Disk full?", fileLockPath); } QString errorMessage; std::unique_ptr calendar = m_model->asCalendar(m_url); if (!calendar->save()) { qCWarning(KTT_LOG) << "TimeTrackerStorage::save: calendar->save() failed"; errorMessage = i18nc("%1=destination file path/URL", "Failed to save iCalendar file as \"%1\".", m_url.toString()); } else { qCDebug(KTT_LOG) << "TimeTrackerStorage::save: wrote tasks to" << m_url; } fileLock.unlock(); if (removedFromDirWatch) { KDirWatch::self()->addFile(m_url.toLocalFile()); } return errorMessage; } //---------------------------------------------------------------------------- bool TimeTrackerStorage::bookTime(const Task* task, const QDateTime& startDateTime, int64_t durationInSeconds) { return eventsModel()->bookTime(task, startDateTime, durationInSeconds); } void TimeTrackerStorage::onFileModified() { if (!m_model) { qCWarning(KTT_LOG) << "TaskView::onFileModified(): model is null"; return; } // TODO resolve conflicts if KTimeTracker has unsaved changes in its data structures qCDebug(KTT_LOG) << "entering function"; FileCalendar m_calendar(m_url); // m_calendar->setTimeSpec( KSystemTimeZones::local() ); m_calendar.reload(); buildTaskView(m_calendar.rawTodos(), m_taskView); m_taskView->storage()->projectModel()->refresh(); m_taskView->tasksWidget()->refresh(); qCDebug(KTT_LOG) << "exiting onFileModified"; } diff --git a/src/timetrackerstorage.h b/src/timetrackerstorage.h index d2c89d3..40ab58b 100644 --- a/src/timetrackerstorage.h +++ b/src/timetrackerstorage.h @@ -1,151 +1,151 @@ /* * Copyright (C) 2003 by Mark Bucciarelli * Copyright (C) 2019 Alexander Potashev * * 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 +#include #include "reportcriteria.h" #include "desktoplist.h" #include "file/filecalendar.h" QT_BEGIN_NAMESPACE class QDateTime; class QLockFile; QT_END_NAMESPACE class ProjectModel; 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 = default; /** 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 KCalendarCore::Todo::List &todos, TaskView *view); + QString buildTaskView(const KCalCore::Todo::List &todos, TaskView *view); /** Close calendar and clear view. Release lock if holding one. */ void closeStorage(); bool isLoaded() const { return m_model; } /** list of all events */ EventsModel *eventsModel(); TasksModel *tasksModel(); ProjectModel *projectModel(); /** * Deliver if all events of a task have an endtime * * If ktimetracker has been quit 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); /** * 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. * * @return Null string on success. On failure, returns human-readable error message to display in a KMessageBox. */ QString save(); bool bookTime(const Task *task, const QDateTime &startDateTime, int64_t durationInSeconds); static QString createLockFileName(const QUrl& url); private Q_SLOTS: void onFileModified(); private: ProjectModel *m_model; QUrl m_url; TaskView* m_taskView; }; #endif // KTIMETRACKER_STORAGE_H