diff options
| author | seraphim <18ycm9tx@anonaddy.me> | 2026-07-24 20:57:33 +0700 |
|---|---|---|
| committer | seraphim <18ycm9tx@anonaddy.me> | 2026-07-24 20:57:33 +0700 |
| commit | 395c08d0a334cdffbcc370384bee67f19f964399 (patch) | |
| tree | d57cdf13385785aaf068d4f8408fe6cc2f095999 /noctalia/.config/plugins/keybind-cheatsheet/Panel.qml | |
frst
Diffstat (limited to 'noctalia/.config/plugins/keybind-cheatsheet/Panel.qml')
| -rw-r--r-- | noctalia/.config/plugins/keybind-cheatsheet/Panel.qml | 687 |
1 files changed, 687 insertions, 0 deletions
diff --git a/noctalia/.config/plugins/keybind-cheatsheet/Panel.qml b/noctalia/.config/plugins/keybind-cheatsheet/Panel.qml new file mode 100644 index 0000000..946431e --- /dev/null +++ b/noctalia/.config/plugins/keybind-cheatsheet/Panel.qml @@ -0,0 +1,687 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI +import qs.Services.Compositor +import qs.Widgets + +Item { + id: root + property var pluginApi: null + + // Settings + property var cfg: pluginApi?.pluginSettings || ({}) + property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) + + // Settings values + property int settingsWidth: cfg.windowWidth ?? defaults.windowWidth ?? 1400 + property int settingsHeight: cfg.windowHeight ?? defaults.windowHeight ?? 0 + property bool autoHeight: cfg.autoHeight ?? defaults.autoHeight ?? true + property int columnCount: cfg.columnCount ?? defaults.columnCount ?? 3 + + // Bug 4 fix: re-evaluate rawCategories whenever Main.qml increments cheatsheetDataVersion + property int _dataVersion: pluginApi?.mainInstance?.cheatsheetDataVersion ?? 0 + property var rawCategories: { + var _v = _dataVersion; // force QML dependency on version counter + return pluginApi?.pluginSettings?.cheatsheetData || []; + } + property var categories: [] + + // Bug 5 fix: timeout if parsing never completes + property bool loadingTimedOut: false + + Timer { + id: loadingTimeoutTimer + interval: 4000 + repeat: false + running: false + onTriggered: { + if (root.isLoading) { + root.loadingTimedOut = true; + } + } + } + + Component.onCompleted: { + categories = processCategories(rawCategories); + // Start timeout timer if we have no data yet + if (root.isLoading) { + loadingTimeoutTimer.start(); + } + } + + + // Dynamic column items (up to 4 columns) + property var columnItems: [] + + // Memory leak prevention: debounce column updates + Timer { + id: columnUpdateDebounce + interval: 100 + repeat: false + onTriggered: updateColumnItemsNow() + } + + Component.onDestruction: { + // Stop timer to prevent firing after destruction + columnUpdateDebounce.stop(); + loadingTimeoutTimer.stop(); + + // Clear column items + columnItems = []; + } + + onRawCategoriesChanged: { + categories = processCategories(rawCategories); + updateColumnItems(); + } + + onCategoriesChanged: { + updateColumnItems(); + contentPreferredHeight = calculateDynamicHeight(); + } + + onColumnCountChanged: { + updateColumnItems(); + contentPreferredHeight = calculateDynamicHeight(); + } + + onPanelOpenScreenChanged: { + // Recalculate height when screen becomes available (important for bar widget opening) + contentPreferredHeight = calculateDynamicHeight(); + root.searchText = ""; + if (searchInput) searchInput.inputItem.forceActiveFocus(); + } + + onMaxScreenHeightChanged: { + contentPreferredHeight = calculateDynamicHeight(); + } + + function updateColumnItems() { + columnUpdateDebounce.restart(); + } + + function updateColumnItemsNow() { + columnItems = []; // Clear old items explicitly + var assignments = distributeCategories(); + var items = []; + for (var i = 0; i < columnCount; i++) { + items.push(buildColumnItems(assignments[i] || [])); + } + columnItems = items; + } + + // Screen height limit (90% of screen) + property var panelOpenScreen: pluginApi?.panelOpenScreen + property real maxScreenHeight: panelOpenScreen ? panelOpenScreen.height * 0.9 : 800 + + property string searchText: "" + + property real contentPreferredWidth: settingsWidth + property real contentPreferredHeight: calculateDynamicHeight() + readonly property var geometryPlaceholder: panelContainer + readonly property bool allowAttach: false + readonly property bool panelAnchorHorizontalCenter: true + readonly property bool panelAnchorVerticalCenter: true + anchors.fill: parent + + // Key badge colors — read from settings with manifest defaults as fallback + readonly property color keyColorAlt: cfg.keyColorAlt || defaults.keyColorAlt || "#FF6B6B" + readonly property color keyColorXF86: cfg.keyColorXF86 || defaults.keyColorXF86 || "#4ECDC4" + readonly property color keyColorPrint: cfg.keyColorPrint || defaults.keyColorPrint || "#95E1D3" + readonly property color keyColorNumeric: cfg.keyColorNumeric || defaults.keyColorNumeric || "#A8DADC" + readonly property color keyColorMouse: cfg.keyColorMouse || defaults.keyColorMouse || "#F38181" + // Empty string = use theme color (mPrimary/mSecondary/mTertiary) + readonly property string keyColorSuperOverride: cfg.keyColorSuper ?? defaults.keyColorSuper ?? "" + readonly property string keyColorCtrlOverride: cfg.keyColorCtrl ?? defaults.keyColorCtrl ?? "" + readonly property string keyColorShiftOverride: cfg.keyColorShift ?? defaults.keyColorShift ?? "" + readonly property color keyColorDefault: cfg.keyColorDefault || defaults.keyColorDefault || "#6C757D" + readonly property color keyLabelColor: cfg.keyLabelColor || defaults.keyLabelColor || "#FFFFFF" + // Empty string = theme-aware fallback (Color.mOnSurface). Any non-empty + // value is treated as an explicit user override. + readonly property string descriptionColorOverride: cfg.descriptionTextColor || defaults.descriptionTextColor || "" + readonly property color descriptionTextColor: descriptionColorOverride !== "" ? descriptionColorOverride : Color.mOnSurface + + // Per-category text color overrides (empty = fall back to keyLabelColor) + readonly property string keyTextSuperOverride: cfg.keyTextSuper ?? defaults.keyTextSuper ?? "" + readonly property string keyTextCtrlOverride: cfg.keyTextCtrl ?? defaults.keyTextCtrl ?? "" + readonly property string keyTextShiftOverride: cfg.keyTextShift ?? defaults.keyTextShift ?? "" + readonly property string keyTextAltOverride: cfg.keyTextAlt ?? defaults.keyTextAlt ?? "" + readonly property string keyTextXF86Override: cfg.keyTextXF86 ?? defaults.keyTextXF86 ?? "" + readonly property string keyTextPrintOverride: cfg.keyTextPrint ?? defaults.keyTextPrint ?? "" + readonly property string keyTextNumericOverride: cfg.keyTextNumeric ?? defaults.keyTextNumeric ?? "" + readonly property string keyTextMouseOverride: cfg.keyTextMouse ?? defaults.keyTextMouse ?? "" + readonly property string keyTextDefaultOverride: cfg.keyTextDefault ?? defaults.keyTextDefault ?? "" + + // The generic X11 mods (Mod2/Mod3/Mod5) are first-class, customizable keys: + // background colour, text colour, and a display-label override that renames + // them to something meaningful (e.g. Mod3 -> "Hyper" for caps:hyper). Empty + // colour = fall back to the default key colour; empty label = raw mod name. + readonly property string keyColorMod2Override: cfg.keyColorMod2 ?? defaults.keyColorMod2 ?? "" + readonly property string keyColorMod3Override: cfg.keyColorMod3 ?? defaults.keyColorMod3 ?? "" + readonly property string keyColorMod5Override: cfg.keyColorMod5 ?? defaults.keyColorMod5 ?? "" + readonly property string keyTextMod2Override: cfg.keyTextMod2 ?? defaults.keyTextMod2 ?? "" + readonly property string keyTextMod3Override: cfg.keyTextMod3 ?? defaults.keyTextMod3 ?? "" + readonly property string keyTextMod5Override: cfg.keyTextMod5 ?? defaults.keyTextMod5 ?? "" + readonly property string keyLabelMod2Override: cfg.keyLabelMod2 ?? defaults.keyLabelMod2 ?? "" + readonly property string keyLabelMod3Override: cfg.keyLabelMod3 ?? defaults.keyLabelMod3 ?? "" + readonly property string keyLabelMod5Override: cfg.keyLabelMod5 ?? defaults.keyLabelMod5 ?? "" + + // Workspace category split tuning + readonly property bool splitWorkspaces: cfg.splitLargeWorkspaceCategory ?? defaults.splitLargeWorkspaceCategory ?? true + readonly property int workspaceSplitThreshold: cfg.workspaceSplitThreshold ?? defaults.workspaceSplitThreshold ?? 12 + + // Data is loaded by Main.qml, we just display it + property bool isLoading: rawCategories.length === 0 + + function calculateDynamicHeight() { + // If auto height is disabled, use manual height (but still respect screen limit) + if (!autoHeight && settingsHeight > 0) { + return Math.min(settingsHeight, maxScreenHeight); + } + + if (categories.length === 0) return Math.min(400, maxScreenHeight); + + var assignments = distributeCategories(); + var maxColumnHeight = 0; + + for (var col = 0; col < columnCount; col++) { + var colHeight = 0; + var catIndices = assignments[col] || []; + + for (var i = 0; i < catIndices.length; i++) { + var catIndex = catIndices[i]; + if (catIndex >= categories.length) continue; + + var cat = categories[catIndex]; + colHeight += 26; // Header + colHeight += cat.binds.length * 20; // Binds + if (i < catIndices.length - 1) { + colHeight += 6; // Spacer + } + } + + if (colHeight > maxColumnHeight) { + maxColumnHeight = colHeight; + } + } + + // header (45) + content + margins (16) + var totalHeight = 45 + maxColumnHeight + 16 + 15 + 15; + // Limit to 80% of screen height + return Math.max(300, Math.min(totalHeight, maxScreenHeight)); + } + + // ========== UI ========== + Rectangle { + id: panelContainer + anchors.fill: parent + color: "transparent" + radius: Style.radiusL + clip: true + + Rectangle { + id: header + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 45 + color: Color.mSurfaceVariant + radius: Style.radiusL + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + spacing: Style.marginS + + // Title section (centered) + Item { Layout.fillWidth: true } + + NIcon { + icon: "keyboard" + pointSize: Style.fontSizeM + color: Color.mPrimary + } + NText { + text: CompositorService.isHyprland ? pluginApi?.tr("panel.title-hyprland") : + CompositorService.isNiri ? pluginApi?.tr("panel.title-niri") : + CompositorService.isMango ? pluginApi?.tr("panel.title-mango") : + pluginApi?.tr("panel.title") + font.pointSize: Style.fontSizeM + font.weight: Font.Bold + color: Color.mPrimary + } + + NTextInput { + id: searchInput + placeholderText: pluginApi?.tr("panel.search-placeholder") + text: root.searchText + + onTextChanged: { + root.searchText = text; + root.updateColumnItems(); + } + } + + Item { Layout.fillWidth: true } + + // Refresh button + NIconButton { + icon: "refresh" + onClicked: { + pluginApi?.mainInstance?.refresh(); + } + } + + // Settings button + NIconButton { + icon: "settings" + onClicked: { + var screen = pluginApi?.panelOpenScreen; + if (screen && pluginApi?.manifest) { + pluginApi.closePanel(screen); + BarService.openPluginSettings(screen, pluginApi.manifest); + } + } + } + } + } + + NText { + id: loadingText + anchors.centerIn: parent + text: root.loadingTimedOut ? pluginApi?.tr("panel.loading-timeout") : pluginApi?.tr("panel.loading") + visible: root.isLoading + font.pointSize: Style.fontSizeL + color: Color.mOnSurface + } + + NScrollView { + id: scrollView + visible: root.categories.length > 0 && !root.isLoading + anchors.top: header.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + clip: true + leftPadding: 35 + rightPadding: -10 + topPadding: 15 + bottomPadding: 15 + + RowLayout { + id: mainLayout + width: scrollView.availableWidth - Style.marginS + spacing: Style.marginS + + Repeater { + model: root.columnItems.length + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: 2 + + property var colItems: root.columnItems[index] || [] + + Repeater { + model: colItems + Loader { + Layout.fillWidth: true + sourceComponent: modelData.type === "header" ? headerComponent : + (modelData.type === "spacer" ? spacerComponent : bindComponent) + property var itemData: modelData + + // Memory leak prevention: explicit cleanup + Component.onDestruction: { + active = false; + sourceComponent = undefined; + } + } + } + } + } + } + } + } + + Component { + id: headerComponent + ColumnLayout { + Layout.preferredWidth: 300 + Layout.topMargin: Style.marginM + Layout.bottomMargin: 4 + spacing: 0 + + Item { Layout.fillWidth: true; height: 1 } + + NText { + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + x: parent.width - implicitWidth + text: itemData.title + font.pointSize: Style.fontSizeM + font.weight: Font.Bold + color: Color.mPrimary + } + + Item { Layout.fillWidth: true; height: 1 } + } + } + + + Component { + id: spacerComponent + Item { + height: 10 + Layout.fillWidth: true + } + } + + Component { + id: bindComponent + RowLayout { + id: bindRow + spacing: Style.marginS + height: 22 + Layout.bottomMargin: 1 + + property bool editing: false + + Flow { + Layout.preferredWidth: 220 + Layout.alignment: Qt.AlignVCenter + spacing: 3 + Repeater { + model: itemData.keys.split(" + ") + Rectangle { + width: keyText.implicitWidth + 10 + height: 18 + color: getKeyColor(modelData) + radius: 3 + NText { + id: keyText + anchors.centerIn: parent + text: getKeyLabel(modelData) + font.pointSize: text.length > 12 ? 7 : 8 + font.weight: Font.Bold + color: getKeyTextColor(modelData) + } + } + } + } + + // Described bind: plain text (unchanged behaviour) + NText { + visible: !itemData.undescribed + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + text: itemData.desc + font.pointSize: Style.fontSizeXS + color: root.descriptionTextColor + elide: Text.ElideRight + } + + // Undescribed bind: placeholder + add-description / hide actions + NText { + visible: itemData.undescribed && !bindRow.editing + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + text: pluginApi?.tr("panel.no-description") + font.pointSize: Style.fontSizeXS + font.italic: true + color: Color.mOnSurfaceVariant + elide: Text.ElideRight + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { bindRow.editing = true; descInput.text = ""; descInput.inputItem.forceActiveFocus(); } + } + } + + NTextInput { + id: descInput + visible: itemData.undescribed && bindRow.editing + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize + placeholderText: pluginApi?.tr("panel.add-description-placeholder") + } + + NIconButton { + visible: itemData.undescribed && bindRow.editing + Layout.preferredHeight: 18 + icon: "check" + tooltipText: pluginApi?.tr("panel.save-description") + onClicked: { + root.saveBindDescription(itemData.bindId, descInput.text); + bindRow.editing = false; + } + } + + NIconButton { + visible: itemData.undescribed && bindRow.editing + Layout.preferredHeight: 18 + icon: "close" + tooltipText: pluginApi?.tr("panel.cancel") + onClicked: { bindRow.editing = false; } + } + + NIconButton { + visible: itemData.undescribed && !bindRow.editing + Layout.preferredHeight: 18 + icon: "edit" + tooltipText: pluginApi?.tr("panel.add-description") + onClicked: { bindRow.editing = true; descInput.text = ""; descInput.inputItem.forceActiveFocus(); } + } + + NIconButton { + visible: itemData.undescribed && !bindRow.editing + Layout.preferredHeight: 18 + icon: "eye-off" + tooltipText: pluginApi?.tr("panel.hide-bind") + onClicked: { root.hideBind(itemData.bindId); } + } + } + } + + function getKeyColor(keyName) { + if (keyName === "Super") return root.keyColorSuperOverride || Color.mPrimary; + if (keyName === "Ctrl") return root.keyColorCtrlOverride || Color.mSecondary; + if (keyName === "Shift") return root.keyColorShiftOverride || Color.mTertiary; + if (keyName === "Alt") return root.keyColorAlt; + if (keyName.startsWith("XF86")) return root.keyColorXF86; + if (keyName === "PRINT" || keyName === "Print") return root.keyColorPrint; + if (keyName.match(/^[0-9]$/)) return root.keyColorNumeric; + if (keyName.includes("MOUSE") || keyName.includes("Wheel")) return root.keyColorMouse; + if (keyName === "Mod2") return root.keyColorMod2Override || root.keyColorDefault; + if (keyName === "Mod3") return root.keyColorMod3Override || root.keyColorDefault; + if (keyName === "Mod5") return root.keyColorMod5Override || root.keyColorDefault; + return root.keyColorDefault; + } + + function getKeyTextColor(keyName) { + if (keyName === "Super") return root.keyTextSuperOverride || root.keyLabelColor; + if (keyName === "Ctrl") return root.keyTextCtrlOverride || root.keyLabelColor; + if (keyName === "Shift") return root.keyTextShiftOverride || root.keyLabelColor; + if (keyName === "Alt") return root.keyTextAltOverride || root.keyLabelColor; + if (keyName.startsWith("XF86")) return root.keyTextXF86Override || root.keyLabelColor; + if (keyName === "PRINT" || keyName === "Print") return root.keyTextPrintOverride || root.keyLabelColor; + if (keyName.match(/^[0-9]$/)) return root.keyTextNumericOverride || root.keyLabelColor; + if (keyName.includes("MOUSE") || keyName.includes("Wheel")) return root.keyTextMouseOverride || root.keyLabelColor; + if (keyName === "Mod2") return root.keyTextMod2Override || root.keyLabelColor; + if (keyName === "Mod3") return root.keyTextMod3Override || root.keyLabelColor; + if (keyName === "Mod5") return root.keyTextMod5Override || root.keyLabelColor; + return root.keyTextDefaultOverride || root.keyLabelColor; + } + + // Display label for a key token. Lets users rename the generic X11 modifier + // names (e.g. Mod3 -> "Hyper"); everything else renders as-is. Purely visual — + // colour lookup and the cached bind still use the original token. + function getKeyLabel(keyName) { + if (keyName === "Mod2" && root.keyLabelMod2Override) return root.keyLabelMod2Override; + if (keyName === "Mod3" && root.keyLabelMod3Override) return root.keyLabelMod3Override; + if (keyName === "Mod5" && root.keyLabelMod5Override) return root.keyLabelMod5Override; + return keyName; + } + + // ===== Bind override helpers (shared keyed map in plugin settings) ===== + function _cloneOverrides() { + if (!pluginApi || !pluginApi.pluginSettings) return ({}); + var src = pluginApi.pluginSettings.bindOverrides || ({}); + try { return JSON.parse(JSON.stringify(src)); } catch (e) { return ({}); } + } + + function saveBindDescription(bindId, desc) { + if (!pluginApi || !bindId) return; + var o = _cloneOverrides(); + if (!o[bindId]) o[bindId] = ({}); + var trimmed = (desc || "").trim(); + if (trimmed.length === 0) { + delete o[bindId].desc; + if (Object.keys(o[bindId]).length === 0) delete o[bindId]; + } else { + o[bindId].desc = trimmed; + } + pluginApi.pluginSettings.bindOverrides = o; + pluginApi.saveSettings(); + pluginApi.mainInstance?.refresh(); + } + + function hideBind(bindId) { + if (!pluginApi || !bindId) return; + var o = _cloneOverrides(); + if (!o[bindId]) o[bindId] = ({}); + o[bindId].hidden = true; + pluginApi.pluginSettings.bindOverrides = o; + pluginApi.saveSettings(); + pluginApi.mainInstance?.refresh(); + } + + function buildColumnItems(categoryIndices) { + var result = []; + if (!categoryIndices) return result; + + for (var i = 0; i < categoryIndices.length; i++) { + var catIndex = categoryIndices[i]; + if (catIndex >= categories.length) continue; + + var cat = categories[catIndex]; + result.push({ type: "header", title: cat.title }); + var term = root.searchText.toLowerCase(); + for (var j = 0; j < cat.binds.length; j++) { + var bnd = cat.binds[j]; + var isUndesc = bnd.undescribed === true; + if (!term || (bnd.desc && bnd.desc.toLowerCase().indexOf(term) !== -1) || (isUndesc && bnd.keys.toLowerCase().indexOf(term) !== -1)) { + result.push({ + type: "bind", + keys: bnd.keys, + desc: bnd.desc, + bindId: bnd.bindId || "", + undescribed: isUndesc + }); + } + } + if (i < categoryIndices.length - 1) { + result.push({ type: "spacer" }); + } + } + return result; + } + + function processCategories(cats) { + if (!cats || cats.length === 0) return []; + if (!root.splitWorkspaces) return cats; + + var result = []; + for (var i = 0; i < cats.length; i++) { + var cat = cats[i]; + if (!cat.binds || cat.binds.length <= root.workspaceSplitThreshold) { + result.push(cat); + continue; + } + + // Detect a category dominated by workspace verbs. + // Hyprland verbs: workspace, movetoworkspace, movetoworkspacesilent, movecurrentworkspacetomonitor + // Niri verbs: focus-workspace, move-window-to-workspace, move-column-to-workspace + var workspaceCount = 0; + for (var k = 0; k < cat.binds.length; k++) { + var v = cat.binds[k]._verb || ""; + if (v.indexOf("workspace") !== -1) workspaceCount++; + } + var workspaceDominated = workspaceCount >= Math.ceil(cat.binds.length * 0.6); + if (!workspaceDominated) { + result.push(cat); + continue; + } + + var switching = [], moving = [], mouse = []; + for (var j = 0; j < cat.binds.length; j++) { + var bind = cat.binds[j]; + var verb = bind._verb || ""; + var isMouse = (bind._mainKey || "").indexOf("MOUSE") !== -1 || + (bind.keys || "").indexOf("MOUSE") !== -1 || + (bind.keys || "").indexOf("Wheel") !== -1; + + if (isMouse) { + mouse.push(bind); + } else if (verb.indexOf("move") !== -1 || verb.indexOf("send") !== -1) { + // Hyprland: movetoworkspace*, Niri: move-window-to-workspace, move-column-to-workspace + moving.push(bind); + } else { + // Hyprland: workspace, Niri: focus-workspace + switching.push(bind); + } + } + + if (switching.length > 0) result.push({ title: pluginApi?.tr("panel.workspace-switching"), binds: switching }); + if (moving.length > 0) result.push({ title: pluginApi?.tr("panel.workspace-moving"), binds: moving }); + if (mouse.length > 0) result.push({ title: pluginApi?.tr("panel.workspace-mouse"), binds: mouse }); + } + return result; + } + + function distributeCategories() { + var numCols = root.columnCount; + + // Calculate weights for each category + var catData = []; + for (var i = 0; i < categories.length; i++) { + var weight = 1 + categories[i].binds.length + 1; // header + binds + spacer + catData.push({ index: i, weight: weight }); + } + + // Sort by weight descending (largest categories first for better distribution) + catData.sort(function(a, b) { return b.weight - a.weight; }); + + var columns = []; + var columnWeights = []; + for (var c = 0; c < numCols; c++) { + columns.push([]); + columnWeights.push(0); + } + + // Assign each category to the column with smallest current weight + for (var i = 0; i < catData.length; i++) { + var minCol = 0; + for (var c = 1; c < numCols; c++) { + if (columnWeights[c] < columnWeights[minCol]) { + minCol = c; + } + } + columns[minCol].push(catData[i].index); + columnWeights[minCol] += catData[i].weight; + } + + // Sort categories within each column by original order for consistent display + for (var c = 0; c < numCols; c++) { + columns[c].sort(function(a, b) { return a - b; }); + } + + return columns; + } + +} |
