650 lines
30 KiB
QML
650 lines
30 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Controls.impl
|
|
import QtQuick.Layouts
|
|
import ISC
|
|
import ISC.Tabs.components
|
|
import QtQuick.Dialogs
|
|
|
|
// ===== Home Workspace Shell =====
|
|
// Unified left rail: pill tab bar → context panel → status footer → utility buttons.
|
|
// Workspace fills the remaining area with the active tab's content.
|
|
Rectangle {
|
|
id: root
|
|
anchors.fill: parent
|
|
color: Theme.pageBackground
|
|
clip: true
|
|
border.color: Theme.outerFrameBorder
|
|
border.width: Theme.borderStrong
|
|
|
|
// ===== View State =====
|
|
property int selectedTabIndex: 0
|
|
onSelectedTabIndexChanged: {
|
|
if (selectedTabIndex === 2) {
|
|
file_browser.refreshFiles();
|
|
streamController.unloadFile()
|
|
}
|
|
}
|
|
|
|
|
|
|
|
property bool memoryRead: false
|
|
property bool waferDetected: false
|
|
// TODO P2.2: add `property bool waferLicensed: false`; set it in
|
|
// onDetectResult below via deviceController.waferLicensed() (slot from
|
|
// P1.2 — the detectResult payload is opaque in QML, can't read serial
|
|
// from it). Then READ/ERASE buttons get
|
|
// `enabled: root.waferDetected && root.waferLicensed`.
|
|
// See docs/pending/license-gating-plan.md §2.2.
|
|
|
|
Connections {
|
|
target: deviceController
|
|
function onDetectResult(result){
|
|
// detectResult payload is Signal(object): a dict may cross into QML
|
|
// as an opaque wrapper whose fields read as undefined, so only
|
|
// test truthiness (None on failure, dict on success).
|
|
root.waferDetected = !!result
|
|
}
|
|
function onParsedDataReady(result) {
|
|
if (result.success && result.csv_path) {
|
|
streamController.loadFile(result.csv_path)
|
|
// TODO P3.1: only auto-jump to Map when unlocked:
|
|
// `&& licenseModel.licenses.length > 0` — otherwise a
|
|
// successful read walks straight into the locked tab.
|
|
// See plan §3.1.
|
|
if (root.selectedTabIndex === 0) {
|
|
root.selectedTabIndex = 2
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Connections {
|
|
target: deviceController
|
|
function onReadResult(result) {
|
|
root.memoryRead = (result !== null &&
|
|
result !== undefined &&
|
|
result.success === true)
|
|
if (root.memoryRead) {
|
|
console.log("[P1.1] Memory read complete:", result.bytes, "bytes")
|
|
} else {
|
|
console.log("[P1.1] Read failed:", result.error || "unknown")
|
|
}
|
|
}
|
|
}
|
|
|
|
function cleanFolderUrl(url) {
|
|
return decodeURIComponent(String(url).replace(/^file:\/\//, ""))
|
|
}
|
|
|
|
function _doDetect() {
|
|
root.memoryRead = false
|
|
root.waferDetected = false
|
|
streamController.setMode("review")
|
|
streamController.stopStream()
|
|
deviceController.detectWafer()
|
|
}
|
|
|
|
FolderDialog {
|
|
id: saveDirDialog
|
|
title: "Choose a folder to save Data"
|
|
onAccepted: {
|
|
deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder))
|
|
root._doDetect()
|
|
}
|
|
}
|
|
|
|
// ===== Split Dialog (Threshold Segmentation) =====
|
|
SplitDialog {
|
|
id: splitDialog
|
|
}
|
|
|
|
// ===== Settings Popup =====
|
|
Popup {
|
|
id: settingsPopup
|
|
modal: true
|
|
dim: true
|
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
width: Math.min(root.width - 80, 800)
|
|
height: Math.min(root.height - 80, 600)
|
|
anchors.centerIn: Overlay.overlay
|
|
|
|
background: Rectangle {
|
|
radius: Theme.radiusMd
|
|
color: Theme.cardBackground
|
|
border.color: Theme.cardBorder
|
|
border.width: 1
|
|
}
|
|
|
|
contentItem: Rectangle {
|
|
color: Theme.pageBackground
|
|
radius: Theme.radiusMd
|
|
clip: true
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 40
|
|
color: Theme.panelBackground
|
|
border.color: Theme.cardBorder
|
|
border.width: 1
|
|
radius: Theme.radiusMd
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 14
|
|
anchors.rightMargin: 4
|
|
|
|
Label {
|
|
text: "Settings"
|
|
font.pixelSize: Theme.fontLg
|
|
font.bold: true
|
|
color: Theme.headingColor
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Button {
|
|
text: "\u2715"
|
|
flat: true
|
|
Layout.preferredWidth: 32
|
|
Layout.preferredHeight: 32
|
|
onClicked: settingsPopup.close()
|
|
|
|
background: Rectangle {
|
|
radius: Theme.radiusXs
|
|
color: parent.hovered ? Theme.buttonNeutralHover : "transparent"
|
|
}
|
|
contentItem: Label {
|
|
text: parent.text
|
|
color: Theme.bodyColor
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
Layout.rightMargin: 6
|
|
clip: true
|
|
|
|
Loader {
|
|
anchors.fill: parent
|
|
source: "Tabs/SettingsTab.qml"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===== About Dialog =====
|
|
AboutDialog {
|
|
id: aboutDialog
|
|
}
|
|
|
|
// ===== Main Layout: Rail + Workspace =====
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
// ── LEFT RAIL ──────────────────────────────────────────────────────
|
|
Rectangle {
|
|
id: rail
|
|
Layout.preferredWidth: Theme.sideRailWidth
|
|
Layout.fillHeight: true
|
|
color: Theme.sideRailBackground
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.sideRailMargin
|
|
spacing: Theme.sideRailSpacing
|
|
|
|
// ── BOX 1: Pill Tab Bar ─────────────────────────────────────
|
|
Rectangle {
|
|
id: pillBar
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: Theme.sidePillHeight
|
|
radius: 999
|
|
border.color: Theme.sideBorder
|
|
border.width: 1
|
|
color: "transparent"
|
|
|
|
ListModel {
|
|
id: pillModel
|
|
ListElement {
|
|
label: "STATUS"
|
|
icon: "Tabs/icons/status.svg"
|
|
expandW: 86
|
|
desc: "Monitor real-time device connection, serial communication, and execute wafer actions."
|
|
}
|
|
ListElement {
|
|
label: "DATA"
|
|
icon: "Tabs/icons/data.svg"
|
|
expandW: 66
|
|
desc: "Manage, review, compare (DTW), and split historical sensor data records."
|
|
}
|
|
ListElement {
|
|
label: "MAP"
|
|
icon: "Tabs/icons/map.svg"
|
|
expandW: 58
|
|
desc: "Visualize spatial sensor readings on a 2D thin-plate spline RBF interpolated heatmap."
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 3
|
|
spacing: 0
|
|
|
|
Repeater {
|
|
model: pillModel
|
|
delegate: Button {
|
|
id: pillBtn
|
|
checked: root.selectedTabIndex === index
|
|
flat: true
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
Layout.preferredWidth: pillBtn.checked ? model.expandW : 44
|
|
Behavior on Layout.preferredWidth {
|
|
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
|
}
|
|
|
|
background: Rectangle {
|
|
radius: 50
|
|
color: pillBtn.checked ? Theme.sidePanelBackground : "transparent"
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durationFast }
|
|
}
|
|
}
|
|
|
|
contentItem: Row {
|
|
id: pillContent
|
|
spacing: pillBtn.checked ? 6 : 0
|
|
width: pillIcon.width + (pillBtn.checked ? pillContent.spacing + pillLabel.width : 0)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
x: (parent.width - width) / 2
|
|
Behavior on x {
|
|
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
|
}
|
|
|
|
IconImage {
|
|
id: pillIcon
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 16; height: 16
|
|
source: model.icon
|
|
sourceSize.width: 16; sourceSize.height: 16
|
|
color: pillBtn.checked ? Theme.headingColor
|
|
: pillBtn.hovered ? Qt.lighter(Theme.sideMutedText, 1.7)
|
|
: Theme.sideMutedText
|
|
opacity: pillBtn.checked ? 0.9 : pillBtn.hovered ? 0.7 : 0.4
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durationFast }
|
|
}
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: Theme.durationFast }
|
|
}
|
|
}
|
|
|
|
Label {
|
|
id: pillLabel
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: pillBtn.checked ? implicitWidth : 0
|
|
text: model.label
|
|
color: pillBtn.checked ? Theme.headingColor : Theme.sideMutedText
|
|
font.family: Theme.uiFontFamily
|
|
font.pixelSize: Theme.fontSm
|
|
font.bold: true
|
|
font.letterSpacing: 1.0
|
|
opacity: pillBtn.checked ? 1.0 : 0.0
|
|
clip: true
|
|
Behavior on width {
|
|
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
|
}
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durationBase; easing.type: Theme.easeStandard }
|
|
}
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
|
}
|
|
}
|
|
}
|
|
|
|
AppToolTip {
|
|
visible: pillBtn.hovered
|
|
text: "<b>" + model.label + "</b>: " + model.desc
|
|
}
|
|
|
|
onClicked: root.selectedTabIndex = index
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── BOX 1.5: Run Comparison (Data tab only) ────────────────
|
|
Rectangle {
|
|
id: runComparisonBox
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: runCompCol.implicitHeight + 28
|
|
visible: root.selectedTabIndex === 1
|
|
radius: Theme.sidePanelRadius
|
|
border.color: Theme.sideBorder
|
|
border.width: 1
|
|
color: Theme.sidePanelBackground
|
|
|
|
readonly property var dataTab: dataTabLoader.item
|
|
|
|
component RunSlot: Rectangle {
|
|
id: slotBox
|
|
property string title
|
|
property string filePath: ""
|
|
property color accent: Theme.primaryAccent
|
|
property bool active: false
|
|
property bool locked: false
|
|
property string placeholderText: "Click to select"
|
|
signal cleared()
|
|
signal armed()
|
|
|
|
Layout.fillWidth: true
|
|
implicitHeight: 58
|
|
radius: Theme.radiusSm
|
|
color: Theme.fieldBackground
|
|
opacity: slotBox.locked ? 0.65 : 1.0
|
|
border.width: slotBox.active ? 2 : 1
|
|
border.color: slotBox.active ? accent : Theme.sideBorder
|
|
|
|
Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
enabled: !slotBox.locked
|
|
cursorShape: slotBox.locked ? Qt.ArrowCursor : Qt.PointingHandCursor
|
|
onClicked: slotBox.armed()
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 10
|
|
spacing: 8
|
|
|
|
Rectangle {
|
|
width: 8; height: 8; radius: 4
|
|
color: slotBox.accent
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 2
|
|
Label {
|
|
text: slotBox.title
|
|
font.pixelSize: Theme.fontXs
|
|
font.bold: true
|
|
font.letterSpacing: 1.0
|
|
color: slotBox.active ? slotBox.accent : Theme.sideMutedText
|
|
}
|
|
Label {
|
|
text: slotBox.filePath !== ""
|
|
? slotBox.filePath.split("/").pop()
|
|
: (slotBox.locked ? slotBox.placeholderText : (slotBox.active ? "<- Select file below..." : "Click to select"))
|
|
font.pixelSize: Theme.fontSm
|
|
color: slotBox.filePath !== "" ? Theme.headingColor : Theme.bodyColor
|
|
opacity: slotBox.filePath !== "" ? 1.0 : 0.6
|
|
elide: Text.ElideMiddle
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
Button {
|
|
visible: slotBox.filePath !== "" && !slotBox.locked
|
|
flat: true
|
|
implicitWidth: 24; implicitHeight: 24
|
|
onClicked: slotBox.cleared()
|
|
background: Rectangle {
|
|
radius: Theme.radiusSm
|
|
color: parent.hovered ? Theme.flatButtonHover : "transparent"
|
|
}
|
|
contentItem: IconImage {
|
|
source: "Tabs/icons/x.svg"
|
|
width: 14; height: 14
|
|
sourceSize.width: 14
|
|
sourceSize.height: 14
|
|
color: Theme.bodyColor
|
|
anchors.centerIn: parent
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: runCompCol
|
|
anchors.fill: parent
|
|
anchors.margins: 14
|
|
spacing: 10
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 8
|
|
|
|
Label {
|
|
text: "RUN COMPARISON (DTW)"
|
|
color: Theme.sideMutedText
|
|
font.family: Theme.uiFontFamily
|
|
font.pixelSize: Theme.fontSm
|
|
font.letterSpacing: 1.5
|
|
font.bold: true
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
Button {
|
|
id: resetRunBtn
|
|
flat: true
|
|
implicitWidth: 32
|
|
implicitHeight: 32
|
|
visible: true
|
|
onClicked: if (runComparisonBox.dataTab) runComparisonBox.dataTab.resetComparison()
|
|
|
|
background: Rectangle {
|
|
radius: Theme.radiusSm
|
|
color: resetRunBtn.hovered ? Theme.flatButtonHover : "transparent"
|
|
}
|
|
contentItem: IconImage {
|
|
source: "Tabs/icons/rotate-ccw.svg"
|
|
width: 18; height: 18
|
|
sourceSize.width: 18
|
|
sourceSize.height: 18
|
|
color: Theme.bodyColor
|
|
anchors.centerIn: parent
|
|
}
|
|
AppToolTip {
|
|
visible: resetRunBtn.hovered
|
|
text: "Reset comparison"
|
|
}
|
|
}
|
|
}
|
|
|
|
PanelCheckBox {
|
|
id: useMasterCheck
|
|
Layout.fillWidth: true
|
|
text: "Compare vs Master File"
|
|
checked: runComparisonBox.dataTab ? runComparisonBox.dataTab.useMasterFile : false
|
|
onToggled: {
|
|
if (!runComparisonBox.dataTab) return
|
|
runComparisonBox.dataTab.useMasterFile = checked
|
|
if (checked) runComparisonBox.dataTab.activeBox = 2
|
|
}
|
|
}
|
|
|
|
RunSlot {
|
|
readonly property bool useMaster: runComparisonBox.dataTab ? runComparisonBox.dataTab.useMasterFile : false
|
|
title: useMaster ? "MASTER FILE" : "REFERENCE (RUN A)"
|
|
accent: Theme.primaryAccent
|
|
locked: useMaster
|
|
placeholderText: useMaster
|
|
? (runComparisonBox.dataTab.runBWaferType !== ""
|
|
? "No master file for type " + runComparisonBox.dataTab.runBWaferType
|
|
: "Select Run B to resolve master")
|
|
: "Click to select"
|
|
active: runComparisonBox.dataTab ? (!useMaster && runComparisonBox.dataTab.activeBox === 1) : false
|
|
filePath: runComparisonBox.dataTab
|
|
? (useMaster ? runComparisonBox.dataTab.masterFileForRunB : runComparisonBox.dataTab.compareFileA)
|
|
: ""
|
|
onArmed: if (runComparisonBox.dataTab && !useMaster) runComparisonBox.dataTab.activeBox = 1
|
|
onCleared: {
|
|
if (!runComparisonBox.dataTab) return
|
|
runComparisonBox.dataTab.compareFileA = ""
|
|
runComparisonBox.dataTab.activeBox = 1
|
|
runComparisonBox.dataTab.clearResults()
|
|
}
|
|
}
|
|
|
|
RunSlot {
|
|
title: "SESSION (RUN B)"
|
|
accent: Theme.themeSkill
|
|
active: runComparisonBox.dataTab ? runComparisonBox.dataTab.activeBox === 2 : false
|
|
filePath: runComparisonBox.dataTab ? runComparisonBox.dataTab.compareFileB : ""
|
|
onArmed: if (runComparisonBox.dataTab) runComparisonBox.dataTab.activeBox = 2
|
|
onCleared: {
|
|
if (!runComparisonBox.dataTab) return
|
|
runComparisonBox.dataTab.compareFileB = ""
|
|
runComparisonBox.dataTab.activeBox = 2
|
|
runComparisonBox.dataTab.clearResults()
|
|
}
|
|
}
|
|
|
|
Button {
|
|
id: compareRunBtn
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 32
|
|
readonly property var dt: runComparisonBox.dataTab
|
|
readonly property string fileA: !!dt ? (dt.useMasterFile ? dt.masterFileForRunB : dt.compareFileA) : ""
|
|
readonly property bool masterMissing: !!dt && dt.useMasterFile && dt.masterFileForRunB === ""
|
|
readonly property bool sameFile: !!dt && fileA !== "" && fileA === dt.compareFileB
|
|
readonly property string gateError: !!dt ? dt.familyGateError : ""
|
|
enabled: !!dt && fileA !== "" && dt.compareFileB !== "" && !sameFile && !dt.comparing && !masterMissing && gateError === ""
|
|
AppToolTip {
|
|
visible: (compareRunBtn.sameFile || compareRunBtn.masterMissing || compareRunBtn.gateError !== "") && compareRunBtn.hovered
|
|
text: compareRunBtn.masterMissing
|
|
? "No master file registered for this wafer type (Settings → Master Files)"
|
|
: compareRunBtn.gateError !== "" ? compareRunBtn.gateError
|
|
: "Choose two different runs to compare"
|
|
}
|
|
onClicked: {
|
|
dt.comparing = true
|
|
dt.clearResults()
|
|
streamController.compareFiles(fileA, dt.compareFileB)
|
|
}
|
|
|
|
background: Rectangle {
|
|
radius: Theme.radiusSm
|
|
color: compareRunBtn.enabled
|
|
? (compareRunBtn.hovered ? Qt.lighter(Theme.primaryAccent, 1.1) : Theme.primaryAccent)
|
|
: Theme.buttonNeutralBackground
|
|
Behavior on color { ColorAnimation { duration: Theme.durationFast } }
|
|
}
|
|
contentItem: Row {
|
|
spacing: 8
|
|
anchors.centerIn: parent
|
|
Label {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: !compareRunBtn.dt ? "Select both runs"
|
|
: compareRunBtn.dt.comparing
|
|
? "Comparing…"
|
|
: compareRunBtn.enabled
|
|
? "Run DTW Comparison"
|
|
: (compareRunBtn.masterMissing ? "No master for this type"
|
|
: compareRunBtn.sameFile ? "Choose different runs"
|
|
: compareRunBtn.gateError !== "" ? "Wafer family mismatch"
|
|
: "Select both runs")
|
|
color: compareRunBtn.enabled ? Theme.tone100 : Theme.sideMutedText
|
|
font.pixelSize: Theme.fontSm
|
|
font.bold: true
|
|
}
|
|
BusyIndicator {
|
|
running: !!compareRunBtn.dt && compareRunBtn.dt.comparing
|
|
visible: !!compareRunBtn.dt && compareRunBtn.dt.comparing
|
|
width: 14; height: 14
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── BOX 2: Context-Sensitive Rail Panel ────────────────────
|
|
StackLayout {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
Layout.minimumHeight: 120
|
|
currentIndex: root.selectedTabIndex === 0 ? 0 : 1
|
|
|
|
// ── Status tab: Hardware Actions + Log Actions ──────────
|
|
StatusActionsPanel {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
}
|
|
|
|
// ── Map tab: Source file browser ──────────────
|
|
Rectangle {
|
|
radius: Theme.sidePanelRadius
|
|
border.color: Theme.sideBorder
|
|
border.width: 1
|
|
color: Theme.sidePanelBackground
|
|
|
|
SourcePanel {
|
|
anchors.fill: parent
|
|
anchors.margins: 14
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── BOX 3: Hardware Status Footer ──────────────────────────
|
|
ConnectionFooter {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
// ── BOX 4: Utility Buttons ─────────────────────────────────
|
|
UtilityFooter {
|
|
id: utilFooter
|
|
Layout.fillWidth: true
|
|
settingsPopup: settingsPopup
|
|
aboutDialog: aboutDialog
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── WORKSPACE ─────────────────────────────────────────────────────
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
color: Theme.workspaceBackground
|
|
|
|
StackLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.mainAreaPadding
|
|
currentIndex: root.selectedTabIndex
|
|
|
|
Loader {
|
|
source: "Tabs/StatusTab.qml"
|
|
active: StackLayout.index === root.selectedTabIndex
|
|
}
|
|
Loader {
|
|
id: dataTabLoader
|
|
source: "Tabs/DataTab.qml"
|
|
active: StackLayout.index === root.selectedTabIndex
|
|
}
|
|
Loader {
|
|
source: "Tabs/WaferMapTab.qml"
|
|
// TODO P3.1: append `&& licenseModel.licenses.length > 0`
|
|
// so the tab never instantiates while locked. See plan §3.1.
|
|
active: StackLayout.index === root.selectedTabIndex
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|