Commit 86bb25f1 authored by Jan Blackquill's avatar Jan Blackquill 🌈 Committed by Nate Graham

Add key-held behaviour

This patch introduces a popup window that appears when you
press and hold down an alphabetic key offering diacritics
that can be selected by then pressing a number.
parent e709d8e5
add_subdirectory(platformtheme)
add_subdirectory(platforminputcontextplugin)
get_target_property(PLUGIN_DIR Qt5::Gui IMPORTED_LOCATION_UNUSED)
# keyholdmanager has a ton of definitions where
# needing explicit qstringliteral would negatively affect
# code readability
remove_definitions(-DQT_NO_CAST_FROM_ASCII)
add_library(input_plugin MODULE
main.cpp
plasmaimcontext.cpp
)
target_link_libraries(input_plugin
Qt5::Core
Qt5::Gui
Qt5::Quick
Qt5::Widgets
Qt5::GuiPrivate
KF5::ConfigCore
)
set_target_properties(input_plugin PROPERTIES OUTPUT_NAME plasmaimplatforminputcontextplugin)
install(TARGETS input_plugin LIBRARY DESTINATION ${KDE_INSTALL_QTPLUGINDIR}/platforminputcontexts)
install(DIRECTORY data/ DESTINATION ${KDE_INSTALL_INCLUDEDIR}/PlasmaKeyData)
configure_file(plasma-key-data.pc.in plasma-key-data.pc @ONLY)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/plasma-key-data.pc DESTINATION ${KDE_INSTALL_LIBDIR}/pkgconfig)
/*
* Copyright 2020 Carson Black <uhhadd@gmail.com>
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2 of the License or ( at
* your option ) version 3 or, at the discretion of KDE e.V. ( which shall
* act as a proxy as in section 14 of the GPLv3 ), any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include <QString>
#include <QMap>
#include <QList>
// Curating key data
// =================
//
// There are two main goals with key data here:
// - to allow users to input letters related to but not in their keyboard language (e.g. Persian glyphs on top of the Arabic set)
// - to allow users to input useful symbols, such as proper arithmetic symbols and various dashes
//
// The former generally is used for letters, while the latter is generally used for punctuation and numbers
//
// Latin Letters:
//
// Generally, these are going to be diacritics (a + ` = à) or diagraphs (a + e = æ).
// Letters should be roughly ordered first by amount of speakers of languages that use them,
// and then by commonality in languages as a secondary factor. For example, the character å is
// found mostly in northern Germanic languages, which don't have nearly as many speakers as
// romance languages such as Spanish, which has á. For this reason, á should be ordered before å.
// Rinse and repeat this process until all the variants you want to offer are well placed.
//
// Cyrilic Letters:
//
// The same principles as Latin letters apply here. For example, the character ҝ (Ka with a vertical stroke; Azerbaijani)
// is found in a much less common language than қ (Ka with a descender; many languages found in former Soviet Union territories)
// so it should be placed after қ in the list of options for к (Ka; lot of languages)
//
// Arabic characters:
//
// Same deal as Latin and Cyrilic, except now it's going to render poorly in your text editors.
// Most of these characters only have one alternate form, mostly to add some extra diacritics
// in order to use a less common form or a form not found in Arabic, e.g. Persian پ from Arabic ب.
// Alef is a special case here, as it has a lot of "variants" in common use. For example, there's the
// alef with a hamza sitting on it (ا + ʾ = أ) or a hamza sitting below it (إ). These can be inputted
// as diagraphs on keyboards, but can also be represented as a held-key variant of alef for input purposes.
//
// Hebrew characters:
//
// The characters here are the iffiest of the selection, since most don't add anything new to input, just a
// "convenience" way for inputting characters with a ׳ added, when you can already type it without extra modifiers.
// However, we're still having them there since they're a logical "variant" of the characters they're for.
//
// On the other hand, the ײַ ײ ױ װ work as held keys to offer a meaningful alternative to other methods of inputting
// them, since these typically require the usage of AltGr to input on most Hebrew keyboards.
//
// Numbers:
//
// These are mostly exponents and fractions. Their ordering should be self explanatory.
//
// Symbols:
//
// These are mostly things that look like the key being held, with the exception
// of ^ being used for directional arrows.
//
namespace KeyData {
const QMap<QString,QList<QString>> KeyMappings = {
//
// Latin
//
{"a", {"à", "á", "â", "ä", "æ", "ã", "å", "ā"}},
{"c", {"ç", "ć", "č"}},
{"d", {"ð"}},
{"e", {"è", "é", "ê", "ë", "ē", "ė", "ę", "ə"}},
{"i", {"î", "ï", "í", "ī", "į", "ì"}},
{"l", {"ł"}},
{"n", {"ñ", "ń"}},
{"o", {"ô", "ö", "ò", "ó", "œ", "ø", "ō", "õ"}},
{"s", {"ß", "ś", "š"}},
{"u", {"û", "ü", "ù", "ú", "ū"}},
{"x", {"×"}},
{"y", {"ÿ", "ұ", "ү", "ӯ", "ў"}},
{"z", {"ž", "ź", "ż"}},
//
// Cyrilic
//
{"г", {"ғ"}},
{"е", {"ё"}}, // this in fact NOT the same E as before
{"и", {"ӣ", "і"}}, // і is not i
{"й", {"ј"}}, // ј is not j
{"к", {"қ", "ҝ",}},
{"н", {"ң", "һ"}}, // һ is not h
{"о", {"ә", "ө"}},
{"ч", {"ҷ", "ҹ"}},
{"ь", {"ъ"}},
//
// Arabic
//
// This renders weirdly in text editors, but is valid code.
{"ا", {"أ", "إ", "آ", "ء"}},
{"ب", {"پ"}},
{"ج", {"چ"}},
{"ز", {"ژ"}},
{"ف", {"ڤ"}},
{"ك", {"گ"}},
{"ل", {"لا"}},
{"ه", {"ه"}},
{"و", {"ؤ"}},
//
// Hebrew
//
// Likewise, this will render oddly, but is still valid code.
{"ג",{"ג׳"}},
{"ז",{"ז׳"}},
{"ח",{"ח׳"}},
{"צ׳",{"צ׳"}},
{"ת",{"ת׳"}},
{"י",{"ײַ"}},
{"י", {"ײ"}},
{"ח", {"ױ"}},
{"ו", {"װ"}},
//
// Numbers
//
{"0", {"∅", "ⁿ", "⁰"}},
{"1", {"¹", "½", "⅓", "¼", "⅕", "⅙", "⅐", "⅛", "⅑", "⅒"}},
{"2", {"²", "⅖", "⅔"}},
{"3", {"³", "⅗", "¾", "⅜"}},
{"4", {"⁴", "⅘", "⁵", "⅝", "⅚"}},
{"5", {"⁵", "⅝", "⅚"}},
{"6", {"⁶"}},
{"7", {"⁷", "⅞"}},
{"8", {"⁸"}},
{"9", {"⁹"}},
//
// Punctuation
//
{R"(-)", {"—", "–", "·"}},
{R"(?)", {"¿", "‽"}},
{R"(')", {"‘", "’", "‚", "‹", "›"}},
{R"(!)", {"¡"}},
{R"(")", {"“", "”", "„", "«", "»"}},
{R"(/)", {"÷"}},
{R"(#)", {"№"}},
{R"(%)", {"‰", "℅"}},
{R"(^)", {"↑", "←", "→", "↓"}},
{R"(+)", {"±"}},
{R"(<)", {"«", "≤", "‹", "⟨"}},
{R"(=)", {"∞", "≠", "≈"}},
{R"(>)", {"⟩", "»", "≥", "›"}},
};
}
/*
* Copyright 2020 Carson Black <uhhadd@gmail.com>
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2 of the License or ( at
* your option ) version 3 or, at the discretion of KDE e.V. ( which shall
* act as a proxy as in section 14 of the GPLv3 ), any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include <QObject>
#include <QtPlugin>
#include <qpa/qplatforminputcontextplugin_p.h>
#include "plasmaimcontext.h"
QT_BEGIN_NAMESPACE
class PlasmaIM : public QPlatformInputContextPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID QPlatformInputContextFactoryInterface_iid FILE "plasmaim.json")
public:
QPlatformInputContext *create(const QString&, const QStringList&) Q_DECL_OVERRIDE;
};
QPlatformInputContext *PlasmaIM::create(const QString& system, const QStringList&)
{
if (system == "plasmaim") {
return new PlasmaIMContext;
}
return nullptr;
}
QT_END_NAMESPACE
#include "main.moc"
prefix=@CMAKE_INSTALL_PREFIX@
includedir=${prefix}/include
Name: PlasmaKeyData
Description: Plasma's key data used for key-holding behaviour
Version: 1.0
Cflags: -I${includedir}/PlasmaKeyData
Libs:
\ No newline at end of file
/*
* Copyright 2020 Carson Black <uhhadd@gmail.com>
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2 of the License or ( at
* your option ) version 3 or, at the discretion of KDE e.V. ( which shall
* act as a proxy as in section 14 of the GPLv3 ), any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
/// Qt Utilities
#include <QGuiApplication>
#include <QInputMethod>
#include <QInputMethodEvent>
#include <QKeyEvent>
#include <QQuickWindow>
#include <QStringBuilder>
#include <QToolTip>
// Widgets for the popup
#include <QGridLayout>
#include <QLabel>
#include <QPushButton>
#include "data/plasmakeydata.h"
#include "plasmaimcontext.h"
static QList<TooltipData> dataForIndex(const QString &ch, bool upperCase)
{
QList<TooltipData> ret;
int i = 0;
for (auto item : KeyData::KeyMappings[ch]) {
ret << TooltipData {upperCase ? item.toUpper() : item, QString::number((i + 1) < 10 ? (i + 1) : 0, 10), i};
i++;
}
return ret;
}
PlasmaIMContext::PlasmaIMContext()
{
connect(watcher.data(), &KConfigWatcher::configChanged, this, &PlasmaIMContext::configChangedHandler);
}
PlasmaIMContext::~PlasmaIMContext()
{
if (!popup.isNull()) {
popup->hide();
popup->deleteLater();
}
}
bool PlasmaIMContext::isValid() const
{
return true;
}
void PlasmaIMContext::cleanUpState()
{
if (!popup.isNull()) {
popup->hide();
popup->deleteLater();
}
isPreHold = false;
preHoldText = QString();
}
void PlasmaIMContext::setFocusObject(QObject *object)
{
m_focusObject = object;
}
void PlasmaIMContext::configChangedHandler(const KConfigGroup&, const QByteArrayList&)
{
config->reparseConfiguration();
}
void PlasmaIMContext::showPopup(const QList<TooltipData> &text)
{
QPoint position;
QWindow *parentWin = nullptr;
auto im = QGuiApplication::inputMethod();
if (im != nullptr && im->cursorRectangle().isValid()) {
position = im->cursorRectangle().topRight().toPoint();
parentWin = QGuiApplication::focusWindow();
}
auto isRtl =
text[0].character[0].script() == QChar::Script_Arabic || text[0].character[0].script() == QChar::Script_Hebrew;
popup = new QWidget;
auto grid = new QGridLayout(popup.data());
popup->setLayoutDirection(isRtl ? Qt::RightToLeft : Qt::LeftToRight);
popup->setLayout(grid);
int col = 0;
for (auto item : text) {
auto label = new QLabel(item.character, popup.data());
auto button = new QPushButton(item.number, popup.data());
button->setMaximumWidth(button->height());
grid->addWidget(label, 0, col, Qt::AlignCenter);
grid->addWidget(button, 1, col, Qt::AlignHCenter);
connect(button, &QPushButton::clicked, [=]() {
applyReplacement(item.character);
popup->hide();
popup->deleteLater();
});
col++;
}
connect(parentWin, &QWindow::activeChanged, this, &PlasmaIMContext::cleanUpState, Qt::UniqueConnection);
if (parentWin != nullptr) {
popup->setWindowFlags(Qt::WindowDoesNotAcceptFocus | Qt::ToolTip);
popup->adjustSize();
popup->move(position + parentWin->framePosition() - QPoint((isRtl ? popup->width() : 0), 0));
popup->show();
}
}
void PlasmaIMContext::applyReplacement(const QString &data)
{
if (m_focusObject != nullptr) {
QInputMethodEvent ev;
ev.setCommitString(data, -1, 1);
QCoreApplication::sendEvent(m_focusObject, &ev);
}
}
bool PlasmaIMContext::filterEvent(const QEvent *event)
{
bool isAccent = keyboard.readEntry("KeyRepeat", "accent") == QLatin1String("accent");
bool isNothing = keyboard.readEntry("KeyRepeat", "accent") == QLatin1String("nothing");
if (!isAccent && !isNothing) {
return false;
}
if (event->type() == QEvent::KeyPress) {
auto ev = static_cast<const QKeyEvent *>(event);
// this is the state when we have a held key
if (isPreHold) {
if (ev->isAutoRepeat() && ev->text() == preHoldText)
return true;
if (ev->key() < 0x30 || 0x39 < ev->key()) {
cleanUpState();
return false;
}
auto str = preHoldText;
bool isUpper = str.isUpper();
str = str.toLower();
int key = ev->key() - 0x30;
if (key == 0) {
key = 10;
}
if (KeyData::KeyMappings[str].count() < key) {
cleanUpState();
return false;
}
auto data = KeyData::KeyMappings[str][key - 1];
applyReplacement(isUpper ? data.toUpper() : data);
isPreHold = false;
preHoldText = QString();
popup->hide();
return true;
}
// this is the state before we have a held key
if (ev->isAutoRepeat()) {
if (isNothing)
return true;
if (!isPreHold) {
if (ev->text().isEmpty())
return false;
if (!KeyData::KeyMappings.contains(ev->text().toLower()))
return false;
auto tooltipText = dataForIndex(ev->text().toLower(), ev->text().isUpper());
showPopup(tooltipText);
isPreHold = true;
preHoldText = ev->text();
}
return true;
}
cleanUpState();
}
return false;
}
/*
* Copyright 2020 Carson Black <uhhadd@gmail.com>
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2 of the License or ( at
* your option ) version 3 or, at the discretion of KDE e.V. ( which shall
* act as a proxy as in section 14 of the GPLv3 ), any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifndef PLASMAIMCONTEXT_H
#define PLASMAIMCONTEXT_H
#include <QPointer>
#include <QWidget>
#include <KConfigGroup>
#include <KSharedConfig>
#include <KConfigWatcher>
#include <qpa/qplatforminputcontext.h>
QT_BEGIN_NAMESPACE
struct TooltipData {
QString character;
QString number;
int idx;
};
class PlasmaIMContext : public QPlatformInputContext
{
Q_OBJECT
public:
PlasmaIMContext();
~PlasmaIMContext();
bool isValid() const Q_DECL_OVERRIDE;
void setFocusObject(QObject *object) Q_DECL_OVERRIDE;
bool filterEvent(const QEvent* event) Q_DECL_OVERRIDE;
private:
void cleanUpState();
void applyReplacement(const QString& data);
void showPopup(const QList<TooltipData>& text);
void configChangedHandler(const KConfigGroup& grp, const QByteArrayList& names);
QPointer<QWidget> popup;
QPointer<QObject> m_focusObject = nullptr;
bool isPreHold = false;
QString preHoldText = QString();
KSharedConfig::Ptr config = KSharedConfig::openConfig( QStringLiteral("kcminputrc") );
KConfigGroup keyboard = KConfigGroup(config, "Keyboard");
KConfigWatcher::Ptr watcher = KConfigWatcher::create(config);
};
QT_END_NAMESPACE
#endif
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment