Evan's Blog
EvanZhu0721 / 公开仓库

SBMS

镜像已同步

SBMS complete the calculation rules for the default logical desktop topology relationship and the physical dimensions of the displays in the Windows multi-screen collaboration system.

上游 GitHub
READ-ONLY MIRROR

README、源码与 Release 已保存到本站;源码视图固定到 commit bd87fa1e。

main/ root / ui/pages
bd87fa1e· 固定源码快照
ui/pages/main.slint54.1 KB
1import { Button, LineEdit, ProgressIndicator, Switch } from "std-widgets.slint";
2import {
3 CompactComboBox,
4 DialogActionButton,
5 FullWidthSettingsButton,
6 FullWidthPrimaryButton,
7 MaterialIconButton,
8} from "../controls.slint";
9import { SevenSegmentDisplay } from "../seven-segment.slint";
10import { MdTokens } from "../theme.slint";
11
12component ProfileMenuRow inherits Rectangle {
13 in property <string> name;
14 in property <bool> selected: false;
15 in property <bool> control-enabled: true;
16 callback picked();
17 callback rename();
18 callback remove();
19
20 height: MdTokens.target;
21 background: root.selected
22 ? MdTokens.primary-container
23 : (pick-touch.has-hover ? MdTokens.surface-container : transparent);
24 border-radius: MdTokens.space-1;
25 animate background { duration: 100ms; easing: ease-in-out; }
26 opacity: root.control-enabled ? 1 : 0.38;
27
28 if root.selected : Path {
29 x: 12px;
30 y: 14px;
31 width: 20px;
32 height: 20px;
33 viewbox-width: 24;
34 viewbox-height: 24;
35 commands: "M9 16.17L4.83 12L3.41 13.41L9 19L21 7L19.59 5.59L9 16.17Z";
36 fill: MdTokens.on-primary-container;
37 accessible-role: none;
38 }
39
40 Text {
41 x: 40px;
42 width: parent.width - 128px;
43 height: parent.height;
44 text: root.name;
45 color: root.selected ? MdTokens.on-primary-container : MdTokens.on-surface;
46 font-size: MdTokens.type-body-large;
47 font-weight: root.selected ? MdTokens.weight-medium : 400;
48 vertical-alignment: center;
49 overflow: elide;
50 accessible-role: none;
51 }
52
53 pick-touch := TouchArea {
54 enabled: root.control-enabled;
55 x: 0;
56 width: parent.width - 88px;
57 accessible-role: button;
58 accessible-label: root.selected
59 ? "Current profile " + root.name
60 : "Switch to profile " + root.name;
61 clicked => { root.picked(); }
62 }
63
64 Rectangle {
65 x: parent.width - 88px;
66 width: 44px;
67 height: parent.height;
68 background: transparent;
69
70 Rectangle {
71 width: parent.width;
72 height: parent.height;
73 background: MdTokens.on-surface;
74 opacity: rename-touch.pressed ? 0.12 : (rename-touch.has-hover ? 0.08 : 0);
75 border-radius: MdTokens.shape-full;
76 animate opacity { duration: 100ms; easing: ease-in-out; }
77 }
78
79 Path {
80 x: 12px;
81 y: 14px;
82 width: 20px;
83 height: 20px;
84 viewbox-width: 24;
85 viewbox-height: 24;
86 commands: "M3 17.25V21H6.75L17.81 9.94L14.06 6.19L3 17.25ZM20.71 7.04C21.1 6.65 21.1 6.02 20.71 5.63L18.37 3.29C17.98 2.9 17.35 2.9 16.96 3.29L15.13 5.12L18.88 8.87L20.71 7.04Z";
87 fill: root.selected ? MdTokens.on-primary-container : MdTokens.on-surface-variant;
88 accessible-role: none;
89 }
90
91 rename-touch := TouchArea {
92 enabled: root.control-enabled;
93 accessible-role: button;
94 accessible-label: "Rename profile " + root.name;
95 clicked => { root.rename(); }
96 }
97 }
98
99 Rectangle {
100 x: parent.width - 44px;
101 width: 44px;
102 height: parent.height;
103 background: transparent;
104
105 Rectangle {
106 width: parent.width;
107 height: parent.height;
108 background: MdTokens.error;
109 opacity: remove-touch.pressed ? 0.12 : (remove-touch.has-hover ? 0.08 : 0);
110 border-radius: MdTokens.shape-full;
111 animate opacity { duration: 100ms; easing: ease-in-out; }
112 }
113
114 Path {
115 x: 12px;
116 y: 14px;
117 width: 20px;
118 height: 20px;
119 viewbox-width: 24;
120 viewbox-height: 24;
121 commands: "M6 19C6 20.1 6.9 21 8 21H16C17.1 21 18 20.1 18 19V7H6V19ZM8 9H16V19H8V9ZM15.5 4L14.5 3H9.5L8.5 4H5V6H19V4H15.5Z";
122 fill: MdTokens.error;
123 accessible-role: none;
124 }
125
126 remove-touch := TouchArea {
127 enabled: root.control-enabled;
128 accessible-role: button;
129 accessible-label: "Delete profile " + root.name;
130 clicked => { root.remove(); }
131 }
132 }
133}
134
135component ProfileMenuAction inherits Rectangle {
136 in property <string> text;
137 in property <int> icon-kind: 0; // 0 add, 1 folder
138 in property <bool> control-enabled: true;
139 callback clicked();
140
141 height: MdTokens.target;
142 background: action-touch.has-hover && root.control-enabled
143 ? MdTokens.surface-container : transparent;
144 border-radius: MdTokens.space-1;
145 opacity: root.control-enabled ? 1 : 0.38;
146 animate background { duration: 100ms; easing: ease-in-out; }
147
148 if root.icon-kind == 0 : Path {
149 x: 12px;
150 y: 14px;
151 width: 20px;
152 height: 20px;
153 viewbox-width: 24;
154 viewbox-height: 24;
155 commands: "M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z";
156 fill: MdTokens.primary;
157 accessible-role: none;
158 }
159
160 if root.icon-kind == 1 : Path {
161 x: 12px;
162 y: 14px;
163 width: 20px;
164 height: 20px;
165 viewbox-width: 24;
166 viewbox-height: 24;
167 commands: "M10 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V8C22 6.9 21.1 6 20 6H12L10 4Z";
168 fill: MdTokens.on-surface-variant;
169 accessible-role: none;
170 }
171
172 Text {
173 x: 44px;
174 width: parent.width - 56px;
175 height: parent.height;
176 text: root.text;
177 color: root.icon-kind == 0 ? MdTokens.primary : MdTokens.on-surface;
178 font-size: MdTokens.type-body-medium;
179 font-weight: MdTokens.weight-medium;
180 vertical-alignment: center;
181 accessible-role: none;
182 }
183
184 action-touch := TouchArea {
185 enabled: root.control-enabled;
186 accessible-role: button;
187 accessible-label: root.text;
188 clicked => { root.clicked(); }
189 }
190}
191
192component ProfileEntryButton inherits FocusScope {
193 in property <bool> expanded: false;
194 in property <string> current-name;
195 callback clicked();
196
197 width: 144px;
198 height: 64px;
199
200 Rectangle {
201 width: root.width;
202 height: root.height;
203 background: transparent;
204
205 Rectangle {
206 x: 0px;
207 y: 0px;
208 width: root.width;
209 height: root.height;
210 background: root.expanded ? MdTokens.primary-container : MdTokens.surface-container;
211 border-width: root.expanded ? 2px : 0px;
212 border-color: root.expanded ? MdTokens.primary : MdTokens.outline-variant;
213 border-radius: MdTokens.shape-medium;
214 animate background, border-color { duration: 150ms; easing: ease-in-out; }
215
216 Rectangle {
217 width: parent.width;
218 height: parent.height;
219 background: MdTokens.primary;
220 border-radius: parent.border-radius;
221 opacity: entry-touch.pressed ? 0.12 : (entry-touch.has-hover ? 0.08 : 0);
222 animate opacity { duration: 100ms; easing: ease-in-out; }
223 }
224
225 Path {
226 x: parent.width - self.width - 12px;
227 y: (parent.height - self.height) / 2;
228 width: 24px;
229 height: 24px;
230 viewbox-width: 24;
231 viewbox-height: 24;
232 commands: "M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2ZM13 3.5L18.5 9H13V3.5ZM16 18H8V16H16V18ZM16 14H8V12H16V14Z";
233 fill: root.expanded ? MdTokens.on-primary-container : MdTokens.on-surface;
234 accessible-role: none;
235 }
236
237 VerticalLayout {
238 x: 12px;
239 width: parent.width - 60px;
240 height: parent.height;
241 alignment: center;
242 spacing: 3px;
243
244 Text {
245 text: "Profile";
246 color: root.expanded ? MdTokens.on-primary-container : MdTokens.on-surface;
247 font-size: MdTokens.type-title-medium;
248 font-weight: MdTokens.weight-medium;
249 horizontal-alignment: left;
250 overflow: elide;
251 accessible-role: none;
252 }
253
254 Text {
255 text: root.current-name;
256 color: root.expanded ? MdTokens.on-primary-container : MdTokens.on-surface-variant;
257 font-size: MdTokens.type-body-small;
258 horizontal-alignment: left;
259 overflow: elide;
260 accessible-role: none;
261 }
262 }
263 }
264
265 entry-touch := TouchArea {
266 accessible-role: button;
267 accessible-label: "Configuration profiles, current " + root.current-name;
268 clicked => {
269 root.focus();
270 root.clicked();
271 }
272 }
273 }
274
275 key-pressed(event) => {
276 if event.text == Key.Return {
277 root.clicked();
278 return accept;
279 }
280 return reject;
281 }
282}
283
284export component MainPage inherits FocusScope {
285 in-out property <[string]> display-names;
286 in-out property <[string]> display-labels;
287 in-out property <[string]> display-ids;
288 in-out property <int> selected-display: -1;
289 in property <bool> stream-only: false;
290 in-out property <[string]> tab-labels;
291 in-out property <[string]> tab-details;
292 in-out property <[int]> tab-indices;
293 in property <int> active-group-index: 0;
294 in property <int> group-count: 1;
295 in property <int> max-group-count: 1;
296 in-out property <int> pending-remove-index: -1;
297 in-out property <[string]> reference-names;
298 in-out property <[string]> reference-labels;
299 in-out property <[string]> reference-ids;
300 in-out property <int> selected-reference: -1;
301 in property <bool> reference-manual: false;
302 in property <bool> reference-action-enabled: false;
303 in-out property <int> sizing-strategy: 0;
304 in property <string> state: "Stopped";
305 in property <string> state-detail: "Choose a target screen to begin";
306 in property <bool> running: false;
307 in property <bool> busy: false;
308 in property <bool> restart-available: false;
309 in property <string> error-text: "";
310 in property <int> mapping-fps: 0;
311 in property <bool> mapping-fps-valid: false;
312 in property <bool> mapping-fps-error: false;
313 in property <bool> mapping-fps-nan: false;
314 in property <bool> geometry-configured: false;
315 in property <string> geometry-summary: "3840 × 2160";
316 in property <string> geometry-summary-detail: "Driver · 240 Hz";
317 in property <bool> sunshine-panel-enabled: false;
318 in property <int> sunshine-state: 0;
319 in property <int> sunshine-port: 0;
320 in property <string> sunshine-lan-ip: "--";
321
322 in-out property <bool> profile-menu-open: false;
323 in property <[string]> profile-names;
324 in property <string> active-profile-name: "default";
325 in property <bool> profile-saving: false;
326 in property <bool> auto-map-on-login: false;
327 in property <int> max-profile-count: 3;
328 in-out property <int> profile-dialog-mode: 0; // 1 new, 2 rename, 3 delete, 4 switch
329 in-out property <string> profile-dialog-target-name: "";
330 in-out property <string> profile-name-draft: "";
331 in-out property <string> profile-name-error: "";
332 property <string> dialog-profile-name: root.profile-dialog-target-name;
333
334 callback start();
335 callback stop();
336 callback restart-mapping();
337 callback display-selected();
338 callback reference-selected();
339 callback strategy-selected();
340 callback calculate-geometry();
341 callback open-manual-geometry();
342 callback dismiss();
343 callback open-diagnostics();
344 callback tab-selected(int);
345 callback tab-wheel(int);
346 callback add-group();
347 callback open-stream-config();
348 callback open-sunshine-panel();
349 callback open-display-settings();
350 callback open-screen-size();
351 callback open-profile-directory();
352 callback set-auto-map-on-login(bool);
353 callback create-profile(string);
354 callback rename-profile(string, string);
355 callback delete-profile(string);
356 callback activate-profile(string);
357
358 key-pressed(event) => {
359 if event.text == Key.Escape {
360 if root.profile-dialog-mode != 0 {
361 root.profile-dialog-mode = 0;
362 root.profile-name-error = "";
363 return accept;
364 }
365 if root.profile-menu-open {
366 root.profile-menu-open = false;
367 return accept;
368 }
369 root.dismiss();
370 return accept;
371 }
372 return reject;
373 }
374
375 Rectangle {
376 x: 0;
377 y: 0;
378 width: parent.width;
379 height: parent.height;
380 background: MdTokens.surface;
381
382 VerticalLayout {
383 padding: MdTokens.space-2;
384 spacing: MdTokens.space-1;
385
386 HorizontalLayout {
387 height: 40px;
388 spacing: MdTokens.space-1;
389
390 tabs-viewport := Rectangle {
391 horizontal-stretch: 1;
392 clip: true;
393 background: transparent;
394
395 HorizontalLayout {
396 spacing: 4px;
397
398 for tab[index] in root.tab-labels : Rectangle {
399 width: 96px;
400 background: root.tab-indices[index] == root.active-group-index
401 ? MdTokens.primary-container
402 : MdTokens.surface-container;
403 border-radius: 12px;
404
405 tab-touch := TouchArea {
406 enabled: !root.busy;
407 accessible-role: button;
408 accessible-label: "Open mapping group " + tab + ", " + root.tab-details[index];
409 clicked => { root.tab-selected(root.tab-indices[index]); }
410 scroll-event(event) => {
411 if event.delta-y < -1px || event.delta-x < -1px {
412 root.tab-wheel(1);
413 return accept;
414 }
415 if event.delta-y > 1px || event.delta-x > 1px {
416 root.tab-wheel(-1);
417 return accept;
418 }
419 return reject;
420 }
421 }
422
423 Rectangle {
424 x: 4px;
425 y: 6px;
426 width: 28px;
427 height: 28px;
428 border-radius: 10px;
429 background: MdTokens.primary;
430
431 Text {
432 width: parent.width;
433 height: parent.height;
434 text: tab;
435 color: MdTokens.on-primary;
436 font-family: "Bahnschrift";
437 font-size: 16px;
438 font-weight: 600;
439 horizontal-alignment: center;
440 vertical-alignment: center;
441 accessible-role: none;
442 }
443 }
444
445 Text {
446 x: 36px;
447 width: 30px;
448 height: parent.height;
449 color: root.tab-indices[index] == root.active-group-index
450 ? MdTokens.on-primary-container
451 : MdTokens.on-surface-variant;
452 font-family: "Bahnschrift";
453 font-size: 11px;
454 font-weight: 600;
455 horizontal-alignment: center;
456 vertical-alignment: center;
457 text: root.tab-details[index];
458 overflow: elide;
459 accessible-role: none;
460 }
461
462 Rectangle {
463 x: 68px;
464 y: 0px;
465 width: 28px;
466 height: parent.height;
467 background: transparent;
468 opacity: root.group-count <= 1 ? 0
469 : (!root.running && !root.busy ? 1 : 0.38);
470
471 Rectangle {
472 x: (parent.width - self.width) / 2;
473 y: (parent.height - self.height) / 2;
474 width: 28px;
475 height: 28px;
476 border-radius: MdTokens.shape-full;
477 background: root.tab-indices[index] == root.active-group-index
478 ? MdTokens.on-primary-container : MdTokens.on-surface-variant;
479 opacity: close-touch.pressed
480 ? 0.12 : (close-touch.has-hover ? 0.08 : 0);
481 animate opacity { duration: 100ms; easing: ease-in-out; }
482 }
483
484 Path {
485 x: (parent.width - self.width) / 2;
486 y: (parent.height - self.height) / 2;
487 width: 18px;
488 height: 18px;
489 viewbox-width: 24;
490 viewbox-height: 24;
491 commands: "M18.3 5.71L12 12L5.7 5.71L4.29 7.12L10.59 13.41L4.29 19.71L5.7 21.12L12 14.83L18.3 21.12L19.71 19.71L13.41 13.41L19.71 7.12L18.3 5.71Z";
492 fill: root.tab-indices[index] == root.active-group-index
493 ? MdTokens.on-primary-container : MdTokens.on-surface-variant;
494 accessible-role: none;
495 }
496
497 close-touch := TouchArea {
498 enabled: root.group-count > 1 && !root.running && !root.busy;
499 accessible-role: button;
500 accessible-label: "Remove mapping group " + tab;
501 clicked => {
502 if root.group-count > 1 {
503 root.pending-remove-index = root.tab-indices[index];
504 }
505 }
506 }
507 }
508 }
509 }
510 }
511
512 MaterialIconButton {
513 icon-kind: 0;
514 compact: true;
515 control-enabled: !root.running && !root.busy && root.group-count < root.max-group-count;
516 field-label: "Add mapping group";
517 clicked => { root.add-group(); }
518 }
519
520 }
521
522 HorizontalLayout {
523 height: MdTokens.target;
524 spacing: MdTokens.space-1;
525
526 VerticalLayout {
527 horizontal-stretch: 1;
528 alignment: center;
529 spacing: 2px;
530
531 Text {
532 text: "SBMS";
533 color: MdTokens.on-surface;
534 font-size: MdTokens.type-title-large;
535 font-weight: MdTokens.weight-medium;
536 }
537
538 Text {
539 text: "Display mapping";
540 color: MdTokens.on-surface-variant;
541 font-size: MdTokens.type-body-small;
542 }
543 }
544
545 SevenSegmentDisplay {
546 fps: root.mapping-fps;
547 valid: root.mapping-fps-valid;
548 running: root.running;
549 error: root.mapping-fps-error;
550 not-a-number: root.mapping-fps-nan;
551 }
552 }
553
554 Rectangle {
555 height: root.error-text != "" || root.restart-available ? 80px : 64px;
556 background: root.error-text != ""
557 ? MdTokens.error-container
558 : (root.running ? MdTokens.primary-container : MdTokens.surface-container);
559 border-radius: MdTokens.shape-medium;
560
561 HorizontalLayout {
562 padding: MdTokens.space-2;
563 spacing: 12px;
564
565 Rectangle {
566 width: 12px;
567 background: transparent;
568 Rectangle {
569 y: (parent.height - self.height) / 2;
570 width: 12px;
571 height: 12px;
572 border-radius: MdTokens.shape-full;
573 background: root.error-text != ""
574 ? MdTokens.on-error-container
575 : (root.running ? MdTokens.primary : MdTokens.outline-variant);
576 }
577 }
578
579 VerticalLayout {
580 horizontal-stretch: 1;
581 alignment: center;
582 spacing: 3px;
583 Text {
584 text: root.error-text != ""
585 ? "Action failed"
586 : (root.state == "Reconnecting" ? "正在重连" : root.state);
587 color: root.error-text != ""
588 ? MdTokens.on-error-container
589 : (root.running ? MdTokens.on-primary-container : MdTokens.on-surface);
590 font-size: MdTokens.type-title-medium;
591 font-weight: MdTokens.weight-medium;
592 }
593 Text {
594 text: root.state-detail;
595 color: root.error-text != ""
596 ? MdTokens.on-error-container
597 : (root.running ? MdTokens.on-primary-container : MdTokens.on-surface-variant);
598 font-size: MdTokens.type-body-small;
599 overflow: elide;
600 }
601 }
602
603 if root.error-text == "" : VerticalLayout {
604 width: root.stream-only ? 176px : 132px;
605 alignment: center;
606 spacing: 3px;
607
608 Text {
609 text: root.stream-only ? "LAN endpoint" : root.geometry-summary;
610 color: root.running ? MdTokens.on-primary-container : MdTokens.on-surface;
611 font-size: MdTokens.type-title-medium;
612 font-weight: MdTokens.weight-medium;
613 horizontal-alignment: right;
614 overflow: elide;
615 }
616
617 Text {
618 text: root.stream-only
619 ? (root.sunshine-state == 2 && root.sunshine-port > 0
620 ? (root.sunshine-lan-ip == "--"
621 ? "LAN unavailable"
622 : root.sunshine-lan-ip + ":" + root.sunshine-port)
623 : (root.sunshine-state == 1
624 ? "Starting"
625 : (root.sunshine-state == 3 ? "Unavailable" : "--")))
626 : root.geometry-summary-detail;
627 color: root.running
628 ? MdTokens.on-primary-container
629 : (root.stream-only && root.sunshine-state == 3
630 ? MdTokens.error
631 : MdTokens.on-surface-variant);
632 font-size: MdTokens.type-body-small;
633 font-weight: root.stream-only && root.sunshine-state == 2
634 ? MdTokens.weight-medium : 400;
635 horizontal-alignment: right;
636 overflow: elide;
637 }
638 }
639
640 if root.restart-available : MaterialIconButton {
641 icon-kind: 1;
642 field-label: "Restart display mapping";
643 clicked => { root.restart-mapping(); }
644 }
645
646 if root.busy && root.error-text == "" : Rectangle {
647 width: 24px;
648 background: transparent;
649 ProgressIndicator {
650 y: (parent.height - self.height) / 2;
651 width: 24px;
652 height: 24px;
653 indeterminate: true;
654 }
655 }
656
657 if root.error-text != "" : MaterialIconButton {
658 icon-kind: 4;
659 field-label: "Show error diagnostics";
660 clicked => { root.open-diagnostics(); }
661 }
662 }
663
664 animate background { duration: 150ms; easing: ease-in-out; }
665 }
666
667 Text {
668 text: "Target screen";
669 height: 20px;
670 color: MdTokens.on-surface-variant;
671 font-size: MdTokens.type-body-medium;
672 font-weight: MdTokens.weight-medium;
673 vertical-alignment: bottom;
674 }
675
676 HorizontalLayout {
677 height: MdTokens.target;
678 spacing: MdTokens.space-1;
679
680 CompactComboBox {
681 horizontal-stretch: 1;
682 height: MdTokens.target;
683 model: root.display-labels;
684 collapsed-model: root.display-names;
685 current-index <=> root.selected-display;
686 control-enabled: !root.running && !root.busy && root.display-labels.length > 0;
687 field-label: "Target screen";
688 popup-width: 352px;
689 selected => { root.display-selected(); }
690 }
691
692 MaterialIconButton {
693 icon-kind: root.stream-only ? 2 : 3;
694 control-enabled: !root.running && !root.busy;
695 field-label: root.stream-only
696 ? "Configure stream-only screen"
697 : "Configure target screen size";
698 clicked => {
699 if root.stream-only { root.open-stream-config(); }
700 else { root.open-screen-size(); }
701 }
702 }
703 }
704
705 VerticalLayout {
706 height: 72px;
707 spacing: 4px;
708
709 HorizontalLayout {
710 height: 20px;
711 spacing: MdTokens.space-1;
712
713 Text {
714 text: "Reference display";
715 color: MdTokens.on-surface-variant;
716 font-size: MdTokens.type-body-medium;
717 font-weight: MdTokens.weight-medium;
718 vertical-alignment: bottom;
719 }
720
721 Text {
722 width: 144px;
723 text: "Sizing";
724 color: MdTokens.on-surface-variant;
725 font-size: MdTokens.type-body-medium;
726 font-weight: MdTokens.weight-medium;
727 vertical-alignment: bottom;
728 }
729
730 Rectangle {
731 width: MdTokens.target;
732 background: transparent;
733 }
734 }
735
736 HorizontalLayout {
737 height: MdTokens.target;
738 spacing: MdTokens.space-1;
739
740 CompactComboBox {
741 horizontal-stretch: 1;
742 height: MdTokens.target;
743 model: root.reference-labels;
744 collapsed-model: root.reference-names;
745 current-index <=> root.selected-reference;
746 control-enabled: !root.running && !root.busy && root.reference-labels.length > 0;
747 field-label: "Reference display";
748 popup-width: 352px;
749 selected => { root.reference-selected(); }
750 }
751
752 CompactComboBox {
753 width: 144px;
754 height: MdTokens.target;
755 model: [
756 "Physical",
757 "Rounded",
758 "Integer",
759 ];
760 current-index <=> root.sizing-strategy;
761 control-enabled: !root.running && !root.busy;
762 field-label: "Virtual display sizing strategy";
763 selected => { root.strategy-selected(); }
764 }
765
766 MaterialIconButton {
767 icon-kind: root.reference-manual ? 2 : 5;
768 control-enabled: root.reference-action-enabled
769 && !root.running && !root.busy;
770 field-label: root.reference-manual
771 ? "Configure manual reference display"
772 : "Calculate virtual display geometry";
773 clicked => {
774 if root.reference-manual {
775 root.open-manual-geometry();
776 } else {
777 root.calculate-geometry();
778 }
779 }
780 }
781 }
782 }
783
784 HorizontalLayout {
785 height: 64px;
786 spacing: MdTokens.space-1;
787
788 Rectangle {
789 horizontal-stretch: 1;
790 background: MdTokens.surface-container;
791 border-radius: MdTokens.shape-medium;
792
793 HorizontalLayout {
794 padding: MdTokens.space-2;
795 spacing: 12px;
796
797 Rectangle {
798 width: 24px;
799 background: transparent;
800
801 ProgressIndicator {
802 x: 2px;
803 y: (parent.height - self.height) / 2;
804 width: 20px;
805 height: 20px;
806 indeterminate: true;
807 opacity: root.profile-saving ? 1 : 0;
808
809 animate opacity {
810 duration: 150ms;
811 easing: cubic-bezier(0.2, 0, 0, 1);
812 }
813 }
814
815 Rectangle {
816 y: (parent.height - self.height) / 2;
817 width: 24px;
818 height: 24px;
819 border-radius: MdTokens.shape-full;
820 background: MdTokens.success-container;
821 opacity: root.profile-saving ? 0 : 1;
822
823 animate opacity {
824 duration: 150ms;
825 easing: cubic-bezier(0.2, 0, 0, 1);
826 }
827
828 Path {
829 x: 3px;
830 y: 3px;
831 width: 18px;
832 height: 18px;
833 viewbox-width: 24;
834 viewbox-height: 24;
835 commands: "M9 16.17L4.83 12L3.41 13.41L9 19L21 7L19.59 5.59L9 16.17Z";
836 fill: MdTokens.on-success-container;
837 accessible-role: none;
838 }
839 }
840 }
841
842 Text {
843 horizontal-stretch: 1;
844 vertical-alignment: center;
845 text: root.profile-saving ? "Saving…" : "Saved";
846 color: MdTokens.on-surface;
847 font-size: MdTokens.type-title-medium;
848 font-weight: MdTokens.weight-medium;
849 overflow: elide;
850 }
851 }
852 }
853
854 profile-entry-anchor := Rectangle {
855 min-width: 144px + MdTokens.space-1 + MdTokens.target;
856 max-width: 144px + MdTokens.space-1 + MdTokens.target;
857 horizontal-stretch: 0;
858 background: transparent;
859
860 ProfileEntryButton {
861 width: parent.width;
862 height: parent.height;
863 expanded: root.profile-menu-open;
864 current-name: root.active-profile-name;
865 enabled: !root.profile-saving;
866 clicked => {
867 root.profile-menu-open = !root.profile-menu-open;
868 }
869 }
870 }
871 }
872
873 Rectangle { vertical-stretch: 1; }
874
875 if root.stream-only && root.running : Button {
876 height: MdTokens.target;
877 text: root.sunshine-panel-enabled
878 ? "Open Sunshine"
879 : "Waiting for Sunshine";
880 enabled: root.sunshine-panel-enabled && !root.busy;
881 accessible-label: "Open this Sunshine instance's web configuration panel";
882 clicked => { root.open-sunshine-panel(); }
883 }
884
885 if root.stream-only && root.running : FullWidthPrimaryButton {
886 text: "Stop and restore";
887 enabled: !root.busy;
888 accessible-label: "Stop mapping and restore windows";
889 clicked => { root.stop(); }
890 }
891
892 if !root.stream-only || !root.running : FullWidthSettingsButton {
893 control-enabled: !root.busy;
894 field-label: "Open Windows display settings";
895 clicked => { root.open-display-settings(); }
896 }
897
898 if !root.stream-only || !root.running : FullWidthPrimaryButton {
899 text: root.running ? "Stop and restore" : (root.busy ? root.state : "Start mapping");
900 enabled: !root.busy && (root.running || root.selected-display >= 0);
901 accessible-label: root.running ? "Stop mapping and restore windows" : "Start mapping";
902 clicked => {
903 if root.running { root.stop(); } else { root.start(); }
904 }
905 }
906
907 }
908 }
909
910 // In-tree menu overlay keeps entry/exit motion smooth and avoids a
911 // second native window for this compact flyout.
912 profile-menu-dismiss := Rectangle {
913 z: 20;
914 width: parent.width;
915 height: parent.height;
916 visible: profile-menu.visible;
917 background: transparent;
918
919 TouchArea {
920 enabled: root.profile-menu-open;
921 accessible-role: none;
922 clicked => { root.profile-menu-open = false; }
923 }
924 }
925
926 profile-menu := Rectangle {
927 z: 21;
928 x: parent.width - self.width - MdTokens.space-2;
929 y: max(4px, root.profile-menu-open
930 ? (root.error-text != "" || root.restart-available
931 ? 332px - self.height
932 : 316px - self.height)
933 : (root.error-text != "" || root.restart-available
934 ? 340px - self.height
935 : 324px - self.height));
936 width: 248px;
937 height: 201px + root.profile-names.length * 48px;
938 visible: root.profile-menu-open || self.opacity > 0;
939 opacity: root.profile-menu-open ? 1 : 0;
940 background: MdTokens.surface;
941 border-width: 1px;
942 border-color: MdTokens.outline-variant;
943 border-radius: MdTokens.shape-medium;
944 clip: true;
945 animate y, opacity {
946 duration: 180ms;
947 easing: cubic-bezier(0.05, 0.7, 0.1, 1);
948 }
949
950 VerticalLayout {
951 padding: 4px;
952 spacing: 0px;
953
954 Rectangle {
955 height: 48px;
956 background: transparent;
957
958 VerticalLayout {
959 x: 12px;
960 width: parent.width - 24px;
961 alignment: center;
962 spacing: 1px;
963
964 Text {
965 text: "Configuration profiles";
966 color: MdTokens.on-surface;
967 font-size: MdTokens.type-body-medium;
968 font-weight: MdTokens.weight-medium;
969 }
970 Text {
971 text: "Current · " + root.active-profile-name;
972 color: MdTokens.on-surface-variant;
973 font-size: MdTokens.type-body-small;
974 overflow: elide;
975 }
976 }
977 }
978
979 for profile[index] in root.profile-names : ProfileMenuRow {
980 name: profile;
981 selected: profile == root.active-profile-name;
982 control-enabled: !root.profile-saving;
983 picked => {
984 root.profile-menu-open = false;
985 if profile != root.active-profile-name {
986 if root.running {
987 root.profile-dialog-target-name = profile;
988 root.profile-dialog-mode = 4;
989 } else {
990 root.activate-profile(profile);
991 }
992 }
993 }
994 rename => {
995 root.profile-menu-open = false;
996 root.profile-dialog-target-name = profile;
997 root.profile-name-draft = profile;
998 root.profile-name-error = "";
999 root.profile-dialog-mode = 2;
1000 }
1001 remove => {
1002 root.profile-menu-open = false;
1003 root.profile-dialog-target-name = profile;
1004 root.profile-dialog-mode = 3;
1005 }
1006 }
1007
1008 Rectangle {
1009 height: 1px;
1010 background: MdTokens.outline-variant;
1011 }
1012
1013 ProfileMenuAction {
1014 text: root.profile-names.length >= root.max-profile-count
1015 ? "Profile limit reached"
1016 : "New profile";
1017 icon-kind: 0;
1018 control-enabled: !root.profile-saving
1019 && root.profile-names.length < root.max-profile-count;
1020 clicked => {
1021 root.profile-menu-open = false;
1022 root.profile-dialog-target-name = "";
1023 root.profile-name-draft = "";
1024 root.profile-name-error = "";
1025 root.profile-dialog-mode = 1;
1026 }
1027 }
1028
1029 ProfileMenuAction {
1030 text: "Open profile folder";
1031 icon-kind: 1;
1032 control-enabled: !root.profile-saving;
1033 clicked => {
1034 root.profile-menu-open = false;
1035 root.open-profile-directory();
1036 }
1037 }
1038
1039 Rectangle {
1040 height: 48px;
1041 background: transparent;
1042
1043 HorizontalLayout {
1044 padding-left: 12px;
1045 padding-right: 12px;
1046 alignment: center;
1047
1048 Switch {
1049 text: "Map on login";
1050 checked: root.auto-map-on-login;
1051 enabled: !root.profile-saving;
1052 toggled => { root.set-auto-map-on-login(self.checked); }
1053 }
1054 }
1055 }
1056 }
1057 }
1058
1059 profile-dialog-layer := Rectangle {
1060 z: 30;
1061 width: parent.width;
1062 height: parent.height;
1063 visible: root.profile-dialog-mode != 0;
1064 opacity: root.profile-dialog-mode != 0 ? 1 : 0;
1065 background: #66000000;
1066 animate opacity {
1067 duration: 150ms;
1068 easing: cubic-bezier(0.05, 0.7, 0.1, 1);
1069 }
1070
1071 TouchArea {
1072 accessible-role: none;
1073 clicked => {
1074 root.profile-dialog-mode = 0;
1075 root.profile-name-error = "";
1076 }
1077 }
1078
1079 profile-dialog := Rectangle {
1080 x: (parent.width - self.width) / 2;
1081 y: root.profile-dialog-mode != 0
1082 ? (parent.height - self.height) / 2
1083 : (parent.height - self.height) / 2 + 12px;
1084 width: 320px;
1085 height: root.profile-dialog-mode == 1 || root.profile-dialog-mode == 2
1086 ? 288px : 216px;
1087 background: MdTokens.surface;
1088 border-width: 1px;
1089 border-color: MdTokens.outline-variant;
1090 border-radius: 28px;
1091 animate y {
1092 duration: 220ms;
1093 easing: cubic-bezier(0.05, 0.7, 0.1, 1);
1094 }
1095
1096 TouchArea { accessible-role: none; }
1097
1098 VerticalLayout {
1099 padding: 20px;
1100 spacing: MdTokens.space-1;
1101
1102 Text {
1103 height: 28px;
1104 text: root.profile-dialog-mode == 1
1105 ? "New profile"
1106 : (root.profile-dialog-mode == 2
1107 ? "Rename profile"
1108 : (root.profile-dialog-mode == 3
1109 ? (root.profile-names.length <= 1
1110 ? "Keep one profile"
1111 : "Delete " + root.dialog-profile-name + "?")
1112 : "Switch to " + root.dialog-profile-name + "?"));
1113 color: MdTokens.on-surface;
1114 font-size: MdTokens.type-title-large;
1115 font-weight: MdTokens.weight-medium;
1116 vertical-alignment: center;
1117 overflow: elide;
1118 }
1119
1120 Text {
1121 height: 44px;
1122 text: root.profile-dialog-mode == 1
1123 ? "Copy the current profile, then customize the new one."
1124 : (root.profile-dialog-mode == 2
1125 ? "Only the profile name will change."
1126 : (root.profile-dialog-mode == 3
1127 ? (root.profile-names.length <= 1
1128 ? "At least one configuration profile is required."
1129 : (root.profile-dialog-target-name == root.active-profile-name
1130 ? (root.running
1131 ? "Another profile will become active and the mapping will restart."
1132 : "Another profile will become active after deletion.")
1133 : "This action cannot be undone."))
1134 : "The current mapping will restart to apply this profile."));
1135 color: MdTokens.on-surface-variant;
1136 font-size: MdTokens.type-body-medium;
1137 wrap: word-wrap;
1138 }
1139
1140 if root.profile-dialog-mode == 1 || root.profile-dialog-mode == 2 : LineEdit {
1141 height: MdTokens.target;
1142 text <=> root.profile-name-draft;
1143 placeholder-text: "Profile name";
1144 accessible-label: root.profile-dialog-mode == 1
1145 ? "New profile name" : "Renamed profile name";
1146 edited => { root.profile-name-error = ""; }
1147 }
1148
1149 if root.profile-dialog-mode == 1 || root.profile-dialog-mode == 2 : Text {
1150 height: 16px;
1151 text: root.profile-name-error;
1152 color: MdTokens.error;
1153 font-size: MdTokens.type-body-small;
1154 overflow: elide;
1155 }
1156
1157 Rectangle { vertical-stretch: 1; }
1158
1159 HorizontalLayout {
1160 height: MdTokens.target;
1161 spacing: MdTokens.space-1;
1162 Rectangle { horizontal-stretch: 1; }
1163
1164 DialogActionButton {
1165 text: root.profile-dialog-mode == 3 && root.profile-names.length <= 1
1166 ? "Close" : "Cancel";
1167 field-label: "Cancel profile action";
1168 clicked => {
1169 root.profile-dialog-mode = 0;
1170 root.profile-name-error = "";
1171 }
1172 }
1173
1174 DialogActionButton {
1175 visible: !(root.profile-dialog-mode == 3 && root.profile-names.length <= 1);
1176 width: root.profile-dialog-mode == 4 ? 128px : 96px;
1177 text: root.profile-dialog-mode == 1
1178 ? "Create"
1179 : (root.profile-dialog-mode == 2
1180 ? "Rename"
1181 : (root.profile-dialog-mode == 3
1182 ? "Delete" : "Switch & restart"));
1183 field-label: "Confirm profile action";
1184 destructive: root.profile-dialog-mode == 3;
1185 clicked => {
1186 if root.profile-dialog-mode == 1 || root.profile-dialog-mode == 2 {
1187 if root.profile-name-draft == "" {
1188 root.profile-name-error = "Enter a profile name";
1189 } else if root.profile-dialog-mode == 1 {
1190 root.create-profile(root.profile-name-draft);
1191 root.profile-dialog-mode = 0;
1192 } else {
1193 root.rename-profile(
1194 root.profile-dialog-target-name,
1195 root.profile-name-draft,
1196 );
1197 root.profile-dialog-mode = 0;
1198 }
1199 } else if root.profile-dialog-mode == 3 {
1200 root.delete-profile(root.profile-dialog-target-name);
1201 root.profile-dialog-mode = 0;
1202 } else {
1203 root.activate-profile(root.profile-dialog-target-name);
1204 root.profile-dialog-mode = 0;
1205 }
1206 root.profile-name-error = "";
1207 }
1208 }
1209 }
1210 }
1211 }
1212 }
1213}
1214