├── .clang-format ├── .github └── workflows │ ├── formatter.yml │ └── main.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── assets └── screenshot.png ├── dist └── PKGBUILD ├── res ├── icons │ ├── active.svg │ ├── add.svg │ ├── delete.svg │ ├── done.svg │ ├── edit.svg │ ├── filter.svg │ ├── qtask.svg │ ├── refresh.png │ ├── start.svg │ ├── stop.svg │ ├── taskwarrior.png │ ├── undo.png │ └── wait.svg ├── qtask.desktop └── qtask.qrc └── src ├── aboutdialog.cpp ├── aboutdialog.hpp ├── agendadialog.cpp ├── agendadialog.hpp ├── config.hpp.in ├── configmanager.cpp ├── configmanager.hpp ├── datetimedialog.cpp ├── datetimedialog.hpp ├── main.cpp ├── mainwindow.cpp ├── mainwindow.hpp ├── optionaldatetimeedit.cpp ├── optionaldatetimeedit.hpp ├── qtutil.cpp ├── qtutil.hpp ├── recurringdialog.cpp ├── recurringdialog.hpp ├── recurringtasksmodel.cpp ├── recurringtasksmodel.hpp ├── settingsdialog.cpp ├── settingsdialog.hpp ├── tagsedit.cpp ├── tagsedit.hpp ├── task.cpp ├── task.hpp ├── taskdescriptiondelegate.cpp ├── taskdescriptiondelegate.hpp ├── taskdialog.cpp ├── taskdialog.hpp ├── tasksmodel.cpp ├── tasksmodel.hpp ├── tasksview.cpp ├── tasksview.hpp ├── taskwarrior.cpp ├── taskwarrior.hpp ├── taskwarriorreferencedialog.cpp ├── taskwarriorreferencedialog.hpp ├── taskwatcher.cpp ├── taskwatcher.hpp ├── trayicon.cpp └── trayicon.hpp /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | IndentWidth: 4 3 | AlignEscapedNewlines: Left 4 | AlignAfterOpenBracket: DontAlign 5 | Cpp11BracedListStyle: false 6 | AlignConsecutiveMacros: 'true' 7 | BreakConstructorInitializers: BeforeComma 8 | ColumnLimit: '80' 9 | TabWidth: 4 10 | UseTab: Never 11 | ForEachMacros: 12 | - forever # avoids { wrapped to next line 13 | - foreach 14 | - Q_FOREACH 15 | - BOOST_FOREACH 16 | IncludeCategories: 17 | - Regex: '^ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QTask 2 | 3 | ![](https://github.com/jubnzv/qtask/workflows/Build/badge.svg) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 5 | 6 | QTask is an open-source Qt-based graphical user interface for managing tasks. It is based on [Taskwarrior](https://taskwarrior.org/), a popular command-line organizer. 7 | 8 | ## Features 9 | 10 | ![image](assets/screenshot.png) 11 | 12 | The goal of this application is to allow users to manage task list quickly using mostly the keyboards shortcuts while still having a user-friendly graphical user interface. 13 | 14 | You may find the following features of this utility useful: 15 | 16 | * Convenient GUI for adding, deleting, and editing tasks; 17 | * Filters to quickly sort tasks based on Taskwarrior commands; 18 | * Keyboard shortcuts for all common actions; 19 | * Access to Taskwarrior CLI commands via the built-in shell; 20 | * This utility monitors changes in the database in the background. Therefore, you will always see new tasks as they arrive. This is useful if you are using the Taskwarrior CLI or scripts like [bugwarrior](https://github.com/ralphbean/bugwarrior) at the same time. 21 | 22 | If you have any ideas on how to improve this utility, feel free to create an [issue](https://github.com/jubnzv/qtask/issues) or open a [PR](https://github.com/jubnzv/qtask/pulls). 23 | 24 | ## Installation 25 | 26 | Arch Linux users could use AUR to install `qtask`: 27 | ```bash 28 | yay -S qtask-git 29 | ``` 30 | 31 | On other distributions you'll need to build it from sources. 32 | 33 | ### Building from source 34 | 35 | First of all, you need to install the dependencies. You will need Qt at least version 5.14. For earlier versions of Qt, some features will be disabled. 36 | 37 | On Debian-based distributions you need to run the following command: 38 | 39 | ```bash 40 | sudo apt-get install qt5-default qttools5-dev libqt5svg5-dev libx11-xcb-dev qtbase5-private-dev 41 | ``` 42 | 43 | Clone the repository with submodules: 44 | 45 | ```bash 46 | git clone --recurse-submodules https://github.com/jubnzv/qtask.git qtask 47 | cd qtask 48 | ``` 49 | 50 | Build QTask in the build directory: 51 | 52 | ```bash 53 | mkdir build 54 | cd build 55 | cmake -DCMAKE_BUILD_TYPE=Release .. 56 | cmake --build . 57 | ``` 58 | 59 | Then you can install the compiled binary: 60 | 61 | ```bash 62 | sudo make install 63 | ``` 64 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jubnzv/qtask/f67c7034ff9576c123b885acbb942638bf85de48/assets/screenshot.png -------------------------------------------------------------------------------- /dist/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Georgiy Komarov 2 | 3 | pkgname=qtask-git 4 | pkgver=0.1 5 | pkgrel=1 6 | pkgdesc="An open-source organizer based on Taskwarrior" 7 | arch=(x86_64 aarch64) 8 | url="https://github.com/jubnzv/qtask" 9 | license=('MIT') 10 | depends=('task') 11 | makedepends=('cmake' 'extra-cmake-modules' 'git' 'make' 'qt5-base') 12 | source=("${pkgname}::git+https://github.com/jubnzv/qtask.git") 13 | sha512sums=('SKIP') 14 | provides=('qtask') 15 | conflicts=('qtask') 16 | options=(!strip) 17 | 18 | prepare() { 19 | cd "$srcdir/$pkgname" 20 | git submodule update --init --recursive 21 | } 22 | 23 | build() { 24 | _cpuCount=$(grep -c -w ^processor /proc/cpuinfo) 25 | 26 | cmake -S"${pkgname}" -Bbuild \ 27 | -GNinja \ 28 | -DCMAKE_BUILD_TYPE=Release \ 29 | -DCMAKE_INSTALL_PREFIX=/usr 30 | cmake --build build --parallel $_cpuCount 31 | } 32 | 33 | package() { 34 | cd "${srcdir}/build" 35 | DESTDIR="${pkgdir}" cmake --build . --target install 36 | 37 | cd "${srcdir}/${pkgname}" 38 | install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" 39 | } 40 | -------------------------------------------------------------------------------- /res/icons/active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 308 - Stopwatch 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /res/icons/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 151 - Add 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /res/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 152 - Minus 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /res/icons/done.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 154 - Sucess 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /res/icons/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 298 - Edit 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /res/icons/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 295 - Funnel 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /res/icons/qtask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 190 - Note 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /res/icons/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jubnzv/qtask/f67c7034ff9576c123b885acbb942638bf85de48/res/icons/refresh.png -------------------------------------------------------------------------------- /res/icons/start.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 132 - Play 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /res/icons/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 133 - Stop 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /res/icons/taskwarrior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jubnzv/qtask/f67c7034ff9576c123b885acbb942638bf85de48/res/icons/taskwarrior.png -------------------------------------------------------------------------------- /res/icons/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jubnzv/qtask/f67c7034ff9576c123b885acbb942638bf85de48/res/icons/undo.png -------------------------------------------------------------------------------- /res/icons/wait.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 309 - Speed Dial 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /res/qtask.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=QTask 3 | Comment=An open-source organizer based on Taskwarrior 4 | Exec=qtask 5 | Terminal=false 6 | X-MultipleArgs=false 7 | Type=Application 8 | Icon=qtask 9 | Categories=Qt;Office;Calendar; 10 | StartupNotify=true 11 | -------------------------------------------------------------------------------- /res/qtask.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/active.svg 4 | icons/add.svg 5 | icons/delete.svg 6 | icons/done.svg 7 | icons/edit.svg 8 | icons/filter.svg 9 | icons/qtask.svg 10 | icons/refresh.png 11 | icons/start.svg 12 | icons/stop.svg 13 | icons/undo.png 14 | icons/wait.svg 15 | icons/taskwarrior.png 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/aboutdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "aboutdialog.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "config.hpp" 11 | 12 | AboutDialog::AboutDialog(const QVariant &task_version, QWidget *parent) 13 | : QDialog(parent) 14 | { 15 | initUI(task_version); 16 | } 17 | 18 | void AboutDialog::initUI(const QVariant &task_version) 19 | { 20 | setWindowTitle(QCoreApplication::applicationName() + " - About"); 21 | 22 | const QString version = QString("%1.%2.%3") 23 | .arg(QString::number(QTASK_VERSION_MAJOR), 24 | QString::number(QTASK_VERSION_MINOR), 25 | QString::number(QTASK_VERSION_PATCH)); 26 | 27 | const QString info = 28 | QString("" 30 | "
" 31 | "
" 32 | "

Version %1
" 33 | "QTask is an open-source organizer based on Taskwarrior.
" 34 | "

Components: Qt-%2,%3 Essential Icon Pack

" 37 | "

https://github.com/" 40 | "jubnzv/qtask

" 41 | "jubnzv@gmail.com

" 43 | "

" 44 | "
") 45 | .arg(version, QT_VERSION_STR, 46 | (task_version.isNull() 47 | ? "" 48 | : QString("task %1,").arg(task_version.toString()))); 49 | 50 | QVBoxLayout *main_layout = new QVBoxLayout(); 51 | QLabel *info_label = new QLabel(info); 52 | info_label->setOpenExternalLinks(true); 53 | info_label->setTextInteractionFlags(Qt::TextBrowserInteraction); 54 | main_layout->addWidget(info_label); 55 | 56 | setLayout(main_layout); 57 | } 58 | -------------------------------------------------------------------------------- /src/aboutdialog.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ABOUTDIALOG_HPP 2 | #define ABOUTDIALOG_HPP 3 | 4 | #include 5 | #include 6 | 7 | class AboutDialog : public QDialog { 8 | public: 9 | explicit AboutDialog(const QVariant &task_version, 10 | QWidget *parent = nullptr); 11 | ~AboutDialog() = default; 12 | 13 | private: 14 | void initUI(const QVariant &task_version); 15 | }; 16 | 17 | #endif // ABOUTDIALOG_HPP 18 | -------------------------------------------------------------------------------- /src/agendadialog.cpp: -------------------------------------------------------------------------------- 1 | #include "agendadialog.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "task.hpp" 16 | 17 | AgendaDialog::AgendaDialog(const QList &tasks, QWidget *parent) 18 | : QDialog(parent) 19 | , m_tasks(tasks) 20 | { 21 | initUI(); 22 | } 23 | 24 | AgendaDialog::~AgendaDialog() {} 25 | 26 | void AgendaDialog::initUI() 27 | { 28 | setSizeGripEnabled(false); 29 | resize(640, 480); 30 | 31 | setWindowTitle(QCoreApplication::applicationName() + " - Agenda"); 32 | 33 | m_calendar = new QCalendarWidget(this); 34 | m_calendar->setSelectedDate(QDate::currentDate()); 35 | setCalendarHighlight(); 36 | 37 | auto *sched_layout = new QVBoxLayout; 38 | auto *sched_label = new QLabel(tr("Scheduled tasks:")); 39 | m_sched_tasks_list = new QListWidget(this); 40 | m_sched_tasks_list->viewport()->setAutoFillBackground(false); 41 | sched_layout->addWidget(sched_label); 42 | sched_layout->addWidget(m_sched_tasks_list); 43 | 44 | auto *due_layout = new QVBoxLayout; 45 | auto *due_label = new QLabel(tr("Due tasks:")); 46 | m_due_tasks_list = new QListWidget(this); 47 | m_due_tasks_list->viewport()->setAutoFillBackground(false); 48 | due_layout->addWidget(due_label); 49 | due_layout->addWidget(m_due_tasks_list); 50 | 51 | connect(m_calendar, &QCalendarWidget::selectionChanged, this, 52 | &AgendaDialog::onUpdateTasks); 53 | onUpdateTasks(); 54 | 55 | auto *main_layout = new QGridLayout(this); 56 | main_layout->addWidget(m_calendar, 0, 0, 2, 1); 57 | main_layout->addLayout(sched_layout, 0, 1); 58 | main_layout->addLayout(due_layout, 1, 1); 59 | } 60 | 61 | void AgendaDialog::setCalendarHighlight() 62 | { 63 | const auto group = QPalette::Active; 64 | const auto role = QPalette::Window; 65 | auto select_color = [](const int &c) { return (c < 255) ? c : c % 255; }; 66 | auto palette = QApplication::palette(); 67 | QColor sched_color = palette.color(group, role).name(); 68 | sched_color.setRed(select_color(sched_color.red() + 150)); 69 | sched_color.setGreen(select_color(sched_color.green() - 25)); 70 | QColor due_color = palette.color(group, role).name(); 71 | due_color.setRed(select_color(due_color.red() + 15)); 72 | due_color.setGreen(select_color(due_color.green() - 25)); 73 | QTextCharFormat hightlight; 74 | for (const auto &t : m_tasks) { 75 | if (!t.due.isNull()) { 76 | hightlight.setBackground(due_color); 77 | m_calendar->setDateTextFormat(t.due.toDate(), hightlight); 78 | } else if (!t.sched.isNull()) { 79 | hightlight.setBackground(sched_color); 80 | m_calendar->setDateTextFormat(t.sched.toDate(), hightlight); 81 | } 82 | } 83 | } 84 | 85 | void AgendaDialog::onUpdateTasks() 86 | { 87 | const auto date = m_calendar->selectedDate(); 88 | m_sched_tasks_list->clear(); 89 | m_due_tasks_list->clear(); 90 | for (const auto &t : m_tasks) { 91 | // Scheduled tasks 92 | if (!t.sched.isNull() && t.sched.toDate() == date) { 93 | QListWidgetItem *task_item = new QListWidgetItem; 94 | const auto text = QString{ "%1: %2" }.arg(t.uuid, t.description); 95 | task_item->setText(text); 96 | m_sched_tasks_list->addItem(task_item); 97 | } 98 | // Due tasks 99 | if (!t.due.isNull() && t.due.toDate() == date) { 100 | QListWidgetItem *task_item = new QListWidgetItem; 101 | const auto text = QString{ "%1: %2" }.arg(t.uuid, t.description); 102 | task_item->setText(text); 103 | m_due_tasks_list->addItem(task_item); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/agendadialog.hpp: -------------------------------------------------------------------------------- 1 | #ifndef AGENDADIALOG_HPP 2 | #define AGENDADIALOG_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "task.hpp" 9 | 10 | class AgendaDialog : public QDialog { 11 | Q_OBJECT 12 | 13 | public: 14 | AgendaDialog(const QList &, QWidget *parent = nullptr); 15 | ~AgendaDialog(); 16 | 17 | private: 18 | void initUI(); 19 | void setCalendarHighlight(); 20 | 21 | private slots: 22 | void onUpdateTasks(); 23 | 24 | private: 25 | QCalendarWidget *m_calendar; 26 | QListWidget *m_sched_tasks_list; 27 | QListWidget *m_due_tasks_list; 28 | QList m_tasks; 29 | }; 30 | 31 | #endif // AGENDADIALOG_HPP 32 | -------------------------------------------------------------------------------- /src/config.hpp.in: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_HPP 2 | #define CONFIG_HPP 3 | 4 | // clang-format off 5 | #cmakedefine QTASK_VERSION_MAJOR @QTASK_VERSION_MAJOR@ 6 | #ifndef QTASK_VERSION_MAJOR 7 | #define QTASK_VERSION_MAJOR 0 8 | #endif // QTASK_VERSION_MAJOR 9 | 10 | #cmakedefine QTASK_VERSION_MINOR @QTASK_VERSION_MINOR@ 11 | #ifndef QTASK_VERSION_MINOR 12 | #define QTASK_VERSION_MINOR 0 13 | #endif // QTASK_VERSION_MINOR 14 | 15 | #cmakedefine QTASK_VERSION_PATCH @QTASK_VERSION_PATCH@ 16 | #ifndef QTASK_VERSION_PATCH 17 | #define QTASK_VERSION_PATCH 0 18 | #endif // QTASK_VERSION_PATCH 19 | // clang-format on 20 | 21 | #endif // CONFIG_HPP 22 | -------------------------------------------------------------------------------- /src/configmanager.cpp: -------------------------------------------------------------------------------- 1 | #include "configmanager.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | ConfigManager *ConfigManager::inst_ = nullptr; 10 | 11 | const QString ConfigManager::s_default_task_bin = "/usr/bin/task"; 12 | const QString ConfigManager::s_default_task_data_path = 13 | QString("%1%2%3%2").arg(QDir::homePath(), QDir::separator(), ".task"); 14 | 15 | ConfigManager::ConfigManager(QObject *parent) 16 | : QObject(parent) 17 | , m_is_new(false) 18 | , m_config_path("") 19 | , m_task_bin(s_default_task_bin) 20 | , m_task_data_path(s_default_task_data_path) 21 | , m_show_task_shell(false) 22 | , m_hide_on_startup(false) 23 | , m_save_filter_on_exit(false) 24 | , m_task_filter({}) 25 | { 26 | } 27 | 28 | ConfigManager *ConfigManager::config() 29 | { 30 | if (inst_ == nullptr) 31 | inst_ = new ConfigManager(); 32 | return (inst_); 33 | } 34 | 35 | bool ConfigManager::initializeFromFile() 36 | { 37 | // QSettings ini(QString("%1%2%1").arg(arch), QSettings::IniFormat); 38 | 39 | // Try to locate existing config directory 40 | QVariant existsting_dir; 41 | foreach (auto const &p, QStandardPaths::standardLocations( 42 | QStandardPaths::ConfigLocation)) { 43 | QDir d(p); 44 | if (d.exists("qtask")) { 45 | existsting_dir = QString("%1%2qtask").arg(p, QDir::separator()); 46 | break; 47 | } 48 | } 49 | 50 | // Try to initialize a new config directory, if not found 51 | if (existsting_dir.isNull()) { 52 | foreach (auto const &p, QStandardPaths::standardLocations( 53 | QStandardPaths::ConfigLocation)) { 54 | QDir d(p); 55 | if (d.mkdir("qtask")) { 56 | existsting_dir = QString("%1%2qtask").arg(p, QDir::separator()); 57 | break; 58 | } 59 | } 60 | } 61 | 62 | if (existsting_dir.isNull()) { 63 | return false; 64 | } 65 | 66 | QDir config_dir(existsting_dir.toString()); 67 | m_config_path = config_dir.absoluteFilePath("qtask.ini"); 68 | if (!config_dir.exists("qtask.ini")) { 69 | if (!createNewConfigFile()) 70 | return false; 71 | m_is_new = true; 72 | return true; 73 | } 74 | 75 | return fillOptionsFromConfigFile(); 76 | } 77 | 78 | void ConfigManager::updateConfigFile() 79 | { 80 | QSettings settings(m_config_path, QSettings::IniFormat); 81 | if (!settings.isWritable()) 82 | return; 83 | settings.setValue("task_bin", m_task_bin); 84 | settings.setValue("task_data_path", m_task_data_path); 85 | settings.setValue("show_task_shell", m_show_task_shell); 86 | settings.setValue("hide_on_startup", m_hide_on_startup); 87 | settings.setValue("save_filter_on_exit", m_save_filter_on_exit); 88 | settings.setValue("task_filter", m_task_filter); 89 | } 90 | 91 | bool ConfigManager::createNewConfigFile() 92 | { 93 | QSettings settings(m_config_path, QSettings::IniFormat); 94 | if (!settings.isWritable()) 95 | return false; 96 | settings.setValue("task_bin", s_default_task_bin); 97 | settings.setValue("task_data_path", s_default_task_data_path); 98 | settings.setValue("show_task_shell", false); 99 | settings.setValue("hide_on_startup", false); 100 | settings.setValue("save_filter_on_exit", false); 101 | settings.setValue("task_filter", {}); 102 | 103 | return true; 104 | } 105 | 106 | bool ConfigManager::fillOptionsFromConfigFile() 107 | { 108 | QSettings settings(m_config_path, QSettings::IniFormat); 109 | m_task_bin = settings.value("task_bin", s_default_task_bin).toString(); 110 | m_task_data_path = 111 | settings.value("task_data_path", s_default_task_data_path).toString(); 112 | m_show_task_shell = settings.value("show_task_shell", false).toBool(); 113 | m_hide_on_startup = settings.value("hide_on_startup", false).toBool(); 114 | m_save_filter_on_exit = 115 | settings.value("save_filter_on_exit", false).toBool(); 116 | m_task_filter = settings.value("task_filter", "").toStringList(); 117 | return true; 118 | } 119 | -------------------------------------------------------------------------------- /src/configmanager.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONFIGMANAGER_HPP 2 | #define CONFIGMANAGER_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class ConfigManager : public QObject { 9 | Q_OBJECT 10 | 11 | public: 12 | ConfigManager(QObject *parent = nullptr); 13 | ~ConfigManager() = default; 14 | 15 | static ConfigManager *config(); 16 | 17 | bool isNew() const { return m_is_new; } 18 | 19 | QString getTaskBin() const { return m_task_bin; } 20 | void setTaskBin(const QString &v) { m_task_bin = v; } 21 | 22 | QString getTaskDataPath() const { return m_task_data_path; } 23 | void setTaskDataPath(const QString &v) 24 | { 25 | m_task_data_path = v; 26 | if (!v.isEmpty() && v[v.size() - 1] != QDir::separator()) 27 | m_task_data_path += QDir::separator(); 28 | } 29 | 30 | bool getShowTaskShell() const { return m_show_task_shell; } 31 | void setShowTaskShell(bool v) { m_show_task_shell = v; } 32 | 33 | bool getHideWindowOnStartup() const { return m_hide_on_startup; } 34 | void setHideWindowOnStartup(bool v) { m_hide_on_startup = v; } 35 | 36 | bool getSaveFilterOnExit() const { return m_save_filter_on_exit; } 37 | void setSaveFilterOnExit(bool v) { m_save_filter_on_exit = v; } 38 | 39 | QStringList getTaskFilter() const { return m_task_filter; } 40 | void setTaskFilter(const QStringList &v) { m_task_filter = v; } 41 | 42 | bool initializeFromFile(); 43 | 44 | void updateConfigFile(); 45 | 46 | private: 47 | bool createNewConfigFile(); 48 | bool fillOptionsFromConfigFile(); 49 | 50 | private: 51 | static ConfigManager *inst_; 52 | 53 | /// Configuration file was created during initialization 54 | bool m_is_new; 55 | 56 | /// Path to configuration file 57 | QString m_config_path; 58 | 59 | /// Path to task binary 60 | QString m_task_bin; 61 | static const QString s_default_task_bin; 62 | 63 | /// Path to taskwarrior data 64 | QString m_task_data_path; 65 | static const QString s_default_task_data_path; 66 | 67 | /// Task shell will be shown in the main window 68 | bool m_show_task_shell; 69 | 70 | /// QTask window is hidden on startup 71 | bool m_hide_on_startup; 72 | 73 | /// QTask will save current task filter on exit and apply it after restart. 74 | bool m_save_filter_on_exit; 75 | 76 | /// task filter from the previous launch. 77 | QStringList m_task_filter; 78 | }; 79 | 80 | #endif // CONFIGMANAGER_HPP 81 | -------------------------------------------------------------------------------- /src/datetimedialog.cpp: -------------------------------------------------------------------------------- 1 | #include "datetimedialog.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | DateTimeDialog::DateTimeDialog(QWidget *parent) 10 | : QDialog(parent) 11 | , m_datetime(QDateTime::currentDateTime()) 12 | { 13 | initUI(); 14 | } 15 | 16 | DateTimeDialog::DateTimeDialog(const QDateTime &datetime, QWidget *parent) 17 | : QDialog(parent) 18 | , m_datetime(datetime) 19 | { 20 | initUI(); 21 | } 22 | 23 | DateTimeDialog::~DateTimeDialog() {} 24 | 25 | QDateTime DateTimeDialog::getDateTime() const { return m_datetime; } 26 | 27 | void DateTimeDialog::initUI() 28 | { 29 | setSizeGripEnabled(false); 30 | resize(260, 230); 31 | 32 | setWindowTitle(QCoreApplication::applicationName() + " - Select date"); 33 | 34 | auto *vl = new QVBoxLayout(this); 35 | vl->setObjectName(QString::fromUtf8("verticalLayout")); 36 | vl->setContentsMargins(0, 0, 0, 0); 37 | 38 | m_calendar = new QCalendarWidget(this); 39 | m_calendar->setObjectName(QString::fromUtf8("m_calendar")); 40 | m_calendar->setSelectedDate(m_datetime.date()); 41 | vl->addWidget(m_calendar); 42 | connect(m_calendar, &QCalendarWidget::selectionChanged, this, [&]() { 43 | m_datetime_edit->setDate(m_calendar->selectedDate()); 44 | m_datetime = m_datetime_edit->dateTime(); 45 | }); 46 | 47 | m_datetime_edit = new QDateTimeEdit(this); 48 | m_datetime_edit->setObjectName(QString::fromUtf8("m_datetime_edit")); 49 | m_datetime_edit->setDateTime(m_datetime); 50 | vl->addWidget(m_datetime_edit); 51 | connect(m_datetime_edit, &QDateTimeEdit::dateTimeChanged, this, [&]() { 52 | m_datetime = m_datetime_edit->dateTime(); 53 | m_calendar->setSelectedDate(m_datetime.date()); 54 | }); 55 | 56 | auto *buttons = new QDialogButtonBox(this); 57 | buttons->setObjectName(QString::fromUtf8("buttons")); 58 | buttons->setOrientation(Qt::Horizontal); 59 | buttons->setStandardButtons(QDialogButtonBox::Cancel | 60 | QDialogButtonBox::Ok); 61 | vl->addWidget(buttons); 62 | 63 | connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); 64 | connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); 65 | } 66 | -------------------------------------------------------------------------------- /src/datetimedialog.hpp: -------------------------------------------------------------------------------- 1 | #ifndef DATETIMEDIALOG_HPP 2 | #define DATETIMEDIALOG_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class DateTimeDialog : public QDialog { 9 | Q_OBJECT 10 | 11 | public: 12 | DateTimeDialog(QWidget *parent = nullptr); 13 | DateTimeDialog(const QDateTime &, QWidget *parent = nullptr); 14 | ~DateTimeDialog(); 15 | 16 | QDateTime getDateTime() const; 17 | 18 | private: 19 | void initUI(); 20 | 21 | private: 22 | QDateTimeEdit *m_datetime_edit; 23 | QCalendarWidget *m_calendar; 24 | 25 | QDateTime m_datetime; 26 | }; 27 | 28 | #endif // DATETIMEDIALOG_HPP 29 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "config.hpp" 10 | #include "configmanager.hpp" 11 | #include "mainwindow.hpp" 12 | #include "singleapplication.h" 13 | #include "taskdialog.hpp" 14 | 15 | using namespace ui; 16 | 17 | int main(int argc, char *argv[]) 18 | { 19 | SingleApplication app(argc, argv, /*allowSecondary=*/true, 20 | SingleApplication::Mode::User | 21 | SingleApplication::Mode::SecondaryNotification); 22 | app.setQuitOnLastWindowClosed(false); // don't close app in tray 23 | app.setApplicationName("QTask"); 24 | app.setApplicationVersion(QString("%1.%2.%3") 25 | .arg(QString::number(QTASK_VERSION_MAJOR), 26 | QString::number(QTASK_VERSION_MINOR), 27 | QString::number(QTASK_VERSION_PATCH))); 28 | 29 | QCommandLineParser parser; 30 | parser.setApplicationDescription( 31 | "An open-source organizer based on Taskwarrior."); 32 | parser.addHelpOption(); 33 | 34 | QCommandLineOption add_task_option( 35 | "a", "Add a task without starting a new instance."); 36 | parser.addOption(add_task_option); 37 | 38 | if (!ConfigManager::config()->initializeFromFile()) { 39 | QMessageBox::warning( 40 | nullptr, QObject::tr("Warning"), 41 | QObject::tr("Can't read configuration file. The default " 42 | "settings will be used.")); 43 | } 44 | 45 | parser.process(app); 46 | 47 | if (parser.isSet(add_task_option)) { 48 | if (app.isSecondary()) { 49 | app.sendMessage("add_task"); 50 | return EXIT_SUCCESS; 51 | } else { 52 | auto *dlg = new AddTaskDialog(); 53 | if (dlg->exec() != QDialog::Accepted) 54 | return EXIT_SUCCESS; 55 | auto t = dlg->getTask(); 56 | Taskwarrior task_provider; 57 | return (task_provider.addTask(t)) ? EXIT_SUCCESS : EXIT_FAILURE; 58 | } 59 | } 60 | 61 | MainWindow main_win; 62 | main_win.resize(700, 200); 63 | 64 | // If this is a secondary instance 65 | if (app.isSecondary()) { 66 | qDebug() << "QTask already running."; 67 | qDebug() << "Primary instance PID: " << app.primaryPid(); 68 | qDebug() << "Primary instance user: " << app.primaryUser(); 69 | app.sendMessage("show"); 70 | return EXIT_SUCCESS; 71 | } else { 72 | QObject::connect(&app, &SingleApplication::receivedMessage, &main_win, 73 | &MainWindow::receiveNewInstanceMessage); 74 | } 75 | 76 | int rc = app.exec(); 77 | ConfigManager::config()->updateConfigFile(); 78 | return rc; 79 | } 80 | -------------------------------------------------------------------------------- /src/mainwindow.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include "aboutdialog.hpp" 19 | #include "agendadialog.hpp" 20 | #include "configmanager.hpp" 21 | #include "datetimedialog.hpp" 22 | #include "qtutil.hpp" 23 | #include "recurringdialog.hpp" 24 | #include "settingsdialog.hpp" 25 | #include "tagsedit.hpp" 26 | #include "taskdescriptiondelegate.hpp" 27 | #include "taskdialog.hpp" 28 | #include "tasksmodel.hpp" 29 | #include "tasksview.hpp" 30 | #include "taskwarrior.hpp" 31 | #include "taskwarriorreferencedialog.hpp" 32 | #include "trayicon.hpp" 33 | 34 | using namespace ui; 35 | 36 | MainWindow::MainWindow() 37 | : m_window_prev_state(Qt::WindowNoState) 38 | , m_is_quit(false) 39 | , m_task_provider(std::make_unique()) 40 | , m_task_watcher(nullptr) 41 | { 42 | if (!m_task_provider->init()) { 43 | QMessageBox::critical( 44 | this, tr("Error"), 45 | tr("Command 'task version' failed. Please make sure that " 46 | "taskwarrior is installed correctly and you have the correct " 47 | "path to the 'task' executable in the settings.")); 48 | } 49 | 50 | if (!initTaskWatcher()) { 51 | QMessageBox::warning( 52 | this, tr("Error"), 53 | tr("Can't initialize file watcher service for %1. The task " 54 | "list will not be updated after external changes.") 55 | .arg(ConfigManager::config()->getTaskDataPath())); 56 | } 57 | 58 | initMainWindow(); 59 | initTrayIcon(); 60 | initFileMenu(); 61 | initViewMenu(); 62 | initToolsMenu(); 63 | initHelpMenu(); 64 | initShortcuts(); 65 | 66 | (ConfigManager::config()->getHideWindowOnStartup()) ? hide() : show(); 67 | 68 | if (ConfigManager::config()->getSaveFilterOnExit()) { 69 | auto tags = ConfigManager::config()->getTaskFilter(); 70 | tags.removeAll(QString("")); 71 | if (!tags.isEmpty()) { 72 | m_task_filter->setTags(tags); 73 | onApplyFilter(); 74 | } 75 | } 76 | } 77 | 78 | MainWindow::~MainWindow() 79 | { 80 | if (m_task_watcher) { 81 | delete m_task_watcher; 82 | m_task_watcher = nullptr; 83 | } 84 | } 85 | 86 | bool MainWindow::initTaskWatcher() 87 | { 88 | Q_ASSERT(!m_task_watcher); 89 | m_task_watcher = new TaskWatcher(); 90 | if (!m_task_watcher->setup(ConfigManager::config()->getTaskDataPath())) { 91 | delete m_task_watcher; 92 | m_task_watcher = nullptr; 93 | return false; 94 | } 95 | connect( 96 | m_task_watcher, &TaskWatcher::dataChanged, this, 97 | [&](const QString & /* filepath */) { updateTasks(/*force=*/true); }); 98 | return true; 99 | } 100 | 101 | void MainWindow::initMainWindow() 102 | { 103 | setWindowIcon(QIcon(":/icons/qtask.svg")); 104 | setWindowTitle(QCoreApplication::applicationName()); 105 | setMinimumSize(400, 500); 106 | 107 | m_window = new QWidget(); 108 | 109 | m_layout = new QGridLayout(); 110 | 111 | initTaskToolbar(); 112 | 113 | initTasksTable(); 114 | 115 | m_task_shell = new QLineEdit(); 116 | m_task_shell->addAction(QIcon(":/icons/taskwarrior.png"), 117 | QLineEdit::LeadingPosition); 118 | connect(m_task_shell, &QLineEdit::returnPressed, this, 119 | &MainWindow::onEnterTaskCommand); 120 | m_task_shell->setVisible(ConfigManager::config()->getShowTaskShell()); 121 | 122 | m_task_filter = new TagsEdit(/* TODO: QIcon(":/icons/filter.svg")*/); 123 | connect(m_task_filter, &TagsEdit::tagsChanged, this, 124 | &MainWindow::onApplyFilter); 125 | 126 | m_layout->addWidget(m_task_toolbar, 0, 0); 127 | m_layout->addWidget(m_task_filter, 0, 1); 128 | m_layout->addWidget(m_tasks_view, 1, 0, 1, 2); 129 | m_layout->addWidget(m_task_shell, 2, 0, 1, 2); 130 | 131 | m_window->setLayout(m_layout); 132 | setCentralWidget(m_window); 133 | 134 | updateTasks(/*force=*/true); 135 | } 136 | 137 | void MainWindow::initTasksTable() 138 | { 139 | m_tasks_view = new TasksView(m_window); 140 | m_tasks_view->setShowGrid(true); 141 | 142 | m_tasks_view->verticalHeader()->setVisible(false); 143 | m_tasks_view->horizontalHeader()->setStretchLastSection(true); 144 | m_tasks_view->setSelectionBehavior(QAbstractItemView::SelectRows); 145 | 146 | TasksModel *model = new TasksModel(); 147 | m_tasks_view->setModel(model); 148 | m_tasks_view->setItemDelegateForColumn(2 /* description */, 149 | new TaskDescriptionDelegate(this)); 150 | 151 | connect(m_tasks_view->selectionModel(), 152 | &QItemSelectionModel::selectionChanged, this, 153 | &MainWindow::updateTaskToolbar); 154 | connect(m_tasks_view, &TasksView::pushProjectFilter, this, 155 | &MainWindow::pushFilterTag); 156 | connect(m_tasks_view, &QTableView::doubleClicked, this, 157 | &MainWindow::showEditTaskDialog); 158 | connect(m_tasks_view, &TasksView::linkActivated, this, 159 | [&](const QString &link) { 160 | if (!link.isEmpty()) { 161 | QDesktopServices::openUrl(link); 162 | } 163 | }); 164 | connect(m_tasks_view, &TasksView::selectedTaskIsActive, this, 165 | [&](bool is_active) { 166 | if (is_active) { 167 | removeShortcutFromToolTip(m_start_action); 168 | removeShortcutFromToolTip(m_stop_action); 169 | m_stop_action->setShortcut(tr("CTRL+S")); 170 | addShortcutToToolTip(m_stop_action); 171 | m_start_action->setEnabled(false); 172 | m_stop_action->setEnabled(true); 173 | } else { 174 | removeShortcutFromToolTip(m_stop_action); 175 | removeShortcutFromToolTip(m_start_action); 176 | m_start_action->setShortcut(tr("CTRL+S")); 177 | addShortcutToToolTip(m_start_action); 178 | m_start_action->setEnabled(true); 179 | m_stop_action->setEnabled(false); 180 | } 181 | }); 182 | 183 | m_tasks_view->installEventFilter(this); 184 | } 185 | 186 | void MainWindow::initTrayIcon() 187 | { 188 | m_tray_icon = new SystemTrayIcon(this); 189 | 190 | connect(m_tray_icon, &SystemTrayIcon::muteNotificationsRequested, this, 191 | &MainWindow::onMuteNotifications); 192 | connect(m_tray_icon, &SystemTrayIcon::addTaskRequested, this, 193 | &MainWindow::onAddTask); 194 | connect(m_tray_icon, &SystemTrayIcon::exitRequested, this, 195 | &MainWindow::quitApp); 196 | 197 | connect(m_tray_icon, &QSystemTrayIcon::activated, this, 198 | [this](QSystemTrayIcon::ActivationReason reason) { 199 | if (reason == QSystemTrayIcon::Trigger) { 200 | this->toggleMainWindow(); 201 | } 202 | }); 203 | 204 | m_tray_icon->show(); 205 | } 206 | 207 | void MainWindow::initFileMenu() 208 | { 209 | QMenu *file_menu = menuBar()->addMenu(tr("&File")); 210 | file_menu->setToolTipsVisible(true); 211 | 212 | QAction *settings = new QAction("&Settings", this); 213 | settings->setShortcut(tr("CTRL+SHIFT+P")); 214 | file_menu->addAction(settings); 215 | connect(settings, &QAction::triggered, this, &MainWindow::onOpenSettings); 216 | 217 | QAction *quit = new QAction("&Quit", this); 218 | quit->setShortcut(tr("CTRL+Q")); 219 | file_menu->addAction(quit); 220 | connect(quit, &QAction::triggered, this, &MainWindow::quitApp); 221 | } 222 | 223 | void MainWindow::initViewMenu() 224 | { 225 | QMenu *view_menu = menuBar()->addMenu(tr("&View")); 226 | view_menu->setToolTipsVisible(true); 227 | 228 | m_toggle_task_shell_action = new QAction("&Task shell", this); 229 | m_toggle_task_shell_action->setCheckable(true); 230 | m_toggle_task_shell_action->setChecked( 231 | ConfigManager::config()->getShowTaskShell()); 232 | view_menu->addAction(m_toggle_task_shell_action); 233 | connect(m_toggle_task_shell_action, &QAction::triggered, this, 234 | &MainWindow::onToggleTaskShell); 235 | } 236 | 237 | void MainWindow::initToolsMenu() 238 | { 239 | QMenu *tools_menu = menuBar()->addMenu(tr("&Tools")); 240 | tools_menu->setToolTipsVisible(true); 241 | 242 | QAction *agenda_action = new QAction("&Agenda view", this); 243 | tools_menu->addAction(agenda_action); 244 | agenda_action->setShortcut(tr("ALT+A")); 245 | connect(agenda_action, &QAction::triggered, this, [&]() { 246 | QList tasks = {}; 247 | if (!m_task_provider->getTasks(tasks)) 248 | return; 249 | auto *dlg = new AgendaDialog(tasks, this); 250 | dlg->open(); 251 | QObject::connect(dlg, &QDialog::finished, dlg, &QDialog::deleteLater); 252 | }); 253 | 254 | QAction *recurring_action = new QAction("&Recurring templates", this); 255 | tools_menu->addAction(recurring_action); 256 | recurring_action->setShortcut(tr("ALT+R")); 257 | connect(recurring_action, &QAction::triggered, this, [&]() { 258 | QList tasks; 259 | if (!m_task_provider->getRecurringTasks(tasks)) 260 | return; 261 | auto *dlg = new RecurringDialog(tasks, this); 262 | dlg->open(); 263 | QObject::connect(dlg, &QDialog::finished, dlg, &QDialog::deleteLater); 264 | }); 265 | 266 | QAction *history_stats_action = new QAction("&Statistics", this); 267 | history_stats_action->setEnabled(false); 268 | tools_menu->addAction(history_stats_action); 269 | connect(history_stats_action, &QAction::triggered, this, 270 | &MainWindow::onToggleTaskShell); 271 | 272 | tools_menu->setVisible(false); 273 | } 274 | 275 | void MainWindow::initHelpMenu() 276 | { 277 | QMenu *help_menu = menuBar()->addMenu(tr("&Help")); 278 | help_menu->setToolTipsVisible(true); 279 | 280 | QAction *reference_action = 281 | new QAction("&Taskwarrior quick reference", this); 282 | help_menu->addAction(reference_action); 283 | connect(reference_action, &QAction::triggered, this, [&]() { 284 | auto *dlg = new TaskwarriorReferenceDialog(this); 285 | dlg->open(); 286 | QObject::connect(dlg, &QDialog::finished, dlg, &QDialog::deleteLater); 287 | }); 288 | 289 | help_menu->addSeparator(); 290 | 291 | QAction *about_action = new QAction("&About", this); 292 | help_menu->addAction(about_action); 293 | about_action->setShortcut(tr("F1")); 294 | connect(about_action, &QAction::triggered, this, [&]() { 295 | auto *dlg = new AboutDialog(m_task_provider->getTaskVersion(), this); 296 | dlg->open(); 297 | QObject::connect(dlg, &QDialog::finished, dlg, &QDialog::deleteLater); 298 | }); 299 | } 300 | 301 | void MainWindow::initShortcuts() 302 | { 303 | QAction *focus_task_shell = new QAction(this); 304 | focus_task_shell->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_T)); 305 | connect(focus_task_shell, &QAction::triggered, this, [&]() { 306 | if (m_task_shell->isVisible()) { 307 | m_task_shell->setFocus(); 308 | } 309 | }); 310 | this->addAction(focus_task_shell); 311 | 312 | QAction *filter_tasks = new QAction(this); 313 | filter_tasks->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_F)); 314 | connect(filter_tasks, &QAction::triggered, this, 315 | [&]() { m_task_filter->setFocus(); }); 316 | this->addAction(filter_tasks); 317 | } 318 | 319 | void MainWindow::initTaskToolbar() 320 | { 321 | m_task_toolbar = new QToolBar("Task toolbar"); 322 | 323 | m_add_action = new QAction(QIcon(":/icons/add.svg"), tr("Add task")); 324 | connect(m_add_action, &QAction::triggered, this, [&]() { 325 | m_tasks_view->selectionModel()->clearSelection(); 326 | onAddTask(); 327 | }); 328 | m_add_action->setShortcut(tr("CTRL+N")); 329 | removeShortcutFromToolTip(m_add_action); 330 | addShortcutToToolTip(m_add_action); 331 | m_task_toolbar->addAction(m_add_action); 332 | 333 | m_undo_action = 334 | new QAction(QIcon(":/icons/undo.png"), tr("Undo last action")); 335 | connect(m_undo_action, &QAction::triggered, this, [&]() { 336 | if (m_task_provider->undoTask()) 337 | m_tasks_view->selectionModel()->clearSelection(); 338 | m_undo_action->setEnabled(m_task_provider->getActionsCounter() > 0); 339 | }); 340 | m_undo_action->setEnabled(false); 341 | m_undo_action->setShortcut(tr("CTRL+Z")); 342 | removeShortcutFromToolTip(m_undo_action); 343 | addShortcutToToolTip(m_undo_action); 344 | m_task_toolbar->addAction(m_undo_action); 345 | 346 | m_update_action = 347 | new QAction(QIcon(":/icons/refresh.png"), tr("Update tasks")); 348 | connect(m_update_action, &QAction::triggered, this, [&]() { 349 | m_tasks_view->selectionModel()->clearSelection(); 350 | updateTasks(/*force=*/true); 351 | }); 352 | m_update_action->setShortcut(tr("CTRL+R")); 353 | removeShortcutFromToolTip(m_update_action); 354 | addShortcutToToolTip(m_update_action); 355 | m_task_toolbar->addAction(m_update_action); 356 | 357 | m_task_toolbar->addSeparator(); 358 | 359 | m_done_action = new QAction(QIcon(":/icons/done.svg"), tr("Done")); 360 | connect(m_done_action, &QAction::triggered, this, 361 | &MainWindow::onSetTasksDone); 362 | m_done_action->setShortcut(tr("CTRL+D")); 363 | removeShortcutFromToolTip(m_done_action); 364 | addShortcutToToolTip(m_done_action); 365 | m_task_toolbar->addAction(m_done_action); 366 | m_task_toolbar->addAction(m_done_action); 367 | m_done_action->setEnabled(false); 368 | 369 | m_edit_action = new QAction(QIcon(":/icons/edit.svg"), tr("Edit")); 370 | connect(m_edit_action, &QAction::triggered, this, 371 | &MainWindow::onEditTaskAction); 372 | m_edit_action->setShortcut(tr("CTRL+E")); 373 | removeShortcutFromToolTip(m_edit_action); 374 | addShortcutToToolTip(m_edit_action); 375 | m_task_toolbar->addAction(m_edit_action); 376 | m_edit_action->setEnabled(false); 377 | 378 | m_wait_action = new QAction(QIcon(":/icons/wait.svg"), tr("Wait")); 379 | connect(m_wait_action, &QAction::triggered, this, [&]() { 380 | auto *dlg = 381 | new DateTimeDialog(QDateTime::currentDateTime().addDays(1), this); 382 | dlg->open(); 383 | QObject::connect(dlg, &QDialog::accepted, [this, dlg]() { 384 | auto dt = dlg->getDateTime(); 385 | if (m_task_provider->waitTask(getSelectedTaskIds(), dt)) 386 | updateTasks(); 387 | }); 388 | QObject::connect(dlg, &QDialog::finished, dlg, &QDialog::deleteLater); 389 | }); 390 | m_wait_action->setShortcut(tr("CTRL+W")); 391 | removeShortcutFromToolTip(m_wait_action); 392 | addShortcutToToolTip(m_wait_action); 393 | m_task_toolbar->addAction(m_wait_action); 394 | m_wait_action->setEnabled(false); 395 | 396 | m_delete_action = new QAction(QIcon(":/icons/delete.svg"), tr("Delete")); 397 | connect(m_delete_action, &QAction::triggered, this, 398 | &MainWindow::onDeleteTasks); 399 | m_delete_action->setShortcut(tr("Delete")); 400 | removeShortcutFromToolTip(m_delete_action); 401 | addShortcutToToolTip(m_delete_action); 402 | m_task_toolbar->addAction(m_delete_action); 403 | m_delete_action->setEnabled(false); 404 | 405 | m_task_toolbar->addSeparator(); 406 | 407 | m_start_action = new QAction(QIcon(":/icons/start.svg"), tr("Start")); 408 | connect(m_start_action, &QAction::triggered, this, [&]() { 409 | auto t_opt = getSelectedTaskId(); 410 | if (t_opt.isNull()) 411 | return; 412 | m_task_provider->startTask(t_opt.toString()); 413 | updateTasks(); 414 | }); 415 | m_task_toolbar->addAction(m_start_action); 416 | m_start_action->setEnabled(false); 417 | 418 | m_stop_action = new QAction(QIcon(":/icons/stop.svg"), tr("Stop")); 419 | connect(m_stop_action, &QAction::triggered, this, [&]() { 420 | auto t_opt = getSelectedTaskId(); 421 | if (t_opt.isNull()) 422 | return; 423 | m_task_provider->stopTask(t_opt.toString()); 424 | updateTasks(); 425 | }); 426 | m_task_toolbar->addAction(m_stop_action); 427 | m_stop_action->setEnabled(false); 428 | } 429 | 430 | void MainWindow::toggleMainWindow() 431 | { 432 | if (this->isHidden()) { 433 | showMainWindow(); 434 | } else { 435 | this->hide(); 436 | } 437 | } 438 | 439 | void MainWindow::onOpenSettings() 440 | { 441 | auto *dlg = new SettingsDialog(this); 442 | dlg->open(); 443 | QObject::connect(dlg, &QDialog::finished, dlg, &QDialog::deleteLater); 444 | } 445 | 446 | void MainWindow::showMainWindow() 447 | { 448 | if (this->isMinimized()) { 449 | if (m_window_prev_state & Qt::WindowMaximized) { 450 | this->showMaximized(); 451 | } else if (m_window_prev_state & Qt::WindowFullScreen) { 452 | this->showFullScreen(); 453 | } else { 454 | this->showNormal(); 455 | } 456 | } else { 457 | this->show(); 458 | this->raise(); 459 | } 460 | 461 | this->activateWindow(); 462 | } 463 | 464 | void MainWindow::receiveNewInstanceMessage(quint32, QByteArray message) 465 | { 466 | if (message == "add_task") { 467 | onAddTask(); 468 | } else { 469 | showMainWindow(); 470 | } 471 | } 472 | 473 | void MainWindow::quitApp() 474 | { 475 | if (ConfigManager::config()->getSaveFilterOnExit()) { 476 | ConfigManager::config()->setTaskFilter(m_task_filter->getTags()); 477 | } 478 | 479 | m_is_quit = true; 480 | close(); 481 | } 482 | 483 | bool MainWindow::eventFilter(QObject *watched, QEvent *event) 484 | { 485 | auto set_task_priority = [&](Task::Priority p) { 486 | auto *smodel = m_tasks_view->selectionModel(); 487 | if (!smodel->hasSelection()) 488 | return; 489 | auto *dmodel = qobject_cast(m_tasks_view->model()); 490 | for (const QModelIndex idx : smodel->selectedRows()) { 491 | auto item = dmodel->itemData(idx); 492 | if (item[0].isNull()) 493 | continue; 494 | QString tid_str = item[0].toString(); 495 | m_task_provider->setPriority(tid_str, p); 496 | } 497 | }; 498 | 499 | if (watched == m_tasks_view && event->type() == QEvent::KeyPress) { 500 | if (static_cast(event)->key() == Qt::Key_Escape) { 501 | m_tasks_view->selectionModel()->clearSelection(); 502 | return true; 503 | } 504 | if (static_cast(event)->key() == Qt::Key_0) { 505 | set_task_priority(Task::Priority::Unset); 506 | return true; 507 | } 508 | if (static_cast(event)->key() == Qt::Key_3) { 509 | set_task_priority(Task::Priority::L); 510 | return true; 511 | } 512 | if (static_cast(event)->key() == Qt::Key_2) { 513 | set_task_priority(Task::Priority::M); 514 | return true; 515 | } 516 | if (static_cast(event)->key() == Qt::Key_1) { 517 | set_task_priority(Task::Priority::H); 518 | return true; 519 | } 520 | } 521 | 522 | // Let any other handlers do their thing 523 | return false; 524 | } 525 | 526 | void MainWindow::changeEvent(QEvent *evt) 527 | { 528 | if (evt->type() == QEvent::WindowStateChange) { 529 | QWindowStateChangeEvent *e = 530 | dynamic_cast(evt); 531 | m_window_prev_state = e->oldState(); 532 | } 533 | 534 | QMainWindow::changeEvent(evt); 535 | } 536 | 537 | void MainWindow::closeEvent(QCloseEvent *event) 538 | { 539 | if (!m_is_quit && m_tray_icon->isVisible()) { 540 | hide(); 541 | event->ignore(); 542 | } else { 543 | ConfigManager::config()->updateConfigFile(); 544 | QMainWindow::closeEvent(event); 545 | qApp->quit(); 546 | } 547 | } 548 | 549 | QVariant MainWindow::getSelectedTaskId() 550 | { 551 | Q_ASSERT(m_tasks_view); 552 | 553 | auto *smodel = m_tasks_view->selectionModel(); 554 | if (smodel->selectedRows().size() != 1) 555 | return {}; 556 | 557 | auto *dmodel = qobject_cast(m_tasks_view->model()); 558 | for (const QModelIndex idx : smodel->selectedRows()) { 559 | auto item = dmodel->itemData(idx); 560 | if (item[0].isNull()) 561 | continue; 562 | return item[0].toString(); 563 | } 564 | 565 | return {}; 566 | } 567 | 568 | QStringList MainWindow::getSelectedTaskIds() 569 | { 570 | auto *smodel = m_tasks_view->selectionModel(); 571 | if (!smodel->hasSelection()) 572 | return {}; 573 | 574 | QStringList ids; 575 | 576 | auto *dmodel = qobject_cast(m_tasks_view->model()); 577 | for (const QModelIndex idx : smodel->selectedRows()) { 578 | auto item = dmodel->itemData(idx); 579 | if (item[0].isNull()) 580 | continue; 581 | ids.push_back(item[0].toString()); 582 | } 583 | 584 | return ids; 585 | } 586 | 587 | void MainWindow::pushFilterTag(const QString &value) 588 | { 589 | if (!m_task_filter->isVisible()) 590 | return; 591 | m_task_filter->pushTag(value); 592 | } 593 | 594 | void MainWindow::onToggleTaskShell() 595 | { 596 | if (m_toggle_task_shell_action->isChecked()) { 597 | m_task_shell->setVisible(true); 598 | ConfigManager::config()->setShowTaskShell(true); 599 | } else { 600 | m_task_shell->setVisible(false); 601 | ConfigManager::config()->setShowTaskShell(false); 602 | } 603 | } 604 | 605 | void MainWindow::onSettingsMenu() {} 606 | 607 | void MainWindow::onMuteNotifications() {} 608 | 609 | void MainWindow::onAddTask() 610 | { 611 | QVariant default_project = {}; 612 | for (const auto &tag : m_task_filter->getTags()) { 613 | if (tag.startsWith("pro:") || tag.startsWith("project:")) { 614 | if (!default_project.isNull()) { 615 | default_project = {}; 616 | break; 617 | } 618 | default_project = { tag.mid(tag.indexOf(':') + 1, tag.size()) }; 619 | } 620 | } 621 | 622 | auto *dlg = new AddTaskDialog(default_project, this); 623 | dlg->open(); 624 | 625 | QObject::connect(this, &MainWindow::acceptContinueCreatingTasks, dlg, 626 | &AddTaskDialog::acceptContinue); 627 | QObject::connect(dlg, &QDialog::accepted, [this, dlg]() { 628 | auto t = dlg->getTask(); 629 | if (m_task_provider->addTask(t)) 630 | updateTasks(); 631 | }); 632 | QObject::connect(dlg, &QDialog::rejected, [this, dlg]() { updateTasks(); }); 633 | QObject::connect(dlg, &AddTaskDialog::createTaskAndContinue, [this, dlg]() { 634 | auto t = dlg->getTask(); 635 | if (m_task_provider->addTask(t)) { 636 | updateTasks(); 637 | emit acceptContinueCreatingTasks(); 638 | } else { 639 | dlg->close(); 640 | } 641 | }); 642 | QObject::connect(dlg, &QDialog::finished, dlg, &QDialog::deleteLater); 643 | } 644 | 645 | void MainWindow::onDeleteTasks() 646 | { 647 | auto *smodel = m_tasks_view->selectionModel(); 648 | QString q; 649 | if (smodel->selectedRows().size() == 1) 650 | q = tr("Delete task?"); 651 | else 652 | q = tr("Delete %1 tasks?").arg(smodel->selectedRows().size()); 653 | QMessageBox::StandardButton reply; 654 | reply = QMessageBox::question(this, tr("Conifrm action"), q, 655 | QMessageBox::Yes | QMessageBox::No); 656 | if (reply == QMessageBox::Yes) { 657 | m_task_provider->deleteTask(getSelectedTaskIds()); 658 | smodel->clearSelection(); 659 | updateTasks(); 660 | } 661 | } 662 | 663 | void MainWindow::onSetTasksDone() 664 | { 665 | if (!m_tasks_view->selectionModel()->hasSelection()) 666 | return; 667 | m_task_provider->setTaskDone(getSelectedTaskIds()); 668 | m_tasks_view->selectionModel()->clearSelection(); 669 | updateTasks(); 670 | } 671 | 672 | void MainWindow::onApplyFilter() 673 | { 674 | if (!m_task_provider->applyFilter(m_task_filter->getTags())) 675 | m_task_filter->popTag(); 676 | // if (!m_task_provider->applyFilter(m_task_filter->getTags())) 677 | // m_task_filter->clearTags(); 678 | updateTasks(/*force=*/true); 679 | } 680 | 681 | void MainWindow::onEnterTaskCommand() 682 | { 683 | auto cmd = m_task_shell->text(); 684 | if (cmd.isEmpty()) 685 | return; 686 | auto rc = m_task_provider->directCmd(cmd); 687 | if (rc == 0) { 688 | updateTasks(); 689 | } 690 | m_task_shell->setText(""); 691 | } 692 | 693 | void MainWindow::showEditTaskDialog(const QModelIndex &idx) 694 | { 695 | const auto *model = m_tasks_view->selectionModel(); 696 | 697 | QString id_str = model->selectedRows()[0].data().toString(); 698 | if (id_str.isEmpty()) { 699 | updateTasks(); 700 | return; 701 | } 702 | 703 | Task task; 704 | if (!m_task_provider->getTask(id_str, task)) { 705 | updateTasks(); 706 | return; 707 | } 708 | 709 | auto *dlg = new EditTaskDialog(task, this); 710 | dlg->open(); 711 | 712 | QObject::connect(dlg, &EditTaskDialog::deleteTask, this, 713 | [&](const QString &uuid) { 714 | m_task_provider->deleteTask(uuid); 715 | m_tasks_view->selectionModel()->clearSelection(); 716 | updateTasks(); 717 | }); 718 | QObject::connect(dlg, &QDialog::accepted, [this, dlg, id_str, task]() { 719 | auto saved_tags = task.tags; 720 | auto saved_pri = task.priority; 721 | auto t = dlg->getTask(); 722 | auto new_tags = t.tags; 723 | t.removed_tags = QStringList(); 724 | for (auto const &st : saved_tags) 725 | if (!new_tags.contains(st)) 726 | t.removed_tags.push_back(st); 727 | t.uuid = id_str; 728 | if (!m_task_provider->editTask(t)) 729 | return; 730 | if (saved_pri != t.priority && 731 | !m_task_provider->setPriority(t.uuid, t.priority)) 732 | return; 733 | updateTasks(); 734 | }); 735 | QObject::connect(dlg, &QDialog::rejected, [this, dlg]() { updateTasks(); }); 736 | QObject::connect(dlg, &QDialog::finished, dlg, &QDialog::deleteLater); 737 | } 738 | 739 | void MainWindow::onEditTaskAction() 740 | { 741 | const auto *model = m_tasks_view->selectionModel(); 742 | Q_ASSERT(model->selectedRows().size() == 1); 743 | showEditTaskDialog(model->selectedRows()[0]); 744 | } 745 | 746 | void MainWindow::updateTasks(bool force) 747 | { 748 | Q_ASSERT(m_task_provider); 749 | 750 | // The commands from m_task_provider will modify the taskwarrior 751 | // database files. This will trigger TaskWatcher to emit update event. 752 | if (!force && m_task_watcher && m_task_watcher->isActive()) 753 | return; 754 | 755 | QList tasks = {}; 756 | m_task_provider->getTasks(tasks); 757 | 758 | auto *model = qobject_cast(m_tasks_view->model()); 759 | model->setTasks(std::move(tasks)); 760 | 761 | updateTaskToolbar(); 762 | } 763 | 764 | void MainWindow::updateTaskToolbar() 765 | { 766 | const auto *model = m_tasks_view->selectionModel(); 767 | const int num_selected = model->selectedRows().size(); 768 | 769 | m_undo_action->setEnabled(m_task_provider->getActionsCounter() > 0); 770 | 771 | if (num_selected == 0) { 772 | m_done_action->setEnabled(false); 773 | m_edit_action->setEnabled(false); 774 | m_wait_action->setEnabled(false); 775 | m_delete_action->setEnabled(false); 776 | m_start_action->setEnabled(false); 777 | m_stop_action->setEnabled(false); 778 | removeShortcutFromToolTip(m_stop_action); 779 | removeShortcutFromToolTip(m_start_action); 780 | } else { 781 | m_done_action->setEnabled(true); 782 | if (num_selected == 1) { 783 | m_edit_action->setEnabled(true); 784 | } else { 785 | m_edit_action->setEnabled(false); 786 | } 787 | m_wait_action->setEnabled(true); 788 | m_delete_action->setEnabled(true); 789 | } 790 | } 791 | -------------------------------------------------------------------------------- /src/mainwindow.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_HPP 2 | #define MAINWINDOW_HPP 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "tagsedit.hpp" 16 | #include "task.hpp" 17 | #include "tasksview.hpp" 18 | #include "taskwarrior.hpp" 19 | #include "taskwatcher.hpp" 20 | #include "trayicon.hpp" 21 | 22 | namespace ui 23 | { 24 | class MainWindow : public QMainWindow { 25 | Q_OBJECT 26 | 27 | public: 28 | MainWindow(); 29 | ~MainWindow(); 30 | 31 | private: 32 | bool initTaskWatcher(); 33 | 34 | void initMainWindow(); 35 | void initTasksTable(); 36 | void initTrayIcon(); 37 | void initFileMenu(); 38 | void initViewMenu(); 39 | void initToolsMenu(); 40 | void initHelpMenu(); 41 | void initShortcuts(); 42 | void initTaskToolbar(); 43 | void toggleMainWindow(); 44 | void onOpenSettings(); 45 | void quitApp(); 46 | 47 | bool eventFilter(QObject *watched, QEvent *event) override; 48 | void changeEvent(QEvent *) override; 49 | void closeEvent(QCloseEvent *event) override; 50 | 51 | QVariant getSelectedTaskId(); 52 | QStringList getSelectedTaskIds(); 53 | 54 | public slots: 55 | /// Add entry to tags filter 56 | void pushFilterTag(const QString &); 57 | 58 | /// Show this window if minimized 59 | void showMainWindow(); 60 | 61 | /// Receive a message from a secondary QTask instance 62 | void receiveNewInstanceMessage(quint32 instanceId, QByteArray message); 63 | 64 | private slots: 65 | void onToggleTaskShell(); 66 | void onSettingsMenu(); 67 | void onMuteNotifications(); 68 | void onAddTask(); 69 | void onDeleteTasks(); 70 | void onSetTasksDone(); 71 | void onEnterTaskCommand(); 72 | void onApplyFilter(); 73 | void onEditTaskAction(); 74 | void showEditTaskDialog(const QModelIndex &); 75 | 76 | void updateTasks(bool force = false); 77 | void updateTaskToolbar(); 78 | 79 | signals: 80 | void acceptContinueCreatingTasks(); 81 | 82 | private: 83 | /// The previous state of the window 84 | Qt::WindowStates m_window_prev_state; 85 | 86 | /// Flag to decide: close application or hide it to tray 87 | bool m_is_quit; 88 | 89 | QAction *m_toggle_task_shell_action; 90 | 91 | QWidget *m_window; 92 | QGridLayout *m_layout; 93 | SystemTrayIcon *m_tray_icon; 94 | TasksView *m_tasks_view; 95 | QToolBar *m_task_toolbar; 96 | QLineEdit *m_task_shell; 97 | TagsEdit *m_task_filter; 98 | 99 | // Toolbar actions 100 | QAction *m_add_action; 101 | QAction *m_undo_action; 102 | QAction *m_update_action; 103 | QAction *m_done_action; 104 | QAction *m_edit_action; 105 | QAction *m_wait_action; 106 | QAction *m_delete_action; 107 | QAction *m_start_action; 108 | QAction *m_stop_action; 109 | 110 | std::unique_ptr m_task_provider; 111 | 112 | TaskWatcher *m_task_watcher; 113 | }; 114 | 115 | } // namespace ui 116 | 117 | #endif // MAINWINDOW_HPP 118 | -------------------------------------------------------------------------------- /src/optionaldatetimeedit.cpp: -------------------------------------------------------------------------------- 1 | #include "optionaldatetimeedit.hpp" 2 | #include "qtutil.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | OptionalDateTimeEdit::OptionalDateTimeEdit(const QString &label, 11 | const QDateTime &dt, QWidget *parent) 12 | : QWidget(parent) 13 | { 14 | initUI(label); 15 | setDateTime(dt); 16 | } 17 | 18 | OptionalDateTimeEdit::OptionalDateTimeEdit(const QString &label, 19 | QWidget *parent) 20 | : QWidget(parent) 21 | { 22 | initUI(label); 23 | setDateTime(startOfDay(QDate{ 1970, 1, 1 })); 24 | } 25 | 26 | OptionalDateTimeEdit::~OptionalDateTimeEdit() {} 27 | 28 | QVariant OptionalDateTimeEdit::getDateTime() const 29 | { 30 | return (m_enabled->isChecked()) ? QVariant{ m_datetime_edit->dateTime() } 31 | : QVariant{}; 32 | } 33 | 34 | void OptionalDateTimeEdit::setDateTime(const QDateTime &dt) 35 | { 36 | m_datetime_edit->setDateTime(dt); 37 | } 38 | 39 | void OptionalDateTimeEdit::setDateTime(const QVariant &dt_opt) 40 | { 41 | if (dt_opt.isNull()) { 42 | setChecked(false); 43 | } else { 44 | m_datetime_edit->setDateTime(dt_opt.toDateTime()); 45 | setChecked(true); 46 | } 47 | } 48 | 49 | void OptionalDateTimeEdit::setChecked(bool state) 50 | { 51 | m_enabled->setChecked(state); 52 | } 53 | 54 | void OptionalDateTimeEdit::setMinimumDateTime(const QDateTime &dt) 55 | { 56 | m_datetime_edit->setMinimumDateTime(dt); 57 | } 58 | 59 | void OptionalDateTimeEdit::setMaximumDateTime(const QDateTime &dt) 60 | { 61 | m_datetime_edit->setMaximumDateTime(dt); 62 | } 63 | 64 | void OptionalDateTimeEdit::initUI(const QString &label) 65 | { 66 | auto layout = new QHBoxLayout(this); 67 | 68 | m_enabled = new QCheckBox(label); 69 | connect(m_enabled, &QCheckBox::stateChanged, this, 70 | [&]() { m_datetime_edit->setEnabled(m_enabled->isChecked()); }); 71 | layout->addWidget(m_enabled); 72 | 73 | m_datetime_edit = new QDateTimeEdit(); 74 | m_datetime_edit->setCalendarPopup(true); 75 | layout->addWidget(m_datetime_edit); 76 | 77 | setChecked(true); 78 | } 79 | -------------------------------------------------------------------------------- /src/optionaldatetimeedit.hpp: -------------------------------------------------------------------------------- 1 | #ifndef OPTIONALDATETIMEEDIT_HPP 2 | #define OPTIONALDATETIMEEDIT_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class OptionalDateTimeEdit : public QWidget { 10 | Q_OBJECT 11 | 12 | public: 13 | OptionalDateTimeEdit(const QString &label, const QDateTime &date, 14 | QWidget *parent = nullptr); 15 | OptionalDateTimeEdit(const QString &label, QWidget *parent = nullptr); 16 | ~OptionalDateTimeEdit(); 17 | 18 | QVariant getDateTime() const; 19 | void setDateTime(const QDateTime &dt); 20 | void setDateTime(const QVariant &); 21 | void setChecked(bool); 22 | 23 | void setMinimumDateTime(const QDateTime &); 24 | void setMaximumDateTime(const QDateTime &); 25 | 26 | private: 27 | void initUI(const QString &label); 28 | 29 | private: 30 | QDateTimeEdit *m_datetime_edit; 31 | QCheckBox *m_enabled; 32 | }; 33 | 34 | #endif // OPTIONALDATETIMEEDIT_HPP 35 | -------------------------------------------------------------------------------- /src/qtutil.cpp: -------------------------------------------------------------------------------- 1 | #include "qtutil.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // Guesses a descriptive text from a text suited for a menu entry. 13 | // This is equivalent to QActions internal qt_strippedText(). 14 | static QString strippedActionText(QString s) 15 | { 16 | s.remove(QString::fromLatin1("...")); 17 | for (int i = 0; i < s.size(); ++i) { 18 | if (s.at(i) == QLatin1Char('&')) 19 | s.remove(i, 1); 20 | } 21 | return s.trimmed(); 22 | } 23 | 24 | void addShortcutToToolTip(QAction *action) 25 | { 26 | if (!action->shortcut().isEmpty()) { 27 | QString tooltip = action->property("tooltipBackup").toString(); 28 | if (tooltip.isEmpty()) { 29 | tooltip = action->toolTip(); 30 | if (tooltip != strippedActionText(action->text())) { 31 | action->setProperty( 32 | "tooltipBackup", 33 | action->toolTip()); // action uses a custom tooltip. Backup 34 | // so that we can restore it later. 35 | } 36 | } 37 | QColor shortcutTextColor = 38 | QApplication::palette().color(QPalette::ToolTipText); 39 | QString shortCutTextColorName; 40 | if (shortcutTextColor.value() == 0) { 41 | shortCutTextColorName = 42 | "gray"; // special handling for black because lighter() does not 43 | // work there [QTBUG-9343]. 44 | } else { 45 | int factor = (shortcutTextColor.value() < 128) ? 150 : 50; 46 | shortCutTextColorName = shortcutTextColor.lighter(factor).name(); 47 | } 48 | action->setToolTip( 49 | QString("

%1  %3

") 51 | .arg(tooltip, shortCutTextColorName, 52 | action->shortcut().toString(QKeySequence::NativeText))); 53 | } 54 | } 55 | 56 | void removeShortcutFromToolTip(QAction *action) 57 | { 58 | action->setToolTip(action->property("tooltipBackup").toString()); 59 | action->setProperty("tooltipBackup", QVariant()); 60 | } 61 | 62 | // clang-format off 63 | // https://github.com/qt/qtbase/tree/ae2c30942086bd0387c6d5297c0cb85b505f29a0/src/corelib/time/qdatetime.cpp#L762 64 | static QDateTime toEarliest(QDate day, const QDateTime &form) 65 | { 66 | const Qt::TimeSpec spec = form.timeSpec(); 67 | const int offset = (spec == Qt::OffsetFromUTC) ? form.offsetFromUtc() : 0; 68 | #if QT_CONFIG(timezone) 69 | QTimeZone zone; 70 | if (spec == Qt::TimeZone) 71 | zone = form.timeZone(); 72 | #endif 73 | auto moment = [=](QTime time) { 74 | switch (spec) { 75 | case Qt::OffsetFromUTC: 76 | return QDateTime(day, time, spec, offset); 77 | #if QT_CONFIG(timezone) 78 | case Qt::TimeZone: 79 | return QDateTime(day, time, zone); 80 | #endif 81 | default: 82 | return QDateTime(day, time, spec); 83 | } 84 | }; 85 | // Longest routine time-zone transition is 2 hours: 86 | QDateTime when = moment(QTime(2, 0)); 87 | if (!when.isValid()) { 88 | // Noon should be safe ... 89 | when = moment(QTime(12, 0)); 90 | if (!when.isValid()) { 91 | // ... unless it's a 24-hour jump (moving the date-line) 92 | when = moment(QTime(23, 59, 59, 999)); 93 | if (!when.isValid()) 94 | return QDateTime(); 95 | } 96 | } 97 | int high = when.time().msecsSinceStartOfDay() / 60000; 98 | int low = 0; 99 | // Binary chop to the right minute 100 | while (high > low + 1) { 101 | int mid = (high + low) / 2; 102 | QDateTime probe = moment(QTime(mid / 60, mid % 60)); 103 | if (probe.isValid() && probe.date() == day) { 104 | high = mid; 105 | when = probe; 106 | } else { 107 | low = mid; 108 | } 109 | } 110 | return when; 111 | } 112 | // clang-format on 113 | 114 | QDateTime startOfDay(const QDate &date) 115 | { 116 | QDateTime when(date, QTime(0, 0), Qt::LocalTime); 117 | if (!when.isValid()) 118 | when = toEarliest(date, when); 119 | return when.isValid() ? when : QDateTime(); 120 | } 121 | -------------------------------------------------------------------------------- /src/qtutil.hpp: -------------------------------------------------------------------------------- 1 | #ifndef QTUTIL_HPP 2 | #define QTUTIL_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | /// Adds possible shortcut information to the tooltip of the action. 9 | /// This provides consistent behavior both with default and custom tooltips 10 | /// when used in combination with removeShortcutToToolTip() 11 | void addShortcutToToolTip(QAction *action); 12 | 13 | /// Removes possible shortcut information from the tooltip of the action. 14 | /// This provides consistent behavior both with default and custom tooltips 15 | /// when used in combination with addShortcutToToolTip() 16 | void removeShortcutFromToolTip(QAction *action); 17 | 18 | /// QDateTime::startOfDay for the deprecated Qt. 19 | /// See: https://doc.qt.io/qt-5/qdate.html#startOfDay 20 | QDateTime startOfDay(const QDate &); 21 | 22 | #endif // QTUTIL_HPP 23 | -------------------------------------------------------------------------------- /src/recurringdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "recurringdialog.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "recurringtasksmodel.hpp" 9 | 10 | RecurringDialog::RecurringDialog(const QList &tasks, 11 | QWidget *parent) 12 | : QDialog(parent) 13 | { 14 | setWindowTitle(QCoreApplication::applicationName() + " - Recurring tasks"); 15 | setMinimumSize(500, 400); 16 | initUI(tasks); 17 | } 18 | 19 | RecurringDialog::~RecurringDialog() {} 20 | 21 | void RecurringDialog::initUI(const QList &tasks) 22 | { 23 | setWindowIcon(QIcon(":/icons/qtask.svg")); 24 | 25 | m_tasks_view = new QTableView(this); 26 | m_tasks_view->setShowGrid(true); 27 | 28 | m_tasks_view->verticalHeader()->setVisible(false); 29 | m_tasks_view->horizontalHeader()->setStretchLastSection(true); 30 | m_tasks_view->setSelectionBehavior(QAbstractItemView::SelectRows); 31 | 32 | RecurringTasksModel *model = new RecurringTasksModel(); 33 | model->setTasks(tasks); 34 | m_tasks_view->setModel(model); 35 | 36 | m_btn_box = 37 | new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); 38 | connect(m_btn_box, &QDialogButtonBox::accepted, this, &QDialog::accept); 39 | connect(m_btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject); 40 | 41 | QVBoxLayout *main_layout = new QVBoxLayout(); 42 | main_layout->addWidget(m_tasks_view); 43 | main_layout->addWidget(m_btn_box); 44 | main_layout->setContentsMargins(5, 5, 5, 5); 45 | 46 | setLayout(main_layout); 47 | } 48 | -------------------------------------------------------------------------------- /src/recurringdialog.hpp: -------------------------------------------------------------------------------- 1 | #ifndef RECURRINGDIALOG_HPP 2 | #define RECURRINGDIALOG_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "recurringtasksmodel.hpp" 10 | #include "task.hpp" 11 | 12 | class RecurringDialog : public QDialog { 13 | Q_OBJECT 14 | 15 | public: 16 | RecurringDialog(const QList &, QWidget *parent = nullptr); 17 | ~RecurringDialog(); 18 | 19 | private: 20 | void initUI(const QList &); 21 | 22 | private: 23 | QTableView *m_tasks_view; 24 | QDialogButtonBox *m_btn_box; 25 | }; 26 | 27 | #endif // RECURRINGDIALOG_HPP 28 | -------------------------------------------------------------------------------- /src/recurringtasksmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "recurringtasksmodel.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | RecurringTasksModel::RecurringTasksModel(QObject *parent) 10 | : QAbstractTableModel(parent) 11 | , m_tasks({}) 12 | { 13 | } 14 | 15 | int RecurringTasksModel::rowCount(const QModelIndex & /*parent*/) const 16 | { 17 | return m_tasks.size(); 18 | } 19 | 20 | int RecurringTasksModel::columnCount(const QModelIndex & /*parent*/) const 21 | { 22 | return 4; 23 | } 24 | 25 | QVariant RecurringTasksModel::data(const QModelIndex &index, int role) const 26 | { 27 | if (role == Qt::DisplayRole) { 28 | RecurringTask task = m_tasks.at(index.row()); 29 | switch (index.column()) { 30 | case 0: 31 | return task.uuid; 32 | case 1: 33 | return task.project; 34 | case 2: 35 | return task.period; 36 | case 3: 37 | return task.description; 38 | default: 39 | return false; 40 | } 41 | } 42 | 43 | return QVariant(); 44 | } 45 | 46 | bool RecurringTasksModel::setData(const QModelIndex &idx, const QVariant &value, 47 | int role) 48 | { 49 | if (!idx.isValid()) 50 | return false; 51 | 52 | if (role == Qt::EditRole) { 53 | int row = idx.row(); 54 | qDebug() << "Requested edit " << row; 55 | } 56 | 57 | return true; 58 | } 59 | 60 | QVariant RecurringTasksModel::headerData(int section, 61 | Qt::Orientation orientation, 62 | int role) const 63 | { 64 | if (role != Qt::DisplayRole) 65 | return {}; 66 | 67 | if (orientation == Qt::Horizontal) { 68 | switch (section) { 69 | case 0: 70 | return tr("id"); 71 | case 1: 72 | return tr("Project"); 73 | case 2: 74 | return tr("Period"); 75 | case 3: 76 | return tr("Description"); 77 | } 78 | } 79 | 80 | return {}; 81 | } 82 | 83 | void RecurringTasksModel::setTasks(const QList &tasks) 84 | { 85 | beginResetModel(); 86 | m_tasks = tasks; 87 | endResetModel(); 88 | } 89 | 90 | void RecurringTasksModel::setTasks(QList &&tasks) 91 | { 92 | beginResetModel(); 93 | m_tasks = tasks; 94 | endResetModel(); 95 | } 96 | -------------------------------------------------------------------------------- /src/recurringtasksmodel.hpp: -------------------------------------------------------------------------------- 1 | #ifndef RECURRINGTASKSMODEL_HPP 2 | #define RECURRINGTASKSMODEL_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "task.hpp" 10 | 11 | class RecurringTasksModel : public QAbstractTableModel { 12 | Q_OBJECT 13 | public: 14 | RecurringTasksModel(QObject *parent = nullptr); 15 | ~RecurringTasksModel() = default; 16 | 17 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 18 | int columnCount(const QModelIndex &parent = QModelIndex()) const override; 19 | QVariant data(const QModelIndex &index, 20 | int role = Qt::DisplayRole) const override; 21 | bool setData(const QModelIndex &, const QVariant &, 22 | int role = Qt::EditRole) override; 23 | QVariant headerData(int section, Qt::Orientation, 24 | int role = Qt::DisplayRole) const override; 25 | 26 | void setTasks(const QList &); 27 | void setTasks(QList &&); 28 | 29 | private: 30 | QList m_tasks; 31 | }; 32 | 33 | #endif // RECURRINGTASKSMODEL_HPP 34 | -------------------------------------------------------------------------------- /src/settingsdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "settingsdialog.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "configmanager.hpp" 13 | 14 | SettingsDialog::SettingsDialog(QWidget *parent) 15 | : QDialog(parent) 16 | { 17 | initUI(); 18 | } 19 | 20 | SettingsDialog::~SettingsDialog() {} 21 | 22 | void SettingsDialog::initUI() 23 | { 24 | QGridLayout *main_layout = new QGridLayout(); 25 | main_layout->setContentsMargins(5, 5, 5, 5); 26 | 27 | setWindowTitle(QCoreApplication::applicationName() + " - Settings"); 28 | setWindowIcon(QIcon(":/icons/qtask.svg")); 29 | 30 | QLabel *task_bin_label = new QLabel("task executable:"); 31 | main_layout->addWidget(task_bin_label, 0, 0); 32 | 33 | m_task_bin_edit = new QLineEdit(); 34 | m_task_bin_edit->setText(ConfigManager::config()->getTaskBin()); 35 | main_layout->addWidget(m_task_bin_edit, 0, 1); 36 | 37 | QLabel *task_data_path_label = new QLabel("Path to task data:"); 38 | main_layout->addWidget(task_data_path_label, 1, 0); 39 | 40 | m_task_data_path_edit = new QLineEdit(); 41 | m_task_data_path_edit->setText(ConfigManager::config()->getTaskDataPath()); 42 | main_layout->addWidget(m_task_data_path_edit, 1, 1); 43 | 44 | m_hide_on_startup_cb = new QCheckBox(tr("Hide QTask window on startup")); 45 | m_hide_on_startup_cb->setChecked( 46 | ConfigManager::config()->getHideWindowOnStartup()); 47 | main_layout->addWidget(m_hide_on_startup_cb, 2, 0, 1, 2); 48 | 49 | m_save_filter_on_exit = new QCheckBox(tr("Save task filter on exit")); 50 | m_save_filter_on_exit->setChecked( 51 | ConfigManager::config()->getSaveFilterOnExit()); 52 | main_layout->addWidget(m_save_filter_on_exit, 3, 0, 1, 2); 53 | 54 | m_buttons = 55 | new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Apply | 56 | QDialogButtonBox::Close); 57 | connect(m_buttons, &QDialogButtonBox::clicked, this, 58 | &SettingsDialog::onButtonBoxClicked); 59 | main_layout->addWidget(m_buttons, 4, 0, 1, 2); 60 | 61 | setLayout(main_layout); 62 | } 63 | 64 | void SettingsDialog::applySettings() 65 | { 66 | auto task_data_path = m_task_data_path_edit->text(); 67 | if (ConfigManager::config()->getTaskDataPath() != task_data_path) 68 | ConfigManager::config()->setTaskDataPath(task_data_path); 69 | 70 | auto task_bin = m_task_bin_edit->text(); 71 | if (ConfigManager::config()->getTaskBin() != task_bin) 72 | ConfigManager::config()->setTaskBin(task_bin); 73 | 74 | ConfigManager::config()->setHideWindowOnStartup( 75 | m_hide_on_startup_cb->isChecked()); 76 | ConfigManager::config()->setSaveFilterOnExit( 77 | m_save_filter_on_exit->isChecked()); 78 | 79 | ConfigManager::config()->updateConfigFile(); 80 | } 81 | 82 | void SettingsDialog::onButtonBoxClicked(QAbstractButton *button) 83 | { 84 | switch (m_buttons->standardButton(button)) { 85 | case QDialogButtonBox::Apply: 86 | applySettings(); 87 | break; 88 | case QDialogButtonBox::Ok: 89 | applySettings(); 90 | close(); 91 | default: 92 | close(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/settingsdialog.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SETTINGSDIALOG_HPP 2 | #define SETTINGSDIALOG_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | class SettingsDialog : public QDialog { 11 | Q_OBJECT 12 | 13 | public: 14 | SettingsDialog(QWidget *parent = nullptr); 15 | ~SettingsDialog(); 16 | 17 | private: 18 | void initUI(); 19 | void applySettings(); 20 | 21 | private slots: 22 | void onButtonBoxClicked(QAbstractButton *); 23 | 24 | private: 25 | QLineEdit *m_task_bin_edit; 26 | QLineEdit *m_task_data_path_edit; 27 | QCheckBox *m_hide_on_startup_cb; 28 | QCheckBox *m_save_filter_on_exit; 29 | QDialogButtonBox *m_buttons; 30 | }; 31 | 32 | #endif // SETTINGSDIALOG_HPP 33 | -------------------------------------------------------------------------------- /src/tagsedit.cpp: -------------------------------------------------------------------------------- 1 | #include "tagsedit.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) 19 | #define FONT_METRICS_WIDTH(fmt, ...) fmt.width(__VA_ARGS__) 20 | #else 21 | #define FONT_METRICS_WIDTH(fmt, ...) fmt.horizontalAdvance(__VA_ARGS__) 22 | #endif 23 | 24 | namespace 25 | { 26 | 27 | constexpr int top_text_margin = 1; 28 | constexpr int bottom_text_margin = 1; 29 | constexpr int left_text_margin = 1; 30 | constexpr int right_text_margin = 1; 31 | 32 | constexpr int vertical_margin = 3; 33 | constexpr int bottommargin = 1; 34 | constexpr int topmargin = 1; 35 | 36 | constexpr int horizontal_margin = 3; 37 | constexpr int leftmargin = 1; 38 | constexpr int rightmargin = 1; 39 | 40 | constexpr int tag_spacing = 3; 41 | constexpr int tag_inner_left_padding = 3; 42 | constexpr int tag_inner_right_padding = 4; 43 | constexpr int tag_cross_width = 5; 44 | constexpr int tag_cross_spacing = 2; 45 | 46 | struct Tag { 47 | QString text; 48 | QRect rect; 49 | }; 50 | 51 | /// Non empty string filtering iterator 52 | template struct EmptySkipIterator { 53 | EmptySkipIterator() = default; 54 | 55 | // skip until `end` 56 | explicit EmptySkipIterator(It it, It end) 57 | : it(it) 58 | , end(end) 59 | { 60 | while (this->it != end && this->it->text.isEmpty()) { 61 | ++this->it; 62 | } 63 | } 64 | 65 | explicit EmptySkipIterator(It it) 66 | : it(it) 67 | { 68 | } 69 | 70 | using difference_type = typename std::iterator_traits::difference_type; 71 | using value_type = typename std::iterator_traits::value_type; 72 | using pointer = typename std::iterator_traits::pointer; 73 | using reference = typename std::iterator_traits::reference; 74 | using iterator_category = std::output_iterator_tag; 75 | 76 | EmptySkipIterator &operator++() 77 | { 78 | while (++it != end && it->text.isEmpty()) 79 | ; 80 | return *this; 81 | } 82 | 83 | value_type &operator*() { return *it; } 84 | 85 | pointer operator->() { return &(*it); } 86 | 87 | bool operator!=(EmptySkipIterator const &rhs) const { return it != rhs.it; } 88 | 89 | private: 90 | It it; 91 | It end; 92 | }; 93 | 94 | template EmptySkipIterator(It, It)->EmptySkipIterator; 95 | 96 | } // namespace 97 | 98 | struct TagsEdit::Impl { 99 | explicit Impl(TagsEdit *const &ifce) 100 | : ifce(ifce) 101 | , tags{ Tag() } 102 | , editing_index(0) 103 | , cursor(0) 104 | , blink_timer(0) 105 | , blink_status(true) 106 | , select_start(0) 107 | , select_size(0) 108 | , ctrl(QInputControl::LineEdit) 109 | , completer(std::make_unique()) 110 | { 111 | } 112 | 113 | void initStyleOption(QStyleOptionFrame *option) const 114 | { 115 | assert(option); 116 | option->initFrom(ifce); 117 | option->rect = ifce->contentsRect(); 118 | option->lineWidth = ifce->style()->pixelMetric( 119 | QStyle::PM_DefaultFrameWidth, option, ifce); 120 | option->midLineWidth = 0; 121 | option->state |= QStyle::State_Sunken; 122 | option->features = QStyleOptionFrame::None; 123 | } 124 | 125 | inline QRectF crossRect(QRectF const &r) const 126 | { 127 | QRectF cross(QPointF{ 0, 0 }, 128 | QSizeF{ tag_cross_width, tag_cross_width }); 129 | cross.moveCenter(QPointF(r.right() - tag_cross_width, r.center().y())); 130 | return cross; 131 | } 132 | 133 | bool inCrossArea(size_t tag_index, QPoint const &point) const 134 | { 135 | return crossRect(tags[tag_index].rect) 136 | .adjusted(-2, 0, 0, 0) 137 | .translated(-hscroll, 0) 138 | .contains(point) && 139 | (!cursorVisible() || tag_index != editing_index); 140 | } 141 | 142 | template 143 | void drawTags(QPainter &p, std::pair range) const 144 | { 145 | for (auto it = range.first; it != range.second; ++it) { 146 | QRect const &i_r = it->rect.translated(-hscroll, 0); 147 | auto const text_pos = 148 | i_r.topLeft() + 149 | QPointF( 150 | tag_inner_left_padding, 151 | ifce->fontMetrics().ascent() + 152 | ((i_r.height() - ifce->fontMetrics().height()) / 2)); 153 | 154 | // drag tag rect 155 | QColor const blue(225, 236, 244); 156 | QPainterPath path; 157 | path.addRoundedRect(i_r, 4, 4); 158 | p.fillPath(path, blue); 159 | 160 | // draw text 161 | p.drawText(text_pos, it->text); 162 | 163 | // calc cross rect 164 | auto const i_cross_r = crossRect(i_r); 165 | 166 | QPen pen = p.pen(); 167 | pen.setWidth(2); 168 | 169 | p.save(); 170 | p.setPen(pen); 171 | p.setRenderHint(QPainter::Antialiasing); 172 | p.drawLine(QLineF(i_cross_r.topLeft(), i_cross_r.bottomRight())); 173 | p.drawLine(QLineF(i_cross_r.bottomLeft(), i_cross_r.topRight())); 174 | p.restore(); 175 | } 176 | } 177 | 178 | QRect cRect() const 179 | { 180 | QStyleOptionFrame panel; 181 | initStyleOption(&panel); 182 | QRect r = ifce->style()->subElementRect(QStyle::SE_LineEditContents, 183 | &panel, ifce); 184 | r.adjust(left_text_margin, top_text_margin, -right_text_margin, 185 | -bottom_text_margin); 186 | return r; 187 | } 188 | 189 | void calcRects() 190 | { 191 | auto const r = cRect(); 192 | auto lt = r.topLeft(); 193 | 194 | if (cursorVisible()) { 195 | calcRects( 196 | lt, r.height(), 197 | std::make_pair(tags.begin(), 198 | tags.begin() + std::ptrdiff_t(editing_index))); 199 | calcEditorRect(lt, r.height()); 200 | calcRects( 201 | lt, r.height(), 202 | std::make_pair(tags.begin() + std::ptrdiff_t(editing_index + 1), 203 | tags.end())); 204 | } else { 205 | calcRects( 206 | lt, r.height(), 207 | std::make_pair(EmptySkipIterator(tags.begin(), tags.end()), 208 | EmptySkipIterator(tags.end()))); 209 | } 210 | } 211 | 212 | template 213 | void calcRects(QPoint <, int height, std::pair range) 214 | { 215 | for (auto it = range.first; it != range.second; ++it) { 216 | // calc text rect 217 | const auto i_width = 218 | FONT_METRICS_WIDTH(ifce->fontMetrics(), it->text); 219 | QRect i_r(lt, QSize(i_width, height)); 220 | i_r.translate(tag_inner_left_padding, 0); 221 | i_r.adjust(-tag_inner_left_padding, 0, 222 | tag_inner_right_padding + tag_cross_spacing + 223 | tag_cross_width, 224 | 0); 225 | it->rect = i_r; 226 | lt.setX(i_r.right() + tag_spacing); 227 | } 228 | } 229 | 230 | void calcEditorRect(QPoint <, int height) 231 | { 232 | auto const w = 233 | FONT_METRICS_WIDTH(ifce->fontMetrics(), text_layout.text()) + 234 | tag_inner_left_padding + tag_inner_right_padding; 235 | currentRect() = QRect(lt, QSize(w, height)); 236 | lt += QPoint(w + tag_spacing, 0); 237 | } 238 | 239 | void setCursorVisible(bool visible) 240 | { 241 | if (blink_timer) { 242 | ifce->killTimer(blink_timer); 243 | blink_timer = 0; 244 | blink_status = true; 245 | } 246 | 247 | if (visible) { 248 | int flashTime = QGuiApplication::styleHints()->cursorFlashTime(); 249 | if (flashTime >= 2) { 250 | blink_timer = ifce->startTimer(flashTime / 2); 251 | } 252 | } else { 253 | blink_status = false; 254 | } 255 | } 256 | 257 | bool cursorVisible() const { return blink_timer; } 258 | 259 | void updateCursorBlinking() { setCursorVisible(cursorVisible()); } 260 | 261 | void updateDisplayText() 262 | { 263 | text_layout.clearLayout(); 264 | text_layout.setText(currentText()); 265 | text_layout.beginLayout(); 266 | text_layout.createLine(); 267 | text_layout.endLayout(); 268 | } 269 | 270 | void setEditingIndex(size_t i) 271 | { 272 | assert(i <= tags.size()); 273 | if (currentText().isEmpty()) { 274 | tags.erase(std::next(tags.begin(), std::ptrdiff_t(editing_index))); 275 | if (editing_index <= i) { 276 | --i; 277 | } 278 | } 279 | editing_index = i; 280 | } 281 | 282 | void currentText(QString const &text) 283 | { 284 | currentText() = text; 285 | moveCursor(currentText().length(), false); 286 | updateDisplayText(); 287 | calcRects(); 288 | ifce->update(); 289 | } 290 | 291 | QString const ¤tText() const { return tags[editing_index].text; } 292 | 293 | QString ¤tText() { return tags[editing_index].text; } 294 | 295 | QRect const ¤tRect() const { return tags[editing_index].rect; } 296 | 297 | QRect ¤tRect() { return tags[editing_index].rect; } 298 | 299 | void editNewTag() 300 | { 301 | tags.push_back(Tag()); 302 | setEditingIndex(tags.size() - 1); 303 | moveCursor(0, false); 304 | } 305 | 306 | void setupCompleter() 307 | { 308 | completer->setWidget(ifce); 309 | connect(completer.get(), 310 | qOverload(&QCompleter::activated), 311 | [this](QString const &text) { currentText(text); }); 312 | } 313 | 314 | QVector formatting() const 315 | { 316 | if (select_size == 0) { 317 | return {}; 318 | } 319 | 320 | QTextLayout::FormatRange selection; 321 | selection.start = select_start; 322 | selection.length = select_size; 323 | selection.format.setBackground( 324 | ifce->palette().brush(QPalette::Highlight)); 325 | selection.format.setForeground( 326 | ifce->palette().brush(QPalette::HighlightedText)); 327 | return { selection }; 328 | } 329 | 330 | bool hasSelection() const noexcept { return select_size > 0; } 331 | 332 | void removeSelection() 333 | { 334 | cursor = select_start; 335 | currentText().remove(cursor, select_size); 336 | deselectAll(); 337 | } 338 | 339 | void removeBackwardOne() 340 | { 341 | if (hasSelection()) { 342 | removeSelection(); 343 | } else { 344 | currentText().remove(--cursor, 1); 345 | } 346 | } 347 | 348 | void removePreviousTag() 349 | { 350 | if (hasSelection()) { 351 | removeSelection(); 352 | return; 353 | } 354 | if ((currentText().size() == 0) && (editing_index > 0)) { 355 | setEditingIndex(editing_index - 1); 356 | moveCursor(currentText().size(), false); 357 | } 358 | cursor -= currentText().size(); 359 | currentText().remove(cursor, currentText().size()); 360 | } 361 | 362 | void selectAll() 363 | { 364 | select_start = 0; 365 | select_size = currentText().size(); 366 | } 367 | 368 | void deselectAll() 369 | { 370 | select_start = 0; 371 | select_size = 0; 372 | } 373 | 374 | void moveCursor(int pos, bool mark) 375 | { 376 | if (mark) { 377 | auto e = select_start + select_size; 378 | int anchor = 379 | select_size > 0 && cursor == select_start 380 | ? e 381 | : select_size > 0 && cursor == e ? select_start : cursor; 382 | select_start = qMin(anchor, pos); 383 | select_size = qMax(anchor, pos) - select_start; 384 | } else { 385 | deselectAll(); 386 | } 387 | 388 | cursor = pos; 389 | } 390 | 391 | qreal natrualWidth() const 392 | { 393 | return tags.back().rect.right() - tags.front().rect.left(); 394 | } 395 | 396 | qreal cursorToX() { return text_layout.lineAt(0).cursorToX(cursor); } 397 | 398 | void calcHScroll(QRect const &r) 399 | { 400 | auto const rect = cRect(); 401 | auto const width_used = qRound(natrualWidth()) + 1; 402 | int const cix = r.x() + qRound(cursorToX()); 403 | if (width_used <= rect.width()) { 404 | // text fit 405 | hscroll = 0; 406 | } else if (cix - hscroll >= rect.width()) { 407 | // text doesn't fit, cursor is to the right of lineRect (scroll 408 | // right) 409 | hscroll = cix - rect.width() + 1; 410 | } else if (cix - hscroll < 0 && hscroll < width_used) { 411 | // text doesn't fit, cursor is to the left of lineRect (scroll left) 412 | hscroll = cix; 413 | } else if (width_used - hscroll < rect.width()) { 414 | // text doesn't fit, text document is to the left of lineRect; align 415 | // right 416 | hscroll = width_used - rect.width() + 1; 417 | } else { 418 | // in case the text is bigger than the lineedit, the hscroll can 419 | // never be negative 420 | hscroll = qMax(0, hscroll); 421 | } 422 | } 423 | 424 | void editPreviousTag() 425 | { 426 | if (editing_index > 0) { 427 | setEditingIndex(editing_index - 1); 428 | moveCursor(currentText().size(), false); 429 | } 430 | } 431 | 432 | void editNextTag() 433 | { 434 | if (editing_index < tags.size() - 1) { 435 | setEditingIndex(editing_index + 1); 436 | moveCursor(0, false); 437 | } 438 | } 439 | 440 | void editTag(size_t i) 441 | { 442 | // assert(i >= 0 && i < tags.size()); 443 | setEditingIndex(i); 444 | moveCursor(currentText().size(), false); 445 | } 446 | 447 | TagsEdit *const ifce; 448 | std::vector tags; 449 | size_t editing_index; 450 | int cursor; 451 | int blink_timer; 452 | bool blink_status; 453 | QTextLayout text_layout; 454 | int select_start; 455 | int select_size; 456 | QInputControl ctrl; 457 | std::unique_ptr completer; 458 | int hscroll{ 0 }; 459 | }; 460 | 461 | TagsEdit::TagsEdit(QWidget *parent) 462 | : QWidget(parent) 463 | , impl(std::make_unique(this)) 464 | { 465 | setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); 466 | setFocusPolicy(Qt::StrongFocus); 467 | setCursor(Qt::IBeamCursor); 468 | setAttribute(Qt::WA_InputMethodEnabled, true); 469 | setMouseTracking(true); 470 | 471 | impl->setupCompleter(); 472 | impl->setCursorVisible(hasFocus()); 473 | impl->updateDisplayText(); 474 | } 475 | 476 | TagsEdit::~TagsEdit() = default; 477 | 478 | void TagsEdit::resizeEvent(QResizeEvent *) { impl->calcRects(); } 479 | 480 | void TagsEdit::focusInEvent(QFocusEvent *) 481 | { 482 | impl->setCursorVisible(true); 483 | impl->updateDisplayText(); 484 | impl->calcRects(); 485 | update(); 486 | } 487 | 488 | void TagsEdit::focusOutEvent(QFocusEvent *) 489 | { 490 | impl->setCursorVisible(false); 491 | impl->updateDisplayText(); 492 | impl->calcRects(); 493 | update(); 494 | } 495 | 496 | void TagsEdit::paintEvent(QPaintEvent *) 497 | { 498 | QPainter p(this); 499 | 500 | // opt 501 | auto const panel = [this] { 502 | QStyleOptionFrame panel; 503 | impl->initStyleOption(&panel); 504 | return panel; 505 | }(); 506 | 507 | // draw frame 508 | style()->drawPrimitive(QStyle::PE_PanelLineEdit, &panel, &p, this); 509 | 510 | // clip 511 | auto const rect = impl->cRect(); 512 | p.setClipRect(rect); 513 | 514 | if (impl->cursorVisible()) { 515 | // not terminated tag pos 516 | auto const &r = impl->currentRect(); 517 | auto const &txt_p = 518 | r.topLeft() + QPointF(tag_inner_left_padding, 519 | ((r.height() - fontMetrics().height()) / 2)); 520 | 521 | // scroll 522 | impl->calcHScroll(r); 523 | 524 | // tags 525 | impl->drawTags( 526 | p, std::make_pair(impl->tags.cbegin(), 527 | std::next(impl->tags.cbegin(), 528 | std::ptrdiff_t(impl->editing_index)))); 529 | 530 | // draw not terminated tag 531 | auto const formatting = impl->formatting(); 532 | impl->text_layout.draw(&p, txt_p - QPointF(impl->hscroll, 0), 533 | formatting); 534 | 535 | // draw cursor 536 | if (impl->blink_status) { 537 | impl->text_layout.drawCursor(&p, txt_p - QPointF(impl->hscroll, 0), 538 | impl->cursor); 539 | } 540 | 541 | // tags 542 | impl->drawTags( 543 | p, 544 | std::make_pair(std::next(impl->tags.cbegin(), 545 | std::ptrdiff_t(impl->editing_index + 1)), 546 | impl->tags.cend())); 547 | } else { 548 | impl->drawTags(p, std::make_pair(EmptySkipIterator(impl->tags.begin(), 549 | impl->tags.end()), 550 | EmptySkipIterator(impl->tags.end()))); 551 | } 552 | } 553 | 554 | void TagsEdit::timerEvent(QTimerEvent *event) 555 | { 556 | if (event->timerId() == impl->blink_timer) { 557 | impl->blink_status = !impl->blink_status; 558 | update(); 559 | } 560 | } 561 | 562 | void TagsEdit::mousePressEvent(QMouseEvent *event) 563 | { 564 | bool found = false; 565 | for (size_t i = 0; i < impl->tags.size(); ++i) { 566 | if (impl->inCrossArea(i, event->pos())) { 567 | impl->tags.erase(impl->tags.begin() + std::ptrdiff_t(i)); 568 | if (i <= impl->editing_index) { 569 | --impl->editing_index; 570 | } 571 | emit tagsChanged(); 572 | found = true; 573 | break; 574 | } 575 | 576 | if (!impl->tags[i] 577 | .rect.translated(-impl->hscroll, 0) 578 | .contains(event->pos())) { 579 | continue; 580 | } 581 | 582 | if (impl->editing_index == i) { 583 | impl->moveCursor( 584 | impl->text_layout.lineAt(0).xToCursor( 585 | (event->pos() - impl->currentRect() 586 | .translated(-impl->hscroll, 0) 587 | .topLeft()) 588 | .x()), 589 | false); 590 | } else { 591 | impl->editTag(i); 592 | } 593 | 594 | found = true; 595 | break; 596 | } 597 | 598 | if (!found) { 599 | impl->editNewTag(); 600 | event->accept(); 601 | } 602 | 603 | if (event->isAccepted()) { 604 | impl->updateDisplayText(); 605 | impl->calcRects(); 606 | impl->updateCursorBlinking(); 607 | update(); 608 | emit tagsChanged(); 609 | } 610 | } 611 | 612 | QSize TagsEdit::sizeHint() const 613 | { 614 | ensurePolished(); 615 | QFontMetrics fm(font()); 616 | int h = fm.height() + 2 * vertical_margin + top_text_margin + 617 | bottom_text_margin + topmargin + bottommargin; 618 | int w = fm.boundingRect(QLatin1Char('x')).width() * 17 + 619 | 2 * horizontal_margin + leftmargin + rightmargin; // "some" 620 | QStyleOptionFrame opt; 621 | impl->initStyleOption(&opt); 622 | return (style()->sizeFromContents( 623 | QStyle::CT_LineEdit, &opt, 624 | QSize(w, h).expandedTo(QApplication::globalStrut()), this)); 625 | } 626 | 627 | QSize TagsEdit::minimumSizeHint() const 628 | { 629 | ensurePolished(); 630 | QFontMetrics fm = fontMetrics(); 631 | int h = fm.height() + qMax(2 * vertical_margin, fm.leading()) + 632 | top_text_margin + bottom_text_margin + topmargin + bottommargin; 633 | int w = fm.maxWidth() + leftmargin + rightmargin; 634 | QStyleOptionFrame opt; 635 | impl->initStyleOption(&opt); 636 | return (style()->sizeFromContents( 637 | QStyle::CT_LineEdit, &opt, 638 | QSize(w, h).expandedTo(QApplication::globalStrut()), this)); 639 | } 640 | 641 | void TagsEdit::keyPressEvent(QKeyEvent *event) 642 | { 643 | event->setAccepted(false); 644 | bool unknown = false; 645 | 646 | if (event == QKeySequence::SelectAll) { 647 | impl->selectAll(); 648 | event->accept(); 649 | } else if (event == QKeySequence::SelectPreviousChar) { 650 | impl->moveCursor(impl->text_layout.previousCursorPosition(impl->cursor), 651 | true); 652 | event->accept(); 653 | } else if (event == QKeySequence::SelectNextChar) { 654 | impl->moveCursor(impl->text_layout.nextCursorPosition(impl->cursor), 655 | true); 656 | event->accept(); 657 | } else if (event == QKeySequence::DeleteStartOfWord) { 658 | impl->removePreviousTag(); 659 | emit tagsChanged(); 660 | event->accept(); 661 | } else { 662 | switch (event->key()) { 663 | case Qt::Key_Left: 664 | if (impl->cursor == 0) { 665 | impl->editPreviousTag(); 666 | } else { 667 | impl->moveCursor( 668 | impl->text_layout.previousCursorPosition(impl->cursor), 669 | false); 670 | } 671 | event->accept(); 672 | break; 673 | case Qt::Key_Right: 674 | if (impl->cursor == impl->currentText().size()) { 675 | impl->editNextTag(); 676 | } else { 677 | impl->moveCursor( 678 | impl->text_layout.nextCursorPosition(impl->cursor), false); 679 | } 680 | event->accept(); 681 | break; 682 | case Qt::Key_Home: 683 | if (impl->cursor == 0) { 684 | impl->editTag(0); 685 | } else { 686 | impl->moveCursor(0, false); 687 | } 688 | event->accept(); 689 | break; 690 | case Qt::Key_End: 691 | if (impl->cursor == impl->currentText().size()) { 692 | impl->editTag(impl->tags.size() - 1); 693 | } else { 694 | impl->moveCursor(impl->currentText().length(), false); 695 | } 696 | event->accept(); 697 | break; 698 | case Qt::Key_Backspace: 699 | if (!impl->currentText().isEmpty()) { 700 | impl->removeBackwardOne(); 701 | } else if (impl->editing_index > 0) { 702 | impl->editPreviousTag(); 703 | } 704 | event->accept(); 705 | break; 706 | case Qt::Key_Enter: 707 | case Qt::Key_Return: 708 | case Qt::Key_Space: 709 | if (!impl->currentText().isEmpty()) { 710 | impl->tags.insert(impl->tags.begin() + 711 | std::ptrdiff_t(impl->editing_index + 1), 712 | Tag()); 713 | impl->editNextTag(); 714 | emit tagsChanged(); 715 | } 716 | event->accept(); 717 | break; 718 | default: 719 | unknown = true; 720 | } 721 | } 722 | 723 | if (unknown && impl->ctrl.isAcceptableInput(event)) { 724 | if (impl->hasSelection()) { 725 | impl->removeSelection(); 726 | } 727 | impl->currentText().insert(impl->cursor, event->text()); 728 | impl->cursor += event->text().length(); 729 | event->accept(); 730 | unknown = false; 731 | } 732 | 733 | if (event->isAccepted()) { 734 | // update content 735 | impl->updateDisplayText(); 736 | impl->calcRects(); 737 | impl->updateCursorBlinking(); 738 | 739 | // complete 740 | impl->completer->setCompletionPrefix(impl->currentText()); 741 | impl->completer->complete(); 742 | 743 | update(); 744 | } 745 | } 746 | 747 | void TagsEdit::completion(std::vector const &completions) 748 | { 749 | impl->completer = std::make_unique([&] { 750 | QStringList ret; 751 | std::copy(completions.begin(), completions.end(), 752 | std::back_inserter(ret)); 753 | return ret; 754 | }()); 755 | impl->setupCompleter(); 756 | } 757 | 758 | void TagsEdit::setTags(const QStringList &tags) 759 | { 760 | std::vector t{ Tag() }; 761 | for (auto tt : tags) { 762 | t.push_back({ tt, QRect() }); 763 | } 764 | impl->tags = std::move(t); 765 | impl->editing_index = 0; 766 | impl->moveCursor(0, false); 767 | 768 | impl->editNewTag(); 769 | impl->updateDisplayText(); 770 | impl->calcRects(); 771 | 772 | update(); 773 | } 774 | 775 | QStringList TagsEdit::getTags() const 776 | { 777 | std::vector ret; 778 | std::transform(EmptySkipIterator(impl->tags.begin(), impl->tags.end()), 779 | EmptySkipIterator(impl->tags.end()), std::back_inserter(ret), 780 | [](Tag const &tag) { return tag.text; }); 781 | QStringList res; 782 | for (auto r : ret) 783 | res.push_back(r); 784 | return res; 785 | } 786 | 787 | void TagsEdit::clearTags() { setTags(QStringList{}); } 788 | 789 | void TagsEdit::pushTag(const QString &value) 790 | { 791 | for (const auto &t : impl->tags) 792 | if (value == t.text) 793 | return; 794 | 795 | auto t = Tag(); 796 | t.text = value; 797 | impl->tags.push_back(t); 798 | impl->updateDisplayText(); 799 | impl->calcRects(); 800 | update(); 801 | emit tagsChanged(); 802 | } 803 | 804 | void TagsEdit::popTag() 805 | { 806 | if (impl->tags.empty()) 807 | return; 808 | impl->tags.pop_back(); 809 | update(); 810 | } 811 | 812 | void TagsEdit::mouseMoveEvent(QMouseEvent *event) 813 | { 814 | for (size_t i = 0; i < impl->tags.size(); ++i) { 815 | if (impl->inCrossArea(i, event->pos())) { 816 | setCursor(Qt::ArrowCursor); 817 | return; 818 | } 819 | } 820 | setCursor(Qt::IBeamCursor); 821 | } 822 | -------------------------------------------------------------------------------- /src/tagsedit.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TAGSEDIT_HPP 2 | #define TAGSEDIT_HPP 3 | 4 | #include 5 | 6 | #include 7 | 8 | class QStyleOptionFrame; 9 | 10 | /// Tag editor widget 11 | /// `Space` commits a tag and initiates a new tag edition 12 | class TagsEdit : public QWidget { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit TagsEdit(QWidget *parent = nullptr); 17 | ~TagsEdit() override; 18 | 19 | // QWidget 20 | QSize sizeHint() const override; 21 | QSize minimumSizeHint() const override; 22 | 23 | /// Set completions 24 | void completion(std::vector const &completions); 25 | 26 | void setTags(const QStringList &tags); 27 | void clearTags(); 28 | QStringList getTags() const; 29 | void pushTag(const QString &); 30 | void popTag(); 31 | 32 | signals: 33 | void tagsChanged(); 34 | 35 | protected: 36 | // QWidget 37 | void paintEvent(QPaintEvent *event) override; 38 | void timerEvent(QTimerEvent *event) override; 39 | void mousePressEvent(QMouseEvent *event) override; 40 | void resizeEvent(QResizeEvent *event) override; 41 | void focusInEvent(QFocusEvent *event) override; 42 | void focusOutEvent(QFocusEvent *event) override; 43 | void keyPressEvent(QKeyEvent *event) override; 44 | void mouseMoveEvent(QMouseEvent *event) override; 45 | 46 | private: 47 | struct Impl; 48 | std::unique_ptr impl; 49 | }; 50 | 51 | #endif // TAGSEDIT_HPP 52 | -------------------------------------------------------------------------------- /src/task.cpp: -------------------------------------------------------------------------------- 1 | #include "task.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | QString Task::priorityToString(const Priority &p) 9 | { 10 | switch (p) { 11 | case Priority::L: 12 | return "L"; 13 | case Priority::M: 14 | return "M"; 15 | case Priority::H: 16 | return "H"; 17 | default: 18 | return ""; 19 | } 20 | } 21 | 22 | Task::Priority Task::priorityFromString(const QString &p) 23 | { 24 | if (p == "L") 25 | return Priority::L; 26 | if (p == "M") 27 | return Priority::M; 28 | if (p == "H") 29 | return Priority::H; 30 | return Priority::Unset; 31 | } 32 | 33 | QStringList Task::getCmdArgs() const 34 | { 35 | QStringList result; 36 | 37 | if (priority != Priority::Unset) 38 | result << QString{ " pri:'%1'" }.arg(Task::priorityToString(priority)); 39 | else 40 | result << QString{ " pri:''" }; 41 | 42 | result << QString{ " project:'%1'" }.arg(project); 43 | for (auto const &t : tags) { 44 | if (!t.isEmpty()) { 45 | QString t_escpaed = t; 46 | t_escpaed.replace("'", ""); 47 | if (!t_escpaed.isEmpty()) 48 | result << QString{ " +%1" }.arg(t_escpaed); 49 | } 50 | } 51 | for (auto const &t : removed_tags) { 52 | if (!t.isEmpty()) { 53 | QString t_escpaed = t; 54 | t_escpaed.replace("'", ""); 55 | if (!t_escpaed.isEmpty()) 56 | result << QString{ " -%1" }.arg(t_escpaed); 57 | } 58 | } 59 | 60 | result << QString{ " sched:%1" }.arg( 61 | sched.toDateTime().toString(Qt::ISODate)); 62 | result << QString{ " due:%1" }.arg(due.toDateTime().toString(Qt::ISODate)); 63 | result << QString{ " wait:%1" }.arg( 64 | wait.toDateTime().toString(Qt::ISODate)); 65 | 66 | QString escaped_desc = description; 67 | escaped_desc.replace("'", "\'"); 68 | result << QString{ " '%1'" }.arg(description); 69 | 70 | return result; 71 | } 72 | -------------------------------------------------------------------------------- /src/task.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TASK_HPP 2 | #define TASK_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | struct AbstractTask { 10 | AbstractTask() 11 | : uuid("") 12 | , description("") 13 | , project("") 14 | , tags({}) 15 | , sched(QVariant{}) 16 | , due(QVariant{}) 17 | { 18 | } 19 | 20 | QString uuid; 21 | QString description; 22 | QString project; 23 | QStringList tags; 24 | QVariant sched; 25 | QVariant due; 26 | }; 27 | 28 | struct Task final : public AbstractTask { 29 | enum class Priority { Unset, L, M, H }; 30 | static QString priorityToString(const Priority &p); 31 | static Priority priorityFromString(const QString &p); 32 | 33 | Task() 34 | : priority(Priority::Unset) 35 | , active(false) 36 | , wait(QVariant{}) 37 | { 38 | } 39 | 40 | Priority priority; 41 | bool active; 42 | QVariant wait; 43 | 44 | /// Tags that will be removed at the next command. 45 | QStringList removed_tags; 46 | 47 | QStringList getCmdArgs() const; 48 | }; 49 | 50 | Q_DECLARE_METATYPE(Task) 51 | 52 | struct RecurringTask final : public AbstractTask { 53 | RecurringTask() 54 | : period("") 55 | { 56 | } 57 | 58 | // Recurring period with date suffix: 59 | // https://taskwarrior.org/docs/design/recurrence.html#special-month-handling 60 | QString period; 61 | }; 62 | 63 | #endif // TASK_HPP 64 | -------------------------------------------------------------------------------- /src/taskdescriptiondelegate.cpp: -------------------------------------------------------------------------------- 1 | #include "taskdescriptiondelegate.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "tasksmodel.hpp" 9 | 10 | TaskDescriptionDelegate::TaskDescriptionDelegate(QObject *parent) 11 | : QStyledItemDelegate(parent) 12 | { 13 | } 14 | 15 | QString TaskDescriptionDelegate::anchorAt(const QString &markdown, 16 | const QPoint &point) const 17 | { 18 | QTextDocument doc; 19 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) 20 | doc.setMarkdown(markdown); 21 | #else 22 | doc.setPlainText(markdown); 23 | #endif // QT_VERSION_CHECK 24 | 25 | auto textLayout = doc.documentLayout(); 26 | Q_ASSERT(textLayout != 0); 27 | 28 | return textLayout->anchorAt(point); 29 | } 30 | 31 | void TaskDescriptionDelegate::paint(QPainter *painter, 32 | const QStyleOptionViewItem &option, 33 | const QModelIndex &index) const 34 | { 35 | if (option.state & QStyle::State_Selected) { 36 | painter->fillRect(option.rect, option.palette.highlight()); 37 | } else { 38 | auto *model = qobject_cast(index.model()); 39 | Q_ASSERT(model); 40 | painter->fillRect(option.rect, model->rowColor(index.row())); 41 | } 42 | 43 | painter->save(); 44 | 45 | QTextDocument document; 46 | document.setTextWidth(option.rect.width()); 47 | document.setPageSize(option.rect.size()); 48 | 49 | QVariant value = index.data(Qt::DisplayRole); 50 | if (value.isValid() && !value.isNull()) { 51 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) 52 | document.setMarkdown(value.toString()); 53 | #else 54 | document.setPlainText(value.toString()); 55 | #endif // QT_VERSION_CHECK 56 | painter->translate(option.rect.topLeft()); 57 | document.drawContents(painter); 58 | } 59 | 60 | painter->restore(); 61 | } 62 | 63 | QSize TaskDescriptionDelegate::sizeHint(const QStyleOptionViewItem &option, 64 | const QModelIndex &index) const 65 | { 66 | QStyleOptionViewItem options = option; 67 | initStyleOption(&options, index); 68 | 69 | QTextDocument doc; 70 | doc.setHtml(options.text); 71 | doc.setTextWidth(options.rect.width()); 72 | 73 | return QSize(doc.idealWidth(), doc.size().height()); 74 | } 75 | -------------------------------------------------------------------------------- /src/taskdescriptiondelegate.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TASKDESCRIPTIONDELEGATE_HPP 2 | #define TASKDESCRIPTIONDELEGATE_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class TaskDescriptionDelegate : public QStyledItemDelegate { 9 | Q_OBJECT 10 | 11 | public: 12 | TaskDescriptionDelegate(QObject *parent = nullptr); 13 | 14 | QString anchorAt(const QString &markdown, const QPoint &point) const; 15 | 16 | protected: 17 | void paint(QPainter *painter, const QStyleOptionViewItem &option, 18 | const QModelIndex &index) const override; 19 | QSize sizeHint(const QStyleOptionViewItem &option, 20 | const QModelIndex &index) const override; 21 | }; 22 | 23 | #endif // TASKDESCRIPTIONDELEGATE_HPP 24 | -------------------------------------------------------------------------------- /src/taskdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "taskdialog.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "mainwindow.hpp" 20 | #include "optionaldatetimeedit.hpp" 21 | #include "qtutil.hpp" 22 | #include "task.hpp" 23 | #include "tasksmodel.hpp" 24 | 25 | TaskDialog::TaskDialog(QWidget *parent) 26 | : QDialog(parent) 27 | { 28 | } 29 | 30 | Task TaskDialog::getTask() 31 | { 32 | Task task; 33 | 34 | task.description = m_task_description->toPlainText(); 35 | task.priority = Task::priorityFromString(m_task_priority->currentText()); 36 | 37 | auto project = m_task_project->text(); 38 | project.replace("pro:", ""); 39 | project.replace("project:", ""); 40 | task.project = project; 41 | 42 | for (const auto &tag : m_task_tags->getTags()) { 43 | QString t(tag); 44 | t.remove(QChar('+')); 45 | if (!t.isEmpty()) 46 | task.tags.push_back(t); 47 | } 48 | 49 | task.sched = m_task_sched->getDateTime(); 50 | task.due = m_task_due->getDateTime(); 51 | task.wait = m_task_wait->getDateTime(); 52 | 53 | return task; 54 | } 55 | 56 | void TaskDialog::keyPressEvent(QKeyEvent *event) 57 | { 58 | if (event == QKeySequence::Close) { 59 | reject(); 60 | } else { 61 | event->ignore(); 62 | } 63 | } 64 | 65 | void TaskDialog::initUI() 66 | { 67 | setWindowIcon(QIcon(":/icons/qtask.svg")); 68 | 69 | QLabel *description_label = new QLabel("Description:"); 70 | m_task_description = new QTextEdit(); 71 | m_task_description->setTabChangesFocus(true); 72 | QObject::connect(m_task_description, &QTextEdit::textChanged, this, 73 | &TaskDialog::onDescriptionChanged); 74 | 75 | QLabel *priority_label = new QLabel("Priority:"); 76 | m_task_priority = new QComboBox(); 77 | m_task_priority->addItem(""); 78 | m_task_priority->addItem("L"); 79 | m_task_priority->addItem("M"); 80 | m_task_priority->addItem("H"); 81 | 82 | QLabel *project_label = new QLabel("Project:"); 83 | m_task_project = new QLineEdit(); 84 | QObject::connect(m_task_project, &QLineEdit::editingFinished, this, 85 | [&]() { focusNextChild(); }); 86 | 87 | QLabel *tags_label = new QLabel("Tags:"); 88 | m_task_tags = new TagsEdit(); 89 | 90 | m_task_sched = new OptionalDateTimeEdit("Schedule:"); 91 | m_task_sched->setChecked(false); 92 | // Taskwarrior's implementation feature 93 | m_task_sched->setMinimumDateTime(startOfDay(QDate(1980, 1, 2))); 94 | m_task_sched->setMaximumDateTime(startOfDay(QDate(2038, 1, 1))); 95 | m_task_sched->setDateTime(startOfDay(QDate::currentDate()).addDays(1)); 96 | 97 | m_task_due = new OptionalDateTimeEdit("Due:"); 98 | m_task_due->setChecked(false); 99 | m_task_due->setMinimumDateTime(startOfDay(QDate(1980, 1, 2))); 100 | m_task_due->setMaximumDateTime(startOfDay(QDate(2038, 1, 1))); 101 | m_task_due->setDateTime(startOfDay(QDate::currentDate()).addDays(5)); 102 | 103 | m_task_wait = new OptionalDateTimeEdit("Wait:"); 104 | m_task_wait->setChecked(false); 105 | m_task_wait->setMinimumDateTime(startOfDay(QDate(1980, 1, 2))); 106 | m_task_wait->setMaximumDateTime(startOfDay(QDate(2038, 1, 1))); 107 | m_task_wait->setDateTime(startOfDay(QDate::currentDate()).addDays(5)); 108 | 109 | QGridLayout *grid_layout = new QGridLayout(); 110 | grid_layout->addWidget(priority_label, 0, 0); 111 | grid_layout->addWidget(m_task_priority, 0, 1); 112 | grid_layout->addWidget(project_label, 1, 0); 113 | grid_layout->addWidget(m_task_project, 1, 1); 114 | grid_layout->addWidget(tags_label, 2, 0); 115 | grid_layout->addWidget(m_task_tags, 2, 1); 116 | grid_layout->addWidget(m_task_sched, 3, 0, 1, 2); 117 | grid_layout->addWidget(m_task_due, 4, 0, 1, 2); 118 | grid_layout->addWidget(m_task_wait, 5, 0, 1, 2); 119 | 120 | m_main_layout = new QVBoxLayout(); 121 | m_main_layout->addWidget(description_label); 122 | m_main_layout->addWidget(m_task_description); 123 | m_main_layout->addLayout(grid_layout); 124 | m_main_layout->setContentsMargins(5, 5, 5, 5); 125 | 126 | setLayout(m_main_layout); 127 | } 128 | 129 | void TaskDialog::setTask(const Task &task) 130 | { 131 | m_task_description->setText(task.description); 132 | m_task_project->setText(task.project); 133 | m_task_tags->setTags(task.tags); 134 | 135 | m_task_sched->setDateTime(task.sched); 136 | m_task_due->setDateTime(task.due); 137 | m_task_wait->setDateTime(task.wait); 138 | 139 | switch (task.priority) { 140 | case Task::Priority::Unset: 141 | m_task_priority->setCurrentIndex(0); 142 | break; 143 | case Task::Priority::L: 144 | m_task_priority->setCurrentIndex(1); 145 | break; 146 | case Task::Priority::M: 147 | m_task_priority->setCurrentIndex(2); 148 | break; 149 | case Task::Priority::H: 150 | m_task_priority->setCurrentIndex(3); 151 | break; 152 | } 153 | 154 | m_task_uuid = task.uuid; 155 | } 156 | 157 | AddTaskDialog::AddTaskDialog(const QVariant &default_project, QWidget *parent) 158 | : TaskDialog(parent) 159 | { 160 | initUI(); 161 | if (!default_project.isNull()) 162 | m_task_project->setText(default_project.toString()); 163 | } 164 | 165 | void AddTaskDialog::initUI() 166 | { 167 | TaskDialog::initUI(); 168 | setWindowTitle(QCoreApplication::applicationName() + " - Add task"); 169 | Q_ASSERT(m_main_layout); 170 | 171 | m_ok_btn = new QPushButton( 172 | QApplication::style()->standardIcon(QStyle::SP_DialogOkButton), 173 | tr("Ok"), this); 174 | m_ok_btn->setEnabled(false); 175 | auto *create_shortcut = new QShortcut(QKeySequence("Ctrl+Return"), this); 176 | QObject::connect(create_shortcut, &QShortcut::activated, this, 177 | &QDialog::accept); 178 | m_ok_btn->setToolTip(tr("Create task")); 179 | 180 | m_continue_btn = new QPushButton(tr("Continue"), this); 181 | m_continue_btn->setEnabled(false); 182 | auto *continue_shortcut = new QShortcut(QKeySequence("Alt+Return"), this); 183 | QObject::connect(continue_shortcut, &QShortcut::activated, this, 184 | &AddTaskDialog::createTaskAndContinue); 185 | m_continue_btn->setToolTip(tr("Create task and continue")); 186 | 187 | auto *cancel_btn = new QPushButton( 188 | QApplication::style()->standardIcon(QStyle::SP_DialogCancelButton), 189 | tr("Cancel"), this); 190 | auto *cancel_shortcut = new QShortcut(QKeySequence("Escape"), this); 191 | QObject::connect(cancel_shortcut, &QShortcut::activated, this, 192 | &QDialog::reject); 193 | cancel_btn->setToolTip(tr("Cancel and close this window")); 194 | 195 | QBoxLayout *button_layout = new QHBoxLayout(); 196 | button_layout->addWidget(cancel_btn); 197 | button_layout->addWidget(m_continue_btn); 198 | button_layout->addWidget(m_ok_btn); 199 | 200 | connect(m_ok_btn, &QPushButton::clicked, this, &QDialog::accept); 201 | connect(m_continue_btn, &QPushButton::clicked, this, 202 | &AddTaskDialog::createTaskAndContinue); 203 | connect(cancel_btn, &QPushButton::clicked, this, &QDialog::reject); 204 | 205 | m_main_layout->addLayout(button_layout); 206 | } 207 | 208 | void AddTaskDialog::acceptContinue() 209 | { 210 | m_task_description->setText(""); 211 | m_task_description->update(); 212 | m_task_description->setFocus(); 213 | } 214 | 215 | void AddTaskDialog::onDescriptionChanged() 216 | { 217 | if (m_task_description->toPlainText().isEmpty()) { 218 | m_ok_btn->setEnabled(false); 219 | m_continue_btn->setEnabled(false); 220 | } else { 221 | m_ok_btn->setEnabled(true); 222 | m_continue_btn->setEnabled(true); 223 | } 224 | } 225 | 226 | EditTaskDialog::EditTaskDialog(const Task &task, QWidget *parent) 227 | : TaskDialog(parent) 228 | { 229 | setWindowTitle(QCoreApplication::applicationName() + " - Edit task"); 230 | initUI(); 231 | setTask(task); 232 | } 233 | 234 | void EditTaskDialog::initUI() 235 | { 236 | TaskDialog::initUI(); 237 | Q_ASSERT(m_main_layout); 238 | 239 | m_ok_btn = new QPushButton( 240 | QApplication::style()->standardIcon(QStyle::SP_DialogOkButton), 241 | tr("Ok"), this); 242 | auto *create_shortcut = new QShortcut(QKeySequence("Ctrl+Return"), this); 243 | QObject::connect(create_shortcut, &QShortcut::activated, this, 244 | &QDialog::accept); 245 | m_ok_btn->setToolTip(tr("Create task")); 246 | 247 | m_delete_btn = new QPushButton(tr("Delete"), this); 248 | auto *delete_shortcut = new QShortcut(QKeySequence("Ctrl+Delete"), this); 249 | QObject::connect(delete_shortcut, &QShortcut::activated, this, 250 | &EditTaskDialog::requestDeleteTask); 251 | m_delete_btn->setToolTip(tr("Delete this task")); 252 | 253 | auto *cancel_btn = new QPushButton( 254 | QApplication::style()->standardIcon(QStyle::SP_DialogCancelButton), 255 | tr("Cancel"), this); 256 | auto *cancel_shortcut = new QShortcut(QKeySequence("Escape"), this); 257 | QObject::connect(cancel_shortcut, &QShortcut::activated, this, 258 | &QDialog::reject); 259 | cancel_btn->setToolTip(tr("Cancel and close this window")); 260 | 261 | QBoxLayout *button_layout = new QHBoxLayout(); 262 | button_layout->addWidget(cancel_btn); 263 | button_layout->addWidget(m_delete_btn); 264 | button_layout->addWidget(m_ok_btn); 265 | 266 | connect(m_ok_btn, &QPushButton::clicked, this, &QDialog::accept); 267 | connect(m_delete_btn, &QPushButton::clicked, this, 268 | &EditTaskDialog::requestDeleteTask); 269 | connect(cancel_btn, &QPushButton::clicked, this, &QDialog::reject); 270 | 271 | m_main_layout->addLayout(button_layout); 272 | } 273 | 274 | void EditTaskDialog::requestDeleteTask() 275 | { 276 | QMessageBox::StandardButton reply; 277 | reply = QMessageBox::question(this, tr("Conifrm action"), 278 | tr("Delete task #%1?").arg(m_task_uuid), 279 | QMessageBox::Yes | QMessageBox::No); 280 | if (reply == QMessageBox::Yes) { 281 | emit deleteTask(m_task_uuid); 282 | reject(); 283 | } 284 | } 285 | 286 | void EditTaskDialog::onDescriptionChanged() 287 | { 288 | m_ok_btn->setEnabled(!m_task_description->toPlainText().isEmpty()); 289 | } 290 | -------------------------------------------------------------------------------- /src/taskdialog.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TASKDIALOG_HPP 2 | #define TASKDIALOG_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "optionaldatetimeedit.hpp" 14 | #include "tagsedit.hpp" 15 | #include "task.hpp" 16 | #include "taskwarrior.hpp" 17 | 18 | class TaskDialog : public QDialog { 19 | Q_OBJECT 20 | 21 | public: 22 | explicit TaskDialog(QWidget *parent = nullptr); 23 | virtual ~TaskDialog() = default; 24 | 25 | Task getTask(); 26 | 27 | protected: 28 | void keyPressEvent(QKeyEvent *) override; 29 | 30 | void initUI(); 31 | void setTask(const Task &task); 32 | 33 | protected slots: 34 | virtual void onDescriptionChanged() = 0; 35 | 36 | protected: 37 | QVBoxLayout *m_main_layout; 38 | QTextEdit *m_task_description; 39 | QComboBox *m_task_priority; 40 | QLineEdit *m_task_project; 41 | TagsEdit *m_task_tags; 42 | OptionalDateTimeEdit *m_task_sched; 43 | OptionalDateTimeEdit *m_task_due; 44 | OptionalDateTimeEdit *m_task_wait; 45 | 46 | QString m_task_uuid = ""; 47 | }; 48 | 49 | class AddTaskDialog final : public TaskDialog { 50 | Q_OBJECT 51 | 52 | public: 53 | explicit AddTaskDialog(const QVariant &default_project = {}, 54 | QWidget *parent = nullptr); 55 | ~AddTaskDialog() = default; 56 | 57 | protected slots: 58 | void onDescriptionChanged() override; 59 | 60 | private: 61 | void initUI(); 62 | 63 | public slots: 64 | void acceptContinue(); 65 | 66 | signals: 67 | void createTaskAndContinue(); 68 | 69 | private: 70 | QPushButton *m_ok_btn; 71 | QPushButton *m_continue_btn; 72 | }; 73 | 74 | class EditTaskDialog final : public TaskDialog { 75 | Q_OBJECT 76 | 77 | public: 78 | explicit EditTaskDialog(const Task &, QWidget *parent = nullptr); 79 | ~EditTaskDialog() = default; 80 | 81 | protected slots: 82 | void onDescriptionChanged() override; 83 | 84 | private: 85 | void initUI(); 86 | void requestDeleteTask(); 87 | 88 | signals: 89 | void deleteTask(const QString &uuid); 90 | 91 | private: 92 | QPushButton *m_ok_btn; 93 | QPushButton *m_delete_btn; 94 | }; 95 | 96 | #endif // TASKDIALOG_HPP 97 | -------------------------------------------------------------------------------- /src/tasksmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "tasksmodel.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | TasksModel::TasksModel(QObject *parent) 11 | : QAbstractTableModel(parent) 12 | , m_tasks({}) 13 | { 14 | } 15 | 16 | int TasksModel::rowCount(const QModelIndex & /*parent*/) const 17 | { 18 | return m_tasks.size(); 19 | } 20 | 21 | int TasksModel::columnCount(const QModelIndex & /*parent*/) const { return 3; } 22 | 23 | QVariant TasksModel::data(const QModelIndex &index, int role) const 24 | { 25 | if (role == Qt::DisplayRole) { 26 | Task task = m_tasks.at(index.row()); 27 | switch (index.column()) { 28 | case 0: 29 | return task.uuid; 30 | case 1: 31 | return task.project; 32 | case 2: 33 | return task.description; 34 | default: 35 | return false; 36 | } 37 | } 38 | 39 | else if (role == Qt::DecorationRole) { 40 | if (index.column() == 0) { 41 | Task task = m_tasks.at(index.row()); 42 | return (task.active) ? QIcon(":/icons/active.svg") : QVariant(); 43 | } 44 | } 45 | 46 | else if (role == Qt::BackgroundRole) { 47 | return QVariant(QBrush(rowColor(index.row()))); 48 | } 49 | 50 | return QVariant(); 51 | } 52 | 53 | bool TasksModel::setData(const QModelIndex &idx, const QVariant &value, 54 | int role) 55 | { 56 | if (!idx.isValid()) 57 | return false; 58 | 59 | if (role == Qt::EditRole) { 60 | int row = idx.row(); 61 | qDebug() << "Requested edit " << row; 62 | } 63 | 64 | return true; 65 | } 66 | 67 | QVariant TasksModel::headerData(int section, Qt::Orientation orientation, 68 | int role) const 69 | { 70 | if (role != Qt::DisplayRole) 71 | return {}; 72 | 73 | if (orientation == Qt::Horizontal) { 74 | switch (section) { 75 | case 0: 76 | return tr("id"); 77 | case 1: 78 | return tr("Project"); 79 | case 2: 80 | return tr("Description"); 81 | } 82 | } 83 | 84 | return {}; 85 | } 86 | 87 | void TasksModel::setTasks(const QList &tasks) 88 | { 89 | beginResetModel(); 90 | m_tasks = tasks; 91 | endResetModel(); 92 | } 93 | 94 | void TasksModel::setTasks(QList &&tasks) 95 | { 96 | beginResetModel(); 97 | m_tasks = tasks; 98 | endResetModel(); 99 | } 100 | 101 | QVariant TasksModel::getTask(const QModelIndex &index) const 102 | { 103 | if (index.isValid() && index.row() < m_tasks.size()) 104 | return QVariant::fromValue(m_tasks.at(index.row())); 105 | return {}; 106 | } 107 | 108 | QColor TasksModel::rowColor(int row) const 109 | { 110 | QColor c; 111 | 112 | if (row < 0 || row >= m_tasks.size()) { 113 | c.setRgb(0xffffff); 114 | return c; 115 | } 116 | 117 | switch (m_tasks.at(row).priority) { 118 | case Task::Priority::Unset: 119 | c.setRgb(0xffffff); 120 | break; 121 | case Task::Priority::L: 122 | c.setRgb(0xf7ffe4); 123 | break; 124 | case Task::Priority::M: 125 | c.setRgb(0xfffae4); 126 | break; 127 | case Task::Priority::H: 128 | c.setRgb(0xd5acbe); 129 | break; 130 | default: 131 | c.setRgb(0xffffff); 132 | break; 133 | } 134 | 135 | return c; 136 | } 137 | -------------------------------------------------------------------------------- /src/tasksmodel.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TASKSMODEL_HPP 2 | #define TASKSMODEL_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "task.hpp" 11 | 12 | class TasksModel : public QAbstractTableModel { 13 | Q_OBJECT 14 | public: 15 | TasksModel(QObject *parent = nullptr); 16 | ~TasksModel() = default; 17 | 18 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 19 | int columnCount(const QModelIndex &parent = QModelIndex()) const override; 20 | QVariant data(const QModelIndex &index, 21 | int role = Qt::DisplayRole) const override; 22 | bool setData(const QModelIndex &, const QVariant &, 23 | int role = Qt::EditRole) override; 24 | QVariant headerData(int section, Qt::Orientation, 25 | int role = Qt::DisplayRole) const override; 26 | 27 | void setTasks(const QList &); 28 | void setTasks(QList &&); 29 | QVariant getTask(const QModelIndex &) const; 30 | 31 | QColor rowColor(int row) const; 32 | 33 | private: 34 | QList m_tasks; 35 | }; 36 | 37 | #endif // TASKSMODEL_HPP 38 | -------------------------------------------------------------------------------- /src/tasksview.cpp: -------------------------------------------------------------------------------- 1 | #include "tasksview.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "taskdescriptiondelegate.hpp" 10 | #include "tasksmodel.hpp" 11 | 12 | TasksView::TasksView(QWidget *parent) 13 | : QTableView(parent) 14 | { 15 | // needed for the hover functionality 16 | setMouseTracking(true); 17 | } 18 | 19 | void TasksView::mousePressEvent(QMouseEvent *event) 20 | { 21 | constexpr int project_column = 1; 22 | 23 | QModelIndex idx = indexAt(event->pos()); 24 | 25 | // Enable "stop" button if the selected task is active 26 | if (idx.isValid() && event->buttons() & Qt::LeftButton) { 27 | const auto task_opt = qobject_cast(model())->getTask(idx); 28 | if (task_opt.isValid()) { 29 | const auto task = task_opt.value(); 30 | emit selectedTaskIsActive(task.active); 31 | } 32 | } 33 | 34 | // Right click to the "project" column will push it to taskwarrior filter 35 | if (idx.isValid() && idx.column() == project_column && 36 | event->buttons() & Qt::RightButton) { 37 | auto d = idx.data(); 38 | if (!d.isNull()) 39 | emit pushProjectFilter("pro:" + d.toString()); 40 | } 41 | 42 | auto anchor = anchorAt(event->pos()); 43 | m_mouse_press_anchor = anchor; 44 | 45 | QTableView::mousePressEvent(event); 46 | } 47 | 48 | void TasksView::mouseMoveEvent(QMouseEvent *event) 49 | { 50 | auto anchor = anchorAt(event->pos()); 51 | 52 | if (m_mouse_press_anchor != anchor) { 53 | m_mouse_press_anchor.clear(); 54 | } 55 | 56 | if (m_last_hovered_anchor != anchor) { 57 | m_last_hovered_anchor = anchor; 58 | if (!m_last_hovered_anchor.isEmpty()) { 59 | QApplication::setOverrideCursor(QCursor(Qt::PointingHandCursor)); 60 | emit linkHovered(m_last_hovered_anchor); 61 | } else { 62 | QApplication::restoreOverrideCursor(); 63 | emit linkUnhovered(); 64 | } 65 | } 66 | } 67 | 68 | void TasksView::mouseReleaseEvent(QMouseEvent *event) 69 | { 70 | if (!m_mouse_press_anchor.isEmpty()) { 71 | auto anchor = anchorAt(event->pos()); 72 | 73 | if (anchor == m_mouse_press_anchor) { 74 | emit linkActivated(m_mouse_press_anchor); 75 | } 76 | 77 | m_mouse_press_anchor.clear(); 78 | } 79 | 80 | QTableView::mouseReleaseEvent(event); 81 | } 82 | 83 | QString TasksView::anchorAt(const QPoint &pos) const 84 | { 85 | auto index = indexAt(pos); 86 | if (index.isValid()) { 87 | auto delegate = itemDelegate(index); 88 | auto task_delegate = qobject_cast(delegate); 89 | if (task_delegate) { 90 | auto item_rect = visualRect(index); 91 | auto relative_click_position = pos - item_rect.topLeft(); 92 | auto markdown = model()->data(index, Qt::DisplayRole).toString(); 93 | return task_delegate->anchorAt(markdown, relative_click_position); 94 | } 95 | } 96 | 97 | return QString(); 98 | } 99 | -------------------------------------------------------------------------------- /src/tasksview.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TASKSVIEW_HPP 2 | #define TASKSVIEW_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class TasksView : public QTableView { 9 | Q_OBJECT 10 | 11 | public: 12 | explicit TasksView(QWidget *parent = nullptr); 13 | ~TasksView() = default; 14 | 15 | protected: 16 | void mousePressEvent(QMouseEvent *event) override; 17 | void mouseMoveEvent(QMouseEvent *event) override; 18 | void mouseReleaseEvent(QMouseEvent *event) override; 19 | 20 | signals: 21 | void selectedTaskIsActive(bool is_active); 22 | void pushProjectFilter(const QString &); 23 | void linkActivated(const QString &link); 24 | void linkHovered(const QString &link); 25 | void linkUnhovered(); 26 | 27 | private: 28 | QString anchorAt(const QPoint &pos) const; 29 | 30 | private: 31 | QString m_mouse_press_anchor; 32 | QString m_last_hovered_anchor; 33 | }; 34 | 35 | #endif // TASKSVIEW_HPP 36 | -------------------------------------------------------------------------------- /src/taskwarrior.cpp: -------------------------------------------------------------------------------- 1 | #include "taskwarrior.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "configmanager.hpp" 11 | #include "task.hpp" 12 | 13 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) 14 | constexpr auto s_split_behaviour = Qt::SkipEmptyParts; 15 | #else 16 | constexpr auto s_split_behaviour = QString::SkipEmptyParts; 17 | #endif // QT_VERSION_CHECK 18 | 19 | const QStringList Taskwarrior::s_args = { "rc.gc=off", "rc.confirmation=off", 20 | "rc.bulk=0", "rc.defaultwidth=0" }; 21 | 22 | Taskwarrior::Taskwarrior() 23 | : m_actions_counter(0ull) 24 | , m_task_version(QVariant{}) 25 | { 26 | } 27 | 28 | Taskwarrior::~Taskwarrior() {} 29 | 30 | bool Taskwarrior::init() 31 | { 32 | QByteArray out; 33 | if (!execCmd({ "version" }, out, false, false)) 34 | return false; 35 | auto out_bytes = out.split('\n'); 36 | if (out_bytes.size() < 2) 37 | return false; 38 | QString line = out_bytes[1]; 39 | QString task_version = line.split(' ', s_split_behaviour)[1]; 40 | if (task_version.isEmpty()) 41 | return false; 42 | m_task_version = { QString(task_version) }; 43 | 44 | if (!execCmd({ "rc.gc=on" }, false, false)) 45 | return false; 46 | 47 | return true; 48 | } 49 | 50 | bool Taskwarrior::addTask(const Task &task) 51 | { 52 | if (execCmd(QStringList() << "add" << task.getCmdArgs())) { 53 | ++m_actions_counter; 54 | return true; 55 | } 56 | return false; 57 | } 58 | 59 | bool Taskwarrior::startTask(const QString &id) 60 | { 61 | if (execCmd({ "start", id })) { 62 | ++m_actions_counter; 63 | return true; 64 | } 65 | return false; 66 | } 67 | 68 | bool Taskwarrior::stopTask(const QString &id) 69 | { 70 | if (execCmd({ "stop", id })) { 71 | ++m_actions_counter; 72 | return true; 73 | } 74 | return false; 75 | } 76 | 77 | bool Taskwarrior::editTask(const Task &task) 78 | { 79 | if (execCmd(QStringList() << task.uuid << "modify" << task.getCmdArgs())) { 80 | ++m_actions_counter; 81 | return true; 82 | } 83 | return false; 84 | } 85 | 86 | bool Taskwarrior::setPriority(const QString &id, Task::Priority p) 87 | { 88 | const auto p_str = QString{ "pri:'%1'" }.arg(Task::priorityToString(p)); 89 | if (execCmd({ id, "modify", p_str })) { 90 | ++m_actions_counter; 91 | return true; 92 | } 93 | return false; 94 | } 95 | 96 | bool Taskwarrior::getTask(const QString &id, Task &task) 97 | { 98 | QByteArray out; 99 | auto args = QStringList() << id << "information"; 100 | if (!execCmd(args, out)) 101 | return false; 102 | 103 | auto out_bytes = out.split('\n'); 104 | if (out_bytes.size() < 5) 105 | return false; // not found 106 | bool desc_done = false; 107 | for (size_t i = 3; i < out_bytes.size() - 3; ++i) { 108 | const QByteArray &bytes = out_bytes[i]; 109 | if (bytes.isEmpty()) 110 | continue; 111 | QString line(bytes); 112 | if (line.startsWith("ID")) { 113 | task.uuid = line.section(' ', 1).simplified(); 114 | continue; 115 | } 116 | if (line.startsWith("Description")) { 117 | task.description = line.section(' ', 1).simplified(); 118 | continue; 119 | } 120 | if (line.startsWith("Project")) { 121 | task.project = line.section(' ', 1).simplified(); 122 | continue; 123 | } 124 | if (line.startsWith("Priority")) { 125 | task.priority = 126 | Task::priorityFromString(line.section(' ', 1).simplified()); 127 | continue; 128 | } 129 | if (line.startsWith("Tags")) { 130 | auto tags_line = line.section(' ', 1).simplified(); 131 | task.tags = tags_line.split(' ', s_split_behaviour); 132 | continue; 133 | } 134 | if (line.startsWith("Start")) { 135 | task.active = true; 136 | continue; 137 | } 138 | if (line.startsWith("Waiting")) { 139 | const QStringList lexemes = line.split(' ', s_split_behaviour); 140 | if (lexemes.size() != 4) 141 | continue; 142 | auto dt = QDateTime::fromString( 143 | QString{ "%1T%2" }.arg(lexemes[2], lexemes[3]), Qt::ISODate); 144 | if (dt.isValid()) 145 | task.wait = dt; 146 | continue; 147 | } 148 | if (line.startsWith("Scheduled")) { 149 | const QStringList lexemes = line.split(' ', s_split_behaviour); 150 | if (lexemes.size() != 3) 151 | continue; 152 | auto dt = QDateTime::fromString( 153 | QString{ "%1T%2" }.arg(lexemes[1], lexemes[2]), Qt::ISODate); 154 | if (dt.isValid()) 155 | task.sched = dt; 156 | continue; 157 | } 158 | if (line.startsWith("Due")) { 159 | const QStringList lexemes = line.split(' ', s_split_behaviour); 160 | if (lexemes.size() != 3) 161 | continue; 162 | auto dt = QDateTime::fromString( 163 | QString{ "%1T%2" }.arg(lexemes[1], lexemes[2]), Qt::ISODate); 164 | if (dt.isValid()) 165 | task.due = dt; 166 | continue; 167 | } else { 168 | // Searching multiline description 169 | if (desc_done || line.startsWith("Status")) { 170 | desc_done = true; 171 | continue; 172 | } 173 | 174 | QString desc_line = line.section(' ', 1).simplified(); 175 | if (desc_line.isEmpty()) 176 | continue; 177 | 178 | QString first_word = 179 | line.section(' ', 0, 0, QString::SectionSkipEmpty).simplified(); 180 | QDateTime dt = QDateTime::fromString(first_word, Qt::ISODate); 181 | if (dt.isValid()) { 182 | desc_done = true; 183 | continue; 184 | } 185 | 186 | task.description += '\n' + desc_line; 187 | } 188 | } 189 | 190 | return true; 191 | } 192 | 193 | bool Taskwarrior::getTasks(QList &tasks) 194 | { 195 | QByteArray out; 196 | auto args = QStringList() << "rc.report.minimal.columns=id,start.active," 197 | "project,priority,scheduled,due,description" 198 | << "rc.report.minimal.labels=',|,|,|,|,|,|'" 199 | << "rc.report.minimal.sort=urgency-" 200 | << "rc.print.empty.columns=yes" 201 | << "rc.dateformat=Y-M-DTH:N:S" 202 | << "+PENDING" 203 | << "minimal"; 204 | 205 | if (!execCmd(args, out, /*filter_enabled=*/true)) 206 | return false; 207 | 208 | auto out_bytes = out.split('\n'); 209 | if (out_bytes.size() < 6) 210 | return true; // no tasks 211 | 212 | // Get positions from the labels string 213 | QVector positions; 214 | constexpr int columns_num = 6; 215 | for (int i = 0; i < out_bytes[1].size(); ++i) { 216 | const auto b = out_bytes[1][i]; 217 | if (b == '|') 218 | positions.push_back(i); 219 | } 220 | if (positions.size() != columns_num) { 221 | return false; 222 | } 223 | 224 | bool found_annotation = false; 225 | for (size_t i = 3; i < out_bytes.size() - 3; ++i) { 226 | const QByteArray &bytes = out_bytes[i]; 227 | if (bytes.isEmpty()) 228 | continue; 229 | QString line(bytes); 230 | if (line.size() < positions[3]) 231 | return false; 232 | 233 | Task task; 234 | 235 | task.uuid = line.mid(0, positions[0]).simplified(); 236 | bool can_convert = false; 237 | (void)task.uuid.toInt(&can_convert); 238 | if (!can_convert) { 239 | // It's probably a continuation of the multiline description or an 240 | // annotation from the previous task. 241 | if ((line.size() < positions[3]) || tasks.isEmpty() || 242 | found_annotation) 243 | continue; 244 | 245 | QString desc_line = 246 | line.right(line.size() - positions[3]).simplified(); 247 | if (desc_line.isEmpty()) 248 | continue; 249 | 250 | // The annotations always start with a timestamp. And they always 251 | // follows the description. 252 | QString first_word = 253 | line.section(' ', 0, 0, QString::SectionSkipEmpty).simplified(); 254 | QDateTime dt = QDateTime::fromString(first_word, Qt::ISODate); 255 | if (dt.isValid()) { 256 | // We won't handle the annotations at all. 257 | found_annotation = true; 258 | continue; 259 | } 260 | 261 | tasks[tasks.size() - 1].description += '\n' + desc_line; 262 | continue; 263 | } 264 | 265 | found_annotation = false; 266 | 267 | const QString start_mark = 268 | line.mid(positions[0], positions[1] - positions[0]).simplified(); 269 | task.active = start_mark.contains('*'); 270 | task.project = 271 | line.mid(positions[1], positions[2] - positions[1]).simplified(); 272 | task.priority = Task::priorityFromString( 273 | line.mid(positions[2], positions[3] - positions[2]).simplified()); 274 | auto sched = QDateTime::fromString( 275 | line.mid(positions[3], positions[4] - positions[3]).simplified(), 276 | Qt::ISODate); 277 | if (sched.isValid()) 278 | task.sched = sched; 279 | auto due = QDateTime::fromString( 280 | line.mid(positions[4], positions[5] - positions[4]).simplified(), 281 | Qt::ISODate); 282 | if (due.isValid()) 283 | task.due = due; 284 | task.description = line.right(line.size() - positions[5]).simplified(); 285 | 286 | tasks.push_back(task); 287 | } 288 | 289 | return true; 290 | } 291 | 292 | bool Taskwarrior::getRecurringTasks(QList &out_tasks) 293 | { 294 | QByteArray out; 295 | 296 | auto args = QStringList() << "recurring_full" 297 | << "rc.report.recurring_full.columns=id,recur," 298 | "project,description" 299 | << "rc.report.recurring_full.labels=',|,|,|'" 300 | << "status:Recurring"; 301 | 302 | if (!execCmd(args, out)) 303 | return false; 304 | 305 | auto out_bytes = out.split('\n'); 306 | if (out_bytes.size() < 5) 307 | return true; // no tasks 308 | 309 | // Get positions from the labels string 310 | // id created mod status recur wait due project description mask 311 | QVector positions; 312 | constexpr int columns_num = 3; 313 | for (int i = 0; i < out_bytes[2].size(); ++i) { 314 | const auto b = out_bytes[2][i]; 315 | if (b == ' ') 316 | positions.push_back(i); 317 | } 318 | if (positions.size() != columns_num) { 319 | return false; 320 | } 321 | 322 | for (size_t i = 3; i < out_bytes.size() - 2; ++i) { 323 | const QByteArray &bytes = out_bytes[i]; 324 | if (bytes.isEmpty()) 325 | continue; 326 | QString line(bytes); 327 | 328 | RecurringTask task; 329 | task.uuid = 330 | line.section(' ', 0, 0, QString::SectionSkipEmpty).simplified(); 331 | task.period = 332 | line.mid(positions[0], positions[1] - positions[0]).simplified(); 333 | task.project = 334 | line.mid(positions[1], positions[2] - positions[1]).simplified(); 335 | task.description = line.right(line.size() - positions[2]).simplified(); 336 | 337 | out_tasks.push_back(task); 338 | } 339 | 340 | return true; 341 | } 342 | 343 | bool Taskwarrior::deleteTask(const QString &id) 344 | { 345 | if (execCmd({ "delete", id })) { 346 | ++m_actions_counter; 347 | return true; 348 | } 349 | return false; 350 | } 351 | 352 | bool Taskwarrior::deleteTask(const QStringList &ids) 353 | { 354 | if (ids.isEmpty()) 355 | return true; 356 | if (execCmd({ "delete", ids.join(',') })) { 357 | ++m_actions_counter; 358 | return true; 359 | } 360 | return false; 361 | } 362 | 363 | bool Taskwarrior::setTaskDone(const QString &id) 364 | { 365 | if (execCmd({ "done", id })) { 366 | ++m_actions_counter; 367 | return true; 368 | } 369 | return false; 370 | } 371 | 372 | bool Taskwarrior::setTaskDone(const QStringList &ids) 373 | { 374 | if (ids.isEmpty()) 375 | return true; 376 | if (execCmd({ "done", ids.join(',') })) { 377 | ++m_actions_counter; 378 | return true; 379 | } 380 | return false; 381 | } 382 | 383 | bool Taskwarrior::waitTask(const QString &id, const QDateTime &datetime) 384 | { 385 | if (execCmd({ "modify", id, 386 | QString("wait:%1").arg(formatDateTime(datetime)) })) { 387 | ++m_actions_counter; 388 | return true; 389 | } 390 | return false; 391 | } 392 | 393 | bool Taskwarrior::waitTask(const QStringList &ids, const QDateTime &datetime) 394 | { 395 | if (ids.isEmpty()) 396 | return true; 397 | if (execCmd({ "modify", ids.join(','), 398 | QString("wait:%1").arg(formatDateTime(datetime)) })) { 399 | ++m_actions_counter; 400 | return true; 401 | } 402 | return false; 403 | } 404 | 405 | bool Taskwarrior::undoTask() 406 | { 407 | if (m_actions_counter == 0) 408 | return true; 409 | if (execCmd({ "undo" })) { 410 | --m_actions_counter; 411 | return true; 412 | } 413 | return false; 414 | } 415 | 416 | bool Taskwarrior::applyFilter(const QStringList &filter) 417 | { 418 | m_filter = filter; 419 | if (filter.isEmpty()) 420 | return true; 421 | if (execCmd({ "ids" }, /*filter_enabled=*/true)) { // test the new filter 422 | return true; 423 | } 424 | m_filter.clear(); 425 | return false; 426 | } 427 | 428 | int Taskwarrior::directCmd(const QString &cmd) 429 | { 430 | QProcess proc; 431 | int rc = -1; 432 | 433 | QStringList args = cmd.split(' ', s_split_behaviour) << s_args; 434 | 435 | proc.start(ConfigManager::config()->getTaskBin(), args); 436 | if (proc.waitForStarted(1000)) { 437 | if (proc.waitForFinished() && 438 | (proc.exitStatus() == QProcess::NormalExit)) 439 | rc = proc.exitCode(); 440 | } 441 | 442 | return rc; 443 | } 444 | 445 | bool Taskwarrior::execCmd(const QStringList &args, bool filter_enabled, 446 | bool use_standard_args) 447 | { 448 | QProcess proc; 449 | int rc = -1; 450 | 451 | QStringList real_args; 452 | if (filter_enabled) 453 | real_args << m_filter; 454 | if (use_standard_args) 455 | real_args << s_args; 456 | real_args << args; 457 | 458 | qDebug() << ConfigManager::config()->getTaskBin() << real_args; 459 | proc.start(ConfigManager::config()->getTaskBin(), real_args); 460 | 461 | if (proc.waitForStarted(1000)) { 462 | if (proc.waitForFinished(1000) && 463 | (proc.exitStatus() == QProcess::NormalExit)) 464 | rc = proc.exitCode(); 465 | } 466 | 467 | return rc == 0; 468 | } 469 | 470 | bool Taskwarrior::execCmd(const QStringList &args, QByteArray &out, 471 | bool filter_enabled, bool use_standard_args) 472 | { 473 | QProcess proc; 474 | int rc = -1; 475 | 476 | QStringList real_args; 477 | if (filter_enabled) 478 | real_args << m_filter; 479 | if (use_standard_args) 480 | real_args << s_args; 481 | real_args << args; 482 | 483 | qDebug() << ConfigManager::config()->getTaskBin() << real_args; 484 | proc.start(ConfigManager::config()->getTaskBin(), real_args); 485 | 486 | if (proc.waitForStarted(1000)) { 487 | if (proc.waitForFinished(1000) && 488 | (proc.exitStatus() == QProcess::NormalExit)) 489 | rc = proc.exitCode(); 490 | } 491 | 492 | if (rc != 0) 493 | return false; 494 | out = proc.readAllStandardOutput(); 495 | return true; 496 | } 497 | 498 | QString Taskwarrior::formatDateTime(const QDateTime &datetime) const 499 | { 500 | return datetime.toString(Qt::ISODate); 501 | } 502 | -------------------------------------------------------------------------------- /src/taskwarrior.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TASKWARRIOR_HPP 2 | #define TASKWARRIOR_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "task.hpp" 11 | 12 | class Taskwarrior { 13 | public: 14 | Taskwarrior(); 15 | ~Taskwarrior(); 16 | 17 | /// Detect the version of task and check that it works. This function 18 | /// will also runs garbage collection. This will un-waiting tasks and add 19 | /// new recurring tasks. 20 | bool init(); 21 | 22 | size_t getActionsCounter() const { return m_actions_counter; } 23 | QVariant getTaskVersion() const { return m_task_version; } 24 | 25 | bool addTask(const Task &task); 26 | bool startTask(const QString &id); 27 | bool stopTask(const QString &id); 28 | bool editTask(const Task &task); 29 | bool setPriority(const QString &id, Task::Priority); 30 | bool getTask(const QString &id, Task &out_task); 31 | bool getTasks(QList &task); 32 | bool getRecurringTasks(QList &out_tasks); 33 | bool deleteTask(const QString &id); 34 | bool deleteTask(const QStringList &ids); 35 | bool setTaskDone(const QString &id); 36 | bool setTaskDone(const QStringList &ids); 37 | bool waitTask(const QString &id, const QDateTime &datetime); 38 | bool waitTask(const QStringList &ids, const QDateTime &datetime); 39 | bool undoTask(); 40 | 41 | bool applyFilter(const QStringList &); 42 | 43 | int directCmd(const QString &cmd); 44 | 45 | private: 46 | bool execCmd(const QStringList &args, bool filter_enabled = false, 47 | bool use_standard_args = true); 48 | bool execCmd(const QStringList &args, QByteArray &out, 49 | bool filter_enabled = false, bool use_standard_args = true); 50 | 51 | bool getActiveIds(QStringList &result); 52 | 53 | QString formatDateTime(const QDateTime &) const; 54 | 55 | private: 56 | static const QStringList s_args; 57 | 58 | QStringList m_filter; 59 | 60 | /// Counter of changes in the taskwarrior database that can be undone. 61 | size_t m_actions_counter; 62 | 63 | QVariant m_task_version; 64 | }; 65 | 66 | #endif // TASKWARRIOR_HPP 67 | -------------------------------------------------------------------------------- /src/taskwarriorreferencedialog.cpp: -------------------------------------------------------------------------------- 1 | #include "taskwarriorreferencedialog.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | TaskwarriorReferenceDialog::TaskwarriorReferenceDialog(QWidget *parent) 11 | : QDialog(parent) 12 | { 13 | initUI(); 14 | } 15 | 16 | void TaskwarriorReferenceDialog::initUI() 17 | { 18 | setWindowTitle(QCoreApplication::applicationName() + 19 | " - Taskwarrior quick reference"); 20 | 21 | const auto group = QPalette::Active; 22 | const auto role = QPalette::Window; 23 | 24 | auto palette = QApplication::palette(); 25 | QColor c1 = palette.color(group, role).name(); 26 | c1.setGreen(c1.green() + 10); 27 | QColor c2 = palette.color(group, role).name(); 28 | c2.setBlue(c2.blue() + 10); 29 | QColor c3 = palette.color(group, role).name(); 30 | c3.setRed(c3.red() + 10); 31 | 32 | const QString info = 33 | QString("" 35 | "" 36 | "" 37 | "" 48 | "" 49 | "" 67 | "" 68 | "
" 38 | "
" 39 | "

Filters

" 40 | "" 41 | "" 42 | "" 43 | "" 44 | "" 45 | "
pro:project_name
pro.not:project_name
+tag_name
-tag_name
" 46 | "
" 47 | "
" 50 | "
" 51 | "

Virtual tags

" 52 | "" 53 | "" 54 | "" 55 | "" 56 | "" 57 | "" 58 | "" 59 | "" 60 | "" 61 | "" 62 | "" 63 | "" 64 | "" 65 | "
+DUE
+DUETODAY
+OVERDUE
+WEEK
+MONTH
+YEAR
+ACTIVE
+SCHEDULED
+UNTIL
+WAITING
+ANNOTATED
" 66 | "
" 69 | "") 70 | .arg(c1.name(), c2.name()); 71 | 72 | QVBoxLayout *main_layout = new QVBoxLayout(); 73 | QLabel *info_label = new QLabel(info); 74 | info_label->setOpenExternalLinks(true); 75 | info_label->setTextInteractionFlags(Qt::TextBrowserInteraction); 76 | main_layout->addWidget(info_label); 77 | 78 | setLayout(main_layout); 79 | } 80 | -------------------------------------------------------------------------------- /src/taskwarriorreferencedialog.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TASKWARRIORREFERENCEDIALOG_HPP 2 | #define TASKWARRIORREFERENCEDIALOG_HPP 3 | 4 | #include 5 | #include 6 | 7 | class TaskwarriorReferenceDialog : public QDialog { 8 | public: 9 | explicit TaskwarriorReferenceDialog(QWidget *parent = nullptr); 10 | ~TaskwarriorReferenceDialog() = default; 11 | 12 | private: 13 | void initUI(); 14 | }; 15 | 16 | #endif // TASKWARRIORREFERENCEDIALOG_HPP 17 | -------------------------------------------------------------------------------- /src/taskwatcher.cpp: -------------------------------------------------------------------------------- 1 | #include "taskwatcher.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | TaskWatcher::TaskWatcher(QObject *parent) 8 | : m_task_data_path("") 9 | , m_active(false) 10 | , m_task_data_watcher(nullptr) 11 | { 12 | } 13 | 14 | TaskWatcher::~TaskWatcher() {} 15 | 16 | bool TaskWatcher::setup(const QString &task_data_path) 17 | { 18 | QStringList watch_files = { task_data_path + "pending.data" }; 19 | for (auto const &f : watch_files) 20 | if (!QFile::exists(f)) 21 | return false; 22 | 23 | m_task_data_watcher = new QFileSystemWatcher(watch_files); 24 | connect(m_task_data_watcher, &QFileSystemWatcher::fileChanged, this, 25 | &TaskWatcher::dataChanged); 26 | 27 | m_active = true; 28 | return true; 29 | } 30 | -------------------------------------------------------------------------------- /src/taskwatcher.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TASKWATCHER_HPP 2 | #define TASKWATCHER_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class TaskWatcher : public QObject { 9 | Q_OBJECT 10 | 11 | public: 12 | TaskWatcher(QObject *parent = nullptr); 13 | ~TaskWatcher(); 14 | 15 | bool setup(const QString &task_data_path); 16 | bool isActive() const { return m_active; } 17 | 18 | signals: 19 | void dataChanged(const QString &); 20 | 21 | private: 22 | QString m_task_data_path; 23 | bool m_active; 24 | QFileSystemWatcher *m_task_data_watcher; 25 | }; 26 | 27 | #endif // TASKWATCHER_HPP 28 | -------------------------------------------------------------------------------- /src/trayicon.cpp: -------------------------------------------------------------------------------- 1 | #include "trayicon.hpp" 2 | 3 | #include 4 | #include 5 | 6 | using namespace ui; 7 | 8 | SystemTrayIcon::SystemTrayIcon(QObject *parent) 9 | : QSystemTrayIcon(parent) 10 | { 11 | tray_icon_menu_ = new QMenu(); 12 | add_task_action_ = new QAction("Add &task"); 13 | mute_notifications_action_ = new QAction("&Mute notifications"); 14 | mute_notifications_action_->setCheckable(true); 15 | exit_action_ = new QAction("Quit"); 16 | 17 | // tray_icon_menu_->addAction(mute_notifications_action_); 18 | // tray_icon_menu_->addSeparator(); 19 | tray_icon_menu_->addAction(add_task_action_); 20 | tray_icon_menu_->addSeparator(); 21 | tray_icon_menu_->addAction(exit_action_); 22 | 23 | connect(mute_notifications_action_, &QAction::triggered, this, 24 | &SystemTrayIcon::muteNotificationsRequested); 25 | connect(add_task_action_, &QAction::triggered, this, 26 | &SystemTrayIcon::addTaskRequested); 27 | connect(exit_action_, &QAction::triggered, this, 28 | &SystemTrayIcon::exitRequested); 29 | 30 | setContextMenu(tray_icon_menu_); 31 | setIcon(QIcon(":/icons/qtask.svg")); 32 | setToolTip("QTask"); 33 | } 34 | -------------------------------------------------------------------------------- /src/trayicon.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SYSTRAYICON_HPP 2 | #define SYSTRAYICON_HPP 3 | 4 | #include 5 | 6 | #include 7 | 8 | class QAction; 9 | class QMenu; 10 | class QObject; 11 | 12 | namespace ui 13 | { 14 | 15 | class SystemTrayIcon : public QSystemTrayIcon { 16 | Q_OBJECT 17 | 18 | public: 19 | SystemTrayIcon(QObject *parent); 20 | 21 | signals: 22 | void muteNotificationsRequested(bool value); 23 | void addTaskRequested(); 24 | void exitRequested(); 25 | 26 | private: 27 | QMenu *tray_icon_menu_; 28 | QAction *add_task_action_; 29 | QAction *mute_notifications_action_; 30 | QAction *exit_action_; 31 | }; 32 | 33 | } // namespace ui 34 | 35 | #endif // SYSTRAYICON_HPP 36 | --------------------------------------------------------------------------------