mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-07-29 03:08:42 +08:00
[refactor] organize GUI code by responsibility
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-06-23
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _LINUX_TRAY_H_
|
||||
#define _LINUX_TRAY_H_
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct LinuxTrayImpl;
|
||||
|
||||
class LinuxTray {
|
||||
public:
|
||||
LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
|
||||
int language_index, uint32_t exit_event_type);
|
||||
~LinuxTray();
|
||||
|
||||
bool MinimizeToTray();
|
||||
void RemoveTrayIcon();
|
||||
void ProcessEvents();
|
||||
|
||||
private:
|
||||
std::unique_ptr<LinuxTrayImpl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // defined(__linux__) && !defined(__APPLE__)
|
||||
|
||||
#endif // _LINUX_TRAY_H_
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-06-23
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _MAC_TRAY_H_
|
||||
#define _MAC_TRAY_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct MacTrayImpl;
|
||||
|
||||
class MacTray {
|
||||
public:
|
||||
MacTray(::SDL_Window* app_window, const std::string& tooltip,
|
||||
int language_index);
|
||||
~MacTray();
|
||||
|
||||
void MinimizeToTray();
|
||||
void RemoveTrayIcon();
|
||||
|
||||
private:
|
||||
std::unique_ptr<MacTrayImpl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _MAC_TRAY_H_
|
||||
@@ -0,0 +1,249 @@
|
||||
#include "mac_tray.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#include "localization.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
@interface CrossDeskMacTrayTarget : NSObject
|
||||
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner;
|
||||
- (void)statusItemClicked:(id)sender;
|
||||
- (void)exitApplication:(id)sender;
|
||||
@end
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct MacTrayImpl {
|
||||
explicit MacTrayImpl(::SDL_Window *window, std::string tray_tooltip,
|
||||
int language_index_value)
|
||||
: app_window(window),
|
||||
tooltip(std::move(tray_tooltip)),
|
||||
language_index(language_index_value),
|
||||
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {}
|
||||
|
||||
~MacTrayImpl() {
|
||||
RemoveTrayIcon();
|
||||
target = nil;
|
||||
}
|
||||
|
||||
void MinimizeToTray() {
|
||||
EnsureStatusItem();
|
||||
if (app_window) {
|
||||
SDL_HideWindow(app_window);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveTrayIcon() {
|
||||
if (!status_item) {
|
||||
return;
|
||||
}
|
||||
|
||||
[[NSStatusBar systemStatusBar] removeStatusItem:status_item];
|
||||
status_item = nil;
|
||||
}
|
||||
|
||||
void ShowWindow() {
|
||||
if (!app_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_ShowWindow(app_window);
|
||||
SDL_RaiseWindow(app_window);
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
}
|
||||
|
||||
void ShowMenu() {
|
||||
EnsureStatusItem();
|
||||
if (!status_item) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"CrossDesk"];
|
||||
NSString *exit_title =
|
||||
NSStringFromUtf8(localization::exit_program
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
language_index)]);
|
||||
NSMenuItem *exit_item = [[NSMenuItem alloc] initWithTitle:exit_title
|
||||
action:@selector(exitApplication:)
|
||||
keyEquivalent:@""];
|
||||
[exit_item setTarget:target];
|
||||
[menu addItem:exit_item];
|
||||
|
||||
NSStatusBarButton *button = [status_item button];
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
const NSRect bounds = [button bounds];
|
||||
[menu popUpMenuPositioningItem:nil
|
||||
atLocation:NSMakePoint(NSMinX(bounds), NSMinY(bounds))
|
||||
inView:button];
|
||||
}
|
||||
|
||||
void RequestExit() {
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_QUIT;
|
||||
SDL_PushEvent(&event);
|
||||
}
|
||||
|
||||
private:
|
||||
void EnsureStatusItem() {
|
||||
if (status_item) {
|
||||
return;
|
||||
}
|
||||
|
||||
status_item = [[NSStatusBar systemStatusBar]
|
||||
statusItemWithLength:NSSquareStatusItemLength];
|
||||
NSStatusBarButton *button = [status_item button];
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
[button setToolTip:NSStringFromUtf8(tooltip)];
|
||||
|
||||
NSImage *crossdesk_icon = LoadCrossDeskIcon();
|
||||
if (crossdesk_icon) {
|
||||
NSImage *status_icon = [crossdesk_icon copy];
|
||||
[status_icon setSize:NSMakeSize(18.0, 18.0)];
|
||||
[status_icon setTemplate:NO];
|
||||
[button setImage:status_icon];
|
||||
[button setImagePosition:NSImageOnly];
|
||||
} else {
|
||||
[button setTitle:@"CD"];
|
||||
}
|
||||
|
||||
[button setTarget:target];
|
||||
[button setAction:@selector(statusItemClicked:)];
|
||||
[button sendActionOn:NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp];
|
||||
}
|
||||
|
||||
NSString *NSStringFromUtf8(const std::string &text) {
|
||||
return [NSString stringWithUTF8String:text.c_str()];
|
||||
}
|
||||
|
||||
NSImage *LoadCrossDeskIcon() {
|
||||
NSImage *icon = LoadIconFromBundleResource(@"crossdesk");
|
||||
if (!icon) {
|
||||
icon = LoadIconFromBundleResource(@"crossedesk");
|
||||
}
|
||||
if (!icon) {
|
||||
icon = LoadIconFromDevelopmentPath();
|
||||
}
|
||||
if (!icon) {
|
||||
icon = [NSApp applicationIconImage];
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
NSImage *LoadIconFromBundleResource(NSString *resource_name) {
|
||||
NSString *icon_path =
|
||||
[[NSBundle mainBundle] pathForResource:resource_name ofType:@"icns"];
|
||||
return LoadIconFromPath(icon_path);
|
||||
}
|
||||
|
||||
NSImage *LoadIconFromDevelopmentPath() {
|
||||
NSMutableArray<NSString *> *candidate_paths = [NSMutableArray array];
|
||||
|
||||
NSString *current_directory =
|
||||
[[NSFileManager defaultManager] currentDirectoryPath];
|
||||
[candidate_paths
|
||||
addObject:[current_directory
|
||||
stringByAppendingPathComponent:
|
||||
@"icons/macos/crossdesk.icns"]];
|
||||
|
||||
const char *base_path = SDL_GetBasePath();
|
||||
if (base_path && base_path[0] != '\0') {
|
||||
NSString *base_directory = NSStringFromUtf8(base_path);
|
||||
[candidate_paths
|
||||
addObject:[base_directory
|
||||
stringByAppendingPathComponent:
|
||||
@"icons/macos/crossdesk.icns"]];
|
||||
[candidate_paths
|
||||
addObject:[base_directory
|
||||
stringByAppendingPathComponent:
|
||||
@"../../../../icons/macos/crossdesk.icns"]];
|
||||
}
|
||||
|
||||
for (NSString *candidate_path in candidate_paths) {
|
||||
NSImage *icon = LoadIconFromPath(
|
||||
[candidate_path stringByStandardizingPath]);
|
||||
if (icon) {
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSImage *LoadIconFromPath(NSString *icon_path) {
|
||||
if (![icon_path length]) {
|
||||
return nil;
|
||||
}
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:icon_path]) {
|
||||
return nil;
|
||||
}
|
||||
return [[NSImage alloc] initWithContentsOfFile:icon_path];
|
||||
}
|
||||
|
||||
::SDL_Window *app_window = nullptr;
|
||||
std::string tooltip;
|
||||
int language_index = 0;
|
||||
NSStatusItem *status_item = nil;
|
||||
CrossDeskMacTrayTarget *target = nil;
|
||||
};
|
||||
|
||||
MacTray::MacTray(::SDL_Window *app_window, const std::string &tooltip,
|
||||
int language_index)
|
||||
: impl_(
|
||||
std::make_unique<MacTrayImpl>(app_window, tooltip, language_index)) {}
|
||||
|
||||
MacTray::~MacTray() = default;
|
||||
|
||||
void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); }
|
||||
|
||||
void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@implementation CrossDeskMacTrayTarget {
|
||||
crossdesk::MacTrayImpl *owner_;
|
||||
}
|
||||
|
||||
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
owner_ = owner;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)statusItemClicked:(id)sender {
|
||||
(void)sender;
|
||||
if (!owner_) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSEvent *event = [NSApp currentEvent];
|
||||
if (event && [event type] == NSEventTypeRightMouseUp) {
|
||||
owner_->ShowMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
owner_->ShowWindow();
|
||||
}
|
||||
|
||||
- (void)exitApplication:(id)sender {
|
||||
(void)sender;
|
||||
if (owner_) {
|
||||
owner_->RequestExit();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "win_tray.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "localization.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// callback for the message-only window that handles tray icon messages
|
||||
static LRESULT CALLBACK MsgWndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
||||
LPARAM lParam) {
|
||||
WinTray* tray =
|
||||
reinterpret_cast<WinTray*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
|
||||
if (!tray) {
|
||||
return DefWindowProc(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
if (msg == WM_TRAY_CALLBACK) {
|
||||
MSG tmpMsg = {};
|
||||
tmpMsg.message = msg;
|
||||
tmpMsg.wParam = wParam;
|
||||
tmpMsg.lParam = lParam;
|
||||
tray->HandleTrayMessage(&tmpMsg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return DefWindowProc(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
WinTray::WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
|
||||
int language_index)
|
||||
: app_hwnd_(app_hwnd),
|
||||
icon_(icon),
|
||||
tip_(tooltip),
|
||||
hwnd_message_only_(nullptr),
|
||||
language_index_(language_index) {
|
||||
WNDCLASS wc = {};
|
||||
wc.lpfnWndProc = MsgWndProc;
|
||||
wc.hInstance = GetModuleHandle(nullptr);
|
||||
wc.lpszClassName = L"TrayMessageWindow";
|
||||
RegisterClass(&wc);
|
||||
|
||||
// create a message-only window to receive tray messages
|
||||
hwnd_message_only_ =
|
||||
CreateWindowEx(0, wc.lpszClassName, L"TrayMsg", 0, 0, 0, 0, 0,
|
||||
HWND_MESSAGE, nullptr, wc.hInstance, nullptr);
|
||||
|
||||
// store pointer to this WinTray instance in window data
|
||||
SetWindowLongPtr(hwnd_message_only_, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(this));
|
||||
|
||||
// initialize NOTIFYICONDATA structure
|
||||
ZeroMemory(&nid_, sizeof(nid_));
|
||||
nid_.cbSize = sizeof(nid_);
|
||||
nid_.hWnd = hwnd_message_only_;
|
||||
nid_.uID = 1;
|
||||
nid_.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
|
||||
nid_.uCallbackMessage = WM_TRAY_CALLBACK;
|
||||
nid_.hIcon = icon_;
|
||||
wcsncpy_s(nid_.szTip, tip_.c_str(), _TRUNCATE);
|
||||
}
|
||||
|
||||
WinTray::~WinTray() {
|
||||
RemoveTrayIcon();
|
||||
if (hwnd_message_only_) DestroyWindow(hwnd_message_only_);
|
||||
}
|
||||
|
||||
void WinTray::MinimizeToTray() {
|
||||
Shell_NotifyIcon(NIM_ADD, &nid_);
|
||||
// hide application window
|
||||
ShowWindow(app_hwnd_, SW_HIDE);
|
||||
}
|
||||
|
||||
void WinTray::RemoveTrayIcon() { Shell_NotifyIcon(NIM_DELETE, &nid_); }
|
||||
|
||||
bool WinTray::HandleTrayMessage(MSG* msg) {
|
||||
if (!msg || msg->message != WM_TRAY_CALLBACK) return false;
|
||||
|
||||
switch (LOWORD(msg->lParam)) {
|
||||
case WM_LBUTTONDBLCLK:
|
||||
case WM_LBUTTONUP: {
|
||||
ShowWindow(app_hwnd_, SW_SHOW);
|
||||
SetForegroundWindow(app_hwnd_);
|
||||
break;
|
||||
}
|
||||
|
||||
case WM_RBUTTONUP: {
|
||||
POINT pt;
|
||||
GetCursorPos(&pt);
|
||||
HMENU menu = CreatePopupMenu();
|
||||
AppendMenuW(menu, MF_STRING, 1001,
|
||||
localization::GetExitProgramLabel(language_index_));
|
||||
|
||||
SetForegroundWindow(hwnd_message_only_);
|
||||
int cmd =
|
||||
TrackPopupMenu(menu, TPM_RETURNCMD | TPM_NONOTIFY | TPM_LEFTALIGN,
|
||||
pt.x, pt.y, 0, hwnd_message_only_, nullptr);
|
||||
DestroyMenu(menu);
|
||||
|
||||
// handle menu command
|
||||
if (cmd == 1001) {
|
||||
// exit application
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_QUIT;
|
||||
SDL_PushEvent(&event);
|
||||
} else if (cmd == 1002) {
|
||||
ShowWindow(app_hwnd_, SW_SHOW); // show main window
|
||||
SetForegroundWindow(app_hwnd_);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-10-22
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WIN_TRAY_H_
|
||||
#define _WIN_TRAY_H_
|
||||
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#define WM_TRAY_CALLBACK (WM_USER + 1)
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class WinTray {
|
||||
public:
|
||||
WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
|
||||
int language_index);
|
||||
~WinTray();
|
||||
|
||||
void MinimizeToTray();
|
||||
void RemoveTrayIcon();
|
||||
bool HandleTrayMessage(MSG* msg);
|
||||
|
||||
private:
|
||||
HWND app_hwnd_;
|
||||
HWND hwnd_message_only_;
|
||||
HICON icon_;
|
||||
std::wstring tip_;
|
||||
int language_index_;
|
||||
NOTIFYICONDATA nid_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
Reference in New Issue
Block a user