diff --git a/CMakeLists.txt b/CMakeLists.txt index a3a3c59..59b80bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,7 @@ set(SRCS src/vpn/nm-pptp-service.h src/networkproxy.cpp src/defaultapplications.cpp + src/inputmethod.cpp ) set(RESOURCES diff --git a/src/application.cpp b/src/application.cpp index c979d42..b576351 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -14,6 +14,7 @@ #include "docksettings.h" #include "fonts/fonts.h" #include "fontsmodel.h" +#include "inputmethod.h" #include "language.h" #include "networkproxy.h" #include "notifications.h" @@ -75,6 +76,7 @@ Application::Application(int &argc, char **argv) qmlRegisterType(uri, 1, 0, "CursorThemeModel"); qmlRegisterType(uri, 1, 0, "About"); qmlRegisterType(uri, 1, 0, "Language"); + qmlRegisterType(uri, 1, 0, "InputMethod"); qmlRegisterType(uri, 1, 0, "Fonts"); qmlRegisterType(uri, 1, 0, "PowerManager"); qmlRegisterType(uri, 1, 0, "Mouse"); diff --git a/src/images/sidebar/dark/inputmethod.svg b/src/images/sidebar/dark/inputmethod.svg new file mode 100644 index 0000000..1da82a5 --- /dev/null +++ b/src/images/sidebar/dark/inputmethod.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/inputmethod.cpp b/src/inputmethod.cpp new file mode 100644 index 0000000..a53c24a --- /dev/null +++ b/src/inputmethod.cpp @@ -0,0 +1,101 @@ +#include "inputmethod.h" + +#include "desktopentry.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr auto service = "com.cutefish.InputMethod1"; +constexpr auto path = "/com/cutefish/InputMethod1"; + +QVariantMap describe(const QString &id) +{ + QVariantMap method {{QStringLiteral("id"), id}, {QStringLiteral("name"), id}}; + const QString file = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, id); + QFile desktop(file); + DesktopEntryData data; + if (!file.isEmpty() && desktop.open(QIODevice::ReadOnly) + && DesktopEntry::parse(id, file, desktop.readAll(), &data)) { + if (!data.name.isEmpty()) + method[QStringLiteral("name")] = data.name; + method[QStringLiteral("icon")] = data.icon; + } + return method; +} +} // namespace + +InputMethod::InputMethod(QObject *parent) + : QObject(parent) +{ + auto *watcher = new QDBusServiceWatcher(QLatin1String(service), QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForOwnerChange, this); + connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, + &InputMethod::refreshInterface); + refreshInterface(); +} + +void InputMethod::refreshInterface() +{ + delete m_interface; + m_interface = new QDBusInterface(QLatin1String(service), QLatin1String(path), + QLatin1String(service), QDBusConnection::sessionBus(), this); + if (m_interface->isValid()) { + connect(m_interface, SIGNAL(inputMethodChanged()), this, SIGNAL(currentChanged())); + connect(m_interface, SIGNAL(runningChanged()), this, SIGNAL(runningChanged())); + } + emit availableChanged(); + emit currentChanged(); + emit runningChanged(); + refreshMethods(); +} + +void InputMethod::refreshMethods() +{ + m_methods.clear(); + if (m_interface->isValid()) { + const QDBusReply reply = + m_interface->call(QStringLiteral("availableInputMethods")); + for (const auto &id : reply.value()) + m_methods << describe(id); + } + emit methodsChanged(); +} + +bool InputMethod::available() const +{ + return m_interface->isValid(); +} + +QString InputMethod::current() const +{ + return m_interface->isValid() ? m_interface->property("inputMethod").toString() : QString(); +} + +bool InputMethod::running() const +{ + return m_interface->isValid() && m_interface->property("running").toBool(); +} + +void InputMethod::select(const QString &id) +{ + if (!m_interface->isValid()) + return; + auto *watcher = new QDBusPendingCallWatcher( + m_interface->asyncCall(QStringLiteral("setInputMethod"), id), this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this, id](QDBusPendingCallWatcher *call) { + const QDBusPendingReply reply = *call; + if (reply.isError() || !reply.value()) { + qWarning() << "Cannot select input method" << id << reply.error().message(); + refreshMethods(); + } + emit currentChanged(); + call->deleteLater(); + }); +} diff --git a/src/inputmethod.h b/src/inputmethod.h new file mode 100644 index 0000000..e206d65 --- /dev/null +++ b/src/inputmethod.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +// Client of the compositor's com.cutefish.InputMethod1 service. +class InputMethod : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool available READ available NOTIFY availableChanged) + Q_PROPERTY(QVariantList methods READ methods NOTIFY methodsChanged) + Q_PROPERTY(QString current READ current NOTIFY currentChanged) + Q_PROPERTY(bool running READ running NOTIFY runningChanged) + +public: + explicit InputMethod(QObject *parent = nullptr); + + bool available() const; + + QVariantList methods() const { return m_methods; } + + QString current() const; + bool running() const; + + // An empty id turns the input method off. + Q_INVOKABLE void select(const QString &id); + +signals: + void availableChanged(); + void methodsChanged(); + void currentChanged(); + void runningChanged(); + +private: + void refreshInterface(); + void refreshMethods(); + QDBusInterface *m_interface = nullptr; + QVariantList m_methods; +}; diff --git a/src/qml/InputMethod/Main.qml b/src/qml/InputMethod/Main.qml new file mode 100644 index 0000000..a85ece4 --- /dev/null +++ b/src/qml/InputMethod/Main.qml @@ -0,0 +1,120 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import FishUI 1.0 as FishUI +import Cutefish.Settings 1.0 +import "../" + +ItemPage { + headerTitle: qsTr("Input Method") + + InputMethod { + id: inputMethod + } + + Scrollable { + anchors.fill: parent + contentHeight: layout.implicitHeight + + ColumnLayout { + id: layout + anchors.fill: parent + spacing: FishUI.Units.largeSpacing * 2 + + RoundedItem { + Label { + text: qsTr("Input method") + color: FishUI.Theme.disabledTextColor + } + + Label { + Layout.fillWidth: true + visible: !inputMethod.available + text: qsTr("Input methods can only be selected in a Cutefish Wayland session.") + wrapMode: Text.WordWrap + } + + Repeater { + model: inputMethod.available + ? [{ "id": "", "name": qsTr("None"), "icon": "" }] + .concat(inputMethod.methods) + : [] + + delegate: MouseArea { + id: item + + property bool isSelected: modelData.id === inputMethod.current + + Layout.fillWidth: true + implicitHeight: Math.max(36, row.implicitHeight + FishUI.Units.largeSpacing) + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onClicked: inputMethod.select(modelData.id) + + Rectangle { + anchors.fill: parent + radius: FishUI.Theme.smallRadius + color: item.isSelected ? FishUI.Theme.highlightColor + : item.containsMouse ? FishUI.Theme.disabledTextColor + : "transparent" + opacity: item.isSelected ? 1 : 0.1 + } + + RowLayout { + id: row + anchors.fill: parent + anchors.leftMargin: FishUI.Units.smallSpacing + anchors.rightMargin: FishUI.Units.largeSpacing + spacing: FishUI.Units.largeSpacing + + FishUI.IconItem { + width: 24 + height: 24 + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + source: modelData.icon ? modelData.icon : "input-keyboard" + smooth: true + } + + Label { + Layout.fillWidth: true + text: modelData.name + elide: Text.ElideRight + color: item.isSelected ? FishUI.Theme.highlightedTextColor + : FishUI.Theme.textColor + } + + Image { + width: 16 + height: 16 + sourceSize: Qt.size(16, 16) + source: "qrc:/images/dark/checked.svg" + visible: item.isSelected + smooth: false + } + } + } + } + + Label { + Layout.fillWidth: true + visible: inputMethod.available && inputMethod.current !== "" && !inputMethod.running + text: qsTr("The selected input method is not running.") + color: FishUI.Theme.disabledTextColor + wrapMode: Text.WordWrap + } + } + + Label { + Layout.fillWidth: true + visible: inputMethod.available + text: qsTr("Applications started before the input method may need to be restarted.") + color: FishUI.Theme.disabledTextColor + wrapMode: Text.WordWrap + leftPadding: FishUI.Units.largeSpacing * 1.5 + rightPadding: FishUI.Units.largeSpacing * 1.5 + } + } + } +} diff --git a/src/qml/SideBar.qml b/src/qml/SideBar.qml index cdc35e6..8766d05 100644 --- a/src/qml/SideBar.qml +++ b/src/qml/SideBar.qml @@ -275,6 +275,15 @@ Item { category: qsTr("System") } + ListElement { + title: qsTr("Input Method") + name: "inputmethod" + page: "qrc:/qml/InputMethod/Main.qml" + iconSource: "inputmethod.svg" + iconColor: "#6F7DE8" + category: qsTr("System") + } + ListElement { title: qsTr("Battery") name: "battery" diff --git a/src/resources.qrc b/src/resources.qrc index 83c5ac4..82a9458 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -60,6 +60,7 @@ images/sidebar/dark/display.svg images/sidebar/dark/dock.svg images/sidebar/dark/language.svg + images/sidebar/dark/inputmethod.svg images/sidebar/dark/network.svg images/sidebar/dark/wallpaper.svg images/sidebar/light/about.svg @@ -108,6 +109,7 @@ images/sidebar/dark/hotspot.svg qml/Application/Main.qml qml/Cursor/Main.qml + qml/InputMethod/Main.qml images/sidebar/dark/cursor.svg qml/DateTime/Main.qml qml/DateTime/TimeZoneDialog.qml diff --git a/translations/zh_CN.ts b/translations/zh_CN.ts index 282a66f..4e06f84 100644 --- a/translations/zh_CN.ts +++ b/translations/zh_CN.ts @@ -52,6 +52,36 @@ Main + + + Input Method + 输入法 + + + + Input method + 输入法 + + + + Input methods can only be selected in a Cutefish Wayland session. + 只能在 Cutefish Wayland 会话中选择输入法。 + + + + None + 无 + + + + The selected input method is not running. + 所选输入法未在运行。 + + + + Applications started before the input method may need to be restarted. + 在输入法启动前打开的应用可能需要重新启动。 + About @@ -908,6 +938,11 @@ SideBar + + + Input Method + 输入法 + Display diff --git a/translations/zh_TW.ts b/translations/zh_TW.ts index b5ac439..3207f4c 100644 --- a/translations/zh_TW.ts +++ b/translations/zh_TW.ts @@ -70,6 +70,36 @@ Main + + + Input Method + 輸入法 + + + + Input method + 輸入法 + + + + Input methods can only be selected in a Cutefish Wayland session. + 只能在 Cutefish Wayland 工作階段中選擇輸入法。 + + + + None + 無 + + + + The selected input method is not running. + 所選輸入法未在執行。 + + + + Applications started before the input method may need to be restarted. + 在輸入法啟動前開啟的應用程式可能需要重新啟動。 + About @@ -533,6 +563,11 @@ SideBar + + + Input Method + 輸入法 + User