#include "config.h" #include "mainwindow.h" #include "ui_mainwindow.h" #include "tasktreemodel.h" #include "timetreemodel.h" #include "helper.h" #include "newpassworddlg.h" #include "passworddlg.h" #include #include #include #include #include #include #include #include "appevents.h" #include "preferencesdlg.h" #include "timetreedlg.h" #include "timereportwizard.h" #include "aboutdlg.h" #include "attachmentslist.h" #include "attachmentsdialog.h" #include "fvupdater.h" #include "taskaction.h" #include "finddialog.h" #include "startworkdialog.h" #include "stopworkdialog.h" #include "connectdb_widget.h" #include "openorcreatedb_widget.h" #include #include #define SETTINGS mSettings->data() MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), mPasswordFailed(false), mFindInTasksDlg(this), mDockRecentMenu(nullptr) { mSettings = QSharedPointer(new Settings()); helper::theme::applyCurrent(*mSettings); mAttachmentsAction = nullptr; mAttachmentsLabel = nullptr; this->setUpdatesEnabled(false); mDuplicationSignalLabel = nullptr; mCurrentIntervalLabel = nullptr; mTrayIcon = nullptr; QCoreApplication::setApplicationName(APPNAME); loadGeometry(); // Now check if database is already configured QWidget* centralWidget = new QWidget(this); setCentralWidget(centralWidget); centralWidget->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); // Check if database file exists QString path = helper::path::pathToDatabase(); if (mSettings->data()[KEY_DB_FILENAME_SPECIFIED].toBool()) path = mSettings->data()[KEY_DB_FILENAME].toString(); QString folder = QFileInfo(path).absoluteDir().path(); Storage::instance().setPath(path); if (!QFile::exists(path)) askNewDbPassword(); else askDbPassword(QString()); this->setUpdatesEnabled(true); } MainWindow::~MainWindow() { delete ui; //delete mCurrentIntervalLabel; } void MainWindow::attachDatabase() { // Open database QString path = helper::path::pathToDatabase(); if (mSettings->data()[KEY_DB_FILENAME_SPECIFIED].toBool()) path = mSettings->data()[KEY_DB_FILENAME].toString(); QString folder = QFileInfo(path).absoluteDir().path(); Storage::instance().setPath(path); /* if (!QFile::exists(path)) { QDir().mkpath(folder); // Ask about new password mNewPasswordDlg = new NewPasswordDlg(this); connect(mNewPasswordDlg, SIGNAL(finished(int)), this, SLOT(newPasswordDialogFinished(int))); mNewPasswordDlg->show(); } else { // See if there is stored password if (mSettings->data()[KEY_AUTOSAVE_PASSWORD].toBool() && mSettings->data()[KEY_PASSWORD].toString() != NOPASSWORDSTRING) password = mSettings->data()[KEY_PASSWORD].toString(); else { mPasswordDlg = new PasswordDlg(this); connect(mPasswordDlg, SIGNAL(finished(int)), this, SLOT(passwordDialogFinished(int))); mPasswordDlg->show(); } }*/ this->setUpdatesEnabled(true); } void MainWindow::connectUiToDatabase() { ui->mTaskTree->setModel(mTaskTreeModel = new TaskTreeModel(false)); ui->mTaskTree->setItemDelegate(&mTaskItemDelegate); connect(mTaskTreeModel, SIGNAL(onTaskMoved(PTask)), this, SLOT(taskMoved(PTask))); ChangesHistory::instance().setTaskTreeModel(mTaskTreeModel); connect(ui->mTaskTree->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(taskIndexChanged(QModelIndex,QModelIndex))); mUpdateTimer = new QTimer(this); connect(mUpdateTimer, SIGNAL(timeout()), SLOT(updateData())); mUpdateTimer->setInterval(1000); mUpdateTimer->setSingleShot(false); mUpdateTimer->start(); connect(this, SIGNAL(onTimeFormatChanged()), this, SLOT(timeFormatChanged())); connect(this, SIGNAL(onTimeChanged()), this, SLOT(timeFormatChanged())); if (mTaskTreeModel->hasChildren()) ui->mTaskTree->setCurrentIndex(mTaskTreeModel->index(0,0, QModelIndex())); mTaskTreeModel->setExpandedState(mSettings->data()[KEY_EXPANDED_TASKS].toString(), *ui->mTaskTree); mTaskTreeModel->setSelected(mSettings->data()[KEY_SELECTED_TASKS].toString(), *ui->mTaskTree); // Load recent tasks QString recent = mSettings->data()[KEY_RECENT_TASKS].toString(); QStringList recentList = recent.split(";", QString::SkipEmptyParts); for (QString& s: recentList) { Task::Id id = s.toULongLong(); PTask t = Storage::instance().findTaskById(id); if (t) mRecentTrackingTasks.push_back(t); } prepareRecentTasksMenu(ui->mStartRecentTaskMenu); if (mDockRecentMenu) prepareRecentTasksMenu(mDockRecentMenu); } void MainWindow::alertBox(const QString &title, const QString &text, AlertType alertType) { mAlertBox = new QMessageBox(alertType == AlertType_Critical ? QMessageBox::Critical : QMessageBox::Warning, title, text, QMessageBox::Ok, this, Qt::Sheet); switch (alertType) { case AlertType_Critical: connect(mAlertBox, SIGNAL(finished(int)), this, SLOT(criticalAlertFinished(int))); break; case AlertType_Warning: connect(mAlertBox, SIGNAL(finished(int)), this, SLOT(warningAlertFinished(int))); break; } mAlertBox->show(); } void MainWindow::initClient() { mFindStartIndex = 0; // Open settings mLogger = QSharedPointer(new Logger()); ui->mAboutAction->setMenuRole(QAction::AboutRole); ui->mExitAction->setMenuRole(QAction::QuitRole); ui->mPreferencesAction->setMenuRole(QAction::PreferencesRole); connect(ui->mAboutAction, SIGNAL(triggered()), this, SLOT(about())); connect(ui->mPreferencesAction, SIGNAL(triggered()), this, SLOT(preferences())); // Add Check for update to OS X application menu ui->mCheckForUpdatesAction->setMenuRole(QAction::ApplicationSpecificRole); // Build tray icon if system support this one if (QSystemTrayIcon::isSystemTrayAvailable() && mSettings->data()[KEY_SHOW_TRAY_ICON].toBool()) initTrayIcon(); loadGeometry(); ui->mTaskTree->setSelectionBehavior(QAbstractItemView::SelectRows); ui->mTaskTree->setFocus(); ui->mTaskTree->setDragEnabled(true); ui->mTaskTree->setAcceptDrops(true); ui->mTaskTree->setDropIndicatorShown(true); ui->mTaskTree->setDragDropMode(QAbstractItemView::InternalMove); ui->mTaskTree->setAttribute(Qt::WA_MacShowFocusRect, false); mCurrentIntervalLabel = new QLabel(); ui->mStatusBar->addPermanentWidget(mCurrentIntervalLabel); mDuplicationSignalLabel = new QLabel(); ui->mStatusBar->addPermanentWidget(mDuplicationSignalLabel); mModifiedLabel = new QLabel(); ui->mStatusBar->addPermanentWidget(mModifiedLabel); mActivityTracker = QSharedPointer(new HIDActivityTracker()); connect(mActivityTracker.data(), SIGNAL(idleDetected()), this, SLOT(idleDetected())); connect(mActivityTracker.data(), SIGNAL(activityDetected()), this, SLOT(activityDetected())); // Configure toolbar connect(ui->mMainToolbar, SIGNAL(visibilityChanged(bool)), this, SLOT(toolbarVisibilityChanged(bool))); QWidget *spacerWidget = new QWidget(this); spacerWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); spacerWidget->setVisible(true); ui->mMainToolbar->addWidget(spacerWidget); mAttachmentsAction = new QAction(this); mAttachmentsAction->setIcon(QIcon(":/icons/icons/mail-attachment.png")); mAttachmentsAction->setText(tr("Attachments")); //ui->mMainToolbar->addAction(mAttachmentsAction); connect(mAttachmentsAction, SIGNAL(triggered()), this, SLOT(showAttachments())); mAttachmentsLabel = new QLabel(this); mAttachmentsLabel->setMargin(5); mAttachmentsLabel->setOpenExternalLinks(false); QFont f = mAttachmentsLabel->font(); f.setUnderline(true); mAttachmentsLabel->setFont(f); mAttachmentsLabel->setTextFormat(Qt::RichText); //mAttachmentsLabel->setStyleSheet("QLabel { color:rgb(142,178,218); }"); QPalette newPal(palette()); newPal.setColor(QPalette::Link, QColor(0x100, 0x100, 0x100)); newPal.setColor(QPalette::LinkVisited, QColor(0x100, 0x100, 0x100)); mAttachmentsLabel->setPalette(newPal); ui->mMainToolbar->addWidget(mAttachmentsLabel); connect(mAttachmentsLabel, SIGNAL(linkActivated(QString)), this, SLOT(showAttachments())); updateAttachmentsLabel(PTask()); #ifdef TARGET_OSX mSleepTracker.install(); connect(&mSleepTracker, SIGNAL(onSystemSleep()), this, SLOT(systemSleep())); connect(&mSleepTracker, SIGNAL(onSystemResume()), this, SLOT(systemResume())); installDockMenu(); #endif mTimeFrameHeight = 64;//ui->mTimeFrame->height(); ui->mStartRecentTaskMenu->setEnabled(false); } void MainWindow::save() { // Flush current task PTask t = mTaskTreeModel->getTask(ui->mTaskTree->currentIndex()); if (t) { t->setHtml(ui->mNoteEdit->document()->toPlainText()); t->save(); mModifiedLabel->setText(tr("Saved")); } try { if (Storage::instance().isOpened()) Storage::instance().save(); saveGeometry(); } catch(std::exception& e) {} catch(...) {} } QSharedPointer MainWindow::getUndoStack() const { //QTextDocument* doc = ui->mNoteEdit->document(); return QSharedPointer(new QByteArray()); } void MainWindow::saveGeometry() { QRect geom = this->geometry(); mSettings->data()[KEY_LEFT] = geom.left(); // "left" mSettings->data()[KEY_TOP] = geom.top(); // "top" mSettings->data()[KEY_WIDTH] = geom.width(); // "width" mSettings->data()[KEY_HEIGHT] = geom.height(); // "height" mSettings->data()[KEY_MAXIMIZED] = isMaximized(); // "maximized" QList sizes = ui->mSplitter->sizes(); if (!sizes.empty()) { mSettings->data()[KEY_SPLITTEROFFSET1] = sizes[0]; // ("splitter_offset1", sizes[0]); mSettings->data()[KEY_SPLITTEROFFSET2] = sizes[1]; //("splitter_offset2", sizes[1]); } sizes = ui->mTimeSplitter->sizes(); if (!sizes.empty()) { SETTINGS[KEY_TIMESPLITTER_OFFSET1] = sizes[0]; SETTINGS[KEY_TIMESPLITTER_OFFSET2] = sizes[1]; } if (mTaskTreeModel) { mSettings->data()[KEY_EXPANDED_TASKS] = this->mTaskTreeModel->getExpandedState(*ui->mTaskTree); mSettings->data()[KEY_SELECTED_TASKS] = this->mTaskTreeModel->getSelected(*ui->mTaskTree); } // Save recent list QString recent; for (PTask t: mRecentTrackingTasks) { recent += QString::number(t->id()) + ";"; } mSettings->data()[KEY_RECENT_TASKS] = recent; mSettings->save(); } void MainWindow::loadGeometry() { QVariantMap& settings = mSettings->data(); QRect geom = this->geometry(); if (settings.value(KEY_MAXIMIZED).toBool()) this->showMaximized(); else { if (settings.contains(KEY_LEFT)) geom.setLeft(settings.value("left").toInt()); if (settings.contains(KEY_TOP)) geom.setTop(settings.value("top").toInt()); if (settings.contains(KEY_WIDTH)) geom.setWidth(settings.value(KEY_WIDTH).toInt()); if (settings.contains(KEY_HEIGHT)) geom.setHeight(settings.value(KEY_HEIGHT).toInt()); setGeometry(geom); } // Set splitter position if (settings.contains(KEY_SPLITTEROFFSET1) && settings.contains(KEY_SPLITTEROFFSET2) && ui) { QList sizes = ui->mSplitter->sizes(); sizes[0] = settings.value(KEY_SPLITTEROFFSET1).toInt(); sizes[1] = settings.value(KEY_SPLITTEROFFSET2).toInt(); ui->mSplitter->setSizes(sizes); } if (settings.contains(KEY_TIMESPLITTER_OFFSET1) && settings.contains(KEY_TIMESPLITTER_OFFSET2) && ui) { QList sizes = ui->mTimeSplitter->sizes(); sizes[0] = settings.value(KEY_TIMESPLITTER_OFFSET1).toInt(); sizes[1] = settings.value(KEY_TIMESPLITTER_OFFSET2).toInt(); ui->mTimeSplitter->setSizes(sizes); } } void MainWindow::sync() { alertBox(APPNAME, NOTIMPLEMENTEDTEXT, AlertType_Warning); } void MainWindow::closeEvent(QCloseEvent * ev) { if (ui) { // Update DB if document was modified if (ui->mNoteEdit->document()->isModified()) { // See if there is active selection QModelIndex index = ui->mTaskTree->currentIndex(); if (index.isValid()) { PTask task = mTaskTreeModel->getTask(index); task->setHtml(ui->mNoteEdit->document()->toPlainText()); task->save(); } } if (!mPasswordFailed) save(); } ev->accept(); } void MainWindow::customEvent(QEvent *ev) { switch (ev->type()) { case (QEvent::Type)ClientInitId: // Process client initialization here initClient(); break; case (QEvent::Type)ClientCloseId: close(); break; case (QEvent::Type)AttachDatabaseId: attachDatabase(); break; case (QEvent::Type)SelectTaskId: { QModelIndex index = mTaskTreeModel->getIndex(dynamic_cast(ev)->task()); if (index.isValid()) { ui->mTaskTree->setCurrentIndex(index); } break; } default: break; } } void MainWindow::preferences() { PreferencesDlg pref(this, *mSettings); if (pref.exec() == QDialog::Accepted) { // Changes are saved into settings already // Repaint time labels if no tracking yet QModelIndex index = ui->mTaskTree->currentIndex(); PTask t = mTaskTreeModel->getTask(index); // Reset time text where needed emit onTimeFormatChanged(); // Delete autosaved password if needed if (mSettings->data()[KEY_AUTOSAVE_PASSWORD].toBool() == false) { mSettings->data()[KEY_PASSWORD] = NOPASSWORDSTRING; mSettings->save(); } // Hide/Show tray icon if need if (mSettings->data()[KEY_SHOW_TRAY_ICON].toBool() && !mTrayIcon && QSystemTrayIcon::isSystemTrayAvailable()) initTrayIcon(); else if (!mSettings->data()[KEY_SHOW_TRAY_ICON].toBool() && mTrayIcon) removeTrayIcon(); updateData(); } } void MainWindow::about() { AboutDlg dlg(this); dlg.exec(); } void MainWindow::quit() { // TODO - check if stop on idle dialog runs now and stop on idle time stopTracking(TSR_Manual); save(); #ifdef TARGET_OSX mSleepTracker.uninstall(); #endif close(); } void MainWindow::newRootTask() { PTask rootTask = mTaskTreeModel->addTask(QModelIndex(), Storage::instance().topOfTaskTree().size()); QModelIndex rootIndex = mTaskTreeModel->getIndex(rootTask); ui->mTaskTree->setCurrentIndex(rootIndex); ui->mTaskTree->edit(rootIndex); } void MainWindow::newTask() { QModelIndex index = ui->mTaskTree->currentIndex(); if (index.isValid()) ui->mTaskTree->expand(index); PTask parent = mTaskTreeModel->getTask(index); if (!parent) return; PTask child = mTaskTreeModel->addTask(index, parent->children().size()); if (!child) return; QModelIndex childIndex = mTaskTreeModel->getIndex(child); ui->mTaskTree->setCurrentIndex(childIndex); ui->mTaskTree->edit(childIndex); } void MainWindow::newSibling() { QModelIndex index = ui->mTaskTree->currentIndex(); if (!index.isValid()) return; PTask currentTask = mTaskTreeModel->getTask(index); if (!currentTask) return; PTask t = mTaskTreeModel->addTask(index.parent(), index.row() + 1); QModelIndex i = mTaskTreeModel->getIndex(t); ui->mTaskTree->setCurrentIndex(i); ui->mTaskTree->edit(i); } void MainWindow::moveUp() { QModelIndex index = ui->mTaskTree->currentIndex(); if (!index.isValid()) return; PTask currentTask = mTaskTreeModel->getTask(index); if (!currentTask) return; if (index.row() == 0) return; mTaskTreeModel->moveTask(currentTask, -1); ui->mTaskTree->setCurrentIndex(mTaskTreeModel->getIndex(currentTask)); } void MainWindow::moveDown() { QModelIndex index = ui->mTaskTree->currentIndex(); if (!index.isValid()) return; PTask currentTask = mTaskTreeModel->getTask(index); if (!currentTask) return; if (currentTask->parent()) { if (index.row() == currentTask->parent()->children().size() - 1) return; } else if (index.row() == Storage::instance().topOfTaskTree().size() - 1) return; mTaskTreeModel->moveTask(currentTask, +1); ui->mTaskTree->setCurrentIndex(mTaskTreeModel->getIndex(currentTask)); } void MainWindow::renameTask() { QModelIndex index = ui->mTaskTree->currentIndex(); if (index.isValid()) ui->mTaskTree->edit(index); } void MainWindow::deleteTask() { QModelIndex index = ui->mTaskTree->currentIndex(); if (!index.isValid()) return; PTask t = mTaskTreeModel->getTask(index); if (!t) return; // Check if deleted task if (mCurrentTask == t) { alertBox(tr("Problem"), tr("Impossible to delete active task. Please stop tracking before task delete."), AlertType_Warning); } else { if (mAutomaticTask == t) mAutomaticTask.reset(); mTaskTreeModel->deleteTask(ui->mTaskTree->currentIndex()); } } void MainWindow::taskTreeContextualMenu(const QPoint& point) { PTask t = mTaskTreeModel->getTask(ui->mTaskTree->currentIndex()); QMenu* menu = new QMenu(); menu->addAction(ui->mNewRootTaskAction); menu->addAction(ui->mNewTaskAction); menu->addAction(ui->mRenameTaskAction); menu->addAction(ui->mDeleteTaskAction); menu->addAction(ui->mMoveUpAction); menu->addAction(ui->mMoveDownAction); menu->addSeparator(); menu->addAction(ui->mTimelineAction); menu->addAction(mAttachmentsAction); menu->addSeparator(); if (t) { ui->mTimeTrackableAction->setChecked(!(t->flags() & Task::Flag_NoTimeTracking)); menu->addAction(ui->mTimeTrackableAction); } //menu->addAction(tr("Add 10 mins to timeline"), this, SLOT(add10Mins())); menu->exec(this->window()->mapToGlobal(point)); } void MainWindow::taskIndexChanged(const QModelIndex& current, const QModelIndex& previous) { PTask previousTask = mTaskTreeModel->getTask(previous); if (previousTask) { if (ui->mNoteEdit->document()->isModified()) { // Copy modified text to task object previousTask->setHtml(ui->mNoteEdit->document()->toPlainText()); // Flush new content to DB previousTask->save(); // Remember corresponding QTextDocument also // task->setUndoStack(ui->mNoteEdit->document()->getUndoStack()); } // Update status bar mModifiedLabel->setText(tr("Saved")); // Save cursor position previousTask->setCursorPosition(ui->mNoteEdit->textCursor().position()); // Unload content previousTask->unloadContent(previousTask != mCurrentTask); } updateTrayIcon(Tray_SkipMessage); PTask t = mTaskTreeModel->getTask(ui->mTaskTree->currentIndex()); if (!mCurrentTask) { if (t) ui->mStartOrStopTrackingAction->setText(tr("Start tracking ") + t->title()); else ui->mStartOrStopTrackingAction->setText(tr("Start tracking")); } if (t) { mAttachmentsAction->setToolTip(tr("Attachments for ") + t->title()); updateAttachmentsLabel(t); } if (current.isValid()) { // Reset search position mFindStartIndex = 0; // Load doc PTask task = mTaskTreeModel->getTask(current); if (task) { task->loadContent(); ui->mNoteEdit->document()->setPlainText(task->html()); ui->mNoteEdit->document()->setModified(false); // Set cursor position to stored one if (task->cursorPosition() <= task->html().length()) { QTextCursor tmpCursor = ui->mNoteEdit->textCursor(); tmpCursor.setPosition(task->cursorPosition()); ui->mNoteEdit->setTextCursor(tmpCursor); ui->mNoteEdit->ensureCursorVisible(); } mModifiedLabel->setText(tr("Saved")); // Enable / disable Move up / move down actions ui->mMoveUpAction->setEnabled(current.row() > 0); if (task->parent()) ui->mMoveDownAction->setEnabled(current.row() < (task->parent()->children().size() - 1)); else ui->mMoveDownAction->setEnabled(current.row() < (Storage::instance().topOfTaskTree().size() - 1)); } // Load corresponding undo stack //ui->mNoteEdit->document()->setUndoStack(task->getUndoStack()); // Update total time emit onTimeChanged(); // Set new timeline tree model /*int w = ui->mTaskTimeTree->columnWidth(0); ui->mTaskTimeTree->setModel(new TimeTreeModel(task->timeline(), *mSettings)); ui->mTaskTimeTree->setColumnWidth(0, w);*/ // Show or hide time table depending on type of loaded task handleTrackableState(task); ui->mTaskTree->selectionModel()->setCurrentIndex(current, QItemSelectionModel::SelectCurrent); } } void MainWindow::idleDetected() { mActivityTracker->acceptIdleState(); // Check if settings allow smart stop if (SETTINGS[KEY_SMART_STOP].toBool() && SETTINGS[KEY_SMART_STOP_MINUTES].toInt() > 0) { if (SETTINGS[KEY_ASK_STOP].toBool()) { // Stop activity tracker to not be confused with following user activity mActivityTracker->stop(); StopWorkDialog* dlg = new StopWorkDialog(); connect(dlg, SIGNAL(continueTracking()), this, SLOT(continueOnIdle())); connect(dlg, SIGNAL(breakTracking(QDateTime)), this, SLOT(breakOnIdle(QDateTime))); showTrayWindow(dlg); } else stopTracking(TSR_Automatic); } } void MainWindow::activityDetected() { mActivityTracker->acceptUserActiveState(); // Check if settings allow smart start if (SETTINGS[KEY_SMART_START].toBool() && mStopReason == TSR_Automatic) { /*if (SETTINGS[KEY_ASK_START].toBool()) { StartWorkDialog* dlg = new StartWorkDialog(mAutomaticTask); connect(dlg, SIGNAL(continueTracking()), this, SLOT(startOnActivity())); connect(dlg, SIGNAL(breakTracking()), this, SLOT(stopOnActivity())); showTrayWindow(dlg); } else*/ { startTracking(); if (mTrayIcon) mTrayIcon->showMessage(tr("Time tracking started"), mCurrentTask->path()); } } } void MainWindow::handleTrackableState(PTask task) { if (!task) return; bool trackable = !(task->flags() & Task::Flag_NoTimeTracking); if (!mCurrentTask) ui->mStartOrStopTrackingAction->setEnabled(trackable); QList sizes; if (trackable) { sizes.push_back(ui->mTimeSplitter->height() - mTimeFrameHeight - ui->mTimeSplitter->handleWidth()); sizes.push_back(mTimeFrameHeight); ui->mTimeSplitter->setHandleWidth(7); } else { sizes.push_back(ui->mTimeSplitter->height() - ui->mTimeSplitter->handleWidth()); sizes.push_back(0); ui->mTimeSplitter->setHandleWidth(0); } ui->mTimeSplitter->setSizes(sizes); } void MainWindow::prepareRecentTasksMenu(QMenu* submenu) { submenu->clear(); submenu->setEnabled(!mRecentTrackingTasks.empty()); for (PTask t: mRecentTrackingTasks) { QAction* action = submenu->addAction(t->title(), this, SLOT(startTrackingRecent())); action->setProperty("taskid", QVariant(t->id())); } } void MainWindow::updateAttachmentsLabel(PTask t) { QString text = "" + tr("No attachments") + ""; if (t) { if (t->getAttachmentCount()) text = "" + QString::number(t->getAttachmentCount()) + " " + tr("attachments") + ""; } mAttachmentsLabel->setText(text); } void MainWindow::setupMainUi() { // Detach old widget setCentralWidget(nullptr); // Construct main UI ui = new Ui::MainWindow(); ui->setupUi(this); // Hide Find line edit for now ui->mFindFrame->setVisible(false); helper::EscapeKeyEventFilter* eventFilter = new helper::EscapeKeyEventFilter(ui->mFindEdit); connect(eventFilter, SIGNAL(escapePressed(QObject*)), this, SLOT(findRejected(QObject*))); ui->mFindEdit->installEventFilter(eventFilter); //QCoreApplication::setOrganizationName(COMPANY); // Set this to your own appcast URL, of course FvUpdater::sharedUpdater()->SetFeedURL("http://satorilight.com/LittAppCast.xml"); initClient(); QApplication::postEvent(this, new AttachDatabaseEvent()); } // Ask password void MainWindow::askDbPassword(const QString& message) { setCentralWidget(nullptr); setCentralWidget(new QWidget(this)); auto cdw = new ConnectDbWidget(message, centralWidget()); connect(cdw, SIGNAL(passwordEntered(QString)), this, SLOT(onDbPasswordEntered(QString))); connect(cdw, SIGNAL(cancelled()), this, SLOT(onDbPasswordCancelled())); QVBoxLayout* l = new QVBoxLayout(centralWidget()); l->addWidget(cdw); l->setAlignment(Qt::AlignCenter); centralWidget()->setLayout(l); } void MainWindow::askNewDbPassword() { setCentralWidget(nullptr); setCentralWidget(new QWidget(this)); auto w = new OpenOrCreateDbWidget(centralWidget()); connect(w, &OpenOrCreateDbWidget::databaseChanged, [this](const QString& path) { onDatabaseChanged(path); }); connect(w, &OpenOrCreateDbWidget::passwordEntered, [this](const QString& password) { onNewDbPasswordEntered(password); }); auto l = new QVBoxLayout(centralWidget()); l->addWidget(w); l->setAlignment(Qt::AlignCenter); centralWidget()->setLayout(l); } void MainWindow::startOrStopTracking() { if (!mCurrentTask) { // Forget about automatic start - it is manual start mAutomaticTask.reset(); startTracking(); } else stopTracking(TSR_Manual); } void MainWindow::startTracking(PTask t) { if (t->flags() & Task::Flag_NoTimeTracking) return; mCurrentTask = t; // Tell timeline to start if (!t->timeline()) t->loadContent(); mCurrentTask->timeline()->start(); mLastTimelineFlush = QDateTime::currentDateTimeUtc(); // Update UI ui->mStartOrStopTrackingAction->setText(tr("Stop tracking ") + mCurrentTask->title()); QIcon icon(ACTION_STOP_ICON_NAME); ui->mStartOrStopTrackingAction->setIcon(icon); updateTrayIcon(Tray_ShowMessage); // Start activity tracker if needed if (mSettings->data()[KEY_SMART_STOP].toBool() && mSettings->data()[KEY_SMART_STOP_MINUTES].toInt() > 0) { mActivityTracker->setInterval(mSettings->data()[KEY_SMART_STOP_MINUTES].toInt() * 60); mActivityTracker->start(); } } void MainWindow::startTracking() { // Start tracking if any task is selected QModelIndex index = ui->mTaskTree->currentIndex(); if (!index.isValid()) return; // Trigger permission dialog if needed if (mSettings->data()[KEY_SMART_STOP].toBool()) { if (!helper::activityTracker::ensureSmartTrackingIsPossible()) mTrayIcon->showMessage(tr("No smart tracking stop/start"), tr("Problem with obtaining permissions"), QSystemTrayIcon::Warning); } // Get pointer to tracking task PTask t; if (mStopReason == TSR_Automatic && mAutomaticTask) { t = mAutomaticTask; mAutomaticTask.reset(); } else t = mTaskTreeModel->getTask(index); if (t) startTracking(t); } void MainWindow::startTrackingRecent() { QObject* sender = QObject::sender(); if (dynamic_cast(sender)) { QVariant taskId = sender->property("taskid"); if (taskId.isValid() && !taskId.isNull()) { PTask t = Storage::instance().findTaskById(taskId.toULongLong()); if (t) { if (mCurrentTask) stopTracking(TSR_Manual); startTracking(t); } } } } void MainWindow::stopTracking(TrackingStopReason reason, time_t current_utc) { // Check if false call if (!mCurrentTask) return; // Save stop reason mStopReason = reason; // If stopping due to idle detection - save current task if (reason == TSR_Automatic) mAutomaticTask = mCurrentTask; // Tell current task to stop mCurrentTask->timeline()->flush(true /* save to DB */, current_utc); mCurrentTask->timeline()->stop(false /* do not update timeline - it is done in the previous line */); // Save stopped task name QString stoppedTaskName = mCurrentTask->path(); // Update recent tracking tasks menu std::deque::iterator taskIter = std::find(mRecentTrackingTasks.begin(), mRecentTrackingTasks.end(), mCurrentTask); if (taskIter == mRecentTrackingTasks.end()) { mRecentTrackingTasks.push_front(mCurrentTask); if (mRecentTrackingTasks.size() > 7) mRecentTrackingTasks.pop_back(); } else { mRecentTrackingTasks.erase(taskIter); mRecentTrackingTasks.push_front(mCurrentTask); } // Update UI prepareRecentTasksMenu(ui->mStartRecentTaskMenu); if (mDockRecentMenu) prepareRecentTasksMenu(mDockRecentMenu); // Reset pointer to current task mCurrentTask.clear(); // Update UI PTask t = mTaskTreeModel->getTask(ui->mTaskTree->currentIndex()); if (t) ui->mStartOrStopTrackingAction->setText(tr("Start tracking ") + t->title()); else ui->mStartOrStopTrackingAction->setText(tr("Start tracking")); QIcon icon(ACTION_START_ICON_NAME); ui->mStartOrStopTrackingAction->setIcon(icon); if (mTrayIcon) { updateTrayIcon(Tray_SkipMessage); if (reason == TSR_Automatic) mTrayIcon->showMessage(tr("Time tracking stopped due to inactivity."), tr("Idle interval detected for ") + stoppedTaskName); else mTrayIcon->showMessage(tr("Time tracking stopped"), stoppedTaskName); } if (mCurrentIntervalLabel) mCurrentIntervalLabel->setText(""); if (reason == TSR_Manual) mActivityTracker->stop(); // Ensure start/stop buttons are enabled or disabled properly handleTrackableState(t); } void MainWindow::updateData() { // Update editor icons bool taskLoaded = ui->mTaskTree->currentIndex().isValid(); bool hasSelection = ui->mNoteEdit->textCursor().hasSelection(); bool inEditor = (ui->mNoteEdit->hasFocus()); bool editCanUndo = ui->mNoteEdit->document()->availableUndoSteps(); bool editCanRedo = ui->mNoteEdit->document()->availableRedoSteps(); ui->mUndoEditAction->setEnabled((taskLoaded & inEditor && editCanUndo) || ChangesHistory::instance().canUndo()); ui->mRedoEditAction->setEnabled((taskLoaded & inEditor && editCanRedo) || ChangesHistory::instance().canRedo()); ui->mSelectAllEditAction->setEnabled(taskLoaded && inEditor); ui->mCutEditAction->setEnabled(hasSelection & taskLoaded && inEditor); ui->mPasteEditAction->setEnabled(ui->mNoteEdit->canPaste() & taskLoaded && inEditor); ui->mCopyEditAction->setEnabled( hasSelection & taskLoaded && inEditor); if (mCurrentTask) { // Ok, smth is tracking now // Save current time on tracking task QDateTime current = QDateTime::currentDateTimeUtc(); bool saveToDb = mLastTimelineFlush.secsTo(current) >= TIMELINE_FLUSH_INTERVAL; if (saveToDb) mLogger->log("Flushing timeline to DB start"); mCurrentTask->timeline()->flush(saveToDb, QDateTime::currentDateTimeUtc().toTime_t()); if (saveToDb) { mLastTimelineFlush = QDateTime::currentDateTimeUtc(); mLogger->log("Flushing timeline to DB end"); } // Check if duplication detected bool duplication = mCurrentTask->timeline()->duplicateDetected(); mDuplicationSignalLabel->setText(duplication ? tr("Duplication detected") : tr("")); // Tell subscribers about changed time //mLogger->log("Sending onTimeChanged() signal begin"); emit onTimeChanged(); //mLogger->log("Sending onTimeChanged() signal end"); } if (ui->mNoteEdit->document() && taskLoaded) { if (ui->mNoteEdit->document()->isModified()) { PTask t = mTaskTreeModel->getTask(ui->mTaskTree->currentIndex()); if (t && QDateTime::currentDateTimeUtc().secsTo(mTextModificationTime) <= -TEXT_FLUSH_INTERVAL) { t->setHtml(ui->mNoteEdit->document()->toPlainText()); t->save(); ui->mNoteEdit->document()->setModified(false); mModifiedLabel->setText(tr("Saved")); mTextModificationTime = QDateTime(); } } } } void MainWindow::add10Mins() { // Start tracking if any task is selected QModelIndex index = ui->mTaskTree->currentIndex(); if (!index.isValid()) return; // Get current task PTask t = mTaskTreeModel->getTask(index); t->timeline()->load(); t->timeline()->putDebugRecord(); } void MainWindow::applyTextFormat(const QTextCharFormat& fmt) { QTextCursor c = ui->mNoteEdit->textCursor(); if (c.hasSelection()) { c.mergeCharFormat(fmt); ui->mNoteEdit->mergeCurrentCharFormat(fmt); } else { QTextCharFormat f = ui->mNoteEdit->currentCharFormat(); f.merge(fmt); ui->mNoteEdit->setCurrentCharFormat(f); } } void MainWindow::print() { QTextDocument *document = ui->mNoteEdit->document(); QPrinter printer; QPrintDialog *dlg = new QPrintDialog(&printer, this); if (dlg->exec() != QDialog::Accepted) return; document->print(&printer); } void MainWindow::editSelectionChanged() { } void MainWindow::editPositionChanged() { } void MainWindow::editFormatChanged(const QTextCharFormat& /*fmt*/) { } void MainWindow::editUndo() { if (ui->mNoteEdit->hasFocus()) { ui->mNoteEdit->undo(); } else if (ChangesHistory::instance().canUndo()) ChangesHistory::instance().undo(); } void MainWindow::editRedo() { if (ui->mNoteEdit->hasFocus()) { ui->mNoteEdit->redo(); } else if (ChangesHistory::instance().canRedo()) ChangesHistory::instance().redo(); } void MainWindow::editCut() { ui->mNoteEdit->cut(); } void MainWindow::editCopy() { ui->mNoteEdit->copy(); } void MainWindow::editPaste() { ui->mNoteEdit->paste(); } void MainWindow::editDelete() { ui->mNoteEdit->clear(); } void MainWindow::editSelectAll() { ui->mNoteEdit->selectAll(); } void MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason) { switch (reason) { case QSystemTrayIcon::Trigger: case QSystemTrayIcon::DoubleClick: //showTrayWindow(); trayContextualMenu(); /*if (ui->mStartOrStopTrackingAction->isEnabled()) startOrStopTracking();*/ break; default: ; } } void MainWindow::trayContextualMenu() { // Build context menu for tray icon QMenu* menu = new QMenu(); menu->addAction(ui->mStartOrStopTrackingAction); QMenu* recentMenu = menu->addMenu(ui->mStartRecentTaskMenu->title()); recentMenu->setIcon(QIcon(":/icons/icons/empty.png")); prepareRecentTasksMenu(recentMenu); menu->addAction(ui->mShowLittAction); //menu->addAction(ui->mPreferencesAction); //connect(menu, SIGNAL(aboutToHide()), this, SLOT(recoverTrayIcon())); mTrayIcon->setContextMenu(menu); } int MainWindow::showTrayWindow(QDialog* dlg) { if (!dlg) return -1; // Delete old window (if it exists) if (mTrayWindow) delete mTrayWindow; mTrayWindow = dlg; connect(mTrayWindow, SIGNAL(destroyed(QObject*)), this, SLOT(trayWindowDestroyed(QObject*))); dlg->setModal(false); int w = dlg->geometry().width(); int h = dlg->geometry().height(); QRect rec = QApplication::desktop()->screenGeometry(); int desktopHeight = rec.height(); int desktopWidth = rec.width(); QRect iconRect; if (mTrayIcon) iconRect = mTrayIcon->geometry(); else iconRect = QRect(desktopWidth - w - 10, 0, 24, 24); #ifdef TARGET_OSX QRect windowRect(desktopWidth - w - 10, iconRect.bottom() + 10, w, h); #endif #ifdef TARGET_WIN #endif dlg->setGeometry(windowRect); dlg->setVisible(true); dlg->show(); dlg->raise(); dlg->activateWindow(); /* #ifdef TARGET_WIN Qt::WindowFlags eFlags = windowFlags (); eFlags |= Qt::WindowStaysOnTopHint; setWindowFlags(eFlags); eFlags &= ~Qt::WindowStaysOnTopHint; setWindowFlags(eFlags); show(); #endif */ return 0; } void MainWindow::installDockMenu() { QMenu* menu = new QMenu(); menu->addAction(ui->mStartOrStopTrackingAction); mDockRecentMenu = menu->addMenu(ui->mStartRecentTaskMenu->title()); mDockRecentMenu->setIcon(QIcon(":/icons/icons/empty.png")); prepareRecentTasksMenu(mDockRecentMenu); qt_mac_set_dock_menu(menu); } void MainWindow::timeFormatChanged() { // If there is selected task showTimeForSelectedTask(); // Show stats for tracking task in status bar showTimeForTrackingTask(); } void MainWindow::showTimeForSelectedTask() { PTask t = mTaskTreeModel->getTask(ui->mTaskTree->currentIndex()); bool showSeconds = mSettings->data()[KEY_SHOW_SECONDS].toBool(); // Show stats for current task if (t) { if (!t->timeline()) t->loadContent(); int spentSecondsToday = t->timeline()->today(); int spentSecondsMonth = t->timeline()->month(); ui->mTodaySpentTimeLabel->setText(QString::fromStdString(helper::chrono::secondsToDisplay(spentSecondsToday, showSeconds))); ui->mThisMonthSpentTimeLabel->setText(QString::fromStdString(helper::chrono::secondsToDisplay(spentSecondsMonth, showSeconds))); } } void MainWindow::showTimeForTrackingTask() { if (mCurrentTask) { bool showSeconds = mSettings->data()[KEY_SHOW_SECONDS].toBool(); // Update status bar QString path; PTask t = mCurrentTask; while (t) { path.insert(0, " / " + t->title()); t = t->parent(); } int spentSecondsToday = mCurrentTask->timeline()->today(); QString timeString = QString::fromStdString(helper::chrono::secondsToDisplay(spentSecondsToday, showSeconds)); path += " : " + timeString; mCurrentIntervalLabel->setText(path); if (mTrayIcon) { QString tooltip = tr("Litt is tracking ") + mCurrentTask->title() + ".\n" + tr("Time spent today for this task is ") + timeString; mTrayIcon->setToolTip(tooltip); } } } void MainWindow::initTrayIcon() { if (QSystemTrayIcon::isSystemTrayAvailable() && !mTrayIcon) { mTrayIcon = new QSystemTrayIcon(); connect(mTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(iconActivated(QSystemTrayIcon::ActivationReason))); updateTrayIcon(Tray_ShowMessage); mTrayIcon->setVisible(true); } } void MainWindow::removeTrayIcon() { if (mTrayIcon) { delete mTrayIcon; mTrayIcon = nullptr; } } void MainWindow::updateTrayIcon(TrayShowMessage flag) { if (!mTrayIcon) return; QString tooltip; if (mCurrentTask) { bool showSeconds = mSettings->data()[KEY_SHOW_SECONDS].toBool(); int spentSecondsToday = mCurrentTask->timeline()->today(); QString timeString = QString::fromStdString(helper::chrono::secondsToDisplay(spentSecondsToday, showSeconds)); tooltip = tr("Litt is tracking ") + mCurrentTask->title() + ".\n" + tr("Time spent today for this task is ") + timeString; } else { tooltip = tr("Litt is not tracking now."); } mTrayIcon->setToolTip(tooltip); if (mCurrentTask) { QIcon icon(TRAY_RUNNING_ICON_NAME); #if defined(TARGET_OSX) icon.setIsMask(true); #endif mTrayIcon->setIcon(icon); if (flag == Tray_ShowMessage) mTrayIcon->showMessage(tr("Time tracking started"), mCurrentTask->path()); } else { QIcon icon(TRAY_DEFAULT_ICON_NAME); #if defined(TARGET_OSX) icon.setIsMask(true); #endif mTrayIcon->setIcon(icon); } } void MainWindow::showTimeline() { if (!ui->mTaskTree->currentIndex().isValid()) return; PTask t = mTaskTreeModel->getTask(ui->mTaskTree->currentIndex()); TimeTreeDlg dlg(this, t->timeline(), *mSettings); dlg.exec(); // Refresh current timeline stats showTimeForSelectedTask(); } void MainWindow::showTimeReport() { TimeReportWizard trz(*mSettings, this); trz.exec(); } void MainWindow::criticalAlertFinished(int /*status*/) { QApplication::postEvent(this, new ClientCloseEvent()); } void MainWindow::warningAlertFinished(int /*status*/) { } void MainWindow::toolbarVisibilityChanged(bool visible) { ui->mShowToolbarAction->setChecked(visible); } void MainWindow::showHideToolbar() { ui->mMainToolbar->setVisible(ui->mShowToolbarAction->isChecked()); } void MainWindow::showAttachments() { QModelIndex taskIndex = ui->mTaskTree->currentIndex(); if (!taskIndex.isValid()) return; PTask t = mTaskTreeModel->getTask(taskIndex); AttachmentsDialog dlg(t, this); dlg.exec(); // Refresh current item in task tree to ensure attachment icon is shown/hidden mTaskTreeModel->dataChanged(taskIndex, taskIndex); // Refresh attachments link updateAttachmentsLabel(t); } void MainWindow::checkForUpdates() { FvUpdater::sharedUpdater()->CheckForUpdatesNotSilent(); } void MainWindow::systemSleep() { //qDebug() << "System goes to sleep"; stopTracking(TSR_Automatic, QDateTime::currentDateTimeUtc().toTime_t()); } void MainWindow::systemResume() { //qDebug() << "System raised from sleep"; if (mStopReason == TSR_Automatic) startTracking(); } void MainWindow::changeTimeTrackableFlag(bool trackable) { QModelIndex index = ui->mTaskTree->currentIndex(); if (!index.isValid()) return; PTask t = mTaskTreeModel->getTask(index); if (!t) return; if (trackable) t->setFlags(t->flags() & ~Task::Flag_NoTimeTracking ); else t->setFlags(t->flags() | Task::Flag_NoTimeTracking ); // Stop task if it is marked as non tracking and it is running now if (!trackable && mCurrentTask == t) stopTracking(TSR_Manual, time(nullptr)); // Update UI handleTrackableState(t); } void MainWindow::find() { ui->mFindFrame->setVisible(true); ui->mFindEdit->setFocus(); } void MainWindow::findInTasks() { if (mFindInTasksDlg.exec() == QDialog::Accepted) { FindResultItem& r = mFindInTasksDlg.getResult(); if (r.mTask) { // Find task in row and load its content to edit control QModelIndex taskIndex = mTaskTreeModel->getIndex(r.mTask); if (taskIndex.isValid()) { ui->mTaskTree->setCurrentIndex(taskIndex); QTextCursor c = ui->mNoteEdit->document()->find(r.mFoundWord, 0); if (!c.isNull()) ui->mNoteEdit->setTextCursor(c); } } } } void MainWindow::findRequested() { QString pattern = ui->mFindEdit->text(); if (pattern.isEmpty()) return; if (pattern != mFindPattern) { mFindPattern = pattern; mFindStartIndex = 0; } //ui->mFindEdit->setVisible(false); QTextCursor c = ui->mNoteEdit->document()->find(pattern, mFindStartIndex, nullptr); if (c.isNull()) { mFindStartIndex = 0; c = ui->mNoteEdit->document()->find(pattern, mFindStartIndex, nullptr); } if (!c.isNull()) { //c.select(QTextCursor::BlockUnderCursor); ui->mNoteEdit->setTextCursor(c); mFindStartIndex = c.position() + 1; } } void MainWindow::findRejected(QObject* obj) { if (obj == ui->mFindEdit) { ui->mFindFrame->setVisible(false); ui->mNoteEdit->setFocus(); } } void MainWindow::taskTextChanged() { mModifiedLabel->setText(tr("Modified")); if (mTextModificationTime.isNull()) mTextModificationTime = QDateTime::currentDateTimeUtc(); } void MainWindow::taskMoved(PTask task) { QApplication::postEvent(this, new SelectTaskEvent(task)); //QModelIndex index = mTaskTreeModel->getIndex(task); //if (index.isValid()) // ui->mTaskTree->setCurrentIndex(index); } void MainWindow::focusTaskTree() { ui->mTaskTree->setFocus(); } void MainWindow::focusTaskText() { ui->mNoteEdit->setFocus(); } void MainWindow::showMainWindow() { this->show(); this->raise(); this->activateWindow(); #ifdef TARGET_OSX #endif #ifdef TARGET_WIN Qt::WindowFlags eFlags = windowFlags (); eFlags |= Qt::WindowStaysOnTopHint; setWindowFlags(eFlags); eFlags &= ~Qt::WindowStaysOnTopHint; setWindowFlags(eFlags); show(); #endif } void MainWindow::continueOnIdle() { // Disable idle detection at all - it is not needed in this session anymore mActivityTracker->stop(); } void MainWindow::breakOnIdle(const QDateTime& stopTime) { // Stop tracking stopTracking(TSR_Manual, stopTime.toUTC().toTime_t()); showTimeForSelectedTask(); } void MainWindow::startOnActivity() { // Start tracking startTracking(); // Show message if needed if (mTrayIcon) mTrayIcon->showMessage(tr("Time tracking started"), mCurrentTask->path()); } void MainWindow::stopOnActivity() { stopTracking(TSR_Manual); } void MainWindow::trayWindowDestroyed(QObject *object) { mTrayWindow = nullptr; } void MainWindow::onDbPasswordEntered(const QString& password) { if (mSettings->data()[KEY_AUTOSAVE_PASSWORD].toBool()) { mSettings->data()[KEY_PASSWORD] = password; mSettings->save(); } Storage::instance().setKey(password); if (!Storage::instance().open()) { askDbPassword(tr("Invalid password, please try again.")); } else { setupMainUi(); connectUiToDatabase(); loadGeometry(); } } void MainWindow::onDbPasswordCancelled() { askNewDbPassword(); } void MainWindow::onNewDbPasswordEntered(const QString& password) { if (mSettings->data()[KEY_AUTOSAVE_PASSWORD].toBool()) { mSettings->data()[KEY_PASSWORD] = password; mSettings->save(); } Storage::instance().setKey(password); // Remove old database ::remove(Storage::instance().path().toStdString().c_str()); // Try to create new one if (!Storage::instance().create()) { showFatal(tr("Failed to create new database. Exiting.")); } else { setupMainUi(); connectUiToDatabase(); loadGeometry(); } } void MainWindow::onDatabaseChanged(const QString& path) { // Bind to specific database mSettings->data()[KEY_DB_FILENAME_SPECIFIED] = true; mSettings->data()[KEY_DB_FILENAME] = path; mSettings->save(); Storage::instance().setPath(path); askDbPassword(); } void MainWindow::showFatal(const QString& message) { std::cerr << message.toStdString() << std::endl; exit(EXIT_FAILURE); }