From 65983ade46c4a131b6534bc264969836745bf362 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:59:52 +0200 Subject: [PATCH] sidebar: translator: language selector --- .../modules/common/ConfigOptions.qml | 8 + .../modules/common/widgets/ButtonGroup.qml | 2 +- .../common/widgets/SelectionDialog.qml | 127 +++++++ .../modules/sidebarLeft/Translator.qml | 311 +++++++++--------- .../translator/LanguageSelectorButton.qml | 42 +++ .../sidebarLeft/translator/TextCanvas.qml | 92 ++++++ .../sidebarRight/volumeMixer/VolumeMixer.qml | 11 +- .config/quickshell/shell.qml | 2 +- 8 files changed, 439 insertions(+), 156 deletions(-) create mode 100644 .config/quickshell/modules/common/widgets/SelectionDialog.qml create mode 100644 .config/quickshell/modules/sidebarLeft/translator/LanguageSelectorButton.qml create mode 100644 .config/quickshell/modules/sidebarLeft/translator/TextCanvas.qml diff --git a/.config/quickshell/modules/common/ConfigOptions.qml b/.config/quickshell/modules/common/ConfigOptions.qml index 4aa54fa52..902049729 100644 --- a/.config/quickshell/modules/common/ConfigOptions.qml +++ b/.config/quickshell/modules/common/ConfigOptions.qml @@ -63,6 +63,14 @@ Singleton { ] } + property QtObject language: QtObject { + property QtObject translator: QtObject { + property string engine: "auto" // Run `trans -list-engines` for available engines. auto should use google + property string sourceLanguage: "auto" + property string targetLanguage: "English" // Run `trans -list-all` for available languages + } + } + property QtObject networking: QtObject { property string userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" } diff --git a/.config/quickshell/modules/common/widgets/ButtonGroup.qml b/.config/quickshell/modules/common/widgets/ButtonGroup.qml index a1570c6af..5356535f4 100644 --- a/.config/quickshell/modules/common/widgets/ButtonGroup.qml +++ b/.config/quickshell/modules/common/widgets/ButtonGroup.qml @@ -11,7 +11,7 @@ import QtQuick.Layouts */ Rectangle { id: root - default property alias content: rowLayout.data + default property alias data: rowLayout.data property real spacing: 5 property real padding: 0 property int clickIndex: rowLayout.clickIndex diff --git a/.config/quickshell/modules/common/widgets/SelectionDialog.qml b/.config/quickshell/modules/common/widgets/SelectionDialog.qml new file mode 100644 index 000000000..1e1446b51 --- /dev/null +++ b/.config/quickshell/modules/common/widgets/SelectionDialog.qml @@ -0,0 +1,127 @@ +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/services" +import Qt5Compat.GraphicalEffects +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell + +Item { + id: root + property real dialogPadding: 15 + property real dialogMargin: 30 + property string titleText: "Selection Dialog" + property alias items: choiceModel.values + property int selectedId: -1 // -1 means no selection + + signal canceled(); + signal selected(var result); + + Rectangle { // Scrim + id: scrimOverlay + anchors.fill: parent + radius: Appearance.rounding.small + color: Appearance.colors.colScrim + MouseArea { + hoverEnabled: true + anchors.fill: parent + preventStealing: true + propagateComposedEvents: false + } + } + + Rectangle { // The dialog + id: dialog + color: Appearance.m3colors.m3surfaceContainerHigh + radius: Appearance.rounding.normal + anchors.fill: parent + anchors.margins: dialogMargin + implicitHeight: dialogColumnLayout.implicitHeight + + ColumnLayout { + id: dialogColumnLayout + anchors.fill: parent + spacing: 16 + + StyledText { + id: dialogTitle + Layout.topMargin: dialogPadding + Layout.leftMargin: dialogPadding + Layout.rightMargin: dialogPadding + Layout.alignment: Qt.AlignLeft + color: Appearance.m3colors.m3onSurface + font.pixelSize: Appearance.font.pixelSize.larger + text: root.titleText + } + + Rectangle { + color: Appearance.m3colors.m3outline + implicitHeight: 1 + Layout.fillWidth: true + Layout.leftMargin: dialogPadding + Layout.rightMargin: dialogPadding + } + + ListView { + id: choiceListView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + model: ScriptModel { + id: choiceModel + } + + delegate: StyledRadioButton { + id: radioButton + required property var modelData + required property int index + anchors { + left: parent?.left + right: parent?.right + leftMargin: root.dialogPadding + rightMargin: root.dialogPadding + } + + description: modelData.toString() + checked: index === root.selectedId + + onCheckedChanged: { + if (checked) { + root.selectedId = index; + } + } + } + } + + Rectangle { + color: Appearance.m3colors.m3outline + implicitHeight: 1 + Layout.fillWidth: true + Layout.leftMargin: dialogPadding + Layout.rightMargin: dialogPadding + } + + RowLayout { + id: dialogButtonsRowLayout + Layout.bottomMargin: dialogPadding + Layout.leftMargin: dialogPadding + Layout.rightMargin: dialogPadding + Layout.alignment: Qt.AlignRight + + DialogButton { + buttonText: qsTr("Cancel") + onClicked: root.canceled() + } + DialogButton { + buttonText: qsTr("OK") + onClicked: root.selected( + root.selectedId === -1 ? null : + root.items[root.selectedId] + ) + } + } + } + } +} diff --git a/.config/quickshell/modules/sidebarLeft/Translator.qml b/.config/quickshell/modules/sidebarLeft/Translator.qml index a853f30cf..92e0505db 100644 --- a/.config/quickshell/modules/sidebarLeft/Translator.qml +++ b/.config/quickshell/modules/sidebarLeft/Translator.qml @@ -3,6 +3,7 @@ import "root:/services" import "root:/modules/common" import "root:/modules/common/widgets" import "root:/modules/common/functions/string_utils.js" as StringUtils +import "./translator/" import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -15,11 +16,26 @@ import Quickshell.Hyprland */ Item { id: root - property var inputField: inputTextArea - property var outputField: outputTextArea - + // Widgets + property var inputField: inputCanvas.inputTextArea + // Widget variables property bool translationFor: false // Indicates if the translation is for an autocorrected text property string translatedText: "" + property list languages: [] + // Options + property string targetLanguage: ConfigOptions.language.translator.targetLanguage + property string sourceLanguage: ConfigOptions.language.translator.sourceLanguage + property string hostLanguage: targetLanguage + + property bool showLanguageSelector: false + property bool languageSelectorTarget: false // true for target language, false for source language + property string languageSelectorLanguage: "" + + function showLanguageSelectorDialog(isTargetLang: bool) { + root.showLanguageSelector = true + root.languageSelectorTarget = isTargetLang; + root.languageSelectorLanguage = isTargetLang ? root.targetLanguage : root.sourceLanguage; + } onFocusChanged: (focus) => { if (focus) { @@ -32,19 +48,23 @@ Item { interval: ConfigOptions.sidebar.translator.delay repeat: false onTriggered: () => { - if (inputTextArea.text.trim().length > 0) { + if (root.inputField.text.trim().length > 0) { + console.log("Translating with command:", translateProc.command); translateProc.running = false; translateProc.buffer = ""; // Clear the buffer translateProc.running = true; // Restart the process } else { - outputTextArea.text = ""; + root.translatedText = ""; } } } Process { id: translateProc - command: ["bash", "-c", `trans -no-theme -no-ansi '${StringUtils.shellSingleQuoteEscape(inputTextArea.text.trim())}'`] + command: ["bash", "-c", `trans -no-theme` + + ` -source '${StringUtils.shellSingleQuoteEscape(root.sourceLanguage)}'` + + ` -target '${StringUtils.shellSingleQuoteEscape(root.targetLanguage)}'` + + ` -no-ansi '${StringUtils.shellSingleQuoteEscape(root.inputField.text.trim())}'`] property string buffer: "" stdout: SplitParser { onRead: data => { @@ -54,12 +74,29 @@ Item { onExited: (exitCode, exitStatus) => { // 1. Split into sections by double newlines const sections = translateProc.buffer.trim().split(/\n\s*\n/); - // console.log("BUFFER:", translateProc.buffer); - // console.log("SECTIONS:", sections); + console.log("BUFFER:", translateProc.buffer); + console.log("SECTIONS:", sections); // 2. Extract relevant data root.translatedText = sections.length > 1 ? sections[1].trim() : ""; - root.outputField.text = root.translatedText; + } + } + + Process { + id: getLanguagesProc + command: ["trans", "-list-languages"] + property list bufferList: ["auto"] + running: true + stdout: SplitParser { + onRead: data => { + getLanguagesProc.bufferList.push(data.trim()); + } + } + onExited: (exitCode, exitStatus) => { + root.languages = getLanguagesProc.bufferList + .filter(lang => lang.trim().length > 0) // Filter out empty lines + .sort((a, b) => a.localeCompare(b)); // Sort alphabetically + getLanguagesProc.bufferList = []; // Clear the buffer } } @@ -71,159 +108,133 @@ Item { id: contentColumn anchors.fill: parent - Rectangle { // INPUT + LanguageSelectorButton { // Source language button + id: sourceLanguageButton + displayText: root.sourceLanguage + onClicked: { + root.showLanguageSelectorDialog(false); + } + } + + TextCanvas { // Content input id: inputCanvas - Layout.fillWidth: true - implicitHeight: Math.max(150, inputColumn.implicitHeight) - color: Appearance.colors.colLayer1 - radius: Appearance.rounding.normal - border.color: Appearance.m3colors.m3outlineVariant - border.width: 1 - - ColumnLayout { - id: inputColumn - anchors.fill: parent - spacing: 0 - - StyledTextArea { // Input area - id: inputTextArea - Layout.fillWidth: true - placeholderText: qsTr("Enter text to translate...") - wrapMode: TextEdit.Wrap - textFormat: TextEdit.PlainText - font.pixelSize: Appearance.font.pixelSize.small - color: Appearance.colors.colOnLayer1 - padding: 15 - background: null - onTextChanged: { - if (inputTextArea.text.trim().length > 0) { - translateTimer.restart(); - } else { - outputTextArea.text = ""; - } - } + isInput: true + placeholderText: qsTr("Enter text to translate...") + onInputTextChanged: { + translateTimer.restart(); + } + GroupButton { + id: pasteButton + baseWidth: height + buttonRadius: Appearance.rounding.small + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + iconSize: Appearance.font.pixelSize.larger + text: "content_paste" + color: deleteButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext } - - Item { Layout.fillHeight: true } - - RowLayout { // Status row - Layout.fillWidth: true - Layout.margins: 10 - spacing: 10 - - Text { - Layout.leftMargin: 10 - text: qsTr("%1 characters").arg(inputTextArea.text.length) - color: Appearance.colors.colOnLayer1 - font.pixelSize: Appearance.font.pixelSize.smaller - } - Item { Layout.fillWidth: true } - ButtonGroup { - GroupButton { - id: pasteButton - baseWidth: height - buttonRadius: Appearance.rounding.small - contentItem: MaterialSymbol { - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - iconSize: Appearance.font.pixelSize.larger - text: "content_paste" - color: deleteButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext - } - onClicked: { - root.inputField.text = Quickshell.clipboardText - } - } - GroupButton { - id: deleteButton - baseWidth: height - buttonRadius: Appearance.rounding.small - enabled: inputTextArea.text.length > 0 - contentItem: MaterialSymbol { - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - iconSize: Appearance.font.pixelSize.larger - text: "close" - color: deleteButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext - } - onClicked: { - root.inputField.text = "" - } - } - } + onClicked: { + root.inputField.text = Quickshell.clipboardText + } + } + GroupButton { + id: deleteButton + baseWidth: height + buttonRadius: Appearance.rounding.small + enabled: inputCanvas.inputTextArea.text.length > 0 + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + iconSize: Appearance.font.pixelSize.larger + text: "close" + color: deleteButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext + } + onClicked: { + root.inputField.text = "" } } } - Rectangle { // OUTPUT + LanguageSelectorButton { // Target language button + id: targetLanguageButton + displayText: root.targetLanguage + onClicked: { + root.showLanguageSelectorDialog(true); + } + } + + TextCanvas { // Content translation id: outputCanvas - Layout.fillWidth: true - implicitHeight: Math.max(150, outputColumn.implicitHeight) - color: Appearance.m3colors.m3surfaceContainer - radius: Appearance.rounding.normal - - ColumnLayout { // Output column - id: outputColumn - anchors.fill: parent - spacing: 0 - - StyledText { // Output area - id: outputTextArea - Layout.fillWidth: true - property bool hasTranslation: (root.translatedText.trim().length > 0) - wrapMode: TextEdit.Wrap - font.pixelSize: Appearance.font.pixelSize.small - color: hasTranslation ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext - padding: 15 - text: hasTranslation ? root.translatedText : "" + isInput: false + placeholderText: qsTr("Translation goes here...") + property bool hasTranslation: (root.translatedText.trim().length > 0) + text: hasTranslation ? root.translatedText : "" + GroupButton { + id: copyButton + baseWidth: height + buttonRadius: Appearance.rounding.small + enabled: outputCanvas.displayedText.trim().length > 0 + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + iconSize: Appearance.font.pixelSize.larger + text: "content_copy" + color: copyButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext } - Item { Layout.fillHeight: true } - RowLayout { // Status row - Layout.fillWidth: true - Layout.margins: 10 - spacing: 10 - Item { Layout.fillWidth: true } - ButtonGroup { - GroupButton { - id: copyButton - baseWidth: height - buttonRadius: Appearance.rounding.small - enabled: root.outputField.text.trim().length > 0 - contentItem: MaterialSymbol { - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - iconSize: Appearance.font.pixelSize.larger - text: "content_copy" - color: deleteButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext - } - onClicked: { - Quickshell.clipboardText = root.outputField.text - } - } - GroupButton { - id: searchButton - baseWidth: height - buttonRadius: Appearance.rounding.small - enabled: root.outputField.text.trim().length > 0 - contentItem: MaterialSymbol { - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - iconSize: Appearance.font.pixelSize.larger - text: "travel_explore" - color: deleteButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext - } - onClicked: { - let url = ConfigOptions.search.engineBaseUrl + root.outputField.text; - for (let site of ConfigOptions.search.excludedSites) { - url += ` -site:${site}`; - } - Qt.openUrlExternally(url); - } - } + onClicked: { + Quickshell.clipboardText = outputCanvas.displayedText + } + } + GroupButton { + id: searchButton + baseWidth: height + buttonRadius: Appearance.rounding.small + enabled: outputCanvas.displayedText.trim().length > 0 + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + iconSize: Appearance.font.pixelSize.larger + text: "travel_explore" + color: searchButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext + } + onClicked: { + let url = ConfigOptions.search.engineBaseUrl + outputCanvas.displayedText; + for (let site of ConfigOptions.search.excludedSites) { + url += ` -site:${site}`; } + Qt.openUrlExternally(url); } } } + } } + + Loader { + anchors.fill: parent + active: root.showLanguageSelector + visible: root.showLanguageSelector + z: 9999 + sourceComponent: SelectionDialog { + id: languageSelectorDialog + titleText: qsTr("Select Language") + items: root.languages + onCanceled: () => { + root.showLanguageSelector = false; + } + onSelected: (result) => { + root.showLanguageSelector = false; + if (!result || result.length === 0) return; // No selection made + + if (root.languageSelectorTarget) { + root.targetLanguage = result; + ConfigOptions.language.translator.targetLanguage = result; // Save to config + } else { + root.sourceLanguage = result; + ConfigOptions.language.translator.sourceLanguage = result; // Save to config + } + } + } + } } diff --git a/.config/quickshell/modules/sidebarLeft/translator/LanguageSelectorButton.qml b/.config/quickshell/modules/sidebarLeft/translator/LanguageSelectorButton.qml new file mode 100644 index 000000000..a78559517 --- /dev/null +++ b/.config/quickshell/modules/sidebarLeft/translator/LanguageSelectorButton.qml @@ -0,0 +1,42 @@ +import "root:/" +import "root:/services" +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/string_utils.js" as StringUtils +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland + +RippleButton { + id: root + property string displayText: "" + colBackground: Appearance.colors.colLayer2 + + contentItem: Item { + anchors.centerIn: parent + implicitWidth: languageRow.implicitWidth + implicitHeight: languageText.implicitHeight + RowLayout { + id: languageRow + anchors.centerIn: parent + spacing: 0 + StyledText { + id: languageText + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: 5 + text: root.displayText + color: Appearance.colors.colOnLayer2 + font.pixelSize: Appearance.font.pixelSize.small + } + MaterialSymbol { + Layout.alignment: Qt.AlignVCenter + iconSize: Appearance.font.pixelSize.hugeass + text: "arrow_drop_down" + color: Appearance.colors.colOnLayer2 + } + } + } +} diff --git a/.config/quickshell/modules/sidebarLeft/translator/TextCanvas.qml b/.config/quickshell/modules/sidebarLeft/translator/TextCanvas.qml new file mode 100644 index 000000000..7f32a63d9 --- /dev/null +++ b/.config/quickshell/modules/sidebarLeft/translator/TextCanvas.qml @@ -0,0 +1,92 @@ +import "root:/" +import "root:/services" +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/string_utils.js" as StringUtils +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland + +Rectangle { + id: root + property bool isInput: true // true for input, false for output + property string placeholderText + property string text: "" + property var inputTextArea: isInput ? inputLoader.item : undefined + readonly property string displayedText: isInput ? inputLoader.item.text : + root.text.length > 0 ? outputLoader.item.text : "" + default property alias actionButtons: actions.data + Layout.fillWidth: true + implicitHeight: Math.max(150, inputColumn.implicitHeight) + color: isInput ? Appearance.colors.colLayer1 : Appearance.m3colors.m3surfaceContainer + radius: Appearance.rounding.normal + border.color: isInput ? Appearance.m3colors.m3outlineVariant : "transparent" + border.width: isInput ? 1 : 0 + + signal inputTextChanged(); // Signal emitted when text changes + + ColumnLayout { + id: inputColumn + anchors.fill: parent + spacing: 0 + + Loader { + id: inputLoader + active: root.isInput + visible: root.isInput + Layout.fillWidth: true + sourceComponent: StyledTextArea { // Input area + id: inputTextArea + placeholderText: root.placeholderText + wrapMode: TextEdit.Wrap + textFormat: TextEdit.PlainText + font.pixelSize: Appearance.font.pixelSize.small + color: Appearance.colors.colOnLayer1 + padding: 15 + background: null + onTextChanged: root.inputTextChanged() + } + } + + Loader { + id: outputLoader + active: !root.isInput + visible: !root.isInput + Layout.fillWidth: true + sourceComponent: StyledText { // Output area + id: outputTextArea + padding: 15 + wrapMode: Text.Wrap + font.pixelSize: Appearance.font.pixelSize.small + color: root.text.length > 0 ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext + text: root.text.length > 0 ? root.text : root.placeholderText + } + } + + Item { Layout.fillHeight: true } + + RowLayout { // Status row + Layout.fillWidth: true + Layout.margins: 10 + spacing: 10 + + Loader { + active: root.isInput + visible: root.isInput + Layout.leftMargin: 10 + sourceComponent: Text { + text: qsTr("%1 characters").arg(inputLoader.item.text.length) + color: Appearance.colors.colOnLayer1 + font.pixelSize: Appearance.font.pixelSize.smaller + } + } + Item { Layout.fillWidth: true } + ButtonGroup { + id: actions + } + } + } +} \ No newline at end of file diff --git a/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixer.qml b/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixer.qml index a7a88a403..1799df556 100644 --- a/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixer.qml +++ b/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixer.qml @@ -5,6 +5,7 @@ import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell import Quickshell.Widgets import Quickshell.Services.Pipewire @@ -16,7 +17,7 @@ Item { property int dialogMargins: 16 property PwNode selectedDevice - function showDeviceSelectorDialog(input) { + function showDeviceSelectorDialog(input: bool) { root.selectedDevice = null root.showDeviceSelector = true root.deviceSelectorInput = input @@ -207,9 +208,11 @@ Item { spacing: 0 Repeater { - model: Pipewire.nodes.values.filter(node => { - return !node.isStream && node.isSink !== root.deviceSelectorInput && node.audio - }) + model: ScriptModel { + values: Pipewire.nodes.values.filter(node => { + return !node.isStream && node.isSink !== root.deviceSelectorInput && node.audio + }) + } // This could and should be refractored, but all data becomes null when passed wtf delegate: StyledRadioButton { diff --git a/.config/quickshell/shell.qml b/.config/quickshell/shell.qml index 4acb7cccb..35a5008ea 100644 --- a/.config/quickshell/shell.qml +++ b/.config/quickshell/shell.qml @@ -29,7 +29,7 @@ ShellRoot { property bool enableBar: true property bool enableBackgroundWidgets: true property bool enableCheatsheet: true - property bool enableDock: true + property bool enableDock: false property bool enableMediaControls: true property bool enableNotificationPopup: true property bool enableOnScreenDisplayBrightness: true