/* SPDX-FileCopyrightText: 2020 Devin Lin SPDX-FileCopyrightText: 2021 Carl Schwan SPDX-FileCopyrightText: 2023 ivan tkachenko SPDX-License-Identifier: LGPL-2.0-or-later */ pragma Singleton import QtQml.Models import QtQuick import QtQuick.Templates as T import org.kde.desktop as QQC2 import org.kde.kirigami as Kirigami import org.kde.sonnet as Sonnet QQC2.Menu { id: root property Item target property bool deselectWhenMenuClosed: true property int restoredCursorPosition: 0 property int restoredSelectionStart property int restoredSelectionEnd property bool persistentSelectionSetting // assuming that Instantiator::active is bound to target.Kirigami.SpellCheck.enabled property Instantiator/**/ spellcheckHighlighterInstantiator // assuming that spellchecker's active state is not writable, use target.Kirigami.SpellCheck.enabled instead. readonly property Sonnet.SpellcheckHighlighter spellcheckHighlighter: spellcheckHighlighterInstantiator?.object as Sonnet.SpellcheckHighlighter property /*list*/var spellcheckSuggestions: [] Component.onCompleted: persistentSelectionSetting = persistentSelectionSetting // break binding property var runOnMenuClose: () => {} function storeCursorAndSelection() { restoredCursorPosition = target.cursorPosition; restoredSelectionStart = target.selectionStart; restoredSelectionEnd = target.selectionEnd; } // target is pressed with mouse function targetClick( handlerPoint, target, spellcheckHighlighterInstantiator, mousePosition, ) { if (!(target instanceof TextInput || target instanceof TextEdit)) { console.warn("Target not supported by standard context menu:", target); return; } if (handlerPoint.pressedButtons === Qt.RightButton) { // only accept just right click if (visible) { deselectWhenMenuClosed = false; // don't deselect text if menu closed by right click on textfield dismiss(); } else { this.target = target; target.persistentSelection = true; // persist selection when menu is opened this.spellcheckHighlighterInstantiator = spellcheckHighlighterInstantiator; spellcheckSuggestions = (spellcheckHighlighter && mousePosition) ? spellcheckHighlighter.suggestions(mousePosition) : []; storeCursorAndSelection(); popup(target); // slightly locate context menu away from mouse so no item is selected when menu is opened x += 1 y += 1 } } else { dismiss(); } } // context menu keyboard key function targetKeyPressed(event, target) { if (event.modifiers === Qt.NoModifier && event.key === Qt.Key_Menu) { this.target = target; target.persistentSelection = true; // persist selection when menu is opened storeCursorAndSelection(); const targetCursorRectangle = target.cursorRectangle; popup(target, targetCursorRectangle.right, targetCursorRectangle.bottom); } } function __hasSelectedText(): bool { return target !== null && target.selectedText !== ""; } function __editable(): bool { return target !== null && !target.readOnly; } function __hasSpellcheckCapability(): bool { return __editable() && spellcheckHighlighterInstantiator !== null; } function __showSpellcheckActions(): bool { return __editable() && spellcheckHighlighter !== null && spellcheckHighlighter.active && spellcheckHighlighter.wordIsMisspelled; } // Show actions which should normally be hidden for password field function __showPasswordRestrictedActions(): bool { return target !== null && target.echoMode !== TextInput.PasswordEchoOnEdit && target.echoMode !== TextInput.Password; } // Show text editing actions which should normally be hidden for password field function __showPasswordRestrictedEditingActions(): bool { return __showPasswordRestrictedActions() && !target.readOnly; } modal: true // deal with whether text should be deselected onClosed: { // reset parent, so OverlayZStacking could refresh z order next time // this menu is about to open for the same item that might have been // reparented to a different popup. parent = null; // restore text field's original persistent selection setting target.persistentSelection = persistentSelectionSetting // deselect text field text if menu is closed not because of a right click on the text field if (deselectWhenMenuClosed) { target.deselect(); } deselectWhenMenuClosed = true; // restore cursor position target.forceActiveFocus(); target.cursorPosition = restoredCursorPosition; target.select(restoredSelectionStart, restoredSelectionEnd); // run action, and free memory try { runOnMenuClose(); } catch (e) { console.error(e); console.trace(); } runOnMenuClose = () => {}; // clean up spellchecker spellcheckHighlighterInstantiator = null; spellcheckSuggestions = []; } onOpened: { runOnMenuClose = () => {}; } Instantiator { active: root.__showSpellcheckActions() model: root.spellcheckSuggestions delegate: QQC2.MenuItem { required property string modelData text: modelData onClicked: { root.deselectWhenMenuClosed = false; root.runOnMenuClose = () => { root.spellcheckHighlighter.replaceWord(modelData); }; } } onObjectAdded: (index, object) => { root.insertItem(0, object); } onObjectRemoved: (index, object) => { root.removeItem(object); } } QQC2.MenuItem { visible: root.__showSpellcheckActions() && root.spellcheckSuggestions.length === 0 action: T.Action { enabled: false text: root.spellcheckHighlighter ? qsTr('No Suggestions for "%1"') .arg(root.spellcheckHighlighter.wordUnderMouse) : "" } } QQC2.MenuSeparator { visible: root.__showSpellcheckActions() } QQC2.MenuItem { visible: root.__showSpellcheckActions() action: T.Action { text: root.spellcheckHighlighter ? qsTr('Add "%1" to Dictionary') .arg(root.spellcheckHighlighter.wordUnderMouse) : "" onTriggered: { root.deselectWhenMenuClosed = false; root.runOnMenuClose = () => { root.spellcheckHighlighter.addWordToDictionary(root.spellcheckHighlighter.wordUnderMouse); }; } } } QQC2.MenuItem { visible: root.__showSpellcheckActions() action: T.Action { text: qsTr("Ignore") onTriggered: { root.deselectWhenMenuClosed = false; root.runOnMenuClose = () => { root.spellcheckHighlighter.ignoreWord(root.spellcheckHighlighter.wordUnderMouse); }; } } } QQC2.MenuItem { visible: root.__hasSpellcheckCapability() checkable: true checked: root.target?.Kirigami.SpellCheck.enabled ?? false text: qsTr("Spell Check") onToggled: { if (root.target) { root.target.Kirigami.SpellCheck.enabled = checked; } } } QQC2.MenuSeparator { visible: root.__hasSpellcheckCapability() && (root.__editable() || root.__showPasswordRestrictedActions()) } QQC2.MenuItem { action: T.Action { icon.name: "edit-undo-symbolic" text: qsTr("Undo") shortcut: StandardKey.Undo } visible: root.__showPasswordRestrictedEditingActions() enabled: root.target?.canUndo ?? false onTriggered: { root.deselectWhenMenuClosed = false; root.runOnMenuClose = () => { root.target.undo(); }; } } QQC2.MenuItem { action: T.Action { icon.name: "edit-redo-symbolic" text: qsTr("Redo") shortcut: StandardKey.Redo } visible: root.__showPasswordRestrictedEditingActions() enabled: root.target?.canRedo ?? false onTriggered: { root.deselectWhenMenuClosed = false; root.runOnMenuClose = () => { root.target.redo(); }; } } QQC2.MenuSeparator { visible: root.__showPasswordRestrictedEditingActions() } QQC2.MenuItem { action: T.Action { icon.name: "edit-cut-symbolic" text: qsTr("Cut") shortcut: StandardKey.Cut } visible: root.__showPasswordRestrictedEditingActions() enabled: root.__hasSelectedText() onTriggered: { root.deselectWhenMenuClosed = false; root.runOnMenuClose = () => { root.target.cut(); }; } } QQC2.MenuItem { action: T.Action { icon.name: "edit-copy-symbolic" text: qsTr("Copy") shortcut: StandardKey.Copy } visible: root.__showPasswordRestrictedActions() enabled: root.__hasSelectedText() onTriggered: { root.deselectWhenMenuClosed = false; root.runOnMenuClose = () => { root.target.copy(); }; } } QQC2.MenuItem { action: T.Action { icon.name: "edit-paste-symbolic" text: qsTr("Paste") shortcut: StandardKey.Paste } visible: root.__editable() enabled: target?.canPaste ?? false onTriggered: { root.deselectWhenMenuClosed = false; root.runOnMenuClose = () => { root.target.paste(); }; } } QQC2.MenuItem { action: T.Action { icon.name: "edit-delete-symbolic" text: qsTr("Delete") shortcut: StandardKey.Delete } visible: root.__editable() enabled: root.__hasSelectedText() onTriggered: { root.deselectWhenMenuClosed = false; root.runOnMenuClose = () => { root.target.remove(root.target.selectionStart, root.target.selectionEnd); }; } } QQC2.MenuSeparator { visible: root.target !== null && (root.__editable() || root.__showPasswordRestrictedActions()) } QQC2.MenuItem { action: T.Action { icon.name: "edit-select-all-symbolic" text: qsTr("Select All") shortcut: StandardKey.SelectAll } visible: root.target !== null onTriggered: { root.deselectWhenMenuClosed = false; root.runOnMenuClose = () => { root.target.selectAll(); }; } } }