mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-17 01:13:19 +08:00
[ui] replace host session window with compact edge controls
This commit is contained in:
@@ -665,7 +665,7 @@ struct WindowDragState {
|
||||
slint::Timer pointer_timer;
|
||||
#endif
|
||||
|
||||
bool ReadGlobalPointer(float* x, float* y) {
|
||||
bool ReadGlobalPointer(float* x, float* y, bool* left_pressed = nullptr) {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
const char* driver = SDL_GetCurrentVideoDriver();
|
||||
if (driver && std::strcmp(driver, "x11") == 0) {
|
||||
@@ -686,12 +686,14 @@ struct WindowDragState {
|
||||
&buttons)) {
|
||||
return false;
|
||||
}
|
||||
if (left_pressed) *left_pressed = (buttons & Button1Mask) != 0;
|
||||
*x = static_cast<float>(root_x);
|
||||
*y = static_cast<float>(root_y);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
SDL_GetGlobalMouseState(x, y);
|
||||
const auto buttons = SDL_GetGlobalMouseState(x, y);
|
||||
if (left_pressed) *left_pressed = (buttons & SDL_BUTTON_LMASK) != 0;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -710,14 +712,6 @@ bool CanUseGlobalPointerPosition() {
|
||||
#endif
|
||||
}
|
||||
|
||||
float ServerWindowLogicalWidth(int language) {
|
||||
return language == 0 ? 250.0f : language == 1 ? 330.0f : 430.0f;
|
||||
}
|
||||
|
||||
float ServerWindowLogicalHeight(bool collapsed) {
|
||||
return collapsed ? 30.0f : 150.0f;
|
||||
}
|
||||
|
||||
constexpr float kStreamWindowLogicalWidth = 1280.0f;
|
||||
constexpr float kStreamWindowLogicalHeight = 720.0f;
|
||||
constexpr float kWaylandTitlebarLogicalHeight = 32.0f;
|
||||
@@ -798,57 +792,6 @@ bool PositionWindowAtCenter(slint::Window& window, const slint::Window& anchor,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PositionWindowAtBottomRight(slint::Window& window, float logical_width,
|
||||
float logical_height) {
|
||||
if (!CanUseGlobalPointerPosition()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto position = window.position();
|
||||
const auto size = window.size();
|
||||
#if defined(__APPLE__)
|
||||
const float scale = std::max(window.scale_factor(), 0.01f);
|
||||
const SDL_Point center{
|
||||
static_cast<int>(
|
||||
std::lround(position.x / scale + size.width / (2.0f * scale))),
|
||||
static_cast<int>(
|
||||
std::lround(position.y / scale + size.height / (2.0f * scale)))};
|
||||
#else
|
||||
const SDL_Point center{
|
||||
position.x + static_cast<int>(size.width / 2),
|
||||
position.y + static_cast<int>(size.height / 2)};
|
||||
#endif
|
||||
SDL_DisplayID display = SDL_GetDisplayForPoint(¢er);
|
||||
if (display == 0) {
|
||||
display = SDL_GetPrimaryDisplay();
|
||||
}
|
||||
|
||||
SDL_Rect usable_bounds{};
|
||||
if (display == 0 || !SDL_GetDisplayUsableBounds(display, &usable_bounds)) {
|
||||
LOG_WARN("Unable to obtain usable display bounds for server window: {}",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
const float target_x =
|
||||
static_cast<float>(usable_bounds.x) +
|
||||
std::max(0.0f, static_cast<float>(usable_bounds.w) - logical_width);
|
||||
const float target_y =
|
||||
static_cast<float>(usable_bounds.y) +
|
||||
std::max(0.0f, static_cast<float>(usable_bounds.h) - logical_height);
|
||||
window.set_position(
|
||||
slint::LogicalPosition(slint::Point<float>{target_x, target_y}));
|
||||
#else
|
||||
const auto target = window_geometry::BottomRightPosition(
|
||||
PhysicalDisplayBounds(usable_bounds),
|
||||
PhysicalWindowSize(window, logical_width, logical_height));
|
||||
window.set_position(slint::PhysicalPosition(
|
||||
slint::Point<int32_t>{target.x, target.y}));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
bool ConfigureWindowsServerWindow(slint::Window& window) {
|
||||
const HWND hwnd = window.win32_hwnd();
|
||||
@@ -977,6 +920,7 @@ struct GuiApplication::SlintUi {
|
||||
std::make_shared<slint::VectorModel<ui::ReleaseNoteBlock>>();
|
||||
std::optional<slint::ComponentHandle<ui::StreamWindow>> stream;
|
||||
std::optional<slint::ComponentHandle<ui::ServerWindow>> server;
|
||||
std::optional<slint::ComponentHandle<ui::ServerStatusBar>> server_bar;
|
||||
std::shared_ptr<slint::VectorModel<ui::RecentConnection>> recent_model =
|
||||
std::make_shared<slint::VectorModel<ui::RecentConnection>>();
|
||||
std::shared_ptr<slint::VectorModel<ui::StreamTab>> tab_model =
|
||||
@@ -989,26 +933,32 @@ struct GuiApplication::SlintUi {
|
||||
std::make_shared<slint::VectorModel<ui::NetworkStatsRow>>();
|
||||
std::shared_ptr<slint::VectorModel<ui::ControllerEntry>> controller_model =
|
||||
std::make_shared<slint::VectorModel<ui::ControllerEntry>>();
|
||||
std::shared_ptr<slint::VectorModel<slint::SharedString>>
|
||||
controller_name_model =
|
||||
std::make_shared<slint::VectorModel<slint::SharedString>>();
|
||||
std::unique_ptr<SlintVideoPresenter> video_presenter;
|
||||
std::vector<std::string> tab_order;
|
||||
std::vector<std::string> tab_ids;
|
||||
std::string tab_model_signature;
|
||||
std::vector<std::string> controller_ids;
|
||||
std::string controller_model_signature;
|
||||
std::string connection_dialog_remote_id;
|
||||
bool connection_password_initialized = false;
|
||||
bool portable_service_dialog_initialized = false;
|
||||
std::string recent_model_signature;
|
||||
slint::Timer timer;
|
||||
slint::Timer server_file_dialog_timer;
|
||||
WindowDragState main_drag;
|
||||
WindowDragState stream_drag;
|
||||
WindowDragState server_drag;
|
||||
int main_native_titlebar_attempts = 30;
|
||||
int stream_live_resize_configuration_attempts = 0;
|
||||
int stream_initial_position_attempts = 0;
|
||||
int server_initial_position_attempts = 0;
|
||||
SDL_DisplayID server_display = 0;
|
||||
float server_anchor = 0.3f;
|
||||
float server_drag_anchor = 0.3f;
|
||||
bool server_drag_moved = false;
|
||||
bool server_pointer_pressed = false;
|
||||
bool server_file_dialog_open = false;
|
||||
bool server_suspended_for_dialog = false;
|
||||
server_window_state::EdgeLayout server_layout{};
|
||||
#if _WIN32
|
||||
int server_native_window_attempts = 0;
|
||||
#endif
|
||||
@@ -1859,18 +1809,37 @@ void GuiApplication::BindServerCallbacks() {
|
||||
return;
|
||||
}
|
||||
auto& server = *ui_->server;
|
||||
server->on_title_drag([this](int phase, float x, float y) {
|
||||
auto& bar = *ui_->server_bar;
|
||||
bar->on_title_drag([this](int phase, float, float) {
|
||||
auto& drag = ui_->server_drag;
|
||||
DragWindow(*ui_->server, phase, x, y, drag);
|
||||
if (phase == 1) {
|
||||
ui_->server_drag_moved = false;
|
||||
(*ui_->server_bar)->set_dragged(false);
|
||||
drag.active = CanUseGlobalPointerPosition() &&
|
||||
drag.ReadGlobalPointer(&drag.pointer_start_x, &drag.pointer_start_y);
|
||||
ui_->server_drag_anchor = ui_->server_anchor;
|
||||
} else if (phase == 0) {
|
||||
PositionServerWindows();
|
||||
drag.active = false;
|
||||
}
|
||||
});
|
||||
server->on_toggle_collapsed([this](bool collapsed) {
|
||||
server_window_collapsed_ = collapsed;
|
||||
const int language = (*ui_->server)->get_language_index();
|
||||
const float width = ServerWindowLogicalWidth(language);
|
||||
(*ui_->server)
|
||||
->window()
|
||||
.set_size(slint::LogicalSize(
|
||||
slint::Size<float>{width, ServerWindowLogicalHeight(collapsed)}));
|
||||
bar->on_toggle_panel([this] {
|
||||
SetServerPanelExpanded(server_window_collapsed_);
|
||||
});
|
||||
bar->on_move_anchor([this](float delta) {
|
||||
ui_->server_anchor = std::clamp(ui_->server_anchor + delta, 0.0f, 1.0f);
|
||||
PositionServerWindows();
|
||||
});
|
||||
const auto dismiss = [this] { SetServerPanelExpanded(false); };
|
||||
bar->on_dismiss_panel(dismiss);
|
||||
server->on_dismiss_panel(dismiss);
|
||||
server->window().on_close_requested([dismiss] {
|
||||
dismiss();
|
||||
return slint::CloseRequestResponse::KeepWindowShown;
|
||||
});
|
||||
bar->window().on_close_requested([dismiss] {
|
||||
dismiss();
|
||||
return slint::CloseRequestResponse::KeepWindowShown;
|
||||
});
|
||||
server->on_controller_selected([this](int index) {
|
||||
if (index < 0 || index >= static_cast<int>(ui_->controller_ids.size())) {
|
||||
@@ -1879,17 +1848,43 @@ void GuiApplication::BindServerCallbacks() {
|
||||
selected_server_remote_id_ = ui_->controller_ids[index];
|
||||
});
|
||||
server->on_select_file([this] {
|
||||
if (selected_server_remote_id_.empty()) {
|
||||
if (selected_server_remote_id_.empty() || ui_->server_file_dialog_open) {
|
||||
return;
|
||||
}
|
||||
transfers_.ProcessSelectedFile(
|
||||
OpenFileDialog(localization::select_file[localization_language_index_]),
|
||||
nullptr, file_label_, selected_server_remote_id_);
|
||||
// A native file picker runs a nested event loop. Keep the recipient stable
|
||||
// if another controller connects or leaves while it is open.
|
||||
const std::string recipient = selected_server_remote_id_;
|
||||
ui_->server_file_dialog_open = true;
|
||||
(*ui_->server)->window().request_redraw();
|
||||
// TouchArea clears pressed only after its clicked callback returns. Allow
|
||||
// that release and a repaint before entering the blocking native picker.
|
||||
ui_->server_file_dialog_timer.start(
|
||||
slint::TimerMode::SingleShot, std::chrono::milliseconds(32),
|
||||
[this, recipient] {
|
||||
const auto connected = [this, &recipient] {
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
const auto found = connection_status_.find(recipient);
|
||||
return found != connection_status_.end() &&
|
||||
found->second == ConnectionStatus::Connected;
|
||||
};
|
||||
if (!connected()) {
|
||||
ui_->server_file_dialog_open = false;
|
||||
return;
|
||||
}
|
||||
const auto path = OpenFileDialog(
|
||||
localization::select_file[localization_language_index_]);
|
||||
ui_->server_file_dialog_open = false;
|
||||
if (connected() && !path.empty())
|
||||
transfers_.ProcessSelectedFile(path, nullptr, file_label_, recipient);
|
||||
});
|
||||
});
|
||||
server->on_disconnect_controller([this] {
|
||||
if (peer_ && !selected_server_remote_id_.empty()) {
|
||||
CloseServerController(selected_server_remote_id_);
|
||||
}
|
||||
server->on_disconnect_controller([this](slint::SharedString id) {
|
||||
CloseServerController(std::string(id));
|
||||
});
|
||||
server->on_disconnect_all([this] {
|
||||
// Work from the confirmed UI snapshot; a later arrival is a new session.
|
||||
const auto ids = ui_->controller_ids;
|
||||
for (const auto& id : ids) CloseServerController(id);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3253,6 +3248,96 @@ void GuiApplication::ScheduleNextVideoFrame() {
|
||||
[this] { ScheduleNextVideoFrame(); });
|
||||
}
|
||||
|
||||
void GuiApplication::SetServerPanelExpanded(bool expanded) {
|
||||
if (!ui_->server || !ui_->server_bar) return;
|
||||
server_window_collapsed_ = !expanded;
|
||||
(*ui_->server_bar)->set_expanded(expanded);
|
||||
if (expanded) {
|
||||
PositionServerWindows();
|
||||
(*ui_->server)->show();
|
||||
#if _WIN32
|
||||
ConfigureWindowsServerWindow((*ui_->server)->window());
|
||||
#endif
|
||||
PositionServerWindows();
|
||||
} else {
|
||||
(*ui_->server)->set_confirming_disconnect(false);
|
||||
(*ui_->server)->hide();
|
||||
}
|
||||
}
|
||||
|
||||
void GuiApplication::PositionServerWindows() {
|
||||
if (!ui_->server || !ui_->server_bar) return;
|
||||
auto& bar = *ui_->server_bar;
|
||||
auto& panel = *ui_->server;
|
||||
const float scale = std::max(bar->window().scale_factor(), 0.01f);
|
||||
SDL_Rect bounds{};
|
||||
if (!SDL_GetDisplayUsableBounds(ui_->server_display, &bounds)) {
|
||||
ui_->server_display = SDL_GetPrimaryDisplay();
|
||||
if (!SDL_GetDisplayUsableBounds(ui_->server_display, &bounds)) return;
|
||||
}
|
||||
// Cocoa/SDL uses points; Windows and X11 report desktop pixels.
|
||||
#if defined(__APPLE__)
|
||||
const float desktop_scale = 1;
|
||||
#else
|
||||
const float desktop_scale = scale;
|
||||
#endif
|
||||
const server_window_state::Rect screen{
|
||||
bounds.x / desktop_scale, bounds.y / desktop_scale,
|
||||
bounds.w / desktop_scale, bounds.h / desktop_scale};
|
||||
float pointer_x = 0, pointer_y = 0;
|
||||
bool pressed = false;
|
||||
const bool pointer_valid = CanUseGlobalPointerPosition() &&
|
||||
ui_->server_drag.ReadGlobalPointer(&pointer_x, &pointer_y, &pressed);
|
||||
if (pointer_valid && ui_->server_drag.active) {
|
||||
const float delta = (pointer_y - ui_->server_drag.pointer_start_y) / desktop_scale;
|
||||
if (std::abs(delta) >= 4) {
|
||||
ui_->server_drag_moved = true;
|
||||
bar->set_dragged(true);
|
||||
}
|
||||
if (ui_->server_drag_moved) {
|
||||
const float travel = std::max(1.0f, screen.height - 52.0f);
|
||||
ui_->server_anchor = std::clamp(ui_->server_drag_anchor + delta / travel, 0.0f, 1.0f);
|
||||
}
|
||||
if (!pressed) ui_->server_drag.active = false;
|
||||
}
|
||||
ui_->server_layout = server_window_state::PlacePanel(
|
||||
screen, ui_->server_anchor, bar->get_bar_width(), panel->get_content_height());
|
||||
const auto& layout = ui_->server_layout;
|
||||
bar->set_opens_above(layout.above);
|
||||
const auto apply = [scale](slint::Window& window, const server_window_state::Rect& rect) {
|
||||
const auto size = window.size();
|
||||
const float own_scale = std::max(window.scale_factor(), 0.01f);
|
||||
if (std::abs(size.width / own_scale - rect.width) > 0.5f ||
|
||||
std::abs(size.height / own_scale - rect.height) > 0.5f) {
|
||||
window.set_size(slint::LogicalSize(slint::Size<float>{rect.width, rect.height}));
|
||||
}
|
||||
if (!CanUseGlobalPointerPosition()) return;
|
||||
const auto position = window.position();
|
||||
if (std::abs(position.x / scale - rect.x) <= 0.5f &&
|
||||
std::abs(position.y / scale - rect.y) <= 0.5f) return;
|
||||
#if defined(__APPLE__)
|
||||
window.set_position(slint::LogicalPosition(slint::Point<float>{rect.x, rect.y}));
|
||||
#else
|
||||
window.set_position(slint::PhysicalPosition(slint::Point<int32_t>{
|
||||
static_cast<int32_t>(std::lround(rect.x * scale)),
|
||||
static_cast<int32_t>(std::lround(rect.y * scale))}));
|
||||
#endif
|
||||
};
|
||||
apply(bar->window(), layout.bar);
|
||||
apply(panel->window(), layout.panel);
|
||||
if (pointer_valid) {
|
||||
const float x = pointer_x / desktop_scale;
|
||||
const float y = pointer_y / desktop_scale;
|
||||
if (pressed && !ui_->server_pointer_pressed && !server_window_collapsed_ &&
|
||||
!ui_->server_file_dialog_open && !ui_->server_drag.active &&
|
||||
!server_window_state::Contains(layout.bar, x, y) &&
|
||||
!server_window_state::Contains(layout.panel, x, y)) {
|
||||
SetServerPanelExpanded(false);
|
||||
}
|
||||
ui_->server_pointer_pressed = pressed;
|
||||
}
|
||||
}
|
||||
|
||||
void GuiApplication::SyncServerWindow() {
|
||||
// The connection map is authoritative. Lifecycle flags record callback
|
||||
// intent, but callbacks for different controllers can cross each other, so
|
||||
@@ -3283,22 +3368,21 @@ void GuiApplication::SyncServerWindow() {
|
||||
#endif
|
||||
if (show_controller_window && !ui_->server) {
|
||||
ui_->server.emplace(ui::ServerWindow::create());
|
||||
ui_->server_bar.emplace(ui::ServerStatusBar::create());
|
||||
RegisterFontAwesome((*ui_->server)->window());
|
||||
RegisterFontAwesome((*ui_->server_bar)->window());
|
||||
(*ui_->server)->set_controllers(ui_->controller_model);
|
||||
(*ui_->server)->set_controller_names(ui_->controller_name_model);
|
||||
(*ui_->server)->set_language_index(localization_language_index_);
|
||||
server_window_collapsed_ = false;
|
||||
(*ui_->server)
|
||||
->window()
|
||||
.set_size(slint::LogicalSize(slint::Size<float>{
|
||||
ServerWindowLogicalWidth(localization_language_index_),
|
||||
ServerWindowLogicalHeight(false)}));
|
||||
server_window_collapsed_ = true;
|
||||
ui_->server_display = DisplayForSlintWindow(ui_->main->window());
|
||||
ui_->server_anchor = 0.3f;
|
||||
ui_->server_drag.active = false;
|
||||
ui_->server_drag_moved = false;
|
||||
ui_->server_pointer_pressed = false;
|
||||
ui_->server_suspended_for_dialog = false;
|
||||
BindServerCallbacks();
|
||||
(*ui_->server)->show();
|
||||
// Apply once immediately and once after the next layout pass. This keeps
|
||||
// the right and bottom edges correct when the backend resolves DPI and
|
||||
// language-dependent preferred width asynchronously on first show.
|
||||
ui_->server_initial_position_attempts = 2;
|
||||
ui_localization::ApplyServerStrings(*ui_->server, localization_language_index_);
|
||||
ui_localization::ApplyServerStrings(*ui_->server_bar, localization_language_index_);
|
||||
(*ui_->server_bar)->show();
|
||||
#if _WIN32
|
||||
ui_->server_native_window_attempts = 30;
|
||||
#endif
|
||||
@@ -3307,98 +3391,75 @@ void GuiApplication::SyncServerWindow() {
|
||||
}
|
||||
if (!show_controller_window && ui_->server) {
|
||||
(*ui_->server)->hide();
|
||||
(*ui_->server_bar)->hide();
|
||||
// Keep handles alive through a native file dialog's nested event loop.
|
||||
if (ui_->server_file_dialog_open) {
|
||||
server_window_collapsed_ = true;
|
||||
(*ui_->server_bar)->set_expanded(false);
|
||||
(*ui_->server)->set_confirming_disconnect(false);
|
||||
ui_->server_suspended_for_dialog = true;
|
||||
return;
|
||||
}
|
||||
ui_->server.reset();
|
||||
ui_->server_bar.reset();
|
||||
ui_->server_drag.active = false;
|
||||
server_window_created_ = false;
|
||||
server_window_inited_ = false;
|
||||
ui_->server_initial_position_attempts = 0;
|
||||
#if _WIN32
|
||||
ui_->server_native_window_attempts = 0;
|
||||
#endif
|
||||
}
|
||||
if (!ui_->server) {
|
||||
return;
|
||||
if (!ui_->server) return;
|
||||
if (ui_->server_suspended_for_dialog) {
|
||||
(*ui_->server_bar)->show();
|
||||
ui_->server_suspended_for_dialog = false;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
ConfigureWindowsWindowIcons((*ui_->server)->window().win32_hwnd());
|
||||
#endif
|
||||
|
||||
std::vector<ui::ControllerEntry> controllers;
|
||||
std::vector<slint::SharedString> names;
|
||||
ui_->controller_ids.clear();
|
||||
{
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
controllers.reserve(connection_status_.size());
|
||||
for (const auto& [id, _] : connection_status_) {
|
||||
for (const auto& [id, status] : connection_status_) {
|
||||
if (status != ConnectionStatus::Connected) continue;
|
||||
const auto host = connection_host_names_.find(id);
|
||||
const std::string name =
|
||||
host != connection_host_names_.end() && !host->second.empty()
|
||||
? host->second
|
||||
: id;
|
||||
ui::ControllerEntry entry;
|
||||
entry.remote_id = UiText(id);
|
||||
entry.display_name = UiText(name);
|
||||
controllers.push_back(entry);
|
||||
names.emplace_back(UiText(name));
|
||||
ui_->controller_ids.push_back(id);
|
||||
? host->second : id;
|
||||
controllers.push_back({UiText(id), UiText(name)});
|
||||
}
|
||||
}
|
||||
ui_->controller_model->set_vector(std::move(controllers));
|
||||
ui_->controller_name_model->set_vector(std::move(names));
|
||||
// Stable ordering and model identity preserve radio selection, hover and
|
||||
// scrolling while the 16 ms UI timer reconciles asynchronous peer events.
|
||||
std::sort(controllers.begin(), controllers.end(), [](const auto& a, const auto& b) {
|
||||
return std::string(a.remote_id) < std::string(b.remote_id);
|
||||
});
|
||||
std::vector<std::string> ids;
|
||||
std::string signature;
|
||||
for (const auto& controller : controllers) {
|
||||
ids.emplace_back(controller.remote_id);
|
||||
signature += std::string(controller.remote_id) + '\0' +
|
||||
std::string(controller.display_name) + '\0';
|
||||
}
|
||||
if (ids != ui_->controller_ids) (*ui_->server)->set_confirming_disconnect(false);
|
||||
ui_->controller_ids = std::move(ids);
|
||||
if (signature != ui_->controller_model_signature) {
|
||||
ui_->controller_model->set_vector(std::move(controllers));
|
||||
ui_->controller_model_signature = std::move(signature);
|
||||
}
|
||||
const int selected = server_window_state::ReconcileSelectedController(
|
||||
ui_->controller_ids, &selected_server_remote_id_);
|
||||
(*ui_->server)->set_selected_controller(selected);
|
||||
(*ui_->server)->set_language_index(localization_language_index_);
|
||||
(*ui_->server_bar)->set_controller_count(static_cast<int>(ui_->controller_ids.size()));
|
||||
ui_localization::ApplyServerStrings(*ui_->server, localization_language_index_);
|
||||
ui_localization::ApplyServerStrings(*ui_->server_bar, localization_language_index_);
|
||||
#if _WIN32
|
||||
if (ui_->server_native_window_attempts > 0) {
|
||||
if (ConfigureWindowsServerWindow((*ui_->server)->window())) {
|
||||
if (ConfigureWindowsServerWindow((*ui_->server_bar)->window()))
|
||||
ui_->server_native_window_attempts = 0;
|
||||
} else {
|
||||
else
|
||||
--ui_->server_native_window_attempts;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (ui_->server_initial_position_attempts > 0) {
|
||||
const float server_width =
|
||||
ServerWindowLogicalWidth(localization_language_index_);
|
||||
const float server_height =
|
||||
ServerWindowLogicalHeight(server_window_collapsed_);
|
||||
(*ui_->server)
|
||||
->window()
|
||||
.set_size(slint::LogicalSize(
|
||||
slint::Size<float>{server_width, server_height}));
|
||||
if (PositionWindowAtBottomRight((*ui_->server)->window(), server_width,
|
||||
server_height)) {
|
||||
--ui_->server_initial_position_attempts;
|
||||
} else {
|
||||
ui_->server_initial_position_attempts = 0;
|
||||
}
|
||||
}
|
||||
(*ui_->server)
|
||||
->set_controller_label(
|
||||
UiText(localization::controller[localization_language_index_]));
|
||||
(*ui_->server)
|
||||
->set_connection_label(UiText(
|
||||
localization::connection_status[localization_language_index_]));
|
||||
(*ui_->server)
|
||||
->set_file_transfer_label(
|
||||
UiText(localization::file_transfer[localization_language_index_]));
|
||||
(*ui_->server)
|
||||
->set_select_file_label(
|
||||
UiText(localization::select_file[localization_language_index_]));
|
||||
|
||||
ConnectionStatus status = ConnectionStatus::Closed;
|
||||
{
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
if (const auto found = connection_status_.find(selected_server_remote_id_);
|
||||
found != connection_status_.end()) {
|
||||
status = found->second;
|
||||
}
|
||||
}
|
||||
(*ui_->server)
|
||||
->set_connection_status(
|
||||
UiText(ConnectionStatusText(status, localization_language_index_)));
|
||||
|
||||
auto& transfer = transfers_.global_state();
|
||||
const auto sent = transfer.file_sent_bytes_.load();
|
||||
const auto total = transfer.file_total_bytes_.load();
|
||||
@@ -3429,6 +3490,7 @@ void GuiApplication::SyncServerWindow() {
|
||||
->set_file_size_text(total == 0 ? slint::SharedString{}
|
||||
: UiText(FormatFileSize(sent) + " / " +
|
||||
FormatFileSize(total)));
|
||||
PositionServerWindows();
|
||||
}
|
||||
|
||||
void GuiApplication::SaveSettingsFromUi() {
|
||||
@@ -3820,6 +3882,7 @@ void GuiApplication::Cleanup() {
|
||||
return;
|
||||
}
|
||||
ui_->timer.stop();
|
||||
ui_->server_file_dialog_timer.stop();
|
||||
ui_->video_timer.stop();
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
JoinPortableWindowsServiceInstallThread();
|
||||
@@ -3848,6 +3911,7 @@ void GuiApplication::Cleanup() {
|
||||
}
|
||||
if (ui_->server) {
|
||||
(*ui_->server)->hide();
|
||||
(*ui_->server_bar)->hide();
|
||||
}
|
||||
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
|
||||
if (ui_->tray) {
|
||||
|
||||
@@ -69,6 +69,8 @@ private:
|
||||
void SyncStreamKeyboardFocus();
|
||||
void SetStreamKeyboardFocus(bool focused);
|
||||
void SyncServerWindow();
|
||||
void SetServerPanelExpanded(bool expanded);
|
||||
void PositionServerWindows();
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
void SyncXWaylandWindowActivation();
|
||||
#endif
|
||||
|
||||
@@ -14,8 +14,47 @@
|
||||
|
||||
namespace crossdesk::server_window_state {
|
||||
|
||||
inline int ReconcileSelectedController(const std::vector<std::string>& ids,
|
||||
std::string* selected_id) {
|
||||
// All values use logical desktop coordinates (including negative monitor
|
||||
// origins). The bar's anchor is independent of the panel's changing height.
|
||||
struct Rect {
|
||||
float x, y, width, height;
|
||||
};
|
||||
|
||||
struct EdgeLayout {
|
||||
Rect bar;
|
||||
Rect panel;
|
||||
bool above;
|
||||
};
|
||||
|
||||
inline EdgeLayout PlacePanel(const Rect &screen, float position,
|
||||
float bar_width, float panel_height) {
|
||||
constexpr float bar_height = 36;
|
||||
constexpr float gap = 6;
|
||||
constexpr float margin = 8;
|
||||
const float travel = std::max(0.0f, screen.height - bar_height - 2 * margin);
|
||||
const float top =
|
||||
screen.y + margin + travel * std::clamp(position, 0.0f, 1.0f);
|
||||
const float above = std::max(0.0f, top - screen.y - margin - gap);
|
||||
const float below = std::max(0.0f, screen.y + screen.height - margin - top -
|
||||
bar_height - gap);
|
||||
const bool opens_above =
|
||||
below < panel_height && (above >= panel_height || above > below);
|
||||
const float height = std::min(panel_height, opens_above ? above : below);
|
||||
const float width = std::min(225.0f, screen.width);
|
||||
return {{screen.x + screen.width - bar_width, top, bar_width, bar_height},
|
||||
{screen.x + screen.width - width,
|
||||
opens_above ? top - gap - height : top + bar_height + gap, width,
|
||||
height},
|
||||
opens_above};
|
||||
}
|
||||
|
||||
inline bool Contains(const Rect &rect, float x, float y) {
|
||||
return x >= rect.x && y >= rect.y && x < rect.x + rect.width &&
|
||||
y < rect.y + rect.height;
|
||||
}
|
||||
|
||||
inline int ReconcileSelectedController(const std::vector<std::string> &ids,
|
||||
std::string *selected_id) {
|
||||
if (!selected_id) {
|
||||
return 0;
|
||||
}
|
||||
@@ -33,4 +72,4 @@ inline int ReconcileSelectedController(const std::vector<std::string>& ids,
|
||||
}
|
||||
} // namespace crossdesk::server_window_state
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -316,6 +316,13 @@ struct TranslationRow {
|
||||
X(completed, u8"已完成", "Completed", u8"Завершено") \
|
||||
X(failed, u8"失败", "Failed", u8"Ошибка") \
|
||||
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
|
||||
X(server_controlled, u8"被控中", "Controlled", u8"Под управлением") \
|
||||
X(server_heading, u8"此设备被控中", "This device is controlled", u8"Устройство под управлением") \
|
||||
X(server_connected, u8"已连接", "Connected", u8"Подключено") \
|
||||
X(server_send_file, u8"发送文件", "Send file", u8"Отправить файл") \
|
||||
X(server_disconnect, u8"断开", "Disconnect", u8"Отключить") \
|
||||
X(server_disconnect_all, u8"全部断开", "Disconnect all", u8"Отключить всех") \
|
||||
X(server_confirm_disconnect, u8"断开所有控制端的连接?", "Disconnect all controllers?", u8"Отключить все контроллеры?") \
|
||||
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
|
||||
X(connection_status, u8"连接状态:", \
|
||||
"Connection Status:", u8"Состояние соединения:") \
|
||||
|
||||
@@ -7,6 +7,10 @@ export global ImGuiLineStyle {
|
||||
out property <color> control-separator: #b2b2b2;
|
||||
}
|
||||
|
||||
export global FloatingBarStyle {
|
||||
out property <length> corner-radius: 6px;
|
||||
}
|
||||
|
||||
// ImGui loads a 32px font atlas and applies per-window font scales. These
|
||||
// logical sizes reproduce those visual tiers on Slint's HiDPI renderer.
|
||||
export global ImGuiFontStyle {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
export { AnnouncementItem, MessageStrings } from "message_panel.slint";
|
||||
export { MainWindow, RecentConnection, UpdateCheckStatus, UiStrings } from "main_window.slint";
|
||||
export { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_window.slint";
|
||||
export { ServerWindow, ControllerEntry } from "server_window.slint";
|
||||
export { ServerWindow, ServerStatusBar, ServerStrings, ControllerEntry } from "server_window.slint";
|
||||
|
||||
@@ -1,150 +1,279 @@
|
||||
import { CompactComboBox, FontAwesomeIcons, IconButton, SecondaryButton, ImGuiLineStyle, ImGuiFontStyle } from "common.slint";
|
||||
import { ScrollView } from "std-widgets.slint";
|
||||
import { FontAwesomeIcons, ImGuiLineStyle, FloatingBarStyle } from "common.slint";
|
||||
|
||||
export struct ControllerEntry {
|
||||
remote-id: string,
|
||||
display-name: string,
|
||||
}
|
||||
|
||||
export component ServerWindow inherits Window {
|
||||
export global ServerStrings {
|
||||
in property <string> controlled: "被控中";
|
||||
in property <string> heading: "此设备被控中";
|
||||
in property <string> connected: "已连接";
|
||||
in property <string> send-file: "发送文件";
|
||||
in property <string> disconnect: "断开";
|
||||
in property <string> disconnect-one: "断开连接";
|
||||
in property <string> disconnect-all: "全部断开";
|
||||
in property <string> confirm-disconnect: "断开所有控制端的连接?";
|
||||
in property <string> cancel: "取消";
|
||||
}
|
||||
|
||||
component SessionButton inherits Rectangle {
|
||||
in property <string> text;
|
||||
in property <bool> danger: false;
|
||||
in property <bool> enabled: true;
|
||||
private property <bool> keyboard-focus: false;
|
||||
private property <bool> pointer-feedback: true;
|
||||
callback clicked;
|
||||
width: label.preferred-width + 20px;
|
||||
height: 28px;
|
||||
border-radius: 5px;
|
||||
border-width: 1px;
|
||||
border-color: root.danger ? #d92d20 : #c9ced7;
|
||||
background: root.danger ? (root.pointer-feedback && touch.pressed ? #b42318 : root.pointer-feedback && touch.has-hover ? #f04438 : #d92d20)
|
||||
: (root.pointer-feedback && touch.pressed ? #dfe3e9 : root.pointer-feedback && touch.has-hover ? #f3f4f6 : white);
|
||||
opacity: root.enabled ? 1 : 0.5;
|
||||
accessible-role: button;
|
||||
accessible-label: root.text;
|
||||
accessible-action-default => { if root.enabled { root.pointer-feedback = false; root.clicked(); } }
|
||||
label := Text {
|
||||
text: root.text; color: root.danger ? white : #24272d;
|
||||
font-size: 12px; horizontal-alignment: center; vertical-alignment: center;
|
||||
}
|
||||
forward-focus: keyboard;
|
||||
keyboard := FocusScope {
|
||||
enabled: root.enabled;
|
||||
focus-gained(reason) => {
|
||||
if reason == FocusReason.tab-navigation {
|
||||
root.keyboard-focus = true;
|
||||
} else if reason == FocusReason.pointer-click {
|
||||
root.keyboard-focus = false;
|
||||
}
|
||||
// Preserve the input method when returning from the file picker.
|
||||
}
|
||||
key-pressed(event) => {
|
||||
root.keyboard-focus = true;
|
||||
if root.enabled && (event.text == Key.Return || event.text == " ") {
|
||||
root.pointer-feedback = false;
|
||||
root.clicked(); return accept;
|
||||
}
|
||||
return reject;
|
||||
}
|
||||
}
|
||||
touch := TouchArea {
|
||||
enabled: root.enabled;
|
||||
pointer-event(event) => {
|
||||
if event.kind == PointerEventKind.down || event.kind == PointerEventKind.move {
|
||||
root.pointer-feedback = true;
|
||||
}
|
||||
}
|
||||
// A native dialog may not deliver a pointer exit to the owner window.
|
||||
// Restore hover feedback only after a fresh pointer move or press.
|
||||
clicked => { root.pointer-feedback = false; root.keyboard-focus = false; keyboard.focus(); root.clicked(); }
|
||||
}
|
||||
if root.enabled && keyboard.has-focus && root.keyboard-focus: Rectangle {
|
||||
x: 2px; y: 2px; width: parent.width - 4px; height: parent.height - 4px;
|
||||
border-radius: 3px; border-width: 1px; border-color: root.danger ? white : #2463c7;
|
||||
}
|
||||
}
|
||||
|
||||
// A separate native window keeps the collapsed footprint as small as the bar.
|
||||
export component ServerStatusBar inherits Window {
|
||||
title: "CrossDesk";
|
||||
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
|
||||
preferred-width: root.language-index == 0 ? 250px : root.language-index == 1 ? 330px : 430px;
|
||||
preferred-height: 150px;
|
||||
min-width: root.language-index == 0 ? 250px : root.language-index == 1 ? 330px : 430px;
|
||||
max-width: root.language-index == 0 ? 250px : root.language-index == 1 ? 330px : 430px;
|
||||
no-frame: true;
|
||||
always-on-top: true;
|
||||
background: transparent;
|
||||
default-font-size: ImGuiFontStyle.base;
|
||||
in property <int> controller-count: 1;
|
||||
in property <bool> expanded: false;
|
||||
in property <bool> opens-above: false;
|
||||
in property <bool> dragged: false;
|
||||
out property <length> bar-width: label.preferred-width + 48px;
|
||||
preferred-width: root.bar-width;
|
||||
preferred-height: 36px;
|
||||
min-width: root.bar-width; max-width: root.bar-width;
|
||||
min-height: 36px; max-height: 36px;
|
||||
callback toggle-panel;
|
||||
callback dismiss-panel;
|
||||
callback title-drag(int, length, length);
|
||||
callback move-anchor(float);
|
||||
forward-focus: keyboard;
|
||||
keyboard := FocusScope {
|
||||
key-pressed(event) => {
|
||||
if event.text == Key.Return || event.text == " " { root.toggle-panel(); return accept; }
|
||||
if event.text == Key.Escape { root.dismiss-panel(); return accept; }
|
||||
if event.text == Key.UpArrow { root.move-anchor(-0.05); return accept; }
|
||||
if event.text == Key.DownArrow { root.move-anchor(0.05); return accept; }
|
||||
return reject;
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
x: 0px; y: 0px; width: parent.width; height: parent.height;
|
||||
background: touch.pressed ? #dfe3e9 : touch.has-hover ? #f3f4f6 : white;
|
||||
border-width: 1px; border-color: ImGuiLineStyle.border;
|
||||
border-radius: FloatingBarStyle.corner-radius;
|
||||
}
|
||||
label := Text {
|
||||
x: 10px; width: root.width - 38px; height: root.height;
|
||||
text: ServerStrings.controlled + (root.controller-count > 1 ? " · " + root.controller-count : "");
|
||||
color: #24272d; font-size: 14px; vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
x: root.width - 26px; width: 18px; height: root.height;
|
||||
text: root.opens-above != root.expanded ? FontAwesomeIcons.angle-up : FontAwesomeIcons.angle-down;
|
||||
color: #24272d; font-family: "Font Awesome 6 Free"; font-weight: 900;
|
||||
font-size: 12px; horizontal-alignment: center; vertical-alignment: center;
|
||||
}
|
||||
touch := TouchArea {
|
||||
property <int> phase: self.pressed ? 1 : 0;
|
||||
changed phase => { root.title-drag(self.phase, self.mouse-x, self.mouse-y); }
|
||||
moved => { if self.pressed { root.title-drag(2, self.mouse-x, self.mouse-y); } }
|
||||
clicked => { keyboard.focus(); if !root.dragged { root.toggle-panel(); } }
|
||||
}
|
||||
accessible-role: button;
|
||||
accessible-label: label.text;
|
||||
accessible-action-default => { root.toggle-panel(); }
|
||||
}
|
||||
|
||||
export component ServerWindow inherits Window {
|
||||
title: "CrossDesk";
|
||||
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
|
||||
preferred-width: 225px; min-width: 225px; max-width: 225px;
|
||||
preferred-height: root.content-height;
|
||||
no-frame: true;
|
||||
always-on-top: true;
|
||||
background: transparent;
|
||||
default-font-size: 12px;
|
||||
in property <[ControllerEntry]> controllers;
|
||||
in property <[string]> controller-names;
|
||||
in property <int> language-index: 0;
|
||||
in-out property <int> selected-controller: 0;
|
||||
in property <string> connection-label: "Connection status";
|
||||
in property <string> connection-status: "";
|
||||
in property <string> controller-label: "Controller";
|
||||
in property <string> file-transfer-label: "File transfer";
|
||||
in property <string> select-file-label: "Select file";
|
||||
in property <bool> sending-file: false;
|
||||
in property <bool> file-transfer-visible: false;
|
||||
in property <float> file-progress: 0;
|
||||
in property <string> file-progress-text: "";
|
||||
in property <string> current-file-name: "";
|
||||
in property <string> file-size-text: "";
|
||||
|
||||
callback title-drag(int, length, length);
|
||||
callback toggle-collapsed(bool);
|
||||
in property <string> file-progress-text;
|
||||
in property <string> current-file-name;
|
||||
in property <string> file-size-text;
|
||||
in-out property <bool> confirming-disconnect: false;
|
||||
private property <length> footer-height: send-file.width + disconnect-all.width + 6px > 205px ? 62px : 28px;
|
||||
private property <string> hovered-name;
|
||||
private property <length> tooltip-y;
|
||||
out property <length> content-height: 10px + 24px + (root.controllers.length > 1 ? min(root.controllers.length, 6) * 34px : 28px) + 8px + root.footer-height + 10px + (root.confirming-disconnect ? 36px : 0px) + (root.file-transfer-visible ? 58px : 0px);
|
||||
callback controller-selected(int);
|
||||
callback select-file;
|
||||
callback disconnect-controller;
|
||||
|
||||
private property <bool> collapsed: false;
|
||||
private property <bool> transfer-hovered: false;
|
||||
|
||||
Rectangle {
|
||||
width: root.width;
|
||||
height: root.height;
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
border-color: ImGuiLineStyle.border;
|
||||
border-radius: 8px;
|
||||
clip: true;
|
||||
|
||||
Rectangle {
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
width: parent.width;
|
||||
height: 30px;
|
||||
background: #f8f9fb;
|
||||
Rectangle { y: parent.height - 1px; height: 1px; background: ImGuiLineStyle.border; }
|
||||
IconButton {
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
width: 30px; height: 30px;
|
||||
icon: root.collapsed ? FontAwesomeIcons.angle-down : FontAwesomeIcons.angle-up;
|
||||
icon-size: 12px;
|
||||
clicked => { root.collapsed = !root.collapsed; root.toggle-collapsed(root.collapsed); }
|
||||
}
|
||||
drag := TouchArea {
|
||||
x: 30px; width: parent.width - 30px;
|
||||
property <int> phase: self.pressed ? 1 : 0;
|
||||
changed phase => { root.title-drag(phase, self.mouse-x, self.mouse-y); }
|
||||
moved => { if (self.pressed) { root.title-drag(2, self.mouse-x, self.mouse-y); } }
|
||||
}
|
||||
callback disconnect-controller(string);
|
||||
callback disconnect-all;
|
||||
callback dismiss-panel;
|
||||
forward-focus: keyboard;
|
||||
keyboard := FocusScope {
|
||||
key-pressed(event) => {
|
||||
if event.text == Key.Escape { root.dismiss-panel(); return accept; }
|
||||
return reject;
|
||||
}
|
||||
|
||||
if !root.collapsed: Rectangle {
|
||||
x: 12px; y: 38px; width: parent.width - 52px; height: 101px;
|
||||
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px;
|
||||
VerticalLayout {
|
||||
padding: 8px; spacing: 3px; alignment: start;
|
||||
HorizontalLayout {
|
||||
height: 24px; spacing: 5px;
|
||||
Text { width: root.language-index == 0 ? 69px : 110px; text: root.controller-label; color: #434953; font-size: ImGuiFontStyle.body; vertical-alignment: center; overflow: elide; }
|
||||
CompactComboBox {
|
||||
width: root.language-index == 0 ? 108px : root.language-index == 1 ? 147px : 247px;
|
||||
model: root.controller-names;
|
||||
current-index <=> root.selected-controller;
|
||||
selected(index) => { root.controller-selected(index); }
|
||||
}
|
||||
}
|
||||
Rectangle { height: 1px; background: ImGuiLineStyle.separator; }
|
||||
HorizontalLayout {
|
||||
height: 24px; spacing: 5px;
|
||||
Text { width: root.language-index == 0 ? 69px : 110px; text: root.connection-label; color: #434953; font-size: ImGuiFontStyle.body; vertical-alignment: center; overflow: elide; }
|
||||
Text { text: root.connection-status; color: #2463c7; font-size: ImGuiFontStyle.body; horizontal-alignment: left; vertical-alignment: center; overflow: elide; }
|
||||
}
|
||||
Rectangle { height: 1px; background: ImGuiLineStyle.separator; }
|
||||
Rectangle {
|
||||
height: 24px;
|
||||
HorizontalLayout {
|
||||
y: 0px;
|
||||
width: parent.width;
|
||||
height: 24px;
|
||||
spacing: 5px;
|
||||
Text { width: root.language-index == 0 ? 69px : 110px; text: root.file-transfer-label; color: #434953; font-size: ImGuiFontStyle.body; vertical-alignment: center; overflow: elide; }
|
||||
SecondaryButton { width: root.language-index == 0 ? 78px : root.language-index == 1 ? 140px : 205px; height: 24px; text: root.select-file-label; clicked => { root.select-file(); } }
|
||||
Rectangle {
|
||||
visible: root.file-transfer-visible;
|
||||
width: 35px;
|
||||
Text {
|
||||
text: root.sending-file ? "↑ " + round(root.file-progress * 100) + "%" : "✓";
|
||||
color: root.sending-file ? #2463c7 : #25874b;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
transfer-touch := TouchArea {
|
||||
changed has-hover => { root.transfer-hovered = self.has-hover; }
|
||||
Rectangle {
|
||||
width: root.width; height: root.height;
|
||||
background: white; border-width: 1px; border-color: ImGuiLineStyle.border;
|
||||
border-radius: 8px;
|
||||
Text {
|
||||
x: 10px; y: 10px; width: parent.width - 20px; height: 24px;
|
||||
text: ServerStrings.heading + (root.controllers.length > 1 ? " · " + root.controllers.length : "");
|
||||
color: #24272d; font-size: 13px; font-weight: 500; vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
list := ScrollView {
|
||||
x: 10px; y: 34px; width: parent.width - 20px;
|
||||
height: max(0px, root.height - 34px - root.footer-height - 18px - (root.confirming-disconnect ? 36px : 0px) - (root.file-transfer-visible ? 58px : 0px));
|
||||
viewport-width: self.visible-width;
|
||||
viewport-height: root.controllers.length * (root.controllers.length > 1 ? 34px : 28px);
|
||||
for controller[index] in root.controllers: row := Rectangle {
|
||||
x: 0px; y: index * (root.controllers.length > 1 ? 34px : 28px);
|
||||
width: list.visible-width; height: root.controllers.length > 1 ? 32px : 28px;
|
||||
select := Rectangle {
|
||||
x: 0px; y: 0px;
|
||||
width: parent.width - (root.controllers.length > 1 ? disconnect.width + 4px : status.preferred-width + 8px);
|
||||
height: parent.height; border-radius: 4px;
|
||||
background: root.controllers.length > 1 && index == root.selected-controller ? #eef5fd : select-touch.has-hover ? #f3f4f6 : transparent;
|
||||
if root.controllers.length > 1: Rectangle {
|
||||
x: 4px; y: (parent.height - 13px) / 2; width: 13px; height: 13px;
|
||||
border-radius: 7px; border-width: 1px; border-color: index == root.selected-controller ? #2463c7 : #8c939e;
|
||||
if index == root.selected-controller: Rectangle { x: 3px; y: 3px; width: 7px; height: 7px; border-radius: 4px; background: #2463c7; }
|
||||
}
|
||||
name := Text {
|
||||
x: root.controllers.length > 1 ? 23px : 0px; width: parent.width - self.x - 4px;
|
||||
height: parent.height; text: controller.display-name; color: #24272d;
|
||||
font-size: 13px; vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
forward-focus: selection-keyboard;
|
||||
selection-keyboard := FocusScope {
|
||||
key-pressed(event) => {
|
||||
if event.text == Key.Return || event.text == " " {
|
||||
root.selected-controller = index; root.controller-selected(index); return accept;
|
||||
}
|
||||
if event.text == Key.UpArrow || event.text == Key.DownArrow {
|
||||
root.selected-controller = max(0, min(root.controllers.length - 1, root.selected-controller + (event.text == Key.UpArrow ? -1 : 1)));
|
||||
root.controller-selected(root.selected-controller); return accept;
|
||||
}
|
||||
return reject;
|
||||
}
|
||||
}
|
||||
select-touch := TouchArea {
|
||||
clicked => { selection-keyboard.focus(); root.selected-controller = index; root.controller-selected(index); }
|
||||
changed has-hover => {
|
||||
root.hovered-name = self.has-hover && name.preferred-width > name.width ? controller.display-name : "";
|
||||
root.tooltip-y = max(0px, min(root.height - 52px, list.y + row.y + list.viewport-y + row.height));
|
||||
}
|
||||
}
|
||||
accessible-role: radio-button;
|
||||
accessible-label: controller.display-name;
|
||||
accessible-checked: index == root.selected-controller;
|
||||
accessible-action-default => { root.selected-controller = index; root.controller-selected(index); }
|
||||
|
||||
}
|
||||
status := Text {
|
||||
visible: root.controllers.length == 1;
|
||||
x: parent.width - self.preferred-width; height: parent.height;
|
||||
text: ServerStrings.connected; color: #16a34a; font-size: 12px; vertical-alignment: center;
|
||||
}
|
||||
disconnect := SessionButton {
|
||||
visible: root.controllers.length > 1;
|
||||
x: parent.width - self.width; y: 2px;
|
||||
text: ServerStrings.disconnect; danger: true;
|
||||
clicked => { root.confirming-disconnect = false; root.disconnect-controller(controller.remote-id); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !root.collapsed: IconButton {
|
||||
x: parent.width - 34px; y: 38px; width: 24px; height: 101px;
|
||||
icon: FontAwesomeIcons.xmark;
|
||||
icon-size: 13px;
|
||||
icon-color: white;
|
||||
normal-background: #ef2b2d;
|
||||
hover-background: #d92d20;
|
||||
pressed-background: #b42318;
|
||||
border-radius: 5px;
|
||||
clicked => { root.disconnect-controller(); }
|
||||
}
|
||||
if root.file-transfer-visible && root.transfer-hovered: Rectangle {
|
||||
x: 15px; y: 67px; width: root.width - 30px; height: 70px;
|
||||
background: #22252bf2;
|
||||
border-radius: 5px;
|
||||
z: 20;
|
||||
VerticalLayout {
|
||||
padding: 8px; spacing: 3px;
|
||||
Text { text: root.current-file-name; color: white; font-size: ImGuiFontStyle.small; overflow: elide; }
|
||||
Text { text: root.file-size-text; color: #e1e4e8; font-size: ImGuiFontStyle.small; }
|
||||
Text { text: round(root.file-progress * 100) + "% " + root.file-progress-text; color: #e1e4e8; font-size: ImGuiFontStyle.small; }
|
||||
footer := Rectangle {
|
||||
x: 10px; y: list.y + list.height + 8px; width: parent.width - 20px; height: root.footer-height;
|
||||
send-file := SessionButton {
|
||||
x: root.footer-height > 28px ? parent.width - self.width : disconnect-all.x - self.width - 6px;
|
||||
y: 0px;
|
||||
text: root.confirming-disconnect ? ServerStrings.cancel : ServerStrings.send-file;
|
||||
enabled: root.confirming-disconnect || !root.sending-file;
|
||||
clicked => { if root.confirming-disconnect { root.confirming-disconnect = false; } else { root.select-file(); } }
|
||||
}
|
||||
disconnect-all := SessionButton {
|
||||
x: parent.width - self.width; y: root.footer-height - 28px;
|
||||
text: root.controllers.length > 1 ? ServerStrings.disconnect-all : ServerStrings.disconnect-one;
|
||||
danger: true;
|
||||
clicked => {
|
||||
if root.controllers.length == 1 { root.disconnect-controller(root.controllers[0].remote-id); }
|
||||
else if root.confirming-disconnect { root.confirming-disconnect = false; root.disconnect-all(); }
|
||||
else { root.confirming-disconnect = true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if root.confirming-disconnect: Text {
|
||||
x: 10px; y: footer.y + footer.height + 6px; width: parent.width - 20px; height: 30px;
|
||||
text: ServerStrings.confirm-disconnect; color: #d92d20; font-size: 12px; wrap: word-wrap;
|
||||
}
|
||||
if root.file-transfer-visible: VerticalLayout {
|
||||
x: 10px; y: root.height - 62px; width: parent.width - 20px; height: 52px; spacing: 2px;
|
||||
Text { text: root.current-file-name; color: #596575; font-size: 11px; overflow: elide; }
|
||||
Text { text: root.file-size-text; color: #596575; font-size: 11px; overflow: elide; }
|
||||
Text { text: round(root.file-progress * 100) + "% " + root.file-progress-text; color: #2463c7; font-size: 11px; overflow: elide; }
|
||||
}
|
||||
}
|
||||
// Draw outside the scrolling list so its last row can reveal its name.
|
||||
if root.hovered-name != "": Rectangle {
|
||||
x: 10px; y: root.tooltip-y; width: 205px; height: tip.preferred-height + 12px; z: 20;
|
||||
background: #24272d; border-radius: 5px;
|
||||
tip := Text { x: 6px; y: 6px; width: parent.width - 12px; text: root.hovered-name; color: white; font-size: 12px; wrap: word-wrap; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ComboBox, ScrollView } from "std-widgets.slint";
|
||||
import { FontAwesomeIcons, IconButton, PrimaryButton, DialogSurface, ImGuiLineStyle, ImGuiFontStyle } from "common.slint";
|
||||
import { FontAwesomeIcons, IconButton, PrimaryButton, DialogSurface, ImGuiLineStyle, ImGuiFontStyle, FloatingBarStyle } from "common.slint";
|
||||
|
||||
export struct StreamTab {
|
||||
remote-id: string,
|
||||
@@ -810,7 +810,7 @@ export component StreamWindow inherits Window {
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
border-color: ImGuiLineStyle.border;
|
||||
border-radius: 6px;
|
||||
border-radius: FloatingBarStyle.corner-radius;
|
||||
z: 8;
|
||||
clip: true;
|
||||
// Keep width changes atomic. When docked right, animating both
|
||||
|
||||
@@ -20,6 +20,21 @@ inline slint::SharedString Text(const std::string& value) {
|
||||
return slint::SharedString(value);
|
||||
}
|
||||
|
||||
template <typename Component>
|
||||
void ApplyServerStrings(const Component& component, int language_index) {
|
||||
const int language = localization::detail::ClampLanguageIndex(language_index);
|
||||
auto& strings = component->template global<ui::ServerStrings>();
|
||||
strings.set_controlled(Text(localization::server_controlled[language]));
|
||||
strings.set_heading(Text(localization::server_heading[language]));
|
||||
strings.set_connected(Text(localization::server_connected[language]));
|
||||
strings.set_send_file(Text(localization::server_send_file[language]));
|
||||
strings.set_disconnect(Text(localization::server_disconnect[language]));
|
||||
strings.set_disconnect_one(Text(localization::disconnect[language]));
|
||||
strings.set_disconnect_all(Text(localization::server_disconnect_all[language]));
|
||||
strings.set_confirm_disconnect(Text(localization::server_confirm_disconnect[language]));
|
||||
strings.set_cancel(Text(localization::cancel[language]));
|
||||
}
|
||||
|
||||
inline int ApplyMainWindowStrings(
|
||||
const slint::ComponentHandle<ui::MainWindow>& window,
|
||||
int language_index) {
|
||||
|
||||
@@ -351,25 +351,11 @@ void ConfigureServerCapturePage(
|
||||
server->set_controllers(
|
||||
std::make_shared<slint::VectorModel<crossdesk::ui::ControllerEntry>>(
|
||||
std::vector{controller}));
|
||||
server->set_controller_names(
|
||||
std::make_shared<slint::VectorModel<slint::SharedString>>(
|
||||
std::vector{slint::SharedString("Mac")}));
|
||||
server->set_language_index(language);
|
||||
server->set_controller_label(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::controller[language]));
|
||||
server->set_connection_label(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::connection_status[language]));
|
||||
server->set_connection_status(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::p2p_connected[language]));
|
||||
server->set_file_transfer_label(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::file_transfer[language]));
|
||||
server->set_select_file_label(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::select_file[language]));
|
||||
crossdesk::ui_localization::ApplyServerStrings(server, language);
|
||||
server->set_file_transfer_visible(false);
|
||||
server->set_sending_file(false);
|
||||
const float width = language == 0 ? 250.0f : language == 1 ? 330.0f : 430.0f;
|
||||
server->window().set_size(
|
||||
slint::LogicalSize(slint::Size<float>{width, 150.0f}));
|
||||
server->window().set_size(slint::LogicalSize(
|
||||
slint::Size<float>{225.0f, server->get_content_height()}));
|
||||
}
|
||||
|
||||
int RunCaptureMode(
|
||||
|
||||
Reference in New Issue
Block a user