Compare commits

...

1 Commits

Author SHA1 Message Date
Joshua Goins
a4e6f0c4c2 Add spell-checking suggestions to the chat bar context menu
This is natively integrated into the existing chat document handler, and
emulates normal Sonnet behavior.
2023-09-13 13:06:05 +00:00
6 changed files with 317 additions and 0 deletions

View File

@@ -90,6 +90,11 @@ public:
}),
mentions->end());
}
QStringList suggestions(const QString &word) const
{
return checker->suggest(word);
}
};
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
@@ -263,6 +268,47 @@ void ChatDocumentHandler::complete(int index)
}
}
QStringList ChatDocumentHandler::getSuggestions(int mousePosition)
{
QTextCursor cursorAtMouse(document()->textDocument());
cursorAtMouse.setPosition(mousePosition);
// Get the word under the (mouse-)cursor and see if it is misspelled.
// Don't include apostrophes at the start/end of the word in the selection.
QTextCursor wordSelectCursor(cursorAtMouse);
wordSelectCursor.clearSelection();
wordSelectCursor.select(QTextCursor::WordUnderCursor);
m_selectedWord = wordSelectCursor.selectedText();
return m_highlighter->suggestions(m_selectedWord);
}
bool ChatDocumentHandler::getActive() const
{
return m_highlighter->settings.checkerEnabledByDefault();
}
bool ChatDocumentHandler::getIsWordIsMisspelled() const
{
return !m_highlighter->errors.isEmpty();
}
QString ChatDocumentHandler::getWordUnderMouse() const
{
return m_selectedWord;
}
void ChatDocumentHandler::replaceWord(const QString &word)
{
QTextCursor cursor(document()->textDocument());
const auto &text = m_room->chatBoxText();
auto at = text.indexOf(m_highlighter->previousText);
cursor.setPosition(at);
cursor.setPosition(at + m_highlighter->previousText.length(), QTextCursor::KeepAnchor);
cursor.insertText(word);
}
CompletionModel *ChatDocumentHandler::completionModel() const
{
return m_completionModel;

View File

@@ -87,6 +87,10 @@ class ChatDocumentHandler : public QObject
*/
Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged)
Q_PROPERTY(bool active READ getActive NOTIFY cursorPositionChanged)
Q_PROPERTY(bool wordIsMisspelled READ getIsWordIsMisspelled NOTIFY cursorPositionChanged)
Q_PROPERTY(QString wordUnderMouse READ getWordUnderMouse NOTIFY cursorPositionChanged)
/**
* @brief The current CompletionModel.
*
@@ -133,6 +137,12 @@ public:
Q_INVOKABLE void complete(int index);
Q_INVOKABLE void replaceWord(const QString &word);
Q_INVOKABLE QStringList getSuggestions(int mousePosition);
bool getActive() const;
bool getIsWordIsMisspelled() const;
QString getWordUnderMouse() const;
void updateCompletions();
CompletionModel *completionModel() const;
@@ -178,4 +188,6 @@ private:
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;
CompletionModel *m_completionModel = nullptr;
QString m_selectedWord;
};

View File

@@ -285,6 +285,7 @@ int main(int argc, char *argv[])
qmlRegisterUncreatableType<NeoChatRoom>("org.kde.neochat", 1, 0, "NeoChatRoom", {});
qmlRegisterUncreatableType<NeoChatConnection>("org.kde.neochat", 1, 0, "NeoChatConnection", {});
qmlRegisterSingletonType(QUrl("qrc:/ContextMenu.qml"), "org.kde.neochat", 1, 0, "ContextMenu");
qRegisterMetaType<User *>("User*");
qRegisterMetaType<User *>("const User*");
qRegisterMetaType<User *>("const Quotient::User*");

View File

@@ -177,6 +177,18 @@ QQC2.Control {
interval: 5000
}
TapHandler {
enabled: true
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
acceptedButtons: Qt.LeftButton | Qt.RightButton
// unfortunately, taphandler's pressed event only triggers when the press is lifted
// we need to use the longpress signal since it triggers when the button is first pressed
longPressThreshold: 0
onLongPressed: ContextMenu.targetClick(point, textField, documentHandler, textField.positionAt(point.position.x, point.position.y));
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
var textExists = text.length > 0

View File

@@ -0,0 +1,245 @@
pragma Singleton
import QtQuick 2.6
import QtQml 2.2
import QtQuick.Controls 2.15
import org.kde.kirigami 2.5 as Kirigami
Menu {
id: contextMenu
property Item target
property bool deselectWhenMenuClosed: true
property int restoredCursorPosition: 0
property int restoredSelectionStart
property int restoredSelectionEnd
property bool persistentSelectionSetting
property var chatdocumenthandler: null
property var suggestions: []
Component.onCompleted: persistentSelectionSetting = persistentSelectionSetting // break binding
property var runOnMenuClose: () => {}
parent: Overlay.overlay
function storeCursorAndSelection() {
contextMenu.restoredCursorPosition = target.cursorPosition;
contextMenu.restoredSelectionStart = target.selectionStart;
contextMenu.restoredSelectionEnd = target.selectionEnd;
}
// target is pressed with mouse
function targetClick(handlerPoint, newTarget, chatdocumenthandler, mousePosition) {
if (handlerPoint.pressedButtons === Qt.RightButton) { // only accept just right click
if (contextMenu.visible) {
deselectWhenMenuClosed = false; // don't deselect text if menu closed by right click on textfield
dismiss();
} else {
contextMenu.target = newTarget;
contextMenu.target.persistentSelection = true; // persist selection when menu is opened
contextMenu.chatdocumenthandler = chatdocumenthandler;
contextMenu.suggestions = mousePosition ? chatdocumenthandler.getSuggestions(mousePosition) : [];
storeCursorAndSelection();
popup(contextMenu.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, newTarget) {
if (event.modifiers === Qt.NoModifier && event.key === Qt.Key_Menu) {
contextMenu.target = newTarget;
target.persistentSelection = true; // persist selection when menu is opened
storeCursorAndSelection();
popup(contextMenu.target);
}
}
readonly property bool targetIsPassword: target !== null && (target.echoMode === TextInput.PasswordEchoOnEdit || target.echoMode === TextInput.Password)
onAboutToShow: {
if (Overlay.overlay) {
let tempZ = 0
for (let i in Overlay.overlay.visibleChildren) {
tempZ = Math.max(tempZ, Overlay.overlay.visibleChildren[i].z)
}
z = tempZ + 1
}
}
// deal with whether or not text should be deselected
onClosed: {
// 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
runOnMenuClose();
runOnMenuClose = () => {};
}
onOpened: {
runOnMenuClose = () => {};
}
Instantiator {
active: target !== null && !target.readOnly && chatdocumenthandler !== null && chatdocumenthandler.active && chatdocumenthandler.wordIsMisspelled
model: suggestions
delegate: MenuItem {
text: modelData
onClicked: {
deselectWhenMenuClosed = false;
runOnMenuClose = () => chatdocumenthandler.replaceWord(modelData);
}
}
onObjectAdded: {
contextMenu.insertItem(0, object)
}
onObjectRemoved: contextMenu.removeItem(0)
}
MenuItem {
visible: target !== null && !target.readOnly && chatdocumenthandler !== null && chatdocumenthandler.active && chatdocumenthandler.wordIsMisspelled && suggestions.length === 0
action: Action {
text: chatdocumenthandler ? qsTr("No suggestions for \"%1\"").arg(chatdocumenthandler.wordUnderMouse) : ''
enabled: false
}
}
MenuItem {
visible: target !== null && !target.readOnly && chatdocumenthandler !== null && chatdocumenthandler.active && chatdocumenthandler.wordIsMisspelled
action: Action {
text: chatdocumenthandler ? qsTr("Add \"%1\" to dictionary").arg(chatdocumenthandler.wordUnderMouse) : ''
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = () => chatdocumenthandler.addWordToDictionary(chatdocumenthandler.wordUnderMouse);
}
}
}
MenuItem {
visible: target !== null && !target.readOnly && chatdocumenthandler !== null && chatdocumenthandler.active && chatdocumenthandler.wordIsMisspelled
action: Action {
text: qsTr("Ignore")
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = () => chatdocumenthandler.ignoreWord(chatdocumenthandler.wordUnderMouse);
}
}
}
MenuSeparator {
visible: target !== null && !target.readOnly && ((chatdocumenthandler !== null && chatdocumenthandler.active && chatdocumenthandler.wordIsMisspelled))
}
MenuItem {
visible: target !== null && !target.readOnly && !targetIsPassword
action: Action {
icon.name: "edit-undo-symbolic"
text: qsTr("Undo")
shortcut: StandardKey.Undo
}
enabled: target !== null && target.canUndo
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = () => target.undo();
}
}
MenuItem {
visible: target !== null && !target.readOnly && !targetIsPassword
action: Action {
icon.name: "edit-redo-symbolic"
text: qsTr("Redo")
shortcut: StandardKey.Redo
}
enabled: target !== null && target.canRedo
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = () => target.redo();
}
}
MenuSeparator {
visible: target !== null && !target.readOnly && !targetIsPassword
}
MenuItem {
visible: target !== null && !target.readOnly && !targetIsPassword
action: Action {
icon.name: "edit-cut-symbolic"
text: qsTr("Cut")
shortcut: StandardKey.Cut
}
enabled: target !== null && target.selectedText
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = () => target.cut();
}
}
MenuItem {
action: Action {
icon.name: "edit-copy-symbolic"
text: qsTr("Copy")
shortcut: StandardKey.Copy
}
enabled: target !== null && target.selectedText
visible: !targetIsPassword
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = () => target.copy();
}
}
MenuItem {
visible: target !== null && !target.readOnly
action: Action {
icon.name: "edit-paste-symbolic"
text: qsTr("Paste")
shortcut: StandardKey.Paste
}
enabled: target !== null && target.canPaste
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = () => target.paste();
}
}
MenuItem {
visible: target !== null && !target.readOnly
action: Action {
icon.name: "edit-delete-symbolic"
text: qsTr("Delete")
shortcut: StandardKey.Delete
}
enabled: target !== null && target.selectedText
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = () => target.remove(target.selectionStart, target.selectionEnd);
}
}
MenuSeparator {
visible: !targetIsPassword
}
MenuItem {
action: Action {
icon.name: "edit-select-all-symbolic"
text: qsTr("Select All")
shortcut: StandardKey.SelectAll
}
visible: !targetIsPassword
onTriggered: {
deselectWhenMenuClosed = false;
runOnMenuClose = () => target.selectAll();
}
}
}

View File

@@ -36,6 +36,7 @@
<file alias="HoverActions.qml">qml/Component/HoverActions.qml</file>
<file alias="ChatBox.qml">qml/Component/ChatBox/ChatBox.qml</file>
<file alias="ChatBar.qml">qml/Component/ChatBox/ChatBar.qml</file>
<file alias="ContextMenu.qml">qml/Component/ChatBox/ContextMenu.qml</file>
<file alias="AttachmentPane.qml">qml/Component/ChatBox/AttachmentPane.qml</file>
<file alias="ReplyPane.qml">qml/Component/ChatBox/ReplyPane.qml</file>
<file alias="CompletionMenu.qml">qml/Component/ChatBox/CompletionMenu.qml</file>