mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-28 16:54:11 +08:00
Compare commits
5
Commits
8ea4668305
...
b854512c24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b854512c24 | ||
|
|
4528caf0f4 | ||
|
|
f6e89725aa | ||
|
|
62f79a8e44 | ||
|
|
096bc68d35 |
@@ -17,6 +17,7 @@ MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
|
||||
DESCRIPTION="A simple cross-platform remote desktop client."
|
||||
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
||||
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
||||
TRAY_RUNTIME_RECOMMENDS="libayatana-appindicator3-1 | libappindicator3-1"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
@@ -145,7 +146,7 @@ for size in 16 24 32 48 64 96 128 256; do
|
||||
"$ICON_BASE_DIR/${size}x${size}/apps/${PKG_NAME}.png"
|
||||
done
|
||||
|
||||
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS"
|
||||
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS, $TRAY_RUNTIME_RECOMMENDS"
|
||||
if [[ -n "$EXTRA_RECOMMENDATION" ]]; then
|
||||
RECOMMENDS="$RECOMMENDS, $EXTRA_RECOMMENDATION"
|
||||
fi
|
||||
|
||||
@@ -34,6 +34,8 @@ int ConfigCenter::Load() {
|
||||
persist_config_migration |= ini_.Delete(section_, "screen_content");
|
||||
persist_config_migration |=
|
||||
ini_.Delete(section_, "enable_desktop_quality_optimization");
|
||||
persist_config_migration |=
|
||||
ini_.Delete(section_, "enable_minimize_to_tray");
|
||||
|
||||
const long language_value =
|
||||
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
|
||||
@@ -109,8 +111,6 @@ int ConfigCenter::Load() {
|
||||
enable_autostart_ =
|
||||
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
enable_minimize_to_tray_ = ini_.GetBoolValue(
|
||||
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
|
||||
portable_service_prompt_suppressed_ =
|
||||
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
@@ -157,8 +157,6 @@ int ConfigCenter::Save() {
|
||||
|
||||
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
|
||||
enable_minimize_to_tray_);
|
||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
|
||||
@@ -338,17 +336,6 @@ int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetMinimizeToTray(bool enable_minimize_to_tray) {
|
||||
enable_minimize_to_tray_ = enable_minimize_to_tray;
|
||||
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
|
||||
enable_minimize_to_tray_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetAutostart(bool enable_autostart) {
|
||||
enable_autostart_ = enable_autostart;
|
||||
bool success = false;
|
||||
@@ -448,8 +435,6 @@ int ConfigCenter::GetDefaultCoturnServerPort() const {
|
||||
|
||||
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
|
||||
|
||||
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
|
||||
|
||||
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
|
||||
|
||||
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
|
||||
|
||||
@@ -43,7 +43,6 @@ class ConfigCenter {
|
||||
int SetServerPort(int signal_server_port);
|
||||
int SetCoturnServerPort(int coturn_server_port);
|
||||
int SetSelfHosted(bool enable_self_hosted);
|
||||
int SetMinimizeToTray(bool enable_minimize_to_tray);
|
||||
int SetAutostart(bool enable_autostart);
|
||||
int SetDaemon(bool enable_daemon);
|
||||
int SetPortableServicePromptSuppressed(bool suppressed);
|
||||
@@ -66,7 +65,6 @@ class ConfigCenter {
|
||||
int GetDefaultSignalServerPort() const;
|
||||
int GetDefaultCoturnServerPort() const;
|
||||
bool IsSelfHosted() const;
|
||||
bool IsMinimizeToTray() const;
|
||||
bool IsEnableAutostart() const;
|
||||
bool IsEnableDaemon() const;
|
||||
bool IsPortableServicePromptSuppressed() const;
|
||||
@@ -94,7 +92,6 @@ class ConfigCenter {
|
||||
int coturn_server_port_ = 0;
|
||||
int coturn_server_port_default_ = 3478;
|
||||
bool enable_self_hosted_ = false;
|
||||
bool enable_minimize_to_tray_ = false;
|
||||
bool enable_autostart_ = false;
|
||||
bool enable_daemon_ = false;
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
|
||||
@@ -1163,22 +1163,24 @@ void GuiApplication::InitializeSystemTray() {
|
||||
};
|
||||
#if _WIN32
|
||||
ui_->tray = std::make_unique<WinTray>(
|
||||
std::move(show_window), std::move(hide_window), std::move(exit_app),
|
||||
LoadSlintTrayIcon(), L"CrossDesk", localization_language_index_);
|
||||
std::move(show_window), std::move(hide_window),
|
||||
std::move(open_settings), std::move(exit_app), LoadSlintTrayIcon(),
|
||||
L"CrossDesk", localization_language_index_);
|
||||
#elif defined(__APPLE__)
|
||||
ui_->tray = std::make_unique<MacTray>(
|
||||
std::move(show_window), std::move(hide_window), std::move(open_settings),
|
||||
std::move(exit_app), "CrossDesk", localization_language_index_);
|
||||
#elif defined(__linux__)
|
||||
ui_->tray = std::make_unique<LinuxTray>(
|
||||
std::move(show_window), std::move(hide_window), std::move(exit_app),
|
||||
"CrossDesk", localization_language_index_);
|
||||
std::move(show_window), std::move(hide_window),
|
||||
std::move(open_settings), std::move(exit_app), "CrossDesk",
|
||||
localization_language_index_);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
bool GuiApplication::MinimizeMainWindowToTray() {
|
||||
if (!enable_minimize_to_tray_ || !ui_) {
|
||||
if (!ui_) {
|
||||
return false;
|
||||
}
|
||||
#if _WIN32 || defined(__APPLE__)
|
||||
@@ -1208,7 +1210,6 @@ void GuiApplication::ResetSettingsUi() {
|
||||
ui_->main->set_self_hosted_enabled(enable_self_hosted_);
|
||||
ui_->main->set_autostart_enabled(enable_autostart_);
|
||||
ui_->main->set_daemon_enabled(enable_daemon_);
|
||||
ui_->main->set_minimize_to_tray_enabled(enable_minimize_to_tray_);
|
||||
ui_->main->set_file_save_path(file_transfer_save_path_buf_);
|
||||
ui_->main->set_server_host(signal_server_ip_self_);
|
||||
ui_->main->set_server_port(signal_server_port_self_);
|
||||
@@ -1218,12 +1219,10 @@ void GuiApplication::ResetSettingsUi() {
|
||||
void GuiApplication::BindMainCallbacks() {
|
||||
auto& main = ui_->main;
|
||||
main->window().on_close_requested([this] {
|
||||
if (MinimizeMainWindowToTray()) {
|
||||
// MinimizeMainWindowToTray() ensures the tray icon exists. Let Slint
|
||||
// complete the close request by hiding the main window as well.
|
||||
return slint::CloseRequestResponse::HideWindow;
|
||||
}
|
||||
exit_ = true;
|
||||
// Keep the application and all active sessions alive. The tray helper
|
||||
// ensures that supported platforms still provide a way to restore the
|
||||
// main window; Slint hides it regardless of tray availability.
|
||||
MinimizeMainWindowToTray();
|
||||
return slint::CloseRequestResponse::HideWindow;
|
||||
});
|
||||
main->on_main_title_drag([this](int phase, float x, float y) {
|
||||
@@ -1240,11 +1239,7 @@ void GuiApplication::BindMainCallbacks() {
|
||||
if (!ui_) {
|
||||
return;
|
||||
}
|
||||
if (MinimizeMainWindowToTray()) {
|
||||
ui_->main->hide();
|
||||
return;
|
||||
}
|
||||
exit_ = true;
|
||||
MinimizeMainWindowToTray();
|
||||
ui_->main->hide();
|
||||
});
|
||||
main->on_copy_local_id([this] {
|
||||
@@ -2756,7 +2751,6 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
enable_self_hosted_ = main->get_self_hosted_enabled();
|
||||
enable_autostart_ = main->get_autostart_enabled();
|
||||
enable_daemon_ = main->get_daemon_enabled();
|
||||
enable_minimize_to_tray_ = main->get_minimize_to_tray_enabled();
|
||||
|
||||
localization_language_ =
|
||||
static_cast<ConfigCenter::LANGUAGE>(language_button_value_);
|
||||
@@ -2775,7 +2769,6 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
config_center_->SetSelfHosted(enable_self_hosted_);
|
||||
config_center_->SetAutostart(enable_autostart_);
|
||||
config_center_->SetDaemon(enable_daemon_);
|
||||
config_center_->SetMinimizeToTray(enable_minimize_to_tray_);
|
||||
|
||||
const std::string path(main->get_file_save_path());
|
||||
std::memset(file_transfer_save_path_buf_, 0,
|
||||
@@ -2805,7 +2798,6 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
enable_self_hosted_last_ = enable_self_hosted_;
|
||||
enable_autostart_last_ = enable_autostart_;
|
||||
enable_daemon_last_ = enable_daemon_;
|
||||
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
|
||||
file_transfer_save_path_last_ = path;
|
||||
ui_->localized_language = -1;
|
||||
|
||||
|
||||
@@ -134,8 +134,10 @@ void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
|
||||
}
|
||||
|
||||
if (main_window_ &&
|
||||
event.window.windowID == SDL_GetWindowID(main_window_) &&
|
||||
MinimizeMainWindowToTray()) {
|
||||
event.window.windowID == SDL_GetWindowID(main_window_)) {
|
||||
if (!MinimizeMainWindowToTray()) {
|
||||
SDL_HideWindow(main_window_);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ int GuiApplication::DrawServerWindow() {
|
||||
}
|
||||
|
||||
bool GuiApplication::MinimizeMainWindowToTray() {
|
||||
if (!enable_minimize_to_tray_ || !main_window_) {
|
||||
if (!main_window_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -135,35 +135,58 @@ inline std::string FormatDisplayLabel(size_t display_index,
|
||||
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
|
||||
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
|
||||
|
||||
#if _WIN32
|
||||
inline const wchar_t* GetExitProgramLabel(int language_index) {
|
||||
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
|
||||
#if _WIN32
|
||||
inline const wchar_t* GetWindowsLocalizedLabel(
|
||||
int language_index, const char* key, const wchar_t* fallback,
|
||||
std::vector<std::wstring>& cache) {
|
||||
const int normalized_index = detail::ClampLanguageIndex(language_index);
|
||||
std::wstring& cached_text = cache[normalized_index];
|
||||
if (!cached_text.empty()) {
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
const std::string& utf8_text =
|
||||
detail::GetTranslatedText("exit_program", normalized_index);
|
||||
if (utf8_text.empty()) {
|
||||
cached_text = L"Exit";
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
int wide_length =
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
|
||||
if (wide_length <= 0) {
|
||||
cached_text = L"Exit";
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
cached_text.resize(static_cast<size_t>(wide_length - 1));
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, cached_text.data(),
|
||||
wide_length);
|
||||
return cached_text.c_str();
|
||||
}
|
||||
#endif
|
||||
std::wstring& cached_text = cache[normalized_index];
|
||||
if (!cached_text.empty()) {
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
const std::string& utf8_text =
|
||||
detail::GetTranslatedText(key, normalized_index);
|
||||
if (utf8_text.empty()) {
|
||||
cached_text = fallback;
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
int wide_length =
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
|
||||
if (wide_length <= 0) {
|
||||
cached_text = fallback;
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
cached_text.resize(static_cast<size_t>(wide_length));
|
||||
if (MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1,
|
||||
cached_text.data(), wide_length) <= 0) {
|
||||
cached_text = fallback;
|
||||
return cached_text.c_str();
|
||||
}
|
||||
cached_text.pop_back();
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
inline const wchar_t* GetShowMainWindowLabel(int language_index) {
|
||||
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
|
||||
return GetWindowsLocalizedLabel(language_index, "show_main_window",
|
||||
L"Show Main Window", cache);
|
||||
}
|
||||
|
||||
inline const wchar_t* GetSettingsLabel(int language_index) {
|
||||
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
|
||||
return GetWindowsLocalizedLabel(language_index, "settings", L"Settings",
|
||||
cache);
|
||||
}
|
||||
|
||||
inline const wchar_t* GetExitProgramLabel(int language_index) {
|
||||
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
|
||||
return GetWindowsLocalizedLabel(language_index, "exit_program", L"Exit",
|
||||
cache);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace localization
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -215,8 +215,6 @@ struct TranslationRow {
|
||||
X(file_transfer_save_path, u8"文件接收保存路径:", \
|
||||
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
|
||||
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
|
||||
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
|
||||
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
|
||||
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
|
||||
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
|
||||
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
|
||||
@@ -233,6 +231,8 @@ struct TranslationRow {
|
||||
X(permission_required_message, u8"该应用需要授权以下权限:", \
|
||||
"The application requires the following permissions:", \
|
||||
u8"Для работы приложения требуются следующие разрешения:") \
|
||||
X(show_main_window, u8"显示主界面", "Show Main Window", \
|
||||
u8"Показать главное окно") \
|
||||
X(exit_program, u8"退出", "Exit", u8"Выход")
|
||||
|
||||
inline constexpr TranslationRow kTranslationRows[] = {
|
||||
|
||||
@@ -310,7 +310,6 @@ int SettingsManager::Load() {
|
||||
owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted();
|
||||
owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart();
|
||||
owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon();
|
||||
owner_.enable_minimize_to_tray_ = owner_.config_center_->IsMinimizeToTray();
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
owner_.portable_service_prompt_suppressed_ =
|
||||
owner_.config_center_->IsPortableServicePromptSuppressed();
|
||||
@@ -333,7 +332,6 @@ int SettingsManager::Load() {
|
||||
owner_.enable_srtp_last_ = owner_.enable_srtp_;
|
||||
owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_;
|
||||
owner_.enable_autostart_last_ = owner_.enable_autostart_;
|
||||
owner_.enable_minimize_to_tray_last_ = owner_.enable_minimize_to_tray_;
|
||||
|
||||
LOG_INFO("Load settings from cache file");
|
||||
return 0;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/shape.h>
|
||||
#include <X11/extensions/Xrender.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -32,14 +33,121 @@ namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr int kTrayIconSize = 24;
|
||||
constexpr int kMenuHeight = 28;
|
||||
constexpr int kMenuItemHeight = 28;
|
||||
constexpr int kMenuItemCount = 3;
|
||||
constexpr int kMenuVerticalPadding = 4;
|
||||
constexpr int kMenuHeight =
|
||||
kMenuItemHeight * kMenuItemCount + kMenuVerticalPadding * 2;
|
||||
constexpr int kMenuHorizontalPadding = 12;
|
||||
constexpr int kMenuCornerRadius = 8;
|
||||
constexpr int kMenuBorderWidth = 1;
|
||||
constexpr int kMenuItemHorizontalInset = 4;
|
||||
constexpr int kMenuItemVerticalInset = 2;
|
||||
constexpr int kMenuItemCornerRadius = 5;
|
||||
constexpr int kMenuSeparatorHorizontalInset = 8;
|
||||
constexpr int kDockTimeoutMs = 800;
|
||||
constexpr int kMaxTrayEventsPerTick = 32;
|
||||
constexpr int kMaxTrayEventsDuringEmbed = 16;
|
||||
constexpr long kSystemTrayRequestDock = 0;
|
||||
constexpr long kXEmbedMapped = 1;
|
||||
constexpr long kXEmbedEmbeddedNotify = 0;
|
||||
constexpr int kAppIndicatorCategoryApplicationStatus = 0;
|
||||
constexpr int kAppIndicatorStatusPassive = 0;
|
||||
constexpr int kAppIndicatorStatusActive = 1;
|
||||
|
||||
// AppIndicator exports the menu to the desktop shell, which lets GNOME, KDE,
|
||||
// and compatible panels render it with their own theme. Load either maintained
|
||||
// implementation dynamically so the legacy XEmbed path remains available.
|
||||
struct DesktopIndicatorApi {
|
||||
using SignalCallback = void (*)(void*, void*);
|
||||
using SignalDestroyNotify = void (*)(void*, void*);
|
||||
|
||||
void* library = nullptr;
|
||||
void* (*indicator_new_with_path)(const char*, const char*, int,
|
||||
const char*) = nullptr;
|
||||
void (*indicator_set_status)(void*, int) = nullptr;
|
||||
void (*indicator_set_menu)(void*, void*) = nullptr;
|
||||
void (*indicator_set_title)(void*, const char*) = nullptr;
|
||||
int (*gtk_init_check)(int*, char***) = nullptr;
|
||||
void* (*gtk_menu_new)() = nullptr;
|
||||
void* (*gtk_menu_item_new_with_label)(const char*) = nullptr;
|
||||
void* (*gtk_separator_menu_item_new)() = nullptr;
|
||||
void (*gtk_menu_shell_append)(void*, void*) = nullptr;
|
||||
void (*gtk_widget_show_all)(void*) = nullptr;
|
||||
void (*gtk_widget_destroy)(void*) = nullptr;
|
||||
int (*gtk_events_pending)() = nullptr;
|
||||
int (*gtk_main_iteration_do)(int) = nullptr;
|
||||
unsigned long (*signal_connect_data)(void*, const char*, SignalCallback,
|
||||
void*, SignalDestroyNotify,
|
||||
int) = nullptr;
|
||||
void (*object_unref)(void*) = nullptr;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
bool LoadDesktopIndicatorSymbol(void* library, T* function,
|
||||
const char* symbol_name) {
|
||||
*function = reinterpret_cast<T>(dlsym(library, symbol_name));
|
||||
return *function != nullptr;
|
||||
}
|
||||
|
||||
void UnloadDesktopIndicatorApi(DesktopIndicatorApi* api) {
|
||||
if (api->library) {
|
||||
dlclose(api->library);
|
||||
}
|
||||
*api = DesktopIndicatorApi{};
|
||||
}
|
||||
|
||||
bool LoadDesktopIndicatorApi(DesktopIndicatorApi* api) {
|
||||
for (const char* library_name : {"libayatana-appindicator3.so.1",
|
||||
"libappindicator3.so.1"}) {
|
||||
api->library = dlopen(library_name, RTLD_NOW | RTLD_LOCAL);
|
||||
if (api->library) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!api->library) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool loaded =
|
||||
LoadDesktopIndicatorSymbol(api->library,
|
||||
&api->indicator_new_with_path,
|
||||
"app_indicator_new_with_path") &&
|
||||
LoadDesktopIndicatorSymbol(api->library, &api->indicator_set_status,
|
||||
"app_indicator_set_status") &&
|
||||
LoadDesktopIndicatorSymbol(api->library, &api->indicator_set_menu,
|
||||
"app_indicator_set_menu") &&
|
||||
LoadDesktopIndicatorSymbol(api->library, &api->indicator_set_title,
|
||||
"app_indicator_set_title") &&
|
||||
LoadDesktopIndicatorSymbol(api->library, &api->gtk_init_check,
|
||||
"gtk_init_check") &&
|
||||
LoadDesktopIndicatorSymbol(api->library, &api->gtk_menu_new,
|
||||
"gtk_menu_new") &&
|
||||
LoadDesktopIndicatorSymbol(api->library,
|
||||
&api->gtk_menu_item_new_with_label,
|
||||
"gtk_menu_item_new_with_label") &&
|
||||
LoadDesktopIndicatorSymbol(api->library,
|
||||
&api->gtk_separator_menu_item_new,
|
||||
"gtk_separator_menu_item_new") &&
|
||||
LoadDesktopIndicatorSymbol(api->library, &api->gtk_menu_shell_append,
|
||||
"gtk_menu_shell_append") &&
|
||||
LoadDesktopIndicatorSymbol(api->library, &api->gtk_widget_show_all,
|
||||
"gtk_widget_show_all") &&
|
||||
LoadDesktopIndicatorSymbol(api->library, &api->gtk_widget_destroy,
|
||||
"gtk_widget_destroy") &&
|
||||
LoadDesktopIndicatorSymbol(api->library, &api->gtk_events_pending,
|
||||
"gtk_events_pending") &&
|
||||
LoadDesktopIndicatorSymbol(api->library, &api->gtk_main_iteration_do,
|
||||
"gtk_main_iteration_do") &&
|
||||
LoadDesktopIndicatorSymbol(api->library, &api->signal_connect_data,
|
||||
"g_signal_connect_data") &&
|
||||
LoadDesktopIndicatorSymbol(api->library, &api->object_unref,
|
||||
"g_object_unref");
|
||||
if (!loaded) {
|
||||
UnloadDesktopIndicatorApi(api);
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
bool IsAsciiPrintable(const std::string& text) {
|
||||
for (unsigned char ch : text) {
|
||||
@@ -50,7 +158,21 @@ bool IsAsciiPrintable(const std::string& text) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string GetMenuLabel(int language_index) {
|
||||
std::string GetShowMainWindowMenuLabel(int language_index) {
|
||||
const int normalized_index = localization::detail::ClampLanguageIndex(
|
||||
language_index);
|
||||
const std::string& label = localization::show_main_window[normalized_index];
|
||||
return label.empty() ? "Show Main Window" : label;
|
||||
}
|
||||
|
||||
std::string GetSettingsMenuLabel(int language_index) {
|
||||
const int normalized_index = localization::detail::ClampLanguageIndex(
|
||||
language_index);
|
||||
const std::string& label = localization::settings[normalized_index];
|
||||
return label.empty() ? "Settings" : label;
|
||||
}
|
||||
|
||||
std::string GetExitMenuLabel(int language_index) {
|
||||
const int normalized_index = localization::detail::ClampLanguageIndex(
|
||||
language_index);
|
||||
const std::string& label = localization::exit_program[normalized_index];
|
||||
@@ -73,6 +195,62 @@ unsigned long AllocateColor(Display* display, int screen, const char* name,
|
||||
return fallback;
|
||||
}
|
||||
|
||||
void FillRoundedRectangle(Display* display, Drawable drawable, GC gc, int x,
|
||||
int y, int width, int height, int radius) {
|
||||
if (!display || !drawable || !gc || width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int clamped_radius =
|
||||
std::clamp(radius, 0, std::min(width, height) / 2);
|
||||
if (clamped_radius == 0) {
|
||||
XFillRectangle(display, drawable, gc, x, y, width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
const int diameter = clamped_radius * 2;
|
||||
XFillRectangle(display, drawable, gc, x + clamped_radius, y,
|
||||
width - diameter, height);
|
||||
XFillRectangle(display, drawable, gc, x, y + clamped_radius, width,
|
||||
height - diameter);
|
||||
XFillArc(display, drawable, gc, x, y, diameter, diameter, 0, 360 * 64);
|
||||
XFillArc(display, drawable, gc, x + width - diameter, y, diameter, diameter,
|
||||
0, 360 * 64);
|
||||
XFillArc(display, drawable, gc, x, y + height - diameter, diameter, diameter,
|
||||
0, 360 * 64);
|
||||
XFillArc(display, drawable, gc, x + width - diameter,
|
||||
y + height - diameter, diameter, diameter, 0, 360 * 64);
|
||||
}
|
||||
|
||||
bool IsPointInsideRoundedRectangle(int point_x, int point_y, int x, int y,
|
||||
int width, int height, int radius) {
|
||||
if (point_x < x || point_y < y || point_x >= x + width ||
|
||||
point_y >= y + height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int clamped_radius =
|
||||
std::clamp(radius, 0, std::min(width, height) / 2);
|
||||
if (clamped_radius == 0 ||
|
||||
(point_x >= x + clamped_radius &&
|
||||
point_x < x + width - clamped_radius) ||
|
||||
(point_y >= y + clamped_radius &&
|
||||
point_y < y + height - clamped_radius)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const int center_x = point_x < x + clamped_radius
|
||||
? x + clamped_radius
|
||||
: x + width - clamped_radius - 1;
|
||||
const int center_y = point_y < y + clamped_radius
|
||||
? y + clamped_radius
|
||||
: y + height - clamped_radius - 1;
|
||||
const int delta_x = point_x - center_x;
|
||||
const int delta_y = point_y - center_y;
|
||||
return delta_x * delta_x + delta_y * delta_y <=
|
||||
clamped_radius * clamped_radius;
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> BuildIconCandidatePaths() {
|
||||
std::vector<std::filesystem::path> paths = {
|
||||
"icons/linux/crossdesk_32x32.png",
|
||||
@@ -117,6 +295,22 @@ std::vector<std::filesystem::path> BuildIconCandidatePaths() {
|
||||
return paths;
|
||||
}
|
||||
|
||||
std::pair<std::string, std::string> ResolveDesktopIndicatorIcon() {
|
||||
for (const auto& candidate : BuildIconCandidatePaths()) {
|
||||
std::error_code error;
|
||||
if (!std::filesystem::exists(candidate, error) || error) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto absolute_path = std::filesystem::absolute(candidate, error);
|
||||
if (!error) {
|
||||
return {absolute_path.stem().string(),
|
||||
absolute_path.parent_path().string()};
|
||||
}
|
||||
}
|
||||
return {"crossdesk", {}};
|
||||
}
|
||||
|
||||
int CountMaskBits(unsigned long mask) {
|
||||
int count = 0;
|
||||
while (mask) {
|
||||
@@ -187,20 +381,42 @@ struct LinuxTrayImpl {
|
||||
tooltip(std::move(tray_tooltip)),
|
||||
language_index(language_index_value),
|
||||
exit_event_type(tray_exit_event_type),
|
||||
menu_label(GetMenuLabel(language_index_value)),
|
||||
menu_ascii_label(IsAsciiPrintable(menu_label) ? menu_label : "Exit") {}
|
||||
show_menu_label(GetShowMainWindowMenuLabel(language_index_value)),
|
||||
show_menu_ascii_label(IsAsciiPrintable(show_menu_label)
|
||||
? show_menu_label
|
||||
: "Show Main Window"),
|
||||
settings_menu_label(GetSettingsMenuLabel(language_index_value)),
|
||||
settings_menu_ascii_label(IsAsciiPrintable(settings_menu_label)
|
||||
? settings_menu_label
|
||||
: "Settings"),
|
||||
exit_menu_label(GetExitMenuLabel(language_index_value)),
|
||||
exit_menu_ascii_label(IsAsciiPrintable(exit_menu_label)
|
||||
? exit_menu_label
|
||||
: "Exit") {}
|
||||
|
||||
explicit LinuxTrayImpl(std::function<void()> show_window_callback,
|
||||
std::function<void()> hide_window_callback,
|
||||
std::function<void()> open_settings_callback,
|
||||
std::function<void()> exit_callback,
|
||||
std::string tray_tooltip, int language_index_value)
|
||||
: show_window(std::move(show_window_callback)),
|
||||
hide_window(std::move(hide_window_callback)),
|
||||
open_settings(std::move(open_settings_callback)),
|
||||
exit_app(std::move(exit_callback)),
|
||||
tooltip(std::move(tray_tooltip)),
|
||||
language_index(language_index_value),
|
||||
menu_label(GetMenuLabel(language_index_value)),
|
||||
menu_ascii_label(IsAsciiPrintable(menu_label) ? menu_label : "Exit") {}
|
||||
show_menu_label(GetShowMainWindowMenuLabel(language_index_value)),
|
||||
show_menu_ascii_label(IsAsciiPrintable(show_menu_label)
|
||||
? show_menu_label
|
||||
: "Show Main Window"),
|
||||
settings_menu_label(GetSettingsMenuLabel(language_index_value)),
|
||||
settings_menu_ascii_label(IsAsciiPrintable(settings_menu_label)
|
||||
? settings_menu_label
|
||||
: "Settings"),
|
||||
exit_menu_label(GetExitMenuLabel(language_index_value)),
|
||||
exit_menu_ascii_label(IsAsciiPrintable(exit_menu_label)
|
||||
? exit_menu_label
|
||||
: "Exit") {}
|
||||
|
||||
~LinuxTrayImpl() { RemoveTrayIcon(); }
|
||||
|
||||
@@ -218,6 +434,7 @@ struct LinuxTrayImpl {
|
||||
}
|
||||
|
||||
void RemoveTrayIcon() {
|
||||
RemoveDesktopIndicator();
|
||||
HideMenu();
|
||||
|
||||
if (display && icon_window) {
|
||||
@@ -264,9 +481,137 @@ struct LinuxTrayImpl {
|
||||
tray_manager_window = 0;
|
||||
}
|
||||
|
||||
void ProcessEvents() { ProcessPendingEvents(kMaxTrayEventsPerTick); }
|
||||
void ProcessEvents() {
|
||||
ProcessDesktopIndicatorEvents();
|
||||
ProcessPendingEvents(kMaxTrayEventsPerTick);
|
||||
}
|
||||
|
||||
private:
|
||||
static void OnDesktopShowWindow(void*, void* user_data) {
|
||||
auto* self = static_cast<LinuxTrayImpl*>(user_data);
|
||||
if (self) {
|
||||
self->ShowWindow();
|
||||
}
|
||||
}
|
||||
|
||||
static void OnDesktopOpenSettings(void*, void* user_data) {
|
||||
auto* self = static_cast<LinuxTrayImpl*>(user_data);
|
||||
if (self) {
|
||||
self->OpenSettings();
|
||||
}
|
||||
}
|
||||
|
||||
static void OnDesktopExit(void*, void* user_data) {
|
||||
auto* self = static_cast<LinuxTrayImpl*>(user_data);
|
||||
if (self) {
|
||||
self->RequestExit();
|
||||
}
|
||||
}
|
||||
|
||||
bool EnsureDesktopIndicator() {
|
||||
if (desktop_indicator) {
|
||||
return true;
|
||||
}
|
||||
if (desktop_indicator_attempted) {
|
||||
return false;
|
||||
}
|
||||
desktop_indicator_attempted = true;
|
||||
|
||||
if (!LoadDesktopIndicatorApi(&desktop_indicator_api)) {
|
||||
LOG_INFO(
|
||||
"Desktop AppIndicator runtime unavailable; using XEmbed tray "
|
||||
"fallback");
|
||||
return false;
|
||||
}
|
||||
if (!desktop_indicator_api.gtk_init_check(nullptr, nullptr)) {
|
||||
LOG_INFO(
|
||||
"GTK could not connect to the desktop; using XEmbed tray fallback");
|
||||
return false;
|
||||
}
|
||||
|
||||
desktop_menu = desktop_indicator_api.gtk_menu_new();
|
||||
void* show_item = desktop_indicator_api.gtk_menu_item_new_with_label(
|
||||
show_menu_label.c_str());
|
||||
void* settings_item = desktop_indicator_api.gtk_menu_item_new_with_label(
|
||||
settings_menu_label.c_str());
|
||||
void* separator = desktop_indicator_api.gtk_separator_menu_item_new();
|
||||
void* exit_item = desktop_indicator_api.gtk_menu_item_new_with_label(
|
||||
exit_menu_label.c_str());
|
||||
if (!desktop_menu || !show_item || !settings_item || !separator ||
|
||||
!exit_item) {
|
||||
if (desktop_menu) {
|
||||
desktop_indicator_api.gtk_widget_destroy(desktop_menu);
|
||||
desktop_menu = nullptr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
desktop_indicator_api.signal_connect_data(
|
||||
show_item, "activate", &LinuxTrayImpl::OnDesktopShowWindow, this,
|
||||
nullptr, 0);
|
||||
desktop_indicator_api.signal_connect_data(
|
||||
settings_item, "activate", &LinuxTrayImpl::OnDesktopOpenSettings, this,
|
||||
nullptr, 0);
|
||||
desktop_indicator_api.signal_connect_data(
|
||||
exit_item, "activate", &LinuxTrayImpl::OnDesktopExit, this, nullptr, 0);
|
||||
desktop_indicator_api.gtk_menu_shell_append(desktop_menu, show_item);
|
||||
desktop_indicator_api.gtk_menu_shell_append(desktop_menu, settings_item);
|
||||
desktop_indicator_api.gtk_menu_shell_append(desktop_menu, separator);
|
||||
desktop_indicator_api.gtk_menu_shell_append(desktop_menu, exit_item);
|
||||
desktop_indicator_api.gtk_widget_show_all(desktop_menu);
|
||||
|
||||
const auto [icon_name, icon_path] = ResolveDesktopIndicatorIcon();
|
||||
desktop_indicator = desktop_indicator_api.indicator_new_with_path(
|
||||
"crossdesk", icon_name.c_str(),
|
||||
kAppIndicatorCategoryApplicationStatus,
|
||||
icon_path.empty() ? nullptr : icon_path.c_str());
|
||||
if (!desktop_indicator) {
|
||||
desktop_indicator_api.gtk_widget_destroy(desktop_menu);
|
||||
desktop_menu = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
desktop_indicator_api.indicator_set_title(desktop_indicator,
|
||||
tooltip.c_str());
|
||||
desktop_indicator_api.indicator_set_menu(desktop_indicator, desktop_menu);
|
||||
desktop_indicator_api.indicator_set_status(desktop_indicator,
|
||||
kAppIndicatorStatusActive);
|
||||
ProcessDesktopIndicatorEvents();
|
||||
LOG_INFO("Linux tray registered through the desktop AppIndicator service");
|
||||
return true;
|
||||
}
|
||||
|
||||
void RemoveDesktopIndicator() {
|
||||
if (desktop_indicator) {
|
||||
desktop_indicator_api.indicator_set_status(
|
||||
desktop_indicator, kAppIndicatorStatusPassive);
|
||||
}
|
||||
if (desktop_menu) {
|
||||
desktop_indicator_api.gtk_widget_destroy(desktop_menu);
|
||||
desktop_menu = nullptr;
|
||||
}
|
||||
if (desktop_indicator) {
|
||||
desktop_indicator_api.object_unref(desktop_indicator);
|
||||
desktop_indicator = nullptr;
|
||||
}
|
||||
// GTK registers process-wide types and callbacks. Keep the dynamically
|
||||
// loaded module resident after initialization and let process teardown
|
||||
// release it, instead of invalidating those registrations with dlclose().
|
||||
}
|
||||
|
||||
void ProcessDesktopIndicatorEvents() {
|
||||
if (!desktop_indicator) {
|
||||
return;
|
||||
}
|
||||
|
||||
int processed = 0;
|
||||
while (processed < kMaxTrayEventsPerTick &&
|
||||
desktop_indicator_api.gtk_events_pending()) {
|
||||
desktop_indicator_api.gtk_main_iteration_do(0);
|
||||
++processed;
|
||||
}
|
||||
}
|
||||
|
||||
int ProcessPendingEvents(int max_events) {
|
||||
if (!display) {
|
||||
return 0;
|
||||
@@ -314,6 +659,8 @@ struct LinuxTrayImpl {
|
||||
SelectArgbVisual();
|
||||
brand_pixel = AllocateColor(display, screen, "#2563eb", black_pixel);
|
||||
hover_pixel = AllocateColor(display, screen, "#e5e7eb", white_pixel);
|
||||
menu_border_pixel =
|
||||
AllocateColor(display, screen, "#d1d5db", black_pixel);
|
||||
int shape_event_base = 0;
|
||||
int shape_error_base = 0;
|
||||
shape_available =
|
||||
@@ -328,6 +675,10 @@ struct LinuxTrayImpl {
|
||||
xembed_info_atom = XInternAtom(display, "_XEMBED_INFO", False);
|
||||
utf8_string_atom = XInternAtom(display, "UTF8_STRING", False);
|
||||
net_wm_name_atom = XInternAtom(display, "_NET_WM_NAME", False);
|
||||
net_wm_window_type_atom =
|
||||
XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
|
||||
net_wm_window_type_popup_menu_atom =
|
||||
XInternAtom(display, "_NET_WM_WINDOW_TYPE_POPUP_MENU", False);
|
||||
|
||||
char** missing_charset_list = nullptr;
|
||||
int missing_charset_count = 0;
|
||||
@@ -386,6 +737,9 @@ struct LinuxTrayImpl {
|
||||
}
|
||||
|
||||
bool EnsureTrayIcon() {
|
||||
if (EnsureDesktopIndicator()) {
|
||||
return true;
|
||||
}
|
||||
if (docked && icon_window) {
|
||||
return true;
|
||||
}
|
||||
@@ -540,6 +894,17 @@ struct LinuxTrayImpl {
|
||||
HandleButtonRelease(event.xbutton);
|
||||
break;
|
||||
|
||||
case MotionNotify:
|
||||
if (event.xmotion.window == menu_window) {
|
||||
const int hovered_item =
|
||||
MenuItemAt(event.xmotion.x, event.xmotion.y);
|
||||
if (hovered_item != hovered_menu_item) {
|
||||
hovered_menu_item = hovered_item;
|
||||
DrawMenu();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case LeaveNotify:
|
||||
if (event.xcrossing.window == menu_window) {
|
||||
HideMenu();
|
||||
@@ -553,12 +918,14 @@ struct LinuxTrayImpl {
|
||||
|
||||
void HandleButtonRelease(const XButtonEvent& event) {
|
||||
if (menu_window && event.window == menu_window) {
|
||||
const bool inside_menu =
|
||||
event.x >= 0 && event.y >= 0 && event.x < menu_width &&
|
||||
event.y < kMenuHeight;
|
||||
const bool activate_exit = inside_menu && event.button == Button1;
|
||||
const int selected_item =
|
||||
event.button == Button1 ? MenuItemAt(event.x, event.y) : -1;
|
||||
HideMenu();
|
||||
if (activate_exit) {
|
||||
if (selected_item == 0) {
|
||||
ShowWindow();
|
||||
} else if (selected_item == 1) {
|
||||
OpenSettings();
|
||||
} else if (selected_item == 2) {
|
||||
RequestExit();
|
||||
}
|
||||
return;
|
||||
@@ -571,9 +938,7 @@ struct LinuxTrayImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.button == Button1) {
|
||||
ShowWindow();
|
||||
} else if (event.button == Button3) {
|
||||
if (event.button == Button1 || event.button == Button3) {
|
||||
ShowMenu(event.x_root, event.y_root);
|
||||
}
|
||||
}
|
||||
@@ -824,29 +1189,76 @@ struct LinuxTrayImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
int MenuTextWidth() const {
|
||||
int MenuTextWidth(const std::string& label,
|
||||
const std::string& ascii_label) const {
|
||||
if (menu_font) {
|
||||
XGlyphInfo extents{};
|
||||
XftTextExtentsUtf8(display, menu_font,
|
||||
reinterpret_cast<const FcChar8*>(menu_label.c_str()),
|
||||
static_cast<int>(menu_label.size()), &extents);
|
||||
reinterpret_cast<const FcChar8*>(label.c_str()),
|
||||
static_cast<int>(label.size()), &extents);
|
||||
return extents.xOff;
|
||||
}
|
||||
|
||||
if (font_set) {
|
||||
XRectangle ink{};
|
||||
XRectangle logical{};
|
||||
Xutf8TextExtents(font_set, menu_label.c_str(),
|
||||
static_cast<int>(menu_label.size()), &ink, &logical);
|
||||
Xutf8TextExtents(font_set, label.c_str(), static_cast<int>(label.size()),
|
||||
&ink, &logical);
|
||||
return logical.width;
|
||||
}
|
||||
|
||||
if (fallback_font) {
|
||||
return XTextWidth(fallback_font, menu_ascii_label.c_str(),
|
||||
static_cast<int>(menu_ascii_label.size()));
|
||||
return XTextWidth(fallback_font, ascii_label.c_str(),
|
||||
static_cast<int>(ascii_label.size()));
|
||||
}
|
||||
|
||||
return static_cast<int>(menu_ascii_label.size()) * 8;
|
||||
return static_cast<int>(ascii_label.size()) * 8;
|
||||
}
|
||||
|
||||
int MenuItemAt(int x, int y) const {
|
||||
if (!IsPointInsideRoundedRectangle(x, y, 0, 0, menu_width, kMenuHeight,
|
||||
kMenuCornerRadius)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int content_y = y - kMenuVerticalPadding;
|
||||
if (content_y < 0 ||
|
||||
content_y >= kMenuItemHeight * kMenuItemCount) {
|
||||
return -1;
|
||||
}
|
||||
return content_y / kMenuItemHeight;
|
||||
}
|
||||
|
||||
void ApplyMenuShape() {
|
||||
if (!shape_available || !display || !menu_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
Pixmap shape_mask =
|
||||
XCreatePixmap(display, menu_window, menu_width, kMenuHeight, 1);
|
||||
if (!shape_mask) {
|
||||
return;
|
||||
}
|
||||
|
||||
GC shape_gc = XCreateGC(display, shape_mask, 0, nullptr);
|
||||
if (!shape_gc) {
|
||||
XFreePixmap(display, shape_mask);
|
||||
return;
|
||||
}
|
||||
|
||||
XSetForeground(display, shape_gc, 0);
|
||||
XFillRectangle(display, shape_mask, shape_gc, 0, 0, menu_width,
|
||||
kMenuHeight);
|
||||
XSetForeground(display, shape_gc, 1);
|
||||
FillRoundedRectangle(display, shape_mask, shape_gc, 0, 0, menu_width,
|
||||
kMenuHeight, kMenuCornerRadius);
|
||||
XShapeCombineMask(display, menu_window, ShapeBounding, 0, 0, shape_mask,
|
||||
ShapeSet);
|
||||
XShapeCombineMask(display, menu_window, ShapeInput, 0, 0, shape_mask,
|
||||
ShapeSet);
|
||||
|
||||
XFreeGC(display, shape_gc);
|
||||
XFreePixmap(display, shape_mask);
|
||||
}
|
||||
|
||||
void ShowMenu(int root_x, int root_y) {
|
||||
@@ -856,7 +1268,15 @@ struct LinuxTrayImpl {
|
||||
|
||||
HideMenu();
|
||||
|
||||
menu_width = std::max(72, MenuTextWidth() + kMenuHorizontalPadding * 2);
|
||||
menu_width =
|
||||
std::max({72,
|
||||
MenuTextWidth(show_menu_label, show_menu_ascii_label) +
|
||||
kMenuHorizontalPadding * 2,
|
||||
MenuTextWidth(settings_menu_label,
|
||||
settings_menu_ascii_label) +
|
||||
kMenuHorizontalPadding * 2,
|
||||
MenuTextWidth(exit_menu_label, exit_menu_ascii_label) +
|
||||
kMenuHorizontalPadding * 2});
|
||||
const int display_width = DisplayWidth(display, screen);
|
||||
const int display_height = DisplayHeight(display, screen);
|
||||
const int x = std::clamp(root_x, 0, std::max(0, display_width - menu_width));
|
||||
@@ -865,23 +1285,31 @@ struct LinuxTrayImpl {
|
||||
|
||||
XSetWindowAttributes attrs{};
|
||||
attrs.override_redirect = True;
|
||||
attrs.background_pixel = white_pixel;
|
||||
attrs.border_pixel = black_pixel;
|
||||
attrs.background_pixel = menu_border_pixel;
|
||||
attrs.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask |
|
||||
LeaveWindowMask;
|
||||
PointerMotionMask | LeaveWindowMask;
|
||||
menu_window = XCreateWindow(
|
||||
display, root_window, x, y, menu_width, kMenuHeight, 1, CopyFromParent,
|
||||
display, root_window, x, y, menu_width, kMenuHeight, 0, CopyFromParent,
|
||||
InputOutput, CopyFromParent,
|
||||
CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &attrs);
|
||||
CWOverrideRedirect | CWBackPixel | CWEventMask, &attrs);
|
||||
if (!menu_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (net_wm_window_type_atom && net_wm_window_type_popup_menu_atom) {
|
||||
const Atom window_type = net_wm_window_type_popup_menu_atom;
|
||||
XChangeProperty(display, menu_window, net_wm_window_type_atom, XA_ATOM,
|
||||
32, PropModeReplace,
|
||||
reinterpret_cast<const unsigned char*>(&window_type), 1);
|
||||
}
|
||||
ApplyMenuShape();
|
||||
|
||||
menu_visible = true;
|
||||
hovered_menu_item = -1;
|
||||
XMapRaised(display, menu_window);
|
||||
XGrabPointer(display, menu_window, False,
|
||||
ButtonPressMask | ButtonReleaseMask, GrabModeAsync,
|
||||
GrabModeAsync, None, None, CurrentTime);
|
||||
ButtonPressMask | ButtonReleaseMask | PointerMotionMask,
|
||||
GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
|
||||
DrawMenu();
|
||||
XFlush(display);
|
||||
}
|
||||
@@ -889,6 +1317,7 @@ struct LinuxTrayImpl {
|
||||
void HideMenu() {
|
||||
if (!display || !menu_window) {
|
||||
menu_visible = false;
|
||||
hovered_menu_item = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -896,6 +1325,7 @@ struct LinuxTrayImpl {
|
||||
XDestroyWindow(display, menu_window);
|
||||
menu_window = 0;
|
||||
menu_visible = false;
|
||||
hovered_menu_item = -1;
|
||||
XFlush(display);
|
||||
}
|
||||
|
||||
@@ -905,47 +1335,82 @@ struct LinuxTrayImpl {
|
||||
}
|
||||
|
||||
GC gc = XCreateGC(display, menu_window, 0, nullptr);
|
||||
XSetForeground(display, gc, menu_border_pixel);
|
||||
FillRoundedRectangle(display, menu_window, gc, 0, 0, menu_width,
|
||||
kMenuHeight, kMenuCornerRadius);
|
||||
XSetForeground(display, gc, white_pixel);
|
||||
XFillRectangle(display, menu_window, gc, 0, 0, menu_width, kMenuHeight);
|
||||
FillRoundedRectangle(
|
||||
display, menu_window, gc, kMenuBorderWidth, kMenuBorderWidth,
|
||||
menu_width - kMenuBorderWidth * 2,
|
||||
kMenuHeight - kMenuBorderWidth * 2,
|
||||
kMenuCornerRadius - kMenuBorderWidth);
|
||||
if (hovered_menu_item >= 0 && hovered_menu_item < kMenuItemCount) {
|
||||
XSetForeground(display, gc, hover_pixel);
|
||||
FillRoundedRectangle(
|
||||
display, menu_window, gc, kMenuItemHorizontalInset,
|
||||
kMenuVerticalPadding + hovered_menu_item * kMenuItemHeight +
|
||||
kMenuItemVerticalInset,
|
||||
menu_width - kMenuItemHorizontalInset * 2,
|
||||
kMenuItemHeight - kMenuItemVerticalInset * 2,
|
||||
kMenuItemCornerRadius);
|
||||
}
|
||||
XSetForeground(display, gc, hover_pixel);
|
||||
XFillRectangle(display, menu_window, gc, 1, 1, menu_width - 2,
|
||||
kMenuHeight - 2);
|
||||
XDrawLine(display, menu_window, gc, kMenuSeparatorHorizontalInset,
|
||||
kMenuVerticalPadding + kMenuItemHeight * 2,
|
||||
menu_width - kMenuSeparatorHorizontalInset,
|
||||
kMenuVerticalPadding + kMenuItemHeight * 2);
|
||||
XSetForeground(display, gc, black_pixel);
|
||||
|
||||
int baseline = kMenuHeight / 2 + 5;
|
||||
if (menu_font && menu_text_color_allocated) {
|
||||
XftDraw* draw = XftDrawCreate(display, menu_window,
|
||||
DefaultVisual(display, screen),
|
||||
DefaultColormap(display, screen));
|
||||
if (draw) {
|
||||
baseline = (kMenuHeight - (menu_font->ascent + menu_font->descent)) /
|
||||
XftDraw* xft_draw = menu_font && menu_text_color_allocated
|
||||
? XftDrawCreate(display, menu_window,
|
||||
DefaultVisual(display, screen),
|
||||
DefaultColormap(display, screen))
|
||||
: nullptr;
|
||||
auto draw_label = [&](int item_index, const std::string& label,
|
||||
const std::string& ascii_label) {
|
||||
const int item_y =
|
||||
kMenuVerticalPadding + item_index * kMenuItemHeight;
|
||||
int baseline = item_y + kMenuItemHeight / 2 + 5;
|
||||
if (xft_draw) {
|
||||
baseline = item_y +
|
||||
(kMenuItemHeight -
|
||||
(menu_font->ascent + menu_font->descent)) /
|
||||
2 +
|
||||
menu_font->ascent;
|
||||
XftDrawStringUtf8(
|
||||
draw, &menu_text_color, menu_font, kMenuHorizontalPadding, baseline,
|
||||
reinterpret_cast<const FcChar8*>(menu_label.c_str()),
|
||||
static_cast<int>(menu_label.size()));
|
||||
XftDrawDestroy(draw);
|
||||
xft_draw, &menu_text_color, menu_font, kMenuHorizontalPadding,
|
||||
baseline, reinterpret_cast<const FcChar8*>(label.c_str()),
|
||||
static_cast<int>(label.size()));
|
||||
} else if (font_set) {
|
||||
XFontSetExtents* extents = XExtentsOfFontSet(font_set);
|
||||
if (extents) {
|
||||
baseline = item_y +
|
||||
(kMenuItemHeight -
|
||||
extents->max_logical_extent.height) /
|
||||
2 -
|
||||
extents->max_logical_extent.y;
|
||||
}
|
||||
Xutf8DrawString(display, menu_window, font_set, gc,
|
||||
kMenuHorizontalPadding, baseline, label.c_str(),
|
||||
static_cast<int>(label.size()));
|
||||
} else {
|
||||
if (fallback_font) {
|
||||
XSetFont(display, gc, fallback_font->fid);
|
||||
baseline = item_y +
|
||||
(kMenuItemHeight + fallback_font->ascent -
|
||||
fallback_font->descent) /
|
||||
2;
|
||||
}
|
||||
XDrawString(display, menu_window, gc, kMenuHorizontalPadding, baseline,
|
||||
ascii_label.c_str(),
|
||||
static_cast<int>(ascii_label.size()));
|
||||
}
|
||||
} else if (font_set) {
|
||||
XFontSetExtents* extents = XExtentsOfFontSet(font_set);
|
||||
if (extents) {
|
||||
baseline = (kMenuHeight - extents->max_logical_extent.height) / 2 -
|
||||
extents->max_logical_extent.y;
|
||||
}
|
||||
Xutf8DrawString(display, menu_window, font_set, gc,
|
||||
kMenuHorizontalPadding, baseline, menu_label.c_str(),
|
||||
static_cast<int>(menu_label.size()));
|
||||
} else {
|
||||
if (fallback_font) {
|
||||
XSetFont(display, gc, fallback_font->fid);
|
||||
baseline = (kMenuHeight + fallback_font->ascent -
|
||||
fallback_font->descent) /
|
||||
2;
|
||||
}
|
||||
XDrawString(display, menu_window, gc, kMenuHorizontalPadding, baseline,
|
||||
menu_ascii_label.c_str(),
|
||||
static_cast<int>(menu_ascii_label.size()));
|
||||
};
|
||||
draw_label(0, show_menu_label, show_menu_ascii_label);
|
||||
draw_label(1, settings_menu_label, settings_menu_ascii_label);
|
||||
draw_label(2, exit_menu_label, exit_menu_ascii_label);
|
||||
if (xft_draw) {
|
||||
XftDrawDestroy(xft_draw);
|
||||
}
|
||||
|
||||
XFreeGC(display, gc);
|
||||
@@ -962,6 +1427,14 @@ struct LinuxTrayImpl {
|
||||
}
|
||||
}
|
||||
|
||||
void OpenSettings() {
|
||||
if (open_settings) {
|
||||
open_settings();
|
||||
} else {
|
||||
ShowWindow();
|
||||
}
|
||||
}
|
||||
|
||||
void RequestExit() {
|
||||
if (exit_app) {
|
||||
exit_app();
|
||||
@@ -978,12 +1451,21 @@ struct LinuxTrayImpl {
|
||||
::SDL_Window* app_window = nullptr;
|
||||
std::function<void()> show_window;
|
||||
std::function<void()> hide_window;
|
||||
std::function<void()> open_settings;
|
||||
std::function<void()> exit_app;
|
||||
std::string tooltip;
|
||||
int language_index = 0;
|
||||
uint32_t exit_event_type = 0;
|
||||
std::string menu_label;
|
||||
std::string menu_ascii_label;
|
||||
std::string show_menu_label;
|
||||
std::string show_menu_ascii_label;
|
||||
std::string settings_menu_label;
|
||||
std::string settings_menu_ascii_label;
|
||||
std::string exit_menu_label;
|
||||
std::string exit_menu_ascii_label;
|
||||
DesktopIndicatorApi desktop_indicator_api;
|
||||
void* desktop_indicator = nullptr;
|
||||
void* desktop_menu = nullptr;
|
||||
bool desktop_indicator_attempted = false;
|
||||
Display* display = nullptr;
|
||||
int screen = 0;
|
||||
::Window root_window = 0;
|
||||
@@ -996,10 +1478,13 @@ struct LinuxTrayImpl {
|
||||
Atom xembed_info_atom = None;
|
||||
Atom utf8_string_atom = None;
|
||||
Atom net_wm_name_atom = None;
|
||||
Atom net_wm_window_type_atom = None;
|
||||
Atom net_wm_window_type_popup_menu_atom = None;
|
||||
unsigned long black_pixel = 0;
|
||||
unsigned long white_pixel = 0;
|
||||
unsigned long brand_pixel = 0;
|
||||
unsigned long hover_pixel = 0;
|
||||
unsigned long menu_border_pixel = 0;
|
||||
Visual* icon_visual = nullptr;
|
||||
int icon_depth = 0;
|
||||
Colormap icon_colormap = 0;
|
||||
@@ -1014,6 +1499,7 @@ struct LinuxTrayImpl {
|
||||
int icon_width = 0;
|
||||
int icon_height = 0;
|
||||
int menu_width = 72;
|
||||
int hovered_menu_item = -1;
|
||||
bool docked = false;
|
||||
bool embedded = false;
|
||||
bool menu_visible = false;
|
||||
@@ -1028,11 +1514,13 @@ LinuxTray::LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
|
||||
|
||||
LinuxTray::LinuxTray(std::function<void()> show_window,
|
||||
std::function<void()> hide_window,
|
||||
std::function<void()> open_settings,
|
||||
std::function<void()> exit_app,
|
||||
const std::string& tooltip, int language_index)
|
||||
: impl_(std::make_unique<LinuxTrayImpl>(
|
||||
std::move(show_window), std::move(hide_window), std::move(exit_app),
|
||||
tooltip, language_index)) {}
|
||||
std::move(show_window), std::move(hide_window),
|
||||
std::move(open_settings), std::move(exit_app), tooltip,
|
||||
language_index)) {}
|
||||
|
||||
LinuxTray::~LinuxTray() = default;
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ class LinuxTray {
|
||||
LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
|
||||
int language_index, uint32_t exit_event_type);
|
||||
LinuxTray(std::function<void()> show_window,
|
||||
std::function<void()> hide_window, std::function<void()> exit_app,
|
||||
std::function<void()> hide_window,
|
||||
std::function<void()> open_settings,
|
||||
std::function<void()> exit_app,
|
||||
const std::string& tooltip, int language_index);
|
||||
~LinuxTray();
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
@interface CrossDeskMacTrayTarget : NSObject
|
||||
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner;
|
||||
- (void)statusItemClicked:(id)sender;
|
||||
- (void)showMainWindow:(id)sender;
|
||||
- (void)openSettings:(id)sender;
|
||||
- (void)exitApplication:(id)sender;
|
||||
@end
|
||||
@@ -84,6 +85,19 @@ struct MacTrayImpl {
|
||||
}
|
||||
|
||||
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"CrossDesk"];
|
||||
[menu setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameAqua]];
|
||||
|
||||
NSString *show_main_window_title =
|
||||
NSStringFromUtf8(localization::show_main_window
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
language_index)]);
|
||||
NSMenuItem *show_main_window_item =
|
||||
[[NSMenuItem alloc] initWithTitle:show_main_window_title
|
||||
action:@selector(showMainWindow:)
|
||||
keyEquivalent:@""];
|
||||
[show_main_window_item setTarget:target];
|
||||
[menu addItem:show_main_window_item];
|
||||
|
||||
NSString *settings_title =
|
||||
NSStringFromUtf8(localization::settings
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
@@ -287,14 +301,7 @@ void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
|
||||
if (!owner_) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSEvent *event = [NSApp currentEvent];
|
||||
if (event && [event type] == NSEventTypeRightMouseUp) {
|
||||
owner_->ShowMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
owner_->ShowWindow();
|
||||
owner_->ShowMenu();
|
||||
}
|
||||
|
||||
- (void)exitApplication:(id)sender {
|
||||
@@ -304,6 +311,13 @@ void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showMainWindow:(id)sender {
|
||||
(void)sender;
|
||||
if (owner_) {
|
||||
owner_->ShowWindow();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)openSettings:(id)sender {
|
||||
(void)sender;
|
||||
if (owner_) {
|
||||
|
||||
@@ -64,11 +64,13 @@ WinTray::WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
|
||||
|
||||
WinTray::WinTray(std::function<void()> show_window,
|
||||
std::function<void()> hide_window,
|
||||
std::function<void()> open_settings,
|
||||
std::function<void()> exit_app, HICON icon,
|
||||
const std::wstring& tooltip, int language_index)
|
||||
: WinTray(nullptr, icon, tooltip, language_index) {
|
||||
show_window_ = std::move(show_window);
|
||||
hide_window_ = std::move(hide_window);
|
||||
open_settings_ = std::move(open_settings);
|
||||
exit_app_ = std::move(exit_app);
|
||||
}
|
||||
|
||||
@@ -97,6 +99,14 @@ void WinTray::ShowApplicationWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
void WinTray::OpenSettings() {
|
||||
if (open_settings_) {
|
||||
open_settings_();
|
||||
} else {
|
||||
ShowApplicationWindow();
|
||||
}
|
||||
}
|
||||
|
||||
bool WinTray::HandleTrayMessage(MSG* msg) {
|
||||
if (!msg || msg->message != WM_TRAY_CALLBACK) return false;
|
||||
|
||||
@@ -111,6 +121,20 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
|
||||
POINT pt;
|
||||
GetCursorPos(&pt);
|
||||
HMENU menu = CreatePopupMenu();
|
||||
HBRUSH menu_background = CreateSolidBrush(RGB(255, 255, 255));
|
||||
if (menu_background) {
|
||||
MENUINFO menu_info{};
|
||||
menu_info.cbSize = sizeof(menu_info);
|
||||
menu_info.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS;
|
||||
menu_info.hbrBack = menu_background;
|
||||
SetMenuInfo(menu, &menu_info);
|
||||
}
|
||||
|
||||
AppendMenuW(menu, MF_STRING, 1002,
|
||||
localization::GetShowMainWindowLabel(language_index_));
|
||||
AppendMenuW(menu, MF_STRING, 1003,
|
||||
localization::GetSettingsLabel(language_index_));
|
||||
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
|
||||
AppendMenuW(menu, MF_STRING, 1001,
|
||||
localization::GetExitProgramLabel(language_index_));
|
||||
|
||||
@@ -119,6 +143,9 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
|
||||
TrackPopupMenu(menu, TPM_RETURNCMD | TPM_NONOTIFY | TPM_LEFTALIGN,
|
||||
pt.x, pt.y, 0, hwnd_message_only_, nullptr);
|
||||
DestroyMenu(menu);
|
||||
if (menu_background) {
|
||||
DeleteObject(menu_background);
|
||||
}
|
||||
|
||||
// handle menu command
|
||||
if (cmd == 1001) {
|
||||
@@ -131,6 +158,8 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
|
||||
}
|
||||
} else if (cmd == 1002) {
|
||||
ShowApplicationWindow();
|
||||
} else if (cmd == 1003) {
|
||||
OpenSettings();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ class WinTray {
|
||||
WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
|
||||
int language_index);
|
||||
WinTray(std::function<void()> show_window,
|
||||
std::function<void()> hide_window, std::function<void()> exit_app,
|
||||
std::function<void()> hide_window,
|
||||
std::function<void()> open_settings,
|
||||
std::function<void()> exit_app,
|
||||
HICON icon, const std::wstring& tooltip, int language_index);
|
||||
~WinTray();
|
||||
|
||||
@@ -32,6 +34,7 @@ class WinTray {
|
||||
|
||||
private:
|
||||
void ShowApplicationWindow();
|
||||
void OpenSettings();
|
||||
|
||||
HWND app_hwnd_;
|
||||
HWND hwnd_message_only_;
|
||||
@@ -41,6 +44,7 @@ class WinTray {
|
||||
NOTIFYICONDATA nid_;
|
||||
std::function<void()> show_window_;
|
||||
std::function<void()> hide_window_;
|
||||
std::function<void()> open_settings_;
|
||||
std::function<void()> exit_app_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -140,8 +140,6 @@ struct UserSettingsState {
|
||||
bool enable_autostart_last_ = false;
|
||||
bool enable_daemon_ = false;
|
||||
bool enable_daemon_last_ = false;
|
||||
bool enable_minimize_to_tray_ = false;
|
||||
bool enable_minimize_to_tray_last_ = false;
|
||||
char file_transfer_save_path_buf_[512] = "";
|
||||
std::string file_transfer_save_path_last_;
|
||||
char signal_server_ip_self_[256] = "";
|
||||
|
||||
@@ -46,7 +46,6 @@ export global UiStrings {
|
||||
in-out property <string> self-hosted: "Self-hosted server";
|
||||
in-out property <string> autostart: "Start with system";
|
||||
in-out property <string> daemon: "Run in background";
|
||||
in-out property <string> minimize-to-tray: "Minimize to tray";
|
||||
in-out property <string> file-save-path: "File save path";
|
||||
in-out property <string> default-desktop: "Desktop";
|
||||
in-out property <string> server-host: "Server host";
|
||||
@@ -242,7 +241,6 @@ export component MainWindow inherits Window {
|
||||
in-out property <bool> self-hosted-enabled: false;
|
||||
in-out property <bool> autostart-enabled: false;
|
||||
in-out property <bool> daemon-enabled: false;
|
||||
in-out property <bool> minimize-to-tray-enabled: false;
|
||||
in-out property <string> file-save-path: "";
|
||||
in-out property <string> server-host: "";
|
||||
in-out property <string> server-port: "";
|
||||
@@ -1488,11 +1486,6 @@ export component MainWindow inherits Window {
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.daemon; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.minimize-to-tray; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.minimize-to-tray-enabled; enabled: !root.settings-session-active; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 154px; height: 24px; text: UiStrings.file-save-path; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
|
||||
@@ -172,6 +172,10 @@ export component StreamWindow inherits Window {
|
||||
private property <bool> display-menu-animation-enabled: false;
|
||||
private property <float> display-menu-progress: 0.0;
|
||||
private property <bool> shortcut-menu-open: false;
|
||||
// A popup can remove itself from the scene in its clicked callback before
|
||||
// the video input layer observes the same pointer release. Remember that
|
||||
// the press belonged to local UI so that release cannot reach the peer.
|
||||
private property <bool> suppress-next-remote-pointer-release: false;
|
||||
private property <bool> control-docked-left: true;
|
||||
private property <bool> control-dragging: false;
|
||||
private property <length> control-drag-x: 0px;
|
||||
@@ -554,7 +558,14 @@ export component StreamWindow inherits Window {
|
||||
&& self.mouse-y <= control.y + control.height;
|
||||
let over-local-ui = root.is-local-control-area(
|
||||
self.mouse-x, self.mouse-y);
|
||||
let suppress-remote-release =
|
||||
root.suppress-next-remote-pointer-release
|
||||
&& (event.kind == PointerEventKind.up
|
||||
|| event.kind == PointerEventKind.cancel);
|
||||
if event.kind == PointerEventKind.down && !over-local-ui {
|
||||
// Discard a stale marker if a popup consumed the
|
||||
// whole previous gesture, including its release.
|
||||
root.suppress-next-remote-pointer-release = false;
|
||||
input-focus.focus();
|
||||
}
|
||||
if event.kind == PointerEventKind.down
|
||||
@@ -568,9 +579,13 @@ export component StreamWindow inherits Window {
|
||||
// The control bar overlays the remote video. Events in
|
||||
// it and its attached menus (including padding and gaps)
|
||||
// are local UI input and must never reach the peer.
|
||||
} else if !root.control-dragging && !over-local-ui {
|
||||
} else if !root.control-dragging && !over-local-ui
|
||||
&& !suppress-remote-release {
|
||||
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
|
||||
}
|
||||
if suppress-remote-release {
|
||||
root.suppress-next-remote-pointer-release = false;
|
||||
}
|
||||
}
|
||||
moved => {
|
||||
if root.control-dragging {
|
||||
@@ -871,6 +886,11 @@ export component StreamWindow inherits Window {
|
||||
// hover stable while still consuming padding, gaps and scroll
|
||||
// events locally instead of forwarding them to the peer.
|
||||
display-touch := TouchArea {
|
||||
pointer-event(event) => {
|
||||
if event.kind == PointerEventKind.down {
|
||||
root.suppress-next-remote-pointer-release = true;
|
||||
}
|
||||
}
|
||||
clicked => {
|
||||
let index = floor((self.mouse-y - 4px) / 30px);
|
||||
let row-y = self.mouse-y - 4px - index * 30px;
|
||||
@@ -896,7 +916,14 @@ export component StreamWindow inherits Window {
|
||||
for shortcut in ["Ctrl+Alt+Del", "Win+L"]: Rectangle {
|
||||
height: 29px; background: shortcut-touch.has-hover ? #e8eef9 : transparent;
|
||||
Text { x: 8px; text: shortcut; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
shortcut-touch := TouchArea { clicked => { root.send-shortcut(shortcut); root.shortcut-menu-open = false; } }
|
||||
shortcut-touch := TouchArea {
|
||||
pointer-event(event) => {
|
||||
if event.kind == PointerEventKind.down {
|
||||
root.suppress-next-remote-pointer-release = true;
|
||||
}
|
||||
}
|
||||
clicked => { root.send-shortcut(shortcut); root.shortcut-menu-open = false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,6 @@ inline int ApplyMainWindowStrings(
|
||||
Text(localization::self_hosted_server_config[language]));
|
||||
strings.set_autostart(Text(localization::enable_autostart[language]));
|
||||
strings.set_daemon(Text(localization::enable_daemon[language]));
|
||||
strings.set_minimize_to_tray(
|
||||
Text(localization::minimize_to_tray[language]));
|
||||
strings.set_file_save_path(
|
||||
Text(localization::file_transfer_save_path[language]));
|
||||
strings.set_default_desktop(Text(localization::default_desktop[language]));
|
||||
|
||||
@@ -299,8 +299,11 @@ int GuiApplication::TitleBar(bool main_window) {
|
||||
}
|
||||
|
||||
if (close_button_clicked) {
|
||||
const bool minimized_to_tray = main_window && MinimizeMainWindowToTray();
|
||||
if (!minimized_to_tray) {
|
||||
if (main_window) {
|
||||
if (!MinimizeMainWindowToTray()) {
|
||||
SDL_HideWindow(main_window_);
|
||||
}
|
||||
} else {
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_QUIT;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
@@ -355,26 +355,6 @@ int GuiApplication::SettingWindow() {
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
{
|
||||
settings_items_offset += settings_items_padding;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%s",
|
||||
localization::minimize_to_tray[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_minimize_to_tray_",
|
||||
&enable_minimize_to_tray_);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
{
|
||||
settings_items_offset += settings_items_padding;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
@@ -618,13 +598,6 @@ int GuiApplication::SettingWindow() {
|
||||
}
|
||||
enable_daemon_last_ = enable_daemon_;
|
||||
|
||||
if (enable_minimize_to_tray_) {
|
||||
config_center_->SetMinimizeToTray(true);
|
||||
} else {
|
||||
config_center_->SetMinimizeToTray(false);
|
||||
}
|
||||
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
|
||||
|
||||
// File transfer save path
|
||||
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
|
||||
file_transfer_save_path_last_ = file_transfer_save_path_buf_;
|
||||
|
||||
+1
-1
Submodule submodules/minirtc updated: 77bba57d4f...d0a78e8bf9
@@ -1,139 +0,0 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(current / "src/gui/ui/stream_window.slint")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool ExpectContains(const char* name, const std::string& value,
|
||||
const std::string& expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectNotContains(const char* name, const std::string& value,
|
||||
const std::string& unexpected) {
|
||||
if (value.find(unexpected) == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectContainsAtLeast(const char* name, const std::string& value,
|
||||
const std::string& expected, size_t min_count) {
|
||||
size_t count = 0;
|
||||
size_t pos = 0;
|
||||
while ((pos = value.find(expected, pos)) != std::string::npos) {
|
||||
++count;
|
||||
pos += expected.size();
|
||||
}
|
||||
if (count >= min_count) {
|
||||
return true;
|
||||
}
|
||||
std::cerr << name << " expected at least " << min_count
|
||||
<< " occurrences of: " << expected << ", found " << count << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string stream =
|
||||
ReadFile(repo_root / "src/gui/ui/stream_window.slint");
|
||||
const std::string common = ReadFile(repo_root / "src/gui/ui/common.slint");
|
||||
const std::string application =
|
||||
ReadFile(repo_root / "src/gui/application/gui_application.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("stream_window.slint", stream,
|
||||
"private property <bool> display-menu-open: false;");
|
||||
ok &= ExpectContains("stream_window.slint", stream,
|
||||
"root.shortcut-menu-open = false;");
|
||||
ok &= ExpectContains("stream_window.slint", stream,
|
||||
"if root.display-menu-open: Rectangle");
|
||||
ok &=
|
||||
ExpectContains("stream_window.slint", stream, "display-touch.has-hover");
|
||||
ok &= ExpectContains("stream_window.slint", stream,
|
||||
"root.switch-display(index)");
|
||||
ok &= ExpectContains("stream_window.slint", stream,
|
||||
"if root.shortcut-menu-open: Rectangle");
|
||||
ok &=
|
||||
ExpectContains("stream_window.slint", stream, "shortcut-touch.has-hover");
|
||||
ok &= ExpectContains("stream_window.slint", stream,
|
||||
"} else if !root.control-dragging && !over-control {");
|
||||
|
||||
ok &= ExpectContains("common.slint", common,
|
||||
"if touch.has-hover && root.tooltip != \"\": Rectangle");
|
||||
ok &= ExpectContainsAtLeast("stream_window.slint", stream, "tooltip:", 9);
|
||||
ok &= ExpectContains("stream_window.slint", stream,
|
||||
"tooltip: StreamStrings.select-display");
|
||||
ok &= ExpectContains("stream_window.slint", stream,
|
||||
"tooltip: StreamStrings.send-shortcut");
|
||||
ok &=
|
||||
ExpectContains("stream_window.slint", stream,
|
||||
"root.mouse-control-enabled ? StreamStrings.release-mouse "
|
||||
": StreamStrings.control-mouse");
|
||||
ok &= ExpectContains(
|
||||
"stream_window.slint", stream,
|
||||
"root.audio-enabled ? StreamStrings.mute : StreamStrings.audio");
|
||||
ok &= ExpectContains("stream_window.slint", stream,
|
||||
"tooltip: StreamStrings.select-file");
|
||||
ok &= ExpectContains("stream_window.slint", stream,
|
||||
"root.stats-visible ? StreamStrings.hide-stats : "
|
||||
"StreamStrings.show-stats");
|
||||
ok &=
|
||||
ExpectContains("stream_window.slint", stream,
|
||||
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen "
|
||||
": StreamStrings.fullscreen");
|
||||
ok &= ExpectContains("stream_window.slint", stream,
|
||||
"tooltip: StreamStrings.disconnect");
|
||||
ok &= ExpectContains("stream_window.slint", stream,
|
||||
"root.control-expanded ? StreamStrings.collapse-control "
|
||||
": StreamStrings.expand-control");
|
||||
|
||||
ok &=
|
||||
ExpectContains("gui_application.cpp", application, "set_select_display(");
|
||||
ok &= ExpectContains("gui_application.cpp", application, "set_show_stats(");
|
||||
ok &=
|
||||
ExpectContains("gui_application.cpp", application, "set_expand_control(");
|
||||
ok &= ExpectContains("gui_application.cpp", application,
|
||||
"set_collapse_control(");
|
||||
ok &= ExpectContains("gui_application.cpp", application,
|
||||
"(*ui_->stream)->global<ui::StreamStrings>()");
|
||||
ok &=
|
||||
ExpectNotContains("gui_application.cpp", application, "#include <imgui");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -100,11 +100,6 @@ function setup_targets()
|
||||
add_includedirs("src/service/windows")
|
||||
add_files("tests/windows_sas_guard_test.cpp")
|
||||
|
||||
target("display_popup_hover_state_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_files("tests/display_popup_hover_state_test.cpp")
|
||||
|
||||
target("slint_ui_smoke_test")
|
||||
set_kind("binary")
|
||||
set_languages("c++20")
|
||||
|
||||
Reference in New Issue
Block a user