diff --git a/src/taskview.cpp b/src/taskview.cpp index 0f4ff73..9132ccb 100644 --- a/src/taskview.cpp +++ b/src/taskview.cpp @@ -1,824 +1,813 @@ /* * Copyright (C) 2003 by Scott Monachello * 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 "taskview.h" #include #include #include #include #include #include #include "dialogs/exportdialog.h" #include "desktoptracker.h" #include "dialogs/edittimedialog.h" #include "dialogs/taskpropertiesdialog.h" #include "export/export.h" #include "focusdetector.h" #include "dialogs/historydialog.h" #include "idletimedetector.h" #include "import/plannerparser.h" #include "ktimetracker.h" #include "ktimetrackerutility.h" #include "ktt_debug.h" #include "model/eventsmodel.h" #include "model/task.h" #include "model/tasksmodel.h" #include "treeviewheadercontextmenu.h" #include "widgets/taskswidget.h" void deleteEntry(const QString& key) { KConfigGroup config = KSharedConfig::openConfig()->group(QString()); config.deleteEntry(key); config.sync(); } TaskView::TaskView(QWidget *parent) : QObject(parent) , m_filterProxyModel(new QSortFilterProxyModel(this)) , m_storage(new TimeTrackerStorage()) , m_focusTrackingActive(false) , m_lastTaskWithFocus(nullptr) , m_focusDetector(new FocusDetector()) , m_tasksWidget(nullptr) { m_filterProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_filterProxyModel->setRecursiveFilteringEnabled(true); m_filterProxyModel->setSortRole(Task::SortRole); connect(m_focusDetector, &FocusDetector::newFocus, this, &TaskView::newFocusWindowDetected); // set up the minuteTimer m_minuteTimer = new QTimer(this); connect(m_minuteTimer, &QTimer::timeout, this, &TaskView::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 (!IdleTimeDetector::isIdleDetectionPossible()) { KTimeTrackerSettings::setEnabled(false); } // Setup auto save timer m_autoSaveTimer = new QTimer(this); connect(m_autoSaveTimer, &QTimer::timeout, this, &TaskView::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, &QTimer::timeout, this, &TaskView::save); // Connect desktop tracker events to task starting/stopping m_desktopTracker = new DesktopTracker(); connect(m_desktopTracker, &DesktopTracker::reachedActiveDesktop, this, &TaskView::startTimerForNow); connect(m_desktopTracker, &DesktopTracker::leftActiveDesktop, this, &TaskView::stopTimerFor); } void TaskView::newFocusWindowDetected(const QString &taskName) { QString newTaskName = taskName; newTaskName.remove('\n'); if (!m_focusTrackingActive) { return; } bool found = false; // has taskName been found in our tasks stopTimerFor(m_lastTaskWithFocus); for (Task *task : storage()->tasksModel()->getAllTasks()) { if (task->name() == newTaskName) { found = true; startTimerForNow(task); m_lastTaskWithFocus = task; } } if (!found) { if (!addTask(newTaskName)) { 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/ ")); } save(); for (Task *task : storage()->tasksModel()->getAllTasks()) { if (task->name() == newTaskName) { startTimerForNow(task); m_lastTaskWithFocus = task; } } } emit updateButtons(); } TimeTrackerStorage *TaskView::storage() { return m_storage; } TaskView::~TaskView() { delete m_storage; KTimeTrackerSettings::self()->save(); } void TaskView::load(const QUrl &url) { if (m_tasksWidget) { delete m_tasksWidget; m_tasksWidget = nullptr; } // 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. QString err = m_storage->load(this, url); if (!err.isEmpty()) { KMessageBox::error(m_tasksWidget, err); qCDebug(KTT_LOG) << "Leaving TaskView::load"; return; } m_tasksWidget = new TasksWidget(dynamic_cast(parent()), m_filterProxyModel, nullptr); connect(m_tasksWidget, &TasksWidget::updateButtons, this, &TaskView::updateButtons); connect(m_tasksWidget, &TasksWidget::contextMenuRequested, this, &TaskView::contextMenuRequested); connect(m_tasksWidget, &TasksWidget::taskDoubleClicked, this, &TaskView::onTaskDoubleClicked); m_tasksWidget->setRootIsDecorated(true); reconfigureModel(); reconfigureView(); // Connect to the new model created by TimeTrackerStorage::load() auto *tasksModel = m_storage->tasksModel(); m_filterProxyModel->setSourceModel(tasksModel); m_tasksWidget->setSourceModel(tasksModel); for (int i = 0; i <= tasksModel->columnCount(QModelIndex()); ++i) { m_tasksWidget->resizeColumnToContents(i); } // Table header context menu auto *headerContextMenu = new TreeViewHeaderContextMenu(this, m_tasksWidget, QVector{0}); connect(headerContextMenu, &TreeViewHeaderContextMenu::columnToggled, this, &TaskView::slotColumnToggled); connect(tasksModel, &TasksModel::taskCompleted, this, &TaskView::stopTimerFor); connect(tasksModel, &TasksModel::taskDropped, this, &TaskView::reFreshTimes); connect(tasksModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &TaskView::taskAboutToBeRemoved); connect(storage()->eventsModel(), &EventsModel::timesChanged, this, &TaskView::reFreshTimes); // Register tasks with desktop tracker for (Task *task : storage()->tasksModel()->getAllTasks()) { m_desktopTracker->registerForDesktops(task, task->desktops()); } // Start all tasks that have an event without endtime for (Task *task : storage()->tasksModel()->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) { m_tasksWidget->restoreItemState(); m_tasksWidget->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.")); } refreshModel(); refreshView(); } for (int i = 0; i <= tasksModel->columnCount(QModelIndex()); ++i) { m_tasksWidget->resizeColumnToContents(i); } } void TaskView::closeStorage() { m_storage->closeStorage(); } void TaskView::refreshModel() { for (Task *task : storage()->tasksModel()->getAllTasks()) { task->invalidateCompletedState(); task->update(); // maybe there was a change in the times's format } } void TaskView::refreshView() { - if (!m_tasksWidget) { - return; + if (m_tasksWidget) { + m_tasksWidget->refresh(); } - - // 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 percent column only - // works properly if rootIsDecorated == true. - m_tasksWidget->setRootIsDecorated(true); - - emit updateButtons(); - qCDebug(KTT_LOG) << "exiting TaskView::refresh()"; } /** * Refresh the times of the tasks, e.g. when the history has been changed by the user. * Re-calculate the time for every task based on events in the history. */ QString TaskView::reFreshTimes() { QString err; // 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 : storage()->tasksModel()->getAllTasks()) { task->resetTimes(); } for (Task *task : storage()->tasksModel()->getAllTasks()) { // get all events for task for (const auto *event : storage()->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 : storage()->tasksModel()->getAllTasks()) { // Start recursive method recalculateTotalTimesSubtree() for each top-level task. if (task->isRoot()) { task->recalculateTotalTimesSubtree(); } } refreshModel(); refreshView(); qCDebug(KTT_LOG) << "Leaving TaskView::reFreshTimes()"; return err; } void TaskView::importPlanner(const QString& fileName) { qCDebug(KTT_LOG) << "entering importPlanner"; auto *handler = new PlannerParser(storage()->projectModel(), m_tasksWidget->currentItem()); QFile xmlFile(fileName); QXmlInputSource source(&xmlFile); QXmlSimpleReader reader; reader.setContentHandler(handler); reader.parse(source); refreshModel(); refreshView(); } void TaskView::scheduleSave() { m_manualSaveTimer->start(10); } void TaskView::save() { qCDebug(KTT_LOG) << "Entering TaskView::save()"; QString err = m_storage->save(); if (!err.isNull()) { KMessageBox::error(m_tasksWidget, err); } } void TaskView::startCurrentTimer() { startTimerForNow(m_tasksWidget->currentItem()); } void TaskView::startTimerFor(Task *task, const QDateTime &startTime) { qCDebug(KTT_LOG) << "Entering function"; if (task != nullptr && m_activeTasks.indexOf(task) == -1) { if (!task->isComplete()) { if (KTimeTrackerSettings::uniTasking()) { stopAllTimers(); } m_idleTimeDetector->startIdleDetection(); task->setRunning(true, startTime); save(); m_activeTasks.append(task); emit updateButtons(); if (m_activeTasks.count() == 1) { emit timersActive(); } emit tasksChanged(m_activeTasks); } } } void TaskView::startTimerForNow(Task *task) { startTimerFor(task, QDateTime::currentDateTime()); } void TaskView::clearActiveTasks() { m_activeTasks.clear(); } void TaskView::stopAllTimers(const QDateTime& when) { qCDebug(KTT_LOG) << "Entering function"; QProgressDialog dialog( i18nc("@info:progress", "Stopping timers..."), i18n("Cancel"), 0, m_activeTasks.count(), m_tasksWidget); if (m_activeTasks.count() > 1) { dialog.show(); } for (Task *task : m_activeTasks) { QApplication::processEvents(); task->setRunning(false, when); save(); 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 : storage()->tasksModel()->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 : storage()->tasksModel()->getAllTasks()) { task->resetTimes(); } storage()->deleteAllEvents(); qCDebug(KTT_LOG) << "Leaving function"; } void TaskView::stopTimerFor(Task* task) { qCDebug(KTT_LOG) << "Entering function"; if (task != nullptr && m_activeTasks.indexOf(task) != -1) { m_activeTasks.removeAll(task); task->setRunning(false); save(); if (m_activeTasks.count() == 0) { m_idleTimeDetector->stopIdleDetection(); emit timersInactive(); } emit updateButtons(); } emit tasksChanged(m_activeTasks); } void TaskView::stopCurrentTimer() { stopTimerFor(m_tasksWidget->currentItem()); if (m_focusTrackingActive && m_lastTaskWithFocus == m_tasksWidget->currentItem()) { toggleFocusTracking(); } } void TaskView::minuteUpdate() { addTimeToActiveTasks(1); } void TaskView::addTimeToActiveTasks(int64_t minutes) { for (Task *task : m_activeTasks) { task->changeTime(minutes, nullptr); } scheduleSave(); } void TaskView::newTask() { newTask(i18nc("@title:window", "New Task"), nullptr); } void TaskView::newTask(const QString &caption, Task *parent) { QPointer dialog = new TaskPropertiesDialog( m_tasksWidget->parentWidget(), caption, QString(), QString(), DesktopList()); if (dialog->exec() == QDialog::Accepted) { QString taskName = i18n("Unnamed Task"); if (!dialog->name().isEmpty()) { taskName = dialog->name(); } QString taskDescription = dialog->description(); auto desktopList = dialog->desktops(); // 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(); } int64_t total = 0; int64_t session = 0; auto *task = addTask(taskName, taskDescription, total, session, desktopList, parent); save(); if (!task) { 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/")); } } delete dialog; emit updateButtons(); } Task *TaskView::addTask( const QString& taskname, const QString& taskdescription, int64_t total, int64_t session, const DesktopList& desktops, Task* parent) { qCDebug(KTT_LOG) << "Entering function; taskname =" << taskname; m_tasksWidget->setSortingEnabled(false); Task *task = new Task( taskname, taskdescription, total, session, desktops, storage()->projectModel(), parent); if (task->uid().isNull()) { qFatal("failed to generate UID"); } m_desktopTracker->registerForDesktops(task, desktops); m_tasksWidget->setCurrentIndex(m_filterProxyModel->mapFromSource(storage()->tasksModel()->index(task, 0))); task->invalidateCompletedState(); m_tasksWidget->setSortingEnabled(true); return task; } void TaskView::newSubTask() { Task* task = m_tasksWidget->currentItem(); if (!task) { return; } newTask(i18nc("@title:window", "New Sub Task"), task); m_tasksWidget->setExpanded(m_filterProxyModel->mapFromSource(storage()->tasksModel()->index(task, 0)), true); refreshModel(); refreshView(); } void TaskView::editTask() { qCDebug(KTT_LOG) <<"Entering editTask"; Task* task = m_tasksWidget->currentItem(); if (!task) { return; } auto oldDeskTopList = task->desktops(); QPointer dialog = new TaskPropertiesDialog( m_tasksWidget->parentWidget(), i18nc("@title:window", "Edit Task"), task->name(), task->description(), oldDeskTopList); if (dialog->exec() == QDialog::Accepted) { QString name = i18n("Unnamed Task"); if (!dialog->name().isEmpty()) { name = dialog->name(); } // setName only does something if the new name is different task->setName(name); task->setDescription(dialog->description()); auto desktopList = dialog->desktops(); // 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(); } delete dialog; } void TaskView::editTaskTime() { qCDebug(KTT_LOG) <<"Entering editTask"; Task* task = m_tasksWidget->currentItem(); if (!task) { return; } QPointer editTimeDialog = new EditTimeDialog( m_tasksWidget->parentWidget(), task->name(), task->description(), static_cast(task->time())); if (editTimeDialog->exec() == QDialog::Accepted) { if (editTimeDialog->editHistoryRequested()) { editHistory(); } else { editTaskTime(task->uid(), editTimeDialog->changeMinutes()); } } delete editTimeDialog; } void TaskView::editHistory() { QPointer dialog = new HistoryDialog(m_tasksWidget->parentWidget(), storage()->projectModel()); dialog->exec(); } void TaskView::setPerCentComplete(int completion) { Task* task = m_tasksWidget->currentItem(); if (!task) { KMessageBox::information(nullptr, i18n("No task selected.")); return; } if (completion < 0) { completion = 0; } if (completion < 100) { task->setPercentComplete(completion); task->invalidateCompletedState(); 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 // 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 = m_tasksWidget->currentItem(); } if (!m_tasksWidget->currentItem()) { KMessageBox::information(nullptr, i18n("No task selected.")); } else { int response = KMessageBox::Continue; if (KTimeTrackerSettings::promptDelete()) { response = KMessageBox::warningContinueCancel(nullptr, 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."), i18nc("@title:window", "Deleting Task"), KStandardGuiItem::del()); } if (response == KMessageBox::Continue) { deleteTaskBatch(task); } } } void TaskView::markTaskAsComplete() { if (!m_tasksWidget->currentItem()) { KMessageBox::information(nullptr, i18n("No task selected.")); return; } m_tasksWidget->currentItem()->setPercentComplete(100); m_tasksWidget->currentItem()->invalidateCompletedState(); emit updateButtons(); } void TaskView::subtractTime(int64_t minutes) { addTimeToActiveTasks(-minutes); } void TaskView::markTaskAsIncomplete() { setPerCentComplete(50); // if it has been reopened, assume half-done } void TaskView::slotColumnToggled(int column) { switch (column) { case 1: KTimeTrackerSettings::setDisplaySessionTime(!m_tasksWidget->isColumnHidden(1)); break; case 2: KTimeTrackerSettings::setDisplayTime(!m_tasksWidget->isColumnHidden(2)); break; case 3: KTimeTrackerSettings::setDisplayTotalSessionTime(!m_tasksWidget->isColumnHidden(3)); break; case 4: KTimeTrackerSettings::setDisplayTotalTime(!m_tasksWidget->isColumnHidden(4)); break; case 5: KTimeTrackerSettings::setDisplayPriority(!m_tasksWidget->isColumnHidden(5)); break; case 6: KTimeTrackerSettings::setDisplayPercentComplete(!m_tasksWidget->isColumnHidden(6)); break; } KTimeTrackerSettings::self()->save(); } bool TaskView::isFocusTrackingActive() const { return m_focusTrackingActive; } void TaskView::reconfigureModel() { /* 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(); } refreshModel(); } void TaskView::reconfigureView() { /* Adapt columns */ m_tasksWidget->setColumnHidden(1, !KTimeTrackerSettings::displaySessionTime()); m_tasksWidget->setColumnHidden(2, !KTimeTrackerSettings::displayTime()); m_tasksWidget->setColumnHidden(3, !KTimeTrackerSettings::displayTotalSessionTime()); m_tasksWidget->setColumnHidden(4, !KTimeTrackerSettings::displayTotalTime()); m_tasksWidget->setColumnHidden(5, !KTimeTrackerSettings::displayPriority()); m_tasksWidget->setColumnHidden(6, !KTimeTrackerSettings::displayPercentComplete()); refreshView(); } //---------------------------------------------------------------------------- void TaskView::onTaskDoubleClicked(Task *task) { if (task->isRunning()) { // if task is running, stop it stopCurrentTimer(); } else if (!task->isComplete()) { // if task is not running, start it stopAllTimers(); startCurrentTimer(); } } void TaskView::editTaskTime(const QString& taskUid, int64_t minutes) { // update session time if the time was changed auto* task = m_storage->tasksModel()->taskByUID(taskUid); if (task) { task->changeTime(minutes, m_storage->eventsModel()); scheduleSave(); } } void TaskView::taskAboutToBeRemoved(const QModelIndex &parent, int first, int last) { if (first != last) { qFatal("taskAboutToBeRemoved: unexpected removal of multiple items at once"); } TasksModelItem *item = nullptr; if (parent.isValid()) { // Nested task auto *parentItem = storage()->tasksModel()->item(parent); if (!parentItem) { qFatal("taskAboutToBeRemoved: parentItem is nullptr"); } item = parentItem->child(first); } else { // Top-level task item = storage()->tasksModel()->topLevelItem(first); } if (!item) { qFatal("taskAboutToBeRemoved: item is nullptr"); } // We use static_cast here instead of dynamic_cast because this // taskAboutToBeRemoved() slot is called from TasksModelItem's destructor // when the Task object is already destructed, thus dynamic_cast would // return nullptr. auto *task = dynamic_cast(item); if (!task) { qFatal("taskAboutToBeRemoved: task is nullptr"); } // Handle task deletion DesktopList desktopList; m_desktopTracker->registerForDesktops(task, desktopList); m_activeTasks.removeAll(task); emit tasksChanged(m_activeTasks); } diff --git a/src/widgets/taskswidget.cpp b/src/widgets/taskswidget.cpp index 42d7be7..17a629b 100644 --- a/src/widgets/taskswidget.cpp +++ b/src/widgets/taskswidget.cpp @@ -1,364 +1,378 @@ /* * 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 "taskswidget.h" #include #include #include #include #include #include #include #include #include #include "ktimetracker.h" #include "ktt_debug.h" #include "model/task.h" #include "model/tasksmodel.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(); } //BEGIN ProgressColumnDelegate (custom painting of the progress column) class ProgressColumnDelegate : public QStyledItemDelegate { public: explicit ProgressColumnDelegate(QObject *parent) : QStyledItemDelegate(parent) {} void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { 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.window().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.window().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 TasksWidget::TasksWidget(QWidget *parent, QSortFilterProxyModel *filterProxyModel, TasksModel *tasksModel) : QTreeView(parent) , m_filterProxyModel(filterProxyModel) , m_tasksModel(tasksModel) , m_popupPercentageMenu(nullptr) , m_popupPriorityMenu(nullptr) { setModel(filterProxyModel); connect(this, &QTreeView::expanded, this, &TasksWidget::itemStateChanged); connect(this, &QTreeView::collapsed, this, &TasksWidget::itemStateChanged); setWindowFlags(windowFlags() | Qt::WindowContextHelpButtonHint); setAllColumnsShowFocus(true); setSortingEnabled(true); setAlternatingRowColors(true); setDragDropMode(QAbstractItemView::InternalMove); setItemDelegateForColumn(6, new ProgressColumnDelegate(this)); // Context menu for task progress percentage m_popupPercentageMenu = new QMenu(this); for (int i = 0; i <= 100; i += 10) { QString label = i18nc("@item:inmenu Task progress", "%1 %", i); m_percentage[m_popupPercentageMenu->addAction(label)] = i; } connect(m_popupPercentageMenu, &QMenu::triggered, this, &TasksWidget::slotSetPercentage); // Context menu for task priority m_popupPriorityMenu = new QMenu(this); for (int i = 0; i <= 9; ++i) { QString label; switch (i) { case 0: label = i18nc("@item:inmenu Task priority", "unspecified"); break; case 1: label = i18nc("@item:inmenu Task priority", "1 (highest)"); break; case 5: label = i18nc("@item:inmenu Task priority", "5 (medium)"); break; case 9: label = i18nc("@item:inmenu Task priority", "9 (lowest)"); break; default: label = QString("%1").arg(i); break; } m_priority[m_popupPriorityMenu->addAction(label)] = i; } connect(m_popupPriorityMenu, &QMenu::triggered, this, &TasksWidget::slotSetPriority); setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &TasksWidget::customContextMenuRequested, this, &TasksWidget::slotCustomContextMenuRequested); sortByColumn(0, Qt::AscendingOrder); } void TasksWidget::itemStateChanged(const QModelIndex &index) { Task *task = taskAtViewIndex(index); if (!task) { return; } qCDebug(KTT_LOG) <<"TaskView::itemStateChanged()" <<" uid=" << task->uid() <<" state=" << isExpanded(index); writeEntry(task->uid(), isExpanded(index)); } void TasksWidget::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 TasksWidget::slotSetPercentage(QAction* action) { if (currentItem()) { currentItem()->setPercentComplete(m_percentage[action]); emit updateButtons(); } } void TasksWidget::slotSetPriority(QAction* action) { if (currentItem()) { currentItem()->setPriority(m_priority[action]); } } void TasksWidget::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); } } bool TasksWidget::mousePositionInsideCheckbox(QMouseEvent *event) const { QModelIndex index = indexAt(event->pos()); return index.isValid() && index.column() == 0 && visualRect(index).x() <= event->pos().x() && event->pos().x() < visualRect(index).x() + 19; } void TasksWidget::mousePressEvent(QMouseEvent *event) { qCDebug(KTT_LOG) << "Entering function, event->button()=" << event->button(); if (mousePositionInsideCheckbox(event)) { // if the user toggles a task as complete/incomplete QModelIndex index = indexAt(event->pos()); 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 TasksWidget::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() && !mousePositionInsideCheckbox(event)) { Task *task = taskAtViewIndex(index); if (task) { emit taskDoubleClicked(task); } } else { QTreeView::mouseDoubleClickEvent(event); } } void TasksWidget::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) { emit updateButtons(); } void TasksWidget::restoreItemState() { qCDebug(KTT_LOG) << "Entering function"; if (m_tasksModel->topLevelItemCount() > 0) { for (auto *item : m_tasksModel->getAllItems()) { auto *task = dynamic_cast(item); setExpanded(m_filterProxyModel->mapFromSource( m_tasksModel->index(task, 0)), readBoolEntry(task->uid())); } } qCDebug(KTT_LOG) << "Leaving function"; } Task* TasksWidget::taskAtViewIndex(QModelIndex viewIndex) { // if (!m_storage->isLoaded()) { // return nullptr; // } if (!m_tasksModel) { return nullptr; } QModelIndex index = m_filterProxyModel->mapToSource(viewIndex); return dynamic_cast(m_tasksModel->item(index)); } void TasksWidget::setSourceModel(TasksModel *tasksModel) { m_tasksModel = tasksModel; } Task* TasksWidget::currentItem() { return taskAtViewIndex(QTreeView::currentIndex()); } void TasksWidget::setFilterText(const QString &text) { m_filterProxyModel->setFilterFixedString(text); } + +void TasksWidget::refresh() +{ + // 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 percent column only + // works properly if rootIsDecorated == true. + setRootIsDecorated(true); + + emit updateButtons(); + qCDebug(KTT_LOG) << "exiting TaskView::refresh()"; +} diff --git a/src/widgets/taskswidget.h b/src/widgets/taskswidget.h index f689100..df63539 100644 --- a/src/widgets/taskswidget.h +++ b/src/widgets/taskswidget.h @@ -1,86 +1,88 @@ /* * 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_TASKSWIDGET_H #define KTIMETRACKER_TASKSWIDGET_H #include QT_BEGIN_NAMESPACE class QSortFilterProxyModel; QT_END_NAMESPACE class TasksModel; class Task; class TasksWidget : public QTreeView { Q_OBJECT public: TasksWidget(QWidget *parent, QSortFilterProxyModel *filterProxyModel, TasksModel *tasksModel); ~TasksWidget() override = default; void setSourceModel(TasksModel *tasksModel); Task* taskAtViewIndex(QModelIndex viewIndex); /** Return the current item in the view, cast to a Task pointer. */ Task* currentItem(); /** * 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. */ void restoreItemState(); + void refresh(); + public Q_SLOTS: /** item state stores if a task is expanded so you can see the subtasks */ void itemStateChanged(const QModelIndex &index); void slotCustomContextMenuRequested(const QPoint&); void slotSetPercentage(QAction*); void slotSetPriority(QAction*); void setFilterText(const QString &text); Q_SIGNALS: void updateButtons(); void contextMenuRequested(const QPoint&); void taskDoubleClicked(Task*); private: bool mousePositionInsideCheckbox(QMouseEvent *event) const; void mouseMoveEvent(QMouseEvent*) override; void mousePressEvent(QMouseEvent*) override; void mouseDoubleClickEvent(QMouseEvent*) override; void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override; QSortFilterProxyModel* m_filterProxyModel; TasksModel *m_tasksModel; QMenu *m_popupPercentageMenu; QMap m_percentage; QMenu *m_popupPriorityMenu; QMap m_priority; }; #endif // KTIMETRACKER_TASKSWIDGET_H