mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
[feat] add resizable DeskHub focus previews
This commit is contained in:
@@ -1278,6 +1278,9 @@ void GuiApplication::InitializeUi() {
|
||||
if (std::isfinite(tile_width))
|
||||
// The rendered grid constrains the restored width to available space.
|
||||
ui_->wall->set_gallery_tile_width(std::max(tile_width, 184.0f));
|
||||
const float focus_rail_height = saved.value("focus_rail_height", 70.875f);
|
||||
if (std::isfinite(focus_rail_height))
|
||||
ui_->wall->set_gallery_focus_rail_height(std::max(focus_rail_height, 54.0f));
|
||||
ui_->connection_window.BeginRestore();
|
||||
}
|
||||
} catch (...) {
|
||||
@@ -4526,6 +4529,7 @@ void GuiApplication::SaveGalleryWindowState() {
|
||||
const float connection_scale = std::max(ui_->main->window().scale_factor(), 1.0f);
|
||||
nlohmann::json state{{"version", 2},
|
||||
{"tile_width", ui_->wall->get_gallery_tile_width()},
|
||||
{"focus_rail_height", ui_->wall->get_gallery_focus_rail_height()},
|
||||
{"fullscreen", ui_->gallery_window.wall_fullscreen},
|
||||
{"maximized", ui_->gallery_window.wall_maximized}};
|
||||
if (ui_->gallery_window.wall_size) {
|
||||
|
||||
@@ -55,6 +55,8 @@ export component GalleryView inherits Rectangle {
|
||||
in property <bool> window-active: true;
|
||||
in-out property <float> tile-width: 216;
|
||||
out property <bool> resizing: false;
|
||||
in-out property <float> focus-rail-height: 70.875;
|
||||
out property <bool> focus-resizing: false;
|
||||
private property <float> preview-aspect-ratio: 16 / 9;
|
||||
private property <length> card-inset: 6px;
|
||||
private property <length> card-chrome-height: root.simplified ? root.card-inset * 2 : 36px;
|
||||
@@ -64,9 +66,19 @@ export component GalleryView inherits Rectangle {
|
||||
private property <length> card-width: clamp(root.tile-width * 1px, 184px, root.max-card-width);
|
||||
private property <length> card-preview-height: (root.card-width - root.card-inset * 2) / root.preview-aspect-ratio;
|
||||
private property <length> card-height: root.card-preview-height + root.card-chrome-height;
|
||||
private property <length> rail-preview-width: 126px;
|
||||
private property <length> rail-preview-height: root.rail-preview-width / root.preview-aspect-ratio;
|
||||
private property <length> rail-card-height: root.rail-preview-height + (root.simplified ? 10px : 24px);
|
||||
private property <bool> focus-rail-visible: root.focused.id != "" && root.tiles.length > 1;
|
||||
private property <length> rail-chrome-height: root.simplified ? 10px : 24px;
|
||||
private property <length> min-rail-preview-height: 54px;
|
||||
// Leave at least 160px for the main preview and keep each thumbnail inside
|
||||
// the window. Clamp the rendered size without losing the saved preference.
|
||||
private property <length> max-rail-preview-height: max(root.min-rail-preview-height,
|
||||
min((focus-view.width - 10px) / root.preview-aspect-ratio,
|
||||
focus-view.height - 36px - root.rail-chrome-height - 14px - 12px - 160px));
|
||||
private property <length> rail-preview-height: clamp(root.focus-rail-height * 1px,
|
||||
root.min-rail-preview-height, root.max-rail-preview-height);
|
||||
private property <length> rail-preview-width: root.rail-preview-height * root.preview-aspect-ratio;
|
||||
private property <length> rail-card-height: root.rail-preview-height + root.rail-chrome-height;
|
||||
private property <length> rail-slot-width: root.rail-preview-width + 18px;
|
||||
private property <string> focused-id: root.focused.id;
|
||||
private property <string> device-tooltip-id;
|
||||
private property <string> device-tooltip-text;
|
||||
@@ -89,19 +101,20 @@ export component GalleryView inherits Rectangle {
|
||||
private property <length> drag-edge-width: 32px;
|
||||
private property <int> drag-page-direction: root.page-direction(root.drag-x, root.drag-y);
|
||||
private property <bool> swap-save-failed: false;
|
||||
changed focused-id => { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); }
|
||||
changed visible => { root.device-tooltip-id = ""; if !root.visible { root.resizing = false; root.cancel-device-drag(); add-menu.close-menu(); batch-menu.close-menu(); } }
|
||||
changed width => { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); }
|
||||
changed height => { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); }
|
||||
changed window-active => { if !root.window-active { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); } }
|
||||
changed focused-id => { root.device-tooltip-id = ""; root.resizing = false; root.focus-resizing = false; root.cancel-device-drag(); }
|
||||
changed focus-rail-visible => { root.focus-resizing = false; }
|
||||
changed visible => { root.device-tooltip-id = ""; if !root.visible { root.resizing = false; root.focus-resizing = false; root.cancel-device-drag(); add-menu.close-menu(); batch-menu.close-menu(); } }
|
||||
changed width => { root.device-tooltip-id = ""; root.resizing = false; root.focus-resizing = false; root.cancel-device-drag(); }
|
||||
changed height => { root.device-tooltip-id = ""; root.resizing = false; root.focus-resizing = false; root.cancel-device-drag(); }
|
||||
changed window-active => { if !root.window-active { root.device-tooltip-id = ""; root.resizing = false; root.focus-resizing = false; root.cancel-device-drag(); } }
|
||||
changed page => { root.device-tooltip-id = ""; if root.page != root.drag-page { root.cancel-device-drag(); } }
|
||||
changed group-index => { root.device-tooltip-id = ""; root.cancel-device-drag(); }
|
||||
changed card-width => { root.cancel-device-drag(); }
|
||||
changed card-height => { root.cancel-device-drag(); }
|
||||
changed device-tooltip-id => {
|
||||
root.device-tooltip-visible = false;
|
||||
device-tooltip-timer.running = root.device-tooltip-id == "resize-grid";
|
||||
if root.device-tooltip-id == "resize-grid" { device-tooltip-timer.restart(); }
|
||||
device-tooltip-timer.running = root.device-tooltip-id == "resize-grid" || root.device-tooltip-id == "resize-focus";
|
||||
if device-tooltip-timer.running { device-tooltip-timer.restart(); }
|
||||
}
|
||||
device-tooltip-timer := Timer {
|
||||
interval: 450ms; running: false;
|
||||
@@ -168,6 +181,7 @@ export component GalleryView inherits Rectangle {
|
||||
}
|
||||
public function reset-transient() {
|
||||
root.resizing = false;
|
||||
root.focus-resizing = false;
|
||||
root.cancel-device-drag();
|
||||
root.batch = false; root.adding = false; root.importing = false; root.confirming = false;
|
||||
root.device-tooltip-id = ""; add-menu.close-menu(); batch-menu.close-menu();
|
||||
@@ -561,7 +575,7 @@ export component GalleryView inherits Rectangle {
|
||||
CompactButton { width: 100px; primary: true; enabled: !root.focused.connected || root.controlling || (root.focused.has-frame && !root.focused.stale); text: root.focused.connected ? (root.controlling ? GalleryStrings.tr("释放操控", "Release", "Отпустить") : GalleryStrings.tr("接管操控", "Control", "Управлять")) : GalleryStrings.tr("连接", "Connect", "Подключить"); clicked => { root.action(root.focused.connected ? (root.controlling ? "release" : "control") : "connect", root.focused.id); } }
|
||||
}
|
||||
Rectangle {
|
||||
x: 0px; y: rail.visible ? rail.y + rail.height + 10px : 36px;
|
||||
x: 0px; y: rail.visible ? focus-divider.y + focus-divider.height : 36px;
|
||||
width: parent.width; height: parent.height - self.y;
|
||||
background: root.focused.online ? #eff1f4 : DeviceCardStyle.offline-background; border-radius: 6px;
|
||||
border-width: root.controlling ? 2px : 1px; border-color: root.controlling ? #76aeea : root.focused.online ? #d5dbe3 : DeviceCardStyle.offline-border;
|
||||
@@ -607,10 +621,10 @@ export component GalleryView inherits Rectangle {
|
||||
}
|
||||
}
|
||||
rail := ScrollView {
|
||||
visible: root.tiles.length > 1;
|
||||
visible: root.focus-rail-visible;
|
||||
x: 0px; y: 36px; width: parent.width; height: root.rail-card-height + 14px;
|
||||
// Reserve 14px below the cards for the horizontal scrollbar.
|
||||
viewport-width: max(self.width, max(0, root.tiles.length - 1) * 144px);
|
||||
viewport-width: max(self.width, max(0, root.tiles.length - 1) * root.rail-slot-width);
|
||||
viewport-height: self.height;
|
||||
horizontal-scrollbar-policy: as-needed;
|
||||
vertical-scrollbar-policy: always-off;
|
||||
@@ -627,13 +641,13 @@ export component GalleryView inherits Rectangle {
|
||||
x: 0px; y: 0px; height: root.rail-card-height; spacing: 0px; padding: 0px;
|
||||
for tile in root.tiles: rail-slot := Rectangle {
|
||||
// Collapse the focused tile and its gap without changing the page model.
|
||||
width: tile.id == root.focused.id ? 0px : 144px;
|
||||
width: tile.id == root.focused.id ? 0px : root.rail-slot-width;
|
||||
height: root.rail-card-height; visible: tile.id != root.focused.id;
|
||||
rail-card := Rectangle {
|
||||
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
|
||||
: tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно");
|
||||
private property <string> details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence;
|
||||
width: 136px; height: root.rail-card-height; border-radius: 5px;
|
||||
width: root.rail-preview-width + 10px; height: root.rail-card-height; border-radius: 5px;
|
||||
background: tile.online ? #f5f6f8 : DeviceCardStyle.offline-background; border-width: 1px; border-color: tile.online ? #d5dbe3 : DeviceCardStyle.offline-border;
|
||||
accessible-role: button;
|
||||
accessible-label: self.details;
|
||||
@@ -649,7 +663,7 @@ export component GalleryView inherits Rectangle {
|
||||
}
|
||||
Text {
|
||||
visible: !root.simplified;
|
||||
x: 7px; y: root.rail-preview-height + 6px; width: 122px; height: 16px;
|
||||
x: 7px; y: root.rail-preview-height + 6px; width: parent.width - 14px; height: 16px;
|
||||
text: tile.name; color: tile.online ? #303943 : DeviceCardStyle.offline-text; font-size: 11px;
|
||||
overflow: elide; vertical-alignment: center;
|
||||
}
|
||||
@@ -670,6 +684,68 @@ export component GalleryView inherits Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
focus-divider := Rectangle {
|
||||
visible: root.focus-rail-visible;
|
||||
x: 0px; y: rail.y + rail.height; width: parent.width; height: 12px;
|
||||
private property <bool> highlighted: divider-touch.has-hover || root.focus-resizing || divider-focus.has-focus;
|
||||
Rectangle {
|
||||
x: 0px; y: 5px; width: parent.width; height: 1px;
|
||||
background: focus-divider.highlighted ? #a6c9ed : #e6e9ed;
|
||||
}
|
||||
Rectangle {
|
||||
x: (parent.width - self.width) / 2; y: 4px; width: 36px; height: 3px; border-radius: 1.5px;
|
||||
background: focus-divider.highlighted ? #6dabea : #bcc4ce;
|
||||
}
|
||||
divider-focus := FocusScope {
|
||||
width: parent.width; height: parent.height;
|
||||
focus-lost => { root.focus-resizing = false; }
|
||||
key-pressed(event) => {
|
||||
if event.text == Key.Escape { root.focus-resizing = false; return accept; }
|
||||
if event.text == Key.UpArrow || event.text == Key.DownArrow {
|
||||
root.focus-rail-height = clamp(root.rail-preview-height
|
||||
+ (event.text == Key.UpArrow ? -8px : 8px),
|
||||
root.min-rail-preview-height, root.max-rail-preview-height) / 1px;
|
||||
return accept;
|
||||
}
|
||||
reject
|
||||
}
|
||||
divider-touch := TouchArea {
|
||||
mouse-cursor: MouseCursor.ns-resize;
|
||||
private property <length> start-y;
|
||||
private property <length> start-height;
|
||||
function show-hint() {
|
||||
if self.has-hover && !root.focus-resizing {
|
||||
root.device-tooltip-id = "resize-focus";
|
||||
root.device-tooltip-text = GalleryStrings.tr("上下拖动,调整缩略图与主画面大小", "Drag up or down to resize the thumbnails and main preview", "Потяните вверх или вниз, чтобы изменить размер миниатюр и основного экрана");
|
||||
root.device-tooltip-x = focus-view.x + self.mouse-x;
|
||||
root.device-tooltip-y = focus-view.y + focus-divider.y + self.mouse-y + 16px;
|
||||
} else if root.device-tooltip-id == "resize-focus" { root.device-tooltip-id = ""; }
|
||||
}
|
||||
function resize-preview() {
|
||||
if !root.focus-resizing { return; }
|
||||
// Use coordinates in the fixed parent: the divider itself
|
||||
// moves while it retains the pointer grab.
|
||||
root.focus-rail-height = clamp(self.start-height
|
||||
+ focus-divider.y + self.mouse-y - self.start-y,
|
||||
root.min-rail-preview-height, root.max-rail-preview-height) / 1px;
|
||||
}
|
||||
changed has-hover => { self.show-hint(); }
|
||||
pointer-event(event) => {
|
||||
if event.button == PointerEventButton.left && event.kind == PointerEventKind.down {
|
||||
divider-focus.focus();
|
||||
self.start-y = focus-divider.y + self.mouse-y;
|
||||
self.start-height = root.rail-preview-height;
|
||||
root.device-tooltip-id = ""; root.focus-resizing = true;
|
||||
} else if event.kind == PointerEventKind.cancel {
|
||||
root.focus-resizing = false;
|
||||
} else if event.button == PointerEventButton.left && event.kind == PointerEventKind.up {
|
||||
self.resize-preview(); root.focus-resizing = false;
|
||||
}
|
||||
}
|
||||
moved => { self.resize-preview(); self.show-hint(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
y: parent.height - 44px; height: 44px; background: #fafbfc;
|
||||
@@ -766,7 +842,7 @@ export component GalleryView inherits Rectangle {
|
||||
}
|
||||
}
|
||||
// Device details are rendered by MainWindow; resize hints stay local.
|
||||
if root.device-tooltip-visible && root.device-tooltip-id == "resize-grid": Rectangle {
|
||||
if root.device-tooltip-visible && (root.device-tooltip-id == "resize-grid" || root.device-tooltip-id == "resize-focus"): Rectangle {
|
||||
x: clamp(root.device-tooltip-x, 8px, parent.width - self.width - 8px);
|
||||
y: clamp(root.device-tooltip-y + self.height > footer.y ? root.device-tooltip-y - self.height - 8px : root.device-tooltip-y, 4px, parent.height - self.height - 4px);
|
||||
width: min(320px, parent.width - 16px); height: device-tooltip-label.preferred-height + 16px;
|
||||
|
||||
@@ -322,7 +322,8 @@ export component MainWindow inherits Window {
|
||||
out property <int> gallery-columns: gallery.columns;
|
||||
out property <int> gallery-rows: gallery.rows;
|
||||
in-out property <float> gallery-tile-width: 216;
|
||||
out property <bool> gallery-resizing: gallery.resizing;
|
||||
in-out property <float> gallery-focus-rail-height: 70.875;
|
||||
out property <bool> gallery-resizing: gallery.resizing || gallery.focus-resizing;
|
||||
in property <int> gallery-page: 0;
|
||||
in property <int> gallery-pages: 1;
|
||||
in property <int> gallery-count: 0;
|
||||
@@ -1317,6 +1318,7 @@ export component MainWindow inherits Window {
|
||||
toggle-fullscreen => { root.toggle-gallery-fullscreen(); }
|
||||
window-active: root.window-active;
|
||||
tile-width <=> root.gallery-tile-width;
|
||||
focus-rail-height <=> root.gallery-focus-rail-height;
|
||||
network-open => { root.gallery-network-open = true; }
|
||||
controlling: root.gallery-controlling;
|
||||
input-suspended: root.gallery-transitioning;
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
- 服务器连接状态、指示灯和重连入口仅在主窗口显示,桌面墙的普通、精简、展开、批量选择和全屏视图均不显示服务器状态栏。桌面墙底部操作栏左侧以普通文字显示已连接设备数量,旁边是紧凑的“网络状态”按钮;点击按钮可打开当前已建立连接的设备列表,空白区域不响应点击,也不显示整条悬停提示。弹窗采用紧凑宽度,高度随设备数量调整,最多同时显示 6 行,更多设备通过滚动查看。网络列表不受桌面墙分组和分页限制;离线、未连接和连接中的设备不列入,断开后自动移除。列表显示设备名称、ID、直连/中继、实际协商的 SRTP 加密状态、往返延迟(RTT)、控制端接收与发送速率、视频丢包率、实际接收分辨率与帧率,打开期间每秒刷新。加密状态紧随连接方式显示为“已加密/未加密”,尚无统计或报告超过 3 秒未更新时显示“待确认”。分辨率与帧率在解码交付时统计,墙内预览和后台会话均可用;尚未收到画面时显示“—”,画面停止更新超过 1 秒时帧率显示 0 FPS。网络数值尚无统计或报告超过 3 秒未更新时显示“—”。可通过关闭按钮、Esc 或点击列表外关闭;打开列表会释放远程操控。
|
||||
- 点卡片进入只读放大视图,点“接管操控”才开始发送键鼠。点“释放操控”或按 Ctrl/⌘ + Shift + Esc 可以释放。切换设备、翻页、打开本地操作面板、切换模式及窗口失焦也会释放。
|
||||
- 桌面墙连接默认静音,包含新建连接、重连、复用已有连接、添加或导入已有会话,以及切入桌面墙时已有的连接。展开、全屏或接管操控不会自动开启声音;需要声音时,可通过批量“取消静音”或在独立串流窗口主动开启。两个入口共用同一静音状态,主动开启的声音在桌面墙可见时播放,翻页、展开及释放操控保留声音设置;每台设备使用独立音频流同时播放,静音一台不会清空其他设备的声音。新版本对端按会话关闭音频订阅;旧版本发送的音频由控制端丢弃,不通过停止对端的全局声音采集影响其他控制端。
|
||||
- 放大视图使用一行设备操作栏,显示返回、设备名称、状态和接管/释放操控按钮,右侧保留精简模式及全屏入口;选中的桌面在下方放大显示,同页其他桌面的缩略图统一在上方横向排列,超出宽度时可横向滚动。点击上方缩略图可切换下方桌面,当前桌面不在缩略图中重复显示;同页只有一台设备时隐藏缩略图栏。普通模式缩略图下方以一行显示设备名,长名称省略,不显示状态圆点;不在线设备的缩略图和下方放大预览统一使用灰色,精简模式同样生效;悬停可查看设备名称、ID 和在线状态,精简模式也支持悬停查看。缺少画面或画面停滞时保留状态占位;滚动条单独留出空间。普通和精简模式的缩略图预览区域均固定为 16:9,普通模式的设备名称单独显示在画面下方。普通、精简、宽窗口及全屏均保持上下布局,调整尺寸后仍保留当前设备所在页。主预览和缩略图均完整等比显示,鼠标坐标按实际预览区域映射。
|
||||
- 放大视图使用一行设备操作栏,显示返回、设备名称、状态和接管/释放操控按钮,右侧保留精简模式及全屏入口;选中的桌面在下方放大显示,同页其他桌面的缩略图统一在上方横向排列,超出宽度时可横向滚动。点击上方缩略图可切换下方桌面,当前桌面不在缩略图中重复显示;同页只有一台设备时隐藏缩略图栏。普通模式缩略图下方以一行显示设备名,长名称省略,不显示状态圆点;不在线设备的缩略图和下方放大预览统一使用灰色,精简模式同样生效;悬停可查看设备名称、ID 和在线状态,精简模式也支持悬停查看。缺少画面或画面停滞时保留状态占位;滚动条单独留出空间。普通和精简模式的缩略图预览区域均固定为 16:9,普通模式的设备名称单独显示在画面下方。普通、精简、宽窗口及全屏均保持上下布局,上下区域之间提供带抓手的分隔条:向上拖动扩大下方主画面,向下拖动同步放大上方缩略图;选中分隔条后也可用上下方向键微调。缩略图始终保持 16:9,并保留主画面的最小可用高度;窗口缩小时临时限制尺寸,恢复窗口后沿用原先尺寸。普通和精简模式共用此尺寸,切换设备后保持,松开鼠标后保存并在重启时恢复;与网格的单元格尺寸独立。同页只有一台设备时同时隐藏缩略图栏和分隔条。拖动分隔条不会切换设备或发送远程键鼠,Esc、窗口失焦及窗口大小变化会结束拖动。调整尺寸后仍保留当前设备所在页。主预览和缩略图均完整等比显示,鼠标坐标按实际预览区域映射。
|
||||
- 在“近期连接”中点击已连接设备的卡片画面,可打开原有独立串流窗口,使用声音、显示器切换、隐私屏、文件传输等完整单会话工具。关闭墙内设备的独立窗口不会断开墙中的会话。
|
||||
- 点击“选择”进入批量模式,“完成”在同一位置退出;顶部设备管理栏原位切换为批量操作栏,从左到右显示已选数量(包含跨页数量)、“全选/取消全选”、“连接”、“断开”、“更多”和“完成”,无需往返底栏。批量模式暂时收起分组筛选和“添加”入口,退出后恢复;工具栏高度不变,设备卡片的位置、尺寸及分页容量保持不变。已选数量占用剩余空间,批量操作按钮同样按文字宽度和统一内边距自适应;“全选/取消全选”使用与“断开”、“更多”一致的浅灰背景和边框,并预留两种文字中较宽的一种,切换选中数量或按钮文字时,后续按钮保持原位。“连接”保留主操作的蓝色强调,“完成”以蓝色表示当前选择模式。点击“全选”会选中当前筛选下的所有设备,跨页生效;全部选中后显示“取消全选”。翻页保留选择,切换分组清空选择;底栏始终保留已连接数量、网络状态入口和分页;静音和文件分发收进顶部向下展开的“更多”菜单。声音菜单项根据已选且已连接设备的状态切换:全部静音时显示“取消静音”,有设备开启声音时显示“静音”,无已连接设备时禁用。点击后统一设置所有已选的已连接设备,包含跨页选择,跳过未连接设备,保留当前选择。断开和文件分发会先列出完整目标;提交后的操作目标不会跟随之后的选择变化。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user