From fe93c7e0e4533a7c6e08aa166caf1e41ee7915ec Mon Sep 17 00:00:00 2001 From: dijunkun Date: Mon, 19 Oct 2026 01:43:52 +0800 Subject: [PATCH] [ui] refine recent connection card actions --- .../src/gui/ui/gallery_menu_button.slint | 63 +++++++++-- apps/desktop/src/gui/ui/main_window.slint | 107 +++++++++++++----- .../src/gui/ui/workbench_controls.slint | 22 ++-- 3 files changed, 148 insertions(+), 44 deletions(-) diff --git a/apps/desktop/src/gui/ui/gallery_menu_button.slint b/apps/desktop/src/gui/ui/gallery_menu_button.slint index a94c3f0e..30673f8e 100644 --- a/apps/desktop/src/gui/ui/gallery_menu_button.slint +++ b/apps/desktop/src/gui/ui/gallery_menu_button.slint @@ -8,10 +8,18 @@ export component GalleryMenuButton inherits Rectangle { in property primary: false; in property enabled: true; in property opens-up: false; + in property compact-trigger: false; + in property compact-menu: false; in property menu-width: 200px; in property horizontal-padding: 12px; out property preferred-button-width: ceil(button-label.preferred-width / 1px) * 1px + root.horizontal-padding * 2 + 18px; in property <[GalleryMenuItem]> items; + in property <[string]> item-icons; + in property destructive-index: -1; + out property menu-open: popup.is-open; + private property row-height: root.compact-menu ? 24px : 30px; + private property menu-padding: root.compact-menu ? 3px : 4px; + private property divider-spacing: root.compact-menu ? 5px : 7px; private property current-index: 0; callback opened; callback selected(int); @@ -60,48 +68,89 @@ export component GalleryMenuButton inherits Rectangle { trigger-focus := FocusScope { enabled: root.enabled; key-pressed(event) => { return root.handle-key(event); } - CompactButton { + if !root.compact-trigger: CompactButton { width: parent.width; height: parent.height; border-radius: UiRadius.control; enabled: root.enabled; primary: root.primary; clicked => { trigger-focus.focus(); root.open-menu(); } } button-label := Text { + visible: !root.compact-trigger; x: root.horizontal-padding; width: parent.width - root.horizontal-padding * 2 - 18px; height: parent.height; text: root.text; color: root.enabled ? #24272d : #8e9298; font-size: 11px; overflow: elide; horizontal-alignment: center; vertical-alignment: center; } Text { + visible: !root.compact-trigger; x: parent.width - root.horizontal-padding - 12px; width: 12px; height: parent.height; text: root.opens-up ? FontAwesomeIcons.angle-up : FontAwesomeIcons.angle-down; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 9px; color: root.enabled ? #62666d : #8e9298; horizontal-alignment: center; vertical-alignment: center; } + if root.compact-trigger: Rectangle { + background: more-touch.pressed || popup.is-open ? #e5edfc : more-touch.has-hover ? white : #fffffff0; + border-radius: UiRadius.menu; + border-width: 1px; + border-color: trigger-focus.has-focus ? #3564e3 : #ffffff80; + animate background, border-color { duration: 120ms; } + for dot in 3: Rectangle { + x: parent.width / 2 - 6px + dot * 5px; + y: (parent.height - self.height) / 2; + width: 2px; height: 2px; border-radius: 1px; + background: #35445e; + } + more-touch := TouchArea { + enabled: root.enabled; + mouse-cursor: pointer; + clicked => { trigger-focus.focus(); root.open-menu(); } + } + } } popup := PopupWindow { x: root.width - self.width; y: root.opens-up ? -self.height - 4px : root.height + 4px; - width: root.menu-width; height: root.items.length * 30px + 8px; + width: root.menu-width; + height: root.items.length * root.row-height + root.menu-padding * 2 + + (root.destructive-index > 0 ? root.divider-spacing : 0px); close-policy: close-on-click-outside; forward-focus: popup-focus; popup-focus := FocusScope { key-pressed(event) => { return root.handle-key(event); } Rectangle { background: white; border-width: 1px; border-color: #c9cdd2; border-radius: UiRadius.menu; + if root.destructive-index > 0: Rectangle { + x: 10px; y: root.menu-padding + root.destructive-index * root.row-height + (root.divider-spacing - 1px) / 2; + width: parent.width - 20px; height: 1px; background: #e8edf4; + } for item[index] in root.items: Rectangle { - x: 4px; y: 4px + index * 30px; width: parent.width - 8px; height: 30px; + private property destructive: index == root.destructive-index; + private property has-icon: index < root.item-icons.length && root.item-icons[index] != ""; + private property foreground: !item.enabled ? #92979f : self.destructive ? #c43c43 : #30343b; + x: root.menu-padding; + y: root.menu-padding + index * root.row-height + + (root.destructive-index > 0 && index >= root.destructive-index ? root.divider-spacing : 0px); + width: parent.width - root.menu-padding * 2; height: root.row-height; border-radius: UiRadius.control; - background: item.enabled && index == root.current-index ? #edf4fc : transparent; + background: item.enabled && index == root.current-index ? (self.destructive ? #fff0f0 : #edf4fc) : transparent; accessible-role: button; accessible-label: item.label; accessible-enabled: item.enabled; accessible-action-default => { root.choose(index); } + if self.has-icon: Text { + x: root.compact-menu ? 8px : 10px; + width: root.compact-menu ? 12px : 14px; height: parent.height; + text: root.item-icons[index]; color: parent.foreground; + font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: root.compact-menu ? 10px : 11px; + horizontal-alignment: center; vertical-alignment: center; + } Text { - x: 10px; width: parent.width - 20px; height: parent.height; - text: item.label; color: item.enabled ? #30343b : #92979f; - font-size: 12px; vertical-alignment: center; + x: parent.has-icon ? (root.compact-menu ? 28px : 34px) : 10px; + width: parent.width - self.x - 10px; height: parent.height; + text: item.label; color: parent.foreground; + font-size: root.compact-menu ? 11px : 12px; vertical-alignment: center; } TouchArea { enabled: item.enabled; + mouse-cursor: pointer; changed has-hover => { if self.has-hover { root.current-index = index; } } clicked => { root.choose(index); } } diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint index e14bf60a..50ea09c2 100644 --- a/apps/desktop/src/gui/ui/main_window.slint +++ b/apps/desktop/src/gui/ui/main_window.slint @@ -1,7 +1,8 @@ import { UiRadius } from "ui_radius.slint"; import { WorkbenchStyle, WorkbenchButton, WorkbenchCard, WorkbenchNotice } from "workbench_controls.slint"; import { GalleryStrings } from "device_strings.slint"; -import { DeviceCardActions, DeviceCardStyle } from "device_card_actions.slint"; +import { DeviceCardStyle } from "device_card_actions.slint"; +import { GalleryMenuButton } from "gallery_menu_button.slint"; import { DeviceGroupDialog, DeviceGroupOption } from "device_group_dialog.slint"; import { GalleryView, GalleryTile, GalleryRecentDevice, GalleryActionDevice } from "gallery_view.slint"; import { GalleryNetworkDialog, GalleryNetworkDevice } from "gallery_network_dialog.slint"; @@ -986,7 +987,7 @@ export component MainWindow inherits Window { } } - // Recent connections: horizontal cards with thumbnail and hover actions. + // Recent connections: previews, presence badges, and separate primary/overflow actions. recent-panel := Rectangle { y: 260px; height: parent.height - self.y; @@ -1060,8 +1061,12 @@ export component MainWindow inherits Window { background: transparent; private property available: connection.online || connection.connected; private property content-width: self.width; - private property toolbar-width: DeviceCardStyle.toolbar-width; - private property toolbar-right-padding: 3px; + private property scroll-offset: recent-scroll.viewport-x; + private property device-id: connection.remote-id; + private property group-index: root.recent-group-index; + changed scroll-offset => { card-menu.close-menu(); } + changed device-id => { card-menu.close-menu(); } + changed group-index => { card-menu.close-menu(); } Rectangle { width: connection-card.content-width; @@ -1104,6 +1109,22 @@ export component MainWindow inherits Window { vertical-alignment: center; overflow: elide; } + Rectangle { + x: 8px; y: 8px; + width: presence-label.preferred-width + 24px; height: 20px; + background: #fffffff0; border-radius: self.height / 2; + Rectangle { + x: 8px; y: (parent.height - self.height) / 2; + width: 5px; height: 5px; border-radius: self.height / 2; + background: connection-card.available ? WorkbenchStyle.success : WorkbenchStyle.muted; + } + presence-label := Text { + x: 17px; width: self.preferred-width; height: parent.height; + text: connection-card.available ? UiStrings.online : UiStrings.offline; + color: connection-card.available ? WorkbenchStyle.success : WorkbenchStyle.muted; + font-size: 10px; font-weight: 500; vertical-alignment: center; + } + } } Rectangle { x: 0px; @@ -1111,25 +1132,20 @@ export component MainWindow inherits Window { width: parent.width; height: 44px; background: white; - Rectangle { x: 0px; y: 0px; width: parent.width; height: 1px; background: WorkbenchStyle.border; } + Rectangle { x: 0px; y: 0px; width: parent.width; height: 1px; background: WorkbenchStyle.separator; } Text { - x: 12px; y: 4px; height: 19px; - width: parent.width - 24px; + x: 12px; y: 5px; height: 19px; + width: parent.width - 56px; text: connection.display-name; color: WorkbenchStyle.text; font-size: 13px; font-weight: 600; overflow: elide; vertical-alignment: center; } Text { - x: 12px; y: 24px; height: 16px; width: parent.width - 92px; + x: 12px; y: 24px; height: 16px; width: parent.width - 56px; text: connection.remote-id; font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; } - presence-label := Text { - x: parent.width - self.preferred-width - 12px; y: 24px; height: 16px; - text: connection-card.available ? UiStrings.online : UiStrings.offline; - font-size: 10px; color: connection-card.available ? WorkbenchStyle.success : WorkbenchStyle.muted; - } } // The software renderer only clips rectangular bounds. // This outer border masks the four corners there too. @@ -1167,6 +1183,12 @@ export component MainWindow inherits Window { clicked => { root.copy-feedback = CopyFeedback.idle; root.recent-connect(connection.remote-id); } function show-details() { if !self.has-hover { return; } + // Leave the menu trigger and its popup free of device details. + if card-menu.menu-open || (self.mouse-x >= card-menu.x - 4px + && self.mouse-y <= card-menu.y + card-menu.height + 4px) { + root.recent-tooltip-visible = false; + return; + } root.recent-tooltip-name = connection.display-name; root.recent-tooltip-host = connection.host-name; root.recent-tooltip-id = connection.remote-id; @@ -1192,32 +1214,57 @@ export component MainWindow inherits Window { changed mouse-x => { self.show-details(); } changed mouse-y => { self.show-details(); } } - if card-touch.has-hover: DeviceCardActions { - x: parent.width - self.width - connection-card.toolbar-right-padding; - y: 52px; - remove-label: GalleryStrings.tr("删除记录", "Delete record", "Удалить запись"); - connected: connection.connected; - edit-alias => { + card-menu := GalleryMenuButton { + x: parent.width - self.width - 8px; y: 8px; + width: 24px; height: 24px; + compact-trigger: true; + compact-menu: true; + enabled: !root.gallery-mode; + text: GalleryStrings.tr("更多操作", "More actions", "Другие действия"); + menu-width: root.compact-language ? 116px : root.language-index == 2 ? 156px : 128px; + items: [ + {label: GalleryStrings.tr("修改名称", "Rename", "Переименовать"), enabled: true}, + {label: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы"), enabled: true}, + {label: GalleryStrings.tr("删除记录", "Delete record", "Удалить запись"), enabled: true}, + ]; + item-icons: [FontAwesomeIcons.pen, FontAwesomeIcons.tags, FontAwesomeIcons.trash-can]; + destructive-index: 2; + opened => { root.recent-tooltip-visible = false; root.copy-feedback = CopyFeedback.idle; } + selected(action) => { root.copy-feedback = CopyFeedback.idle; - root.alias-remote-id = connection.remote-id; - root.alias-input = connection.display-name; - root.alias-open = true; + if action == 0 { + root.alias-remote-id = connection.remote-id; + root.alias-input = connection.display-name; + root.alias-open = true; + } else if action == 1 { + root.open-device-groups(connection.remote-id, connection.display-name); + } else if action == 2 { + root.delete-remote-id = connection.remote-id; + root.delete-from-gallery = false; + root.delete-open = true; + } } - edit-groups => { root.copy-feedback = CopyFeedback.idle; root.open-device-groups(connection.remote-id, connection.display-name); } - remove-device => { + } + WorkbenchButton { + x: parent.width - self.width - 8px; y: 88px; + width: 28px; height: 28px; + soft: true; + primary: !connection.connected; + icon: connection.connected ? FontAwesomeIcons.xmark : FontAwesomeIcons.arrow-right-long; + tooltip: connection.connected ? GalleryStrings.tr("断开连接", "Disconnect", "Отключить") : UiStrings.connect; + tooltip-above: true; + clicked => { root.copy-feedback = CopyFeedback.idle; - root.delete-remote-id = connection.remote-id; - root.delete-from-gallery = false; - root.delete-open = true; + root.recent-tooltip-visible = false; + if connection.connected { root.recent-disconnect(connection.remote-id); } + else { root.recent-connect(connection.remote-id); } } - connect-device => { root.copy-feedback = CopyFeedback.idle; root.recent-connect(connection.remote-id); } - disconnect-device => { root.copy-feedback = CopyFeedback.idle; root.recent-disconnect(connection.remote-id); } } } Rectangle { width: parent.width; height: 124px; border-width: 1px; border-radius: UiRadius.surface; - border-color: card-touch.has-hover ? WorkbenchStyle.hover-border : WorkbenchStyle.border; + border-color: card-touch.has-hover || card-menu.menu-open ? WorkbenchStyle.hover-border : WorkbenchStyle.border; animate border-color { duration: 120ms; } } } diff --git a/apps/desktop/src/gui/ui/workbench_controls.slint b/apps/desktop/src/gui/ui/workbench_controls.slint index 7958ed5e..52acac65 100644 --- a/apps/desktop/src/gui/ui/workbench_controls.slint +++ b/apps/desktop/src/gui/ui/workbench_controls.slint @@ -20,9 +20,13 @@ export component WorkbenchButton inherits Rectangle { in property text; in property icon; in property tooltip; + in property tooltip-above: false; in property tooltip-suppressed: false; - in property icon-color: root.primary ? white : WorkbenchStyle.muted; + in property icon-color: root.primary + ? (root.soft && !touch.has-hover && !root.show-focus ? WorkbenchStyle.accent : white) + : WorkbenchStyle.muted; in property primary: false; + in property soft: false; in property quiet: false; in property enabled: true; out property has-focus: button-focus.has-focus; @@ -37,12 +41,16 @@ export component WorkbenchButton inherits Rectangle { width: 112px; height: 36px; border-radius: UiRadius.control; - border-width: root.show-focus ? 2px : 1px; + border-width: root.show-focus ? 2px : root.soft ? 0px : 1px; border-color: root.show-focus ? (root.primary ? #193da0 : WorkbenchStyle.accent) : root.primary ? WorkbenchStyle.accent : touch.has-hover ? WorkbenchStyle.hover-border : root.quiet ? transparent : WorkbenchStyle.control-border; - background: root.primary + background: root.soft + ? (root.primary + ? (touch.pressed ? #254cc0 : touch.has-hover || root.show-focus ? WorkbenchStyle.accent : #edf2ff) + : (touch.pressed ? WorkbenchStyle.pressed-surface : touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.field-surface)) + : root.primary ? (touch.pressed ? #254cc0 : touch.has-hover ? #4574f0 : WorkbenchStyle.accent) : (touch.pressed ? WorkbenchStyle.pressed-surface : touch.has-hover ? WorkbenchStyle.hover-surface : root.quiet ? transparent : white); opacity: root.enabled ? 1 : 0.45; @@ -54,15 +62,15 @@ export component WorkbenchButton inherits Rectangle { accessible-action-default => { if root.enabled { root.clicked(); } } HorizontalLayout { - padding-left: 10px; - padding-right: 10px; + padding-left: root.text == "" ? 0px : 10px; + padding-right: root.text == "" ? 0px : 10px; spacing: 8px; alignment: center; if root.icon != "": Text { text: root.icon; font-family: "Font Awesome 6 Free"; font-weight: 900; - font-size: 14px; + font-size: root.soft ? 12px : 14px; color: root.icon-color; vertical-alignment: center; accessible-role: none; @@ -113,7 +121,7 @@ export component WorkbenchButton inherits Rectangle { } if root.tooltip-active && root.tooltip-visible: Rectangle { x: min(0px, root.width - self.width); - y: root.height + 6px; + y: root.tooltip-above ? -self.height - 6px : root.height + 6px; width: min(260px, tip.preferred-width + 20px); height: max(28px, tip.preferred-height + 12px); background: #24324a;