diff --git a/CMakeLists.txt b/CMakeLists.txt index 5af4a9ff5..ae6fbb85d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,7 @@ if(ANDROID) ) else() find_package(Qt5 ${QT_MIN_VERSION} COMPONENTS Widgets) + find_package(KF5 ${KF5_MIN_VERSION} COMPONENTS REQUIRED KIO) find_package(KF5QQC2DesktopStyle ${KF5_MIN_VERSION} REQUIRED) set_package_properties(KF5QQC2DesktopStyle PROPERTIES TYPE RUNTIME diff --git a/imports/NeoChat/Menu/Timeline/MessageDelegateContextMenu.qml b/imports/NeoChat/Menu/Timeline/MessageDelegateContextMenu.qml index 953f39817..a4529e085 100644 --- a/imports/NeoChat/Menu/Timeline/MessageDelegateContextMenu.qml +++ b/imports/NeoChat/Menu/Timeline/MessageDelegateContextMenu.qml @@ -19,6 +19,7 @@ Loader { property string eventType: "" property string formattedBody: "" required property string source + property string selectedText: "" property list actions: [ Kirigami.Action { @@ -65,89 +66,33 @@ Loader { onClicked: loadRoot.item.close(); } } + QQC2.Menu { + id: webshortcutmenu + title: i18n("Search for '%1'", webshortcutmodel.trunkatedSearchText) + property bool isVisible: selectedText && selectedText.length > 0 && webshortcutmodel.enabled + Component.onCompleted: webshortcutmenu.parent.visible = isVisible + onIsVisibleChanged: webshortcutmenu.parent.visible = isVisible + Instantiator { + model: WebShortcutModel { + id: webshortcutmodel + selectedText: loadRoot.selectedText + onOpenUrl: RoomManager.visitNonMatrix(url) + } + delegate: QQC2.MenuItem { + text: model.display + icon.name: model.decoration + onTriggered: webshortcutmodel.trigger(model.edit) + } + onObjectAdded: webshortcutmenu.insertItem(0, object) + } + QQC2.MenuSeparator {} + QQC2.MenuItem { + text: i18n("Configure Web Shortcuts...") + icon.name: "configure" + onTriggered: webshortcutmodel.configureWebShortcuts() + } + } } - /* - Kirigami.OverlaySheet { - id: root - - parent: applicationWindow().overlay - - leftPadding: 0 - rightPadding: 0 - - header: Kirigami.Heading { - text: i18nc("@title:menu Message detail dialog", "Message detail") - } - - contentItem: ColumnLayout { - spacing: 0 - RowLayout { - id: headerLayout - Layout.fillWidth: true - Layout.margins: Kirigami.Units.largeSpacing - spacing: Kirigami.Units.largeSpacing - Kirigami.Avatar { - id: avatar - source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : "" - Layout.preferredWidth: Kirigami.Units.gridUnit * 3 - Layout.preferredHeight: Kirigami.Units.gridUnit * 3 - Layout.alignment: Qt.AlignTop - name: author.displayName - color: author.color - } - ColumnLayout { - Layout.fillWidth: true - Kirigami.Heading { - level: 3 - Layout.fillWidth: true - text: author.displayName - wrapMode: Text.WordWrap - } - QQC2.Label { - text: message - Layout.fillWidth: true - Layout.maximumWidth: Kirigami.Units.gridUnit * 24 - wrapMode: Text.WordWrap - - onLinkActivated: RoomManager.openResource(link); - } - } - } - Kirigami.Separator { - Layout.fillWidth: true - } - RowLayout { - spacing: 0 - Layout.fillWidth: true - Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5 - Repeater { - model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"] - delegate: QQC2.ItemDelegate { - Layout.fillWidth: true - Layout.fillHeight: true - - contentItem: QQC2.Label { - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - - font.pixelSize: 16 - font.family: "emoji" - text: modelData - - } - - onClicked: { - currentRoom.toggleReaction(eventId, modelData) - loadRoot.item.close(); - } - } - } - } - Kirigami.Separator { - Layout.fillWidth: true - } - } - }*/ } Component { id: mobileMenu diff --git a/imports/NeoChat/Page/RoomPage.qml b/imports/NeoChat/Page/RoomPage.qml index 279121aad..0532bab05 100644 --- a/imports/NeoChat/Page/RoomPage.qml +++ b/imports/NeoChat/Page/RoomPage.qml @@ -372,11 +372,11 @@ Kirigami.ScrollablePage { Layout.bottomMargin: Kirigami.Units.largeSpacing * 2 TapHandler { acceptedButtons: Qt.RightButton - onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody) + onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody, parent.selectedText) } TapHandler { acceptedButtons: Qt.LeftButton - onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody) + onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody, parent.selectedText) } } } @@ -399,11 +399,11 @@ Kirigami.ScrollablePage { Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0 TapHandler { acceptedButtons: Qt.RightButton - onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody) + onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody, parent.selectedText) } TapHandler { acceptedButtons: Qt.LeftButton - onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody) + onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody, parent.selectedText) } } } @@ -881,8 +881,9 @@ Kirigami.ScrollablePage { } /// Open context menu for normal message - function openMessageContext(author, message, eventId, source, eventType, formattedBody) { + function openMessageContext(author, message, eventId, source, eventType, formattedBody, selectedText) { const contextMenu = messageDelegateContextMenu.createObject(page, { + selectedText: selectedText, author: author, message: message, eventId: eventId, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4300c1d86..0493532c3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -30,6 +30,7 @@ add_executable(neochat stickerevent.cpp chatboxhelper.cpp commandmodel.cpp + webshortcutmodel.cpp ../res.qrc ) @@ -91,7 +92,7 @@ if(ANDROID) "network-connect" ) else() - target_link_libraries(neochat PRIVATE Qt5::Widgets) + target_link_libraries(neochat PRIVATE Qt5::Widgets KF5::KIOWidgets) endif() if(TARGET KF5::DBusAddons) @@ -99,4 +100,8 @@ if(TARGET KF5::DBusAddons) target_compile_definitions(neochat PRIVATE -DHAVE_KDBUSADDONS) endif() +if (TARGET KF5::KIOWidgets) + target_compile_definitions(neochat PRIVATE -DHAVE_KIO) +endif() + install(TARGETS neochat ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/main.cpp b/src/main.cpp index 498307cd0..214094e87 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -56,6 +56,7 @@ #include "sortfilterroomlistmodel.h" #include "userdirectorylistmodel.h" #include "userlistmodel.h" +#include "webshortcutmodel.h" using namespace Quotient; @@ -147,6 +148,7 @@ int main(int argc, char *argv[]) qmlRegisterType("org.kde.neochat", 1, 0, "ActionsHandler"); qmlRegisterType("org.kde.neochat", 1, 0, "ChatDocumentHandler"); qmlRegisterType("org.kde.neochat", 1, 0, "RoomListModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "WebShortcutModel"); qmlRegisterType("org.kde.neochat", 1, 0, "UserListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "MessageEventModel"); qmlRegisterType("org.kde.neochat", 1, 0, "MessageFilterModel"); diff --git a/src/webshortcutmodel.cpp b/src/webshortcutmodel.cpp new file mode 100644 index 000000000..e536a7304 --- /dev/null +++ b/src/webshortcutmodel.cpp @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2010 Eike Hein +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "webshortcutmodel.h" + +#ifdef HAVE_KIO +#include +#include +#endif +#include + +struct KWebShortcutModelPrivate +{ + QString selectedText; + KUriFilterData filterData; + QStringList searchProviders; +}; + +KWebShortcutModel::KWebShortcutModel(QObject *parent) + : QAbstractListModel(parent) + , d(new KWebShortcutModelPrivate) +{ +} + +KWebShortcutModel::~KWebShortcutModel() +{ +} + + +QString KWebShortcutModel::selectedText() const +{ + return d->selectedText; +} + +QString KWebShortcutModel::trunkatedSearchText() const +{ + return KStringHandler::rsqueeze(d->selectedText, 21); +} + +bool KWebShortcutModel::enabled() const +{ +#ifdef HAVE_KIO + return true; +#else + return false; +#endif +} + +void KWebShortcutModel::setSelectedText(const QString &selectedText) +{ + if (d->selectedText == selectedText) { + return; + } +#ifdef HAVE_KIO + beginResetModel(); + d->selectedText = selectedText; + + if (selectedText.isEmpty()) { + endResetModel(); + return; + } + + QString searchText = selectedText; + searchText = searchText.replace('\n', ' ').replace('\r', ' ').simplified(); + if (searchText.isEmpty()) { + endResetModel(); + return; + } + d->filterData.setData(searchText); + d->filterData.setSearchFilteringOptions(KUriFilterData::RetrievePreferredSearchProvidersOnly); + + if (KUriFilter::self()->filterSearchUri(d->filterData, KUriFilter::NormalTextFilter)) { + d->searchProviders = d->filterData.preferredSearchProviders(); + } + endResetModel(); +#else + d->selectedText = selectedText; +#endif + Q_EMIT selectedTextChanged(); +} + +int KWebShortcutModel::rowCount(const QModelIndex &parent) const +{ +#ifdef HAVE_KIO + if (d->selectedText.count() > 0) { + return d->searchProviders.count(); + } +#endif + return 0; +} + +QVariant KWebShortcutModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + +#ifdef HAVE_KIO + switch (role) { + case Qt::DisplayRole: + return d->searchProviders[index.row()]; + case Qt::DecorationRole: + return d->filterData.iconNameForPreferredSearchProvider(d->searchProviders[index.row()]); + case Qt::EditRole: + return d->filterData.queryForPreferredSearchProvider(d->searchProviders[index.row()]); + } +#endif + return {}; +} + +void KWebShortcutModel::trigger(const QString &data) +{ + KUriFilterData filterData(data); + if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::WebShortcutFilter)) { + Q_EMIT openUrl(filterData.uri().url()); + } +} + +void KWebShortcutModel::configureWebShortcuts() +{ + auto job = new KIO::CommandLauncherJob("kcmshell5", QStringList() << "webshortcuts", this); + job->exec(); +} diff --git a/src/webshortcutmodel.h b/src/webshortcutmodel.h new file mode 100644 index 000000000..2a4bc3eb9 --- /dev/null +++ b/src/webshortcutmodel.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include +#include + +class KWebShortcutModelPrivate; + +//! List model of web shortcuts for a a specified selectedText. +//! +//! This can be used as following in your QML code +//! +//! ```qml +//! QQC2.Menu { +//! id: webshortcutmenu +//! title: i18n("Search for '%1'", webshortcutmodel.trunkatedSearchText) +//! property bool isVisible: selectedText && selectedText.length > 0 && webshortcutmodel.enabled +//! Component.onCompleted: webshortcutmenu.parent.visible = isVisible +//! onIsVisibleChanged: webshortcutmenu.parent.visible = isVisible +//! Instantiator { +//! model: WebShortcutModel { +//! id: webshortcutmodel +//! selectedText: loadRoot.selectedText +//! onOpenUrl: Qt.openUrlExternally(url) +//! } +//! delegate: QQC2.MenuItem { +//! text: model.display +//! icon.name: model.decoration +//! onTriggered: webshortcutmodel.trigger(model.edit) +//! } +//! onObjectAdded: webshortcutmenu.insertItem(0, object) +//! } +//! QQC2.MenuSeparator {} +//! QQC2.MenuItem { +//! text: i18n("Configure Web Shortcuts...") +//! icon.name: "configure" +//! onTriggered: webshortcutmodel.configureWebShortcuts() +//! } +//! } +//! ``` +class KWebShortcutModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QString selectedText READ selectedText WRITE setSelectedText NOTIFY selectedTextChanged) + Q_PROPERTY(QString trunkatedSearchText READ trunkatedSearchText NOTIFY selectedTextChanged) + Q_PROPERTY(bool enabled READ enabled CONSTANT) +public: + explicit KWebShortcutModel(QObject *parent = nullptr); + ~KWebShortcutModel(); + + QString selectedText() const; + void setSelectedText(const QString &selectedText); + QString trunkatedSearchText() const; + bool enabled() const; + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + Q_INVOKABLE void trigger(const QString &data); + Q_INVOKABLE void configureWebShortcuts(); +Q_SIGNALS: + void selectedTextChanged(); + void openUrl(const QUrl &url); +private: + std::unique_ptr d; +};