From 959109c87858bed70308caff561de1c9762e2aec Mon Sep 17 00:00:00 2001 From: Rob Caelers Date: Fri, 3 Jan 2025 20:07:01 +0100 Subject: [PATCH] Qt update --- CMakeLists.txt | 3 + libs/updater/CMakeLists.txt | 2 +- ui/app/Application.cc | 4 + ui/app/Menus.cc | 3 + ui/app/main.cc | 43 +- .../platforms/windows/WindowsHarpoonLocker.cc | 4 +- ui/app/toolkits/gtkmm/AutoUpdateDialog.hh | 3 - ui/app/toolkits/qt/AutoUpdateDialog.cc | 282 +++++++++++++ ui/app/toolkits/qt/AutoUpdateDialog.hh | 74 ++++ ui/app/toolkits/qt/AutoUpdater.cc | 377 ++++++++++++++++++ ui/app/toolkits/qt/AutoUpdater.hh | 80 ++++ ui/app/toolkits/qt/BreakWindow.cc | 2 + ui/app/toolkits/qt/CMakeLists.txt | 105 +++-- ui/app/toolkits/qt/CrashDialog.cc | 242 +++++++++++ ui/app/toolkits/qt/CrashDialog.hh | 81 ++++ ui/app/toolkits/qt/ExercisesPanel.cc | 7 +- ui/app/toolkits/qt/ExercisesPanel.hh | 2 +- ui/app/toolkits/qt/MicroBreakWindow.cc | 83 +++- ui/app/toolkits/qt/MicroBreakWindow.hh | 1 + ui/app/toolkits/qt/PreferencesDialog.cc | 34 +- ui/app/toolkits/qt/PreferencesDialog.hh | 4 + ui/app/toolkits/qt/Toolkit.cc | 4 + ui/app/toolkits/qt/Toolkit.hh | 11 +- ui/app/toolkits/qt/ToolkitMacOS.cc | 2 +- ui/app/toolkits/qt/ToolkitUnix.cc | 28 +- ui/app/toolkits/qt/ToolkitUnix.hh | 6 +- ui/app/toolkits/qt/ToolkitWindows.cc | 49 ++- ui/app/toolkits/qt/ToolkitWindows.hh | 7 + .../toolkits/qt/dist/windows/CMakeLists.txt | 6 +- ui/app/toolkits/qt/resource.rc.in | 32 ++ 30 files changed, 1501 insertions(+), 80 deletions(-) create mode 100644 ui/app/toolkits/qt/AutoUpdateDialog.cc create mode 100644 ui/app/toolkits/qt/AutoUpdateDialog.hh create mode 100644 ui/app/toolkits/qt/AutoUpdater.cc create mode 100644 ui/app/toolkits/qt/AutoUpdater.hh create mode 100644 ui/app/toolkits/qt/CrashDialog.cc create mode 100644 ui/app/toolkits/qt/CrashDialog.hh create mode 100644 ui/app/toolkits/qt/resource.rc.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 276f98068..fc951860b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1028,6 +1028,7 @@ if (WITH_CRASHPAD) ) FetchContent_MakeAvailable(dump_syms) + set(DUMP_SYMS ${dump_syms_SOURCE_DIR}/target/release/dump_syms.exe) add_custom_target(dump_syms ALL DEPENDS ${DUMP_SYMS}) add_custom_command( @@ -1036,6 +1037,8 @@ if (WITH_CRASHPAD) WORKING_DIRECTORY ${dump_syms_SOURCE_DIR} ) + + # --target x86_64-pc-windows-gnu set(HAVE_CRASHPAD ON) set(HAVE_CRASH_REPORT ON) if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") diff --git a/libs/updater/CMakeLists.txt b/libs/updater/CMakeLists.txt index c7826d9c4..12ff3dce6 100644 --- a/libs/updater/CMakeLists.txt +++ b/libs/updater/CMakeLists.txt @@ -6,7 +6,7 @@ if (HAVE_UNFOLD_AUTO_UPDATE) "Auto-update framework" "MIT" GIT_REPOSITORY https://github.com/rcaelers/unfold.git - GIT_TAG abc833f80b39e256c4a6ab88917ba0a223d0a1fd + GIT_TAG b6519c96d062aad4826b44afa59a624558cfb9dd ) if(NOT unfold_POPULATED) FetchContent_MakeAvailable(unfold) diff --git a/ui/app/Application.cc b/ui/app/Application.cc index a88e6e49f..a9eee0544 100644 --- a/ui/app/Application.cc +++ b/ui/app/Application.cc @@ -490,6 +490,8 @@ Application::hide_break_window() TRACE_ENTRY(); active_break_id = BREAK_ID_NONE; + spdlog::info("Hide break window"); + for (auto &window: prelude_windows) { window->stop(); @@ -504,6 +506,7 @@ Application::hide_break_window() prelude_windows.clear(); toolkit->get_locker()->unlock(); + spdlog::info("Unlocking screen"); } void @@ -529,6 +532,7 @@ Application::show_break_window() if (!break_windows.empty() && GUIConfig::block_mode()() != BlockMode::Off) { TRACE_MSG("Locking screen"); + spdlog::info("Locking screen"); toolkit->get_locker()->lock(); } } diff --git a/ui/app/Menus.cc b/ui/app/Menus.cc index 316ee5ac0..362f17504 100644 --- a/ui/app/Menus.cc +++ b/ui/app/Menus.cc @@ -214,12 +214,15 @@ Menus::on_menu_restbreak_now() void Menus::on_menu_about() { + // int *p = NULL; + // *p = 1; toolkit->show_window(IToolkit::WindowType::About); } void Menus::on_menu_quit() { + spdlog::info("Quitting Workrave"); app->get_toolkit()->terminate(); } diff --git a/ui/app/main.cc b/ui/app/main.cc index e1d07eb90..526bd44ec 100644 --- a/ui/app/main.cc +++ b/ui/app/main.cc @@ -67,7 +67,7 @@ init_logging() logger->flush_on(spdlog::level::critical); spdlog::set_default_logger(logger); - spdlog::set_level(spdlog::level::info); + spdlog::set_level(spdlog::level::debug); spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%^%-5l%$] %v"); spdlog::info("Workrave started"); spdlog::info("Log file: {}", log_file.string()); @@ -88,12 +88,53 @@ init_logging() #endif } +static void +update_keymap() +{ + HKL current_layout; + BOOL changed = FALSE; + int i; + + const int MAXSIZE = 32; + HKL layout_handles[MAXSIZE]; + int n_layouts = GetKeyboardLayoutList(0, nullptr); // this doesn't work on Win7 64Bit + if (n_layouts <= 0 || n_layouts > MAXSIZE) + { + n_layouts = GetKeyboardLayoutList(MAXSIZE, layout_handles); // this seems be slow on some systems + } + else + { + GetKeyboardLayoutList(n_layouts, layout_handles); + } + + spdlog::info("size = {}", n_layouts); + + current_layout = GetKeyboardLayout(0); + + spdlog::info("size = {}", (void *)current_layout); + + for (i = 0; i < n_layouts; ++i) + { + HKL hkl = layout_handles[i]; + + ActivateKeyboardLayout(hkl, 0); + + char layout_name[KL_NAMELENGTH]; + GetKeyboardLayoutNameA(layout_name); + spdlog::info("name = {}", layout_name); + } + + ActivateKeyboardLayout(current_layout, 0); +} + int run(int argc, char **argv) { init_logging(); TRACE_ENTRY(); + update_keymap(); + #if defined(HAVE_CRASH_REPORT) try { diff --git a/ui/app/platforms/windows/WindowsHarpoonLocker.cc b/ui/app/platforms/windows/WindowsHarpoonLocker.cc index c19f4fcce..ed3d4d4f6 100644 --- a/ui/app/platforms/windows/WindowsHarpoonLocker.cc +++ b/ui/app/platforms/windows/WindowsHarpoonLocker.cc @@ -15,6 +15,7 @@ // along with this program. If not, see . // +#include #ifdef HAVE_CONFIG_H # include "config.h" #endif @@ -57,6 +58,7 @@ WindowsHarpoonLocker::prepare_lock() text.resize(GetWindowTextLengthA(active_window)); GetWindowTextA(active_window, text.data(), text.size() + 1); TRACE_MSG("Save active window: {}", text); + spdlog::info("Save active window: {} {}", text, reinterpret_cast(active_window)); } void @@ -77,7 +79,7 @@ WindowsHarpoonLocker::unlock() text.resize(GetWindowTextLengthA(active_window)); GetWindowTextA(active_window, text.data(), text.size() + 1); TRACE_MSG("Restore active window: {}", text); - + spdlog::info("Restore active window: {} {}", text, reinterpret_cast(active_window)); SetForegroundWindow(active_window); active_window = nullptr; } diff --git a/ui/app/toolkits/gtkmm/AutoUpdateDialog.hh b/ui/app/toolkits/gtkmm/AutoUpdateDialog.hh index e64f53c4b..6a8ab514f 100644 --- a/ui/app/toolkits/gtkmm/AutoUpdateDialog.hh +++ b/ui/app/toolkits/gtkmm/AutoUpdateDialog.hh @@ -66,9 +66,6 @@ public: void set_status(std::string status); void start_install(); -private: - void on_auto_toggled(); - private: update_choice_callback_t callback; Gtk::TextView *text_view{nullptr}; diff --git a/ui/app/toolkits/qt/AutoUpdateDialog.cc b/ui/app/toolkits/qt/AutoUpdateDialog.cc new file mode 100644 index 000000000..7b468909a --- /dev/null +++ b/ui/app/toolkits/qt/AutoUpdateDialog.cc @@ -0,0 +1,282 @@ +// Copyright (C) 2025 Rob Caelers +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#define WIN32_LEAN_AND_MEAN + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "AutoUpdateDialog.hh" + +#include +#include +#include + +#include "cmark.h" + +#include "ui/GUIConfig.hh" + +#ifdef PLATFORM_OS_WINDOWS +# include "ToolkitWindows.hh" +#endif + +static constexpr const char *doc = + R"( + + + +{} + + +
+ {} +
+ +)"; + +static constexpr const char *lightstyle = + R"( +)"; + +static constexpr const char *darkstyle = + R"( +)"; + +AutoUpdateDialog::AutoUpdateDialog(std::shared_ptr info, AutoUpdateDialog::update_choice_callback_t callback) + : callback(std::move(callback)) +{ + setWindowTitle(tr("Software Update")); + // resize(800, 600); + setMinimumSize(800, 600); + + auto *main_layout = new QVBoxLayout(); + setLayout(main_layout); + + auto *content_area = new QHBoxLayout(); + main_layout->addLayout(content_area); + + auto *logo_box = new QVBoxLayout(); + content_area->addLayout(logo_box); + + try + { + auto pix = QPixmap(":/workrave/workrave.png"); + auto *logo = new QLabel(); + logo->setPixmap(pix); + logo_box->addWidget(logo); + } + catch (const std::exception &e) + { + spdlog::info("error loading image {}", std::string(e.what())); + } + + auto *update_info_box = new QVBoxLayout(); + content_area->addLayout(update_info_box); + + QString bold = ""; + QString end = ""; + + auto *title_label = new QLabel(bold + tr("A new version of %1 is available").arg(QString::fromStdString(info->title)) + end); + update_info_box->addWidget(title_label); + + auto *info_label = new QLabel(tr("%1 %2 is now available -- you have %3. Would you like to download it now?") + .arg(QString::fromStdString(info->title)) + .arg(QString::fromStdString(info->version)) + .arg(QString::fromStdString(info->current_version))); + info_label->setWordWrap(true); + update_info_box->addWidget(info_label); + + auto *notes_label = new QLabel(bold + tr("Release notes") + end); + update_info_box->addWidget(notes_label); + + auto *notes_frame = new QFrame(); + notes_frame->setFrameShadow(QFrame::Sunken); + notes_frame->setFrameShape(QFrame::StyledPanel); + update_info_box->addWidget(notes_frame); + + auto *notes_box = new QVBoxLayout(notes_frame); + + web = new QTextBrowser(); + web->setOpenExternalLinks(true); + + std::string body; + for (const auto ¬e: info->release_notes) + { + body += fmt::format(fmt::runtime(tr("

Version {}

\n").toStdString()), note.version); + auto *html = cmark_markdown_to_html(note.markdown.c_str(), note.markdown.length(), CMARK_OPT_DEFAULT); + + if (html != nullptr) + { + spdlog::info("body: {}", html); + body += html; + free(html); + } + } + + bool dark = false; + switch (GUIConfig::light_dark_mode()()) + { + case LightDarkTheme::Light: + dark = false; + break; + case LightDarkTheme::Dark: + dark = true; + break; + case LightDarkTheme::Auto: +#ifdef PLATFORM_OS_WINDOWS + dark = ToolkitWindows::is_windows_app_theme_dark(); +#endif + break; + } + + web->setHtml(QString::fromStdString(fmt::format(doc, dark ? darkstyle : lightstyle, body))); + notes_box->addWidget(web); + + auto *status_box = new QHBoxLayout(); + main_layout->addLayout(status_box); + + status_label = new QLabel(); + status_box->addWidget(status_label); + + progress_bar_frame = new QFrame(); + progress_bar_frame->setFrameShadow(QFrame::Sunken); + progress_bar_frame->setFrameShape(QFrame::StyledPanel); + auto *progress_bar_box = new QVBoxLayout(progress_bar_frame); + + status_box->addWidget(progress_bar_frame); + + progress_bar = new QProgressBar(); + progress_bar->setOrientation(Qt::Horizontal); + progress_bar_box->addWidget(progress_bar); + + auto *bottom_box = new QHBoxLayout(); + main_layout->addLayout(bottom_box); + + left_button_box = new QHBoxLayout(); + bottom_box->addLayout(left_button_box); + + right_button_box = new QHBoxLayout(); + bottom_box->addLayout(right_button_box); + + close_button_box = new QHBoxLayout(); + bottom_box->addLayout(close_button_box); + + skip_button = new QPushButton(tr("Skip this version")); + connect(skip_button, &QPushButton::clicked, [callback]() { callback(UpdateChoice::Skip); }); + left_button_box->addWidget(skip_button); + + remind_button = new QPushButton(tr("Remind me later")); + connect(remind_button, &QPushButton::clicked, [callback]() { callback(UpdateChoice::Later); }); + right_button_box->addWidget(remind_button); + + install_button = new QPushButton(tr("Install update")); + connect(install_button, &QPushButton::clicked, [callback]() { callback(UpdateChoice::Now); }); + right_button_box->addWidget(install_button); + + close_button = new QPushButton(tr("Close")); + connect(close_button, &QPushButton::clicked, this, &QDialog::close); + close_button_box->addWidget(close_button); + + // connect(this, &QDialog::closeEvent, [callback]() { callback(UpdateChoice::Later); }); + + install_button->setEnabled(true); + progress_bar->setVisible(false); + progress_bar_frame->setVisible(false); + close_button->setVisible(false); +} + +void +AutoUpdateDialog::set_progress_visible(bool visible) +{ + progress_bar->setVisible(visible); + progress_bar_frame->setVisible(true); +} + +void +AutoUpdateDialog::set_stage(unfold::UpdateStage stage, double progress) +{ + if (!current_stage || *current_stage != stage) + { + spdlog::info("Update stage: {}", static_cast(stage)); + current_stage = stage; + progress_bar->setVisible(stage == unfold::UpdateStage::DownloadInstaller); + progress_bar_frame->setVisible(stage == unfold::UpdateStage::DownloadInstaller); + + switch (stage) + { + case unfold::UpdateStage::DownloadInstaller: + status_label->setText(tr("Downloading installer")); + break; + case unfold::UpdateStage::VerifyInstaller: + status_label->setText(tr("Verifying installer")); + break; + case unfold::UpdateStage::RunInstaller: + status_label->setText(tr("Running installer")); + break; + default: + break; + } + } + + if (fabs(progress - progress_bar->value() / 100.0) >= 0.01) + { + progress_bar->setValue(static_cast(progress * 100)); + } + + if (progress >= 1.0) + { + progress_bar->setVisible(false); + progress_bar_frame->setVisible(false); + } +} + +void +AutoUpdateDialog::set_status(const std::string &status) +{ + spdlog::info("Update status: {}", status); + status_label->setText(QString::fromStdString(status)); + progress_bar->setVisible(false); + progress_bar_frame->setVisible(false); +} + +void +AutoUpdateDialog::start_install() +{ + status_label->setText(""); + skip_button->setVisible(false); + remind_button->setVisible(false); + progress_bar->setVisible(true); + progress_bar_frame->setVisible(true); + install_button->setVisible(true); +} diff --git a/ui/app/toolkits/qt/AutoUpdateDialog.hh b/ui/app/toolkits/qt/AutoUpdateDialog.hh new file mode 100644 index 000000000..891e3fec6 --- /dev/null +++ b/ui/app/toolkits/qt/AutoUpdateDialog.hh @@ -0,0 +1,74 @@ +// Copyright (C) 2025 Rob Caelers +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#ifndef UPDATE_DIALOG_HH +#define UPDATE_DIALOG_HH + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "unfold/Unfold.hh" + +class AutoUpdateDialog : public QDialog +{ + Q_OBJECT + +public: + enum class UpdateChoice + { + Skip, + Later, + Now + }; + using update_choice_callback_t = std::function; + + AutoUpdateDialog(std::shared_ptr info, update_choice_callback_t callback); + ~AutoUpdateDialog() override = default; + + void set_progress_visible(bool visible); + void set_stage(unfold::UpdateStage stage, double progress); + void set_status(const std::string &status); + void start_install(); + +private: + update_choice_callback_t callback; + QTextEdit *text_view{nullptr}; + QScrollArea *scrolled_window{nullptr}; + QFrame *progress_bar_frame{nullptr}; + QProgressBar *progress_bar{nullptr}; + QLabel *status_label{nullptr}; + QHBoxLayout *left_button_box{nullptr}; + QHBoxLayout *right_button_box{nullptr}; + QHBoxLayout *close_button_box{nullptr}; + QPushButton *install_button{nullptr}; + QPushButton *close_button{nullptr}; + QPushButton *skip_button{nullptr}; + QPushButton *remind_button{nullptr}; + + std::optional current_stage; + QTextBrowser *web{nullptr}; +}; + +#endif // UPDATE_DIALOG_HH diff --git a/ui/app/toolkits/qt/AutoUpdater.cc b/ui/app/toolkits/qt/AutoUpdater.cc new file mode 100644 index 000000000..6bf125b6e --- /dev/null +++ b/ui/app/toolkits/qt/AutoUpdater.cc @@ -0,0 +1,377 @@ +// Copyright (C) 2022 Rob Caelers +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include + +#include "AutoUpdater.hh" + +#include +#include + +#include "commonui/nls.h" +#include "debug.hh" +#include "unfold/Unfold.hh" +#include "unfold/UnfoldErrors.hh" +#include "updater/Config.hh" + +#include "AutoUpdateDialog.hh" + +AutoUpdater::AutoUpdater(std::shared_ptr context) + : context(context) + , scheduler(&main_thread, io_context.get_io_context()) + , updater(unfold::Unfold::create(io_context)) + +{ + TRACE_ENTRY(); + + workrave::updater::Config::init(context->get_configurator()); + + auto rc = updater->set_appcast("https://snapshots.workrave.org/snapshots/v1.11-qt/appcast.xml"); + // rc = updater->set_appcast("https://snapshots.workrave.org/snapshots/staging/v1.11/appcast.xml"); + if (!rc) + { + logger->error("Invalid appcast URL"); + return; + } + + rc = updater->set_signature_verification_key("MCowBQYDK2VwAyEAZ1I+iYYFpFMPcSj15BnHl6x7uow2CdxT0t2BmUzMGXk="); + + if (!rc) + { + logger->error("Invalid signature key"); + return; + } + + rc = updater->set_current_version(WORKRAVE_VERSION); + if (!rc) + { + logger->error("Invalid version"); + return; + } + + updater->set_configuration_prefix("Software\\Workrave"); + + init_channels(); + + updater->set_update_available_callback( + [&]() -> boost::asio::awaitable { return on_update_available(); }); + + updater->set_periodic_update_check_interval(std::chrono::hours{24}); + updater->set_periodic_update_check_enabled(workrave::updater::Config::enabled()()); + updater->set_proxy(workrave::updater::Config::proxy_type()()); + updater->set_custom_proxy(workrave::updater::Config::proxy()()); + + rc = updater->set_priority(workrave::updater::Config::priority()()); + if (!rc) + { + logger->error("Invalid priority"); + workrave::updater::Config::priority()(0); + return; + } + + init_preferences(); + init_menu(); + + workrave::updater::Config::enabled().connect(this, [this](auto enabled) { + logger->info("Enabled changed to {}", enabled); + updater->set_periodic_update_check_enabled(enabled); + }); + + workrave::updater::Config::channel().connect(this, [this](auto channel) { + logger->info("Channel changed to {}", workrave::utils::enum_to_string(channel)); + init_channels(); + }); + + workrave::updater::Config::proxy_type().connect(this, [this](auto proxy_type) { + logger->info("Proxy type changed to {}", workrave::utils::enum_to_string(proxy_type)); + updater->set_proxy(proxy_type); + }); + + workrave::updater::Config::proxy().connect(this, [this](auto proxy) { + logger->info("Proxy changed to {}", proxy); + updater->set_custom_proxy(proxy); + }); + + workrave::updater::Config::priority().connect(this, [this](auto priority) { + auto rc = updater->set_priority(priority); + if (!rc) + { + logger->error("Invalid priority"); + } + else + { + logger->info("Priority changed to {}, active priority {}", + priority == 1 ? "high" : "normal", + updater->get_active_priority()); + } + }); + + updater->set_update_status_callback([&](unfold::outcome::std_result state) -> void { set_error_state(state); }); + + updater->set_download_progress_callback([this](unfold::UpdateStage stage, double p) -> void { + this->progress = p; + unfold::coro::qttask task = [this](unfold::UpdateStage stage) -> unfold::coro::qttask { + if (dialog) + { + dialog->set_stage(stage, progress); + } + co_return; + }(stage); + scheduler.spawn(std::move(task)); + }); + + logger->info("Auto updater initialized, channel: {} prio: {}", + workrave::utils::enum_to_string(workrave::updater::Config::channel()()), + updater->get_active_priority()); +} + +AutoUpdater::~AutoUpdater() +{ + TRACE_ENTRY(); +} + +void +AutoUpdater::init_channels() +{ + TRACE_ENTRY(); + + auto channel = workrave::updater::Config::channel()(); + + std::vector allowed_channels; + + switch (channel) + { + case workrave::updater::Channel::Beta: + allowed_channels.emplace_back("beta"); + [[fallthrough]]; + + case workrave::updater::Channel::Candidate: + allowed_channels.emplace_back("rc"); + [[fallthrough]]; + + case workrave::updater::Channel::Stable: + allowed_channels.emplace_back("stable"); + break; + } + + auto rc = updater->set_allowed_channels(allowed_channels); + if (!rc) + { + logger->error("Invalid allowed channels"); + } +} + +void +AutoUpdater::init_preferences() +{ + std::vector channels{_("Stable"), _("Release Candidate"), _("Beta")}; + std::vector proxy_types{_("No proxy"), _("System proxy"), _("Custom proxy")}; + + auto_update_def = ui::prefwidgets::PanelDef::create("auto-update", "auto-update", N_("Software updates")) + << (ui::prefwidgets::Frame::create(N_("Auto update")) + << ui::prefwidgets::Toggle::create(N_("Automatically check for updates")) + ->connect(&workrave::updater::Config::enabled()) + << ui::prefwidgets::Toggle::create(N_("Get updates as soon as they are available")) + ->connect(&workrave::updater::Config::priority(), + []() { + auto priority = workrave::updater::Config::priority()(); + return priority == 1; + }) + ->when(&workrave::updater::Config::enabled()) + ->on_save([](bool first) { workrave::updater::Config::priority().set(first ? 1 : 0); }) + << ui::prefwidgets::Choice::create(N_("Release channel:")) + ->connect(&workrave::updater::Config::channel(), + {{workrave::updater::Channel::Stable, 0}, + {workrave::updater::Channel::Candidate, 1}, + {workrave::updater::Channel::Beta, 2}}) + ->assign(channels) + ->when(&workrave::updater::Config::enabled()) + << ui::prefwidgets::Choice::create(N_("Proxy Type:")) + ->connect( + &workrave::updater::Config::proxy_type(), + {{unfold::ProxyType::None, 0}, {unfold::ProxyType::System, 1}, {unfold::ProxyType::Custom, 2}}) + ->assign(proxy_types) + ->when(&workrave::updater::Config::enabled()) + << ui::prefwidgets::Entry::create(N_("Proxy:")) + ->connect(&workrave::updater::Config::proxy()) + ->when(&workrave::updater::Config::proxy_type(), + [](unfold::ProxyType t) { return t == unfold::ProxyType::Custom; })); + + context->get_preferences_registry()->add_page("auto-update", N_("Software updates"), "workrave-update-symbolic"); + context->get_preferences_registry()->add(auto_update_def); +} + +void +AutoUpdater::init_menu() +{ + auto menu_model = context->get_menu_model(); + auto section = menu_model->find_section("workrave.section.tail"); + auto item = menus::ActionNode::create(CHECK_FOR_UPDATE, N_("Check for _Updates"), [this] { on_check_for_update(); }); + section->add_before(item, "workrave.about"); + menu_model->update(); +} + +unfold::coro::qttask +AutoUpdater::show_update() +{ + auto update_info = updater->get_update_info(); + if (!update_info) + { + co_return; + } + + dialog = std::make_shared(update_info, [this](auto choice) { + auto response = unfold::UpdateResponse::Later; + switch (choice) + { + case AutoUpdateDialog::UpdateChoice::Now: + { + response = unfold::UpdateResponse::Install; + unfold::coro::qttask task = [this]() -> unfold::coro::qttask { + if (dialog) + { + dialog->start_install(); + } + co_return; + }(); + scheduler.spawn(std::move(task)); + } + break; + + case AutoUpdateDialog::UpdateChoice::Later: + response = unfold::UpdateResponse::Later; + dialog->close(); + break; + + case AutoUpdateDialog::UpdateChoice::Skip: + response = unfold::UpdateResponse::Skip; + dialog->close(); + break; + } + if (dialog_handler) + { + (*dialog_handler)(response); + dialog_handler.reset(); + } + }); + //dialog->signal_hide().connect([this]() { dialog.reset(); }); + dialog->show(); + dialog->raise(); +} + +boost::asio::awaitable +AutoUpdater::on_update_available() +{ + logger->info("Update available"); + + if (dialog_handler) + { + logger->error("Update dialog already open"); + co_return unfold::UpdateResponse::Later; + } + + auto response = co_await boost::asio::async_initiate( + [this](auto &&handler) { + dialog_handler.emplace(std::forward(handler)); + unfold::coro::qttask task = show_update(); + scheduler.spawn(std::move(task)); + }, + boost::asio::use_awaitable); + + co_return response; +} + +void +AutoUpdater::on_check_for_update() +{ + boost::asio::co_spawn( + *io_context.get_io_context(), + [&]() -> boost::asio::awaitable { + try + { + auto rc = co_await updater->check_for_update_and_notify(); + if (!rc) + { + logger->error("Check for update failed"); + } + } + catch (std::exception &e) + { + logger->error("Exception in on_check_for_update: {}", e.what()); + } + }, + boost::asio::detached); +} + +void +AutoUpdater::set_error_state(unfold::outcome::std_result state) +{ + logger->error("Error state {}", state.error().message()); + auto errc = unfold::to_errc(state.error()); + + if (errc) + { + switch (*errc) + { + case unfold::UnfoldErrc::Success: + break; + case unfold::UnfoldErrc::InvalidAppcast: + set_status(_("Invalid appcast")); + break; + case unfold::UnfoldErrc::AppcastDownloadFailed: + set_status(_("Failed to download appcast")); + break; + case unfold::UnfoldErrc::InstallerDownloadFailed: + set_status(_("Failed to download installer")); + break; + case unfold::UnfoldErrc::InstallerVerificationFailed: + set_status(_("Failed to validate installer integrity. Aborting upgrade")); + break; + case unfold::UnfoldErrc::InstallerExecutionFailed: + set_status(_("Failed to execute installer")); + break; + case unfold::UnfoldErrc::InvalidArgument: + case unfold::UnfoldErrc::InternalError: + default: + set_status(_("Internal failure. Aborting upgrade")); + break; + } + } + else + { + set_status(_("Internal failure. Aborting upgrade")); + } +} + +void +AutoUpdater::set_status(std::string status) +{ + unfold::coro::qttask task = [this](std::string status) -> unfold::coro::qttask { + if (dialog) + { + dialog->set_status(status); + } + co_return; + }(status); + scheduler.spawn(std::move(task)); +} diff --git a/ui/app/toolkits/qt/AutoUpdater.hh b/ui/app/toolkits/qt/AutoUpdater.hh new file mode 100644 index 000000000..13568ff74 --- /dev/null +++ b/ui/app/toolkits/qt/AutoUpdater.hh @@ -0,0 +1,80 @@ +// Copyright (C) 2022 Rob Caelers +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#ifndef AUTO_UPDATER_HH +#define AUTO_UPDATER_HH + +#include +#include + +#include "unfold/Unfold.hh" +#include "unfold/coro/qttask.hh" +#include "unfold/coro/IOContext.hh" + +#include "ui/Plugin.hh" +#include "ui/prefwidgets/Widgets.hh" + +#include "utils/Logging.hh" +#include "utils/Signals.hh" + +class AutoUpdateDialog; + +class AutoUpdater + : public Plugin + , public workrave::utils::Trackable +{ +public: + explicit AutoUpdater(std::shared_ptr context); + ~AutoUpdater() override; + + std::string get_plugin_id() const override + { + return "workrave.AutoUpdater"; + } + +private: + void init_channels(); + void init_preferences(); + void init_menu(); + boost::asio::awaitable on_update_available(); + void on_check_for_update(); + unfold::coro::qttask show_update(); + + void set_error_state(unfold::outcome::std_result state); + void set_status(std::string status); + +private: + using handler_type = boost::asio::async_result::type, + void(unfold::UpdateResponse)>::handler_type; + + QObject main_thread; + std::shared_ptr context; + unfold::coro::IOContext io_context; + unfold::coro::qt::scheduler scheduler; + std::shared_ptr updater; + std::shared_ptr dialog; + std::optional dialog_handler; + + std::shared_ptr auto_update_def; + double progress{0.0}; + + using sv = std::string_view; + static constexpr std::string_view CHECK_FOR_UPDATE = sv("workrave.check_for_updates"); + std::shared_ptr logger{workrave::utils::Logging::create("updater")}; +}; + +#endif // AUTO_UPDATER_HH diff --git a/ui/app/toolkits/qt/BreakWindow.cc b/ui/app/toolkits/qt/BreakWindow.cc index 17a1a1050..687a2c2a9 100644 --- a/ui/app/toolkits/qt/BreakWindow.cc +++ b/ui/app/toolkits/qt/BreakWindow.cc @@ -21,6 +21,8 @@ #include "BreakWindow.hh" +#include + #include #include #include diff --git a/ui/app/toolkits/qt/CMakeLists.txt b/ui/app/toolkits/qt/CMakeLists.txt index 36758680b..5f77f885b 100644 --- a/ui/app/toolkits/qt/CMakeLists.txt +++ b/ui/app/toolkits/qt/CMakeLists.txt @@ -1,4 +1,6 @@ -add_library(workrave-toolkit-qt OBJECT +add_library(workrave-toolkit-qt OBJECT) + +target_sources(workrave-toolkit-qt PRIVATE AboutDialog.cc BreakWindow.cc DailyLimitWindow.cc @@ -7,10 +9,6 @@ add_library(workrave-toolkit-qt OBJECT ExercisesPanel.cc MainWindow.cc MicroBreakWindow.cc - preferences/GeneralUiPreferencesPanel.cc - preferences/SoundsPreferencesPanel.cc - preferences/TimerBoxPreferencesPanel.cc - preferences/TimerPreferencesPanel.cc PreferencesDialog.cc PreludeWindow.cc RestBreakWindow.cc @@ -18,29 +16,23 @@ add_library(workrave-toolkit-qt OBJECT Toolkit.cc ToolkitFactory.cc ToolkitMenu.cc + preferences/GeneralUiPreferencesPanel.cc + preferences/SoundsPreferencesPanel.cc + preferences/TimerBoxPreferencesPanel.cc + preferences/TimerPreferencesPanel.cc utils/DataConnector.cc - utils/qformat.cc utils/SizeGroup.cc utils/Ui.cc utils/UiUtil.cc + utils/qformat.cc widgets/Frame.cc + widgets/Icon.cc widgets/IconListNotebook.cc widgets/StatusIcon.cc widgets/TimeBar.cc widgets/TimeEntry.cc widgets/TimerBoxView.cc - widgets/Icon.cc - ) - -if (PLATFORM_OS_MACOS) - target_sources(workrave-toolkit-qt PRIVATE - ToolkitMacOS.cc - platforms/macos/MouseMonitor.cc - platforms/macos/MacOSDesktopWindow.cc) - - set_source_files_properties(BreakWindow.cc PreludeWindow.cc ToolkitMacOS.cc platforms/macos/MouseMonitor.cc platforms/macos/MacOSDesktopWindow.cc PROPERTIES COMPILE_FLAGS "-x objective-c++ -fobjc-arc") - target_include_directories(workrave-toolkit-qt PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos) -endif() +) if (PLATFORM_OS_UNIX) target_sources(workrave-toolkit-qt PRIVATE ToolkitLinux.cc) @@ -53,8 +45,19 @@ if (PLATFORM_OS_WINDOWS) target_link_libraries(workrave-toolkit-qt PUBLIC Unfold::unfold cmark webview2 ws2_32 mswsock) endif() +if (PLATFORM_OS_MACOS) + target_sources(workrave-toolkit-qt PRIVATE + ToolkitMacOS.cc + platforms/macos/MouseMonitor.cc + platforms/macos/MacOSDesktopWindow.cc) + + set_source_files_properties(BreakWindow.cc PreludeWindow.cc ToolkitMacOS.cc platforms/macos/MouseMonitor.cc platforms/macos/MacOSDesktopWindow.cc PROPERTIES COMPILE_FLAGS "-x objective-c++ -fobjc-arc") + target_include_directories(workrave-toolkit-qt PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos) +endif() + set(MOC_SOURCES AboutDialog.hh + AutoUpdateDialog.hh BreakWindow.hh DailyLimitWindow.hh DebugDialog.hh @@ -62,16 +65,16 @@ set(MOC_SOURCES ExercisesPanel.hh MainWindow.hh MicroBreakWindow.hh - preferences/GeneralUiPreferencesPanel.hh - preferences/SoundsPreferencesPanel.hh - preferences/TimerBoxPreferencesPanel.hh - preferences/TimerPreferencesPanel.hh PreferencesDialog.hh PreludeWindow.hh RestBreakWindow.hh StatisticsDialog.hh Toolkit.hh ToolkitMenu.hh + preferences/GeneralUiPreferencesPanel.hh + preferences/SoundsPreferencesPanel.hh + preferences/TimerBoxPreferencesPanel.hh + preferences/TimerPreferencesPanel.hh utils/SizeGroup.hh widgets/Frame.hh widgets/IconListNotebook.hh @@ -99,6 +102,17 @@ target_link_directories(workrave-toolkit-qt PRIVATE ${X11_INCLUDE_PATH} ${X11_INCLUDE_PATH}) +if (HAVE_UNFOLD_AUTO_UPDATE) + target_link_libraries(workrave-toolkit-qt PUBLIC Unfold::unfold cmark) + target_compile_definitions(workrave-toolkit-qt PRIVATE CMARK_NO_SHORT_NAMES) + + target_sources( + workrave-toolkit-qt + PRIVATE + AutoUpdater.cc + AutoUpdateDialog.cc) +endif() + target_link_libraries(workrave-toolkit-qt PRIVATE workrave-app workrave-libs-audio @@ -117,10 +131,55 @@ target_link_libraries(workrave-toolkit-qt PRIVATE ${EXTRA_LIBRARIES} ${X11_X11_LIB} ${X11_Xtst_LIB} - ${X11_Xss_LIB}) + ${X11_Xss_LIB} + fmt::fmt) + +if (HAVE_UNFOLD_AUTO_UPDATE) + target_link_libraries(workrave-toolkit-qt PRIVATE workrave-libs-updater) +endif() if (HAVE_GLIB) target_include_directories(workrave-toolkit-qt PRIVATE ${GLIB_INCLUDE_DIRS}) target_link_directories(workrave-toolkit-qt PRIVATE ${GLIB_LIBPATH}) target_link_libraries(workrave-toolkit-qt PRIVATE ${GLIB_LIBRARIES}) endif() + +if (HAVE_CRASH_REPORT) + add_executable(WorkraveCrashHandler CrashDialog.cc) + + set(WR_VERSION ${WORKRAVE_VERSION}) + set(WR_RESOURCE_VERSION ${WORKRAVE_RESOURCE_VERSION}) + set(WR_TOP_SOURCE_DIR ${CMAKE_SOURCE_DIR}) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/resource.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/resource.rc) + + target_sources(WorkraveCrashHandler PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/resource.rc) + + target_include_directories(WorkraveCrashHandler PRIVATE ${GTK_INCLUDE_DIRS}) + target_link_directories(WorkraveCrashHandler PRIVATE ${GTK_LIBRARY_DIRS}) + target_include_directories(WorkraveCrashHandler PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/commonui/include) + + target_link_libraries(WorkraveCrashHandler + PRIVATE + crashpad::handler_lib + crashpad::tools + workrave-libs-utils + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Svg + Qt${QT_VERSION_MAJOR}::Xml + ${LIBINTL_LIBRARIES} + ) + + if (MSVC) + target_link_options(WorkraveCrashHandler PRIVATE "/SUBSYSTEM:WINDOWS") + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set_target_properties(WorkraveCrashHandler PROPERTIES COMPILE_FLAGS "-municode") + set_target_properties(WorkraveCrashHandler PROPERTIES LINK_FLAGS "-Wl,--subsystem,windows -municode") + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set_target_properties(WorkraveCrashHandler PROPERTIES COMPILE_FLAGS "-mwindows -municode") + set_target_properties(WorkraveCrashHandler PROPERTIES LINK_FLAGS "-mwindows -municode") + endif() + + install(TARGETS WorkraveCrashHandler RUNTIME DESTINATION "${BINDIR}") +endif() diff --git a/ui/app/toolkits/qt/CrashDialog.cc b/ui/app/toolkits/qt/CrashDialog.cc new file mode 100644 index 000000000..39ce2e440 --- /dev/null +++ b/ui/app/toolkits/qt/CrashDialog.cc @@ -0,0 +1,242 @@ +// Copyright (C) 2020-2021 Rob Caelers +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#include "CrashDialog.hh" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "base/logging.h" +#include "handler/handler_main.h" +#include "build/build_config.h" +#include "tools/tool_support.h" + +#include "commonui/nls.h" +#include "utils/StringUtils.hh" + +CrashDetailsDialog::CrashDetailsDialog(const std::vector &attachments, QWidget *parent) + : QDialog(parent) +{ + setWindowTitle("Crash report details"); + setMinimumSize(600, 400); + + vbox = new QVBoxLayout(this); + + QLabel *info_label = new QLabel("Workrave has crashed.", this); + vbox->addWidget(info_label); + + if (!attachments.empty()) + { + QLabel *attachments_label = new QLabel("The following logging will be attached to the crash report:", this); + vbox->addWidget(attachments_label); + + scroll_area = new QScrollArea(this); + scroll_area->setWidgetResizable(true); + vbox->addWidget(scroll_area); + + text_edit = new QTextEdit(this); + text_edit->setReadOnly(true); + scroll_area->setWidget(text_edit); + + for (const auto &a: attachments) + { + std::ifstream f(workrave::utils::utf16_to_utf8(a.value()).c_str()); + if (f.is_open()) + { + text_edit->append(QString::fromStdString(workrave::utils::utf16_to_utf8(a.BaseName().value()) + ":\n\n")); + + std::string line; + while (std::getline(f, line)) + { + text_edit->append(QString::fromStdString(line)); + } + text_edit->append("\n"); + } + } + } + + QLabel *more_info_label = new QLabel( + "Note that this crash report also contains technical information about the state of Workrave when it crashed.", + this); + vbox->addWidget(more_info_label); + + QPushButton *close_button = new QPushButton("Close", this); + connect(close_button, &QPushButton::clicked, this, &QDialog::accept); + vbox->addWidget(close_button); +} + +QVBoxLayout * +create_indented_box(QVBoxLayout *container) +{ + QHBoxLayout *ibox = new QHBoxLayout(); + container->addLayout(ibox); + + QLabel *indent_lab = new QLabel(" "); + ibox->addWidget(indent_lab); + + QVBoxLayout *box = new QVBoxLayout(); + ibox->addLayout(box); + box->setSpacing(6); + return box; +} + +CrashDialog::CrashDialog(const std::map &annotations, + const std::vector &attachments, + QWidget *parent) + : QDialog(parent) + , details_dlg(new CrashDetailsDialog(attachments, this)) +{ + setWindowTitle("Workrave crash reporter"); + setMinimumSize(600, 400); + + vbox = new QVBoxLayout(this); + + QLabel *title_label = new QLabel("Workrave has crashed.", this); + vbox->addWidget(title_label); + + QLabel *info_label = new QLabel( + "Workrave encountered a problem and crashed. Please help us to diagnose and fix this problem by sending a crash report.", + this); + info_label->setWordWrap(true); + vbox->addWidget(info_label); + + submit_cb = new QCheckBox("Submit crash report to the Workrave developers", this); + connect(submit_cb, &QCheckBox::toggled, this, &CrashDialog::on_submit_toggled); + vbox->addWidget(submit_cb); + + QVBoxLayout *ibox = create_indented_box(vbox); + + QPushButton *details_btn = new QPushButton("Details...", this); + connect(details_btn, &QPushButton::clicked, this, &CrashDialog::on_details_clicked); + ibox->addWidget(details_btn); + + user_text_frame = new QFrame(this); + user_text_frame->setFrameShape(QFrame::StyledPanel); + ibox->addWidget(user_text_frame); + + QVBoxLayout *frame_layout = new QVBoxLayout(user_text_frame); + text_edit = new QTextEdit(this); + frame_layout->addWidget(text_edit); + + QPushButton *close_button = new QPushButton("Close", this); + connect(close_button, &QPushButton::clicked, this, &QDialog::accept); + vbox->addWidget(close_button); + + submit_cb->setChecked(true); + on_submit_toggled(); +} + +void +CrashDialog::on_submit_toggled() +{ + bool enabled = submit_cb->isChecked(); + user_text_frame->setEnabled(enabled); +} + +void +CrashDialog::on_details_clicked() +{ + details_dlg->show(); +} + +std::string +CrashDialog::get_user_text() const +{ + return text_edit->toPlainText().toStdString(); +} + +bool +CrashDialog::get_consent() const +{ + return submit_cb->isChecked(); +} + +bool +UserInteraction::requestUserConsent(const std::map &annotations, + const std::vector &attachments) +{ + SetEnvironmentVariableA("GTK_DEBUG", 0); + SetEnvironmentVariableA("G_MESSAGES_DEBUG", 0); + SetEnvironmentVariableA("GTK_OVERLAY_SCROLLING", "0"); + SetEnvironmentVariableA("GTK_CSD", "0"); + SetEnvironmentVariableA("GDK_WIN32_DISABLE_HIDPI", "1"); + + LOG(INFO) << "Creating user consent app."; + int argc = 0; + char **argv = nullptr; + QApplication app(argc, argv); + + LOG(INFO) << "Creating user consent dialog."; + CrashDialog dlg(annotations, attachments); + dlg.exec(); + + user_text = dlg.get_user_text(); + consent = dlg.get_consent(); + + LOG(INFO) << "User consent complete:" << consent; + return consent; +} + +std::string +UserInteraction::getUserText() +{ + return user_text; +} + +void +UserInteraction::reportCompleted(const crashpad::UUID &uuid) +{ + LOG(INFO) << "Report files as: " << uuid.ToString(); +} + +namespace +{ + int HandlerMainAdaptor(int argc, char *argv[]) + { + LOG(INFO) << "Workrave crashed."; + auto *user_interaction = new UserInteraction; + int ret = crashpad::HandlerMain(argc, argv, nullptr, user_interaction); + LOG(INFO) << "Crash handled"; + delete user_interaction; + LOG(INFO) << "Exit:" << ret; + return ret; + } +} // namespace + +int APIENTRY +wWinMain(HINSTANCE, HINSTANCE, wchar_t *, int) +{ + return crashpad::ToolSupport::Wmain(__argc, __wargv, HandlerMainAdaptor); +} + +int +wmain(int argc, wchar_t *argv[]) +{ + return crashpad::ToolSupport::Wmain(argc, argv, HandlerMainAdaptor); +} diff --git a/ui/app/toolkits/qt/CrashDialog.hh b/ui/app/toolkits/qt/CrashDialog.hh new file mode 100644 index 000000000..18f404657 --- /dev/null +++ b/ui/app/toolkits/qt/CrashDialog.hh @@ -0,0 +1,81 @@ +// Copyright (C) 2020-2021 Rob Caelers +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#ifndef CRASH_DIALOG_HH +#define CRASH_DIALOG_HH + +#include + +#include +#include + +#include "handler/user_hook.h" + +class CrashDetailsDialog : public QDialog +{ +public: + CrashDetailsDialog(const std::vector &attachments, QWidget *parent = nullptr); + ~CrashDetailsDialog() override = default; + +private: + QVBoxLayout *vbox{nullptr}; + QScrollArea *scroll_area{nullptr}; + QTextEdit *text_edit{nullptr}; +}; + +class CrashDialog : public QDialog +{ +public: + CrashDialog(const std::map &annotations, + const std::vector &attachments, + QWidget *parent = nullptr); + ~CrashDialog() override = default; + + std::string get_user_text() const; + bool get_consent() const; + +private: + void on_submit_toggled(); + void on_details_clicked(); + +private: + QTextEdit *text_edit{nullptr}; + QVBoxLayout *vbox{nullptr}; + QScrollArea *scroll_area{nullptr}; + CrashDetailsDialog *details_dlg{nullptr}; + QCheckBox *submit_cb{nullptr}; + QLabel *user_text_label{nullptr}; + QFrame *user_text_frame{nullptr}; +}; + +class UserInteraction : public crashpad::UserHook +{ +public: + UserInteraction() = default; + ~UserInteraction() override = default; + + bool requestUserConsent(const std::map &annotations, + const std::vector &attachments) override; + std::string getUserText() override; + void reportCompleted(const crashpad::UUID &uuid) override; + +private: + std::string user_text; + bool consent{false}; +}; + +#endif // CRASH_DIALOG_HH diff --git a/ui/app/toolkits/qt/ExercisesPanel.cc b/ui/app/toolkits/qt/ExercisesPanel.cc index d1568e5d8..2eb8f016e 100644 --- a/ui/app/toolkits/qt/ExercisesPanel.cc +++ b/ui/app/toolkits/qt/ExercisesPanel.cc @@ -16,19 +16,20 @@ // along with this program. If not, see . // -#include "ui/IApplicationContext.hh" -#include #ifdef HAVE_CONFIG_H # include "config.h" #endif -#include #include "ExercisesPanel.hh" +#include +#include + #include "debug.hh" #include "utils/AssetPath.hh" +#include "ui/IApplicationContext.hh" #include "UiUtil.hh" diff --git a/ui/app/toolkits/qt/ExercisesPanel.hh b/ui/app/toolkits/qt/ExercisesPanel.hh index d9325a867..2c7ce70b0 100644 --- a/ui/app/toolkits/qt/ExercisesPanel.hh +++ b/ui/app/toolkits/qt/ExercisesPanel.hh @@ -76,6 +76,7 @@ private: std::vector shuffled_exercises; std::vector::const_iterator exercise_iterator; std::list::const_iterator image_iterator; + boost::signals2::signal stop_signal; int exercise_time{0}; int seq_time{0}; @@ -85,7 +86,6 @@ private: int exercise_count{0}; static int exercises_pointer; - boost::signals2::signal stop_signal; }; #endif // EXERCISES_PANEL_HH diff --git a/ui/app/toolkits/qt/MicroBreakWindow.cc b/ui/app/toolkits/qt/MicroBreakWindow.cc index e07200d23..6638557ef 100644 --- a/ui/app/toolkits/qt/MicroBreakWindow.cc +++ b/ui/app/toolkits/qt/MicroBreakWindow.cc @@ -58,6 +58,44 @@ MicroBreakWindow::create_gui() -> QWidget * auto core = app->get_core(); auto restbreak = core->get_break(BREAK_ID_REST_BREAK); + // if ((break_flags != BREAK_FLAGS_NONE) || restbreak->is_enabled()) + // { + // Gtk::HBox *button_box = nullptr; + // if (break_flags != BREAK_FLAGS_NONE) + // { + // button_box = Gtk::manage(new Gtk::HBox(false, 6)); + + // Gtk::HBox *bbox = Gtk::manage(new Gtk::HBox(true, 6)); + + // if ((break_flags & BREAK_FLAGS_POSTPONABLE) != 0) + // { + // Gtk::Button *postpone_button = create_postpone_button(); + // bbox->pack_end(*postpone_button, Gtk::PACK_EXPAND_WIDGET, 0); + // } + + // if ((break_flags & BREAK_FLAGS_SKIPPABLE) != 0) + // { + // Gtk::Button *skip_button = create_skip_button(); + // bbox->pack_end(*skip_button, Gtk::PACK_EXPAND_WIDGET, 0); + // } + + // Gtk::Alignment *bboxa = Gtk::manage(new Gtk::Alignment(1.0, 0.0, 0.0, 0.0)); + // bboxa->add(*bbox); + + // if (restbreak->is_enabled()) + // { + // button_box->pack_start(*Gtk::manage(create_restbreaknow_button(false)), Gtk::PACK_SHRINK, 0); + // } + // button_box->pack_end(*bboxa, Gtk::PACK_EXPAND_WIDGET, 0); + // } + // else + // { + // button_box = Gtk::manage(new Gtk::HBox(false, 6)); + // button_box->pack_end(*Gtk::manage(create_restbreaknow_button(true)), Gtk::PACK_SHRINK, 0); + // } + // box->pack_start(*button_box, Gtk::PACK_EXPAND_WIDGET, 0); + // } + // QHBoxLayout *button_box = new QHBoxLayout; // if (restbreak->is_enabled()) // { @@ -82,18 +120,53 @@ MicroBreakWindow::on_restbreaknow_button_clicked() // gui->restbreak_now(); } +// Moved to BreakWindow? +// void +// MicroBreakWindow::update_time_bar() +// { +// TRACE_ENTRY(); +// time_t time = progress_max_value - progress_value; +// string s = _("Micro-break"); +// s += ' '; +// s += Text::time_to_string(time); + +// time_bar->set_progress(progress_value, progress_max_value - 1); +// time_bar->set_text(s); + +// auto core = app->get_core(); +// bool user_active = core->is_user_active(); +// if (frame != nullptr) +// { +// if (user_active && !is_flashing) +// { +// frame->set_frame_color(Gdk::Color("orange")); +// frame->set_frame_visible(true); +// frame->set_frame_flashing(500); +// is_flashing = true; +// } +// else if (!user_active && is_flashing) +// { +// frame->set_frame_flashing(0); +// frame->set_frame_visible(false); +// is_flashing = false; +// } +// } +// time_bar->update(); +// TRACE_VAR(progress_value, progress_max_value); +// } + void MicroBreakWindow::update_label() { + TRACE_ENTRY(); auto core = app->get_core(); - - IBreak::Ptr restbreak_timer = core->get_break(BREAK_ID_REST_BREAK); - IBreak::Ptr daily_timer = core->get_break(BREAK_ID_DAILY_LIMIT); + auto restbreak_timer = core->get_break(BREAK_ID_REST_BREAK); + auto daily_timer = core->get_break(BREAK_ID_DAILY_LIMIT); BreakId show_next = BREAK_ID_NONE; - time_t rb = restbreak_timer->get_limit() - restbreak_timer->get_elapsed_time(); - time_t dl = daily_timer->get_limit() - daily_timer->get_elapsed_time(); + int64_t rb = restbreak_timer->get_limit() - restbreak_timer->get_elapsed_time(); + int64_t dl = daily_timer->get_limit() - daily_timer->get_elapsed_time(); if (restbreak_timer->is_enabled()) { diff --git a/ui/app/toolkits/qt/MicroBreakWindow.hh b/ui/app/toolkits/qt/MicroBreakWindow.hh index ab9e7d611..9181d5c24 100644 --- a/ui/app/toolkits/qt/MicroBreakWindow.hh +++ b/ui/app/toolkits/qt/MicroBreakWindow.hh @@ -27,6 +27,7 @@ class MicroBreakWindow : public BreakWindow public: MicroBreakWindow(std::shared_ptr app, QScreen *screen, BreakFlags break_flags); + ~MicroBreakWindow() override = default; void set_progress(int value, int max_value) override; diff --git a/ui/app/toolkits/qt/PreferencesDialog.cc b/ui/app/toolkits/qt/PreferencesDialog.cc index 71184ff4f..4576ef488 100644 --- a/ui/app/toolkits/qt/PreferencesDialog.cc +++ b/ui/app/toolkits/qt/PreferencesDialog.cc @@ -15,7 +15,6 @@ // along with this program. If not, see . // -#include #ifdef HAVE_CONFIG_H # include "config.h" #endif @@ -58,6 +57,13 @@ PreferencesDialog::PreferencesDialog(std::shared_ptr app) create_plugin_panels(); } +PreferencesDialog::~PreferencesDialog() +{ + TRACE_ENTRY(); + auto core = app->get_core(); + core->remove_operation_mode_override("preferences"); +} + void PreferencesDialog::init_ui() { @@ -198,6 +204,32 @@ PreferencesDialog::add_page(const std::string &id, const QString &label, const s return page_info; } +// bool +// PreferencesDialog::on_focus_in_event(GdkEventFocus *event) +// { +// TRACE_ENTRY(); +// BlockMode block_mode = GUIConfig::block_mode()(); +// if (block_mode != BlockMode::Off) +// { +// auto core = app->get_core(); +// OperationMode mode = core->get_active_operation_mode(); +// if (mode == OperationMode::Normal) +// { +// core->set_operation_mode_override(OperationMode::Quiet, "preferences"); +// } +// } +// return Gtk::Dialog::on_focus_in_event(event); +// } + +// bool +// PreferencesDialog::on_focus_out_event(GdkEventFocus *event) +// { +// TRACE_ENTRY(); +// auto core = app->get_core(); +// core->remove_operation_mode_override("preferences"); +// return Gtk::Dialog::on_focus_out_event(event); +// } + PreferencesPage::PreferencesPage(const std::string &id, QTabWidget *notebook) : id(id) , notebook(notebook) diff --git a/ui/app/toolkits/qt/PreferencesDialog.hh b/ui/app/toolkits/qt/PreferencesDialog.hh index 0aad4b7cf..c2b710d1f 100644 --- a/ui/app/toolkits/qt/PreferencesDialog.hh +++ b/ui/app/toolkits/qt/PreferencesDialog.hh @@ -52,6 +52,7 @@ class PreferencesDialog : public QDialog public: explicit PreferencesDialog(std::shared_ptr app); + ~PreferencesDialog() override; private: std::shared_ptr add_page(const std::string &id, const QString &label, const std::string &image); @@ -66,6 +67,9 @@ private: void create_plugin_panels(); void create_panel(std::shared_ptr &def); + // bool on_focus_in_event(GdkEventFocus *event) override; + // bool on_focus_out_event(GdkEventFocus *event) override; + private: std::shared_ptr app; std::map> pages; diff --git a/ui/app/toolkits/qt/Toolkit.cc b/ui/app/toolkits/qt/Toolkit.cc index 504085031..345de23f6 100644 --- a/ui/app/toolkits/qt/Toolkit.cc +++ b/ui/app/toolkits/qt/Toolkit.cc @@ -39,6 +39,7 @@ Toolkit::Toolkit(int argc, char **argv) : QApplication(argc, argv) , heartbeat_timer(new QTimer(this)) { + TRACE_ENTRY(); QCoreApplication::setOrganizationName("Workrave"); QCoreApplication::setOrganizationDomain("workrave.org"); QCoreApplication::setApplicationName("Workrave"); @@ -215,6 +216,7 @@ Toolkit::show_exercises() void Toolkit::show_main_window() { + TRACE_ENTRY(); main_window->show(); main_window->raise(); } @@ -329,12 +331,14 @@ Toolkit::on_status_icon_activated() void Toolkit::notify_add_confirm_function(const std::string &id, std::function func) { + TRACE_ENTRY(); notifiers[id] = func; } void Toolkit::notify_confirm(const std::string &id) { + TRACE_ENTRY(); if (notifiers.find(id) != notifiers.end()) { notifiers[id](); diff --git a/ui/app/toolkits/qt/Toolkit.hh b/ui/app/toolkits/qt/Toolkit.hh index e055588a8..97d21d851 100644 --- a/ui/app/toolkits/qt/Toolkit.hh +++ b/ui/app/toolkits/qt/Toolkit.hh @@ -39,6 +39,7 @@ #include "ui/IApplicationContext.hh" #include "ui/IToolkit.hh" #include "utils/Signals.hh" +#include "utils/Logging.hh" class Toolkit : public QApplication @@ -85,6 +86,10 @@ public: public Q_SLOTS: void on_timer(); +protected: + void notify_add_confirm_function(const std::string &id, std::function func); + void notify_confirm(const std::string &id); + private: void show_about(); void show_debug(); @@ -97,10 +102,6 @@ private: void on_status_icon_balloon_activated(const std::string &id); void on_status_icon_activated(); -protected: - void notify_add_confirm_function(const std::string &id, std::function func); - void notify_confirm(const std::string &id); - protected: std::shared_ptr app; MainWindow *main_window{nullptr}; @@ -128,6 +129,8 @@ private: boost::signals2::signal session_idle_changed_signal; boost::signals2::signal session_unlocked_signal; boost::signals2::signal status_icon_activated_signal; + + std::shared_ptr logger{workrave::utils::Logging::create("toolkit")}; }; class OneshotTimer : public QObject diff --git a/ui/app/toolkits/qt/ToolkitMacOS.cc b/ui/app/toolkits/qt/ToolkitMacOS.cc index 059d4e446..24ea021e2 100644 --- a/ui/app/toolkits/qt/ToolkitMacOS.cc +++ b/ui/app/toolkits/qt/ToolkitMacOS.cc @@ -22,7 +22,7 @@ #include "ToolkitMacOS.hh" #include "commonui/MenuDefs.hh" #include "MacOSDesktopWindow.hh" - +#include "ui/macos/MacOSLocker.hh" // TODO: // #if defined(PLATFORM_OS_MACOS) diff --git a/ui/app/toolkits/qt/ToolkitUnix.cc b/ui/app/toolkits/qt/ToolkitUnix.cc index 3cdd27e15..8595047c6 100644 --- a/ui/app/toolkits/qt/ToolkitUnix.cc +++ b/ui/app/toolkits/qt/ToolkitUnix.cc @@ -22,27 +22,35 @@ #include "ToolkitUnix.hh" #include -#include "X11SystrayAppletWindow.hh" -#include "GnomeSession.hh" - -#if defined(HAVE_INDICATOR) -# include "IndicatorAppletMenu.hh" -#endif #include "BreakWindow.hh" +#include "config/IConfigurator.hh" +#include "debug.hh" + ToolkitUnix::ToolkitUnix(int argc, char **argv) : Toolkit(argc, argv) { +} + +void +ToolkitUnix::preinit(std::shared_ptr config) +{ + TRACE_ENTRY(); + if (GUIConfig::force_x11()()) + { + spdlog::info("Forcing X11 backend -> Not implemented"); + } + + XInitThreads(); + locker = std::make_shared(); } void ToolkitUnix::init(std::shared_ptr app) { -#if defined(PLATFORM_OS_UNIX) - XInitThreads(); -#endif + TRACE_ENTRY(); Toolkit::init(app); @@ -60,7 +68,7 @@ ToolkitUnix::create_break_window(int screen_index, workrave::BreakId break_id, B // FIXME: remove hack if (auto break_window = std::dynamic_pointer_cast(ret); break_window) { - auto gdk_window = break_window->get_window()->gobj(); + auto *gdk_window = break_window->get_window()->gobj(); locker->set_window(gdk_window); } return ret; diff --git a/ui/app/toolkits/qt/ToolkitUnix.hh b/ui/app/toolkits/qt/ToolkitUnix.hh index 96fb955bb..2621d1a3c 100644 --- a/ui/app/toolkits/qt/ToolkitUnix.hh +++ b/ui/app/toolkits/qt/ToolkitUnix.hh @@ -29,11 +29,15 @@ public: ~ToolkitUnix() override = default; // IToolkit + void preinit(std::shared_ptr config) override; void init(std::shared_ptr app) override; IBreakWindow::Ptr create_break_window(int screen_index, workrave::BreakId break_id, BreakFlags break_flags) override; std::shared_ptr get_locker() override; - void show_notification(const std::string &id, const std::string &title, const std::string &balloon, std::function func); + void show_notification(const std::string &id, + const std::string &title, + const std::string &balloon, + std::function func) override; private: std::shared_ptr locker; diff --git a/ui/app/toolkits/qt/ToolkitWindows.cc b/ui/app/toolkits/qt/ToolkitWindows.cc index f0c2a1ddb..48a70e25d 100644 --- a/ui/app/toolkits/qt/ToolkitWindows.cc +++ b/ui/app/toolkits/qt/ToolkitWindows.cc @@ -21,6 +21,7 @@ #include "ToolkitWindows.hh" +#include #ifndef PLATFORM_OS_WINDOWS_NATIVE # include #endif @@ -28,7 +29,6 @@ #include #include -#include "ui/GUIConfig.hh" #include "debug.hh" using namespace workrave; @@ -38,26 +38,35 @@ ToolkitWindows::ToolkitWindows(int argc, char **argv) : Toolkit(argc, argv) { #if defined(HAVE_HARPOON) + spdlog::info("Using Harpoon locker"); locker = std::make_shared(); #else + spdlog::info("Using standard locker"); locker = std::make_shared(); #endif } ToolkitWindows::~ToolkitWindows() { + TRACE_ENTRY(); } void ToolkitWindows::init(std::shared_ptr app) { - init_gui(); Toolkit::init(app); + init_gui(); init_filter(); } +void +ToolkitWindows::deinit() +{ + Toolkit::deinit(); +} + void ToolkitWindows::release() { @@ -157,26 +166,6 @@ ToolkitWindows::filter_func(MSG *msg) } break; #endif - - case WM_DEVICECHANGE: - { - TRACE_MSG("WM_DEVICECHANGE {} {}", msg->wParam, msg->lParam); - switch (msg->wParam) - { - case DBT_DEVICEARRIVAL: - case DBT_DEVICEREMOVECOMPLETE: - { - HWND hwnd = FindWindowExA(NULL, NULL, "GdkDisplayChange", NULL); - if (hwnd) - { - SendMessage(hwnd, WM_DISPLAYCHANGE, 0, 0); - } - } - default: - break; - } - break; - } } event_hook(msg); @@ -202,3 +191,19 @@ ToolkitWindows::get_locker() { return locker; } + +// TODO: Duplicate code gtkmm and qt toolkits. Move to platform. +bool +ToolkitWindows::is_windows_app_theme_dark() +{ + DWORD value = 1; // Default to light theme + DWORD dataSize = sizeof(value); + HKEY hKey = nullptr; + if (RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", 0, KEY_READ, &hKey) + == ERROR_SUCCESS) + { + RegQueryValueExW(hKey, L"AppsUseLightTheme", nullptr, nullptr, (LPBYTE)&value, &dataSize); + RegCloseKey(hKey); + } + return value == 0; +} diff --git a/ui/app/toolkits/qt/ToolkitWindows.hh b/ui/app/toolkits/qt/ToolkitWindows.hh index f3f2a2161..2feef3e29 100644 --- a/ui/app/toolkits/qt/ToolkitWindows.hh +++ b/ui/app/toolkits/qt/ToolkitWindows.hh @@ -23,6 +23,8 @@ #include "ui/windows/IToolkitWindows.hh" #include "Toolkit.hh" +#include "utils/Logging.hh" +#include "utils/Signals.hh" #include "ui/windows/WindowsLocker.hh" #if defined(HAVE_HARPOON) @@ -38,6 +40,7 @@ public: ~ToolkitWindows() override; void init(std::shared_ptr app) override; + void deinit() override; void release() override; std::shared_ptr get_locker() override; @@ -47,6 +50,8 @@ public: auto get_desktop_image() -> QPixmap override; + static bool is_windows_app_theme_dark(); + private: void init_filter(); void init_gui(); @@ -60,6 +65,8 @@ private: #else std::shared_ptr locker; #endif + + std::shared_ptr logger{workrave::utils::Logging::create("toolkit:windows")}; }; #endif // TOOLKIT_WINDOWS_HH diff --git a/ui/app/toolkits/qt/dist/windows/CMakeLists.txt b/ui/app/toolkits/qt/dist/windows/CMakeLists.txt index 6743f9b23..01f3c5ee9 100644 --- a/ui/app/toolkits/qt/dist/windows/CMakeLists.txt +++ b/ui/app/toolkits/qt/dist/windows/CMakeLists.txt @@ -74,7 +74,7 @@ add_custom_target(installer DEPENDS ${INSTALLER_TARGET}) if (CMAKE_CROSSCOMPILING) ExternalProject_Add(ZapperQt32Bit SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src - BINARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/.32 + BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/.32 CMAKE_CACHE_ARGS -DCMAKE_TOOLCHAIN_FILE:FILEPATH=${CMAKE_SOURCE_DIR}/cmake/toolchains/mingw32-gcc.cmake -DCMAKE_INSTALL_PREFIX:FILEPATH=${CMAKE_INSTALL_PREFIX} @@ -82,7 +82,7 @@ if (CMAKE_CROSSCOMPILING) elseif (MSVC) ExternalProject_Add(ZapperQt32Bit SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src - BINARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/.32 + BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/.32 CMAKE_GENERATOR "Visual Studio 17 2022" CMAKE_ARGS -A Win32 CMAKE_CACHE_ARGS @@ -94,7 +94,7 @@ elseif (MINGW) set(CMAKE_COMMAND32 ${MSYS_CMD} -here -mingw32 -no-start -defterm -c "cmake \$*" cmake) ExternalProject_Add(ZapperQt32Bit SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src - BINARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/.32 + BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/.32 CMAKE_COMMAND ${CMAKE_COMMAND32} CMAKE_CACHE_ARGS -DCMAKE_TOOLCHAIN_FILE:FILEPATH=${CMAKE_SOURCE_DIR}/cmake/toolchains/msys2.cmake diff --git a/ui/app/toolkits/qt/resource.rc.in b/ui/app/toolkits/qt/resource.rc.in new file mode 100644 index 000000000..6fec4721b --- /dev/null +++ b/ui/app/toolkits/qt/resource.rc.in @@ -0,0 +1,32 @@ +#include + +VS_VERSION_INFO VERSIONINFO + FILEVERSION @WR_RESOURCE_VERSION@ + PRODUCTVERSION @WR_RESOURCE_VERSION@ + FILEFLAGSMASK 0 + FILEFLAGS 0 + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE VFT2_UNKNOWN + BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "The Workrave development team" + VALUE "FileDescription", "Workrave" + VALUE "FileVersion", "@WR_VERSION@" + VALUE "InternalName", "workrave" + VALUE "LegalCopyright", "Copyright (C) 2001-2024 The Workrave development team." + VALUE "OriginalFilename", "WorkraveCrashHandler.exe" + VALUE "ProductName", "Workrave" + VALUE "ProductVersion", "@WR_VERSION@" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END + END + +workrave ICON DISCARDABLE "@WR_TOP_SOURCE_DIR@/ui/data/images/windows/workrave-normal.ico"