mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-05 21:52:46 +08:00
[fix] improve file browser drag cursors and previews
This commit is contained in:
@@ -17,6 +17,10 @@ export struct BrowserTask {
|
|||||||
progress: float, active: bool, failed: bool, can-cancel: bool, receiving: bool,
|
progress: float, active: bool, failed: bool, can-cancel: bool, receiving: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
component BrowserButton inherits WorkbenchButton {
|
||||||
|
mouse-cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
component BrowserScrollView inherits Rectangle {
|
component BrowserScrollView inherits Rectangle {
|
||||||
// Fluent overlays a 14px scrollbar on its viewport. Keep a 4px gap between
|
// Fluent overlays a 14px scrollbar on its viewport. Keep a 4px gap between
|
||||||
// that track and row content, including while the scrollbar expands on hover.
|
// that track and row content, including while the scrollbar expands on hover.
|
||||||
@@ -92,7 +96,7 @@ component BrowserCheckBox inherits Rectangle {
|
|||||||
reject
|
reject
|
||||||
}
|
}
|
||||||
touch := TouchArea {
|
touch := TouchArea {
|
||||||
enabled: root.enabled; mouse-cursor: pointer;
|
enabled: root.enabled; mouse-cursor: default;
|
||||||
pointer-event(event) => {
|
pointer-event(event) => {
|
||||||
if event.kind == PointerEventKind.down { root.keyboard-focus = false; }
|
if event.kind == PointerEventKind.down { root.keyboard-focus = false; }
|
||||||
}
|
}
|
||||||
@@ -149,7 +153,7 @@ component BrowserSortHeader inherits FlatSurface {
|
|||||||
reject
|
reject
|
||||||
}
|
}
|
||||||
touch := TouchArea {
|
touch := TouchArea {
|
||||||
enabled: root.enabled; mouse-cursor: pointer;
|
enabled: root.enabled; mouse-cursor: default;
|
||||||
pointer-event(event) => { if event.kind == PointerEventKind.down { root.keyboard-focus = false; } }
|
pointer-event(event) => { if event.kind == PointerEventKind.down { root.keyboard-focus = false; } }
|
||||||
clicked => { focus.focus(); root.keyboard-focus = false; root.clicked(); }
|
clicked => { focus.focus(); root.keyboard-focus = false; root.clicked(); }
|
||||||
}
|
}
|
||||||
@@ -177,6 +181,7 @@ component BrowserPane inherits WorkbenchCard {
|
|||||||
in property <int> selection-revision;
|
in property <int> selection-revision;
|
||||||
in property <int> drag-count;
|
in property <int> drag-count;
|
||||||
in property <bool> dragging;
|
in property <bool> dragging;
|
||||||
|
in property <bool> drop-hover;
|
||||||
in property <[BrowserEntry]> entries;
|
in property <[BrowserEntry]> entries;
|
||||||
in property <[BrowserTreeEntry]> tree;
|
in property <[BrowserTreeEntry]> tree;
|
||||||
in property <int> sort-column;
|
in property <int> sort-column;
|
||||||
@@ -196,9 +201,15 @@ component BrowserPane inherits WorkbenchCard {
|
|||||||
callback toggle-hidden;
|
callback toggle-hidden;
|
||||||
callback sort(int);
|
callback sort(int);
|
||||||
pure callback drag-data(string, int) -> data-transfer;
|
pure callback drag-data(string, int) -> data-transfer;
|
||||||
callback drag-state(data-transfer, bool);
|
callback drag-start(data-transfer, BrowserEntry);
|
||||||
callback can-drop(data-transfer) -> bool;
|
callback drag-move(length, length);
|
||||||
callback dropped(data-transfer) -> bool;
|
callback drag-end;
|
||||||
|
callback drag-cancel;
|
||||||
|
public pure function contains-drop(x: length, y: length) -> bool {
|
||||||
|
return root.enabled && root.transfer-ready
|
||||||
|
&& x >= body.absolute-position.x && x < body.absolute-position.x + body.width
|
||||||
|
&& y >= body.absolute-position.y && y < body.absolute-position.y + body.height;
|
||||||
|
}
|
||||||
changed enabled => { if !root.enabled { directory-popup.close(); } }
|
changed enabled => { if !root.enabled { directory-popup.close(); } }
|
||||||
changed dragging => { if root.dragging { directory-popup.close(); } }
|
changed dragging => { if root.dragging { directory-popup.close(); } }
|
||||||
changed current-path => { file-list.reset-scroll(); }
|
changed current-path => { file-list.reset-scroll(); }
|
||||||
@@ -229,7 +240,7 @@ component BrowserPane inherits WorkbenchCard {
|
|||||||
}
|
}
|
||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
height: 32px; spacing: 6px;
|
height: 32px; spacing: 6px;
|
||||||
WorkbenchButton {
|
BrowserButton {
|
||||||
width: 32px; height: 32px; quiet: true; icon: FontAwesomeIcons.arrow-up;
|
width: 32px; height: 32px; quiet: true; icon: FontAwesomeIcons.arrow-up;
|
||||||
tooltip: root.chinese ? "打开上一级目录" : "Open parent folder";
|
tooltip: root.chinese ? "打开上一级目录" : "Open parent folder";
|
||||||
tooltip-above: true; tooltip-align-left: true;
|
tooltip-above: true; tooltip-align-left: true;
|
||||||
@@ -256,7 +267,7 @@ component BrowserPane inherits WorkbenchCard {
|
|||||||
accepted => { root.navigate(self.text); self.clear-focus(); }
|
accepted => { root.navigate(self.text); self.clear-focus(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tree-trigger := WorkbenchButton {
|
tree-trigger := BrowserButton {
|
||||||
width: 28px; height: 32px; quiet: true; icon: FontAwesomeIcons.folder-open;
|
width: 28px; height: 32px; quiet: true; icon: FontAwesomeIcons.folder-open;
|
||||||
enabled: root.enabled && !root.dragging;
|
enabled: root.enabled && !root.dragging;
|
||||||
accessible-label: root.chinese ? "浏览目录树" : "Browse folder tree";
|
accessible-label: root.chinese ? "浏览目录树" : "Browse folder tree";
|
||||||
@@ -265,10 +276,8 @@ component BrowserPane inherits WorkbenchCard {
|
|||||||
}
|
}
|
||||||
body := Rectangle {
|
body := Rectangle {
|
||||||
vertical-stretch: 1;
|
vertical-stretch: 1;
|
||||||
drop-zone := DropArea {
|
Rectangle {
|
||||||
enabled: root.transfer-ready;
|
width: parent.width; height: parent.height;
|
||||||
can-drop(event) => { return root.can-drop(event.data) ? DragAction.copy : DragAction.none; }
|
|
||||||
dropped(event) => { return root.dropped(event.data) ? DragAction.copy : DragAction.none; }
|
|
||||||
VerticalLayout {
|
VerticalLayout {
|
||||||
spacing: 0px;
|
spacing: 0px;
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -319,7 +328,7 @@ component BrowserPane inherits WorkbenchCard {
|
|||||||
background: entry.selected
|
background: entry.selected
|
||||||
? (row-touch.pressed ? ControlStyle.selected-pressed.with-alpha(0.4) : row-touch.has-hover ? ControlStyle.selected-hover.with-alpha(0.35) : ControlStyle.selected.with-alpha(0.3))
|
? (row-touch.pressed ? ControlStyle.selected-pressed.with-alpha(0.4) : row-touch.has-hover ? ControlStyle.selected-hover.with-alpha(0.35) : ControlStyle.selected.with-alpha(0.3))
|
||||||
: row-touch.pressed ? ControlStyle.pressed.with-alpha(0.3) : row-touch.has-hover ? ControlStyle.hover.with-alpha(0.2) : transparent;
|
: row-touch.pressed ? ControlStyle.pressed.with-alpha(0.3) : row-touch.has-hover ? ControlStyle.hover.with-alpha(0.2) : transparent;
|
||||||
opacity: drag-source.dragging ? 0.55 : 1;
|
opacity: root.dragging && row-touch.drag-started ? 0.55 : 1;
|
||||||
folder-menu := ContextMenuArea {
|
folder-menu := ContextMenuArea {
|
||||||
enabled: entry.directory && root.enabled && !root.busy && root.error == "" && !root.dragging;
|
enabled: entry.directory && root.enabled && !root.busy && root.error == "" && !root.dragging;
|
||||||
Menu {
|
Menu {
|
||||||
@@ -336,25 +345,46 @@ component BrowserPane inherits WorkbenchCard {
|
|||||||
Text { width: root.modified-column-width; text: entry.modified != "" ? entry.modified : "—"; color: WorkbenchStyle.muted; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; horizontal-alignment: right; }
|
Text { width: root.modified-column-width; text: entry.modified != "" ? entry.modified : "—"; color: WorkbenchStyle.muted; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; horizontal-alignment: right; }
|
||||||
Text { width: root.size-column-width; text: entry.directory ? "—" : entry.size; color: WorkbenchStyle.muted; font-size: ImGuiFontStyle.body; vertical-alignment: center; horizontal-alignment: right; }
|
Text { width: root.size-column-width; text: entry.directory ? "—" : entry.size; color: WorkbenchStyle.muted; font-size: ImGuiFontStyle.body; vertical-alignment: center; horizontal-alignment: right; }
|
||||||
}
|
}
|
||||||
drag-source := DragArea {
|
row-touch := TouchArea {
|
||||||
// Only the row body starts drags; checkboxes remain selection controls.
|
// Only the row body starts drags; checkboxes remain selection controls.
|
||||||
x: 34px; width: parent.width - 34px; height: parent.height;
|
x: 34px; width: parent.width - 34px; height: parent.height;
|
||||||
enabled: root.transfer-ready && !entry.link;
|
enabled: root.enabled && !root.busy && root.error == "";
|
||||||
allow-copy: true;
|
private property <bool> drag-armed;
|
||||||
data: root.drag-data(entry.path, root.selection-revision);
|
out property <bool> drag-started;
|
||||||
changed dragging => { if self.dragging { root.drag-state(self.data, true); } }
|
// Keep pointer capture throughout the in-window drag, so the cursor
|
||||||
drag-finished(action) => { root.drag-state(self.data, false); }
|
// remains a closed hand instead of DragArea's copy/no-drop cursor.
|
||||||
row-touch := TouchArea {
|
mouse-cursor: self.drag-started && root.dragging ? MouseCursor.grabbing : MouseCursor.default;
|
||||||
enabled: root.enabled && !root.busy && root.error == "";
|
function update-drag-position() {
|
||||||
mouse-cursor: entry.link ? MouseCursor.default : MouseCursor.grab;
|
root.drag-move(self.absolute-position.x + self.mouse-x,
|
||||||
pointer-event(event) => {
|
self.absolute-position.y + self.mouse-y);
|
||||||
if event.button == PointerEventButton.right && event.kind == PointerEventKind.up && folder-menu.enabled {
|
|
||||||
folder-menu.show({ x: self.mouse-x + 34px, y: self.mouse-y });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clicked => { root.select-entry(index); }
|
|
||||||
double-clicked => { root.open-entry(index); }
|
|
||||||
}
|
}
|
||||||
|
pointer-event(event) => {
|
||||||
|
if event.button == PointerEventButton.left && event.kind == PointerEventKind.down {
|
||||||
|
self.drag-armed = root.transfer-ready && !entry.link;
|
||||||
|
self.drag-started = false;
|
||||||
|
} else if event.kind == PointerEventKind.cancel {
|
||||||
|
self.drag-armed = false;
|
||||||
|
if self.drag-started && root.dragging { root.drag-cancel(); }
|
||||||
|
} else if event.button == PointerEventButton.left && event.kind == PointerEventKind.up {
|
||||||
|
self.drag-armed = false;
|
||||||
|
if self.drag-started && root.dragging {
|
||||||
|
self.update-drag-position();
|
||||||
|
root.drag-end();
|
||||||
|
}
|
||||||
|
} else if event.button == PointerEventButton.right && event.kind == PointerEventKind.up && folder-menu.enabled {
|
||||||
|
folder-menu.show({ x: self.mouse-x + 34px, y: self.mouse-y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
moved => {
|
||||||
|
if self.drag-armed && !self.drag-started && root.transfer-ready
|
||||||
|
&& (abs(self.mouse-x - self.pressed-x) >= 5px || abs(self.mouse-y - self.pressed-y) >= 5px) {
|
||||||
|
self.drag-started = true;
|
||||||
|
root.drag-start(root.drag-data(entry.path, root.selection-revision), entry);
|
||||||
|
}
|
||||||
|
if self.drag-started && root.dragging { self.update-drag-position(); }
|
||||||
|
}
|
||||||
|
clicked => { if !self.drag-started { root.select-entry(index); } }
|
||||||
|
double-clicked => { if !self.drag-started { root.open-entry(index); } }
|
||||||
}
|
}
|
||||||
BrowserCheckBox { x: 6px; y: (parent.height - self.height) / 2; width: 24px; checked: entry.selected; label: (root.chinese ? "选择 " : "Select ") + entry.name; enabled: !entry.link && root.enabled && !root.busy && root.error == "" && !root.dragging; toggled => { root.select-entry(index); } }
|
BrowserCheckBox { x: 6px; y: (parent.height - self.height) / 2; width: 24px; checked: entry.selected; label: (root.chinese ? "选择 " : "Select ") + entry.name; enabled: !entry.link && root.enabled && !root.busy && root.error == "" && !root.dragging; toggled => { root.select-entry(index); } }
|
||||||
}
|
}
|
||||||
@@ -373,7 +403,7 @@ component BrowserPane inherits WorkbenchCard {
|
|||||||
}
|
}
|
||||||
if root.filter-text != "": HorizontalLayout {
|
if root.filter-text != "": HorizontalLayout {
|
||||||
alignment: center;
|
alignment: center;
|
||||||
WorkbenchButton { width: 112px; height: 28px; text-size: ImGuiFontStyle.base; text: root.chinese ? "清空筛选" : "Clear filter"; clicked => { root.filter-text = ""; root.filter(""); } }
|
BrowserButton { width: 112px; height: 28px; text-size: ImGuiFontStyle.base; text: root.chinese ? "清空筛选" : "Clear filter"; clicked => { root.filter-text = ""; root.filter(""); } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if root.error != "" && !root.busy: Rectangle {
|
if root.error != "" && !root.busy: Rectangle {
|
||||||
@@ -385,7 +415,7 @@ component BrowserPane inherits WorkbenchCard {
|
|||||||
Text { text: root.error; color: WorkbenchStyle.muted; font-size: ImGuiFontStyle.body; wrap: word-wrap; horizontal-alignment: center; }
|
Text { text: root.error; color: WorkbenchStyle.muted; font-size: ImGuiFontStyle.body; wrap: word-wrap; horizontal-alignment: center; }
|
||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
alignment: center;
|
alignment: center;
|
||||||
WorkbenchButton { width: 128px; height: 28px; text-size: ImGuiFontStyle.base; text: root.chinese ? "打开主目录" : "Open home folder"; clicked => { root.navigate(""); } }
|
BrowserButton { width: 128px; height: 28px; text-size: ImGuiFontStyle.base; text: root.chinese ? "打开主目录" : "Open home folder"; clicked => { root.navigate(""); } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -393,7 +423,7 @@ component BrowserPane inherits WorkbenchCard {
|
|||||||
width: parent.width; height: parent.height; background: WorkbenchStyle.surface.with-alpha(0.9);
|
width: parent.width; height: parent.height; background: WorkbenchStyle.surface.with-alpha(0.9);
|
||||||
VerticalLayout { alignment: center; spacing: 12px; Spinner { width: 24px; height: 24px; } Text { text: root.status; horizontal-alignment: center; color: WorkbenchStyle.muted; font-size: ImGuiFontStyle.base; } }
|
VerticalLayout { alignment: center; spacing: 12px; Spinner { width: 24px; height: 24px; } Text { text: root.status; horizontal-alignment: center; color: WorkbenchStyle.muted; font-size: ImGuiFontStyle.base; } }
|
||||||
}
|
}
|
||||||
if drop-zone.has-drag: FlatSurface {
|
if root.drop-hover: FlatSurface {
|
||||||
width: parent.width; height: parent.height;
|
width: parent.width; height: parent.height;
|
||||||
background: ControlStyle.selected.with-alpha(0.3); outline-color: UiSurface.focus; border-radius: UiRadius.control;
|
background: ControlStyle.selected.with-alpha(0.3); outline-color: UiSurface.focus; border-radius: UiRadius.control;
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -426,8 +456,8 @@ component BrowserPane inherits WorkbenchCard {
|
|||||||
padding: 12px; spacing: 10px;
|
padding: 12px; spacing: 10px;
|
||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
height: 28px; spacing: 8px;
|
height: 28px; spacing: 8px;
|
||||||
WorkbenchButton { text: root.chinese ? "主目录" : "Home"; quiet: true; height: 28px; clicked => { root.navigate(""); directory-popup.close(); } }
|
BrowserButton { text: root.chinese ? "主目录" : "Home"; quiet: true; height: 28px; clicked => { root.navigate(""); directory-popup.close(); } }
|
||||||
WorkbenchButton { text: root.chinese ? "磁盘" : "Drives"; quiet: true; height: 28px; clicked => { root.navigate("@roots"); directory-popup.close(); } }
|
BrowserButton { text: root.chinese ? "磁盘" : "Drives"; quiet: true; height: 28px; clicked => { root.navigate("@roots"); directory-popup.close(); } }
|
||||||
}
|
}
|
||||||
tree-list := Rectangle {
|
tree-list := Rectangle {
|
||||||
out property <length> content-gutter: 18px;
|
out property <length> content-gutter: 18px;
|
||||||
@@ -443,7 +473,7 @@ component BrowserPane inherits WorkbenchCard {
|
|||||||
background: node.selected ? ControlStyle.selected.with-alpha(0.3) : tree-touch.has-hover ? ControlStyle.hover.with-alpha(0.2) : transparent;
|
background: node.selected ? ControlStyle.selected.with-alpha(0.3) : tree-touch.has-hover ? ControlStyle.hover.with-alpha(0.2) : transparent;
|
||||||
Text { x: 24px + min(node.depth, 8) * 12px; width: parent.width - self.x - 4px; height: parent.height; text: node.name; color: node.selected ? WorkbenchStyle.accent : WorkbenchStyle.text; font-size: ImGuiFontStyle.base; vertical-alignment: center; overflow: elide; }
|
Text { x: 24px + min(node.depth, 8) * 12px; width: parent.width - self.x - 4px; height: parent.height; text: node.name; color: node.selected ? WorkbenchStyle.accent : WorkbenchStyle.text; font-size: ImGuiFontStyle.base; vertical-alignment: center; overflow: elide; }
|
||||||
tree-touch := TouchArea { clicked => { root.tree-open(index); directory-popup.close(); } }
|
tree-touch := TouchArea { clicked => { root.tree-open(index); directory-popup.close(); } }
|
||||||
WorkbenchButton { x: min(node.depth, 8) * 12px; width: 22px; height: 28px; quiet: true; icon: node.expanded ? FontAwesomeIcons.angle-down : FontAwesomeIcons.angle-right; clicked => { root.tree-toggle(index); } }
|
BrowserButton { x: min(node.depth, 8) * 12px; width: 22px; height: 28px; quiet: true; icon: node.expanded ? FontAwesomeIcons.angle-down : FontAwesomeIcons.angle-right; clicked => { root.tree-toggle(index); } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,6 +538,12 @@ export component FileBrowserWindow inherits Window {
|
|||||||
in property <int> selection-revision;
|
in property <int> selection-revision;
|
||||||
in property <bool> drag-active;
|
in property <bool> drag-active;
|
||||||
in property <int> drag-count;
|
in property <int> drag-count;
|
||||||
|
private property <data-transfer> drag-payload;
|
||||||
|
private property <BrowserEntry> drag-entry;
|
||||||
|
private property <length> drag-x;
|
||||||
|
private property <length> drag-y;
|
||||||
|
private property <int> drop-target: -1;
|
||||||
|
changed drag-active => { if !root.drag-active { root.drop-target = -1; } }
|
||||||
in-out property <bool> tasks-expanded: false;
|
in-out property <bool> tasks-expanded: false;
|
||||||
private property <length> task-list-max-height: min(144px, max(72px, root.height - 440px));
|
private property <length> task-list-max-height: min(144px, max(72px, root.height - 440px));
|
||||||
private property <int> selected-count: root.local-selected + root.remote-selected;
|
private property <int> selected-count: root.local-selected + root.remote-selected;
|
||||||
@@ -540,6 +576,27 @@ export component FileBrowserWindow inherits Window {
|
|||||||
callback can-drop(bool, data-transfer) -> bool;
|
callback can-drop(bool, data-transfer) -> bool;
|
||||||
callback dropped(bool, data-transfer) -> bool;
|
callback dropped(bool, data-transfer) -> bool;
|
||||||
callback cancel-drag;
|
callback cancel-drag;
|
||||||
|
function start-drag(data: data-transfer, entry: BrowserEntry) {
|
||||||
|
root.drag-payload = data;
|
||||||
|
root.drag-entry = entry;
|
||||||
|
root.drop-target = -1;
|
||||||
|
root.drag-state(data, true);
|
||||||
|
}
|
||||||
|
function move-drag(x: length, y: length) {
|
||||||
|
if !root.drag-active { return; }
|
||||||
|
root.drag-x = x;
|
||||||
|
root.drag-y = y;
|
||||||
|
root.drop-target = local-pane.contains-drop(x, y) && root.can-drop(false, root.drag-payload) ? 0
|
||||||
|
: remote-pane.contains-drop(x, y) && root.can-drop(true, root.drag-payload) ? 1 : -1;
|
||||||
|
}
|
||||||
|
function finish-drag() {
|
||||||
|
if !root.drag-active { return; }
|
||||||
|
// The payload stays fixed from press-and-drag through release; C++ validates
|
||||||
|
// its source, destination, session and generation again before copying.
|
||||||
|
let data = root.drag-payload;
|
||||||
|
if root.drop-target >= 0 { root.dropped(root.drop-target == 1, data); }
|
||||||
|
root.drag-state(data, false);
|
||||||
|
}
|
||||||
forward-focus: focus;
|
forward-focus: focus;
|
||||||
SurfaceSeparator { x: 0px; y: 0px; width: parent.width; }
|
SurfaceSeparator { x: 0px; y: 0px; width: parent.width; }
|
||||||
focus := FocusScope {
|
focus := FocusScope {
|
||||||
@@ -557,7 +614,7 @@ export component FileBrowserWindow inherits Window {
|
|||||||
Text { text: root.peer-name; color: WorkbenchStyle.muted; vertical-alignment: center; font-size: ImGuiFontStyle.base; overflow: elide; }
|
Text { text: root.peer-name; color: WorkbenchStyle.muted; vertical-alignment: center; font-size: ImGuiFontStyle.base; overflow: elide; }
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: root.chinese ? 104px : 144px;
|
width: root.chinese ? 104px : 144px;
|
||||||
WorkbenchButton {
|
BrowserButton {
|
||||||
y: (parent.height - self.height) / 2;
|
y: (parent.height - self.height) / 2;
|
||||||
width: parent.width; height: 28px; icon: FontAwesomeIcons.rotate;
|
width: parent.width; height: 28px; icon: FontAwesomeIcons.rotate;
|
||||||
text: root.chinese ? "刷新目录" : "Refresh folders";
|
text: root.chinese ? "刷新目录" : "Refresh folders";
|
||||||
@@ -570,7 +627,7 @@ export component FileBrowserWindow inherits Window {
|
|||||||
}
|
}
|
||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
vertical-stretch: 1; spacing: 12px;
|
vertical-stretch: 1; spacing: 12px;
|
||||||
BrowserPane {
|
local-pane := BrowserPane {
|
||||||
width: (parent.width - 12px) / 2;
|
width: (parent.width - 12px) / 2;
|
||||||
horizontal-stretch: 1; chinese: root.chinese; heading: root.chinese ? "本机" : "Local"; subtitle: root.local-role;
|
horizontal-stretch: 1; chinese: root.chinese; heading: root.chinese ? "本机" : "Local"; subtitle: root.local-role;
|
||||||
path <=> root.local-path; status: root.local-status; error: root.local-error; busy: root.local-busy;
|
path <=> root.local-path; status: root.local-status; error: root.local-error; busy: root.local-busy;
|
||||||
@@ -583,16 +640,18 @@ export component FileBrowserWindow inherits Window {
|
|||||||
sort(column) => { root.sort(false, column); }
|
sort(column) => { root.sort(false, column); }
|
||||||
selected-count: root.local-selected; destination: root.remote-selected > 0;
|
selected-count: root.local-selected; destination: root.remote-selected > 0;
|
||||||
transfer-ready: root.transfer-ready; selection-revision: root.selection-revision; dragging: root.drag-active; drag-count: root.drag-count;
|
transfer-ready: root.transfer-ready; selection-revision: root.selection-revision; dragging: root.drag-active; drag-count: root.drag-count;
|
||||||
|
drop-hover: root.drag-active && root.drop-target == 0;
|
||||||
navigate(path) => { root.navigate(false, path); } up => { root.up(false); }
|
navigate(path) => { root.navigate(false, path); } up => { root.up(false); }
|
||||||
select-entry(index) => { root.select-entry(false, index); } open-entry(index) => { root.open-entry(false, index); }
|
select-entry(index) => { root.select-entry(false, index); } open-entry(index) => { root.open-entry(false, index); }
|
||||||
select-all => { root.select-all(false); } tree-open(index) => { root.tree-open(false, index); } tree-toggle(index) => { root.tree-toggle(false, index); }
|
select-all => { root.select-all(false); } tree-open(index) => { root.tree-open(false, index); } tree-toggle(index) => { root.tree-toggle(false, index); }
|
||||||
filter(text) => { root.filter(false, text); }
|
filter(text) => { root.filter(false, text); }
|
||||||
drag-data(path, revision) => { return root.drag-data(false, path, revision); }
|
drag-data(path, revision) => { return root.drag-data(false, path, revision); }
|
||||||
drag-state(data, active) => { root.drag-state(data, active); }
|
drag-start(data, entry) => { root.start-drag(data, entry); }
|
||||||
can-drop(data) => { return root.can-drop(false, data); }
|
drag-move(x, y) => { root.move-drag(x, y); }
|
||||||
dropped(data) => { return root.dropped(false, data); }
|
drag-end => { root.finish-drag(); }
|
||||||
|
drag-cancel => { root.cancel-drag(); }
|
||||||
}
|
}
|
||||||
BrowserPane {
|
remote-pane := BrowserPane {
|
||||||
width: (parent.width - 12px) / 2;
|
width: (parent.width - 12px) / 2;
|
||||||
horizontal-stretch: 1; chinese: root.chinese; heading: root.chinese ? "远端" : "Remote"; subtitle: root.remote-role; enabled: root.connected;
|
horizontal-stretch: 1; chinese: root.chinese; heading: root.chinese ? "远端" : "Remote"; subtitle: root.remote-role; enabled: root.connected;
|
||||||
path <=> root.remote-path; status: root.remote-status; error: root.remote-error; busy: root.remote-busy;
|
path <=> root.remote-path; status: root.remote-status; error: root.remote-error; busy: root.remote-busy;
|
||||||
@@ -605,14 +664,16 @@ export component FileBrowserWindow inherits Window {
|
|||||||
sort(column) => { root.sort(true, column); }
|
sort(column) => { root.sort(true, column); }
|
||||||
selected-count: root.remote-selected; destination: root.local-selected > 0;
|
selected-count: root.remote-selected; destination: root.local-selected > 0;
|
||||||
transfer-ready: root.transfer-ready; selection-revision: root.selection-revision; dragging: root.drag-active; drag-count: root.drag-count;
|
transfer-ready: root.transfer-ready; selection-revision: root.selection-revision; dragging: root.drag-active; drag-count: root.drag-count;
|
||||||
|
drop-hover: root.drag-active && root.drop-target == 1;
|
||||||
navigate(path) => { root.navigate(true, path); } up => { root.up(true); }
|
navigate(path) => { root.navigate(true, path); } up => { root.up(true); }
|
||||||
select-entry(index) => { root.select-entry(true, index); } open-entry(index) => { root.open-entry(true, index); }
|
select-entry(index) => { root.select-entry(true, index); } open-entry(index) => { root.open-entry(true, index); }
|
||||||
select-all => { root.select-all(true); } tree-open(index) => { root.tree-open(true, index); } tree-toggle(index) => { root.tree-toggle(true, index); }
|
select-all => { root.select-all(true); } tree-open(index) => { root.tree-open(true, index); } tree-toggle(index) => { root.tree-toggle(true, index); }
|
||||||
filter(text) => { root.filter(true, text); }
|
filter(text) => { root.filter(true, text); }
|
||||||
drag-data(path, revision) => { return root.drag-data(true, path, revision); }
|
drag-data(path, revision) => { return root.drag-data(true, path, revision); }
|
||||||
drag-state(data, active) => { root.drag-state(data, active); }
|
drag-start(data, entry) => { root.start-drag(data, entry); }
|
||||||
can-drop(data) => { return root.can-drop(true, data); }
|
drag-move(x, y) => { root.move-drag(x, y); }
|
||||||
dropped(data) => { return root.dropped(true, data); }
|
drag-end => { root.finish-drag(); }
|
||||||
|
drag-cancel => { root.cancel-drag(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkbenchCard {
|
WorkbenchCard {
|
||||||
@@ -643,7 +704,7 @@ export component FileBrowserWindow inherits Window {
|
|||||||
}
|
}
|
||||||
if root.selected-count > 0: Rectangle {
|
if root.selected-count > 0: Rectangle {
|
||||||
width: root.chinese ? 76px : 100px;
|
width: root.chinese ? 76px : 100px;
|
||||||
WorkbenchButton {
|
BrowserButton {
|
||||||
y: (parent.height - self.height) / 2; width: parent.width; height: 28px; quiet: true;
|
y: (parent.height - self.height) / 2; width: parent.width; height: 28px; quiet: true;
|
||||||
text-size: ImGuiFontStyle.base; text: root.chinese ? "取消选择" : "Clear selection";
|
text-size: ImGuiFontStyle.base; text: root.chinese ? "取消选择" : "Clear selection";
|
||||||
enabled: !root.drag-active; clicked => { root.clear-selection(); }
|
enabled: !root.drag-active; clicked => { root.clear-selection(); }
|
||||||
@@ -656,7 +717,7 @@ export component FileBrowserWindow inherits Window {
|
|||||||
text: root.source-remote ? root.send-local-text : root.send-remote-text;
|
text: root.source-remote ? root.send-local-text : root.send-remote-text;
|
||||||
font-size: ImGuiFontStyle.base; font-weight: 600;
|
font-size: ImGuiFontStyle.base; font-weight: 600;
|
||||||
}
|
}
|
||||||
WorkbenchButton {
|
BrowserButton {
|
||||||
y: (parent.height - self.height) / 2; width: parent.width; height: 32px; primary: true;
|
y: (parent.height - self.height) / 2; width: parent.width; height: 32px; primary: true;
|
||||||
text-size: ImGuiFontStyle.base;
|
text-size: ImGuiFontStyle.base;
|
||||||
enabled: !root.drag-active && (root.source-remote ? root.can-download : root.can-upload);
|
enabled: !root.drag-active && (root.source-remote ? root.can-download : root.can-upload);
|
||||||
@@ -672,7 +733,7 @@ export component FileBrowserWindow inherits Window {
|
|||||||
padding-left: 10px; padding-right: 10px; spacing: 8px;
|
padding-left: 10px; padding-right: 10px; spacing: 8px;
|
||||||
Rectangle {
|
Rectangle {
|
||||||
horizontal-stretch: 1;
|
horizontal-stretch: 1;
|
||||||
WorkbenchButton { x: 0px; width: min(parent.width, task-label.preferred-width + 46px); height: 32px; quiet: true; accessible-label: task-label.text; clicked => { root.tasks-expanded = !root.tasks-expanded; } }
|
BrowserButton { x: 0px; width: min(parent.width, task-label.preferred-width + 46px); height: 32px; quiet: true; accessible-label: task-label.text; clicked => { root.tasks-expanded = !root.tasks-expanded; } }
|
||||||
Text { x: 8px; width: 14px; height: parent.height; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: ImGuiFontStyle.base; color: ControlStyle.icon; vertical-alignment: center; text: root.tasks-expanded ? FontAwesomeIcons.angle-down : FontAwesomeIcons.angle-right; }
|
Text { x: 8px; width: 14px; height: parent.height; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: ImGuiFontStyle.base; color: ControlStyle.icon; vertical-alignment: center; text: root.tasks-expanded ? FontAwesomeIcons.angle-down : FontAwesomeIcons.angle-right; }
|
||||||
task-label := Text { x: 30px; width: parent.width - 40px; height: parent.height; font-size: ImGuiFontStyle.base; color: WorkbenchStyle.muted; vertical-alignment: center; overflow: elide; text: (root.chinese ? "传输任务" : "Transfers") + " · " + (root.tasks.length == 0 ? (root.chinese ? "暂无任务" : "No tasks") : root.task-summary != "" ? root.task-summary : root.tasks.length); }
|
task-label := Text { x: 30px; width: parent.width - 40px; height: parent.height; font-size: ImGuiFontStyle.base; color: WorkbenchStyle.muted; vertical-alignment: center; overflow: elide; text: (root.chinese ? "传输任务" : "Transfers") + " · " + (root.tasks.length == 0 ? (root.chinese ? "暂无任务" : "No tasks") : root.task-summary != "" ? root.task-summary : root.tasks.length); }
|
||||||
}
|
}
|
||||||
@@ -682,7 +743,7 @@ export component FileBrowserWindow inherits Window {
|
|||||||
visible: false; text: root.clear-completed-text;
|
visible: false; text: root.clear-completed-text;
|
||||||
font-size: ImGuiFontStyle.body; font-weight: 500;
|
font-size: ImGuiFontStyle.body; font-weight: 500;
|
||||||
}
|
}
|
||||||
WorkbenchButton { y: (parent.height - self.height) / 2; width: parent.width; height: 28px; quiet: true; text-size: ImGuiFontStyle.body; text: root.clear-completed-text; clicked => { root.clear-finished(); } }
|
BrowserButton { y: (parent.height - self.height) / 2; width: parent.width; height: 28px; quiet: true; text-size: ImGuiFontStyle.body; text: root.clear-completed-text; clicked => { root.clear-finished(); } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -722,7 +783,7 @@ export component FileBrowserWindow inherits Window {
|
|||||||
}
|
}
|
||||||
Rectangle { width: root.width < 820px ? 80px : 110px; Rectangle { x: 0px; y: (parent.height - 4px) / 2; width: parent.width; height: 4px; background: WorkbenchStyle.separator; border-radius: 2px; Rectangle { x: 0px; y: 0px; width: parent.width * task.progress; height: 4px; border-radius: 2px; background: task.failed ? UiSurface.error : WorkbenchStyle.accent; } } }
|
Rectangle { width: root.width < 820px ? 80px : 110px; Rectangle { x: 0px; y: (parent.height - 4px) / 2; width: parent.width; height: 4px; background: WorkbenchStyle.separator; border-radius: 2px; Rectangle { x: 0px; y: 0px; width: parent.width * task.progress; height: 4px; border-radius: 2px; background: task.failed ? UiSurface.error : WorkbenchStyle.accent; } } }
|
||||||
Text { width: root.width < 820px ? 92px : 110px; text: task.status; color: task.failed ? UiSurface.error : WorkbenchStyle.muted; font-size: ImGuiFontStyle.body; vertical-alignment: center; horizontal-alignment: right; }
|
Text { width: root.width < 820px ? 92px : 110px; text: task.status; color: task.failed ? UiSurface.error : WorkbenchStyle.muted; font-size: ImGuiFontStyle.body; vertical-alignment: center; horizontal-alignment: right; }
|
||||||
Rectangle { width: 56px; if task.can-cancel: WorkbenchButton { y: (parent.height - self.height) / 2; width: 56px; height: 26px; text: root.chinese ? "取消" : "Cancel"; text-size: ImGuiFontStyle.body; quiet: true; clicked => { root.cancel-task(task.id); } } }
|
Rectangle { width: 56px; if task.can-cancel: BrowserButton { y: (parent.height - self.height) / 2; width: 56px; height: 26px; text: root.chinese ? "取消" : "Cancel"; text-size: ImGuiFontStyle.body; quiet: true; clicked => { root.cancel-task(task.id); } } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -732,4 +793,44 @@ export component FileBrowserWindow inherits Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Visual only: pointer capture and drop validation stay with the source row.
|
||||||
|
if root.drag-active: FlatSurface {
|
||||||
|
private property <bool> label-on-left: root.drag-x - 12px + self.width > root.width - 8px;
|
||||||
|
z: 100;
|
||||||
|
width: min(300px, max(112px, drag-label.preferred-width + 54px + drag-badge.width));
|
||||||
|
height: 40px;
|
||||||
|
// Keep the icon's top-left corner at the pointer when the label flips at the right edge.
|
||||||
|
x: root.drag-x - drag-icon.x;
|
||||||
|
y: root.drag-y - drag-icon.y;
|
||||||
|
border-radius: UiRadius.control;
|
||||||
|
background: WorkbenchStyle.surface;
|
||||||
|
outline-color: root.drop-target >= 0 ? WorkbenchStyle.accent : UiSurface.hover;
|
||||||
|
opacity: 0.94;
|
||||||
|
drag-icon := Text {
|
||||||
|
x: parent.label-on-left ? parent.width - 30px : 12px;
|
||||||
|
y: (parent.height - self.height) / 2; width: 18px; height: 18px;
|
||||||
|
text: root.drag-entry.directory ? FontAwesomeIcons.folder-open : FontAwesomeIcons.file-lines;
|
||||||
|
font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 17px;
|
||||||
|
color: root.drag-entry.directory ? WorkbenchStyle.accent : ControlStyle.icon;
|
||||||
|
horizontal-alignment: left; vertical-alignment: top;
|
||||||
|
}
|
||||||
|
drag-label := Text {
|
||||||
|
x: parent.label-on-left ? 12px : 40px;
|
||||||
|
width: parent.width - 54px - drag-badge.width; height: parent.height;
|
||||||
|
text: root.drag-entry.name; color: WorkbenchStyle.text;
|
||||||
|
font-size: ImGuiFontStyle.base; font-weight: 500;
|
||||||
|
vertical-alignment: center; overflow: elide;
|
||||||
|
}
|
||||||
|
drag-badge := Rectangle {
|
||||||
|
visible: root.drag-count > 1;
|
||||||
|
x: parent.width - self.width - (parent.label-on-left ? 38px : 10px); y: 10px;
|
||||||
|
width: root.drag-count > 1 ? max(24px, count-label.preferred-width + 12px) : 0px;
|
||||||
|
height: 20px; border-radius: 10px; background: WorkbenchStyle.accent;
|
||||||
|
count-label := Text {
|
||||||
|
width: parent.width; height: parent.height; text: root.drag-count;
|
||||||
|
color: white; font-size: ImGuiFontStyle.body; font-weight: 600;
|
||||||
|
horizontal-alignment: center; vertical-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export component WorkbenchButton inherits FlatSurface {
|
|||||||
in property <bool> soft: false;
|
in property <bool> soft: false;
|
||||||
in property <bool> quiet: false;
|
in property <bool> quiet: false;
|
||||||
in property <bool> enabled: true;
|
in property <bool> enabled: true;
|
||||||
|
in property <MouseCursor> mouse-cursor: pointer;
|
||||||
out property <bool> has-focus: button-focus.has-focus;
|
out property <bool> has-focus: button-focus.has-focus;
|
||||||
private property <bool> keyboard-focus: false;
|
private property <bool> keyboard-focus: false;
|
||||||
private property <bool> show-focus: button-focus.has-focus && root.keyboard-focus;
|
private property <bool> show-focus: button-focus.has-focus && root.keyboard-focus;
|
||||||
@@ -113,7 +114,7 @@ export component WorkbenchButton inherits FlatSurface {
|
|||||||
}
|
}
|
||||||
touch := TouchArea {
|
touch := TouchArea {
|
||||||
enabled: root.enabled;
|
enabled: root.enabled;
|
||||||
mouse-cursor: pointer;
|
mouse-cursor: root.mouse-cursor;
|
||||||
pointer-event(event) => {
|
pointer-event(event) => {
|
||||||
if event.kind == PointerEventKind.down {
|
if event.kind == PointerEventKind.down {
|
||||||
root.keyboard-focus = false;
|
root.keyboard-focus = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user