mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-03-20 04:18:30 +08:00
[feat] add controller info and file transfer in server window
This commit is contained in:
25
src/common/window_util_mac.h
Normal file
25
src/common/window_util_mac.h
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-01-20
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOW_UTIL_MAC_H_
|
||||
#define _WINDOW_UTIL_MAC_H_
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Best-effort: keep an SDL window above normal windows on macOS.
|
||||
// No-op on non-macOS builds.
|
||||
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top);
|
||||
|
||||
// Best-effort: exclude an SDL window from the Window menu and window cycling.
|
||||
// Note: Cmd-Tab switches apps (not individual windows), so this primarily
|
||||
// affects the Window menu and Cmd-` window cycling.
|
||||
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
64
src/common/window_util_mac.mm
Normal file
64
src/common/window_util_mac.mm
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "window_util_mac.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static NSWindow* GetNSWindowFromSDL(::SDL_Window* window) {
|
||||
if (!window) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
#if !defined(SDL_PROP_WINDOW_COCOA_WINDOW_POINTER)
|
||||
return nil;
|
||||
#else
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
void* cocoa_window_ptr =
|
||||
SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL);
|
||||
if (!cocoa_window_ptr) {
|
||||
return nil;
|
||||
}
|
||||
return (__bridge NSWindow*)cocoa_window_ptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top) {
|
||||
NSWindow* ns_window = GetNSWindowFromSDL(window);
|
||||
if (!ns_window) {
|
||||
(void)always_on_top;
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep above normal windows.
|
||||
const NSInteger level = always_on_top ? NSFloatingWindowLevel : NSNormalWindowLevel;
|
||||
[ns_window setLevel:level];
|
||||
|
||||
// Optional: keep visible across Spaces/fullscreen. Safe as best-effort.
|
||||
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
|
||||
behavior |= NSWindowCollectionBehaviorCanJoinAllSpaces;
|
||||
behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
|
||||
[ns_window setCollectionBehavior:behavior];
|
||||
}
|
||||
|
||||
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded) {
|
||||
NSWindow* ns_window = GetNSWindowFromSDL(window);
|
||||
if (!ns_window) {
|
||||
(void)excluded;
|
||||
return;
|
||||
}
|
||||
|
||||
[ns_window setExcludedFromWindowsMenu:excluded];
|
||||
|
||||
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
|
||||
behavior |= NSWindowCollectionBehaviorIgnoresCycle;
|
||||
behavior |= NSWindowCollectionBehaviorTransient;
|
||||
[ns_window setCollectionBehavior:behavior];
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -197,6 +197,10 @@ static std::vector<std::string> completed = {
|
||||
reinterpret_cast<const char*>(u8"已完成"), "Completed"};
|
||||
static std::vector<std::string> failed = {
|
||||
reinterpret_cast<const char*>(u8"失败"), "Failed"};
|
||||
static std::vector<std::string> controller = {
|
||||
reinterpret_cast<const char*>(u8"控制端:"), "Controller:"};
|
||||
static std::vector<std::string> file_transfer = {
|
||||
reinterpret_cast<const char*>(u8"文件传输:"), "File Transfer:"};
|
||||
|
||||
#if _WIN32
|
||||
static std::vector<std::string> minimize_to_tray = {
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
#include <libyuv.h>
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -21,10 +27,123 @@
|
||||
#include "screen_capturer_factory.h"
|
||||
#include "version_checker.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include "window_util_mac.h"
|
||||
#endif
|
||||
|
||||
#define NV12_BUFFER_SIZE 1280 * 720 * 3 / 2
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
inline bool X11GetDisplayAndWindow(SDL_Window* window, Display** display_out,
|
||||
::Window* x11_window_out) {
|
||||
if (!window || !display_out || !x11_window_out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if !defined(SDL_PROP_WINDOW_X11_DISPLAY_POINTER) || \
|
||||
!defined(SDL_PROP_WINDOW_X11_WINDOW_NUMBER)
|
||||
// SDL build does not expose X11 window properties.
|
||||
return false;
|
||||
#else
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
Display* display = (Display*)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL);
|
||||
const Sint64 x11_window_num =
|
||||
SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
|
||||
const ::Window x11_window = (::Window)x11_window_num;
|
||||
|
||||
if (!display || !x11_window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*display_out = display;
|
||||
*x11_window_out = x11_window;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void X11SendNetWmState(Display* display, ::Window x11_window,
|
||||
long action, Atom state1, Atom state2 = 0) {
|
||||
if (!display || !x11_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom wm_state = XInternAtom(display, "_NET_WM_STATE", False);
|
||||
|
||||
XEvent event;
|
||||
memset(&event, 0, sizeof(event));
|
||||
event.xclient.type = ClientMessage;
|
||||
event.xclient.serial = 0;
|
||||
event.xclient.send_event = True;
|
||||
event.xclient.message_type = wm_state;
|
||||
event.xclient.window = x11_window;
|
||||
event.xclient.format = 32;
|
||||
event.xclient.data.l[0] = action;
|
||||
event.xclient.data.l[1] = (long)state1;
|
||||
event.xclient.data.l[2] = (long)state2;
|
||||
event.xclient.data.l[3] = 1; // normal source indication
|
||||
event.xclient.data.l[4] = 0;
|
||||
|
||||
XSendEvent(display, DefaultRootWindow(display), False,
|
||||
SubstructureRedirectMask | SubstructureNotifyMask, &event);
|
||||
}
|
||||
|
||||
inline void X11SetWindowTypeUtility(Display* display, ::Window x11_window) {
|
||||
if (!display || !x11_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom wm_window_type =
|
||||
XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
|
||||
const Atom wm_window_type_utility =
|
||||
XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
|
||||
|
||||
XChangeProperty(display, x11_window, wm_window_type, XA_ATOM, 32,
|
||||
PropModeReplace, (unsigned char*)&wm_window_type_utility, 1);
|
||||
}
|
||||
|
||||
inline void X11SetWindowAlwaysOnTop(SDL_Window* window) {
|
||||
Display* display = nullptr;
|
||||
::Window x11_window = 0;
|
||||
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom state_above = XInternAtom(display, "_NET_WM_STATE_ABOVE", False);
|
||||
const Atom state_stays_on_top =
|
||||
XInternAtom(display, "_NET_WM_STATE_STAYS_ON_TOP", False);
|
||||
|
||||
// Request _NET_WM_STATE_ADD for ABOVE + STAYS_ON_TOP.
|
||||
X11SendNetWmState(display, x11_window, 1, state_above, state_stays_on_top);
|
||||
XFlush(display);
|
||||
}
|
||||
|
||||
inline void X11SetWindowSkipTaskbar(SDL_Window* window) {
|
||||
Display* display = nullptr;
|
||||
::Window x11_window = 0;
|
||||
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom skip_taskbar =
|
||||
XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", False);
|
||||
const Atom skip_pager =
|
||||
XInternAtom(display, "_NET_WM_STATE_SKIP_PAGER", False);
|
||||
|
||||
// Request _NET_WM_STATE_ADD for SKIP_TASKBAR + SKIP_PAGER.
|
||||
X11SendNetWmState(display, x11_window, 1, skip_taskbar, skip_pager);
|
||||
|
||||
// Hint the WM that this is an auxiliary/utility window.
|
||||
X11SetWindowTypeUtility(display, x11_window);
|
||||
|
||||
XFlush(display);
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
std::vector<char> Render::SerializeRemoteAction(const RemoteAction& action) {
|
||||
std::vector<char> buffer;
|
||||
buffer.push_back(static_cast<char>(action.type));
|
||||
@@ -131,6 +250,20 @@ SDL_HitTestResult Render::HitTestCallback(SDL_Window* window,
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
// Server window: OS-level dragging for the title bar, but keep the left-side
|
||||
// collapse/expand button clickable.
|
||||
if (render->server_window_ && window == render->server_window_) {
|
||||
const float title_h = render->server_window_title_bar_height_;
|
||||
const float button_w = title_h;
|
||||
if (area->y >= 0 && area->y < title_h) {
|
||||
if (area->x >= 0 && area->x < button_w) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
return SDL_HITTEST_DRAGGABLE;
|
||||
}
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
int window_width, window_height;
|
||||
SDL_GetWindowSize(window, &window_width, &window_height);
|
||||
|
||||
@@ -963,6 +1096,51 @@ int Render::CreateServerWindow() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
// Hide server window from the taskbar by making it an owned tool window.
|
||||
{
|
||||
SDL_PropertiesID server_props = SDL_GetWindowProperties(server_window_);
|
||||
HWND server_hwnd = (HWND)SDL_GetPointerProperty(
|
||||
server_props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
|
||||
|
||||
HWND owner_hwnd = nullptr;
|
||||
if (main_window_) {
|
||||
SDL_PropertiesID main_props = SDL_GetWindowProperties(main_window_);
|
||||
owner_hwnd = (HWND)SDL_GetPointerProperty(
|
||||
main_props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
|
||||
}
|
||||
|
||||
if (server_hwnd) {
|
||||
LONG_PTR ex_style = GetWindowLongPtr(server_hwnd, GWL_EXSTYLE);
|
||||
ex_style |= WS_EX_TOOLWINDOW;
|
||||
ex_style &= ~WS_EX_APPWINDOW;
|
||||
SetWindowLongPtr(server_hwnd, GWL_EXSTYLE, ex_style);
|
||||
|
||||
if (owner_hwnd) {
|
||||
SetWindowLongPtr(server_hwnd, GWLP_HWNDPARENT, (LONG_PTR)owner_hwnd);
|
||||
}
|
||||
|
||||
// Keep the server window above normal windows.
|
||||
SetWindowPos(server_hwnd, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOACTIVATE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
// Best-effort keep above other windows on X11.
|
||||
X11SetWindowAlwaysOnTop(server_window_);
|
||||
// Best-effort hide from taskbar on X11.
|
||||
X11SetWindowSkipTaskbar(server_window_);
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// Best-effort keep above other windows on macOS.
|
||||
MacSetWindowAlwaysOnTop(server_window_, true);
|
||||
// Best-effort exclude from Window menu / window cycling.
|
||||
MacSetWindowExcludedFromWindowMenu(server_window_, true);
|
||||
#endif
|
||||
|
||||
// Set window position to bottom-right corner
|
||||
SDL_Rect display_bounds;
|
||||
if (SDL_GetDisplayUsableBounds(SDL_GetDisplayForWindow(server_window_),
|
||||
@@ -2028,10 +2206,7 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_DROP_FILE:
|
||||
if (stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == event.window.windowID) {
|
||||
ProcessFileDropEvent(event);
|
||||
}
|
||||
ProcessFileDropEvent(event);
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
@@ -2115,61 +2290,110 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
|
||||
}
|
||||
|
||||
void Render::ProcessFileDropEvent(const SDL_Event& event) {
|
||||
if (!((stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == event.window.windowID) ||
|
||||
(server_window_ &&
|
||||
SDL_GetWindowID(server_window_) == event.window.windowID))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type != SDL_EVENT_DROP_FILE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stream_window_inited_) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_lock lock(client_properties_mutex_);
|
||||
for (auto& [_, props] : client_properties_) {
|
||||
if (props->tab_selected_) {
|
||||
if (event.drop.data == nullptr) {
|
||||
LOG_ERROR("ProcessFileDropEvent: drop event data is null");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!props || !props->peer_) {
|
||||
LOG_ERROR("ProcessFileDropEvent: invalid props or peer");
|
||||
break;
|
||||
}
|
||||
|
||||
std::string utf8_path = static_cast<const char*>(event.drop.data);
|
||||
std::filesystem::path file_path = std::filesystem::u8path(utf8_path);
|
||||
|
||||
// Check if file exists
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(file_path, ec)) {
|
||||
LOG_ERROR("ProcessFileDropEvent: file does not exist: {}",
|
||||
file_path.string().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if it's a regular file
|
||||
if (!std::filesystem::is_regular_file(file_path, ec)) {
|
||||
LOG_ERROR("ProcessFileDropEvent: path is not a regular file: {}",
|
||||
file_path.string().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
// Get file size
|
||||
uint64_t file_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("ProcessFileDropEvent: failed to get file size: {}",
|
||||
ec.message().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_INFO("Drop file [{}] to send (size: {} bytes)", event.drop.data,
|
||||
file_size);
|
||||
|
||||
// Use ProcessSelectedFile to handle the file processing
|
||||
ProcessSelectedFile(utf8_path, props, props->file_label_);
|
||||
|
||||
break;
|
||||
if (SDL_GetWindowID(stream_window_) == event.window.windowID) {
|
||||
if (!stream_window_inited_) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_lock lock(client_properties_mutex_);
|
||||
for (auto& [_, props] : client_properties_) {
|
||||
if (props->tab_selected_) {
|
||||
if (event.drop.data == nullptr) {
|
||||
LOG_ERROR("ProcessFileDropEvent: drop event data is null");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!props || !props->peer_) {
|
||||
LOG_ERROR("ProcessFileDropEvent: invalid props or peer");
|
||||
break;
|
||||
}
|
||||
|
||||
std::string utf8_path = static_cast<const char*>(event.drop.data);
|
||||
std::filesystem::path file_path = std::filesystem::u8path(utf8_path);
|
||||
|
||||
// Check if file exists
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(file_path, ec)) {
|
||||
LOG_ERROR("ProcessFileDropEvent: file does not exist: {}",
|
||||
file_path.string().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if it's a regular file
|
||||
if (!std::filesystem::is_regular_file(file_path, ec)) {
|
||||
LOG_ERROR("ProcessFileDropEvent: path is not a regular file: {}",
|
||||
file_path.string().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
// Get file size
|
||||
uint64_t file_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("ProcessFileDropEvent: failed to get file size: {}",
|
||||
ec.message().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_INFO("Drop file [{}] to send (size: {} bytes)", event.drop.data,
|
||||
file_size);
|
||||
|
||||
// Use ProcessSelectedFile to handle the file processing
|
||||
ProcessSelectedFile(utf8_path, props, props->file_label_);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (SDL_GetWindowID(server_window_) == event.window.windowID) {
|
||||
if (!server_window_inited_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.drop.data == nullptr) {
|
||||
LOG_ERROR("ProcessFileDropEvent: drop event data is null");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string utf8_path = static_cast<const char*>(event.drop.data);
|
||||
std::filesystem::path file_path = std::filesystem::u8path(utf8_path);
|
||||
|
||||
// Check if file exists
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(file_path, ec)) {
|
||||
LOG_ERROR("ProcessFileDropEvent: file does not exist: {}",
|
||||
file_path.string().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a regular file
|
||||
if (!std::filesystem::is_regular_file(file_path, ec)) {
|
||||
LOG_ERROR("ProcessFileDropEvent: path is not a regular file: {}",
|
||||
file_path.string().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get file size
|
||||
uint64_t file_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("ProcessFileDropEvent: failed to get file size: {}",
|
||||
ec.message().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Drop file [{}] on server window (size: {} bytes)",
|
||||
event.drop.data, file_size);
|
||||
|
||||
// Handle the dropped file on server window as needed
|
||||
}
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -204,6 +204,7 @@ class Render {
|
||||
int UpdateNotificationWindow();
|
||||
int StreamWindow();
|
||||
int ServerWindow();
|
||||
int RemoteClientInfoWindow();
|
||||
int LocalWindow();
|
||||
int RemoteWindow();
|
||||
int RecentConnectionsWindow();
|
||||
@@ -220,6 +221,7 @@ class Render {
|
||||
void Hyperlink(const std::string& label, const std::string& url,
|
||||
const float window_width);
|
||||
int FileTransferWindow(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
std::string OpenFileDialog(std::string title);
|
||||
|
||||
private:
|
||||
int ConnectTo(const std::string& remote_id, const char* password,
|
||||
@@ -468,6 +470,7 @@ class Render {
|
||||
std::string controlled_remote_id_ = "";
|
||||
std::string focused_remote_id_ = "";
|
||||
bool need_to_send_host_info_ = false;
|
||||
std::string remote_client_id_ = "";
|
||||
SDL_Event last_mouse_event;
|
||||
SDL_AudioStream* output_stream_;
|
||||
uint32_t STREAM_REFRESH_EVENT = 0;
|
||||
@@ -504,14 +507,14 @@ class Render {
|
||||
bool need_to_destroy_server_window_ = false;
|
||||
bool server_window_created_ = false;
|
||||
bool server_window_inited_ = false;
|
||||
int server_window_width_default_ = 300;
|
||||
int server_window_height_default_ = 450;
|
||||
float server_window_width_ = 300;
|
||||
float server_window_height_ = 450;
|
||||
float server_window_title_bar_height_ = 50.0f;
|
||||
int server_window_width_default_ = 250;
|
||||
int server_window_height_default_ = 150;
|
||||
float server_window_width_ = 250;
|
||||
float server_window_height_ = 150;
|
||||
float server_window_title_bar_height_ = 30.0f;
|
||||
SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
int server_window_normal_width_ = 300;
|
||||
int server_window_normal_height_ = 450;
|
||||
int server_window_normal_width_ = 250;
|
||||
int server_window_normal_height_ = 150;
|
||||
float server_window_dpi_scaling_w_ = 1.0f;
|
||||
float server_window_dpi_scaling_h_ = 1.0f;
|
||||
|
||||
|
||||
@@ -628,6 +628,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
render->is_server_mode_ = true;
|
||||
render->start_screen_capturer_ = true;
|
||||
render->start_speaker_capturer_ = true;
|
||||
render->remote_client_id_ = remote_id;
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
render->start_mouse_controller_ = false;
|
||||
render->start_keyboard_capturer_ = false;
|
||||
@@ -657,6 +658,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
render->start_mouse_controller_ = false;
|
||||
render->start_keyboard_capturer_ = false;
|
||||
render->need_to_send_host_info_ = false;
|
||||
render->remote_client_id_ = "";
|
||||
if (props) props->connection_established_ = false;
|
||||
if (render->audio_capture_) {
|
||||
render->StopSpeakerCapturer();
|
||||
|
||||
@@ -15,18 +15,6 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
std::string OpenFileDialog(std::string title) {
|
||||
const char* path = tinyfd_openFileDialog(title.c_str(),
|
||||
"", // default path
|
||||
0, // number of filters
|
||||
nullptr, // filters
|
||||
nullptr, // filter description
|
||||
0 // no multiple selection
|
||||
);
|
||||
|
||||
return path ? path : "";
|
||||
}
|
||||
|
||||
int CountDigits(int number) {
|
||||
if (number == 0) return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
@@ -53,6 +41,18 @@ int LossRateDisplay(float loss_rate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string Render::OpenFileDialog(std::string title) {
|
||||
const char* path = tinyfd_openFileDialog(title.c_str(),
|
||||
"", // default path
|
||||
0, // number of filters
|
||||
nullptr, // filters
|
||||
nullptr, // filter description
|
||||
0 // no multiple selection
|
||||
);
|
||||
|
||||
return path ? path : "";
|
||||
}
|
||||
|
||||
void Render::ProcessSelectedFile(
|
||||
const std::string& path, std::shared_ptr<SubStreamWindowProperties>& props,
|
||||
const std::string& file_label) {
|
||||
|
||||
@@ -1,50 +1,11 @@
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
constexpr float kDragThresholdPx = 3.0f;
|
||||
|
||||
// Handles dragging for the *last submitted ImGui item*.
|
||||
// `reset_on_deactivate` should be false when the caller needs to know whether a
|
||||
// deactivation was a click (no drag) vs a drag-release (dragging==true).
|
||||
inline void HandleWindowDragForLastItem(SDL_Window* window, bool& dragging,
|
||||
float& start_mouse_x,
|
||||
float& start_mouse_y, int& start_win_x,
|
||||
int& start_win_y,
|
||||
bool reset_on_deactivate = true) {
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::IsItemActivated()) {
|
||||
SDL_GetGlobalMouseState(&start_mouse_x, &start_mouse_y);
|
||||
SDL_GetWindowPosition(window, &start_win_x, &start_win_y);
|
||||
dragging = false;
|
||||
}
|
||||
|
||||
if (ImGui::IsItemActive()) {
|
||||
if (!dragging &&
|
||||
ImGui::IsMouseDragging(ImGuiMouseButton_Left, kDragThresholdPx)) {
|
||||
dragging = true;
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
float mouse_x = 0.0f;
|
||||
float mouse_y = 0.0f;
|
||||
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
|
||||
const int dx = (int)(mouse_x - start_mouse_x);
|
||||
const int dy = (int)(mouse_y - start_mouse_y);
|
||||
SDL_SetWindowPosition(window, start_win_x + dx, start_win_y + dy);
|
||||
}
|
||||
}
|
||||
|
||||
if (reset_on_deactivate && ImGui::IsItemDeactivated()) {
|
||||
dragging = false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
namespace {} // namespace
|
||||
|
||||
int Render::ServerWindow() {
|
||||
ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_),
|
||||
@@ -57,44 +18,6 @@ int Render::ServerWindow() {
|
||||
ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoScrollWithMouse);
|
||||
|
||||
// Collapsed mode: no buttons; drag to move; click to restore.
|
||||
if (server_window_collapsed_) {
|
||||
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
|
||||
ImGui::InvisibleButton("##server_collapsed_area",
|
||||
ImVec2(server_window_width_, server_window_height_));
|
||||
|
||||
HandleWindowDragForLastItem(server_window_,
|
||||
server_window_collapsed_dragging_,
|
||||
server_window_collapsed_drag_start_mouse_x_,
|
||||
server_window_collapsed_drag_start_mouse_y_,
|
||||
server_window_collapsed_drag_start_win_x_,
|
||||
server_window_collapsed_drag_start_win_y_,
|
||||
/*reset_on_deactivate=*/false);
|
||||
|
||||
const bool request_restore =
|
||||
ImGui::IsItemDeactivated() && !server_window_collapsed_dragging_;
|
||||
if (ImGui::IsItemDeactivated()) {
|
||||
server_window_collapsed_dragging_ = false;
|
||||
}
|
||||
|
||||
if (request_restore && server_window_) {
|
||||
int w = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int h = 0;
|
||||
SDL_GetWindowSize(server_window_, &w, &h);
|
||||
SDL_GetWindowPosition(server_window_, &x, &y);
|
||||
|
||||
const int normal_h = server_window_normal_height_;
|
||||
SDL_SetWindowSize(server_window_, w, normal_h);
|
||||
SDL_SetWindowPosition(server_window_, x, y);
|
||||
server_window_collapsed_ = false;
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||
@@ -105,97 +28,132 @@ int Render::ServerWindow() {
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
|
||||
float server_title_bar_button_width = server_window_title_bar_height_;
|
||||
float server_title_bar_button_height = server_window_title_bar_height_;
|
||||
|
||||
// Drag area: the title bar excluding the right-side buttons.
|
||||
// Collapse/expand toggle button (FontAwesome icon).
|
||||
{
|
||||
const float drag_w =
|
||||
server_window_width_ - server_title_bar_button_width * 2;
|
||||
const float drag_h = server_title_bar_button_height;
|
||||
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
|
||||
ImGui::InvisibleButton("##server_title_drag_area", ImVec2(drag_w, drag_h));
|
||||
|
||||
HandleWindowDragForLastItem(
|
||||
server_window_, server_window_dragging_,
|
||||
server_window_drag_start_mouse_x_, server_window_drag_start_mouse_y_,
|
||||
server_window_drag_start_win_x_, server_window_drag_start_win_y_);
|
||||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
||||
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
|
||||
float minimize_button_pos_x =
|
||||
server_window_width_ - server_title_bar_button_width * 2;
|
||||
ImGui::SetCursorPos(ImVec2(minimize_button_pos_x, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
const char* icon =
|
||||
server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP;
|
||||
std::string toggle_label = std::string(icon) + "##server_toggle";
|
||||
if (ImGui::Button(toggle_label.c_str(),
|
||||
ImVec2(server_title_bar_button_width,
|
||||
server_title_bar_button_height))) {
|
||||
if (server_window_) {
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
SDL_GetWindowSize(server_window_, &w, &h);
|
||||
SDL_GetWindowPosition(server_window_, &x, &y);
|
||||
|
||||
float minimize_pos_x =
|
||||
minimize_button_pos_x + server_title_bar_button_width * 0.33f;
|
||||
float minimize_pos_y = server_title_bar_button_height * 0.5f;
|
||||
std::string server_minimize_button = "##minimize"; // ICON_FA_MINUS;
|
||||
if (ImGui::Button(server_minimize_button.c_str(),
|
||||
ImVec2(server_title_bar_button_width,
|
||||
server_title_bar_button_height))) {
|
||||
if (server_window_) {
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
SDL_GetWindowSize(server_window_, &w, &h);
|
||||
SDL_GetWindowPosition(server_window_, &x, &y);
|
||||
|
||||
const int collapsed_h = (int)server_window_title_bar_height_;
|
||||
// Collapse upward: keep top edge stable.
|
||||
SDL_SetWindowSize(server_window_, w, collapsed_h);
|
||||
SDL_SetWindowPosition(server_window_, x, y);
|
||||
server_window_collapsed_ = true;
|
||||
if (server_window_collapsed_) {
|
||||
const int normal_h = server_window_normal_height_;
|
||||
SDL_SetWindowSize(server_window_, w, normal_h);
|
||||
SDL_SetWindowPosition(server_window_, x, y);
|
||||
server_window_collapsed_ = false;
|
||||
} else {
|
||||
const int collapsed_h = (int)server_window_title_bar_height_;
|
||||
// Collapse upward: keep top edge stable.
|
||||
SDL_SetWindowSize(server_window_, w, collapsed_h);
|
||||
SDL_SetWindowPosition(server_window_, x, y);
|
||||
server_window_collapsed_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
draw_list->AddLine(
|
||||
ImVec2(minimize_pos_x, minimize_pos_y),
|
||||
ImVec2(minimize_pos_x + server_title_bar_button_width * 0.33f,
|
||||
minimize_pos_y),
|
||||
IM_COL32(0, 0, 0, 255));
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
float xmark_button_pos_x =
|
||||
server_window_width_ - server_title_bar_button_width;
|
||||
ImGui::SetCursorPos(ImVec2(xmark_button_pos_x, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0, 0, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0, 0, 0.5f));
|
||||
|
||||
float xmark_pos_x = xmark_button_pos_x + server_title_bar_button_width * 0.5f;
|
||||
float xmark_pos_y = server_title_bar_button_height * 0.5f;
|
||||
float xmark_size = server_title_bar_button_width * 0.33f;
|
||||
std::string server_close_button = "##xmark"; // ICON_FA_XMARK;
|
||||
if (ImGui::Button(server_close_button.c_str(),
|
||||
ImVec2(server_title_bar_button_width,
|
||||
server_title_bar_button_height))) {
|
||||
LOG_ERROR("Close button clicked");
|
||||
LeaveConnection(peer_, self_hosted_id_);
|
||||
}
|
||||
|
||||
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
|
||||
xmark_pos_y - xmark_size / 2 + 0.75f),
|
||||
ImVec2(xmark_pos_x + xmark_size / 2 - 1.5f,
|
||||
xmark_pos_y + xmark_size / 2 - 0.5f),
|
||||
IM_COL32(0, 0, 0, 255));
|
||||
draw_list->AddLine(
|
||||
ImVec2(xmark_pos_x + xmark_size / 2 - 1.75f,
|
||||
xmark_pos_y - xmark_size / 2 + 0.75f),
|
||||
ImVec2(xmark_pos_x - xmark_size / 2, xmark_pos_y + xmark_size / 2 - 1.0f),
|
||||
IM_COL32(0, 0, 0, 255));
|
||||
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
RemoteClientInfoWindow();
|
||||
|
||||
ImGui::End();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::RemoteClientInfoWindow() {
|
||||
float remote_client_info_window_width = server_window_width_ * 0.75f;
|
||||
float remote_client_info_window_height =
|
||||
(server_window_height_ - server_window_title_bar_height_) * 0.3f;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);
|
||||
ImGui::BeginChild(
|
||||
"RemoteClientInfoWindow",
|
||||
ImVec2(remote_client_info_window_width, remote_client_info_window_height),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::SetWindowFontScale(0.7f);
|
||||
ImGui::Text("%s",
|
||||
localization::controller[localization_language_index_].c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%s", remote_client_id_.c_str());
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
float close_connection_button_width = server_window_width_ * 0.15f;
|
||||
float close_connection_button_height = remote_client_info_window_height;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0.5f, 0.5f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 5.0f);
|
||||
ImGui::SetWindowFontScale(0.7f);
|
||||
if (ImGui::Button(ICON_FA_XMARK, ImVec2(close_connection_button_width,
|
||||
close_connection_button_height))) {
|
||||
LeaveConnection(peer_, self_hosted_id_);
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
|
||||
ImGui::BeginChild(
|
||||
"RemoteClientInfoFileTransferWindow",
|
||||
ImVec2(remote_client_info_window_width, remote_client_info_window_height),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::SetWindowFontScale(0.7f);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text(
|
||||
"%s", localization::file_transfer[localization_language_index_].c_str());
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(
|
||||
localization::select_file[localization_language_index_].c_str())) {
|
||||
std::string title = localization::select_file[localization_language_index_];
|
||||
std::string path = OpenFileDialog(title);
|
||||
LOG_INFO("Selected file path: {}", path.c_str());
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
Reference in New Issue
Block a user