File indexing completed on 2024-11-17 04:49:34

0001 /*
0002  * Copyright (c) 2019 Alexander Potashev <aspotashev@gmail.com>
0003  *
0004  * This program is free software; you can redistribute it and/or
0005  * modify it under the terms of the GNU General Public License as
0006  * published by the Free Software Foundation; either version 2 of
0007  * the License or (at your option) version 3 or any later version
0008  * accepted by the membership of KDE e.V. (or its successor approved
0009  * by the membership of KDE e.V.), which shall act as a proxy
0010  * defined in Section 14 of version 3 of the license.
0011  *
0012  * This program is distributed in the hope that it will be useful,
0013  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0014  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0015  * GNU General Public License for more details.
0016  *
0017  * You should have received a copy of the GNU General Public License
0018  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0019  */
0020 
0021 #include "tasksmodel.h"
0022 
0023 #include <QMovie>
0024 #include <QPixmap>
0025 #include <QStack>
0026 
0027 #include <KLocalizedString>
0028 
0029 #include "task.h"
0030 
0031 TasksModel::TasksModel()
0032     : m_rootItem(new TasksModelItem(this, nullptr))
0033     , m_headerLabels{i18nc("@title:column", "Task Name"),
0034                      i18nc("@title:column", "Session Time"),
0035                      i18nc("@title:column", "Time"),
0036                      i18nc("@title:column", "Total Session Time"),
0037                      i18nc("@title:column", "Total Time"),
0038                      i18nc("@title:column", "Priority"),
0039                      i18nc("@title:column", "Percent Complete")}
0040     , m_clockAnimation(nullptr)
0041     , m_dragCutTaskId()
0042 {
0043     Q_INIT_RESOURCE(pics);
0044 
0045     m_clockAnimation = new QMovie(QStringLiteral(":/pics/watch.gif"), QByteArray(), this);
0046 
0047     // Prepare animated icon
0048     connect(m_clockAnimation, &QMovie::frameChanged, this, &TasksModel::setActiveIcon);
0049 
0050     // TODO: stop animation when no tasks are active
0051     m_clockAnimation->start();
0052 }
0053 
0054 void TasksModel::clear()
0055 {
0056     beginResetModel();
0057 
0058     // Empty "m_children", move it to "children".
0059     QList<TasksModelItem *> children;
0060     children.swap(m_rootItem->m_children);
0061 
0062     for (int i = 0; i < children.count(); ++i) {
0063         TasksModelItem *item = children.at(i);
0064         item->m_parent = nullptr;
0065         delete item;
0066     }
0067 
0068     endResetModel();
0069 }
0070 
0071 int TasksModel::topLevelItemCount() const
0072 {
0073     return m_rootItem->childCount();
0074 }
0075 
0076 int TasksModel::indexOfTopLevelItem(TasksModelItem *item) const
0077 {
0078     return m_rootItem->m_children.indexOf(item);
0079 }
0080 
0081 TasksModelItem *TasksModel::topLevelItem(int index) const
0082 {
0083     return m_rootItem->child(index);
0084 }
0085 
0086 TasksModelItem *TasksModel::takeTopLevelItem(int index)
0087 {
0088     return m_rootItem->takeChild(index);
0089 }
0090 
0091 TasksModelItem *TasksModel::item(const QModelIndex &index) const
0092 {
0093     if (!index.isValid()) {
0094         return nullptr;
0095     }
0096 
0097     return static_cast<TasksModelItem *>(index.internalPointer());
0098 }
0099 
0100 QModelIndex TasksModel::index(TasksModelItem *item, int column) const
0101 {
0102     if (!item || item == m_rootItem) {
0103         return {};
0104     }
0105 
0106     const TasksModelItem *par = item->parent();
0107     auto *itm = const_cast<TasksModelItem *>(item);
0108     if (!par) {
0109         par = m_rootItem;
0110     }
0111     int row = par->m_children.lastIndexOf(itm);
0112     return createIndex(row, column, itm);
0113 }
0114 
0115 QModelIndex TasksModel::index(int row, int column, const QModelIndex &parent) const
0116 {
0117     int c = columnCount(parent);
0118     if (row < 0 || column < 0 || column >= c) {
0119         return {};
0120     }
0121 
0122     TasksModelItem *parentItem = parent.isValid() ? item(parent) : m_rootItem;
0123     if (parentItem && row < parentItem->childCount()) {
0124         TasksModelItem *itm = parentItem->child(row);
0125         if (itm) {
0126             return createIndex(row, column, itm);
0127         }
0128 
0129         return {};
0130     }
0131 
0132     return {};
0133 }
0134 
0135 QModelIndex TasksModel::parent(const QModelIndex &child) const
0136 {
0137     if (!child.isValid()) {
0138         return {};
0139     }
0140 
0141     auto *item = static_cast<TasksModelItem *>(child.internalPointer());
0142     if (!item || item == m_rootItem) {
0143         return {};
0144     }
0145 
0146     TasksModelItem *parent = item->parent();
0147     return index(parent, 0);
0148 }
0149 
0150 int TasksModel::rowCount(const QModelIndex &parent) const
0151 {
0152     if (!parent.isValid()) {
0153         return m_rootItem->childCount();
0154     }
0155 
0156     TasksModelItem *parentItem = item(parent);
0157     if (parentItem) {
0158         return parentItem->childCount();
0159     }
0160 
0161     return 0;
0162 }
0163 
0164 int TasksModel::columnCount(const QModelIndex & /*parent*/) const
0165 {
0166     return m_headerLabels.size();
0167 }
0168 
0169 QVariant TasksModel::data(const QModelIndex &index, int role) const
0170 {
0171     if (!index.isValid()) {
0172         return {};
0173     }
0174 
0175     switch (role) {
0176     case Qt::TextAlignmentRole:
0177         // Align task name: left
0178         // Align priority: center
0179         // Align HH:MM: right
0180         if (index.column() == 5) {
0181             return Qt::AlignCenter;
0182         } else if (index.column() >= 1) {
0183             return {Qt::AlignRight | Qt::AlignVCenter};
0184         }
0185         break;
0186     case Qt::WhatsThisRole:
0187         if (index.column() == 0) {
0188             return i18nc("@info:whatsthis",
0189                          "The task name is what you call the task, it can be "
0190                          "chosen freely.");
0191         } else if (index.column() == 1) {
0192             return i18nc("@info:whatsthis",
0193                          "The session time is the time since you last chose "
0194                          "\"Start New Session\".");
0195         }
0196         break;
0197     case Qt::DecorationRole: {
0198         auto *task = dynamic_cast<Task *>(item(index));
0199         if (!task) {
0200             return {};
0201         }
0202 
0203         if (index.column() == 0) {
0204             return QPixmap(task->isComplete() ? QStringLiteral(":/pics/task-complete.xpm") : QStringLiteral(":/pics/task-incomplete.xpm"));
0205         } else if (index.column() == 1) {
0206             return task->isRunning() ? m_clockAnimation->currentPixmap() : QPixmap(QStringLiteral(":/pics/empty-watch.xpm"));
0207         }
0208         break;
0209     }
0210     default: {
0211         TasksModelItem *itm = item(index);
0212         if (itm) {
0213             return itm->data(index.column(), role);
0214         }
0215         break;
0216     }
0217     }
0218 
0219     return {};
0220 }
0221 
0222 QList<TasksModelItem *> TasksModel::getAllItems()
0223 {
0224     QList<TasksModelItem *> res;
0225 
0226     QStack<TasksModelItem *> stack;
0227     stack.push(m_rootItem);
0228     while (!stack.isEmpty()) {
0229         TasksModelItem *item = stack.pop();
0230         if (item != m_rootItem) {
0231             res.append(item);
0232         }
0233 
0234         for (int c = 0; c < item->m_children.count(); ++c) {
0235             stack.push(item->m_children.at(c));
0236         }
0237     }
0238 
0239     return res;
0240 }
0241 
0242 QList<Task *> TasksModel::getAllTasks()
0243 {
0244     QList<Task *> tasks;
0245     for (TasksModelItem *item : getAllItems()) {
0246         // If "item" is not a Task, then we are probably in the middle
0247         // of class Task or TasksModelItem destructor.
0248         Task *task = dynamic_cast<Task *>(item);
0249         if (task) {
0250             tasks.append(task);
0251         }
0252     }
0253 
0254     return tasks;
0255 }
0256 
0257 QList<Task *> TasksModel::getActiveTasks()
0258 {
0259     QList<Task *> activeTasks;
0260     for (Task *task : getAllTasks()) {
0261         if (task->isRunning()) {
0262             activeTasks.append(task);
0263         }
0264     }
0265 
0266     return activeTasks;
0267 }
0268 
0269 QVariant TasksModel::headerData(int section, Qt::Orientation orientation, int role) const
0270 {
0271     if (section < 0 || orientation != Qt::Horizontal || section >= m_headerLabels.size()) {
0272         return {};
0273     }
0274 
0275     switch (role) {
0276     case Qt::DisplayRole:
0277         return m_headerLabels[section];
0278     case Qt::WhatsThisRole:
0279         switch (section) {
0280         case 0:
0281             return i18nc("@info:whatsthis",
0282                          "The task name is what you call the task, it can be "
0283                          "chosen freely.");
0284         case 1:
0285             return i18nc("@info:whatsthis",
0286                          "The session time is the time since you last chose "
0287                          "\"Start New Session\".");
0288         case 3:
0289             return i18nc("@info:whatsthis",
0290                          "The total session time is the session time of this "
0291                          "task and all its subtasks.");
0292         case 4:
0293             return i18nc("@info:whatsthis",
0294                          "The total time is the time of this task and all its "
0295                          "subtasks.");
0296         default:
0297             break;
0298         }
0299         break;
0300     default:
0301         break;
0302     }
0303 
0304     return QAbstractItemModel::headerData(section, orientation, role);
0305 }
0306 
0307 void TasksModel::setActiveIcon(int /*unused*/)
0308 {
0309     // TODO: only touch those tasks that are currently running
0310     for (auto *task : getAllItems()) {
0311         task->invalidateRunningState();
0312     }
0313 }
0314 
0315 void TasksModel::addChild(TasksModelItem *item)
0316 {
0317     m_rootItem->addChild(item);
0318 }
0319 
0320 Qt::ItemFlags TasksModel::flags(const QModelIndex &index) const
0321 {
0322     if (!index.isValid()) {
0323         // Allow move to top-level
0324         return Qt::ItemIsDropEnabled;
0325     }
0326 
0327     return QAbstractItemModel::flags(index) | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
0328 }
0329 
0330 Qt::DropActions TasksModel::supportedDropActions() const
0331 {
0332     return Qt::MoveAction;
0333 }
0334 
0335 bool TasksModel::canDropMimeData(const QMimeData *data,
0336                                  Qt::DropAction action,
0337                                  int row,
0338                                  int column,
0339                                  const QModelIndex &parent) const
0340 {
0341     if (m_dragCutTaskId.isEmpty()) {
0342         return false;
0343     }
0344 
0345     return QAbstractItemModel::canDropMimeData(data, action, row, column, parent);
0346 }
0347 
0348 bool TasksModel::dropMimeData(const QMimeData *data,
0349                               Qt::DropAction action,
0350                               int row,
0351                               int column,
0352                               const QModelIndex &parent)
0353 {
0354     if (!canDropMimeData(data, action, row, column, parent)) {
0355         return false;
0356     }
0357 
0358     if (action == Qt::IgnoreAction) {
0359         return true;
0360     }
0361 
0362     auto *task = taskByUID(m_dragCutTaskId);
0363     if (!task) {
0364         return false;
0365     }
0366 
0367     // If have parent, move to subtasks of the parent task.
0368     // If don't have parent, move to top-level tasks.
0369     TasksModelItem *newParent = parent.isValid() ? item(parent) : m_rootItem;
0370     if (!newParent) {
0371         return false;
0372     }
0373 
0374     task->move(newParent);
0375     Q_EMIT taskDropped();
0376     return true;
0377 }
0378 
0379 QMimeData *TasksModel::mimeData(const QModelIndexList &indexes) const
0380 {
0381     auto *task = dynamic_cast<Task *>(item(indexes[0]));
0382     if (!task) {
0383         return nullptr;
0384     }
0385 
0386     m_dragCutTaskId = task->uid();
0387     return QAbstractItemModel::mimeData(indexes);
0388 }
0389 
0390 Task *TasksModel::taskByUID(const QString &uid)
0391 {
0392     for (auto *item : getAllItems()) {
0393         auto *task = dynamic_cast<Task *>(item);
0394         if (task->uid() == uid) {
0395             return task;
0396         }
0397     }
0398 
0399     return nullptr;
0400 }
0401 
0402 void TasksModel::startNewSession()
0403 {
0404     for (Task *task : getAllTasks()) {
0405         task->startNewSession();
0406     }
0407 }
0408 
0409 void TasksModel::addTimeToActiveTasks(int64_t minutes)
0410 {
0411     for (Task *task : getActiveTasks()) {
0412         task->changeTime(minutes, nullptr);
0413     }
0414 }
0415 
0416 #include "moc_tasksmodel.cpp"