From 2fa116b76b34ef73b7aaedb6d06d3c1559807901 Mon Sep 17 00:00:00 2001 From: Green Sky Date: Wed, 25 Dec 2024 17:21:07 +0100 Subject: [PATCH] system tray support --- flake.nix | 2 + res/example_config.json | 7 ++- src/CMakeLists.txt | 3 ++ src/main_screen.cpp | 32 ++++++++++++- src/main_screen.hpp | 2 + src/start_screen.cpp | 8 +++- src/status_indicator.cpp | 10 +++- src/status_indicator.hpp | 7 ++- src/sys_tray.cpp | 100 +++++++++++++++++++++++++++++++++++++++ src/sys_tray.hpp | 24 ++++++++++ 10 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 src/sys_tray.cpp create mode 100644 src/sys_tray.hpp diff --git a/flake.nix b/flake.nix index 12cc3b88..936738af 100644 --- a/flake.nix +++ b/flake.nix @@ -65,6 +65,8 @@ pipewire + libayatana-appindicator + # sdl3_image: libpng libjpeg diff --git a/res/example_config.json b/res/example_config.json index e53687c4..3ca5949c 100644 --- a/res/example_config.json +++ b/res/example_config.json @@ -3,7 +3,12 @@ "save_file_path": "tomato.tox" }, "tomato": { - "skip_setup_and_load": true + "skip_setup_and_load": true, + + "system_tray": true, + + "start_to_tray": true, + "__comment_start_to_tray": "Implies both skip_setup_and_load and system_tray" }, "PluginManager": { "autoload": { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7a99effa..159fbd9c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -71,6 +71,9 @@ target_sources(tomato PUBLIC ./sdl_clipboard_utils.hpp ./sdl_clipboard_utils.cpp + ./sys_tray.hpp + ./sys_tray.cpp + ./chat_gui/theme.hpp ./chat_gui/theme.cpp ./chat_gui/icons/direct.hpp diff --git a/src/main_screen.cpp b/src/main_screen.cpp index 23018e80..22492c71 100644 --- a/src/main_screen.cpp +++ b/src/main_screen.cpp @@ -16,6 +16,22 @@ #include #include +static std::unique_ptr constructSystemTray(SimpleConfigModel& conf, SDL_Window* window) { + bool conf_system_tray {false}; + if (auto value_stt = conf.get_bool("tomato", "start_to_tray"); value_stt.has_value) { + conf_system_tray = value_stt.value(); + // TODO: warn or error if "system_try" is false + } else if (auto value_st = conf.get_bool("tomato", "system_tray"); value_st.has_value) { + conf_system_tray = value_st.value(); + } + + if (conf_system_tray) { + return std::make_unique(window); + } else { + return nullptr; + } +} + MainScreen::MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme& theme_, std::string save_path, std::string save_password, std::string new_username, std::vector plugins) : renderer(renderer_), conf(std::move(conf_)), @@ -43,7 +59,8 @@ MainScreen::MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme contact_tc(tal, sdlrtu), mil(), msg_tc(mil, sdlrtu), - si(rmm, cr, SDL_GetRenderWindow(renderer_)), + st(constructSystemTray(conf, SDL_GetRenderWindow(renderer_))), + si(rmm, cr, SDL_GetRenderWindow(renderer_), st.get()), cg(conf, os, rmm, cr, sdlrtu, contact_tc, msg_tc, theme), sw(conf), osui(os), @@ -248,13 +265,13 @@ bool MainScreen::handleEvent(SDL_Event& e) { if (e.type == SDL_EVENT_DROP_FILE) { std::cout << "DROP FILE: " << e.drop.data << "\n"; _dopped_files.emplace_back(e.drop.data); - //cg.sendFilePath(e.drop.data); _render_interval = 1.f/60.f; // TODO: magic _time_since_event = 0.f; return true; } if ( + e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED || e.type == SDL_EVENT_WINDOW_MINIMIZED || e.type == SDL_EVENT_WINDOW_HIDDEN || e.type == SDL_EVENT_WINDOW_OCCLUDED // does this trigger on partial occlusion? @@ -262,6 +279,11 @@ bool MainScreen::handleEvent(SDL_Event& e) { auto* window = SDL_GetWindowFromID(e.window.windowID); auto* event_renderer = SDL_GetRenderer(window); if (event_renderer != nullptr && event_renderer == renderer) { + if (e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { + // this event only fires if not last window (systrays count as windows) + SDL_HideWindow(window); + } + // our window is now obstructed if (_window_hidden_ts < e.window.timestamp) { _window_hidden_ts = e.window.timestamp; @@ -269,6 +291,9 @@ bool MainScreen::handleEvent(SDL_Event& e) { //std::cout << "TOMAT: window hidden " << e.type << " " << e.window.timestamp << "\n"; } } + if (st) { + st->update(); + } return true; // forward? } @@ -296,6 +321,9 @@ bool MainScreen::handleEvent(SDL_Event& e) { } _render_interval = 1.f/60.f; // TODO: magic _time_since_event = 0.f; + if (st) { + st->update(); // TODO: limit this + } return true; // forward? } diff --git a/src/main_screen.hpp b/src/main_screen.hpp index ae3b0059..55c14a74 100644 --- a/src/main_screen.hpp +++ b/src/main_screen.hpp @@ -29,6 +29,7 @@ #include "./tox_avatar_loader.hpp" #include "./message_image_loader.hpp" +#include "./sys_tray.hpp" #include "./status_indicator.hpp" #include "./chat_gui4.hpp" #include "./chat_gui/settings_window.hpp" @@ -92,6 +93,7 @@ struct MainScreen final : public Screen { MessageImageLoader mil; TextureCache msg_tc; + std::unique_ptr st; StatusIndicator si; ChatGui4 cg; SettingsWindow sw; diff --git a/src/start_screen.cpp b/src/start_screen.cpp index 7eaad59c..6163045f 100644 --- a/src/start_screen.cpp +++ b/src/start_screen.cpp @@ -311,7 +311,13 @@ Screen* StartScreen::render(float, bool&) { // TODO: dont check every frame // do in tick instead? - if (_conf.get_bool("tomato", "skip_setup_and_load").value_or(false)) { + const bool start_to_tray = _conf.get_bool("tomato", "start_to_tray").value_or(false); + const bool skip_setup = _conf.get_bool("tomato", "skip_setup_and_load").value_or(false); + if (start_to_tray || skip_setup) { + if (start_to_tray) { + // TODO: the window should really be created hidden in the first place + SDL_HideWindow(SDL_GetRenderWindow(_renderer)); + } auto new_screen = std::make_unique(std::move(_conf), _renderer, _theme, _tox_profile_path, _password, _user_name, queued_plugin_paths); return new_screen.release(); } else { diff --git a/src/status_indicator.cpp b/src/status_indicator.cpp index ab37bb9e..2e52d419 100644 --- a/src/status_indicator.cpp +++ b/src/status_indicator.cpp @@ -28,16 +28,22 @@ void StatusIndicator::updateState(State state) { } SDL_SetWindowIcon(_main_window, surf.get()); + + if (_tray != nullptr) { + _tray->setIcon(surf.get()); + } } StatusIndicator::StatusIndicator( RegistryMessageModelI& rmm, Contact3Registry& cr, - SDL_Window* main_window + SDL_Window* main_window, + SystemTray* tray ) : _rmm(rmm), _cr(cr), - _main_window(main_window) + _main_window(main_window), + _tray(tray) { // start off with base icon updateState(State::base); diff --git a/src/status_indicator.hpp b/src/status_indicator.hpp index 29e852fb..dbb009bc 100644 --- a/src/status_indicator.hpp +++ b/src/status_indicator.hpp @@ -2,6 +2,8 @@ #include +#include "./sys_tray.hpp" + #include // service that sets window and tray icon depending on program state @@ -11,7 +13,7 @@ class StatusIndicator { Contact3Registry& _cr; SDL_Window* _main_window; - // systray ptr here + SystemTray* _tray; float _cooldown {1.f}; @@ -27,7 +29,8 @@ class StatusIndicator { StatusIndicator( RegistryMessageModelI& rmm, Contact3Registry& cr, - SDL_Window* main_window + SDL_Window* main_window, + SystemTray* tray = nullptr ); // does not actually render, just on the render thread diff --git a/src/sys_tray.cpp b/src/sys_tray.cpp new file mode 100644 index 00000000..acde6f63 --- /dev/null +++ b/src/sys_tray.cpp @@ -0,0 +1,100 @@ +#include "./sys_tray.hpp" + +#include "./icon_generator.hpp" + +#include +#include +#include + +SystemTray::SystemTray(SDL_Window* main_window) : _main_window(main_window) { + std::cout << "ST: adding system tray\n"; + + _tray = SDL_CreateTray(nullptr, "tomato"); + if (_tray == nullptr) { + //std::cerr << "ST: failed to create SystemTray: " << SDL_GetError() << "\n"; + throw std::runtime_error(std::string{"ST: failed to create SystemTray: "} + SDL_GetError()); + return; + } + + auto* tray_menu = SDL_CreateTrayMenu(_tray); + { + auto* entry_quit = SDL_InsertTrayEntryAt(tray_menu, 0, "Quit Tomato", SDL_TRAYENTRY_BUTTON); + SDL_SetTrayEntryCallback(entry_quit, + +[](void*, SDL_TrayEntry*){ + // this is thread safe and triggers the shutdown in the main thread + SDL_Event quit_event; + quit_event.quit = { + SDL_EVENT_QUIT, + 0, + SDL_GetTicksNS(), + }; + SDL_PushEvent(&quit_event); + } + , nullptr); + } + { + _entry_showhide = SDL_InsertTrayEntryAt(tray_menu, 0, "Hide Tomato", SDL_TRAYENTRY_BUTTON); + SDL_SetTrayEntryCallback(_entry_showhide, + +[](void* userdata, SDL_TrayEntry*){ + SDL_HideWindow(static_cast(userdata)->_main_window); + } + , this); + } +} + +SystemTray::~SystemTray(void) { + if (_tray != nullptr) { + SDL_DestroyTray(_tray); + _tray = nullptr; + } +} + +void SystemTray::setIcon(SDL_Surface* surf) { + if (_tray == nullptr) { + return; + } + + SDL_SetTrayIcon(_tray, surf); +} + +void SystemTray::setStatusText(const std::string& status) { + if (_tray == nullptr) { + return; + } + + if (_entry_status == nullptr) { + _entry_status = SDL_InsertTrayEntryAt(SDL_GetTrayMenu(_tray), 0, status.c_str(), SDL_TRAYENTRY_DISABLED); + return; + } + + SDL_SetTrayEntryLabel(_entry_status, status.c_str()); +} + +void SystemTray::update(void) { + if (_tray == nullptr) { + return; + } + + if (_entry_showhide != nullptr) { + // check if window is open and adjust text and callback + const bool hidden {(SDL_GetWindowFlags(_main_window) & SDL_WINDOW_HIDDEN) > 0}; + // TODO: cache state + + if (hidden) { + SDL_SetTrayEntryLabel(_entry_showhide, "Show Tomato"); + SDL_SetTrayEntryCallback(_entry_showhide, + +[](void* userdata, SDL_TrayEntry*){ + SDL_ShowWindow(static_cast(userdata)->_main_window); + } + , this); + } else { + SDL_SetTrayEntryLabel(_entry_showhide, "Hide Tomato"); + SDL_SetTrayEntryCallback(_entry_showhide, + +[](void* userdata, SDL_TrayEntry*){ + SDL_HideWindow(static_cast(userdata)->_main_window); + } + , this); + } + } +} + diff --git a/src/sys_tray.hpp b/src/sys_tray.hpp new file mode 100644 index 00000000..4de109c5 --- /dev/null +++ b/src/sys_tray.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include + +class SystemTray { + SDL_Window* _main_window {nullptr}; + SDL_Tray* _tray {nullptr}; + + SDL_TrayEntry* _entry_showhide {nullptr}; + SDL_TrayEntry* _entry_status {nullptr}; + + public: + SystemTray(SDL_Window* main_window); + ~SystemTray(void); + + void setIcon(SDL_Surface* surf); + void setStatusText(const std::string& status); + + // check if window is visible and adjust text + void update(void); +}; +