Loading files
c730ec352This file is pinned to the selected release. Download the original to open it in your design tool.
import {
Badge,
Button,
FilterTermEdit,
IconButton,
LinkText,
ListScrollView,
MessagePopup,
PanelHeader,
ProgressBar,
Switch,
} from "../widgets.slint";
import {
Action,
Backend,
Data,
LibraryAction,
LibraryInfoData,
LibraryListData,
} from "../api.slint";
component LibraryUninstallPopup inherits MessagePopup {
in-out property <[LibraryInfoData]> model;
in property <int> index;
title: @tr("Uninstall {}", model[index].name);
arrow: left;
has-ok: false;
has-yes: true;
has-cancel: true;
Text {
font-weight: 700;
color: root.text-color;
text: @tr("Attention").to-uppercase() + ": " + @tr("This will remove the whole library directory and is not reversible:");
}
Text {
font-family: "monospace";
color: root.text-color;
wrap: word-wrap;
text: model[index].path;
}
Text {
color: root.text-color;
text: @tr("Are you really sure to remove '{}'?", model[index].name);
}
accepted => {
Backend.trigger-library(model[index].path, LibraryAction.uninstall);
self.close();
}
}
component LibraryListViewItem inherits Rectangle {
in-out property <[LibraryInfoData]> model;
in property <int> index;
in property <bool> remote: false;
in property <bool> is-current: false;
in property <bool> enabled: true;
property <LibraryInfoData> lib: model[index];
property <bool> has-switch: remote && (!lib.uuid.is-empty);
property <bool> buttons-visible: ta.has-hover || is-current;
property <string> status-text;
property <color> status-color;
property <image> status-icon;
states [
duplicate when lib.duplicate: {
status-text: @tr("Duplicate, will be removed");
status-color: Data.theme.base-text-warning;
status-icon: @image-url("../../../font-awesome/svgs/solid/triangle-exclamation.svg");
}
outdated when lib.outdated: {
status-text: @tr("Outdated:") + " v" + lib.installed-version + " < v" + lib.online-version;
status-color: Data.theme.base-text-error;
status-icon: @image-url("../../../font-awesome/svgs/solid/angles-up.svg");
}
up-to-date when (!lib.installed-version.is-empty) && (!lib.online-version.is-empty): {
status-text: @tr("Up-to-date:") + " v" + lib.installed-version;
status-color: Data.theme.base-text-success;
status-icon: @image-url("../../../font-awesome/svgs/solid/check.svg");
}
]
background: {
if !enabled {
Data.theme.control-disabled
} else if is-current {
Data.theme.control-checked
} else if ta.has-hover {
Data.theme.control-hovered
} else {
Data.theme.control
}
};
border-color: Data.theme.accent;
border-width: lib.highlight ? 1px : 0;
// Accessibility
accessible-role: list-item;
accessible-description: lib.description;
accessible-enabled: self.enabled;
accessible-label: lib.name;
accessible-item-selectable: true;
accessible-item-selected: is-current;
accessible-item-index: index;
accessible-action-default => {
ta.double-clicked();
}
callback clicked <=> ta.clicked;
callback uninstall-clicked(index: int);
init => {
if lib.highlight {
clicked(); // Make sure the library is visible in the view.
}
}
if has-switch && (lib.outdated || (lib.installed-version.is-empty == lib.checked)): Rectangle {
x: parent.width - self.width;
y: 0;
width: 12px;
height: self.width;
Path {
commands: "M 100 100 L 100 0 L 0 0 Z";
fill: Data.theme.base-text-warning;
}
}
ta := TouchArea {
enabled: root.enabled;
HorizontalLayout {
padding: 2px;
padding-left: 4px;
padding-right: 9px;
spacing: 3px;
img := Image {
width: root.height - 6px;
height: root.height - 6px;
image-fit: contain;
source: lib.icon;
accessible-role: none;
}
VerticalLayout {
z: 100; // For the tooltips.
horizontal-stretch: 1;
padding: 3px;
HorizontalLayout {
z: 100; // For the tooltips.
vertical-stretch: 0;
spacing: 5px;
alignment: start;
name-txt := Text {
horizontal-stretch: 0;
color: {
if !enabled {
Data.theme.control-text-muted
} else {
Data.theme.control-text
}
};
font-size: 13px;
font-weight: 700;
vertical-alignment: bottom;
overflow: elide;
text: lib.name;
accessible-role: none;
}
if !status-text.is-empty: status-btn := IconButton {
height: 13px;
width: self.height;
style: read-only;
icon: status-icon;
tooltip: status-text;
color-disabled: status-color.with-alpha(0.5);
color-enabled: status-color;
enabled: enabled;
}
if status-text.is-empty && buttons-visible: version-txt := Text {
horizontal-stretch: 0;
color: name-txt.color;
font-size: 10px;
vertical-alignment: bottom;
text: "v" + (lib.installed-version.is-empty ? lib.online-version : lib.installed-version);
accessible-role: none;
}
}
if lib.progress == 0: description-txt := Text {
vertical-stretch: 1;
vertical-alignment: center;
color: name-txt.color;
font-size: 10px;
overflow: TextOverflow.elide;
wrap: word-wrap;
text: lib.description;
accessible-role: none;
}
if lib.progress > 0: Rectangle {
progress-bar := ProgressBar {
height: 5px;
progress: lib.progress / 100;
accessible-role: none;
}
}
}
if remote && lib.recommended && lib.installed-version.is-empty: recommended-btn := IconButton {
z: 100;
width: 16px;
style: read-only;
icon: @image-url("../../../font-awesome/svgs/solid/star.svg");
tooltip: @tr("Recommended");
tooltip-position: left;
color-disabled: self.color-enabled.with-alpha(0.5);
color-enabled: Data.theme.base-text-warning;
enabled: enabled;
accessible-role: none;
}
if !has-switch: uninstall-btn := IconButton {
width: 18px;
style: hyperlink;
icon: @image-url("../../../font-awesome/svgs/solid/trash-can.svg");
tooltip: @tr("Uninstall");
tooltip-position: left;
enabled: enabled;
visible: buttons-visible; // Only hide to avoid layout changes on hover.
clicked => {
root.clicked();
uninstall-clicked(index);
}
}
if has-switch: checked-sw := Switch {
// Avoid disconnecting from model when toggling.
property <bool> lib-checked: lib.checked;
changed lib-checked => {
self.checked = lib-checked;
}
horizontal-stretch: 0;
checked: lib-checked;
enabled: enabled;
read-only: lib.duplicate;
focusable: false;
accessible-label: "use library";
states [
install when lib.installed-version.is-empty && lib.checked: {
checked-color: Data.theme.accent;
checked-knob-color: Data.theme.accent-text;
}
update when (!lib.installed-version.is-empty) && lib.outdated && lib.checked: {
checked-color: Data.theme.info;
checked-knob-color: Data.theme.info-text;
}
uninstall when (!lib.installed-version.is-empty) && (!lib.checked): {
unchecked-color: Data.theme.error;
unchecked-knob-color: Data.theme.error-text;
}
unchanged when (!lib.outdated) && (lib.installed-version.is-empty != lib.checked): {
checked-color: Data.theme.accent;
unchecked-color: Data.theme.control-checked;
unchecked-knob-color: Data.theme.control-text-muted;
}
]
toggled(checked) => {
model[index].checked = checked;
}
}
}
double-clicked => {
if !lib.path.is-empty {
Backend.trigger-library(lib.path, LibraryAction.open);
}
}
}
}
component LibrariesPanelSection inherits FocusScope {
in property <string> title;
in property <string> title-count;
in-out property <[LibraryInfoData]> libraries;
in property <LibraryListData> data;
in property <bool> remote;
in property <int> min-items-height: 1;
property <int> current-index: -1;
property <length> item-height: 50px;
min-height: l.min-height + max(min(libraries.length, min-items-height) - 1, 0) * item-height;
function set-current-index(index: int) {
if (index + 1) * item-height > view.height - view.viewport-y {
view.viewport-y = -(index + 1) * item-height + view.height;
} else if index * item-height < -(view.viewport-y) {
view.viewport-y = -(index * item-height);
}
current-index = index;
}
function uninstall-library(index: int) {
set-current-index(index); // Update current-index & scroll to item.
uninstall-popup.show();
}
l := VerticalLayout {
alignment: start;
header := PanelHeader {
z: 10; // For the tooltip.
title: title + " (" + title-count + ")";
highlight: root.has-focus;
show-spinner: data.refreshing;
if remote && (!data.refreshing) && (self.max-buttons >= 5): LinkText {
width: self.preferred-width + 2px;
horizontal-alignment: left;
vertical-alignment: center;
font-size: 10px;
text-color: Data.theme.control-text-muted;
text: @tr("Toggle All");
enabled: root.enabled;
clicked => {
Backend.trigger(Action.library-panel-toggle-all);
}
}
if remote: Rectangle {
width: 2px;
}
if !data.refreshing-error.is-empty: IconButton {
width: self.height;
style: read-only;
icon: @image-url("../../../font-awesome/svgs/solid/triangle-exclamation.svg");
icon-scale: 0.7;
color-disabled: self.color-enabled.darker(1);
color-enabled: Data.theme.base-text-warning;
tooltip: data.refreshing-error;
enabled: root.enabled;
}
if !remote: download-by-url-btn := IconButton {
width: self.height;
icon: @image-url("../../../bootstrap-icons/icons/cloud-arrow-down-fill.svg");
icon-scale: 0.7;
tooltip: @tr("Download a Library by URL") + "...";
enabled: root.enabled;
clicked => {
Backend.trigger(Action.library-download);
}
}
if !remote: create-btn := IconButton {
width: self.height;
icon: @image-url("../../../font-awesome/svgs/solid/plus.svg");
icon-scale: 0.7;
tooltip: @tr("Create a New Library") + "...";
enabled: root.enabled;
clicked => {
Backend.trigger(Action.library-create);
}
}
property <bool> check-for-updates-enabled: true;
if remote && (!data.refreshing): check-for-updates-btn := IconButton {
width: self.height;
icon: @image-url("../../../font-awesome/svgs/solid/rotate.svg");
icon-scale: 0.65;
tooltip: @tr("Check For Updates");
enabled: root.enabled && check-for-updates-enabled;
clicked => {
check-for-updates-enabled = false; // DoS attack prevention ;-)
Backend.trigger(Action.library-panel-check-for-updates);
}
}
if remote && data.refreshing: cancel-update-check-btn := IconButton {
width: self.height;
icon: @image-url("../../../font-awesome/svgs/solid/ban.svg");
icon-scale: 0.7;
tooltip: @tr("Cancel Update Check");
clicked => {
Backend.trigger(Action.library-panel-cancel-update-check);
}
}
}
// Show a recommendation to install remote libraries if none are
// installed. However, if several local libraries are installed, do not
// shot the recommendation as this is a sign that the user has installed
// all the libraries (even the official ones) with Git (the typical
// development setup for library contributors).
if remote && (data.installed == 0) && (data.count > 0) && (Data.local-libraries-data.count < 5): HorizontalLayout {
padding: 8px;
Rectangle {
height: no-remote-libs-installed-txt.height;
background: Data.theme.warning;
border-radius: 5px;
no-remote-libs-installed-txt := Text {
x: 6px;
width: parent.width - (2 * self.x);
height: self.preferred-height + (2 * self.x);
font-italic: true;
wrap: word-wrap;
color: Data.theme.warning-text;
vertical-alignment: center;
text: @tr("No remote part libraries are currently installed. To get parts like resistors, LEDs, microcontrollers etc., choose some libraries to be downloaded from the internet.");
}
}
}
if (!remote) && (data.count == 0): HorizontalLayout {
padding: 8px;
no-local-libs-installed-txt := Text {
color: Data.theme.control-text;
font-italic: true;
wrap: word-wrap;
text: @tr("No local libraries exist in this workspace. In order to create your own parts, add a new local library first with the button above.");
}
}
if (!Data.libraries-panel-filter.is-empty) && (libraries.length == 0) && (data.installed > 0): HorizontalLayout {
padding: 8px;
no-libs-matching-filter-txt := Text {
color: Data.theme.control-text-muted;
font-italic: true;
wrap: word-wrap;
text: @tr("No libraries matching the filter.");
}
}
view := ListScrollView {
property <int> item-count: libraries.length;
vertical-stretch: 0;
min-height: item-height;
preferred-height: libraries.length * item-height;
viewport-height: libraries.length * item-height;
force-show-scrollbars: root.has-focus;
// Accessibility
accessible-role: list;
accessible-item-count: libraries.length;
changed item-count => {
if !Data.libraries-panel-filter.is-empty {
self.viewport-y = 0;
current-index = (item-count > 0) ? 0 : -1;
}
}
for lib[index] in libraries: LibraryListViewItem {
width: parent.width;
height: item-height;
model: libraries;
index: index;
remote: remote;
is-current: index == current-index;
enabled: root.enabled;
clicked => {
set-current-index(index);
root.focus();
}
uninstall-clicked(index) => {
uninstall-library(index);
}
}
}
}
key-pressed(event) => {
if event.text == Key.DownArrow {
set-current-index(min(current-index + 1, libraries.length - 1));
return accept;
} else if event.text == Key.UpArrow {
set-current-index(max(current-index - 1, 0));
return accept;
} else if event.modifiers.control && (event.text == "a") {
Backend.trigger(Action.library-panel-toggle-all);
return accept;
} else if (event.text == Key.Escape) && remote && (Data.remote-libraries-pending-operations > 0) {
Backend.trigger(Action.library-panel-cancel);
return accept;
} else if event.modifiers.control && (event.text == Key.Return) && remote && (Data.remote-libraries-pending-operations > 0) {
Backend.trigger(Action.library-panel-apply);
return accept;
} else if (current-index >= 0) && (current-index < libraries.length) {
if (event.text == Key.Return) && (!libraries[current-index].path.is-empty) {
Backend.trigger-library(libraries[current-index].path, LibraryAction.open);
return accept;
} else if event.text == Key.Delete {
uninstall-library(current-index);
} else if (event.text == Key.Space) && remote && (!libraries[current-index].uuid.is-empty) {
libraries[current-index].checked = !libraries[current-index].checked;
return accept;
} else if (event.text == Key.Delete) && remote && (!libraries[current-index].uuid.is-empty) {
libraries[current-index].checked = false;
return accept;
}
}
Backend.libraries-key-pressed(event)
}
uninstall-popup := LibraryUninstallPopup {
x: root.width;
y: view.y + (current-index * item-height) + (item-height / 2) - (self.height / 2) + view.viewport-y;
model: libraries;
index: current-index;
}
}
export component LibrariesPanel inherits VerticalLayout {
forward-focus: local-libs;
init => {
Backend.trigger(Action.library-panel-ensure-populated);
local-libs.focus();
}
local-libs := LibrariesPanelSection {
property <length> max-height-impl: (root.height * self.preferred-height) / (self.preferred-height + remote-libs.preferred-height);
vertical-stretch: 0;
max-height: (self.preferred-height > max-height-impl) ? max-height-impl : self.preferred-height;
title: @tr("Local Libraries").to-uppercase();
title-count: Data.local-libraries-data.count;
libraries: Data.local-libraries;
data: Data.local-libraries-data;
remote: false;
min-items-height: (root.height >= 500px) ? 3 : 1;
enabled: !Data.libraries-operation-in-progress;
}
Rectangle {
height: 20px;
}
remote-libs := LibrariesPanelSection {
vertical-stretch: 1;
title: @tr("Remote Libraries").to-uppercase();
title-count: Data.remote-libraries-data.installed + "/" + Data.remote-libraries-data.count;
libraries: Data.remote-libraries;
data: Data.remote-libraries-data;
remote: true;
enabled: (!Data.libraries-operation-in-progress) && (!Data.remote-libraries-data.refreshing);
}
Rectangle {
property <bool> show: Data.preview-mode ? Data.remote-libraries[0].checked : (Data.remote-libraries-pending-operations > 0);
height: show ? self.preferred-height : 0;
background: Data.theme.window;
// Clip for the slide-in/out effect, but do not clip in steady
// state for the tooltips.
clip: self.height != self.preferred-height;
animate height { duration: 150ms; }
Rectangle {
y: 0;
height: 1px;
background: Data.theme.base-border;
}
VerticalLayout {
spacing: 8px;
padding-bottom: 8px;
HorizontalLayout {
padding-top: 5px;
padding-left: 8px;
padding-right: 3px;
spacing: 3px;
z: 10; // For the tooltips.
Rectangle {
Text {
x: 0;
y: (parent.height - self.height) / 2 + 1px;
width: 100%;
height: 100%;
font-weight: 700;
overflow: elide;
vertical-alignment: center;
color: Data.theme.base-text;
text: @tr("Pending Operations").to-uppercase();
accessible-role: none;
}
}
if Data.remote-libraries-data.pending-installs > 0: pending-installs-badge := Badge {
font-weight: 700;
font-size: 12px;
border-width: 0;
background: Data.theme.accent;
text-color: Data.theme.accent-text;
text: "+" + Data.remote-libraries-data.pending-installs;
}
if Data.remote-libraries-data.pending-updates > 0: pending-updates-badge := Badge {
font-weight: 700;
font-size: 12px;
border-width: 0;
background: Data.theme.info;
text-color: Data.theme.info-text;
text: "↑" + Data.remote-libraries-data.pending-updates;
}
if Data.remote-libraries-data.pending-uninstalls > 0: pending-uninstalls-badge := Badge {
font-weight: 700;
font-size: 12px;
border-width: 0;
background: Data.theme.error;
text-color: Data.theme.error-text;
text: "–" + Data.remote-libraries-data.pending-uninstalls;
}
}
VerticalLayout {
padding-left: 8px;
padding-right: 8px;
spacing: 8px;
if Data.remote-libraries-data.pending-oneway-uninstalls > 0: Rectangle {
height: nonreversible-warning-txt.preferred-height + 12px;
border-radius: 5px;
background: Data.theme.error;
nonreversible-warning-txt := Text {
width: parent.width - 12px;
color: Data.theme.error-text;
font-weight: 700;
horizontal-alignment: left;
wrap: word-wrap;
text: @tr("Warning").to-uppercase() + ": " + @tr("Some libraries marked for uninstallation are currently not available for installation, thus their deletion will be non-reversible!");
}
}
HorizontalLayout {
spacing: 6px;
apply-btn := Button {
horizontal-stretch: 1;
icon: {
if Data.remote-libraries-data.pending-oneway-uninstalls > 0 {
@image-url("../../../font-awesome/svgs/solid/triangle-exclamation.svg");
} else {
@image-url("../../../font-awesome/svgs/solid/check.svg");
}
};
text: (root.width > 300px) ? @tr("" | "Apply {n} Modification(s)" % Data.remote-libraries-pending-operations) : @tr("Apply");
primary: Data.remote-libraries-data.pending-oneway-uninstalls == 0;
border-color: {
if !self.enabled {
Data.theme.control-border-disabled
} else if (Data.remote-libraries-data.pending-oneway-uninstalls > 0) {
Data.theme.base-text-error.with-alpha(self.has-focus ? 1 : 0.5)
} else if self.has-focus {
Data.theme.accent
} else {
Data.theme.control-border
}
};
enabled: !Data.libraries-operation-in-progress;
clicked => {
Backend.trigger(Action.library-panel-apply);
}
}
cancel-btn := Button {
horizontal-stretch: 0;
text: @tr("Cancel");
clicked => {
Backend.trigger(Action.library-panel-cancel);
}
}
}
}
}
}
if !Data.libraries-panel-filter.is-empty: Rectangle {
background: Data.theme.window;
Rectangle {
y: 0;
height: 1px;
background: Data.theme.base-border;
}
filter-l := HorizontalLayout {
padding: 5px;
filter-edt := FilterTermEdit {
height: self.preferred-height;
term: Data.libraries-panel-filter;
animate height { duration: 100ms; }
clear-clicked => {
Backend.libraries-key-pressed({ text: Key.Escape })
}
}
}
}
}