From 4b310eb70e3387291c27b2fbef154466c20a6cec Mon Sep 17 00:00:00 2001 From: Darkheim Date: Fri, 11 Jul 2025 22:17:18 -0500 Subject: [PATCH] Initial commit --- .gitignore | 16 ++ CMakeLists.txt | 109 +++++++++ CircularProgressBar.cpp | 69 ++++++ CircularProgressBar.h | 29 +++ KeyboardShortcuts.cpp | 24 ++ KeyboardShortcuts.h | 33 +++ LICENSE | 22 ++ NotificationManager.cpp | 34 +++ NotificationManager.h | 29 +++ PomodoroTimer.cpp | 500 ++++++++++++++++++++++++++++++++++++++ PomodoroTimer.h | 136 +++++++++++ README.md | 44 ++++ SettingsDialog.cpp | 102 ++++++++ SettingsDialog.h | 57 +++++ StatisticsDialog.cpp | 402 ++++++++++++++++++++++++++++++ StatisticsDialog.h | 85 +++++++ SystemTrayManager.cpp | 107 ++++++++ SystemTrayManager.h | 45 ++++ TimerState.cpp | 1 + TimerState.h | 65 +++++ main.cpp | 131 ++++++++++ pomodoro-timer.desktop.in | 9 + 22 files changed, 2049 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 CircularProgressBar.cpp create mode 100644 CircularProgressBar.h create mode 100644 KeyboardShortcuts.cpp create mode 100644 KeyboardShortcuts.h create mode 100644 LICENSE create mode 100644 NotificationManager.cpp create mode 100644 NotificationManager.h create mode 100644 PomodoroTimer.cpp create mode 100644 PomodoroTimer.h create mode 100644 README.md create mode 100644 SettingsDialog.cpp create mode 100644 SettingsDialog.h create mode 100644 StatisticsDialog.cpp create mode 100644 StatisticsDialog.h create mode 100644 SystemTrayManager.cpp create mode 100644 SystemTrayManager.h create mode 100644 TimerState.cpp create mode 100644 TimerState.h create mode 100644 main.cpp create mode 100644 pomodoro-timer.desktop.in diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9170fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# CLion +.idea/ +cmake-build-*/ + +# Build artifacts +*.o +*.a +*.so +*.exe +*_autogen/ + +# User-specific files +*.user +*~ +.DS_Store + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..88ae94e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,109 @@ +cmake_minimum_required(VERSION 3.20) +project(PomodoroTimer VERSION 2.0.0 LANGUAGES CXX) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Find Qt6 +find_package(Qt6 REQUIRED COMPONENTS Core Widgets) +qt6_standard_project_setup() + +# Define source files +set(HEADERS + PomodoroTimer.h + CircularProgressBar.h + SettingsDialog.h + StatisticsDialog.h + TimerState.h + SystemTrayManager.h + KeyboardShortcuts.h + NotificationManager.h +) + +set(SOURCES + main.cpp + PomodoroTimer.cpp + CircularProgressBar.cpp + SettingsDialog.cpp + StatisticsDialog.cpp + TimerState.cpp + SystemTrayManager.cpp + KeyboardShortcuts.cpp + NotificationManager.cpp +) + +# Create executable +qt6_add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) + +# Link Qt libraries +target_link_libraries(${PROJECT_NAME} PRIVATE + Qt6::Core + Qt6::Widgets +) + +# Set target properties +set_target_properties(${PROJECT_NAME} PROPERTIES + AUTOMOC ON + AUTORCC ON + AUTOUIC ON + OUTPUT_NAME "PomodoroTimer" +) + +# Platform-specific configurations +if(WIN32) + set_target_properties(${PROJECT_NAME} PROPERTIES + WIN32_EXECUTABLE TRUE + ) +elseif(APPLE) + set_target_properties(${PROJECT_NAME} PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_BUNDLE_NAME "Pomodoro Timer" + MACOSX_BUNDLE_GUI_IDENTIFIER "com.pomodoroapp.timer" + MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}" + MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}" + MACOSX_BUNDLE_INFO_STRING "Pomodoro Focus Timer" + ) +endif() + +# Compiler warnings and optimizations +if(MSVC) + target_compile_options(${PROJECT_NAME} PRIVATE + /W4 /WX- + $<$:/O2> + ) +else() + target_compile_options(${PROJECT_NAME} PRIVATE + -Wall -Wextra -Wpedantic + $<$:-O3> + $<$:-g -O0> + ) +endif() + +# --- Installation Rules --- + +# Configure desktop file for Linux +if(UNIX AND NOT APPLE) + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/pomodoro-timer.desktop.in" + "${CMAKE_CURRENT_BINARY_DIR}/pomodoro-timer.desktop" + @ONLY + ) + + install( + FILES "${CMAKE_CURRENT_BINARY_DIR}/pomodoro-timer.desktop" + DESTINATION "${CMAKE_INSTALL_PREFIX}/share/applications" + ) +endif() + + +# Install rules (optional) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install" CACHE PATH "Install prefix" FORCE) +endif() + +install(TARGETS ${PROJECT_NAME} + BUNDLE DESTINATION . + RUNTIME DESTINATION bin +) diff --git a/CircularProgressBar.cpp b/CircularProgressBar.cpp new file mode 100644 index 0000000..bc0d332 --- /dev/null +++ b/CircularProgressBar.cpp @@ -0,0 +1,69 @@ +#include "CircularProgressBar.h" +#include +#include + +CircularProgressBar::CircularProgressBar(QWidget *parent) + : QWidget(parent) + , m_value(0) + , m_maximum(100) +{ + setFixedSize(220, 220); +} + +void CircularProgressBar::setValue(int value) +{ + int clampedValue = qBound(0, value, m_maximum); + if (clampedValue != m_value) { + m_value = clampedValue; + update(); + } +} + +void CircularProgressBar::setMaximum(int maximum) +{ + m_maximum = maximum; + update(); +} + +void CircularProgressBar::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + int side = qMin(width(), height()); + QRect rect((width() - side) / 2 + 20, (height() - side) / 2 + 20, + side - 40, side - 40); + + // Background circle + QPen backgroundPen(QColor(200, 200, 200), 12); + backgroundPen.setCapStyle(Qt::RoundCap); + painter.setPen(backgroundPen); + painter.drawEllipse(rect); + + // Progress arc + if (m_value > 0) { + QPen progressPen(QColor(70, 130, 180), 12); + progressPen.setCapStyle(Qt::RoundCap); + painter.setPen(progressPen); + + int angle = (360 * m_value) / m_maximum; + painter.drawArc(rect, 90 * 16, -angle * 16); + } +} + +void CircularProgressBar::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + updateChildPositions(); +} + +void CircularProgressBar::updateChildPositions() +{ + const auto children = findChildren(QString(), Qt::FindDirectChildrenOnly); + for (QWidget *child : children) { + child->resize(width(), 40); + child->move(0, (height() - 40) / 2); + } +} diff --git a/CircularProgressBar.h b/CircularProgressBar.h new file mode 100644 index 0000000..5e174f8 --- /dev/null +++ b/CircularProgressBar.h @@ -0,0 +1,29 @@ +#ifndef CIRCULARPROGRESSBAR_H +#define CIRCULARPROGRESSBAR_H + +#include + +class CircularProgressBar : public QWidget +{ + Q_OBJECT + +public: + explicit CircularProgressBar(QWidget *parent = nullptr); + + void setValue(int value); + void setMaximum(int maximum); + int value() const { return m_value; } + int maximum() const { return m_maximum; } + +protected: + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + +private: + int m_value; + int m_maximum; + + void updateChildPositions(); +}; + +#endif // CIRCULARPROGRESSBAR_H diff --git a/KeyboardShortcuts.cpp b/KeyboardShortcuts.cpp new file mode 100644 index 0000000..d9bab28 --- /dev/null +++ b/KeyboardShortcuts.cpp @@ -0,0 +1,24 @@ +#include "KeyboardShortcuts.h" +#include + +KeyboardShortcuts::KeyboardShortcuts(QWidget *parent) + : QObject(parent) +{ + setupShortcuts(parent); +} + +void KeyboardShortcuts::setupShortcuts(QWidget *parent) +{ + m_startShortcut = new QShortcut(QKeySequence(Qt::Key_Space), parent); + m_pauseShortcut = new QShortcut(QKeySequence(Qt::Key_P), parent); + m_resetShortcut = new QShortcut(QKeySequence(Qt::Key_R), parent); + m_settingsShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Comma), parent); + m_skipShortcut = new QShortcut(QKeySequence(Qt::Key_S), parent); + + connect(m_startShortcut, &QShortcut::activated, this, &KeyboardShortcuts::startPauseRequested); + connect(m_pauseShortcut, &QShortcut::activated, this, &KeyboardShortcuts::pauseRequested); + connect(m_resetShortcut, &QShortcut::activated, this, &KeyboardShortcuts::resetRequested); + connect(m_settingsShortcut, &QShortcut::activated, this, &KeyboardShortcuts::settingsRequested); + connect(m_skipShortcut, &QShortcut::activated, this, &KeyboardShortcuts::skipRequested); +} + diff --git a/KeyboardShortcuts.h b/KeyboardShortcuts.h new file mode 100644 index 0000000..b9dab73 --- /dev/null +++ b/KeyboardShortcuts.h @@ -0,0 +1,33 @@ +#ifndef KEYBOARDSHORTCUTS_H +#define KEYBOARDSHORTCUTS_H + +#include +#include + +class KeyboardShortcuts : public QObject +{ + Q_OBJECT + +public: + explicit KeyboardShortcuts(QWidget *parent = nullptr); + ~KeyboardShortcuts() = default; + +signals: + void startPauseRequested(); + void pauseRequested(); + void resetRequested(); + void settingsRequested(); + void skipRequested(); + +private: + void setupShortcuts(QWidget *parent); + + QShortcut *m_startShortcut; + QShortcut *m_pauseShortcut; + QShortcut *m_resetShortcut; + QShortcut *m_settingsShortcut; + QShortcut *m_skipShortcut; +}; + +#endif // KEYBOARDSHORTCUTS_H + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bce361a --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/NotificationManager.cpp b/NotificationManager.cpp new file mode 100644 index 0000000..fdba42c --- /dev/null +++ b/NotificationManager.cpp @@ -0,0 +1,34 @@ +#include "NotificationManager.h" +#include "SystemTrayManager.h" +#include +#include + +NotificationManager::NotificationManager(QWidget *parent) + : QObject(parent) + , m_parent(parent) + , m_trayManager(nullptr) + , m_notificationsEnabled(true) +{ +} + +void NotificationManager::setSystemTrayManager(SystemTrayManager *trayManager) +{ + m_trayManager = trayManager; +} + +void NotificationManager::setNotificationsEnabled(bool enabled) +{ + m_notificationsEnabled = enabled; +} + +void NotificationManager::showNotification(const QString &message) +{ + if (!m_notificationsEnabled) return; + + if (m_trayManager && m_trayManager->isVisible()) { + m_trayManager->showMessage("Pomodoro Timer", message); + } else if (m_parent) { + QMessageBox::information(m_parent, "Pomodoro Timer", message); + } +} + diff --git a/NotificationManager.h b/NotificationManager.h new file mode 100644 index 0000000..1aafc63 --- /dev/null +++ b/NotificationManager.h @@ -0,0 +1,29 @@ +#ifndef NOTIFICATIONMANAGER_H +#define NOTIFICATIONMANAGER_H + +#include +#include + +class QWidget; +class SystemTrayManager; + +class NotificationManager : public QObject +{ + Q_OBJECT + +public: + explicit NotificationManager(QWidget *parent = nullptr); + ~NotificationManager() = default; + + void setSystemTrayManager(SystemTrayManager *trayManager); + void setNotificationsEnabled(bool enabled); + void showNotification(const QString &message); + +private: + QWidget *m_parent; + SystemTrayManager *m_trayManager; + bool m_notificationsEnabled; +}; + +#endif // NOTIFICATIONMANAGER_H + diff --git a/PomodoroTimer.cpp b/PomodoroTimer.cpp new file mode 100644 index 0000000..d326bd0 --- /dev/null +++ b/PomodoroTimer.cpp @@ -0,0 +1,500 @@ +#include "PomodoroTimer.h" +#include "CircularProgressBar.h" +#include "SettingsDialog.h" +#include "StatisticsDialog.h" +#include "SystemTrayManager.h" +#include "KeyboardShortcuts.h" +#include "NotificationManager.h" +#include "TimerState.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + // UI Layout constants + constexpr int WINDOW_WIDTH = 440; + constexpr int WINDOW_HEIGHT = 550; + constexpr int PROGRESS_BAR_SIZE = 220; + constexpr int MAIN_BUTTON_WIDTH = 100; + constexpr int MAIN_BUTTON_HEIGHT = 40; + constexpr int SMALL_BUTTON_WIDTH = 90; + constexpr int SMALL_BUTTON_HEIGHT = 35; + constexpr int LAYOUT_SPACING = 10; + constexpr int LAYOUT_MARGIN = 25; + + // Font sizes + constexpr int TIME_FONT_SIZE = 24; + constexpr int STATUS_FONT_SIZE = 14; + constexpr int SESSION_FONT_SIZE = 12; + constexpr int BUTTON_FONT_SIZE = 11; + + // Progress bar positioning + constexpr int TIME_LABEL_Y_OFFSET = 90; +} + +PomodoroTimer::PomodoroTimer(QWidget *parent) + : QWidget(parent) + , m_timer(new QTimer(this)) + , m_trayManager(std::make_unique(this)) + , m_keyboardShortcuts(std::make_unique(this)) + , m_notificationManager(std::make_unique(this)) +{ + loadSettings(); + setupUI(); + setupConnections(); + resetTimerState(); + updateDisplay(); + + // Setup manager connections + m_notificationManager->setSystemTrayManager(m_trayManager.get()); + m_notificationManager->setNotificationsEnabled(m_showNotifications); + m_trayManager->show(); +} + +PomodoroTimer::~PomodoroTimer() +{ + saveSettings(); +} + +void PomodoroTimer::setupUI() +{ + setFixedSize(WINDOW_WIDTH, WINDOW_HEIGHT); + + m_mainFrame = new QFrame(this); + m_circularProgress = new CircularProgressBar(m_mainFrame); + m_circularProgress->setFixedSize(PROGRESS_BAR_SIZE, PROGRESS_BAR_SIZE); + + createLabels(); + createButtons(); + createLayouts(); + applyStyles(); + updateButtonStates(); +} + +void PomodoroTimer::createLabels() +{ + // Time display label + m_timeLabel = new QLabel("25:00", m_circularProgress); + m_timeLabel->setAlignment(Qt::AlignCenter); + m_timeLabel->resize(PROGRESS_BAR_SIZE, 40); + m_timeLabel->move(0, TIME_LABEL_Y_OFFSET); + + // Status indicator label + m_statusLabel = new QLabel("Ready to Focus", m_mainFrame); + m_statusLabel->setAlignment(Qt::AlignCenter); + + // Session progress label + m_sessionLabel = new QLabel("Session 1 of 4", m_mainFrame); + m_sessionLabel->setAlignment(Qt::AlignCenter); +} + +void PomodoroTimer::createButtons() +{ + m_startButton = new QPushButton("Start", m_mainFrame); + m_pauseButton = new QPushButton("Pause", m_mainFrame); + m_resetButton = new QPushButton("Reset", m_mainFrame); + m_settingsButton = new QPushButton("Settings", m_mainFrame); + m_skipButton = new QPushButton("Skip", m_mainFrame); + m_statsButton = new QPushButton("Statistics", m_mainFrame); + + // Set button sizes + const QSize mainButtonSize(MAIN_BUTTON_WIDTH, MAIN_BUTTON_HEIGHT); + const QSize smallButtonSize(SMALL_BUTTON_WIDTH, SMALL_BUTTON_HEIGHT); + + m_startButton->setFixedSize(mainButtonSize); + m_pauseButton->setFixedSize(mainButtonSize); + m_resetButton->setFixedSize(mainButtonSize); + m_settingsButton->setFixedSize(mainButtonSize); + m_skipButton->setFixedSize(smallButtonSize); + m_statsButton->setFixedSize(smallButtonSize); +} + +void PomodoroTimer::createLayouts() +{ + // Progress bar layout + auto progressLayout = new QVBoxLayout(); + progressLayout->addWidget(m_circularProgress, 0, Qt::AlignCenter); + + // Main control buttons layout + auto buttonLayout = new QHBoxLayout(); + buttonLayout->setSpacing(LAYOUT_SPACING); + buttonLayout->addWidget(m_startButton); + buttonLayout->addWidget(m_pauseButton); + buttonLayout->addWidget(m_resetButton); + + // Header layout with settings button + auto headerLayout = new QHBoxLayout(); + headerLayout->addStretch(); + headerLayout->addWidget(m_settingsButton); + + // Extra buttons layout + auto extraButtonLayout = new QHBoxLayout(); + extraButtonLayout->setSpacing(LAYOUT_SPACING); + extraButtonLayout->addStretch(); + extraButtonLayout->addWidget(m_skipButton); + extraButtonLayout->addWidget(m_statsButton); + extraButtonLayout->addStretch(); + + // Main layout assembly + auto mainLayout = new QVBoxLayout(); + mainLayout->addLayout(headerLayout); + mainLayout->addSpacing(5); + mainLayout->addWidget(m_statusLabel); + mainLayout->addSpacing(15); + mainLayout->addLayout(progressLayout); + mainLayout->addSpacing(15); + mainLayout->addWidget(m_sessionLabel); + mainLayout->addSpacing(20); + mainLayout->addLayout(buttonLayout); + mainLayout->addSpacing(15); + mainLayout->addLayout(extraButtonLayout); + mainLayout->setContentsMargins(LAYOUT_MARGIN, LAYOUT_MARGIN, LAYOUT_MARGIN, LAYOUT_MARGIN); + + m_mainFrame->setLayout(mainLayout); + + // Frame layout + auto frameLayout = new QVBoxLayout(); + frameLayout->addWidget(m_mainFrame); + frameLayout->setContentsMargins(0, 0, 0, 0); + setLayout(frameLayout); +} + +void PomodoroTimer::applyStyles() +{ + QFont timeFont = m_timeLabel->font(); + timeFont.setPointSize(TIME_FONT_SIZE); + timeFont.setBold(true); + m_timeLabel->setFont(timeFont); + + QFont statusFont = m_statusLabel->font(); + statusFont.setPointSize(STATUS_FONT_SIZE); + m_statusLabel->setFont(statusFont); + + QFont sessionFont = m_sessionLabel->font(); + sessionFont.setPointSize(SESSION_FONT_SIZE); + m_sessionLabel->setFont(sessionFont); + + QFont buttonFont; + buttonFont.setPointSize(BUTTON_FONT_SIZE); + + const auto buttons = {m_startButton, m_pauseButton, m_resetButton, + m_settingsButton, m_skipButton, m_statsButton}; + for (auto* button : buttons) { + if (button) button->setFont(buttonFont); + } +} + +void PomodoroTimer::setupConnections() +{ + // Timer connections + connect(m_timer, &QTimer::timeout, this, &PomodoroTimer::onUpdateTimer); + + // Button connections + connect(m_startButton, &QPushButton::clicked, this, &PomodoroTimer::onStartTimer); + connect(m_pauseButton, &QPushButton::clicked, this, &PomodoroTimer::onPauseTimer); + connect(m_resetButton, &QPushButton::clicked, this, &PomodoroTimer::onResetTimer); + connect(m_settingsButton, &QPushButton::clicked, this, &PomodoroTimer::onShowSettings); + connect(m_skipButton, &QPushButton::clicked, this, &PomodoroTimer::onSkipSession); + connect(m_statsButton, &QPushButton::clicked, this, &PomodoroTimer::onShowStatistics); + + // System tray connections + connect(m_trayManager.get(), &SystemTrayManager::showMainWindow, this, &PomodoroTimer::onToggleVisibility); + connect(m_trayManager.get(), &SystemTrayManager::startPauseRequested, this, [this]() { + m_isRunning ? onPauseTimer() : onStartTimer(); + }); + connect(m_trayManager.get(), &SystemTrayManager::resetRequested, this, &PomodoroTimer::onResetTimer); + connect(m_trayManager.get(), &SystemTrayManager::skipRequested, this, &PomodoroTimer::onSkipSession); + connect(m_trayManager.get(), &SystemTrayManager::settingsRequested, this, &PomodoroTimer::onShowSettings); + connect(m_trayManager.get(), &SystemTrayManager::statisticsRequested, this, &PomodoroTimer::onShowStatistics); + connect(m_trayManager.get(), &SystemTrayManager::quitRequested, qApp, &QCoreApplication::quit); + + // Keyboard shortcuts connections + connect(m_keyboardShortcuts.get(), &KeyboardShortcuts::startPauseRequested, this, [this]() { + m_isRunning ? onPauseTimer() : onStartTimer(); + }); + connect(m_keyboardShortcuts.get(), &KeyboardShortcuts::pauseRequested, this, &PomodoroTimer::onPauseTimer); + connect(m_keyboardShortcuts.get(), &KeyboardShortcuts::resetRequested, this, &PomodoroTimer::onResetTimer); + connect(m_keyboardShortcuts.get(), &KeyboardShortcuts::settingsRequested, this, &PomodoroTimer::onShowSettings); + connect(m_keyboardShortcuts.get(), &KeyboardShortcuts::skipRequested, this, &PomodoroTimer::onSkipSession); +} + +// Timer control methods +void PomodoroTimer::onStartTimer() +{ + if (m_isRunning) return; + + m_isRunning = true; + m_sessionStartTime = QDateTime::currentDateTime(); + m_timer->start(TIMER_INTERVAL_MS); + + m_statusLabel->setText(TimerStateHelper::getStatusMessage(m_currentState, true)); + updateButtonStates(); + updateDisplay(); +} + +void PomodoroTimer::onPauseTimer() +{ + if (!m_isRunning) return; + + m_timer->stop(); + m_isRunning = false; + m_statusLabel->setText("⏸ Paused"); + updateButtonStates(); + updateDisplay(); +} + +void PomodoroTimer::onResetTimer() +{ + m_timer->stop(); + m_isRunning = false; + resetTimerState(); + updateDisplay(); + updateButtonStates(); +} + +void PomodoroTimer::onUpdateTimer() +{ + --m_currentTime; + updateDisplay(); + + if (m_currentTime <= 0) { + onTimerFinished(); + } +} + +void PomodoroTimer::onTimerFinished() +{ + m_timer->stop(); + m_isRunning = false; + + // Update statistics + const QDateTime now = QDateTime::currentDateTime(); + const int sessionDuration = static_cast(m_sessionStartTime.secsTo(now)); + + if (m_currentState == TimerState::Work) { + m_totalWorkTime += sessionDuration; + m_totalSessions++; + m_completedSessions++; + } else { + m_totalBreakTime += sessionDuration; + } + + // Determine next state and show notifications + QString message, nextAction; + if (m_currentState == TimerState::Work) { + message = "🎉 Focus session completed!"; + + const bool isLongBreakTime = (m_completedSessions % SESSIONS_BEFORE_LONG_BREAK == 0); + updateTimerState(isLongBreakTime ? TimerState::LongBreak : TimerState::ShortBreak); + nextAction = isLongBreakTime ? "Time for a long break! 🧘‍♀️" : "Take a short break! ☕"; + } else { + message = "Break time finished!"; + nextAction = "Ready to focus again? 🎯"; + updateTimerState(TimerState::Work); + } + + m_notificationManager->showNotification(message + " " + nextAction); + onResetTimer(); + + // Auto-start if enabled + const bool shouldAutoStart = (m_currentState != TimerState::Work && m_autoStartBreaks) || + (m_currentState == TimerState::Work && m_autoStartWork); + if (shouldAutoStart) { + QTimer::singleShot(AUTO_START_DELAY_MS, this, &PomodoroTimer::onStartTimer); + } + + saveSettings(); +} + +// Helper methods +void PomodoroTimer::resetTimerState() +{ + m_currentTime = TimerStateHelper::getDurationForState(m_currentState, m_workDuration, m_shortBreakDuration, m_longBreakDuration); + m_totalDuration = m_currentTime; + m_statusLabel->setText(TimerStateHelper::getStatusMessage(m_currentState, false)); +} + +void PomodoroTimer::updateTimerState(TimerState newState) +{ + m_currentState = newState; +} + +QString PomodoroTimer::formatTime(int seconds) const +{ + const int minutes = seconds / 60; + const int secs = seconds % 60; + return QString("%1:%2").arg(minutes, 2, 10, QChar('0')) + .arg(secs, 2, 10, QChar('0')); +} + +void PomodoroTimer::updateDisplay() +{ + m_timeLabel->setText(formatTime(m_currentTime)); + + const int progress = m_totalDuration > 0 ? + ((m_totalDuration - m_currentTime) * 100) / m_totalDuration : 0; + m_circularProgress->setValue(progress); + + updateSessionCounter(); + updateWindowTitle(); + + // Update system tray + const int currentSession = (m_completedSessions % SESSIONS_BEFORE_LONG_BREAK) + 1; + m_trayManager->updateTooltip(m_currentState, m_isRunning, formatTime(m_currentTime), + currentSession, SESSIONS_BEFORE_LONG_BREAK); +} + +void PomodoroTimer::updateSessionCounter() +{ + const int currentSession = (m_completedSessions % SESSIONS_BEFORE_LONG_BREAK) + 1; + m_sessionLabel->setText(QString("Session %1 of %2") + .arg(currentSession) + .arg(SESSIONS_BEFORE_LONG_BREAK)); +} + +void PomodoroTimer::updateButtonStates() +{ + m_startButton->setEnabled(!m_isRunning); + m_pauseButton->setEnabled(m_isRunning); + + const bool shouldShowReset = m_isRunning || m_currentTime < m_totalDuration; + m_resetButton->setVisible(shouldShowReset); + m_resetButton->setEnabled(true); + + m_skipButton->setEnabled(m_isRunning); + m_skipButton->setVisible(m_isRunning); + + // Force layout update + if (m_mainFrame && m_mainFrame->layout()) { + m_mainFrame->layout()->update(); + m_mainFrame->updateGeometry(); + } +} + +void PomodoroTimer::updateWindowTitle() +{ + const QString timeRemaining = formatTime(m_currentTime); + const QString emoji = TimerStateHelper::getStateEmoji(m_currentState); + + QString title; + if (m_isRunning) { + const QString stateText = (m_currentState == TimerState::Work) ? "Focus" : "Break"; + title = QString("%1 %2: %3").arg(emoji, stateText, timeRemaining); + } else { + title = "🍅 Pomodoro Timer"; + } + + setWindowTitle(title); +} + +void PomodoroTimer::onShowSettings() +{ + SettingsDialog dialog(this); + + dialog.setWorkDuration(m_workDuration / 60); + dialog.setShortBreakDuration(m_shortBreakDuration / 60); + dialog.setLongBreakDuration(m_longBreakDuration / 60); + dialog.setAutoStartBreaks(m_autoStartBreaks); + dialog.setAutoStartWork(m_autoStartWork); + dialog.setMinimizeToTray(m_minimizeToTray); + dialog.setShowNotifications(m_showNotifications); + + if (dialog.exec() == QDialog::Accepted) { + m_workDuration = dialog.workDuration() * 60; + m_shortBreakDuration = dialog.shortBreakDuration() * 60; + m_longBreakDuration = dialog.longBreakDuration() * 60; + m_autoStartBreaks = dialog.autoStartBreaks(); + m_autoStartWork = dialog.autoStartWork(); + m_minimizeToTray = dialog.minimizeToTray(); + m_showNotifications = dialog.showNotifications(); + + saveSettings(); + onResetTimer(); + } +} + +void PomodoroTimer::onSkipSession() +{ + if (m_isRunning) { + onTimerFinished(); + } +} + +void PomodoroTimer::onShowStatistics() +{ + StatisticsDialog dialog(this); + dialog.setStatistics(m_totalSessions, m_totalWorkTime, m_totalBreakTime); + dialog.exec(); +} + +void PomodoroTimer::onToggleVisibility() +{ + if (isVisible()) { + hide(); + } else { + show(); + raise(); + activateWindow(); + } +} + +void PomodoroTimer::loadSettings() +{ + QSettings settings; + m_workDuration = settings.value("workDuration", DEFAULT_WORK_DURATION).toInt(); + m_shortBreakDuration = settings.value("shortBreakDuration", DEFAULT_SHORT_BREAK).toInt(); + m_longBreakDuration = settings.value("longBreakDuration", DEFAULT_LONG_BREAK).toInt(); + m_autoStartBreaks = settings.value("autoStartBreaks", false).toBool(); + m_autoStartWork = settings.value("autoStartWork", false).toBool(); + m_minimizeToTray = settings.value("minimizeToTray", true).toBool(); + m_showNotifications = settings.value("showNotifications", true).toBool(); + m_totalSessions = settings.value("totalSessions", 0).toInt(); + m_totalWorkTime = settings.value("totalWorkTime", 0).toInt(); + m_totalBreakTime = settings.value("totalBreakTime", 0).toInt(); +} + +void PomodoroTimer::saveSettings() +{ + QSettings settings; + settings.setValue("workDuration", m_workDuration); + settings.setValue("shortBreakDuration", m_shortBreakDuration); + settings.setValue("longBreakDuration", m_longBreakDuration); + settings.setValue("autoStartBreaks", m_autoStartBreaks); + settings.setValue("autoStartWork", m_autoStartWork); + settings.setValue("minimizeToTray", m_minimizeToTray); + settings.setValue("showNotifications", m_showNotifications); + settings.setValue("totalSessions", m_totalSessions); + settings.setValue("totalWorkTime", m_totalWorkTime); + settings.setValue("totalBreakTime", m_totalBreakTime); +} + +void PomodoroTimer::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Escape) { + hide(); + } + QWidget::keyPressEvent(event); +} + +void PomodoroTimer::closeEvent(QCloseEvent *event) +{ + if (m_trayManager && m_trayManager->isVisible()) { + hide(); + event->ignore(); + + static bool firstHide = true; + if (firstHide) { + m_notificationManager->showNotification("Application was minimized to tray. Click the tray icon to show again."); + firstHide = false; + } + } else { + saveSettings(); + event->accept(); + } +} diff --git a/PomodoroTimer.h b/PomodoroTimer.h new file mode 100644 index 0000000..88e01a9 --- /dev/null +++ b/PomodoroTimer.h @@ -0,0 +1,136 @@ +#ifndef POMODOROTIMER_H +#define POMODOROTIMER_H + +#include +#include +#include +#include +#include +#include +#include +#include "TimerState.h" + +// Forward declarations +class CircularProgressBar; +class SettingsDialog; +class StatisticsDialog; +class SystemTrayManager; +class KeyboardShortcuts; +class NotificationManager; + +class PomodoroTimer : public QWidget +{ + Q_OBJECT + +public: + explicit PomodoroTimer(QWidget *parent = nullptr); + ~PomodoroTimer() override; + + // Timer configuration constants + static constexpr int DEFAULT_WORK_DURATION = 1500; // 25 minutes in seconds + static constexpr int DEFAULT_SHORT_BREAK = 300; // 5 minutes in seconds + static constexpr int DEFAULT_LONG_BREAK = 900; // 15 minutes in seconds + static constexpr int SESSIONS_BEFORE_LONG_BREAK = 4; + static constexpr int TIMER_INTERVAL_MS = 1000; + static constexpr int AUTO_START_DELAY_MS = 3000; + +protected: + void keyPressEvent(QKeyEvent *event) override; + void closeEvent(QCloseEvent *event) override; + +private slots: + // Timer control slots + void onStartTimer(); + void onPauseTimer(); + void onResetTimer(); + void onUpdateTimer(); + void onTimerFinished(); + + // UI interaction slots + void onShowSettings(); + void onSkipSession(); + void onShowStatistics(); + void onToggleVisibility(); + +private: + // Setup methods + void setupUI(); + void setupConnections(); + + // UI creation helpers + void createLabels(); + void createButtons(); + void createLayouts(); + void applyStyles(); + + // Settings management + void loadSettings(); + void saveSettings(); + + // Timer state management + void resetTimerState(); + void updateTimerState(TimerState newState); + + // Display update methods - optimized to avoid unnecessary updates + void updateDisplay(); + void updateSessionCounter(); + void updateButtonStates(); + void updateWindowTitle(); + void updateTrayTooltip(); + + // Utility methods + QString formatTime(int seconds) const; + bool needsDisplayUpdate() const; + + // UI elements - use raw pointers for Qt objects with parent ownership + QTimer *m_timer; + QLabel *m_timeLabel{nullptr}; + QLabel *m_statusLabel{nullptr}; + QLabel *m_sessionLabel{nullptr}; + QPushButton *m_startButton{nullptr}; + QPushButton *m_pauseButton{nullptr}; + QPushButton *m_resetButton{nullptr}; + QPushButton *m_settingsButton{nullptr}; + QPushButton *m_skipButton{nullptr}; + QPushButton *m_statsButton{nullptr}; + CircularProgressBar *m_circularProgress{nullptr}; + QFrame *m_mainFrame{nullptr}; + + // Managers - keep as unique_ptr for complex objects + std::unique_ptr m_trayManager; + std::unique_ptr m_keyboardShortcuts; + std::unique_ptr m_notificationManager; + + // Timer state + int m_currentTime{0}; + int m_totalDuration{0}; + TimerState m_currentState{TimerState::Work}; + int m_completedSessions{0}; + bool m_isRunning{false}; + + // Cache for reducing string operations + mutable QString m_cachedTimeString; + mutable int m_lastFormattedTime{-1}; + + // Settings + int m_workDuration{DEFAULT_WORK_DURATION}; + int m_shortBreakDuration{DEFAULT_SHORT_BREAK}; + int m_longBreakDuration{DEFAULT_LONG_BREAK}; + bool m_autoStartBreaks{false}; + bool m_autoStartWork{false}; + bool m_minimizeToTray{true}; + bool m_showNotifications{true}; + + // Statistics + int m_totalSessions{0}; + int m_totalWorkTime{0}; + int m_totalBreakTime{0}; + QDateTime m_sessionStartTime; + + // Update tracking to prevent unnecessary redraws + bool m_needsButtonUpdate{true}; + bool m_needsSessionUpdate{true}; + QString m_lastWindowTitle; +}; + +#endif // POMODOROTIMER_H diff --git a/README.md b/README.md new file mode 100644 index 0000000..d231528 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Pomodoro Timer + +A simple Pomodoro Timer application built with C++ and the Qt framework. + +## Features + +* Work, Short Break, and Long Break timers. +* Customizable timer durations. +* System tray integration with status updates. +* Desktop notifications for state changes. +* Session tracking. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Building + +### Prerequisites + +* A C++17 compatible compiler +* Qt 6 +* CMake + +### Steps + +1. Clone the repository: + ```bash + git clone + ``` +2. Create a build directory: + ```bash + cd PomodoroTimer + mkdir build && cd build + ``` +3. Run CMake and build the project: + ```bash + cmake .. + make + ``` +4. Run the application: + ```bash + ./PomodoroTimer + ``` diff --git a/SettingsDialog.cpp b/SettingsDialog.cpp new file mode 100644 index 0000000..868bcdb --- /dev/null +++ b/SettingsDialog.cpp @@ -0,0 +1,102 @@ +#include "SettingsDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include + +SettingsDialog::SettingsDialog(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle("Pomodoro Settings"); + setFixedSize(350, 300); // Reduced from 400 since we removed sound settings + // Remove all custom styling + setStyleSheet(""); + setupUI(); +} + +void SettingsDialog::setupUI() +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + // Timer Settings Group + QGroupBox *timerGroup = new QGroupBox("Timer Settings"); + QFormLayout *timerLayout = new QFormLayout(timerGroup); + + m_workDurationSpin = new QSpinBox(); + m_workDurationSpin->setRange(1, 60); + m_workDurationSpin->setSuffix(" minutes"); + timerLayout->addRow("Work Duration:", m_workDurationSpin); + + m_shortBreakSpin = new QSpinBox(); + m_shortBreakSpin->setRange(1, 30); + m_shortBreakSpin->setSuffix(" minutes"); + timerLayout->addRow("Short Break:", m_shortBreakSpin); + + m_longBreakSpin = new QSpinBox(); + m_longBreakSpin->setRange(5, 60); + m_longBreakSpin->setSuffix(" minutes"); + timerLayout->addRow("Long Break:", m_longBreakSpin); + + // Behavior Settings Group + QGroupBox *behaviorGroup = new QGroupBox("Behavior"); + QVBoxLayout *behaviorLayout = new QVBoxLayout(behaviorGroup); + + m_autoStartBreaksCheck = new QCheckBox("Auto-start breaks"); + m_autoStartWorkCheck = new QCheckBox("Auto-start work sessions"); + m_minimizeToTrayCheck = new QCheckBox("Minimize to system tray"); + m_showNotificationsCheck = new QCheckBox("Show notifications"); + + behaviorLayout->addWidget(m_autoStartBreaksCheck); + behaviorLayout->addWidget(m_autoStartWorkCheck); + behaviorLayout->addWidget(m_minimizeToTrayCheck); + behaviorLayout->addWidget(m_showNotificationsCheck); + + // Dialog Buttons + m_buttonBox = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::RestoreDefaults); + + // Layout + mainLayout->addWidget(timerGroup); + mainLayout->addWidget(behaviorGroup); + mainLayout->addStretch(); + mainLayout->addWidget(m_buttonBox); + + // Connections + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(m_buttonBox->button(QDialogButtonBox::RestoreDefaults), + &QPushButton::clicked, this, &SettingsDialog::resetToDefaults); +} + +// Getters +int SettingsDialog::workDuration() const { return m_workDurationSpin->value(); } +int SettingsDialog::shortBreakDuration() const { return m_shortBreakSpin->value(); } +int SettingsDialog::longBreakDuration() const { return m_longBreakSpin->value(); } +bool SettingsDialog::autoStartBreaks() const { return m_autoStartBreaksCheck->isChecked(); } +bool SettingsDialog::autoStartWork() const { return m_autoStartWorkCheck->isChecked(); } +bool SettingsDialog::minimizeToTray() const { return m_minimizeToTrayCheck->isChecked(); } +bool SettingsDialog::showNotifications() const { return m_showNotificationsCheck->isChecked(); } + +// Setters +void SettingsDialog::setWorkDuration(int minutes) { m_workDurationSpin->setValue(minutes); } +void SettingsDialog::setShortBreakDuration(int minutes) { m_shortBreakSpin->setValue(minutes); } +void SettingsDialog::setLongBreakDuration(int minutes) { m_longBreakSpin->setValue(minutes); } +void SettingsDialog::setAutoStartBreaks(bool enabled) { m_autoStartBreaksCheck->setChecked(enabled); } +void SettingsDialog::setAutoStartWork(bool enabled) { m_autoStartWorkCheck->setChecked(enabled); } +void SettingsDialog::setMinimizeToTray(bool enabled) { m_minimizeToTrayCheck->setChecked(enabled); } +void SettingsDialog::setShowNotifications(bool enabled) { m_showNotificationsCheck->setChecked(enabled); } + +void SettingsDialog::resetToDefaults() +{ + setWorkDuration(25); + setShortBreakDuration(5); + setLongBreakDuration(15); + setAutoStartBreaks(false); + setAutoStartWork(false); + setMinimizeToTray(true); + setShowNotifications(true); +} diff --git a/SettingsDialog.h b/SettingsDialog.h new file mode 100644 index 0000000..e99b93e --- /dev/null +++ b/SettingsDialog.h @@ -0,0 +1,57 @@ +#ifndef SETTINGSDIALOG_H +#define SETTINGSDIALOG_H + +#include + +// Forward declarations +class QSpinBox; +class QCheckBox; +class QPushButton; +class QDialogButtonBox; + +class SettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SettingsDialog(QWidget *parent = nullptr); + + // Getters + int workDuration() const; + int shortBreakDuration() const; + int longBreakDuration() const; + bool autoStartBreaks() const; + bool autoStartWork() const; + bool minimizeToTray() const; + bool showNotifications() const; + + // Setters + void setWorkDuration(int minutes); + void setShortBreakDuration(int minutes); + void setLongBreakDuration(int minutes); + void setAutoStartBreaks(bool enabled); + void setAutoStartWork(bool enabled); + void setMinimizeToTray(bool enabled); + void setShowNotifications(bool enabled); + +signals: + void settingsChanged(); + +private slots: + void resetToDefaults(); + +private: + void setupUI(); + + // UI elements + QSpinBox *m_workDurationSpin; + QSpinBox *m_shortBreakSpin; + QSpinBox *m_longBreakSpin; + QCheckBox *m_autoStartBreaksCheck; + QCheckBox *m_autoStartWorkCheck; + QCheckBox *m_minimizeToTrayCheck; + QCheckBox *m_showNotificationsCheck; + QDialogButtonBox *m_buttonBox; +}; + +#endif // SETTINGSDIALOG_H diff --git a/StatisticsDialog.cpp b/StatisticsDialog.cpp new file mode 100644 index 0000000..9ed37f5 --- /dev/null +++ b/StatisticsDialog.cpp @@ -0,0 +1,402 @@ +#include "StatisticsDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// StatisticsChart implementation +StatisticsChart::StatisticsChart(QWidget *parent) + : QWidget(parent), m_maxValue(0) +{ + setMinimumSize(400, 200); + setStyleSheet("background-color: white; border: 1px solid #ccc;"); +} + +void StatisticsChart::setData(const QMap &workTimeData) +{ + m_workTimeData = workTimeData; + m_maxValue = 0; + + for (auto it = workTimeData.begin(); it != workTimeData.end(); ++it) { + if (it.value() > m_maxValue) { + m_maxValue = it.value(); + } + } + + update(); +} + +void StatisticsChart::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + if (m_workTimeData.isEmpty()) { + painter.drawText(rect(), Qt::AlignCenter, "No data available"); + return; + } + + const int margin = 40; + const int chartWidth = width() - 2 * margin; + const int chartHeight = height() - 2 * margin; + + // Draw axes + painter.setPen(QPen(Qt::black, 2)); + painter.drawLine(margin, height() - margin, width() - margin, height() - margin); // X-axis + painter.drawLine(margin, margin, margin, height() - margin); // Y-axis + + if (m_maxValue == 0) return; + + // Draw bars + const int barWidth = chartWidth / qMax(1, m_workTimeData.size()); + int x = margin; + + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(70, 130, 180)); + + auto sortedData = m_workTimeData; + + for (auto it = sortedData.begin(); it != sortedData.end(); ++it) { + const int barHeight = (it.value() * chartHeight) / m_maxValue; + const int barY = height() - margin - barHeight; + + QRect barRect(x + 2, barY, barWidth - 4, barHeight); + painter.drawRect(barRect); + + // Draw date labels (every 3rd day to avoid crowding) + if ((x - margin) % (barWidth * 3) == 0) { + painter.setPen(Qt::black); + QString dateStr = it.key().toString("dd/MM"); + painter.drawText(x, height() - 10, dateStr); + painter.setPen(Qt::NoPen); + } + + x += barWidth; + } + + // Draw Y-axis labels + painter.setPen(Qt::black); + for (int i = 0; i <= 5; ++i) { + int y = height() - margin - (i * chartHeight / 5); + int minutes = (i * m_maxValue) / 5; + painter.drawText(5, y + 5, QString::number(minutes) + "m"); + } +} + +// StatisticsDialog implementation +StatisticsDialog::StatisticsDialog(QWidget *parent) + : QDialog(parent) + , m_totalSessions(0) + , m_totalWorkTime(0) + , m_totalBreakTime(0) + , m_chartPeriod("week") +{ + setWindowTitle("📊 Pomodoro Statistics"); + setMinimumSize(600, 500); + setupUI(); + loadDailyStatistics(); +} + +void StatisticsDialog::setupUI() +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + // Create tab widget + m_tabWidget = new QTabWidget(); + + setupOverviewTab(); + setupChartTab(); + setupDetailsTab(); + + m_tabWidget->addTab(m_overviewTab, "📈 Overview"); + m_tabWidget->addTab(m_chartTab, "📊 Chart"); + m_tabWidget->addTab(m_detailsTab, "📋 Details"); + + // No additional buttons - only native window close button + mainLayout->addWidget(m_tabWidget); + + // Connect chart period buttons + connect(m_weekButton, &QPushButton::clicked, this, [this]() { + m_chartPeriod = "week"; + updateChart(); + }); + connect(m_monthButton, &QPushButton::clicked, this, [this]() { + m_chartPeriod = "month"; + updateChart(); + }); + connect(m_yearButton, &QPushButton::clicked, this, [this]() { + m_chartPeriod = "year"; + updateChart(); + }); +} + +void StatisticsDialog::setupOverviewTab() +{ + m_overviewTab = new QWidget(); + QVBoxLayout *layout = new QVBoxLayout(m_overviewTab); + + // Title + QLabel *titleLabel = new QLabel("📊 Statistics Overview"); + QFont titleFont = titleLabel->font(); + titleFont.setPointSize(16); + titleFont.setBold(true); + titleLabel->setFont(titleFont); + titleLabel->setAlignment(Qt::AlignCenter); + + // Main statistics grid + QGridLayout *statsGrid = new QGridLayout(); + + // Create labels + m_totalSessionsLabel = new QLabel(); + m_totalTimeLabel = new QLabel(); + m_workTimeLabel = new QLabel(); + m_breakTimeLabel = new QLabel(); + m_avgSessionLabel = new QLabel(); + m_efficiencyLabel = new QLabel(); + + // Period statistics + m_todayLabel = new QLabel(); + m_weekLabel = new QLabel(); + m_monthLabel = new QLabel(); + + // Add to grid + statsGrid->addWidget(new QLabel("🍅 Total Sessions:"), 0, 0); + statsGrid->addWidget(m_totalSessionsLabel, 0, 1); + + statsGrid->addWidget(new QLabel("⏱️ Total Time:"), 1, 0); + statsGrid->addWidget(m_totalTimeLabel, 1, 1); + + statsGrid->addWidget(new QLabel("🎯 Work Time:"), 2, 0); + statsGrid->addWidget(m_workTimeLabel, 2, 1); + + statsGrid->addWidget(new QLabel("☕ Break Time:"), 3, 0); + statsGrid->addWidget(m_breakTimeLabel, 3, 1); + + statsGrid->addWidget(new QLabel("📊 Avg Session:"), 4, 0); + statsGrid->addWidget(m_avgSessionLabel, 4, 1); + + statsGrid->addWidget(new QLabel("⚡ Efficiency:"), 5, 0); + statsGrid->addWidget(m_efficiencyLabel, 5, 1); + + // Period statistics section + QLabel *periodTitle = new QLabel("📅 Period Statistics"); + QFont periodFont = periodTitle->font(); + periodFont.setPointSize(14); + periodFont.setBold(true); + periodTitle->setFont(periodFont); + + QGridLayout *periodGrid = new QGridLayout(); + periodGrid->addWidget(new QLabel("📅 Today:"), 0, 0); + periodGrid->addWidget(m_todayLabel, 0, 1); + + periodGrid->addWidget(new QLabel("📅 This Week:"), 1, 0); + periodGrid->addWidget(m_weekLabel, 1, 1); + + periodGrid->addWidget(new QLabel("📅 This Month:"), 2, 0); + periodGrid->addWidget(m_monthLabel, 2, 1); + + layout->addWidget(titleLabel); + layout->addLayout(statsGrid); + layout->addSpacing(20); + layout->addWidget(periodTitle); + layout->addLayout(periodGrid); + layout->addStretch(); +} + +void StatisticsDialog::setupChartTab() +{ + m_chartTab = new QWidget(); + QVBoxLayout *layout = new QVBoxLayout(m_chartTab); + + // Chart period buttons + QHBoxLayout *periodLayout = new QHBoxLayout(); + m_chartPeriodLabel = new QLabel("📊 Work Time Chart"); + QFont chartFont = m_chartPeriodLabel->font(); + chartFont.setPointSize(14); + chartFont.setBold(true); + m_chartPeriodLabel->setFont(chartFont); + + m_weekButton = new QPushButton("Last 7 Days"); + m_monthButton = new QPushButton("Last 30 Days"); + m_yearButton = new QPushButton("Last Year"); + + m_weekButton->setCheckable(true); + m_monthButton->setCheckable(true); + m_yearButton->setCheckable(true); + m_weekButton->setChecked(true); + + periodLayout->addWidget(m_chartPeriodLabel); + periodLayout->addStretch(); + periodLayout->addWidget(m_weekButton); + periodLayout->addWidget(m_monthButton); + periodLayout->addWidget(m_yearButton); + + // Chart widget + m_chart = new StatisticsChart(); + + layout->addLayout(periodLayout); + layout->addWidget(m_chart); +} + +void StatisticsDialog::setupDetailsTab() +{ + m_detailsTab = new QWidget(); + QVBoxLayout *layout = new QVBoxLayout(m_detailsTab); + + QLabel *detailsTitle = new QLabel("📋 Detailed Statistics"); + QFont detailsFont = detailsTitle->font(); + detailsFont.setPointSize(14); + detailsFont.setBold(true); + detailsTitle->setFont(detailsFont); + + m_detailsText = new QTextEdit(); + m_detailsText->setReadOnly(true); + m_detailsText->setFont(QFont("Courier", 10)); + + layout->addWidget(detailsTitle); + layout->addWidget(m_detailsText); +} + +void StatisticsDialog::setStatistics(int totalSessions, int totalWorkTime, int totalBreakTime) +{ + m_totalSessions = totalSessions; + m_totalWorkTime = totalWorkTime; + m_totalBreakTime = totalBreakTime; + + updateOverview(); + updateChart(); + + // Update details tab + QString details = QString( + "DETAILED POMODORO STATISTICS\n" + "=============================\n\n" + "Overall Performance:\n" + "- Total Sessions Completed: %1\n" + "- Total Time Spent: %2 hours %3 minutes\n" + "- Work Time: %4 hours %5 minutes\n" + "- Break Time: %6 hours %7 minutes\n\n" + "Productivity Metrics:\n" + "- Average Session Length: %8 minutes\n" + "- Work/Break Ratio: %9\n" + "- Sessions per Day (avg): %10\n\n" + "Recent Activity:\n" + "- Today: %11 minutes\n" + "- Yesterday: %12 minutes\n" + "- This Week: %13 minutes\n" + "- Last Week: %14 minutes\n" + ).arg(m_totalSessions) + .arg(m_totalWorkTime / 3600).arg((m_totalWorkTime % 3600) / 60) + .arg(m_totalWorkTime / 3600).arg((m_totalWorkTime % 3600) / 60) + .arg(m_totalBreakTime / 3600).arg((m_totalBreakTime % 3600) / 60) + .arg(m_totalSessions > 0 ? m_totalWorkTime / (m_totalSessions * 60) : 0) + .arg(m_totalBreakTime > 0 ? QString::number(double(m_totalWorkTime) / m_totalBreakTime, 'f', 2) : "N/A") + .arg(m_totalSessions > 0 ? QString::number(double(m_totalSessions) / 7, 'f', 1) : "0") + .arg(m_dailyWorkTime.value(QDate::currentDate(), 0)) + .arg(m_dailyWorkTime.value(QDate::currentDate().addDays(-1), 0)) + .arg(m_dailyWorkTime.value(QDate::currentDate(), 0)) // This week calculation can be improved + .arg(0); // Last week calculation can be improved + + m_detailsText->setPlainText(details); +} + +void StatisticsDialog::loadDailyStatistics() +{ + QSettings settings; + + // Load daily work time data for the last 30 days + QDate today = QDate::currentDate(); + for (int i = 0; i < 30; ++i) { + QDate date = today.addDays(-i); + QString key = QString("dailyStats/workTime_%1").arg(date.toString("yyyy-MM-dd")); + int workTime = settings.value(key, 0).toInt(); + if (workTime > 0) { + m_dailyWorkTime[date] = workTime / 60; // Convert to minutes + } + + QString sessionKey = QString("dailyStats/sessions_%1").arg(date.toString("yyyy-MM-dd")); + int sessions = settings.value(sessionKey, 0).toInt(); + if (sessions > 0) { + m_dailySessions[date] = sessions; + } + } +} + +void StatisticsDialog::updateOverview() +{ + m_totalSessionsLabel->setText(QString::number(m_totalSessions)); + + int totalMinutes = (m_totalWorkTime + m_totalBreakTime) / 60; + m_totalTimeLabel->setText(QString("%1h %2m") + .arg(totalMinutes / 60) + .arg(totalMinutes % 60)); + + int workMinutes = m_totalWorkTime / 60; + m_workTimeLabel->setText(QString("%1h %2m") + .arg(workMinutes / 60) + .arg(workMinutes % 60)); + + int breakMinutes = m_totalBreakTime / 60; + m_breakTimeLabel->setText(QString("%1h %2m") + .arg(breakMinutes / 60) + .arg(breakMinutes % 60)); + + int avgSession = m_totalSessions > 0 ? workMinutes / m_totalSessions : 0; + m_avgSessionLabel->setText(QString("%1 minutes").arg(avgSession)); + + int efficiency = totalMinutes > 0 ? (workMinutes * 100 / totalMinutes) : 0; + m_efficiencyLabel->setText(QString("%1%").arg(efficiency)); + + // Period statistics + QDate today = QDate::currentDate(); + int todayMinutes = m_dailyWorkTime.value(today, 0); + m_todayLabel->setText(QString("%1 minutes").arg(todayMinutes)); + + // Calculate week total + int weekTotal = 0; + for (int i = 0; i < 7; ++i) { + weekTotal += m_dailyWorkTime.value(today.addDays(-i), 0); + } + m_weekLabel->setText(QString("%1 minutes").arg(weekTotal)); + + // Calculate month total + int monthTotal = 0; + for (int i = 0; i < 30; ++i) { + monthTotal += m_dailyWorkTime.value(today.addDays(-i), 0); + } + m_monthLabel->setText(QString("%1 minutes").arg(monthTotal)); +} + +void StatisticsDialog::updateChart() +{ + QMap chartData; + QDate today = QDate::currentDate(); + + int days = 7; + if (m_chartPeriod == "month") days = 30; + else if (m_chartPeriod == "year") days = 365; + + for (int i = days - 1; i >= 0; --i) { + QDate date = today.addDays(-i); + chartData[date] = m_dailyWorkTime.value(date, 0); + } + + m_chart->setData(chartData); + + // Update button states + m_weekButton->setChecked(m_chartPeriod == "week"); + m_monthButton->setChecked(m_chartPeriod == "month"); + m_yearButton->setChecked(m_chartPeriod == "year"); +} diff --git a/StatisticsDialog.h b/StatisticsDialog.h new file mode 100644 index 0000000..95e2e00 --- /dev/null +++ b/StatisticsDialog.h @@ -0,0 +1,85 @@ +#ifndef STATISTICSDIALOG_H +#define STATISTICSDIALOG_H + +#include +#include +#include + +class QLabel; +class QVBoxLayout; +class QHBoxLayout; +class QPushButton; +class QTabWidget; +class QTextEdit; +class QScrollArea; + +class StatisticsChart : public QWidget +{ + Q_OBJECT + +public: + explicit StatisticsChart(QWidget *parent = nullptr); + void setData(const QMap &workTimeData); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QMap m_workTimeData; + int m_maxValue; +}; + +class StatisticsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit StatisticsDialog(QWidget *parent = nullptr); + void setStatistics(int totalSessions, int totalWorkTime, int totalBreakTime); + +private: + void setupUI(); + void setupOverviewTab(); + void setupChartTab(); + void setupDetailsTab(); + void loadDailyStatistics(); + void updateOverview(); + void updateChart(); + + // UI elements + QTabWidget *m_tabWidget; + + // Overview tab + QWidget *m_overviewTab; + QLabel *m_totalSessionsLabel; + QLabel *m_totalTimeLabel; + QLabel *m_workTimeLabel; + QLabel *m_breakTimeLabel; + QLabel *m_avgSessionLabel; + QLabel *m_efficiencyLabel; + QLabel *m_todayLabel; + QLabel *m_weekLabel; + QLabel *m_monthLabel; + + // Chart tab + QWidget *m_chartTab; + StatisticsChart *m_chart; + QLabel *m_chartPeriodLabel; + QPushButton *m_weekButton; + QPushButton *m_monthButton; + QPushButton *m_yearButton; + + // Details tab + QWidget *m_detailsTab; + QTextEdit *m_detailsText; + + // Data + int m_totalSessions; + int m_totalWorkTime; + int m_totalBreakTime; + QMap m_dailyWorkTime; + QMap m_dailySessions; + QString m_chartPeriod; +}; + +#endif // STATISTICSDIALOG_H diff --git a/SystemTrayManager.cpp b/SystemTrayManager.cpp new file mode 100644 index 0000000..d650126 --- /dev/null +++ b/SystemTrayManager.cpp @@ -0,0 +1,107 @@ +#include "SystemTrayManager.h" +#include +#include + +SystemTrayManager::SystemTrayManager(QObject *parent) + : QObject(parent) + , m_trayIcon(nullptr) + , m_trayMenu(nullptr) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) { + qWarning("System tray is not available on this system."); + return; + } + + m_trayIcon = new QSystemTrayIcon(this); + m_trayIcon->setIcon(qApp->windowIcon()); + + setupTrayMenu(); + + connect(m_trayIcon, QOverload::of(&QSystemTrayIcon::activated), + this, &SystemTrayManager::onTrayIconActivated); +} + +void SystemTrayManager::setupTrayMenu() +{ + m_trayMenu = new QMenu(); + + // Use QStringLiteral for string literals to avoid runtime allocations + m_trayMenu->addAction(QStringLiteral("Show Timer"), this, &SystemTrayManager::showMainWindow); + m_trayMenu->addSeparator(); + m_trayMenu->addAction(QStringLiteral("Start/Pause"), this, &SystemTrayManager::startPauseRequested); + m_trayMenu->addAction(QStringLiteral("Reset"), this, &SystemTrayManager::resetRequested); + m_trayMenu->addAction(QStringLiteral("Skip Session"), this, &SystemTrayManager::skipRequested); + m_trayMenu->addSeparator(); + m_trayMenu->addAction(QStringLiteral("Settings"), this, &SystemTrayManager::settingsRequested); + m_trayMenu->addAction(QStringLiteral("Statistics"), this, &SystemTrayManager::statisticsRequested); + m_trayMenu->addSeparator(); + m_trayMenu->addAction(QStringLiteral("Quit"), this, &SystemTrayManager::quitRequested); + + m_trayIcon->setContextMenu(m_trayMenu); +} + +void SystemTrayManager::show() +{ + if (m_trayIcon) { + m_trayIcon->show(); + } +} + +void SystemTrayManager::hide() +{ + if (m_trayIcon) { + m_trayIcon->hide(); + } +} + +bool SystemTrayManager::isVisible() const +{ + return m_trayIcon && m_trayIcon->isVisible(); +} + +void SystemTrayManager::updateTooltip(TimerState state, bool isRunning, const QString &timeRemaining, int currentSession, int totalSessions) +{ + if (!m_trayIcon) return; + + // Build tooltip more efficiently + QString tooltip; + tooltip.reserve(100); // Reserve space to avoid reallocations + + QString emoji = TimerStateHelper::getStateEmoji(state); + QString stateText = TimerStateHelper::getStateText(state); + + if (isRunning) { + tooltip = QStringLiteral("%1 %2 - %3 remaining").arg( + emoji, + stateText, + timeRemaining); + } else { + stateText += " (Paused)"; + tooltip = QStringLiteral("%1 %2 Ready (%3)").arg( + emoji, + stateText, + timeRemaining); + } + + tooltip += QStringLiteral("\nSession %1 of %2").arg(currentSession).arg(totalSessions); + + // Only update if tooltip changed + if (tooltip != m_lastTooltip) { + m_trayIcon->setToolTip(tooltip); + m_lastTooltip = std::move(tooltip); + } +} + +void SystemTrayManager::showMessage(const QString &title, const QString &message) +{ + if (m_trayIcon && m_trayIcon->isVisible()) { + m_trayIcon->showMessage(title, message, QSystemTrayIcon::Information, 5000); + } +} + +void SystemTrayManager::onTrayIconActivated(QSystemTrayIcon::ActivationReason reason) +{ + if (reason == QSystemTrayIcon::Trigger) { + emit showMainWindow(); + } +} diff --git a/SystemTrayManager.h b/SystemTrayManager.h new file mode 100644 index 0000000..7eddab1 --- /dev/null +++ b/SystemTrayManager.h @@ -0,0 +1,45 @@ +#ifndef SYSTEMTRAYMANAGER_H +#define SYSTEMTRAYMANAGER_H + +#include +#include +#include +#include "TimerState.h" + +class SystemTrayManager : public QObject +{ + Q_OBJECT + +public: + explicit SystemTrayManager(QObject *parent = nullptr); + ~SystemTrayManager() = default; + + void show(); + void hide(); + bool isVisible() const; + void updateTooltip(TimerState state, bool isRunning, const QString &timeRemaining, int currentSession, int totalSessions); + void showMessage(const QString &title, const QString &message); + +signals: + void showMainWindow(); + void startPauseRequested(); + void resetRequested(); + void skipRequested(); + void settingsRequested(); + void statisticsRequested(); + void quitRequested(); + +private slots: + void onTrayIconActivated(QSystemTrayIcon::ActivationReason reason); + +private: + void setupTrayMenu(); + + QSystemTrayIcon *m_trayIcon; + QMenu *m_trayMenu; + + // Cache to avoid unnecessary tooltip updates + QString m_lastTooltip; +}; + +#endif // SYSTEMTRAYMANAGER_H diff --git a/TimerState.cpp b/TimerState.cpp new file mode 100644 index 0000000..03a6d36 --- /dev/null +++ b/TimerState.cpp @@ -0,0 +1 @@ +#include "TimerState.h" diff --git a/TimerState.h b/TimerState.h new file mode 100644 index 0000000..a49ed54 --- /dev/null +++ b/TimerState.h @@ -0,0 +1,65 @@ +#ifndef TIMERSTATE_H +#define TIMERSTATE_H + +#include + +enum class TimerState : quint8 { // Use smaller enum type + Work, + ShortBreak, + LongBreak +}; + +class TimerStateHelper +{ +public: + // Use constexpr and string literals for better performance + static QString getStateText(TimerState state) noexcept { + switch (state) { + case TimerState::Work: + return "Work"; + case TimerState::ShortBreak: + return "Short Break"; + case TimerState::LongBreak: + return "Long Break"; + } + return "Unknown"; + } + + static QString getStateEmoji(TimerState state) noexcept { + switch (state) { + case TimerState::Work: + return "👨‍💻"; + case TimerState::ShortBreak: + return "☕️"; + case TimerState::LongBreak: + return "😴"; + } + return "❓"; + } + + static QString getStatusMessage(TimerState state, bool isRunning) noexcept { + QString text = getStateText(state); + if (!isRunning) { + text += " (Paused)"; + } + return text; + } + + static QString getStateTextString(TimerState state) { + return getStateText(state); + } + + static int getDurationForState(TimerState state, int workDuration, int shortBreak, int longBreak) noexcept { + switch (state) { + case TimerState::Work: + return workDuration; + case TimerState::ShortBreak: + return shortBreak; + case TimerState::LongBreak: + return longBreak; + } + return 0; + } +}; + +#endif // TIMERSTATE_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..ae1aa4a --- /dev/null +++ b/main.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "PomodoroTimer.h" + +namespace { + // Application constants - use QStringLiteral for compile-time optimization + const QString APP_NAME = QStringLiteral("Pomodoro Timer"); + const QString APP_VERSION = QStringLiteral("0.1.0"); + const QString ORGANIZATION = QStringLiteral("PomodoroApp"); + + // Icon generation constants + constexpr double TOMATO_SIZE_RATIO = 0.8; + constexpr double STEM_SIZE_RATIO = 0.15; + constexpr double TOMATO_FLATTEN_RATIO = 0.9; + constexpr int PEN_WIDTH = 1; + constexpr int HIGHLIGHT_ALPHA = 100; + + // Cache for generated icons to avoid regeneration + QIcon g_cachedAppIcon; + + QIcon createTomatoIcon(int size = 32) + { + QPixmap pixmap(size, size); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing); + + // Calculate proportions + const int tomatoSize = static_cast(size * TOMATO_SIZE_RATIO); + const int stemSize = static_cast(size * STEM_SIZE_RATIO); + const int margin = (size - tomatoSize) / 2; + + // Draw tomato body with gradient + QRadialGradient tomatoGradient(size/2, size/2 + stemSize, tomatoSize/2); + tomatoGradient.setColorAt(0.0, QColor(255, 100, 100)); + tomatoGradient.setColorAt(0.7, QColor(220, 50, 50)); + tomatoGradient.setColorAt(1.0, QColor(180, 30, 30)); + + painter.setBrush(QBrush(tomatoGradient)); + painter.setPen(QPen(QColor(150, 20, 20), PEN_WIDTH)); + + const QRect tomatoRect(margin, margin + stemSize, tomatoSize, + static_cast(tomatoSize * TOMATO_FLATTEN_RATIO)); + painter.drawEllipse(tomatoRect); + + // Draw stem and leaves + painter.setBrush(QBrush(QColor(34, 139, 34))); + painter.setPen(QPen(QColor(20, 100, 20), PEN_WIDTH)); + + // Main stem + const QRect stemRect(size/2 - stemSize/3, margin, stemSize/1.5, stemSize); + painter.drawRect(stemRect); + + // Draw leaves using polygons - optimize polygon creation + QPolygon leftLeaf, rightLeaf; + leftLeaf.reserve(3); + rightLeaf.reserve(3); + + leftLeaf << QPoint(size/2 - stemSize/2, margin + stemSize/3) + << QPoint(size/2 - stemSize, margin) + << QPoint(size/2 - stemSize/3, margin + stemSize/2); + + rightLeaf << QPoint(size/2 + stemSize/2, margin + stemSize/3) + << QPoint(size/2 + stemSize, margin) + << QPoint(size/2 + stemSize/3, margin + stemSize/2); + + painter.drawPolygon(leftLeaf); + painter.drawPolygon(rightLeaf); + + // Add 3D highlight effect + painter.setBrush(QBrush(QColor(255, 150, 150, HIGHLIGHT_ALPHA))); + painter.setPen(Qt::NoPen); + const QRect highlightRect(margin + tomatoSize/4, margin + stemSize + tomatoSize/4, + tomatoSize/3, tomatoSize/4); + painter.drawEllipse(highlightRect); + + return QIcon(pixmap); + } + + const QIcon& createApplicationIcon() + { + if (g_cachedAppIcon.isNull()) { + constexpr int iconSizes[] = {16, 24, 32, 48, 64}; + + for (int size : iconSizes) { + g_cachedAppIcon.addPixmap(createTomatoIcon(size).pixmap(size, size)); + } + } + return g_cachedAppIcon; + } + + void centerWindow(QWidget* window) + { + const QScreen* screen = QApplication::primaryScreen(); + if (!screen) return; + + const QRect screenGeometry = screen->availableGeometry(); + const int x = (screenGeometry.width() - window->width()) / 2; + const int y = (screenGeometry.height() - window->height()) / 2; + window->move(x, y); + } + + void setupApplicationProperties(QApplication& app) + { + app.setApplicationName(APP_NAME); + app.setApplicationVersion(APP_VERSION); + app.setOrganizationName(ORGANIZATION); + app.setWindowIcon(createApplicationIcon()); + app.setQuitOnLastWindowClosed(false); + } +} + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + setupApplicationProperties(app); + + PomodoroTimer timer; + timer.show(); + centerWindow(&timer); + + return app.exec(); +} \ No newline at end of file diff --git a/pomodoro-timer.desktop.in b/pomodoro-timer.desktop.in new file mode 100644 index 0000000..df15857 --- /dev/null +++ b/pomodoro-timer.desktop.in @@ -0,0 +1,9 @@ +[Desktop Entry] +Version=1.0 +Name=Pomodoro Timer +GenericName=Time Management +Comment=A simple Pomodoro timer to help you focus. +Exec=@CMAKE_INSTALL_PREFIX@/bin/PomodoroTimer +Terminal=false +Type=Application +Categories=Utility;Office;