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.
文件
../ diagnostics.slint8.2 KBgeometry.slint10.0 KBmain.slint54.1 KBscreen-size.slint9.3 KBstream.slint9.4 KBui/pages/main.slint54.1 KB
1
import { Button, LineEdit, ProgressIndicator, Switch } from "std-widgets.slint";2
import {3
CompactComboBox,4
DialogActionButton,5
FullWidthSettingsButton,6
FullWidthPrimaryButton,7
MaterialIconButton,8
} from "../controls.slint";9
import { SevenSegmentDisplay } from "../seven-segment.slint";10
import { MdTokens } from "../theme.slint";11
12
component 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.selected22
? MdTokens.primary-container23
: (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.selected59
? "Current profile " + root.name60
: "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
135
component ProfileMenuAction inherits Rectangle {136
in property <string> text;137
in property <int> icon-kind: 0; // 0 add, 1 folder138
in property <bool> control-enabled: true;139
callback clicked();140
141
height: MdTokens.target;142
background: action-touch.has-hover && root.control-enabled143
? 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
192
component 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
284
export 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 switch329
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-index401
? MdTokens.primary-container402
: 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-index450
? MdTokens.on-primary-container451
: 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 ? 0469
: (!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-index478
? MdTokens.on-primary-container : MdTokens.on-surface-variant;479
opacity: close-touch.pressed480
? 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-index493
? 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-container558
: (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-container575
: (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-container589
: (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-container597
: (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-only619
? (root.sunshine-state == 2 && root.sunshine-port > 0620
? (root.sunshine-lan-ip == "--"621
? "LAN unavailable"622
: root.sunshine-lan-ip + ":" + root.sunshine-port)623
: (root.sunshine-state == 1624
? "Starting"625
: (root.sunshine-state == 3 ? "Unavailable" : "--")))626
: root.geometry-summary-detail;627
color: root.running628
? MdTokens.on-primary-container629
: (root.stream-only && root.sunshine-state == 3630
? MdTokens.error631
: MdTokens.on-surface-variant);632
font-size: MdTokens.type-body-small;633
font-weight: root.stream-only && root.sunshine-state == 2634
? 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-only696
? "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-enabled769
&& !root.running && !root.busy;770
field-label: root.reference-manual771
? "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-enabled878
? "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 a911
// 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-open930
? (root.error-text != "" || root.restart-available931
? 332px - self.height932
: 316px - self.height)933
: (root.error-text != "" || root.restart-available934
? 340px - self.height935
: 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-count1015
? "Profile limit reached"1016
: "New profile";1017
icon-kind: 0;1018
control-enabled: !root.profile-saving1019
&& 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 != 01082
? (parent.height - self.height) / 21083
: (parent.height - self.height) / 2 + 12px;1084
width: 320px;1085
height: root.profile-dialog-mode == 1 || root.profile-dialog-mode == 21086
? 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 == 11105
? "New profile"1106
: (root.profile-dialog-mode == 21107
? "Rename profile"1108
: (root.profile-dialog-mode == 31109
? (root.profile-names.length <= 11110
? "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 == 11123
? "Copy the current profile, then customize the new one."1124
: (root.profile-dialog-mode == 21125
? "Only the profile name will change."1126
: (root.profile-dialog-mode == 31127
? (root.profile-names.length <= 11128
? "At least one configuration profile is required."1129
: (root.profile-dialog-target-name == root.active-profile-name1130
? (root.running1131
? "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 == 11145
? "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 <= 11166
? "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 == 11178
? "Create"1179
: (root.profile-dialog-mode == 21180
? "Rename"1181
: (root.profile-dialog-mode == 31182
? "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