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"