commit 4b310eb70e3387291c27b2fbef154466c20a6cec Author: Darkheim Date: Fri Jul 11 22:17:18 2025 -0500 Initial commit 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;