Initial commit

This commit is contained in:
Sigismund Dijkstra 2025-07-11 22:17:18 -05:00
commit 4b310eb70e
22 changed files with 2049 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
# CLion
.idea/
cmake-build-*/
# Build artifacts
*.o
*.a
*.so
*.exe
*_autogen/
# User-specific files
*.user
*~
.DS_Store

109
CMakeLists.txt Normal file
View File

@ -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-
$<$<CONFIG:Release>:/O2>
)
else()
target_compile_options(${PROJECT_NAME} PRIVATE
-Wall -Wextra -Wpedantic
$<$<CONFIG:Release>:-O3>
$<$<CONFIG:Debug>:-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
)

69
CircularProgressBar.cpp Normal file
View File

@ -0,0 +1,69 @@
#include "CircularProgressBar.h"
#include <QPainter>
#include <QResizeEvent>
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<QWidget*>(QString(), Qt::FindDirectChildrenOnly);
for (QWidget *child : children) {
child->resize(width(), 40);
child->move(0, (height() - 40) / 2);
}
}

29
CircularProgressBar.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef CIRCULARPROGRESSBAR_H
#define CIRCULARPROGRESSBAR_H
#include <QWidget>
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

24
KeyboardShortcuts.cpp Normal file
View File

@ -0,0 +1,24 @@
#include "KeyboardShortcuts.h"
#include <QWidget>
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);
}

33
KeyboardShortcuts.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef KEYBOARDSHORTCUTS_H
#define KEYBOARDSHORTCUTS_H
#include <QObject>
#include <QShortcut>
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

22
LICENSE Normal file
View File

@ -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.

34
NotificationManager.cpp Normal file
View File

@ -0,0 +1,34 @@
#include "NotificationManager.h"
#include "SystemTrayManager.h"
#include <QWidget>
#include <QMessageBox>
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);
}
}

29
NotificationManager.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef NOTIFICATIONMANAGER_H
#define NOTIFICATIONMANAGER_H
#include <QObject>
#include <QString>
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

500
PomodoroTimer.cpp Normal file
View File

@ -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 <QApplication>
#include <QCloseEvent>
#include <QDateTime>
#include <QFont>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QSettings>
#include <QVBoxLayout>
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<SystemTrayManager>(this))
, m_keyboardShortcuts(std::make_unique<KeyboardShortcuts>(this))
, m_notificationManager(std::make_unique<NotificationManager>(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<int>(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();
}
}

136
PomodoroTimer.h Normal file
View File

@ -0,0 +1,136 @@
#ifndef POMODOROTIMER_H
#define POMODOROTIMER_H
#include <QWidget>
#include <QTimer>
#include <QLabel>
#include <QPushButton>
#include <QFrame>
#include <QDateTime>
#include <memory>
#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<SystemTrayManager> m_trayManager;
std::unique_ptr<KeyboardShortcuts> m_keyboardShortcuts;
std::unique_ptr<NotificationManager> 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

44
README.md Normal file
View File

@ -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 <repository-url>
```
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
```

102
SettingsDialog.cpp Normal file
View File

@ -0,0 +1,102 @@
#include "SettingsDialog.h"
#include <QApplication>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QGroupBox>
#include <QPushButton>
#include <QSpinBox>
#include <QVBoxLayout>
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);
}

57
SettingsDialog.h Normal file
View File

@ -0,0 +1,57 @@
#ifndef SETTINGSDIALOG_H
#define SETTINGSDIALOG_H
#include <QDialog>
// 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

402
StatisticsDialog.cpp Normal file
View File

@ -0,0 +1,402 @@
#include "StatisticsDialog.h"
#include <QTabWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QLabel>
#include <QPushButton>
#include <QTextEdit>
#include <QScrollArea>
#include <QPainter>
#include <QSettings>
#include <QDate>
#include <QMessageBox>
#include <QFileDialog>
#include <QTextStream>
#include <QApplication>
// 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<QDate, int> &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<QDate, int> 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");
}

85
StatisticsDialog.h Normal file
View File

@ -0,0 +1,85 @@
#ifndef STATISTICSDIALOG_H
#define STATISTICSDIALOG_H
#include <QDialog>
#include <QMap>
#include <QDate>
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<QDate, int> &workTimeData);
protected:
void paintEvent(QPaintEvent *event) override;
private:
QMap<QDate, int> 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<QDate, int> m_dailyWorkTime;
QMap<QDate, int> m_dailySessions;
QString m_chartPeriod;
};
#endif // STATISTICSDIALOG_H

107
SystemTrayManager.cpp Normal file
View File

@ -0,0 +1,107 @@
#include "SystemTrayManager.h"
#include <QApplication>
#include <QAction>
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<QSystemTrayIcon::ActivationReason>::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();
}
}

45
SystemTrayManager.h Normal file
View File

@ -0,0 +1,45 @@
#ifndef SYSTEMTRAYMANAGER_H
#define SYSTEMTRAYMANAGER_H
#include <QObject>
#include <QSystemTrayIcon>
#include <QMenu>
#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

1
TimerState.cpp Normal file
View File

@ -0,0 +1 @@
#include "TimerState.h"

65
TimerState.h Normal file
View File

@ -0,0 +1,65 @@
#ifndef TIMERSTATE_H
#define TIMERSTATE_H
#include <QString>
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

131
main.cpp Normal file
View File

@ -0,0 +1,131 @@
#include <QApplication>
#include <QScreen>
#include <QIcon>
#include <QPainter>
#include <QPixmap>
#include <QRadialGradient>
#include <QPen>
#include <QBrush>
#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<int>(size * TOMATO_SIZE_RATIO);
const int stemSize = static_cast<int>(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<int>(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();
}

View File

@ -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;