Compare commits

..

21 Commits

Author SHA1 Message Date
9c48fe5f38 - refresh Qt version + refresh linuxdeployqt + better logging in idle tracking + version increase 2025-04-26 21:49:28 +03:00
61ef94b474 - minor cleanup 2023-06-12 08:15:13 +03:00
aba5e21271 - add debug build script + don't set the default tray icon tooltip 2023-05-22 11:13:01 +03:00
76a63b29c2 - support Qt6 (no audio yet) + increase version to 0.1.16 2023-05-20 11:59:07 +03:00
37840dfdb1 - attempt to fix the problem with non-working setTooltip() 2023-05-13 20:32:45 +03:00
9ff378d5e0 - fix missing tooltip if app is in autostart + increase version to 0.1.15 2023-05-11 15:32:17 +03:00
317b7097f6 - fix logging to console 2023-05-10 08:13:30 +03:00
64658a7778 - create ~/.config/autostart if not exists + adopt for new build environment 2023-04-21 11:42:27 +03:00
447f33bf94 - fix idle settings processing + version 0.1.13 2022-12-11 08:42:19 +03:00
6200504627 - minor fixes + renamed audio alarms + increased version to 0.1.12 2022-11-28 21:21:44 +03:00
6af94a3a6f - fixes in idle handling + notifying is fixed + version increased to 0.1.11 2022-11-27 19:37:46 +03:00
305ee12496 - fix release builds - now splash window is fullscreen again + switch to version 0.1.10 2022-11-25 23:20:23 +03:00
3f8aa69945 - fixed bad time displaying (minutes were decreased for 30 seconds earlier) + this should fix the postpone counter 2022-11-19 18:54:33 +03:00
21046d9c35 - many fixes + version increase to 0.1.9 2022-11-18 21:48:55 +03:00
6c495b6c7f - fixes 2022-11-15 20:27:26 +03:00
4e51dc85e8 - allow debug builds 2022-11-13 21:04:50 +03:00
6b88c48c3a - fix test calls 2022-11-13 20:53:35 +03:00
266d6ddf09 - attempt to make app easier to understand using state management 2022-11-13 20:16:09 +03:00
fff67bbedb - version 0.1.8 - new state machine + fix problems with desktop file. 2022-09-20 11:19:36 +03:00
Dmytro Bogovych
f1ca3ed0ed - better idle handling 2022-06-17 22:01:54 +03:00
eed9e13e91 - fix the strange behavior when user missed break and start of next working interval 2022-05-31 14:40:21 +03:00
24 changed files with 486 additions and 211 deletions

View File

@ -13,6 +13,9 @@ AboutDlg::AboutDlg(QWidget *parent) :
.arg(QBREAK_VERSION_MAJOR) .arg(QBREAK_VERSION_MAJOR)
.arg(QBREAK_VERSION_MINOR) .arg(QBREAK_VERSION_MINOR)
.arg(QBREAK_VERSION_SUFFIX); .arg(QBREAK_VERSION_SUFFIX);
#if defined(DEBUG)
version_text += ". Debug build.";
#endif
ui->mAppVersionLabel->setText(version_text); ui->mAppVersionLabel->setText(version_text);
} }

View File

@ -1,3 +0,0 @@
URL for used image:
https://thenounproject.com/icon/coffee-break-537907/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m228 72c-9.332 0-14.488 4.707-18.375 9.375-5.1367 6.5391-7.332 15.398-4.125 23.25 18.855 47.793 10.898 59.164 0.375 85.5s-22.758 65.922-0.375 129.75c3.3164 12.883 18.977 20.988 31.5 16.5s19.121-20.566 13.5-32.625c-19.188-54.711-10.637-70.312-0.375-96s23.09-63.176 0.375-120.75c-3.4375-9.0625-12.848-14.941-22.5-15zm324 0c-9.332 0-14.488 4.707-18.375 9.375-5.1367 6.5391-7.332 15.398-4.125 23.25 18.855 47.793 10.898 59.164 0.375 85.5s-22.758 65.922-0.375 129.75c3.3164 12.883 18.977 20.988 31.5 16.5s19.121-20.566 13.5-32.625c-19.188-54.711-10.637-70.312-0.375-96s23.09-63.176 0.375-120.75c-3.4375-9.0625-12.848-14.941-22.5-15zm324 0c-9.332 0-14.488 4.707-18.375 9.375-5.1367 6.5391-7.332 15.398-4.125 23.25 18.855 47.793 10.898 59.164 0.375 85.5s-22.758 65.922-0.375 129.75c3.3164 12.883 18.977 20.988 31.5 16.5s19.121-20.566 13.5-32.625c-19.188-54.711-10.637-70.312-0.375-96s23.09-63.176 0.375-120.75c-3.4375-9.0625-12.848-14.941-22.5-15zm-782.25 360c-11.797 1.1133-21.801 12.148-21.75 24 0 304.82 77.43 453.77 174.75 624h-126.75c-13.297 0-24 10.703-24 24s10.703 24 24 24h864c13.297 0 24-10.703 24-24s-10.703-24-24-24h-126.75c25.375-44.387 49.617-87.379 71.25-132h115.5c46.531 0 84-37.469 84-84v-264c0-46.531-37.465-84-84-84h-13.125c0.69922-19.352 1.125-39.258 1.125-60 0-12.566-11.434-24-24-24h-912c-0.75-0.035156-1.5-0.035156-2.25 0zm27 48h862.5c-4.168 292.51-80.758 424.66-181.5 600h-499.5c-100.74-175.34-177.33-307.49-181.5-600zm907.5 84h15.75c24.383 0 36 11.621 36 36v264c0 24.383-11.617 36-36 36h-93.75c39.703-92.473 68.336-195.91 78-336z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1,10 +1,12 @@
#include "audio_support.h" #include "audio_support.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <QSound> #include <QSound>
extern void play_audio(const app_settings::selected_audio& item) extern void play_audio(const app_settings::selected_audio& item)
{ {
// Play audio // Play audio
if (item.name != Audio_Empty && item.name != Audio_Custom) if (item.name != Audio_Empty && item.name != Audio_Custom)
{ {
// Find bundled audio // Find bundled audio
@ -16,4 +18,7 @@ extern void play_audio(const app_settings::selected_audio& item)
if (item.name == Audio_Custom && !item.path.isEmpty()) if (item.name == Audio_Custom && !item.path.isEmpty())
QSound::play(item.path); QSound::play(item.path);
} }
#else
extern void play_audio(const app_settings::selected_audio& item)
{}
#endif

View File

@ -64,6 +64,17 @@ static fs::path autostart_path()
void autostart::enable(const std::string& path_to_me) void autostart::enable(const std::string& path_to_me)
{ {
// Ensure autostart directory exists at all
if (!fs::exists(autostart_dir()))
{
std::error_code ec;
if (!fs::create_directory(autostart_dir(), ec))
{
qDebug() << "Failed to create autostart directory. Error: " << QString::fromStdString(ec.message());
return;
}
}
// Put .desktop file to ~/.config/autostart // Put .desktop file to ~/.config/autostart
std::ofstream ofs(autostart_path()); std::ofstream ofs(autostart_path());
if (ofs.is_open()) if (ofs.is_open())
@ -98,8 +109,21 @@ static fs::path appmenu_install_dir()
void appmenu::install(const std::string& path_to_me) void appmenu::install(const std::string& path_to_me)
{ {
auto path_to_desktop = appmenu_install_dir() / QBREAK_DESKTOP_NAME;
// Check if app is installed already.
// The code below checks for path to app; as this app is distributed as .AppImage with version numbers - every new version will trigger desktop file rewriting
std::ifstream ifs(path_to_desktop);
std::string content((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
if (content.find(path_to_me) != std::string::npos)
return;
// Remove old one
std::string uninstall_cmd = "/usr/bin/xdg-desktop-menu uninstall --novendor " + path_to_desktop.string();
system(uninstall_cmd.c_str());
// Put .desktop file to ~/.config/autostart // Put .desktop file to ~/.config/autostart
std::ofstream ofs(appmenu_install_dir() / QBREAK_DESKTOP_NAME); std::ofstream ofs(path_to_desktop);
if (ofs.is_open()) if (ofs.is_open())
{ {
ofs << fixup_desktop_file(read_desktop_file(), path_to_me); ofs << fixup_desktop_file(read_desktop_file(), path_to_me);
@ -125,6 +149,9 @@ void appmenu::install(const std::string& path_to_me)
} }
} }
} }
std::string install_cmd = "/usr/bin/xdg-desktop-menu install --novendor " + path_to_desktop.string();
system(install_cmd.c_str());
} }
void appmenu::uninstall() void appmenu::uninstall()

View File

@ -4,15 +4,15 @@
// App version // App version
#define QBREAK_VERSION_MAJOR 0 #define QBREAK_VERSION_MAJOR 0
#define QBREAK_VERSION_MINOR 1 #define QBREAK_VERSION_MINOR 1
#define QBREAK_VERSION_SUFFIX 5 #define QBREAK_VERSION_SUFFIX 17
// How often UI is updated - interval in seconds // How often UI is updated - interval in seconds
#define INTERVAL_UPDATE_UI (60) #define INTERVAL_UPDATE_UI (10)
// How often progress bar is updated - interval in milliseconds // How often progress bar is updated - interval in milliseconds
#define INTERVAL_UPDATE_PROGRESS (1000) #define INTERVAL_UPDATE_PROGRESS (1000)
// How long notification is shown - in milliseconds // How long notification is shown - in seconds
#define INTERVAL_NOTIFICATION (30000) #define INTERVAL_NOTIFICATION (30)
#endif // CONFIG_H #endif // CONFIG_H

View File

@ -6,9 +6,9 @@
#if defined(TARGET_LINUX) #if defined(TARGET_LINUX)
#include <QObject> #include <QObject>
#include <QDBusConnection> #include <QtDBus/QDBusConnection>
#include <QDBusReply> #include <QtDBus/QDBusReply>
#include <QDBusInterface> #include <QtDBus/QDBusInterface>
// Thanks to https://stackoverflow.com/questions/222606/detecting-keyboard-mouse-activity-in-linux // Thanks to https://stackoverflow.com/questions/222606/detecting-keyboard-mouse-activity-in-linux
@ -210,6 +210,7 @@ int get_idle_time_kde_wayland()
#endif #endif
static bool Warning_X11InWayland = false;
int get_idle_time_dynamically() int get_idle_time_dynamically()
{ {
const char* wl_display = std::getenv("WAYLAND_DISPLAY"); const char* wl_display = std::getenv("WAYLAND_DISPLAY");
@ -235,8 +236,14 @@ int get_idle_time_dynamically()
#else #else
// Restrict to X11 // Restrict to X11
if (wl_display) if (wl_display)
{
// One time error message
if (!Warning_X11InWayland) {
qDebug() << "Wayland is found, but app built for X11 only. Idle tracking is not supported.";
Warning_X11InWayland = true;
}
return 0; return 0;
}
return get_idle_time_x11(); return get_idle_time_x11();
#endif #endif
} }

View File

@ -6,17 +6,67 @@
#include <QCommandLineParser> #include <QCommandLineParser>
#include <QTranslator> #include <QTranslator>
#include <QFileInfo> #include <QFileInfo>
#include <QDateTime>
#if defined(TARGET_LINUX)
# include <syslog.h>
#endif
// Handler for Qt log messages that sends output to syslog as well as standard error.
void SyslogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
Q_UNUSED(context)
std::string timePrefix = QDateTime::currentDateTime().toString().toStdString();
QByteArray localMsg = msg.toLocal8Bit();
switch (type) {
case QtDebugMsg:
fprintf(stderr, "debug: %s %s\n", timePrefix.c_str(), localMsg.constData());
#if defined(TARGET_LINUX)
syslog(LOG_DEBUG, "debug: %s", localMsg.constData());
#endif
break;
case QtInfoMsg:
fprintf(stderr, "info: %s %s\n", timePrefix.c_str(), localMsg.constData());
#if defined(TARGET_LINUX)
syslog(LOG_INFO, "info: %s", localMsg.constData());
#endif
break;
case QtWarningMsg:
fprintf(stderr, "warning: %s %s\n", timePrefix.c_str(), localMsg.constData());
#if defined(TARGET_LINUX)
syslog(LOG_WARNING, "warning: %s", localMsg.constData());
#endif
break;
case QtCriticalMsg:
fprintf(stderr, "critical: %s %s\n", timePrefix.c_str(), localMsg.constData());
#if defined(TARGET_LINUX)
syslog(LOG_CRIT, "critical: %s", localMsg.constData());
#endif
break;
case QtFatalMsg:
fprintf(stderr, "fatal: %s %s\n", timePrefix.c_str(), localMsg.constData());
#if defined(TARGET_LINUX)
syslog(LOG_ALERT, "fatal: %s", localMsg.constData());
#endif
abort();
}
}
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
RunGuard guard("adjfaifaif"); RunGuard guard("QBreak app - runguard");
if ( !guard.tryToRun() ) if ( !guard.tryToRun() )
return 0; return 0;
// Needed to enable logging to syslog or journald.
qInstallMessageHandler(SyslogMessageHandler);
QApplication app(argc, argv); QApplication app(argc, argv);
QCoreApplication::setOrganizationName("qbreak.com"); QCoreApplication::setOrganizationName("voipobjects.com");
QCoreApplication::setOrganizationDomain("qbreak.com"); QCoreApplication::setOrganizationDomain("voipobjects.com");
QCoreApplication::setApplicationName("QBreak"); QCoreApplication::setApplicationName("QBreak");
QTranslator translator; QTranslator translator;
@ -27,30 +77,19 @@ int main(int argc, char *argv[])
app.setQuitOnLastWindowClosed(false); app.setQuitOnLastWindowClosed(false);
QCommandLineParser parser;
QCommandLineOption test_1(QStringList() << "t1" << "test_1");
parser.addOption(test_1);
// Run app with break intervals & duration 60 seconds.
// This should trigger the notification in 30 seconds before breaks.
QCommandLineOption test_2(QStringList() << "t2" << "test_2");
parser.addOption(test_2);
parser.process(app);
// Put itself into app menu // Put itself into app menu
appmenu::install(QFileInfo(argv[0]).absoluteFilePath().toStdString()); auto exe_path = QCoreApplication::applicationFilePath();
const char* appimage = std::getenv("APPIMAGE");
if (appimage != nullptr)
exe_path = appimage;
appmenu::install(exe_path.toStdString());
// Main window is full screen window, so start with tray icon only // Main window is full screen window, so start with tray icon only
MainWindow w; MainWindow w;
w.hide(); w.hide();
if (parser.isSet(test_1))
w.test_1();
else
if (parser.isSet(test_2))
w.test_2();
int retcode = app.exec(); int retcode = app.exec();
return retcode; return retcode;

View File

@ -12,20 +12,46 @@
#include <QAction> #include <QAction>
#include <QSettings> #include <QSettings>
#include <QDebug> #include <QDebug>
#include <QDesktopWidget>
#include <QSvgGenerator> #include <QSvgGenerator>
#include <QPalette> #include <QPalette>
#include <QScreen> #include <QScreen>
#include <QWindow> #include <QWindow>
#include <QFileInfo> #include <QFileInfo>
#include <QSound> #include <QCommandLineOption>
#include <QCommandLineParser>
#include <QDateTime>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
# include <QDesktopWidget>
# include <QSound>
#endif
#include <string>
static void dispatchToMainThread(std::function<void()> callback, std::chrono::milliseconds delay = std::chrono::milliseconds(0))
{
// any thread
QTimer* timer = new QTimer();
timer->moveToThread(qApp->thread());
timer->setSingleShot(true);
timer->setInterval(delay);
QObject::connect(timer, &QTimer::timeout, [=]()
{
// main thread
callback();
timer->deleteLater();
});
QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, 0));
}
MainWindow::MainWindow(QWidget *parent) MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent) : QMainWindow(parent)
, ui(new Ui::MainWindow) , ui(new Ui::MainWindow)
{ {
ui->setupUi(this); ui->setupUi(this);
init();
// Defer init call for 100 ms - attempt to resolve the problem with non working setTooltip()
dispatchToMainThread([this](){init();}, std::chrono::milliseconds(100));
} }
MainWindow::~MainWindow() MainWindow::~MainWindow()
@ -83,16 +109,16 @@ void MainWindow::init()
mLastIdleMilliseconds = 0; mLastIdleMilliseconds = 0;
// Timer to start break // Timer to start break
mTimer = new QTimer(this); mBreakStartTimer = new QTimer(this);
mTimer->setTimerType(Qt::TimerType::CoarseTimer); mBreakStartTimer->setTimerType(Qt::TimerType::CoarseTimer);
mTimer->setSingleShot(true); mBreakStartTimer->setSingleShot(true);
connect(mTimer, SIGNAL(timeout()), this, SLOT(onLongBreakStart())); connect(mBreakStartTimer, &QTimer::timeout, this, [this](){shiftTo(AppState::Break);});
// Timer to run notification about upcoming break // Timer to run notification about upcoming break
mNotifyTimer = new QTimer(this); mBreakNotifyTimer = new QTimer(this);
mNotifyTimer->setTimerType(Qt::TimerType::CoarseTimer); mBreakNotifyTimer->setTimerType(Qt::TimerType::CoarseTimer);
mNotifyTimer->setSingleShot(true); mBreakNotifyTimer->setSingleShot(true);
connect(mNotifyTimer, SIGNAL(timeout()), this, SLOT(onLongBreakNotify())); connect(mBreakNotifyTimer, SIGNAL(timeout()), this, SLOT(onLongBreakNotify()));
// Just update UI once per minute // Just update UI once per minute
mUpdateUITimer = new QTimer(this); mUpdateUITimer = new QTimer(this);
@ -109,34 +135,66 @@ void MainWindow::init()
connect(mProgressTimer, SIGNAL(timeout()), this, SLOT(onProgress())); connect(mProgressTimer, SIGNAL(timeout()), this, SLOT(onProgress()));
connect(ui->mPostponeButton, SIGNAL(clicked()), this, SLOT(onLongBreakPostpone())); connect(ui->mPostponeButton, SIGNAL(clicked()), this, SLOT(onLongBreakPostpone()));
connect(ui->mSkipButton, SIGNAL(clicked()), this, SLOT(onLongBreakEnd())); connect(ui->mSkipButton, &QPushButton::clicked, this, [this](){shiftTo(AppState::Counting);});
// Use the latest config // Use the latest config
applyConfig(); applyConfig();
// Refresh UI shiftTo(AppState::Counting);
onUpdateUI(); }
static int str_to_seconds(const QString& s)
{
if (s.isEmpty())
throw std::runtime_error("Bad parameter value.");
if (s.back() == 'h')
return s.left(s.size()-1).toInt() * 3600;
if (s.back() == 'm')
return s.left(s.size()-1).toInt() * 60;
if (s.back() == 's')
return s.left(s.size()-1).toInt();
if (s.back().isDigit())
return s.toInt();
throw std::runtime_error("Bad parameter value");
} }
void MainWindow::loadConfig() void MainWindow::loadConfig()
{ {
app_settings settings; mAppConfig = app_settings::load();
mAppConfig = settings.load();
// Check for command line options
QCommandLineParser parser;
QCommandLineOption
param_break("break-duration", "Long break duration.", "break-duration"),
param_work("work-duration", "Work time duration.", "work-duration"),
param_idle("idle-timeout", "Idle timeout.", "idle-timeout");
parser.addOptions({param_break, param_work, param_idle});
parser.process(*QApplication::instance());
if (parser.isSet(param_break))
mAppConfig.longbreak_length = str_to_seconds(parser.value(param_break));
if (parser.isSet(param_work))
mAppConfig.longbreak_interval = str_to_seconds(parser.value(param_work));
if (parser.isSet(param_idle))
mAppConfig.idle_timeout = str_to_seconds(parser.value(param_idle));
} }
void MainWindow::applyConfig() void MainWindow::applyConfig()
{ {
if (mTimer) if (mBreakStartTimer)
{ {
if (mTimer->interval() != mAppConfig.longbreak_interval) if (mBreakStartTimer->interval() != mAppConfig.longbreak_interval)
{ {
mTimer->stop(); mBreakStartTimer->stop();
mTimer->setInterval(std::chrono::seconds(mAppConfig.longbreak_interval)); mBreakStartTimer->setInterval(std::chrono::seconds(mAppConfig.longbreak_interval));
mTimer->start(); mBreakStartTimer->start();
mNotifyTimer->stop(); mBreakNotifyTimer->stop();
mNotifyTimer->setInterval(std::chrono::seconds(mAppConfig.longbreak_interval - 30)); mBreakNotifyTimer->setInterval(std::chrono::seconds(mAppConfig.longbreak_interval - 30));
mNotifyTimer->start(); mBreakNotifyTimer->start();
} }
} }
@ -154,39 +212,6 @@ void MainWindow::applyConfig()
autostart::disable(); autostart::disable();
} }
void MainWindow::schedule()
{
;
}
void MainWindow::test_1()
{
// 10 seconds test break
mAppConfig.longbreak_length = 10;
mAppConfig.longbreak_postpone_interval = 10;
mAppConfig.longbreak_interval = 10;
mAppConfig.window_on_top = true;
mAppConfig.verbose = true;
applyConfig();
onUpdateUI();
onLongBreakStart();
}
void MainWindow::test_2()
{
// 60 seconds test break
mAppConfig.longbreak_length = 60;
mAppConfig.longbreak_postpone_interval = 60;
mAppConfig.longbreak_interval = 60;
mAppConfig.window_on_top = true;
mAppConfig.verbose = true;
applyConfig();
onUpdateUI();
}
void MainWindow::showMe() void MainWindow::showMe()
{ {
QScreen* screen = nullptr; QScreen* screen = nullptr;
@ -210,7 +235,15 @@ void MainWindow::showMe()
// qDebug() << "Window moved to screen " << screen->name() + " / " + screen->model() + " " + screen->manufacturer(); // qDebug() << "Window moved to screen " << screen->name() + " / " + screen->model() + " " + screen->manufacturer();
} }
} }
// else
// qDebug() << "Screen not found!";
#if defined(DEBUG)
showFullScreen(); showFullScreen();
//showMaximized();
#else
showFullScreen();
#endif
} }
void MainWindow::hideMe() void MainWindow::hideMe()
@ -223,7 +256,10 @@ static QString secondsToText(int seconds)
if (seconds < 60) if (seconds < 60)
return QObject::tr("%1 seconds").arg(seconds); return QObject::tr("%1 seconds").arg(seconds);
else else
return QObject::tr("%1 minutes").arg(seconds / 60); {
int minutes = int(float(seconds) / 60 + 0.5f);
return QObject::tr("%1 minutes").arg(minutes);
}
} }
void MainWindow::createTrayIcon() void MainWindow::createTrayIcon()
@ -250,10 +286,11 @@ void MainWindow::createTrayIcon()
mTrayIcon->setContextMenu(menu); mTrayIcon->setContextMenu(menu);
mTrayIcon->setIcon(getTrayIcon()); mTrayIcon->setIcon(getTrayIcon());
mTrayIcon->setToolTip(AppName); // mTrayIcon->setToolTip(AppName);
mTrayIcon->show();
connect(mTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), connect(mTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
this, SLOT(onTrayIconActivated(QSystemTrayIcon::ActivationReason))); this, SLOT(onTrayIconActivated(QSystemTrayIcon::ActivationReason)));
mTrayIcon->show();
} }
static int msec2min(int msec) static int msec2min(int msec)
@ -262,66 +299,121 @@ static int msec2min(int msec)
return (int)(min_f + 0.5f); return (int)(min_f + 0.5f);
} }
void MainWindow::onUpdateUI() QString state2str(AppState state)
{ {
if (mAppConfig.idle_timeout != 0 && (mTimer->isActive() || mIdleStart)) switch (state)
{ {
int idle_milliseconds = get_idle_time_dynamically(); case AppState::None:
if (idle_milliseconds >= mAppConfig.idle_timeout * 60 * 1000) return "None";
{ case AppState::Break:
if (!mIdleStart) return "Break";
{ case AppState::Idle:
// Start idle mode. Save idle start time return "Idle";
mIdleStart = std::chrono::steady_clock::now() - std::chrono::milliseconds(idle_milliseconds); case AppState::Counting:
if (mTimer->isActive()) return "Counting";
{ }
// Save how much time was remaininig when idle was detected + add idle length return QString();
// Later timer will restart with this interval time }
mIdleRemaining = mTimer->remainingTime() + idle_milliseconds;
// Stop counting
mTimer->stop(); void MainWindow::shiftTo(AppState newState)
mNotifyTimer->stop(); {
} if (newState == mState)
} return;
else qDebug() << state2str(mState) << " -> " << state2str(newState);
{
// Do nothing here - main timer is stopped, idle time & timer remaining duration are recorded already switch (newState)
// How much time remains ? {
} case AppState::None:
} // Do nothing, app is not started
else break;
{
if (mIdleStart) case AppState::Idle:
{ onIdleStart();
// Idle interval ended break;
mIdleStart.reset();
mTimer->start(mIdleRemaining); case AppState::Break:
mNotifyTimer->start(std::max(1, mIdleRemaining - 30 * 1000)); // Break is active
} onLongBreakStart();
else break;
{
// Do nothing here - timer is running already case AppState::Counting:
} // Working, break is closing
} if (mState == AppState::Break)
onLongBreakEnd();
else
if (mState == AppState::Idle)
onIdleEnd();
break;
} }
if (mTrayIcon) mState = newState;
{
if (mProgressTimer->isActive()) // Run deferred in main UI thread
dispatchToMainThread([this](){
onUpdateUI();
});
}
void MainWindow::onUpdateUI()
{
qDebug() << "UI timer fired.";
int idle_milliseconds = 0;
switch (mState) {
case AppState::None:
// Do nothing, app is not started
break;
case AppState::Idle:
// Detected idle, don't count this time as working
// But check - maybe idle is over
idle_milliseconds = get_idle_time_dynamically();
qDebug() << "Idle found: " << idle_milliseconds / 1000 << "s";
if (idle_milliseconds < mAppConfig.idle_timeout * 1000)
{ {
// Break is in effect now shiftTo(AppState::Counting);
return;
}
break;
case AppState::Break:
// Break is active
if (mTrayIcon)
mTrayIcon->setToolTip(QString()); mTrayIcon->setToolTip(QString());
break;
case AppState::Counting:
// Working, break is closing
// Check maybe it is idle ?
if (!mIdleStart && mAppConfig.idle_timeout)
{
idle_milliseconds = get_idle_time_dynamically();
qDebug() << "Idle found: " << idle_milliseconds / 1000 << "s";
if (idle_milliseconds >= mAppConfig.idle_timeout * 1000)
{
shiftTo(AppState::Idle);
return;
}
}
// Update tray icon
if (mTrayIcon) {
auto remaining_milliseconds = mBreakStartTimer->remainingTime();
if (remaining_milliseconds < 60000)
mTrayIcon->setToolTip(
tr("Less than a minute left until the next break."));
else
mTrayIcon->setToolTip(
tr("There are %1 minutes left until the next break.")
.arg(msec2min(remaining_milliseconds)));
} }
else else
if (mTimer->isActive()) qDebug() << "No tray icon available.";
{
auto remaining_milliseconds = mTimer->remainingTime(); break;
if (remaining_milliseconds < 60000)
mTrayIcon->setToolTip(tr("Less than a minute left until the next break."));
else
mTrayIcon->setToolTip(tr("There are %1 minutes left until the next break.").arg(msec2min(remaining_milliseconds)));
}
} }
ui->mSkipButton->setVisible(mPostponeCount > 0); ui->mSkipButton->setVisible(mPostponeCount > 0);
@ -332,39 +424,38 @@ void MainWindow::onLongBreakNotify()
mTrayIcon->showMessage(tr("New break"), mTrayIcon->showMessage(tr("New break"),
tr("New break will start in %1 secs").arg(Default_Notify_Length), tr("New break will start in %1 secs").arg(Default_Notify_Length),
getAppIcon(), getAppIcon(),
INTERVAL_NOTIFICATION); INTERVAL_NOTIFICATION * 1000);
} }
void MainWindow::onLongBreakStart() void MainWindow::onLongBreakStart()
{ {
// qDebug() << "Long break starts for " << secondsToText(mAppConfig.longbreak_postpone_interval); mBreakStartTimer->stop();
mBreakNotifyTimer->stop();
mTimer->stop(); qDebug() << "Stop main and notify timers.";
mNotifyTimer->stop();
// Reset idle counter // Reset idle counter
mIdleStart.reset();
mLastIdleMilliseconds = 0; // Show the button "Postpone"
ui->mPostponeButton->setText(tr("Postpone for ") + secondsToText(mAppConfig.longbreak_postpone_interval)); ui->mPostponeButton->setText(tr("Postpone for ") + secondsToText(mAppConfig.longbreak_postpone_interval));
// Show the screen
showMe(); showMe();
// Save start time
mBreakStartTime = std::chrono::steady_clock::now(); mBreakStartTime = std::chrono::steady_clock::now();
// Start progress bar // Start progress bar
mProgressTimer->start(); mProgressTimer->start();
// Refresh UI // Update title immediate
onUpdateUI(); onProgress();
} }
void MainWindow::onLongBreakEnd() void MainWindow::onLongBreakEnd()
{ {
// qDebug() << "Long break ends."; // qDebug() << "Long break ends.";
// Reset postpone counter
mPostponeCount = 0;
// Prepare to next triggering // Prepare to next triggering
ui->mProgressBar->setValue(0); ui->mProgressBar->setValue(0);
@ -374,16 +465,20 @@ void MainWindow::onLongBreakEnd()
mProgressTimer->stop(); mProgressTimer->stop();
// Start new timer // Start new timer
mTimer->stop(); if (!mBreakStartTimer->isActive())
mTimer->start(std::chrono::seconds(mAppConfig.longbreak_interval)); {
mNotifyTimer->stop(); mBreakStartTimer->start(std::chrono::seconds(mAppConfig.longbreak_interval));
mNotifyTimer->start(std::chrono::seconds(mAppConfig.longbreak_interval - 30)); qDebug() << "Start main timer for " << mAppConfig.longbreak_interval << " seconds.";
}
if (!mBreakNotifyTimer->isActive())
{
mBreakNotifyTimer->start(std::chrono::seconds(mAppConfig.longbreak_interval - 30));
qDebug() << "Start notify timer for " << mAppConfig.longbreak_interval - 30 << " seconds.";
}
// Refresh UI // Play selected audio. When break is postponed - audio is not played
onUpdateUI(); if (0 == mPostponeCount)
play_audio(mAppConfig.play_audio);
// Play selecged audio
play_audio(mAppConfig.play_audio);
// Run script // Run script
if (!mAppConfig.script_on_break_finish.isEmpty()) if (!mAppConfig.script_on_break_finish.isEmpty())
@ -405,13 +500,12 @@ void MainWindow::onLongBreakPostpone()
ui->mProgressBar->setValue(0); ui->mProgressBar->setValue(0);
// Start timer again // Start timer again
mTimer->stop(); mBreakStartTimer->stop();
mTimer->start(std::chrono::seconds(mAppConfig.longbreak_postpone_interval)); mBreakStartTimer->start(std::chrono::seconds(mAppConfig.longbreak_postpone_interval));
mNotifyTimer->stop(); mBreakNotifyTimer->stop();
mNotifyTimer->start(std::chrono::seconds(mAppConfig.longbreak_postpone_interval - 30)); mBreakNotifyTimer->start(std::chrono::seconds(mAppConfig.longbreak_postpone_interval - 30));
// Refresh UI shiftTo(AppState::Counting);
onUpdateUI();
} }
void MainWindow::onProgress() void MainWindow::onProgress()
@ -425,12 +519,16 @@ void MainWindow::onProgress()
if (remaining < 0) if (remaining < 0)
remaining = 0; remaining = 0;
ui->mRemainingLabel->setText(QString("Remaining: ") + secondsToText(remaining)); auto text = QString("Remaining: ") + secondsToText(remaining);
ui->mRemainingLabel->setText(text);
if (mTrayIcon)
mTrayIcon->setToolTip(text);
if (percents > 100) if (percents > 100)
{ {
mProgressTimer->stop(); mProgressTimer->stop();
onLongBreakEnd(); mPostponeCount = 0; // Reset postpone counter
shiftTo(AppState::Counting);
} }
else else
showMe(); showMe();
@ -438,13 +536,58 @@ void MainWindow::onProgress()
void MainWindow::onNextBreak() void MainWindow::onNextBreak()
{ {
mIdleRemaining = 0; shiftTo(AppState::Break);
}
void MainWindow::onIdleStart()
{
if (mState != AppState::Counting)
return;
// Detected idle
// Timestamp when idle started
mIdleStart = std::chrono::steady_clock::now();
// How much working time remains
mRemainingWorkInterval = mBreakStartTimer->remainingTime() / 1000;
// Stop main & notify timers
mBreakStartTimer->stop();
mBreakNotifyTimer->stop();
qDebug() << "Stop main and notify timers. Remaining time is " << mRemainingWorkInterval.value() << "s";
}
void MainWindow::onIdleEnd()
{
if (mState != AppState::Idle)
return;
mIdleStart.reset(); mIdleStart.reset();
mTimer->stop(); // Update timer(s) duration
mNotifyTimer->stop(); if (mRemainingWorkInterval)
{
qDebug() << "Reset main timer to " << *mRemainingWorkInterval << "s";
mBreakStartTimer->setInterval(std::chrono::seconds(*mRemainingWorkInterval));
onLongBreakStart(); if (mRemainingWorkInterval > INTERVAL_NOTIFICATION)
{
mBreakNotifyTimer->setInterval(std::chrono::seconds(*mRemainingWorkInterval - INTERVAL_NOTIFICATION));
qDebug() << "Reset notify timer to " << *mRemainingWorkInterval - INTERVAL_NOTIFICATION << "s";
}
mRemainingWorkInterval.reset();
}
else
{
mBreakStartTimer->setInterval(std::chrono::seconds(mAppConfig.longbreak_interval));
if (mRemainingWorkInterval > INTERVAL_NOTIFICATION)
mBreakNotifyTimer->setInterval(std::chrono::seconds(mAppConfig.longbreak_interval - INTERVAL_NOTIFICATION));
}
if (!mBreakStartTimer->isActive())
mBreakStartTimer->start();
if (!mBreakNotifyTimer->isActive())
mBreakNotifyTimer->start();
} }
void MainWindow::onSettings() void MainWindow::onSettings()

View File

@ -11,6 +11,15 @@
#include "settings.h" #include "settings.h"
#include "settingsdialog.h" #include "settingsdialog.h"
// Possible app states
enum class AppState
{
None,
Counting,
Idle,
Break
};
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; } namespace Ui { class MainWindow; }
QT_END_NAMESPACE QT_END_NAMESPACE
@ -31,29 +40,31 @@ public:
private: private:
Ui::MainWindow *ui; Ui::MainWindow *ui;
QTimer* mTimer; QTimer* mBreakStartTimer; // Main timer - triggers when break occurs
QTimer* mNotifyTimer; QTimer* mBreakNotifyTimer; // Timer to show notification from system tray
QTimer* mShowNotifyTimer; QTimer* mUpdateUITimer; // Update UI timer - triggers every minute to update UI and checks for idle
QTimer* mUpdateUITimer; QTimer* mProgressTimer; // Break progress timer - updates an UI
QTimer* mProgressTimer;
QSystemTrayIcon* mTrayIcon; QSystemTrayIcon* mTrayIcon;
SettingsDialog* mSettingsDialog; SettingsDialog* mSettingsDialog;
std::chrono::steady_clock::time_point mBreakStartTime; std::chrono::steady_clock::time_point mBreakStartTime;
// How much milliseconds remains for main break
std::optional<int> mRemainingWorkInterval;
app_settings::config mAppConfig; app_settings::config mAppConfig;
int mPostponeCount; int mPostponeCount = 0;
// Time when idle was started // Time when idle was started
std::optional<std::chrono::steady_clock::time_point> mIdleStart; std::optional<std::chrono::steady_clock::time_point> mIdleStart;
// Time remaining in main timer int mLastIdleMilliseconds = 0;
int mIdleRemaining;
int mLastIdleMilliseconds; AppState mState = AppState::None;
void init(); void init();
void loadConfig(); void loadConfig();
void applyConfig(); void applyConfig();
void schedule();
void createTrayIcon(); void createTrayIcon();
void showMe(); void showMe();
void hideMe(); void hideMe();
@ -63,12 +74,17 @@ private:
QIcon getAppIcon(); QIcon getAppIcon();
QIcon getTrayIcon(); QIcon getTrayIcon();
// Function to switch state
void shiftTo(AppState state);
public slots: public slots:
void onUpdateUI(); void onUpdateUI();
void onLongBreakNotify(); void onLongBreakNotify();
void onLongBreakStart(); void onLongBreakStart();
void onLongBreakPostpone(); void onLongBreakPostpone();
void onLongBreakEnd(); void onLongBreakEnd();
void onIdleStart();
void onIdleEnd();
void onProgress(); void onProgress();
void onNextBreak(); void onNextBreak();

View File

@ -1,6 +1,7 @@
QT += core gui svg multimedia QT += core gui svg multimedia widgets dbus
CONFIG += debug_and_release console
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets dbus
CONFIG += c++17 lrelease embed_translations CONFIG += c++17 lrelease embed_translations
@ -52,8 +53,9 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin
unix:LIBS += -L/usr/X11R6/lib/ \ unix:LIBS += -L/usr/X11R6/lib/ \
-lX11 -lXext -lXss -ldl -lX11 -lXext -lXss -ldl
Debug: DEFINES += DEBUG
Release: DEFINES += QT_NO_DEBUG_OUTPUT
# When using wayland: # When using wayland:
# unix:LIBS += -L/usr/local/lib \ # unix:LIBS += -L/usr/local/lib \
# -lwayland-client-unstable++ -lwayland-client-extra++ -lwayland-client++ # -lwayland-client-unstable++ -lwayland-client-extra++ -lwayland-client++

View File

@ -1,7 +1,5 @@
<RCC> <RCC>
<qresource prefix="/"> <qresource prefix="/">
<file>assets/images/app_icon_dark.png</file>
<file>assets/images/app_icon_light.png</file>
<file>assets/misc/qbreak.desktop</file> <file>assets/misc/qbreak.desktop</file>
<file>assets/images/coffee_cup/icon_16x16.png</file> <file>assets/images/coffee_cup/icon_16x16.png</file>
<file>assets/images/coffee_cup/icon_24x24.png</file> <file>assets/images/coffee_cup/icon_24x24.png</file>

View File

@ -27,8 +27,8 @@ const QString Primary_Monitor = "[Primary]";
// Default behavior is not play any audio on break end. // Default behavior is not play any audio on break end.
const QString Audio_Empty = "None"; const QString Audio_Empty = "None";
const QString Audio_Default_1 = "Default 1"; const QString Audio_Retro = "Retro";
const QString Audio_Default_2 = "Default 2"; const QString Audio_Gagarin = "Gagarin";
const QString Audio_Custom = "Custom..."; const QString Audio_Custom = "Custom...";
struct audio_item struct audio_item
@ -38,8 +38,8 @@ struct audio_item
}; };
const std::map<QString, QString> AudioMap { const std::map<QString, QString> AudioMap {
{Audio_Default_1, ":/assets/sound/alarm-retro.wav"}, {Audio_Retro, ":/assets/sound/alarm-retro.wav"},
{Audio_Default_2, ":/assets/sound/alarm-poehali.wav"}, {Audio_Gagarin, ":/assets/sound/alarm-poehali.wav"},
}; };
@ -71,7 +71,7 @@ public:
selected_audio play_audio; selected_audio play_audio;
QString script_on_break_finish; QString script_on_break_finish;
// Zero means "idle is not tracked" // Zero means "idle is not tracked". Value in seconds.
int idle_timeout = Default_Idle_Timeout; int idle_timeout = Default_Idle_Timeout;
}; };

View File

@ -55,7 +55,7 @@ void SettingsDialog::init()
ui->mPreferredMonitorCombobox->setCurrentIndex(found_idx); ui->mPreferredMonitorCombobox->setCurrentIndex(found_idx);
// Fill audio combo box // Fill audio combo box
auto audios = {Audio_Empty, Audio_Default_1, Audio_Default_2, Audio_Custom}; auto audios = {Audio_Empty, Audio_Retro, Audio_Gagarin, Audio_Custom};
ui->mAudioComboBox->addItems(audios); ui->mAudioComboBox->addItems(audios);
mCustomAudioIdx = audios.size() - 1; mCustomAudioIdx = audios.size() - 1;
@ -70,7 +70,7 @@ void SettingsDialog::init()
connect(ui->mAudioComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onAudioIndexChanged(int))); connect(ui->mAudioComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onAudioIndexChanged(int)));
// Idle timeout // Idle timeout
ui->mIdleTimeoutSpinbox->setValue(c.idle_timeout); ui->mIdleTimeoutSpinbox->setValue(c.idle_timeout / 60);
mSkipAudioChangeEvent = false; mSkipAudioChangeEvent = false;
} }
@ -90,7 +90,7 @@ void SettingsDialog::accept()
if (c.play_audio.name == Audio_Custom) if (c.play_audio.name == Audio_Custom)
c.play_audio.path = mCustomAudioPath; c.play_audio.path = mCustomAudioPath;
c.idle_timeout = ui->mIdleTimeoutSpinbox->value(); c.idle_timeout = ui->mIdleTimeoutSpinbox->value() * 60;
app_settings::save(c); app_settings::save(c);
emit accepted(); emit accepted();

View File

@ -131,7 +131,7 @@
<item row="9" column="0"> <item row="9" column="0">
<widget class="QLabel" name="label_8"> <widget class="QLabel" name="label_8">
<property name="text"> <property name="text">
<string>Command to run when break finish</string> <string>Shell command to run when break finish</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -2,11 +2,11 @@
# I use this script on two different hosts so there are logic to find proper Qt installation # I use this script on two different hosts so there are logic to find proper Qt installation
export QT_HOME=/home/$USER/qt/5.12.10/gcc_64 export QT_HOME=/home/$USER/tools/qt/5.15.2/gcc_64
if [ ! -d "$QT_HOME" ] ; then if [ ! -d "$QT_HOME" ] ; then
export QT_HOME=/home/$USER/qt5/5.12.12/gcc_64 export QT_HOME=/home/$USER/tools/qt/5.15.2/gcc_64
fi fi
# Build .appimage # Build .appimage
/usr/bin/python3 build_qbreak.py /usr/bin/python3 build_qbreak.py release

12
scripts/build_linux_debug.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
# I use this script on two different hosts so there are logic to find proper Qt installation
export QT_HOME=/home/$USER/tools/qt/5.15.2/gcc_64
if [ ! -d "$QT_HOME" ] ; then
export QT_HOME=/home/$USER/qt/5.15.2/gcc_64
fi
# Build .appimage
/usr/bin/python3 build_qbreak.py debug

12
scripts/build_linux_qt6.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
# I use this script on two different hosts so there are logic to find proper Qt installation
export QT_HOME=/home/$USER/tools/qt/6.8.0/gcc_64
if [ ! -d "$QT_HOME" ] ; then
export QT_HOME=/home/$USER/tools/qt/6.8.0/gcc_64
fi
# Build .appimage
/usr/bin/python3 build_qbreak.py release

View File

@ -0,0 +1,12 @@
#!/bin/bash
# I use this script on two different hosts so there are logic to find proper Qt installation
export QT_HOME=/home/$USER/tools/qt/6.8.0/gcc_64
if [ ! -d "$QT_HOME" ] ; then
export QT_HOME=/home/$USER/tools/qt/6.8.0/gcc_64
fi
# Build .appimage
/usr/bin/python3 build_qbreak.py debug

View File

@ -5,6 +5,7 @@ import platform
import os import os
import shutil import shutil
import glob import glob
import sys
from pathlib import Path from pathlib import Path
import multiprocessing import multiprocessing
import build_utils import build_utils
@ -48,8 +49,13 @@ if platform.system() == 'Linux':
print(f'qmake call failed with code {retcode}') print(f'qmake call failed with code {retcode}')
exit(retcode) exit(retcode)
# Check the requested type of build - debug or release
build_type = 'release'
if len(sys.argv) == 2:
build_type = sys.argv[1]
print('Build...') print('Build...')
retcode = os.system('make -j4') retcode = os.system(f'make {build_type} -j4')
if retcode != 0: if retcode != 0:
print(f'make call failed with code {retcode}') print(f'make call failed with code {retcode}')
exit(retcode) exit(retcode)
@ -78,7 +84,7 @@ if platform.system() == 'Linux':
'-qmake=' + os.environ['QT_HOME'] + '/bin/qmake', '-qmake=' + os.environ['QT_HOME'] + '/bin/qmake',
'-unsupported-allow-new-glibc', '-unsupported-allow-new-glibc',
#'-no-translations', #'-no-translations',
'-extra-plugins=iconengines,platformthemes/libqgtk3.so' '-extra-plugins=iconengines,platformthemes/libqgtk3.so,platforms/libqxcb.so'
] ]
desktop_path = 'appimage_dir/usr/share/applications/qbreak.desktop' desktop_path = 'appimage_dir/usr/share/applications/qbreak.desktop'

Binary file not shown.