Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
94ec4b8feThis file is pinned to the selected release. Download the original to open it in your design tool.
import {
AngleEdit,
Button,
ComboBox,
IconButton,
LengthEdit,
LineEdit,
LinkText,
MenuItem,
MenuPopup,
ScrollView,
} from "../../widgets.slint";
import {
ComponentGateAction,
ComponentGateData,
ComponentPinoutDisplayMode,
ComponentVariantAction,
ComponentVariantData,
Data,
EditorCommandSet as Cmd,
LengthUnit,
} from "../../api.slint";
component ComponentGateListItem {
in-out property <[ComponentGateData]> model;
in property <int> index;
in property <bool> has-unassigned-signals;
in-out property <[string]> signal-names;
in property <bool> wizard-mode;
in property <bool> read-only;
in property <length> window-height;
property <length> pinout-row-height: 25px;
pure callback render-scene(width: length, height: length, frame-dummy: int) -> image;
callback auto-connect-pins;
HorizontalLayout {
padding: 5px;
padding-bottom: 7px;
spacing: 10px;
Rectangle {
max-width: 500px; // Only to ensure some alignment across gates on a large screen
min-height: 120px;
background: white; // For the preview
border-color: Data.theme.control-border;
border-width: 1px;
border-radius: 3px;
clip: true; // For the rounded corners
Image {
x: 0;
y: 0;
width: 100%;
height: 100%;
source: render-scene(parent.width, parent.height, model[index].frame-index);
accessible-role: none;
}
}
VerticalLayout {
width: self.preferred-width;
spacing: 2px;
Text {
height: pinout-row-height;
horizontal-alignment: right;
vertical-alignment: center;
color: Data.theme.control-text-muted;
text: @tr("Gate") + ":";
accessible-role: none;
}
for pinout in model[index].pinout: pin-name-txt := Text {
height: pinout-row-height;
horizontal-alignment: right;
vertical-alignment: center;
color: Data.theme.control-text-muted;
overflow: elide;
text: pinout.pin-name + ":";
accessible-role: none;
}
}
VerticalLayout {
spacing: 2px;
symbol-name-txt := Text {
height: pinout-row-height;
font-size: 13px;
font-weight: 700;
color: Data.theme.control-text;
vertical-alignment: center;
overflow: elide;
text: model[index].symbol-name;
accessible-description: "symbol name";
}
for pinout[pinout-index] in model[index].pinout: HorizontalLayout {
if wizard-mode: signal-edt := LineEdit {
height: pinout-row-height;
text: signal-names[pinout.signal-index];
show-clear-button: false;
enabled: pinout.signal-index > 0; // 0 = unconnected
accessible-label: "signal name";
text-edited => {
// Ignore intermediate edits
}
text-accepted(text) => {
signal-names[pinout.signal-index] = text;
}
}
if !wizard-mode: signal-cbx := ComboBox {
height: pinout-row-height;
window-height: window-height;
model: signal-names;
current-index: pinout.signal-index;
editable-only-when-focused: true;
enabled: !read-only;
accessible-label: "signal of pin " + pinout.pin-name;
current-index-changed(idx) => {
model[index].pinout[pinout-index].signal-index = idx;
}
}
}
Rectangle {
// Stretch button to bottom
}
if (!wizard-mode) && (!read-only) && has-unassigned-signals && (index == (model.length - 1)): auto-assign-btn := Button {
height: pinout-row-height;
text: @tr("Auto-Connect all Pins by Name");
primary: true;
clicked => {
auto-connect-pins();
}
}
}
VerticalLayout {
width: self.preferred-width;
spacing: 2px;
suffix-txt := Text {
height: pinout-row-height;
horizontal-alignment: right;
vertical-alignment: center;
color: Data.theme.control-text-muted;
text: @tr("Suffix") + ":";
accessible-role: none;
}
for pinout in model[index].pinout: Text {
height: pinout-row-height;
horizontal-alignment: right;
vertical-alignment: center;
color: Data.theme.control-text-muted;
text: @tr("Label") + ":";
accessible-role: none;
}
}
VerticalLayout {
width: self.preferred-width;
spacing: 2px;
HorizontalLayout {
height: pinout-row-height;
spacing: 5px;
suffix-edt := LineEdit {
height: 22px;
min-width: 60px;
text: model[index].suffix;
placeholder-text: "-";
has-error: self.text.is-empty && (model.length > 1);
show-clear-button: false;
read-only: read-only;
frameless: true;
accessible-label: suffix-txt.text;
text-edited(text) => {
// Ignore intermediate edits
}
text-accepted(text) => {
model[index].suffix = text;
}
}
required-sw := IconButton {
style: hyperlink;
icon-scale: 0.7;
icon: {
if model[index].required {
@image-url("../../../../bootstrap-icons/icons/exclamation-circle-fill.svg")
} else {
@image-url("../../../../bootstrap-icons/icons/exclamation-circle.svg")
}
};
tooltip: model[index].required ? @tr("Gate is Required") : @tr("Gate is Optional");
tooltip-position: left;
status-tip: @tr("If marked as required, an ERC error is raised when this gate is not placed in the schematics");
enabled: !read-only;
clicked => {
model[index].required = !model[index].required;
}
}
if !read-only: menu-btn := IconButton {
style: hyperlink;
icon-scale: 0.7;
icon: @image-url("../../../../font-awesome/svgs/solid/caret-down.svg");
accessible-label: "menu";
clicked => {
context-menu-x = self.absolute-position.x - root.absolute-position.x;
context-menu-y = self.absolute-position.y - root.absolute-position.y + self.height;
context-menu-popup.show();
}
}
}
for pinout[pinout-index] in model[index].pinout: HorizontalLayout {
height: pinout-row-height;
spacing: 8px;
display-none-btn := LinkText {
text: @tr("None").to-uppercase();
text-color: {
if pinout.display-mode == ComponentPinoutDisplayMode.none {
Data.theme.control-text
} else {
Data.theme.control-text-disabled
}
};
status-tip: @tr("When added to a schematic, hide this pin name");
vertical-alignment: center;
font-size: 10px;
enabled: !read-only;
accessible-description: "label";
clicked => {
model[index].pinout[pinout-index].display-mode = ComponentPinoutDisplayMode.none;
}
}
display-pin-btn := LinkText {
text: @tr("Pin").to-uppercase();
text-color: {
if pinout.display-mode == ComponentPinoutDisplayMode.pin-name {
Data.theme.control-text
} else {
Data.theme.control-text-disabled
}
};
status-tip: @tr("When added to a schematic, display the pin name");
vertical-alignment: center;
font-size: 10px;
enabled: !read-only;
accessible-description: "label";
clicked => {
model[index].pinout[pinout-index].display-mode = ComponentPinoutDisplayMode.pin-name;
}
}
display-signal-btn := LinkText {
text: @tr("Signal").to-uppercase();
text-color: {
if pinout.display-mode == ComponentPinoutDisplayMode.signal-name {
Data.theme.control-text
} else {
Data.theme.control-text-disabled
}
};
status-tip: @tr("When added to a schematic, display the signal name (default)");
vertical-alignment: center;
font-size: 10px;
enabled: !read-only;
accessible-description: "label";
clicked => {
model[index].pinout[pinout-index].display-mode = ComponentPinoutDisplayMode.signal-name;
}
}
display-net-btn := LinkText {
text: @tr("Net").to-uppercase();
text-color: {
if pinout.display-mode == ComponentPinoutDisplayMode.net-name {
Data.theme.control-text
} else {
Data.theme.control-text-disabled
}
};
status-tip: @tr("When added to a schematic, display the name of the net connected to this pin (if any)");
vertical-alignment: center;
font-size: 10px;
enabled: !read-only;
accessible-description: "label";
clicked => {
model[index].pinout[pinout-index].display-mode = ComponentPinoutDisplayMode.net-name;
}
}
}
}
}
property <length> context-menu-x;
property <length> context-menu-y;
context-menu-popup := MenuPopup {
x: min(context-menu-x, parent.width - self.width);
y: context-menu-y;
// Note: Currently disabled in wizard mode because the signal list
// in the backend is not updated yet during symbol change.
if !wizard-mode: change-symbol-item := MenuItem {
text: @tr("Change Symbol") + "...";
icon: @image-url("../../../../font-awesome/svgs/solid/folder-open.svg");
clicked => {
model[index].action = ComponentGateAction.choose-symbol;
}
}
move-up-item := MenuItem {
text: @tr("Move Up");
icon: @image-url("../../../../font-awesome/svgs/solid/arrow-up.svg");
enabled: index > 0;
clicked => {
model[index].action = ComponentGateAction.move-up;
}
}
delete-item := MenuItem {
cmd: Cmd.remove;
icon: @image-url("../../../../font-awesome/svgs/solid/trash-can.svg");
shortcuts: "";
clicked => {
model[index].action = ComponentGateAction.delete;
}
}
}
}
component ComponentVariantListItem inherits Rectangle {
in-out property <[ComponentVariantData]> model;
in property <int> index;
in-out property <[string]> signal-names;
in property <LengthUnit> unit;
in property <bool> wizard-mode: false;
in property <bool> read-only: false;
in property <length> window-height;
pure callback render-scene(gate: int, width: length, height: length, frame-dummy: int) -> image;
height: self.preferred-height;
background: Data.theme.control;
border-color: Data.theme.control-border;
border-width: 1px;
border-radius: 5px;
VerticalLayout {
if wizard-mode && (model[index].gates.length > 0) && (signal-names.length > 1): VerticalLayout {
padding: 8px;
Text {
property <string> s1: @tr("Now define the names of the electrical signals of the component in the table below. Usually you can just keep the names from the symbol pins.");
property <string> s2: @tr("Note that unconnected or inter-connected pins (i.e. duplicate signal names) are not allowed in this step, but you can rework them on the following pages.");
color: Data.theme.control-text;
wrap: word-wrap;
text: s1 + " " + s2;
}
}
if !wizard-mode: HorizontalLayout {
padding: 3px;
padding-left: 8px;
spacing: 5px;
name-txt := Text {
horizontal-stretch: 0;
color: Data.theme.control-text-muted;
vertical-alignment: center;
text: @tr("Variant") + ":";
accessible-role: none;
}
name-edt := LineEdit {
horizontal-stretch: 1;
font-size: 13px;
font-weight: 700;
text: model[index].name;
show-clear-button: false; // Empty name is not valid
read-only: read-only;
frameless: true;
accessible-label: name-txt.text;
text-edited(text) => {
// Ignore intermediate edits
}
text-accepted(text) => {
model[index].name = text;
}
}
description-txt := Text {
horizontal-stretch: 0;
color: Data.theme.control-text-muted;
vertical-alignment: center;
text: @tr("Description") + ":";
accessible-role: none;
}
description-edt := LineEdit {
horizontal-stretch: 1;
text: model[index].description;
placeholder-text: "-";
read-only: read-only;
frameless: true;
accessible-label: description-txt.text;
text-edited(text) => {
// Ignore intermediate edits
}
text-accepted(text) => {
model[index].description = text;
}
clear-clicked => {
model[index].description = "";
}
}
norm-txt := Text {
horizontal-stretch: 0;
color: Data.theme.control-text-muted;
vertical-alignment: center;
text: @tr("Norm") + ":";
accessible-role: none;
}
norm-edt := LineEdit {
horizontal-stretch: 0;
text: model[index].norm;
placeholder-text: "-";
suggestions: Data.norms;
show-clear-button: false;
read-only: read-only;
frameless: true;
accessible-label: norm-txt.text;
text-edited(text) => {
// Ignore intermediate edits
}
text-accepted(text) => {
model[index].norm = text;
}
suggestion-accepted(text) => {
model[index].norm = text;
}
}
if !read-only: menu-btn := IconButton {
height: norm-edt.preferred-height;
style: hyperlink;
icon-scale: 0.7;
icon: @image-url("../../../../font-awesome/svgs/solid/ellipsis-vertical.svg");
clicked => {
context-menu-x = self.absolute-position.x - root.absolute-position.x;
context-menu-y = self.absolute-position.y - root.absolute-position.y + self.height;
context-menu-popup.show();
}
}
}
if !wizard-mode: Rectangle {
height: 1px;
background: Data.theme.control-border;
}
for gate[gate-index] in model[index].gates: gate-item := ComponentGateListItem {
model: model[index].gates;
index: gate-index;
has-unassigned-signals: model[index].has-unassigned-signals;
signal-names: signal-names;
wizard-mode: wizard-mode;
read-only: read-only;
window-height: window-height;
render-scene(width, height, frame-dummy) => {
root.render-scene(gate-index, width, height, frame-dummy)
}
auto-connect-pins => {
model[index].action = ComponentVariantAction.auto-connect-pins;
}
}
if model[index].gates.length > 1: VerticalLayout {
property <bool> expanded: false;
padding: 5px;
padding-top: 3px;
expand-placement-btn := LinkText {
text: (expanded ? "▼ " : "► ") + @tr("Component Preview Placement");
clicked => {
expanded = !expanded;
}
}
placement-l := HorizontalLayout {
height: expanded ? self.preferred-height : 0;
padding-top: 5px;
spacing: 10px;
// To avoid unnecessary overhead, do not render during animation.
// https://github.com/LibrePCB/LibrePCB/issues/1652
visible: self.height == self.preferred-height;
animate height { duration: 150ms; }
Rectangle {
max-width: 500px; // Only to ensure some alignment across gates on a large screen
min-height: 250px; // Vertically aligned symbols require a lot of space
background: Data.preview-mode ? white : transparent; // For the preview
border-color: Data.theme.control-border;
border-width: 1px;
border-radius: 3px;
clip: true; // For the rounded corners
Image {
x: 0;
y: 0;
width: 100%;
height: 100%;
source: render-scene(model[index].gates.length, parent.width, parent.height, model[index].frame-index);
visible: placement-l.height == placement-l.preferred-height; // Avoid rendering during animation
accessible-role: none;
}
}
VerticalLayout {
alignment: start;
spacing: 3px;
visible: placement-l.height == placement-l.preferred-height;
for gate[gate-index] in model[index].gates: Text {
height: 25px;
horizontal-alignment: right;
vertical-alignment: center;
color: Data.theme.control-text;
text: (gate.suffix.is-empty ? (gate-index + 1) : gate.suffix) + ":";
accessible-role: none;
}
}
VerticalLayout {
alignment: start;
spacing: 3px;
visible: parent.height == parent.preferred-height;
for gate[gate-index] in model[index].gates: x-edt := LengthEdit {
height: 25px;
value: gate.symbol-x;
unit: unit;
min-width-text: "12.345";
can-change-unit: false;
enabled: !read-only;
accessible-label: gate.suffix + " x";
value-changed(v) => {
// Ignore intermediate values.
}
value-accepted(v) => {
model[index].gates[gate-index].symbol-x = v;
}
}
}
VerticalLayout {
alignment: start;
spacing: 3px;
visible: parent.height == parent.preferred-height;
for gate[gate-index] in model[index].gates: y-edt := LengthEdit {
height: 25px;
value: gate.symbol-y;
unit: unit;
min-width-text: "12.345";
can-change-unit: false;
enabled: !read-only;
accessible-label: gate.suffix + " y";
value-changed(v) => {
// Ignore intermediate values.
}
value-accepted(v) => {
model[index].gates[gate-index].symbol-y = v;
}
}
}
VerticalLayout {
alignment: start;
spacing: 3px;
visible: parent.height == parent.preferred-height;
for gate[gate-index] in model[index].gates: rotation-edt := AngleEdit {
height: 25px;
value: gate.symbol-rotation;
icon: @image-url("");
min-width-text: "12.345 °";
enabled: !read-only;
accessible-label: gate.suffix + " rotation";
value-changed(v) => {
// Ignore intermediate values.
}
value-accepted(v) => {
model[index].gates[gate-index].symbol-rotation = v;
}
increase-triggered => {
model[index].gates[gate-index].symbol-rotation += 90000000;
}
decrease-triggered => {
model[index].gates[gate-index].symbol-rotation -= 90000000;
}
}
}
}
}
if wizard-mode || (model[index].gates.length == 0): VerticalLayout {
padding: 10px;
spacing: 10px;
if !wizard-mode: Text {
color: red;
font-weight: 700;
horizontal-alignment: center;
overflow: elide;
text: @tr("This variant contains no gate!");
}
if wizard-mode && (model[index].gates.length == 0): Text {
property <string> s1: @tr("Add gates to this component by choosing the corresponding symbols.");
property <string> s2: @tr("Most components consist of only one gate, but for example a 4-channel ADC could be built up with four single-channel ADC gates.");
property <string> s3: @tr("Those gates are all part of the same component, but can be placed individually in the schematics.");
color: Data.theme.control-text;
wrap: word-wrap;
text: s1 + " " + s2 + " " + s3;
}
if wizard-mode: add-gate-btn := Button {
width: self.preferred-width;
icon: @image-url("../../../../font-awesome/svgs/solid/plus.svg");
text: @tr("Add Gate") + "...";
primary: model[index].gates.length == 0;
clicked => {
model[index].action = ComponentVariantAction.add-gate;
}
}
}
}
property <length> context-menu-x;
property <length> context-menu-y;
context-menu-popup := MenuPopup {
x: min(context-menu-x, parent.width - self.width);
y: context-menu-y;
add-gate-item := MenuItem {
text: @tr("Add Gate") + "...";
icon: @image-url("../../../../font-awesome/svgs/solid/plus.svg");
clicked => {
model[index].action = ComponentVariantAction.add-gate;
}
}
move-up-item := MenuItem {
text: @tr("Move Up");
icon: @image-url("../../../../font-awesome/svgs/solid/arrow-up.svg");
enabled: index > 0;
clicked => {
model[index].action = ComponentVariantAction.move-up;
}
}
set-as-default-item := MenuItem {
text: @tr("Set as Default");
icon: @image-url("../../../../font-awesome/svgs/solid/star.svg");
enabled: index > 0;
clicked => {
model[index].action = ComponentVariantAction.set-as-default;
}
}
delete-item := MenuItem {
cmd: Cmd.remove;
icon: @image-url("../../../../font-awesome/svgs/solid/trash-can.svg");
shortcuts: "";
clicked => {
model[index].action = ComponentVariantAction.delete;
}
}
}
}
export component ComponentVariantListView inherits ScrollView {
in-out property <[ComponentVariantData]> model;
in-out property <[string]> signal-names;
in property <LengthUnit> unit;
in property <bool> wizard-mode: false;
in property <bool> read-only: false;
in property <length> window-height;
pure callback render-scene(variant: int, gate: int, width: length, height: length, frame-dummy: int) -> image;
callback add-variant-clicked;
preferred-height: l.preferred-height;
mouse-drag-pan-enabled: true;
l := VerticalLayout {
alignment: start;
padding: 8px;
spacing: 10px;
for item[index] in model: variant-item := ComponentVariantListItem {
model: model;
index: index;
signal-names: signal-names;
unit: unit;
wizard-mode: wizard-mode;
read-only: read-only;
window-height: window-height;
render-scene(gate, width, height, frame-dummy) => {
root.render-scene(index, gate, width, height, frame-dummy)
}
}
if (!wizard-mode) && (!read-only): add-variant-btn := Button {
width: self.preferred-width;
icon: @image-url("../../../../font-awesome/svgs/solid/plus.svg");
text: @tr("Add Variant") + "...";
clicked => {
add-variant-clicked();
}
}
}
}