2299 lines
103 KiB
Plaintext
2299 lines
103 KiB
Plaintext
import { Theme } from "theme.slint";
|
||
import { SftpPanel, SftpEntry, SftpTreeNode } from "sftp_panel.slint";
|
||
|
||
// Embed the terminal fonts here too so the cell-probe Text below measures
|
||
// with the CORRECT font (Cascadia Mono), not a system fallback. Font imports
|
||
// in app.slint are globally available in theory, but some Slint builds only
|
||
// register them in the entry-point file's component tree; importing them here
|
||
// guarantees the probe uses Cascadia Mono for its preferred-width calculation.
|
||
import "fonts/CascadiaMono-Regular.ttf";
|
||
import "fonts/CascadiaMono-Bold.ttf";
|
||
|
||
// One coloured run of text on the terminal grid. The Rust side groups
|
||
// consecutive vt100 cells that share fg + bg + bold into a single span and
|
||
// tags it with its grid position (row, col) and width in cells, so the UI can
|
||
// place both a background fill and the text with absolute monospace coordinates.
|
||
// `bg` is transparent (alpha 0) when the cell uses the default background.
|
||
export struct TermSpan {
|
||
text: string,
|
||
fg: color,
|
||
bg: color,
|
||
bold: bool,
|
||
use-fallback-font: bool,
|
||
row: int,
|
||
col: int,
|
||
cells: int,
|
||
}
|
||
|
||
// A find-match highlight rectangle on the terminal grid.
|
||
export struct TermMatch {
|
||
row: int,
|
||
col: int,
|
||
len: int,
|
||
}
|
||
|
||
export struct CommandCategory {
|
||
id: string,
|
||
name: string,
|
||
builtin: bool,
|
||
}
|
||
|
||
export struct CommandItem {
|
||
id: string,
|
||
category-id: string,
|
||
name: string,
|
||
command: string,
|
||
append-cr: bool,
|
||
}
|
||
|
||
// --- MenuItem --------------------------------------------------------------
|
||
// One row of the terminal right-click context menu (MDL2 icon + label).
|
||
component MenuItem inherits Rectangle {
|
||
in property <string> icon;
|
||
in property <string> label;
|
||
callback clicked();
|
||
|
||
height: 28px;
|
||
border-radius: Theme.radius-sm;
|
||
background: ta.has-hover ? Theme.bg-hover : transparent;
|
||
|
||
ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.clicked(); }
|
||
}
|
||
HorizontalLayout {
|
||
height: parent.height;
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
spacing: 8px;
|
||
Text {
|
||
width: 18px;
|
||
height: parent.height;
|
||
text: root.icon;
|
||
font-family: "Material Icons";
|
||
color: Theme.text-secondary;
|
||
font-size: Theme.fs-sm;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
Text {
|
||
text: root.label;
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-md;
|
||
vertical-alignment: center;
|
||
horizontal-stretch: 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
component CommandIconButton inherits Rectangle {
|
||
in property <string> glyph;
|
||
in property <brush> fg: Theme.text-secondary;
|
||
in property <brush> fg-hover: Theme.text-primary;
|
||
callback clicked();
|
||
|
||
width: 26px;
|
||
height: 24px;
|
||
border-radius: Theme.radius-sm;
|
||
background: touch.has-hover ? Theme.bg-hover : transparent;
|
||
|
||
touch := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.clicked(); }
|
||
}
|
||
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: root.glyph;
|
||
font-family: "Material Icons";
|
||
color: touch.has-hover ? root.fg-hover : root.fg;
|
||
font-size: Theme.fs-sm;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
|
||
component CommandParamField inherits Rectangle {
|
||
in property <string> label;
|
||
in-out property <string> value;
|
||
in property <bool> missing: label != "" && value == "";
|
||
property <string> label-text: label + ":";
|
||
|
||
height: 30px;
|
||
vertical-stretch: 0;
|
||
border-radius: Theme.radius-sm;
|
||
border-width: 1px;
|
||
border-color: missing ? Theme.danger : (input.has-focus ? Theme.accent : Theme.border-subtle);
|
||
background: Theme.bg-root;
|
||
|
||
label-probe := Text {
|
||
text: root.label-text;
|
||
font-size: Theme.fs-xs;
|
||
opacity: 0;
|
||
x: -1000px;
|
||
y: -1000px;
|
||
}
|
||
|
||
label-hover := TouchArea {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
}
|
||
|
||
input := TextInput {
|
||
x: 8px;
|
||
y: (parent.height - self.height) / 2;
|
||
width: parent.width - 16px;
|
||
height: 24px;
|
||
text <=> root.value;
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-sm;
|
||
single-line: true;
|
||
vertical-alignment: center;
|
||
}
|
||
|
||
if root.value == "" : Text {
|
||
x: 10px;
|
||
y: (parent.height - self.height) / 2;
|
||
width: parent.width - 20px;
|
||
height: 22px;
|
||
text: root.label-text;
|
||
color: Theme.text-muted;
|
||
font-size: Theme.fs-xs;
|
||
vertical-alignment: center;
|
||
overflow: elide;
|
||
}
|
||
|
||
if label-hover.has-hover && label-probe.preferred-width > root.width - 20px : Rectangle {
|
||
x: 0px;
|
||
y: -28px;
|
||
width: label-probe.preferred-width + 16px;
|
||
height: 24px;
|
||
z: 100;
|
||
border-radius: Theme.radius-sm;
|
||
border-width: 1px;
|
||
border-color: Theme.border-strong;
|
||
background: Theme.bg-panel;
|
||
drop-shadow-blur: 8px;
|
||
drop-shadow-color: #00000050;
|
||
|
||
Text {
|
||
x: 8px;
|
||
width: parent.width - 16px;
|
||
height: parent.height;
|
||
text: root.label-text;
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-xs;
|
||
vertical-alignment: center;
|
||
overflow: elide;
|
||
}
|
||
}
|
||
}
|
||
|
||
component CommandParamInsertButton inherits Rectangle {
|
||
in property <string> label;
|
||
callback clicked();
|
||
|
||
width: 44px;
|
||
height: 24px;
|
||
border-radius: Theme.radius-sm;
|
||
background: touch.has-hover ? Theme.bg-hover : Theme.bg-elevated;
|
||
|
||
touch := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.clicked(); }
|
||
}
|
||
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: root.label;
|
||
color: Theme.text-secondary;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
|
||
// --- CommandManager --------------------------------------------------------
|
||
// Bottom dock for FinalShell-style custom commands.
|
||
component CommandManager inherits Rectangle {
|
||
in property <[CommandCategory]> command-categories: [];
|
||
in property <[CommandItem]> command-items: [];
|
||
in-out property <string> selected-category;
|
||
in-out property <string> selected-command;
|
||
in-out property <string> category-draft;
|
||
in-out property <string> edit-id;
|
||
in-out property <string> edit-name;
|
||
in-out property <string> edit-command;
|
||
in-out property <bool> edit-append-cr: true;
|
||
in-out property <string> param1-name;
|
||
in-out property <string> param1-value;
|
||
in-out property <string> param2-name;
|
||
in-out property <string> param2-value;
|
||
in-out property <string> param3-name;
|
||
in-out property <string> param3-value;
|
||
in-out property <string> param4-name;
|
||
in-out property <string> param4-value;
|
||
in-out property <string> param5-name;
|
||
in-out property <string> param5-value;
|
||
|
||
callback category-select(string);
|
||
callback category-add(string);
|
||
callback category-rename(string, string);
|
||
callback category-delete(string);
|
||
callback command-new(string);
|
||
callback command-select(string);
|
||
callback command-save(string, string, string, string, bool);
|
||
callback command-delete(string);
|
||
callback command-run(string, string, string, string, string, string);
|
||
callback command-move(string, string);
|
||
|
||
in-out property <bool> editor-open;
|
||
in-out property <bool> category-editor-open;
|
||
in-out property <string> category-edit-id;
|
||
in-out property <bool> category-can-delete;
|
||
property <bool> params-missing:
|
||
(param1-name != "" && param1-value == "")
|
||
|| (param2-name != "" && param2-value == "")
|
||
|| (param3-name != "" && param3-value == "")
|
||
|| (param4-name != "" && param4-value == "")
|
||
|| (param5-name != "" && param5-value == "");
|
||
property <length> ctx-x;
|
||
property <length> ctx-y;
|
||
|
||
background: Theme.bg-panel;
|
||
|
||
selected-name-probe := Text {
|
||
text: root.edit-name;
|
||
font-size: Theme.fs-sm;
|
||
opacity: 0;
|
||
x: -1000px;
|
||
y: -1000px;
|
||
}
|
||
|
||
HorizontalLayout {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
spacing: 0;
|
||
|
||
// Categories --------------------------------------------------------
|
||
Rectangle {
|
||
width: 178px;
|
||
background: Theme.bg-panel;
|
||
|
||
VerticalLayout {
|
||
spacing: 0;
|
||
|
||
Rectangle {
|
||
height: 28px;
|
||
background: Theme.bg-root;
|
||
header-ta := TouchArea {
|
||
mouse-cursor: default;
|
||
pointer-event(e) => {
|
||
if (e.kind == PointerEventKind.down && e.button == PointerEventButton.right) {
|
||
root.ctx-x = self.mouse-x;
|
||
root.ctx-y = self.mouse-y;
|
||
root.category-edit-id = "";
|
||
root.category-draft = "";
|
||
root.category-can-delete = false;
|
||
cat-new-menu.show();
|
||
}
|
||
}
|
||
}
|
||
HorizontalLayout {
|
||
padding-left: 8px;
|
||
padding-right: 4px;
|
||
spacing: 4px;
|
||
Text {
|
||
text: @tr("Categories");
|
||
color: Theme.text-muted;
|
||
font-size: Theme.fs-xs;
|
||
vertical-alignment: center;
|
||
horizontal-stretch: 1;
|
||
}
|
||
}
|
||
|
||
cat-new-menu := PopupWindow {
|
||
x: root.ctx-x;
|
||
y: root.ctx-y;
|
||
width: 132px;
|
||
height: 36px;
|
||
|
||
Rectangle {
|
||
background: Theme.bg-panel;
|
||
border-radius: Theme.radius-sm;
|
||
border-width: 1px;
|
||
border-color: Theme.border-strong;
|
||
drop-shadow-blur: 10px;
|
||
drop-shadow-color: #00000050;
|
||
|
||
VerticalLayout {
|
||
padding: 4px;
|
||
MenuItem {
|
||
icon: "\u{E145}";
|
||
label: @tr("New category");
|
||
clicked => {
|
||
cat-new-menu.close();
|
||
root.category-editor-open = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Flickable {
|
||
vertical-stretch: 1;
|
||
viewport-width: self.width;
|
||
viewport-height: max(self.height, categories-col.preferred-height);
|
||
categories-col := VerticalLayout {
|
||
spacing: 0;
|
||
for cat[i] in root.command-categories : Rectangle {
|
||
height: 30px;
|
||
background: root.selected-category == cat.id
|
||
? Theme.bg-active
|
||
: cat-ta.has-hover
|
||
? Theme.bg-hover
|
||
: (mod(i, 2) == 0 ? transparent : #ffffff08);
|
||
cat-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.category-select(cat.id); }
|
||
pointer-event(e) => {
|
||
if (e.kind == PointerEventKind.down && e.button == PointerEventButton.right) {
|
||
root.ctx-x = self.mouse-x;
|
||
root.ctx-y = self.mouse-y;
|
||
root.category-edit-id = cat.id;
|
||
root.category-draft = cat.name;
|
||
root.category-can-delete = !cat.builtin;
|
||
cat-menu.show();
|
||
}
|
||
}
|
||
}
|
||
cat-menu := PopupWindow {
|
||
x: root.ctx-x;
|
||
y: root.ctx-y;
|
||
width: 132px;
|
||
height: cat.builtin ? 64px : 92px;
|
||
|
||
Rectangle {
|
||
background: Theme.bg-panel;
|
||
border-radius: Theme.radius-sm;
|
||
border-width: 1px;
|
||
border-color: Theme.border-strong;
|
||
drop-shadow-blur: 10px;
|
||
drop-shadow-color: #00000050;
|
||
|
||
VerticalLayout {
|
||
padding: 4px;
|
||
spacing: 2px;
|
||
MenuItem {
|
||
icon: "\u{E145}";
|
||
label: @tr("New category");
|
||
clicked => {
|
||
cat-menu.close();
|
||
root.category-edit-id = "";
|
||
root.category-draft = "";
|
||
root.category-can-delete = false;
|
||
root.category-editor-open = true;
|
||
}
|
||
}
|
||
MenuItem {
|
||
icon: "\u{E3C9}";
|
||
label: @tr("Rename");
|
||
clicked => {
|
||
cat-menu.close();
|
||
root.category-editor-open = true;
|
||
}
|
||
}
|
||
if !cat.builtin : MenuItem {
|
||
icon: "\u{E872}";
|
||
label: @tr("Delete");
|
||
clicked => {
|
||
cat-menu.close();
|
||
root.category-delete(cat.id);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
HorizontalLayout {
|
||
height: parent.height;
|
||
padding-left: 8px;
|
||
padding-right: 4px;
|
||
spacing: 6px;
|
||
Text {
|
||
width: 16px;
|
||
height: parent.height;
|
||
text: "\u{E2C7}";
|
||
font-family: "Material Icons";
|
||
color: Theme.text-secondary;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
Text {
|
||
text: cat.name;
|
||
color: root.selected-category == cat.id ? Theme.text-primary : Theme.text-secondary;
|
||
font-size: Theme.fs-sm;
|
||
vertical-alignment: center;
|
||
horizontal-stretch: 1;
|
||
overflow: elide;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle { width: 1px; background: Theme.border-subtle; }
|
||
|
||
// Commands + parameter runner --------------------------------------
|
||
VerticalLayout {
|
||
horizontal-stretch: 1;
|
||
spacing: 0;
|
||
|
||
Rectangle {
|
||
height: 38px;
|
||
background: Theme.bg-root;
|
||
HorizontalLayout {
|
||
height: parent.height;
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
spacing: 8px;
|
||
alignment: center;
|
||
|
||
if root.selected-command != "" : Text {
|
||
height: parent.height;
|
||
text: root.edit-name;
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-sm;
|
||
font-weight: 600;
|
||
vertical-alignment: center;
|
||
width: min(160px, max(52px, selected-name-probe.preferred-width));
|
||
overflow: elide;
|
||
}
|
||
|
||
if root.param1-name != "" : CommandParamField {
|
||
y: (parent.height - self.height) / 2;
|
||
width: 132px;
|
||
label: root.param1-name;
|
||
value <=> root.param1-value;
|
||
}
|
||
if root.param2-name != "" : CommandParamField {
|
||
y: (parent.height - self.height) / 2;
|
||
width: 132px;
|
||
label: root.param2-name;
|
||
value <=> root.param2-value;
|
||
}
|
||
if root.param3-name != "" : CommandParamField {
|
||
y: (parent.height - self.height) / 2;
|
||
width: 132px;
|
||
label: root.param3-name;
|
||
value <=> root.param3-value;
|
||
}
|
||
if root.param4-name != "" : CommandParamField {
|
||
y: (parent.height - self.height) / 2;
|
||
width: 132px;
|
||
label: root.param4-name;
|
||
value <=> root.param4-value;
|
||
}
|
||
if root.param5-name != "" : CommandParamField {
|
||
y: (parent.height - self.height) / 2;
|
||
width: 132px;
|
||
label: root.param5-name;
|
||
value <=> root.param5-value;
|
||
}
|
||
|
||
if root.params-missing : Text {
|
||
height: parent.height;
|
||
text: @tr("Missing parameters");
|
||
color: Theme.danger;
|
||
font-size: Theme.fs-xs;
|
||
font-weight: 600;
|
||
vertical-alignment: center;
|
||
}
|
||
|
||
Rectangle { horizontal-stretch: 1; }
|
||
}
|
||
}
|
||
|
||
Rectangle { height: 1px; background: Theme.border-subtle; }
|
||
|
||
Rectangle {
|
||
vertical-stretch: 1;
|
||
background: Theme.bg-panel;
|
||
|
||
cmd-bg-ta := TouchArea {
|
||
pointer-event(e) => {
|
||
if (e.kind == PointerEventKind.down && e.button == PointerEventButton.right) {
|
||
root.ctx-x = self.mouse-x;
|
||
root.ctx-y = self.mouse-y;
|
||
cmd-new-menu.show();
|
||
}
|
||
}
|
||
}
|
||
|
||
Flickable {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
horizontal-stretch: 1;
|
||
vertical-stretch: 1;
|
||
viewport-width: max(self.width, commands-row.preferred-width);
|
||
viewport-height: self.height;
|
||
|
||
commands-row := HorizontalLayout {
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
padding-top: 8px;
|
||
spacing: 6px;
|
||
|
||
for cmd[i] in root.command-items : Rectangle {
|
||
visible: cmd.category-id == root.selected-category;
|
||
width: cmd.category-id == root.selected-category
|
||
? min(168px, max(48px, cmd-name-probe.preferred-width + 24px))
|
||
: 0px;
|
||
height: cmd.category-id == root.selected-category ? 28px : 0px;
|
||
border-radius: Theme.radius-sm;
|
||
background: root.selected-command == cmd.id
|
||
? Theme.bg-active
|
||
: cmd-row-ta.has-hover
|
||
? Theme.bg-hover
|
||
: Theme.bg-elevated;
|
||
cmd-row-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => {
|
||
if (root.selected-command != cmd.id) {
|
||
root.command-select(cmd.id);
|
||
}
|
||
}
|
||
pointer-event(e) => {
|
||
if (e.kind == PointerEventKind.down && e.button == PointerEventButton.right) {
|
||
root.ctx-x = self.mouse-x;
|
||
root.ctx-y = self.mouse-y;
|
||
if (root.selected-command != cmd.id) {
|
||
root.command-select(cmd.id);
|
||
}
|
||
cmd-menu.show();
|
||
}
|
||
}
|
||
double-clicked => {
|
||
root.command-run(cmd.id, root.param1-value, root.param2-value, root.param3-value, root.param4-value, root.param5-value);
|
||
}
|
||
}
|
||
cmd-menu := PopupWindow {
|
||
x: root.ctx-x;
|
||
y: root.ctx-y;
|
||
width: 132px;
|
||
height: 92px;
|
||
|
||
Rectangle {
|
||
background: Theme.bg-panel;
|
||
border-radius: Theme.radius-sm;
|
||
border-width: 1px;
|
||
border-color: Theme.border-strong;
|
||
drop-shadow-blur: 10px;
|
||
drop-shadow-color: #00000050;
|
||
|
||
VerticalLayout {
|
||
padding: 4px;
|
||
spacing: 2px;
|
||
MenuItem {
|
||
icon: "\u{E037}";
|
||
label: @tr("Run");
|
||
clicked => {
|
||
cmd-menu.close();
|
||
root.command-run(cmd.id, root.param1-value, root.param2-value, root.param3-value, root.param4-value, root.param5-value);
|
||
}
|
||
}
|
||
MenuItem {
|
||
icon: "\u{E3C9}";
|
||
label: @tr("Edit");
|
||
clicked => {
|
||
cmd-menu.close();
|
||
root.editor-open = true;
|
||
}
|
||
}
|
||
MenuItem {
|
||
icon: "\u{E872}";
|
||
label: @tr("Delete");
|
||
clicked => {
|
||
cmd-menu.close();
|
||
root.command-delete(cmd.id);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
cmd-name-probe := Text {
|
||
text: cmd.name;
|
||
font-size: Theme.fs-sm;
|
||
opacity: 0;
|
||
x: -1000px;
|
||
y: -1000px;
|
||
}
|
||
|
||
Text {
|
||
x: 10px;
|
||
width: parent.width - 20px;
|
||
height: parent.height;
|
||
text: cmd.name;
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-sm;
|
||
vertical-alignment: center;
|
||
horizontal-alignment: center;
|
||
overflow: elide;
|
||
}
|
||
|
||
if cmd.append-cr : Rectangle {
|
||
x: parent.width - 7px;
|
||
y: 5px;
|
||
width: 4px;
|
||
height: 4px;
|
||
border-radius: 2px;
|
||
background: Theme.success;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
cmd-new-menu := PopupWindow {
|
||
x: root.ctx-x;
|
||
y: root.ctx-y;
|
||
width: 132px;
|
||
height: 36px;
|
||
|
||
Rectangle {
|
||
background: Theme.bg-panel;
|
||
border-radius: Theme.radius-sm;
|
||
border-width: 1px;
|
||
border-color: Theme.border-strong;
|
||
drop-shadow-blur: 10px;
|
||
drop-shadow-color: #00000050;
|
||
|
||
VerticalLayout {
|
||
padding: 4px;
|
||
MenuItem {
|
||
icon: "\u{E145}";
|
||
label: @tr("New command");
|
||
clicked => {
|
||
cmd-new-menu.close();
|
||
root.command-new(root.selected-category);
|
||
root.editor-open = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if false : Rectangle {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
background: #00000088;
|
||
|
||
close-backdrop := TouchArea {
|
||
clicked => { root.editor-open = false; }
|
||
}
|
||
|
||
Rectangle {
|
||
x: 24px;
|
||
y: 8px;
|
||
width: parent.width - 48px;
|
||
height: parent.height - 16px;
|
||
border-radius: Theme.radius-md;
|
||
border-width: 1px;
|
||
border-color: Theme.border-strong;
|
||
background: Theme.bg-panel;
|
||
drop-shadow-blur: 16px;
|
||
drop-shadow-color: #00000070;
|
||
|
||
swallow := TouchArea { clicked => {} }
|
||
|
||
VerticalLayout {
|
||
padding: 10px;
|
||
spacing: 8px;
|
||
|
||
HorizontalLayout {
|
||
height: 26px;
|
||
spacing: 8px;
|
||
Text {
|
||
text: root.edit-id == "" ? @tr("New command") : @tr("Command detail");
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-sm;
|
||
font-weight: 600;
|
||
vertical-alignment: center;
|
||
horizontal-stretch: 1;
|
||
}
|
||
CommandIconButton {
|
||
y: (parent.height - self.height) / 2;
|
||
glyph: "\u{E5CD}";
|
||
clicked => { root.editor-open = false; }
|
||
}
|
||
}
|
||
|
||
HorizontalLayout {
|
||
height: 28px;
|
||
spacing: 8px;
|
||
Rectangle {
|
||
horizontal-stretch: 1;
|
||
border-radius: Theme.radius-sm;
|
||
border-width: 1px;
|
||
border-color: name-input.has-focus ? Theme.accent : Theme.border-subtle;
|
||
background: Theme.bg-root;
|
||
name-input := TextInput {
|
||
x: 8px;
|
||
width: parent.width - 16px;
|
||
height: parent.height;
|
||
text <=> root.edit-name;
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-sm;
|
||
single-line: true;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
Rectangle {
|
||
width: 118px;
|
||
height: parent.height;
|
||
border-radius: Theme.radius-sm;
|
||
background: root.edit-append-cr ? Theme.bg-active : Theme.bg-elevated;
|
||
cr-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.edit-append-cr = !root.edit-append-cr; }
|
||
}
|
||
HorizontalLayout {
|
||
height: parent.height;
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
spacing: 6px;
|
||
Text {
|
||
width: 16px;
|
||
height: parent.height;
|
||
text: root.edit-append-cr ? "\u{E5CA}" : "";
|
||
font-family: "Material Icons";
|
||
color: Theme.accent;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
Text {
|
||
height: parent.height;
|
||
text: @tr("Append CR");
|
||
color: Theme.text-secondary;
|
||
font-size: Theme.fs-xs;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
height: 52px;
|
||
border-radius: Theme.radius-sm;
|
||
border-width: 1px;
|
||
border-color: command-input.has-focus ? Theme.accent : Theme.border-subtle;
|
||
background: Theme.bg-root;
|
||
command-input := TextInput {
|
||
x: 8px;
|
||
width: parent.width - 16px;
|
||
height: parent.height;
|
||
text <=> root.edit-command;
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-sm;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
|
||
HorizontalLayout {
|
||
height: 24px;
|
||
spacing: 6px;
|
||
CommandParamInsertButton {
|
||
label: "p#1";
|
||
clicked => { root.edit-command = root.edit-command + (root.edit-command == "" ? "[p#1 参数1]" : " [p#1 参数1]"); }
|
||
}
|
||
CommandParamInsertButton {
|
||
label: "p#2";
|
||
clicked => { root.edit-command = root.edit-command + (root.edit-command == "" ? "[p#2 参数2]" : " [p#2 参数2]"); }
|
||
}
|
||
CommandParamInsertButton {
|
||
label: "p#3";
|
||
clicked => { root.edit-command = root.edit-command + (root.edit-command == "" ? "[p#3 参数3]" : " [p#3 参数3]"); }
|
||
}
|
||
CommandParamInsertButton {
|
||
label: "p#4";
|
||
clicked => { root.edit-command = root.edit-command + (root.edit-command == "" ? "[p#4 参数4]" : " [p#4 参数4]"); }
|
||
}
|
||
CommandParamInsertButton {
|
||
label: "p#5";
|
||
clicked => { root.edit-command = root.edit-command + (root.edit-command == "" ? "[p#5 参数5]" : " [p#5 参数5]"); }
|
||
}
|
||
Rectangle { horizontal-stretch: 1; }
|
||
}
|
||
|
||
if root.command-categories.length > 1 && root.edit-id != "" : HorizontalLayout {
|
||
height: 24px;
|
||
spacing: 4px;
|
||
for cat in root.command-categories : Rectangle {
|
||
y: (parent.height - self.height) / 2;
|
||
width: 72px;
|
||
height: 22px;
|
||
border-radius: Theme.radius-sm;
|
||
background: cat.id == root.selected-category
|
||
? Theme.bg-active
|
||
: (move-cat-ta.has-hover ? Theme.bg-hover : Theme.bg-elevated);
|
||
move-cat-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.command-move(root.edit-id, cat.id); }
|
||
}
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: cat.name;
|
||
color: Theme.text-secondary;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
overflow: elide;
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle { vertical-stretch: 1; }
|
||
|
||
HorizontalLayout {
|
||
height: 28px;
|
||
spacing: 8px;
|
||
if root.edit-id != "" : Rectangle {
|
||
width: 72px;
|
||
height: parent.height;
|
||
border-radius: Theme.radius-sm;
|
||
background: del-cmd-ta.has-hover ? #cc333320 : Theme.bg-elevated;
|
||
del-cmd-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => {
|
||
root.command-delete(root.edit-id);
|
||
root.editor-open = false;
|
||
}
|
||
}
|
||
HorizontalLayout {
|
||
height: parent.height;
|
||
padding-left: 10px;
|
||
padding-right: 10px;
|
||
spacing: 6px;
|
||
Text {
|
||
width: 16px;
|
||
height: parent.height;
|
||
text: "\u{E872}";
|
||
font-family: "Material Icons";
|
||
color: Theme.danger;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
Text {
|
||
height: parent.height;
|
||
text: @tr("Delete");
|
||
color: Theme.danger;
|
||
font-size: Theme.fs-xs;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
}
|
||
Rectangle { horizontal-stretch: 1; }
|
||
Rectangle {
|
||
width: 72px;
|
||
height: parent.height;
|
||
border-radius: Theme.radius-sm;
|
||
background: cancel-ta.has-hover ? Theme.bg-hover : Theme.bg-elevated;
|
||
cancel-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.editor-open = false; }
|
||
}
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: @tr("Cancel");
|
||
color: Theme.text-secondary;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
Rectangle {
|
||
width: 72px;
|
||
height: parent.height;
|
||
border-radius: Theme.radius-sm;
|
||
background: save-cmd-ta.has-hover ? Theme.accent-hover : Theme.accent;
|
||
save-cmd-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => {
|
||
root.command-save(root.edit-id, root.selected-category, root.edit-name, root.edit-command, root.edit-append-cr);
|
||
root.editor-open = false;
|
||
}
|
||
}
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: @tr("Save");
|
||
color: #ffffff;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
component CommandEditorModal inherits Rectangle {
|
||
in-out property <bool> is-open;
|
||
in property <[CommandCategory]> command-categories: [];
|
||
in-out property <string> selected-category;
|
||
in-out property <string> edit-id;
|
||
in-out property <string> edit-name;
|
||
in-out property <string> edit-command;
|
||
in-out property <bool> edit-append-cr: true;
|
||
callback command-save(string, string, string, string, bool);
|
||
callback command-delete(string);
|
||
callback command-move(string, string);
|
||
|
||
visible: is-open;
|
||
background: #00000088;
|
||
|
||
TouchArea {
|
||
clicked => { root.is-open = false; }
|
||
}
|
||
|
||
Rectangle {
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: min(540px, parent.width - 48px);
|
||
height: 260px;
|
||
border-radius: Theme.radius-md;
|
||
border-width: 1px;
|
||
border-color: Theme.border-strong;
|
||
background: Theme.bg-panel;
|
||
drop-shadow-blur: 18px;
|
||
drop-shadow-color: #00000080;
|
||
|
||
TouchArea {}
|
||
|
||
VerticalLayout {
|
||
padding: 12px;
|
||
spacing: 8px;
|
||
|
||
HorizontalLayout {
|
||
height: 26px;
|
||
spacing: 8px;
|
||
Text {
|
||
text: root.edit-id == "" ? @tr("New command") : @tr("Command detail");
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-sm;
|
||
font-weight: 600;
|
||
vertical-alignment: center;
|
||
horizontal-stretch: 1;
|
||
}
|
||
CommandIconButton {
|
||
y: (parent.height - self.height) / 2;
|
||
glyph: "\u{E5CD}";
|
||
clicked => { root.is-open = false; }
|
||
}
|
||
}
|
||
|
||
HorizontalLayout {
|
||
height: 28px;
|
||
spacing: 8px;
|
||
Rectangle {
|
||
horizontal-stretch: 1;
|
||
border-radius: Theme.radius-sm;
|
||
border-width: 1px;
|
||
border-color: name-input.has-focus ? Theme.accent : Theme.border-subtle;
|
||
background: Theme.bg-root;
|
||
name-input := TextInput {
|
||
x: 8px;
|
||
width: parent.width - 16px;
|
||
height: parent.height;
|
||
text <=> root.edit-name;
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-sm;
|
||
single-line: true;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
Rectangle {
|
||
width: 118px;
|
||
height: parent.height;
|
||
border-radius: Theme.radius-sm;
|
||
background: root.edit-append-cr ? Theme.bg-active : Theme.bg-elevated;
|
||
cr-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.edit-append-cr = !root.edit-append-cr; }
|
||
}
|
||
HorizontalLayout {
|
||
height: parent.height;
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
spacing: 6px;
|
||
Text {
|
||
width: 16px;
|
||
height: parent.height;
|
||
text: root.edit-append-cr ? "\u{E5CA}" : "";
|
||
font-family: "Material Icons";
|
||
color: Theme.accent;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
Text {
|
||
height: parent.height;
|
||
text: @tr("Append CR");
|
||
color: Theme.text-secondary;
|
||
font-size: Theme.fs-xs;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
height: 52px;
|
||
border-radius: Theme.radius-sm;
|
||
border-width: 1px;
|
||
border-color: command-input.has-focus ? Theme.accent : Theme.border-subtle;
|
||
background: Theme.bg-root;
|
||
command-input := TextInput {
|
||
x: 8px;
|
||
width: parent.width - 16px;
|
||
height: parent.height;
|
||
text <=> root.edit-command;
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-sm;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
|
||
HorizontalLayout {
|
||
height: 24px;
|
||
spacing: 6px;
|
||
CommandParamInsertButton {
|
||
label: "p#1";
|
||
clicked => { root.edit-command = root.edit-command + (root.edit-command == "" ? "[p#1 参数1]" : " [p#1 参数1]"); }
|
||
}
|
||
CommandParamInsertButton {
|
||
label: "p#2";
|
||
clicked => { root.edit-command = root.edit-command + (root.edit-command == "" ? "[p#2 参数2]" : " [p#2 参数2]"); }
|
||
}
|
||
CommandParamInsertButton {
|
||
label: "p#3";
|
||
clicked => { root.edit-command = root.edit-command + (root.edit-command == "" ? "[p#3 参数3]" : " [p#3 参数3]"); }
|
||
}
|
||
CommandParamInsertButton {
|
||
label: "p#4";
|
||
clicked => { root.edit-command = root.edit-command + (root.edit-command == "" ? "[p#4 参数4]" : " [p#4 参数4]"); }
|
||
}
|
||
CommandParamInsertButton {
|
||
label: "p#5";
|
||
clicked => { root.edit-command = root.edit-command + (root.edit-command == "" ? "[p#5 参数5]" : " [p#5 参数5]"); }
|
||
}
|
||
Rectangle { horizontal-stretch: 1; }
|
||
}
|
||
|
||
if root.command-categories.length > 1 && root.edit-id != "" : HorizontalLayout {
|
||
height: 24px;
|
||
spacing: 4px;
|
||
for cat in root.command-categories : Rectangle {
|
||
y: (parent.height - self.height) / 2;
|
||
width: 72px;
|
||
height: 22px;
|
||
border-radius: Theme.radius-sm;
|
||
background: cat.id == root.selected-category
|
||
? Theme.bg-active
|
||
: (move-cat-ta.has-hover ? Theme.bg-hover : Theme.bg-elevated);
|
||
move-cat-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.command-move(root.edit-id, cat.id); }
|
||
}
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: cat.name;
|
||
color: Theme.text-secondary;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
overflow: elide;
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle { vertical-stretch: 1; }
|
||
|
||
HorizontalLayout {
|
||
height: 28px;
|
||
spacing: 8px;
|
||
if root.edit-id != "" : Rectangle {
|
||
width: 72px;
|
||
height: parent.height;
|
||
border-radius: Theme.radius-sm;
|
||
background: del-cmd-ta.has-hover ? #cc333320 : Theme.bg-elevated;
|
||
del-cmd-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => {
|
||
root.command-delete(root.edit-id);
|
||
root.is-open = false;
|
||
}
|
||
}
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: @tr("Delete");
|
||
color: Theme.danger;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
Rectangle { horizontal-stretch: 1; }
|
||
Rectangle {
|
||
width: 72px;
|
||
height: parent.height;
|
||
border-radius: Theme.radius-sm;
|
||
background: cancel-ta.has-hover ? Theme.bg-hover : Theme.bg-elevated;
|
||
cancel-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.is-open = false; }
|
||
}
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: @tr("Cancel");
|
||
color: Theme.text-secondary;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
Rectangle {
|
||
width: 72px;
|
||
height: parent.height;
|
||
border-radius: Theme.radius-sm;
|
||
background: save-cmd-ta.has-hover ? Theme.accent-hover : Theme.accent;
|
||
save-cmd-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => {
|
||
root.command-save(root.edit-id, root.selected-category, root.edit-name, root.edit-command, root.edit-append-cr);
|
||
root.is-open = false;
|
||
}
|
||
}
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: @tr("Save");
|
||
color: #ffffff;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
component CategoryEditorModal inherits Rectangle {
|
||
in-out property <bool> is-open;
|
||
in-out property <string> edit-id;
|
||
in-out property <string> draft;
|
||
in property <bool> can-delete: false;
|
||
callback category-add(string);
|
||
callback category-rename(string, string);
|
||
callback category-delete(string);
|
||
|
||
visible: is-open;
|
||
background: #00000088;
|
||
|
||
TouchArea {
|
||
clicked => { root.is-open = false; }
|
||
}
|
||
|
||
Rectangle {
|
||
x: (parent.width - self.width) / 2;
|
||
y: (parent.height - self.height) / 2;
|
||
width: min(360px, parent.width - 48px);
|
||
height: 148px;
|
||
border-radius: Theme.radius-md;
|
||
border-width: 1px;
|
||
border-color: Theme.border-strong;
|
||
background: Theme.bg-panel;
|
||
drop-shadow-blur: 18px;
|
||
drop-shadow-color: #00000080;
|
||
|
||
TouchArea {}
|
||
|
||
VerticalLayout {
|
||
padding: 12px;
|
||
spacing: 10px;
|
||
|
||
HorizontalLayout {
|
||
height: 24px;
|
||
Text {
|
||
text: root.edit-id == "" ? @tr("New category") : @tr("Rename category");
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-sm;
|
||
font-weight: 600;
|
||
vertical-alignment: center;
|
||
horizontal-stretch: 1;
|
||
}
|
||
CommandIconButton {
|
||
y: (parent.height - self.height) / 2;
|
||
glyph: "\u{E5CD}";
|
||
clicked => { root.is-open = false; }
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
height: 28px;
|
||
border-radius: Theme.radius-sm;
|
||
border-width: 1px;
|
||
border-color: category-input.has-focus ? Theme.accent : Theme.border-subtle;
|
||
background: Theme.bg-root;
|
||
category-input := TextInput {
|
||
x: 8px;
|
||
width: parent.width - 16px;
|
||
height: parent.height;
|
||
text <=> root.draft;
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-sm;
|
||
single-line: true;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
|
||
HorizontalLayout {
|
||
height: 28px;
|
||
spacing: 8px;
|
||
if root.can-delete : Rectangle {
|
||
width: 72px;
|
||
height: parent.height;
|
||
border-radius: Theme.radius-sm;
|
||
background: cat-delete-ta.has-hover ? #cc333320 : Theme.bg-elevated;
|
||
cat-delete-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => {
|
||
root.category-delete(root.edit-id);
|
||
root.is-open = false;
|
||
}
|
||
}
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: @tr("Delete");
|
||
color: Theme.danger;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
Rectangle { horizontal-stretch: 1; }
|
||
Rectangle {
|
||
width: 72px;
|
||
height: parent.height;
|
||
border-radius: Theme.radius-sm;
|
||
background: cat-cancel-ta.has-hover ? Theme.bg-hover : Theme.bg-elevated;
|
||
cat-cancel-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => { root.is-open = false; }
|
||
}
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: @tr("Cancel");
|
||
color: Theme.text-secondary;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
Rectangle {
|
||
width: 72px;
|
||
height: parent.height;
|
||
border-radius: Theme.radius-sm;
|
||
background: cat-save-ta.has-hover ? Theme.accent-hover : Theme.accent;
|
||
cat-save-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => {
|
||
if (root.edit-id == "") {
|
||
root.category-add(root.draft);
|
||
} else {
|
||
root.category-rename(root.edit-id, root.draft);
|
||
}
|
||
root.is-open = false;
|
||
}
|
||
}
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: @tr("Save");
|
||
color: #ffffff;
|
||
font-size: Theme.fs-xs;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- TerminalView ----------------------------------------------------------
|
||
|
||
export component TerminalView inherits Rectangle {
|
||
in property <string> tab-id;
|
||
in property <string> status-line: @tr("Not connected");
|
||
in property <[TermSpan]> spans; // coloured runs of the visible screen
|
||
in property <int> cursor-row; // cursor cell (grid coords)
|
||
in property <int> cursor-col;
|
||
in property <int> rows-used; // content rows, for viewport sizing
|
||
in property <bool> is-alt-screen;
|
||
in property <bool> mouse-tracking: false;
|
||
in property <[TermMatch]> find-matches; // search-highlight rectangles
|
||
in property <[TermMatch]> selection; // drag-selection highlight rectangles
|
||
|
||
in property <string> sftp-path: "/";
|
||
in property <[SftpEntry]> sftp-entries;
|
||
in property <string> sftp-status: "";
|
||
in property <bool> sftp-loading: false;
|
||
in property <[SftpTreeNode]> sftp-tree-nodes: [];
|
||
in property <length> terminal-font-size: 13px;
|
||
in property <bool> click-debug-enabled: false;
|
||
in property <string> background-path;
|
||
in property <int> background-mode: 1;
|
||
in property <image> background-image;
|
||
in property <bool> app-background-active: false;
|
||
in property <float> terminal-bg-opacity: 0.88;
|
||
in property <float> ui-opacity: 0.72;
|
||
in property <length> sftp-file-font-size: 16px;
|
||
in-out property <length> sftp-name-column-width: 180px;
|
||
in-out property <bool> sftp-name-width-manual: false;
|
||
in property <[CommandCategory]> command-categories: [];
|
||
in property <[CommandItem]> command-items: [];
|
||
in-out property <string> command-selected-category;
|
||
in-out property <string> command-selected-id;
|
||
in-out property <string> command-category-draft;
|
||
in-out property <string> command-edit-id;
|
||
in-out property <string> command-edit-name;
|
||
in-out property <string> command-edit-command;
|
||
in-out property <bool> command-edit-append-cr: true;
|
||
in-out property <string> command-param1-name;
|
||
in-out property <string> command-param1-value;
|
||
in-out property <string> command-param2-name;
|
||
in-out property <string> command-param2-value;
|
||
in-out property <string> command-param3-name;
|
||
in-out property <string> command-param3-value;
|
||
in-out property <string> command-param4-name;
|
||
in-out property <string> command-param4-value;
|
||
in-out property <string> command-param5-name;
|
||
in-out property <string> command-param5-value;
|
||
property <bool> command-editor-open: false;
|
||
property <bool> command-category-editor-open: false;
|
||
property <string> command-category-edit-id;
|
||
property <bool> command-category-can-delete: false;
|
||
|
||
in-out property <length> sftp-panel-height: 220px;
|
||
|
||
callback send-key(string, bool, bool, bool);
|
||
callback terminal-resize(float, float);
|
||
callback sftp-navigate(string);
|
||
callback sftp-download(string);
|
||
callback sftp-upload-clicked(string);
|
||
callback sftp-refresh(string);
|
||
callback sftp-tree-expand(string);
|
||
callback sftp-delete(string);
|
||
callback sftp-rename(string, string);
|
||
callback sftp-edit(string);
|
||
callback sftp-name-column-width-changed(float);
|
||
callback sftp-name-column-auto();
|
||
callback paste-from-clipboard();
|
||
callback copy-terminal-text();
|
||
callback clear-terminal(); // wipe this session's screen buffer
|
||
callback find-query-changed(string); // recompute search highlights
|
||
callback terminal-scroll(int); // scroll history by N lines (+ = up)
|
||
callback select-start(int, int); // begin drag-selection at (row, col)
|
||
callback select-update(int, int); // extend drag-selection to (row, col)
|
||
callback select-end(); // finish selection → copy to clipboard
|
||
callback select-autoscroll(int); // auto-scroll while dragging past edge (dir: -1 up / +1 down)
|
||
callback command-category-select(string);
|
||
callback command-category-add(string);
|
||
callback command-category-rename(string, string);
|
||
callback command-category-delete(string);
|
||
callback command-new(string);
|
||
callback command-select(string);
|
||
callback command-save(string, string, string, string, bool);
|
||
callback command-delete(string);
|
||
callback command-run(string, string, string, string, string, string);
|
||
callback command-move(string, string);
|
||
callback terminal-mouse-event(string, int, int, int);
|
||
callback debug-click(string, string);
|
||
|
||
// Right-click context-menu anchor + find-bar visibility (private UI state).
|
||
property <length> ctx-x;
|
||
property <length> ctx-y;
|
||
property <bool> find-active: false;
|
||
property <bool> selecting: false;
|
||
property <int> mouse-drag-button: -1;
|
||
property <length> mouse-drag-start-x: 0px;
|
||
property <length> mouse-drag-start-y: 0px;
|
||
property <bool> mouse-drag-started: false;
|
||
// Auto-scroll direction while drag-selecting past the visible edge.
|
||
// 0 = none, -1 = above top (scroll to older), +1 = below bottom (newer).
|
||
property <int> autoscroll-dir: 0;
|
||
property <length> vis-y: 0px; // scratch: cursor Y within the visible viewport
|
||
property <int> tool-tab: 0; // 0 files / 1 commands
|
||
property <float> sent-cols: -1;
|
||
property <float> sent-rows: -1;
|
||
|
||
background: root.app-background-active
|
||
? Theme.term-bg.with-alpha(root.terminal-bg-opacity)
|
||
: root.background-mode == 1 && root.background-path != ""
|
||
? Theme.term-bg.with-alpha(root.terminal-bg-opacity)
|
||
: Theme.term-bg;
|
||
forward-focus: ime-input;
|
||
|
||
if root.background-mode == 1 && root.background-path != "" : Image {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
source: root.background-image;
|
||
image-fit: cover;
|
||
opacity: 0.34;
|
||
}
|
||
|
||
// Drag state (private)
|
||
property <bool> sftp-dragging: false;
|
||
property <length> drag-start-mouse-y: 0px;
|
||
|
||
// --- Real cell-size measurement ----------------------------------------
|
||
// A hidden Text rendered with the exact terminal font reports the true
|
||
// Consolas cell width/height for the current font size. We derive the PTY
|
||
// column/row counts from these measured cells instead of guessing 8×16px,
|
||
// so full-screen programs (nano/vim) get an accurate row count and their
|
||
// bottom shortcut bar is no longer clipped off the visible area.
|
||
// Measure a 50-char run, not a single glyph: a single Text's preferred-width
|
||
// includes left/right side bearings and is wider than the real per-character
|
||
// advance, which would make absolutely-positioned spans drift apart. The
|
||
// average advance over a long run (side bearings amortised) is accurate.
|
||
// Cell-size probe: must be RENDERED (opacity:0, not visible:false) so Slint
|
||
// resolves the correct embedded font before computing preferred-width/height.
|
||
// visible:false can skip font loading and fall back to a system font (e.g.
|
||
// Consolas ~6.5 px/char instead of Cascadia Mono ~7.6 px/char), giving a
|
||
// cell-w that is too narrow and a term-cols that is too large.
|
||
cell-probe := Text {
|
||
text: "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"; // exactly 50×M
|
||
font-family: "Cascadia Mono";
|
||
font-size: root.terminal-font-size;
|
||
// Keep it in layout but invisible: opacity:0 forces full font loading.
|
||
opacity: 0;
|
||
// Park it at y<0 so it never overlaps terminal content.
|
||
y: -200px;
|
||
width: 0px; // zero out layout contribution
|
||
height: 0px;
|
||
}
|
||
// cell-w / cell-h come straight from the probe so they ALWAYS match the
|
||
// font the spans below are actually rendered with. (Forcing a hard-coded
|
||
// minimum here is dangerous: if the spans fall back to a system font, a
|
||
// mismatched cell-w garbles the grid. Keep them derived from one source.)
|
||
property <length> cell-w: max(1px, cell-probe.preferred-width / 50);
|
||
// Guard band avoids clipping glyph ascenders/descenders during repaint.
|
||
property <length> cell-h: max(1px, cell-probe.preferred-height + 2px);
|
||
// Usable text box = root.width minus the 10px left/right padding of the
|
||
// terminal grid. Derive the usable height from TerminalView's outer size and
|
||
// fixed chrome only. Reading key-capture.height or flickable.viewport-height
|
||
// here can feed Slint's preferred-size calculation back into itself when a
|
||
// terminal tab becomes visible, which trips "Recursion detected".
|
||
property <length> key-capture-h: max(
|
||
0px,
|
||
root.height - 24px - 4px - root.sftp-panel-height
|
||
);
|
||
property <length> terminal-viewport-h: root.key-capture-h;
|
||
property <length> terminal-pad-y: 4px;
|
||
property <length> terminal-grid-h: max(0px, root.terminal-viewport-h - root.terminal-pad-y * 2);
|
||
property <length> terminal-content-h: root.is-alt-screen
|
||
? (root.selecting ? max(root.terminal-grid-h, root.rows-used * root.cell-h) : root.terminal-grid-h)
|
||
: max(root.terminal-grid-h, root.rows-used * root.cell-h);
|
||
property <length> terminal-scroll-h: root.terminal-content-h + root.terminal-pad-y * 2;
|
||
property <float> term-cols: max(10, floor((root.width - 20px) / cell-w));
|
||
property <float> term-rows: max(5, floor(root.terminal-grid-h / cell-h));
|
||
// Only resize the PTY while this tab is visible. Inactive tabs are laid out
|
||
// at width 0 (-> term-cols collapses to 10); resizing the remote to 10
|
||
// columns reflows the vt100 screen and destroys the scrollback, so hidden
|
||
// sessions stay frozen at their last real size.
|
||
//
|
||
// Do not emit resize from init/changed term-cols/changed term-rows. Those
|
||
// handlers run while Slint is still solving the parent layout and force
|
||
// term-rows -> root.height -> parent layout-cache, which recursively asks
|
||
// this TerminalView for layout info. The timer fires after layout has
|
||
// settled and only forwards actual size changes.
|
||
Timer {
|
||
running: root.visible;
|
||
interval: 200ms;
|
||
triggered => {
|
||
if (root.term-cols != root.sent-cols || root.term-rows != root.sent-rows) {
|
||
root.sent-cols = root.term-cols;
|
||
root.sent-rows = root.term-rows;
|
||
root.terminal-resize(root.term-cols, root.term-rows);
|
||
}
|
||
}
|
||
}
|
||
|
||
changed spans => {
|
||
// Bash: keep the latest output pinned to the bottom.
|
||
// Alt-screen (nano/vim/htop/btop): the program owns a fixed full screen,
|
||
// so the viewport must stay top-aligned (viewport-y = 0). Scrolling to
|
||
// the bottom would push the title bar + content off the top, leaving only
|
||
// the program's status bar — exactly the "nano shows nothing" symptom.
|
||
if (!root.is-alt-screen && !root.selecting) {
|
||
flickable.viewport-y = flickable.height - flickable.viewport-height;
|
||
} else if (!root.selecting) {
|
||
flickable.viewport-y = 0;
|
||
}
|
||
}
|
||
changed is-alt-screen => {
|
||
// Reset scroll on every mode switch (entering/leaving nano, vim, btop…).
|
||
// Snapping to top on alt-screen entry ensures the full-screen program's
|
||
// frame is shown from row 0, not from wherever history left off.
|
||
if (root.is-alt-screen) {
|
||
flickable.viewport-y = 0;
|
||
} else {
|
||
// Return to normal mode: pin to the most recent output.
|
||
flickable.viewport-y = flickable.height - flickable.viewport-height;
|
||
}
|
||
}
|
||
changed visible => {
|
||
if self.visible {
|
||
// Force the timer to re-sync the PTY size on return.
|
||
root.sent-cols = -1;
|
||
root.sent-rows = -1;
|
||
if (!root.command-editor-open && !root.command-category-editor-open) {
|
||
ime-input.focus();
|
||
}
|
||
}
|
||
}
|
||
|
||
VerticalLayout {
|
||
spacing: 0;
|
||
|
||
// --- Status strip ---------------------------------------------------
|
||
Rectangle {
|
||
height: 24px;
|
||
background: root.app-background-active ? Theme.bg-panel-alt.with-alpha(root.ui-opacity) : Theme.bg-panel-alt;
|
||
HorizontalLayout {
|
||
padding-left: 10px; padding-right: 10px;
|
||
Text {
|
||
text: root.is-alt-screen ? @tr("Running in alternate screen") : "";
|
||
color: Theme.text-secondary;
|
||
font-size: Theme.fs-xs;
|
||
vertical-alignment: center;
|
||
horizontal-stretch: 1;
|
||
}
|
||
// Alt-screen indicator only. IMPORTANT: never put cell-w / cell-h
|
||
// (a Text's preferred-size) inside another Text's `text` — doing so
|
||
// makes Slint compute one text layout *inside* another, which
|
||
// re-enters parley's FontContext RefCell and panics ("already
|
||
// borrowed"). A plain bool flag is safe.
|
||
if root.is-alt-screen : Text {
|
||
text: "[ALT]";
|
||
color: Theme.success;
|
||
font-size: Theme.fs-xs;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Key-capture scope + scrollable output --------------------------
|
||
key-capture := FocusScope {
|
||
vertical-stretch: 1;
|
||
// Any click inside this scope (e.g. the middle-click paste strip)
|
||
// can grab focus to the FocusScope itself. Key forwarding lives on
|
||
// the hidden `ime-input` TextInput, so a bare FocusScope focus means
|
||
// the cursor stops blinking and typing stops working. Forwarding
|
||
// focus to ime-input guarantees the text input always ends up with
|
||
// the keyboard focus, no matter what grabbed it.
|
||
forward-focus: ime-input;
|
||
property <bool> cursor-blink: true;
|
||
|
||
Timer {
|
||
running: ime-input.has-focus;
|
||
interval: 530ms;
|
||
triggered => { key-capture.cursor-blink = !key-capture.cursor-blink; }
|
||
}
|
||
|
||
// Auto-scroll while the drag-selection is held past the top/bottom
|
||
// edge: ticks repeatedly so the scrollback keeps moving even when the
|
||
// mouse is held still beyond the edge.
|
||
Timer {
|
||
running: root.selecting && root.autoscroll-dir != 0;
|
||
interval: 50ms;
|
||
triggered => { root.select-autoscroll(root.autoscroll-dir); }
|
||
}
|
||
|
||
VerticalLayout {
|
||
spacing: 0;
|
||
|
||
flickable := Flickable {
|
||
vertical-stretch: 1;
|
||
viewport-width: self.width;
|
||
// Alt-screen: lock the viewport to exactly the visible height
|
||
// so the full-screen program (nano/vim) renders top-aligned
|
||
// with no internal scroll. Bash: let the viewport grow with
|
||
// the content so we can scroll back through history.
|
||
viewport-height: root.terminal-scroll-h;
|
||
|
||
// The terminal grid. Each coloured run is placed with
|
||
// absolute coordinates derived from its (row, col) and the
|
||
// measured Consolas cell size — no per-line layout needed,
|
||
// which keeps the model flat (a single `for`).
|
||
Rectangle {
|
||
x: 10px;
|
||
y: root.terminal-pad-y;
|
||
width: parent.width - 20px;
|
||
height: root.terminal-content-h;
|
||
|
||
// Click-to-focus over the whole terminal body. Without
|
||
// this, clicking anywhere in the output area blurs the
|
||
// hidden ime-input (Slint clears focus on a click that
|
||
// lands on a non-focusable element), which makes the
|
||
// cursor vanish and keystrokes stop — recoverable only by
|
||
// reopening the tab. This TouchArea sits *below* the text
|
||
// (Text never grabs pointer events, so clicks fall through
|
||
// to it) and restores focus on every click. In mouse-
|
||
// tracking apps such as tmux, unmodified mouse input is
|
||
// forwarded; Shift+drag/Shift+right-click stay local.
|
||
body-touch := TouchArea {
|
||
mouse-cursor: text;
|
||
pointer-event(ev) => {
|
||
if (ev.kind == PointerEventKind.down) {
|
||
// Any button press re-takes keyboard focus.
|
||
ime-input.focus();
|
||
if (root.mouse-tracking && !ev.modifiers.shift) {
|
||
root.mouse-drag-button =
|
||
ev.button == PointerEventButton.left ? 0
|
||
: ev.button == PointerEventButton.middle ? 1
|
||
: 2;
|
||
root.mouse-drag-start-x = self.mouse-x;
|
||
root.mouse-drag-start-y = self.mouse-y;
|
||
root.mouse-drag-started = false;
|
||
root.terminal-mouse-event(
|
||
"down",
|
||
root.mouse-drag-button,
|
||
floor(self.mouse-y / root.cell-h),
|
||
floor(self.mouse-x / root.cell-w));
|
||
} else if (ev.button == PointerEventButton.right) {
|
||
root.ctx-x = self.mouse-x;
|
||
root.ctx-y = self.mouse-y;
|
||
ctx-menu.show();
|
||
} else if (ev.button == PointerEventButton.left
|
||
&& (!root.mouse-tracking || ev.modifiers.shift)) {
|
||
root.selecting = true;
|
||
root.select-start(
|
||
floor(self.mouse-y / root.cell-h),
|
||
floor(self.mouse-x / root.cell-w));
|
||
}
|
||
} else if (ev.kind == PointerEventKind.up) {
|
||
if (ev.button == PointerEventButton.middle) {
|
||
if (root.mouse-tracking && !ev.modifiers.shift) {
|
||
root.terminal-mouse-event(
|
||
"up", 1,
|
||
floor(self.mouse-y / root.cell-h),
|
||
floor(self.mouse-x / root.cell-w));
|
||
root.mouse-drag-button = -1;
|
||
root.mouse-drag-started = false;
|
||
} else {
|
||
root.paste-from-clipboard();
|
||
}
|
||
ime-input.focus();
|
||
} else if (ev.button == PointerEventButton.left
|
||
&& root.selecting) {
|
||
root.selecting = false;
|
||
root.autoscroll-dir = 0;
|
||
root.select-end();
|
||
} else if (root.mouse-tracking && !ev.modifiers.shift) {
|
||
root.terminal-mouse-event(
|
||
"up",
|
||
ev.button == PointerEventButton.left ? 0
|
||
: ev.button == PointerEventButton.middle ? 1
|
||
: 2,
|
||
floor(self.mouse-y / root.cell-h),
|
||
floor(self.mouse-x / root.cell-w));
|
||
root.mouse-drag-button = -1;
|
||
root.mouse-drag-started = false;
|
||
}
|
||
}
|
||
}
|
||
moved => {
|
||
if (root.mouse-tracking && !self.pressed && root.mouse-drag-button < 0) {
|
||
root.terminal-mouse-event(
|
||
"move",
|
||
3,
|
||
floor(self.mouse-y / root.cell-h),
|
||
floor(self.mouse-x / root.cell-w));
|
||
} else if (root.mouse-tracking && root.mouse-drag-button >= 0) {
|
||
if (!root.mouse-drag-started
|
||
&& abs(self.mouse-x - root.mouse-drag-start-x) < 2px
|
||
&& abs(self.mouse-y - root.mouse-drag-start-y) < 2px) {
|
||
return;
|
||
}
|
||
root.mouse-drag-started = true;
|
||
root.terminal-mouse-event(
|
||
"drag",
|
||
root.mouse-drag-button,
|
||
floor(self.mouse-y / root.cell-h),
|
||
floor(self.mouse-x / root.cell-w));
|
||
} else if (root.selecting) {
|
||
// Position of the cursor within the *visible*
|
||
// viewport (body-touch sits on the grid Rectangle
|
||
// at content y=8px; viewport-y offsets the scroll).
|
||
root.select-update(
|
||
floor(self.mouse-y / root.cell-h),
|
||
floor(self.mouse-x / root.cell-w));
|
||
// vy ∈ [0, flickable.height] = where the mouse
|
||
// is inside the visible window. Shift+drag in
|
||
// tmux still uses local selection, so allow
|
||
// edge auto-scroll even on the alt screen.
|
||
root.vis-y = self.mouse-y + root.terminal-pad-y + flickable.viewport-y;
|
||
if (root.vis-y < root.cell-h) {
|
||
root.autoscroll-dir = -1; // past top → older
|
||
} else if (root.vis-y > flickable.height - root.cell-h) {
|
||
root.autoscroll-dir = 1; // past bottom → newer
|
||
} else {
|
||
root.autoscroll-dir = 0;
|
||
}
|
||
}
|
||
}
|
||
scroll-event(ev) => {
|
||
if (root.mouse-tracking) {
|
||
root.terminal-mouse-event(
|
||
"wheel",
|
||
ev.delta-y > 0 ? 64 : 65,
|
||
floor(self.mouse-y / root.cell-h),
|
||
floor(self.mouse-x / root.cell-w));
|
||
} else {
|
||
// Wheel scrolls local scrollback (~3 lines per
|
||
// notch). Mouse-tracking apps such as tmux own
|
||
// their history, so wheel input is forwarded.
|
||
root.terminal-scroll(ev.delta-y > 0 ? 3 : -3);
|
||
flickable.viewport-y = clamp(
|
||
flickable.viewport-y + (ev.delta-y > 0 ? 3 * root.cell-h : -3 * root.cell-h),
|
||
0px,
|
||
max(0px, flickable.viewport-height - flickable.height)
|
||
);
|
||
}
|
||
accept
|
||
}
|
||
}
|
||
|
||
// Drag-selection highlights (under the text, blue tint).
|
||
for s in root.selection : Rectangle {
|
||
x: s.col * root.cell-w;
|
||
y: s.row * root.cell-h;
|
||
width: s.len * root.cell-w;
|
||
height: root.cell-h;
|
||
background: Theme.accent.with-alpha(0.40);
|
||
}
|
||
|
||
// Search-match highlights (behind the text).
|
||
for m in root.find-matches : Rectangle {
|
||
x: m.col * root.cell-w;
|
||
y: m.row * root.cell-h;
|
||
width: m.len * root.cell-w;
|
||
height: root.cell-h;
|
||
background: Theme.warning.with-alpha(0.45);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
// Each span = a background fill (transparent for the
|
||
// default bg) with its text on top. Wrapping the Text in
|
||
// the bg Rectangle keeps one element per span and renders
|
||
// btop/htop/mc panels, meter bars and graph fills.
|
||
for span in root.spans : Rectangle {
|
||
x: span.col * root.cell-w;
|
||
y: span.row * root.cell-h;
|
||
width: span.cells * root.cell-w;
|
||
height: root.cell-h;
|
||
background: span.bg;
|
||
// Keep text constrained to the exact terminal cells
|
||
// it occupies. Letting Text auto-size can overpaint
|
||
// the following absolute-positioned cells when the
|
||
// font fallback/wide-glyph metrics differ slightly.
|
||
Text {
|
||
x: 0px;
|
||
y: 0px;
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: span.text;
|
||
color: span.fg;
|
||
font-family: span.use-fallback-font ? Theme.font-ui : "Cascadia Mono";
|
||
font-size: root.terminal-font-size;
|
||
font-weight: span.bold ? 700 : 400;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
|
||
// Blinking block cursor (overlay, decoupled from text).
|
||
Rectangle {
|
||
x: root.cursor-col * root.cell-w;
|
||
y: root.cursor-row * root.cell-h;
|
||
width: root.cell-w;
|
||
height: root.cell-h;
|
||
background: Theme.term-fg;
|
||
visible: ime-input.has-focus && key-capture.cursor-blink;
|
||
}
|
||
|
||
// --- Right-click context menu ---------------------
|
||
ctx-menu := PopupWindow {
|
||
x: root.ctx-x;
|
||
y: root.ctx-y;
|
||
width: 132px;
|
||
height: 132px;
|
||
Rectangle {
|
||
background: Theme.bg-panel;
|
||
border-radius: Theme.radius-sm;
|
||
border-width: 1px;
|
||
border-color: Theme.border-strong;
|
||
drop-shadow-blur: 12px;
|
||
drop-shadow-color: #00000060;
|
||
VerticalLayout {
|
||
padding: 4px;
|
||
spacing: 1px;
|
||
MenuItem {
|
||
icon: "\u{E14D}"; label: @tr("Copy");
|
||
clicked => {
|
||
if root.click-debug-enabled { root.debug-click("terminal.context-menu", "copy"); }
|
||
root.copy-terminal-text();
|
||
ime-input.focus();
|
||
}
|
||
}
|
||
MenuItem {
|
||
icon: "\u{E14F}"; label: @tr("Paste");
|
||
clicked => {
|
||
if root.click-debug-enabled { root.debug-click("terminal.context-menu", "paste"); }
|
||
root.paste-from-clipboard();
|
||
ime-input.focus();
|
||
}
|
||
}
|
||
MenuItem {
|
||
icon: "\u{E872}"; label: @tr("Clear buffer");
|
||
clicked => {
|
||
if root.click-debug-enabled { root.debug-click("terminal.context-menu", "clear-buffer"); }
|
||
root.clear-terminal();
|
||
ime-input.focus();
|
||
}
|
||
}
|
||
MenuItem {
|
||
icon: "\u{E8B6}"; label: @tr("Find");
|
||
clicked => {
|
||
if root.click-debug-enabled { root.debug-click("terminal.context-menu", "find"); }
|
||
root.find-active = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// --- Hidden TextInput — keyboard + Chinese IME capture -----------
|
||
//
|
||
// Key design decision: return `accept` for EVERY key and forward
|
||
// them all via send-key. Returning `reject` makes Slint bubble the
|
||
// event without inserting the character into `text`, so `edited`
|
||
// never fires for regular keystrokes — only IME-committed chars
|
||
// (which go through a separate WM_IME_CHAR path) reach `edited`.
|
||
//
|
||
// By forwarding everything in key-pressed we guarantee English and
|
||
// all direct-mode input reaches the PTY. `edited` is kept as a
|
||
// safety net for IME commits that Windows delivers via WM_IME_CHAR
|
||
// rather than WM_CHAR (and therefore don't fire key-pressed).
|
||
ime-input := TextInput {
|
||
x: 0;
|
||
y: 0;
|
||
width: key-capture.width;
|
||
height: 1px;
|
||
opacity: 0;
|
||
single-line: true;
|
||
|
||
// Reset blink phase on focus gain so cursor appears immediately.
|
||
changed has-focus => {
|
||
if (self.has-focus) { key-capture.cursor-blink = true; }
|
||
}
|
||
|
||
key-pressed(event) => {
|
||
// ── Ctrl+Shift+C: copy terminal buffer ───────────────────
|
||
if (event.modifiers.control && event.modifiers.shift
|
||
&& (event.text == "c" || event.text == "C"
|
||
|| event.text == "\u{0003}")) {
|
||
root.copy-terminal-text();
|
||
accept
|
||
// ── Ctrl+V or Ctrl+Shift+V: paste from clipboard ─────────
|
||
// We accept Ctrl+V (with or without Shift) so the familiar
|
||
// Windows paste shortcut works in addition to Ctrl+Shift+V.
|
||
} else if (event.modifiers.control
|
||
&& (event.text == "v" || event.text == "V"
|
||
|| event.text == "\u{0016}")) {
|
||
root.paste-from-clipboard();
|
||
// Clipboard access on Windows can deliver WM_KILLFOCUS.
|
||
// Restore focus explicitly so the cursor keeps blinking.
|
||
ime-input.focus();
|
||
accept
|
||
// ── All other keys (printable, Ctrl+X, arrows, …) ────────
|
||
// Return `accept` for everything: `reject` bubbles without
|
||
// inserting the char into TextInput, so `edited` never fires
|
||
// for regular keystrokes — the character would be lost.
|
||
} else {
|
||
root.send-key(event.text, event.modifiers.control,
|
||
event.modifiers.alt, event.modifiers.shift);
|
||
accept
|
||
}
|
||
}
|
||
|
||
// Safety-net for IME commits delivered via WM_IME_CHAR (which
|
||
// may not fire key-pressed). Guard with preedit-text == "" to
|
||
// skip in-progress compositions.
|
||
edited => {
|
||
if (self.preedit-text == "" && self.text != "") {
|
||
root.send-key(self.text, false, false, false);
|
||
self.text = "";
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Find bar (overlay, top) ------------------------------------
|
||
if root.find-active : Rectangle {
|
||
x: 0;
|
||
y: 0;
|
||
width: parent.width;
|
||
height: 34px;
|
||
background: Theme.bg-panel;
|
||
border-width: 1px;
|
||
border-color: Theme.border-strong;
|
||
border-radius: Theme.radius-sm;
|
||
|
||
HorizontalLayout {
|
||
height: parent.height;
|
||
padding: 6px;
|
||
spacing: 6px;
|
||
Text {
|
||
y: (parent.height - self.height) / 2;
|
||
width: 18px;
|
||
height: 22px;
|
||
text: "\u{E8B6}";
|
||
font-family: "Material Icons";
|
||
color: Theme.text-secondary;
|
||
font-size: Theme.fs-sm;
|
||
vertical-alignment: center;
|
||
}
|
||
Rectangle {
|
||
y: (parent.height - self.height) / 2;
|
||
height: 22px;
|
||
background: Theme.bg-root;
|
||
border-radius: 3px;
|
||
horizontal-stretch: 1;
|
||
find-input := TextInput {
|
||
x: 6px;
|
||
width: parent.width - 12px;
|
||
height: parent.height;
|
||
color: Theme.text-primary;
|
||
font-size: Theme.fs-md;
|
||
vertical-alignment: center;
|
||
single-line: true;
|
||
// Grab focus as soon as the bar appears.
|
||
init => { self.focus(); }
|
||
edited => { root.find-query-changed(self.text); }
|
||
key-pressed(e) => {
|
||
if (e.text == "\u{001b}") { // Esc closes find
|
||
root.find-active = false;
|
||
root.find-query-changed("");
|
||
self.text = "";
|
||
ime-input.focus();
|
||
accept
|
||
} else {
|
||
reject
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Rectangle {
|
||
y: (parent.height - self.height) / 2;
|
||
width: 24px;
|
||
height: 22px;
|
||
close-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => {
|
||
root.find-active = false;
|
||
root.find-query-changed("");
|
||
find-input.text = "";
|
||
ime-input.focus();
|
||
}
|
||
}
|
||
Text {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
text: "\u{E5CD}";
|
||
font-family: "Material Icons";
|
||
color: close-ta.has-hover ? Theme.text-primary : Theme.text-secondary;
|
||
font-size: Theme.fs-sm;
|
||
horizontal-alignment: center;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Drag handle ----------------------------------------------------
|
||
Rectangle {
|
||
height: 4px;
|
||
background: divider-ta.has-hover || root.sftp-dragging
|
||
? Theme.accent
|
||
: Theme.border-subtle;
|
||
|
||
divider-ta := TouchArea {
|
||
mouse-cursor: row-resize;
|
||
|
||
pointer-event(ev) => {
|
||
if (ev.kind == PointerEventKind.down) {
|
||
root.sftp-dragging = true;
|
||
root.drag-start-mouse-y = self.mouse-y;
|
||
} else if (ev.kind == PointerEventKind.up) {
|
||
root.sftp-dragging = false;
|
||
}
|
||
}
|
||
|
||
moved => {
|
||
if (root.sftp-dragging) {
|
||
root.sftp-panel-height = clamp(
|
||
root.sftp-panel-height + root.drag-start-mouse-y - self.mouse-y,
|
||
60px,
|
||
root.height - 24px - 4px - 16px - 80px
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Bottom tools: files / commands --------------------------------
|
||
Rectangle {
|
||
height: root.sftp-panel-height;
|
||
background: root.app-background-active ? Theme.bg-panel.with-alpha(root.ui-opacity) : Theme.bg-panel;
|
||
clip: true;
|
||
|
||
VerticalLayout {
|
||
spacing: 0;
|
||
|
||
Rectangle {
|
||
height: 30px;
|
||
background: root.app-background-active ? Theme.bg-panel-alt.with-alpha(root.ui-opacity) : Theme.bg-panel-alt;
|
||
border-width: 1px;
|
||
border-color: Theme.border-subtle;
|
||
|
||
HorizontalLayout {
|
||
height: parent.height;
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
spacing: 4px;
|
||
|
||
Rectangle {
|
||
y: (parent.height - self.height) / 2;
|
||
width: 96px;
|
||
height: 24px;
|
||
border-radius: Theme.radius-sm;
|
||
background: root.tool-tab == 0
|
||
? Theme.bg-active
|
||
: (files-tab-ta.has-hover ? Theme.bg-hover : transparent);
|
||
files-tab-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => {
|
||
if root.click-debug-enabled { root.debug-click("terminal.bottom-tools", "files-tab"); }
|
||
root.tool-tab = 0;
|
||
}
|
||
}
|
||
HorizontalLayout {
|
||
height: parent.height;
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
spacing: 6px;
|
||
Text {
|
||
height: parent.height;
|
||
text: "\u{E2C7}";
|
||
font-family: "Material Icons";
|
||
color: root.tool-tab == 0 ? Theme.text-primary : Theme.text-secondary;
|
||
font-size: Theme.fs-sm;
|
||
vertical-alignment: center;
|
||
}
|
||
Text {
|
||
height: parent.height;
|
||
text: @tr("File management");
|
||
color: root.tool-tab == 0 ? Theme.text-primary : Theme.text-secondary;
|
||
font-size: Theme.fs-sm;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
y: (parent.height - self.height) / 2;
|
||
width: 96px;
|
||
height: 24px;
|
||
border-radius: Theme.radius-sm;
|
||
background: root.tool-tab == 1
|
||
? Theme.bg-active
|
||
: (commands-tab-ta.has-hover ? Theme.bg-hover : transparent);
|
||
commands-tab-ta := TouchArea {
|
||
mouse-cursor: pointer;
|
||
clicked => {
|
||
if root.click-debug-enabled { root.debug-click("terminal.bottom-tools", "commands-tab"); }
|
||
root.tool-tab = 1;
|
||
}
|
||
}
|
||
HorizontalLayout {
|
||
height: parent.height;
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
spacing: 6px;
|
||
Text {
|
||
height: parent.height;
|
||
text: "\u{E8F4}";
|
||
font-family: "Material Icons";
|
||
color: root.tool-tab == 1 ? Theme.text-primary : Theme.text-secondary;
|
||
font-size: Theme.fs-sm;
|
||
vertical-alignment: center;
|
||
}
|
||
Text {
|
||
height: parent.height;
|
||
text: @tr("Command management");
|
||
color: root.tool-tab == 1 ? Theme.text-primary : Theme.text-secondary;
|
||
font-size: Theme.fs-sm;
|
||
vertical-alignment: center;
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle { horizontal-stretch: 1; }
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
vertical-stretch: 1;
|
||
background: root.app-background-active ? Theme.bg-panel.with-alpha(max(0.30, root.ui-opacity - 0.08)) : Theme.bg-panel;
|
||
clip: true;
|
||
|
||
if root.tool-tab == 0 : SftpPanel {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
current-path: root.sftp-path;
|
||
entries: root.sftp-entries;
|
||
status: root.sftp-status;
|
||
loading: root.sftp-loading;
|
||
tree-nodes: root.sftp-tree-nodes;
|
||
app-background-active: root.app-background-active;
|
||
ui-opacity: root.ui-opacity;
|
||
file-font-size: root.sftp-file-font-size;
|
||
name-column-width <=> root.sftp-name-column-width;
|
||
name-column-manual <=> root.sftp-name-width-manual;
|
||
navigate(path) => { root.sftp-navigate(path); }
|
||
download(path) => { root.sftp-download(path); }
|
||
upload-clicked(dir) => { root.sftp-upload-clicked(dir); }
|
||
refresh(path) => { root.sftp-refresh(path); }
|
||
tree-expand(path) => { root.sftp-tree-expand(path); }
|
||
delete(path) => { root.sftp-delete(path); }
|
||
rename(path, name) => { root.sftp-rename(path, name); }
|
||
edit(path) => { root.sftp-edit(path); }
|
||
name-column-width-changed(px) => { root.sftp-name-column-width-changed(px); }
|
||
name-column-auto() => { root.sftp-name-column-auto(); }
|
||
}
|
||
|
||
if root.tool-tab == 1 : CommandManager {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
command-categories: root.command-categories;
|
||
command-items: root.command-items;
|
||
selected-category <=> root.command-selected-category;
|
||
selected-command <=> root.command-selected-id;
|
||
category-draft <=> root.command-category-draft;
|
||
edit-id <=> root.command-edit-id;
|
||
edit-name <=> root.command-edit-name;
|
||
edit-command <=> root.command-edit-command;
|
||
edit-append-cr <=> root.command-edit-append-cr;
|
||
param1-name <=> root.command-param1-name;
|
||
param1-value <=> root.command-param1-value;
|
||
param2-name <=> root.command-param2-name;
|
||
param2-value <=> root.command-param2-value;
|
||
param3-name <=> root.command-param3-name;
|
||
param3-value <=> root.command-param3-value;
|
||
param4-name <=> root.command-param4-name;
|
||
param4-value <=> root.command-param4-value;
|
||
param5-name <=> root.command-param5-name;
|
||
param5-value <=> root.command-param5-value;
|
||
editor-open <=> root.command-editor-open;
|
||
category-editor-open <=> root.command-category-editor-open;
|
||
category-edit-id <=> root.command-category-edit-id;
|
||
category-can-delete <=> root.command-category-can-delete;
|
||
category-select(id) => { root.command-category-select(id); }
|
||
category-add(name) => { root.command-category-add(name); }
|
||
category-rename(id, name) => { root.command-category-rename(id, name); }
|
||
category-delete(id) => { root.command-category-delete(id); }
|
||
command-new(category) => { root.command-new(category); }
|
||
command-select(id) => { root.command-select(id); }
|
||
command-save(id, category, name, command, append_cr) => {
|
||
root.command-save(id, category, name, command, append_cr);
|
||
}
|
||
command-delete(id) => { root.command-delete(id); }
|
||
command-run(id, p1, p2, p3, p4, p5) => {
|
||
root.command-run(id, p1, p2, p3, p4, p5);
|
||
ime-input.focus();
|
||
}
|
||
command-move(id, category) => { root.command-move(id, category); }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
CommandEditorModal {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
is-open <=> root.command-editor-open;
|
||
command-categories: root.command-categories;
|
||
selected-category <=> root.command-selected-category;
|
||
edit-id <=> root.command-edit-id;
|
||
edit-name <=> root.command-edit-name;
|
||
edit-command <=> root.command-edit-command;
|
||
edit-append-cr <=> root.command-edit-append-cr;
|
||
command-save(id, category, name, command, append_cr) => {
|
||
root.command-save(id, category, name, command, append_cr);
|
||
}
|
||
command-delete(id) => { root.command-delete(id); }
|
||
command-move(id, category) => { root.command-move(id, category); }
|
||
}
|
||
|
||
CategoryEditorModal {
|
||
width: parent.width;
|
||
height: parent.height;
|
||
is-open <=> root.command-category-editor-open;
|
||
edit-id <=> root.command-category-edit-id;
|
||
draft <=> root.command-category-draft;
|
||
can-delete: root.command-category-can-delete;
|
||
category-add(name) => { root.command-category-add(name); }
|
||
category-rename(id, name) => { root.command-category-rename(id, name); }
|
||
category-delete(id) => { root.command-category-delete(id); }
|
||
}
|
||
}
|