Fix double-space period, shift double-tap; settings; letters from system keymap
- Double-space replaced the space via a Backspace keysym, which clients apply after the commit: use delete_surrounding_text with the commit - Tests use a shared FakeEngine that applies ops to a simulated field - Double-tap shift locks at a sentence start; typing between taps doesn't - Organization name set: config lives in ~/.config/sloposk/ - Settings (auto-capitalization, double-space period) with a ⚙ panel - Letter rows come from the compositor's XKB keymap; other letters of the layout become long-press alternates; globe switches the system layout over org.kde.keyboard - One JSON template for non-letter layers; stretch keys even out rows Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+10
-1
@@ -10,15 +10,18 @@ qt_add_library(sloposk_core STATIC
|
||||
input/AlternatesPicker.h input/AlternatesPicker.cpp
|
||||
input/KeyRepeater.h input/KeyRepeater.cpp
|
||||
input/TextRules.h input/TextRules.cpp
|
||||
input/TypingOptions.h
|
||||
input/KeyboardController.h input/KeyboardController.cpp
|
||||
input/KeyboardSurface.h input/KeyboardSurface.cpp
|
||||
layout/Layout.h
|
||||
layout/LayoutLoader.h layout/LayoutLoader.cpp
|
||||
layout/LayoutRepository.h layout/LayoutRepository.cpp
|
||||
layout/KeymapLetters.h layout/KeymapLetters.cpp
|
||||
layout/LayoutBuilder.h layout/LayoutBuilder.cpp
|
||||
layout/KeyGeometry.h layout/KeyGeometry.cpp
|
||||
layout/LayoutModel.h layout/LayoutModel.cpp
|
||||
window/VisibilityController.h window/VisibilityController.cpp
|
||||
window/WindowSettings.h window/WindowSettings.cpp
|
||||
settings/Settings.h settings/Settings.cpp
|
||||
)
|
||||
|
||||
# QML lives in the top-level qml/ dir; alias each file so the module sees it at its root.
|
||||
@@ -29,6 +32,9 @@ set(SLOPOSK_QML_FILES
|
||||
AlternatesPopup.qml
|
||||
DragHandle.qml
|
||||
ResizeHandle.qml
|
||||
SettingsButton.qml
|
||||
SettingsPanel.qml
|
||||
ToggleRow.qml
|
||||
Theme.qml
|
||||
)
|
||||
list(TRANSFORM SLOPOSK_QML_FILES PREPEND ${PROJECT_SOURCE_DIR}/qml/ OUTPUT_VARIABLE sloposk_qml_paths)
|
||||
@@ -55,6 +61,7 @@ target_include_directories(sloposk_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_include_directories(sloposk_core PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/input
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/layout
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/settings
|
||||
)
|
||||
target_link_libraries(sloposk_core PUBLIC Qt6::Quick PkgConfig::XKB)
|
||||
|
||||
@@ -64,9 +71,11 @@ qt_add_executable(sloposk
|
||||
main.cpp
|
||||
app/DevHooks.h app/DevHooks.cpp
|
||||
app/DBusControl.h app/DBusControl.cpp
|
||||
app/SystemLayouts.h app/SystemLayouts.cpp
|
||||
backend/InputMethodProtocol.h
|
||||
backend/KWinInputMethod.h backend/KWinInputMethod.cpp
|
||||
backend/KWinInputContext.h backend/KWinInputContext.cpp
|
||||
backend/WaylandKeymap.h backend/WaylandKeymap.cpp
|
||||
window/FloatingWindow.h window/FloatingWindow.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "SystemLayouts.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusReply>
|
||||
|
||||
namespace {
|
||||
const QString Service = QStringLiteral("org.kde.keyboard");
|
||||
const QString Path = QStringLiteral("/Layouts");
|
||||
const QString Interface = QStringLiteral("org.kde.KeyboardLayouts");
|
||||
|
||||
QDBusMessage call(const QString &method, const QVariantList &args = {})
|
||||
{
|
||||
QDBusMessage message = QDBusMessage::createMethodCall(Service, Path, Interface, method);
|
||||
message.setArguments(args);
|
||||
return message;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SystemLayouts::SystemLayouts(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
const QDBusReply<uint> reply = bus.call(call(QStringLiteral("getLayout")));
|
||||
m_system = reply.isValid();
|
||||
if (!m_system)
|
||||
return;
|
||||
m_current = reply.value();
|
||||
bus.connect(Service, Path, Interface, QStringLiteral("layoutChanged"), this, SLOT(setCurrent(uint)));
|
||||
}
|
||||
|
||||
void SystemLayouts::next(quint32 count)
|
||||
{
|
||||
if (count < 2)
|
||||
return;
|
||||
const quint32 index = (m_current + 1) % count;
|
||||
if (m_system)
|
||||
QDBusConnection::sessionBus().asyncCall(call(QStringLiteral("setLayout"), {index})); // echoes layoutChanged
|
||||
else
|
||||
setCurrent(index);
|
||||
}
|
||||
|
||||
void SystemLayouts::setCurrent(uint index)
|
||||
{
|
||||
if (index == m_current)
|
||||
return;
|
||||
m_current = index;
|
||||
Q_EMIT currentChanged();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
// Which of the system's keyboard layouts is active, followed and switched
|
||||
// over KWin's org.kde.keyboard D-Bus interface, so the on-screen and the
|
||||
// physical keyboard always agree. Without that service it switches locally.
|
||||
class SystemLayouts : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SystemLayouts(QObject *parent = nullptr);
|
||||
|
||||
quint32 current() const { return m_current; }
|
||||
// Switches to the next of `count` layouts.
|
||||
void next(quint32 count);
|
||||
|
||||
Q_SIGNALS:
|
||||
void currentChanged();
|
||||
|
||||
private Q_SLOTS:
|
||||
void setCurrent(uint index);
|
||||
|
||||
private:
|
||||
bool m_system = false;
|
||||
quint32 m_current = 0;
|
||||
};
|
||||
@@ -53,6 +53,14 @@ void KWinInputContext::commit(const QString &text)
|
||||
commit_string(m_serial, text);
|
||||
}
|
||||
|
||||
void KWinInputContext::replaceBeforeCursor(const QString &before, const QString &text)
|
||||
{
|
||||
// Offsets are UTF-8 bytes relative to the cursor; applied with the commit.
|
||||
const auto bytes = uint32_t(before.toUtf8().size());
|
||||
delete_surrounding_text(-int32_t(bytes), bytes);
|
||||
commit_string(m_serial, text);
|
||||
}
|
||||
|
||||
void KWinInputContext::preedit(const QString &text)
|
||||
{
|
||||
preedit_string(m_serial, text, text);
|
||||
|
||||
@@ -19,6 +19,7 @@ public:
|
||||
TextContext context() const { return m_context; }
|
||||
|
||||
void commit(const QString &text);
|
||||
void replaceBeforeCursor(const QString &before, const QString &text);
|
||||
void preedit(const QString &text);
|
||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers);
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ void KWinInputMethod::commitText(const QString &text)
|
||||
m_context->commit(text);
|
||||
}
|
||||
|
||||
void KWinInputMethod::replaceBeforeCursor(const QString &before, const QString &text)
|
||||
{
|
||||
if (m_context)
|
||||
m_context->replaceBeforeCursor(before, text);
|
||||
}
|
||||
|
||||
void KWinInputMethod::setPreedit(const QString &text)
|
||||
{
|
||||
if (m_context)
|
||||
|
||||
@@ -43,6 +43,7 @@ public:
|
||||
TextContext context() const override;
|
||||
|
||||
void commitText(const QString &text) override;
|
||||
void replaceBeforeCursor(const QString &before, const QString &text) override;
|
||||
void setPreedit(const QString &text) override;
|
||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override;
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ public:
|
||||
virtual TextContext context() const { return {}; }
|
||||
|
||||
Q_INVOKABLE virtual void commitText(const QString &text) = 0;
|
||||
// Replaces `before`, the text right before the cursor, with `text` in one
|
||||
// step. Don't emulate this with a Backspace keysym: clients may apply key
|
||||
// events after commits, so the deletion would hit the new text.
|
||||
virtual void replaceBeforeCursor(const QString &before, const QString &text) = 0;
|
||||
Q_INVOKABLE virtual void setPreedit(const QString &text) = 0;
|
||||
Q_INVOKABLE virtual void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier) = 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#include "WaylandKeymap.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <wayland-client.h>
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// Plain libwayland on Qt's display: Qt's own seat wrappers are private API.
|
||||
// Our proxies live on the default queue, which Qt dispatches.
|
||||
class WaylandKeymap::Seat
|
||||
{
|
||||
public:
|
||||
Seat(wl_display *display, WaylandKeymap &owner)
|
||||
: m_owner(owner)
|
||||
, m_registry(wl_display_get_registry(display))
|
||||
{
|
||||
wl_registry_add_listener(m_registry, &RegistryListener, this);
|
||||
}
|
||||
|
||||
~Seat()
|
||||
{
|
||||
if (m_keyboard)
|
||||
wl_keyboard_release(m_keyboard);
|
||||
if (m_seat)
|
||||
wl_seat_release(m_seat);
|
||||
wl_registry_destroy(m_registry);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint32_t SeatVersion = 5; // wl_keyboard.release, wl_seat.release
|
||||
|
||||
static void global(void *data, wl_registry *registry, uint32_t name, const char *interface, uint32_t version)
|
||||
{
|
||||
auto *self = static_cast<Seat *>(data);
|
||||
if (self->m_seat || std::strcmp(interface, wl_seat_interface.name) != 0 || version < SeatVersion)
|
||||
return;
|
||||
self->m_seat = static_cast<wl_seat *>(wl_registry_bind(registry, name, &wl_seat_interface, SeatVersion));
|
||||
wl_seat_add_listener(self->m_seat, &SeatListener, self);
|
||||
}
|
||||
|
||||
static void capabilities(void *data, wl_seat *seat, uint32_t caps)
|
||||
{
|
||||
auto *self = static_cast<Seat *>(data);
|
||||
const bool hasKeyboard = caps & WL_SEAT_CAPABILITY_KEYBOARD;
|
||||
if (hasKeyboard && !self->m_keyboard) {
|
||||
self->m_keyboard = wl_seat_get_keyboard(seat);
|
||||
wl_keyboard_add_listener(self->m_keyboard, &KeyboardListener, self);
|
||||
} else if (!hasKeyboard && self->m_keyboard) {
|
||||
wl_keyboard_release(std::exchange(self->m_keyboard, nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
static void keymap(void *data, wl_keyboard *, uint32_t format, int32_t fd, uint32_t size)
|
||||
{
|
||||
if (format == WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)
|
||||
static_cast<Seat *>(data)->m_owner.loadKeymap(fd, size);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static constexpr wl_registry_listener RegistryListener = {
|
||||
.global = global,
|
||||
.global_remove = [](void *, wl_registry *, uint32_t) {},
|
||||
};
|
||||
static constexpr wl_seat_listener SeatListener = {
|
||||
.capabilities = capabilities,
|
||||
.name = [](void *, wl_seat *, const char *) {},
|
||||
};
|
||||
// The window never has keyboard focus; only the keymap matters.
|
||||
static constexpr wl_keyboard_listener KeyboardListener = {
|
||||
.keymap = keymap,
|
||||
.enter = [](void *, wl_keyboard *, uint32_t, wl_surface *, wl_array *) {},
|
||||
.leave = [](void *, wl_keyboard *, uint32_t, wl_surface *) {},
|
||||
.key = [](void *, wl_keyboard *, uint32_t, uint32_t, uint32_t, uint32_t) {},
|
||||
.modifiers = [](void *, wl_keyboard *, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) {},
|
||||
.repeat_info = [](void *, wl_keyboard *, int32_t, int32_t) {},
|
||||
};
|
||||
|
||||
WaylandKeymap &m_owner;
|
||||
wl_registry *m_registry;
|
||||
wl_seat *m_seat = nullptr;
|
||||
wl_keyboard *m_keyboard = nullptr;
|
||||
};
|
||||
|
||||
WaylandKeymap::WaylandKeymap(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_context(xkb_context_new(XKB_CONTEXT_NO_FLAGS), xkb_context_unref)
|
||||
, m_keymap(nullptr, xkb_keymap_unref)
|
||||
{
|
||||
setKeymap(xkb_keymap_new_from_names(m_context.get(), nullptr, XKB_KEYMAP_COMPILE_NO_FLAGS));
|
||||
|
||||
if (auto *wayland = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>())
|
||||
m_seat = std::make_unique<Seat>(wayland->display(), *this);
|
||||
}
|
||||
|
||||
WaylandKeymap::~WaylandKeymap() = default;
|
||||
|
||||
quint32 WaylandKeymap::layoutCount() const
|
||||
{
|
||||
return m_keymap ? xkb_keymap_num_layouts(m_keymap.get()) : 0;
|
||||
}
|
||||
|
||||
void WaylandKeymap::loadKeymap(int fd, uint32_t size)
|
||||
{
|
||||
void *data = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
if (data == MAP_FAILED)
|
||||
return;
|
||||
const auto *text = static_cast<const char *>(data);
|
||||
// `size` counts the terminating NUL; the buffer API wants the text length.
|
||||
setKeymap(xkb_keymap_new_from_buffer(m_context.get(), text, strnlen(text, size), XKB_KEYMAP_FORMAT_TEXT_V1,
|
||||
XKB_KEYMAP_COMPILE_NO_FLAGS));
|
||||
munmap(data, size);
|
||||
}
|
||||
|
||||
void WaylandKeymap::setKeymap(xkb_keymap *keymap)
|
||||
{
|
||||
if (!keymap) {
|
||||
qWarning("Failed to compile the keyboard keymap");
|
||||
return;
|
||||
}
|
||||
m_keymap.reset(keymap);
|
||||
Q_EMIT changed();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
struct xkb_context;
|
||||
struct xkb_keymap;
|
||||
|
||||
// The compositor's keymap, received on a wl_keyboard of our own (the keyboard
|
||||
// window never has focus, but the keymap is sent regardless). Until it
|
||||
// arrives, or without a keyboard on the seat, XKB's default is used
|
||||
// (XKB_DEFAULT_LAYOUT etc., else "us").
|
||||
class WaylandKeymap : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WaylandKeymap(QObject *parent = nullptr);
|
||||
~WaylandKeymap() override;
|
||||
|
||||
// Valid until the next changed().
|
||||
xkb_keymap *keymap() const { return m_keymap.get(); }
|
||||
quint32 layoutCount() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void changed();
|
||||
|
||||
private:
|
||||
class Seat;
|
||||
|
||||
void loadKeymap(int fd, uint32_t size);
|
||||
void setKeymap(xkb_keymap *keymap);
|
||||
|
||||
std::unique_ptr<xkb_context, void (*)(xkb_context *)> m_context;
|
||||
std::unique_ptr<xkb_keymap, void (*)(xkb_keymap *)> m_keymap;
|
||||
std::unique_ptr<Seat> m_seat;
|
||||
};
|
||||
@@ -12,6 +12,11 @@ void DirectEngine::commitText(const QString &text)
|
||||
m_backend.commitText(text);
|
||||
}
|
||||
|
||||
void DirectEngine::replaceBeforeCursor(const QString &before, const QString &text)
|
||||
{
|
||||
m_backend.replaceBeforeCursor(before, text);
|
||||
}
|
||||
|
||||
void DirectEngine::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers)
|
||||
{
|
||||
m_backend.sendKeysym(keysym, modifiers);
|
||||
|
||||
@@ -10,6 +10,7 @@ public:
|
||||
explicit DirectEngine(TextBackend &backend);
|
||||
|
||||
void commitText(const QString &text) override;
|
||||
void replaceBeforeCursor(const QString &before, const QString &text) override;
|
||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override;
|
||||
|
||||
private:
|
||||
|
||||
@@ -12,5 +12,7 @@ public:
|
||||
virtual ~InputEngine() = default;
|
||||
|
||||
virtual void commitText(const QString &text) = 0;
|
||||
// See TextBackend::replaceBeforeCursor.
|
||||
virtual void replaceBeforeCursor(const QString &before, const QString &text) = 0;
|
||||
virtual void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier) = 0;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "engine/InputEngine.h"
|
||||
#include "layout/LayoutModel.h"
|
||||
#include "layout/LayoutRepository.h"
|
||||
|
||||
#include <xkbcommon/xkbcommon-keysyms.h>
|
||||
|
||||
@@ -17,15 +16,11 @@ namespace {
|
||||
constexpr qsizetype MaxTextBefore = 256;
|
||||
}
|
||||
|
||||
KeyboardController::KeyboardController(InputEngine &engine, LayoutModel &model,
|
||||
const LayoutRepository &layouts, QObject *parent)
|
||||
KeyboardController::KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_engine(engine)
|
||||
, m_model(model)
|
||||
, m_layouts(layouts)
|
||||
{
|
||||
if (!m_layouts.layouts().isEmpty())
|
||||
m_model.setLayout(m_layouts.layouts().first());
|
||||
connect(&m_backspaceRepeater, &KeyRepeater::repeat, this, &KeyboardController::deleteBackward);
|
||||
}
|
||||
|
||||
@@ -79,7 +74,7 @@ void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QStrin
|
||||
autoCapitalize();
|
||||
break;
|
||||
case KeyType::Globe:
|
||||
nextLayout();
|
||||
Q_EMIT nextLayoutRequested();
|
||||
break;
|
||||
case KeyType::Dismiss:
|
||||
Q_EMIT dismissRequested();
|
||||
@@ -130,26 +125,25 @@ void KeyboardController::setContext(const TextContext &context)
|
||||
autoCapitalize();
|
||||
}
|
||||
|
||||
void KeyboardController::nextLayout()
|
||||
void KeyboardController::setOptions(const TypingOptions &options)
|
||||
{
|
||||
const auto &layouts = m_layouts.layouts();
|
||||
if (layouts.size() < 2)
|
||||
return;
|
||||
m_layoutIndex = int((m_layoutIndex + 1) % layouts.size());
|
||||
m_model.setLayout(layouts[m_layoutIndex]);
|
||||
m_options = options;
|
||||
autoCapitalize();
|
||||
}
|
||||
|
||||
void KeyboardController::typeText(const QString &text)
|
||||
{
|
||||
if (text == u" " && m_context.isProse() && TextRules::wantsDoubleSpacePeriod(m_textBefore)) {
|
||||
m_engine.sendKeysym(XKB_KEY_BackSpace);
|
||||
if (text == u" " && m_options.doubleSpacePeriod && m_context.isProse()
|
||||
&& TextRules::wantsDoubleSpacePeriod(m_textBefore)) {
|
||||
// "word " + space → "word. ", replacing the first space in place.
|
||||
m_engine.replaceBeforeCursor(QStringLiteral(" "), QStringLiteral(". "));
|
||||
m_textBefore.chop(1);
|
||||
typeText(QStringLiteral(". "));
|
||||
return;
|
||||
m_textBefore += QStringLiteral(". ");
|
||||
} else {
|
||||
m_engine.commitText(text);
|
||||
m_textBefore += text;
|
||||
}
|
||||
|
||||
m_engine.commitText(text);
|
||||
m_textBefore = (m_textBefore + text).right(MaxTextBefore);
|
||||
m_textBefore = m_textBefore.right(MaxTextBefore);
|
||||
changeShift([&] { m_shift.characterTyped(); });
|
||||
autoCapitalize();
|
||||
}
|
||||
@@ -164,7 +158,8 @@ void KeyboardController::deleteBackward(bool wholeWord)
|
||||
|
||||
void KeyboardController::autoCapitalize()
|
||||
{
|
||||
const bool capitalize = m_context.allowsAutoCapitalization() && TextRules::startsSentence(m_textBefore);
|
||||
const bool capitalize = m_options.autoCapitalize && m_context.allowsAutoCapitalization()
|
||||
&& TextRules::startsSentence(m_textBefore);
|
||||
changeShift([&] { m_shift.setAutomatic(capitalize); });
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "KeyRepeater.h"
|
||||
#include "ShiftState.h"
|
||||
#include "TypingOptions.h"
|
||||
#include "backend/TextContext.h"
|
||||
#include "layout/Layout.h"
|
||||
|
||||
@@ -11,12 +12,11 @@
|
||||
|
||||
class InputEngine;
|
||||
class LayoutModel;
|
||||
class LayoutRepository;
|
||||
|
||||
// Which of a key's outputs the gesture selected.
|
||||
enum class KeyOutput { Primary, Flick, Alternate };
|
||||
|
||||
// Keyboard rules: what each key does, shift, layers, layout switching and the
|
||||
// Keyboard rules: what each key does, shift, layers and the
|
||||
// iOS conveniences (auto-capitalization, double-space period, backspace
|
||||
// repeat). Knows nothing about touch or Wayland: it receives key presses and
|
||||
// text-field context, and talks to an InputEngine.
|
||||
@@ -31,8 +31,7 @@ public:
|
||||
enum class ShiftMode { Off, Once, Locked };
|
||||
Q_ENUM(ShiftMode)
|
||||
|
||||
KeyboardController(InputEngine &engine, LayoutModel &model, const LayoutRepository &layouts,
|
||||
QObject *parent = nullptr);
|
||||
KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent = nullptr);
|
||||
|
||||
ShiftMode shiftMode() const;
|
||||
|
||||
@@ -48,12 +47,12 @@ public:
|
||||
void resetContext();
|
||||
// The focused field reported new text/content type.
|
||||
void setContext(const TextContext &context);
|
||||
|
||||
Q_INVOKABLE void nextLayout();
|
||||
void setOptions(const TypingOptions &options);
|
||||
|
||||
Q_SIGNALS:
|
||||
void shiftChanged();
|
||||
void dismissRequested();
|
||||
void nextLayoutRequested(); // the globe key: switching is the system's job
|
||||
|
||||
private:
|
||||
void typeText(const QString &text);
|
||||
@@ -64,10 +63,9 @@ private:
|
||||
|
||||
InputEngine &m_engine;
|
||||
LayoutModel &m_model;
|
||||
const LayoutRepository &m_layouts;
|
||||
TypingOptions m_options;
|
||||
ShiftState m_shift;
|
||||
KeyRepeater m_backspaceRepeater;
|
||||
int m_layoutIndex = 0;
|
||||
QString m_layerBeforeSwitch;
|
||||
|
||||
TextContext m_context;
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
|
||||
void ShiftState::press(qint64 timeMs)
|
||||
{
|
||||
const bool doubleTap = timeMs - m_lastPress <= DoubleTapMs;
|
||||
// Locks whatever the first tap did: at a sentence start that tap turns
|
||||
// auto-shift off, and the second must still lock.
|
||||
const bool doubleTap = timeMs - m_lastPress <= DoubleTapMs && !m_lastPressUnlocked;
|
||||
m_lastPress = timeMs;
|
||||
m_lastPressUnlocked = m_mode == Mode::Locked;
|
||||
m_held = true;
|
||||
m_typedWhileHeld = false;
|
||||
|
||||
switch (m_mode) {
|
||||
case Mode::Off: m_mode = Mode::Once; break;
|
||||
case Mode::Off: m_mode = doubleTap ? Mode::Locked : Mode::Once; break;
|
||||
case Mode::Once: m_mode = doubleTap ? Mode::Locked : Mode::Off; break;
|
||||
case Mode::Locked: m_mode = Mode::Off; break;
|
||||
}
|
||||
@@ -23,6 +26,7 @@ void ShiftState::release()
|
||||
|
||||
void ShiftState::characterTyped()
|
||||
{
|
||||
m_lastPress = NoPress; // shift, type, shift is not a double tap
|
||||
if (m_held)
|
||||
m_typedWhileHeld = true;
|
||||
else if (m_mode == Mode::Once)
|
||||
|
||||
@@ -29,5 +29,7 @@ private:
|
||||
Mode m_mode = Mode::Off;
|
||||
bool m_held = false;
|
||||
bool m_typedWhileHeld = false;
|
||||
qint64 m_lastPress = std::numeric_limits<qint64>::min() / 2;
|
||||
bool m_lastPressUnlocked = false; // a tap leaving caps lock doesn't start a double tap
|
||||
static constexpr qint64 NoPress = std::numeric_limits<qint64>::min() / 2;
|
||||
qint64 m_lastPress = NoPress;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
// User choices for the typing conveniences. Plain data: the controller reads
|
||||
// it, Settings persists it.
|
||||
struct TypingOptions {
|
||||
bool autoCapitalize = true;
|
||||
bool doubleSpacePeriod = true;
|
||||
|
||||
bool operator==(const TypingOptions &) const = default;
|
||||
};
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
|
||||
qreal rowUnits(const Row &row)
|
||||
{
|
||||
qreal units = 0;
|
||||
@@ -12,8 +10,6 @@ qreal rowUnits(const Row &row)
|
||||
return units;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics)
|
||||
{
|
||||
QList<QRectF> rects;
|
||||
|
||||
@@ -14,3 +14,6 @@ struct KeyboardMetrics {
|
||||
// order (spacers included). One key unit has the same width in every row, so
|
||||
// rows with equal unit totals line up; shorter rows are centered.
|
||||
QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics = {});
|
||||
|
||||
// Total width of a row in key units.
|
||||
qreal rowUnits(const Row &row);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#include "KeymapLetters.h"
|
||||
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
|
||||
namespace {
|
||||
|
||||
// XKB keycodes are evdev codes + 8.
|
||||
constexpr xkb_keycode_t fromEvdev(int code) { return xkb_keycode_t(code + 8); }
|
||||
|
||||
struct KeyRange {
|
||||
int first;
|
||||
int last;
|
||||
};
|
||||
|
||||
// AD01–AD12, AC01–AC11, AB01–AB10 in evdev codes.
|
||||
constexpr KeyRange LetterRows[] = {{16, 27}, {30, 40}, {44, 53}};
|
||||
// AE01–AE12 (number row), BKSL, LSGT: letters here become extras.
|
||||
constexpr KeyRange ExtraKeys[] = {{2, 13}, {43, 43}, {86, 86}};
|
||||
|
||||
QString symbolText(xkb_keymap *keymap, xkb_keycode_t key, xkb_layout_index_t layout, xkb_level_index_t level)
|
||||
{
|
||||
const xkb_keysym_t *syms = nullptr;
|
||||
if (xkb_keymap_key_get_syms_by_level(keymap, key, layout, level, &syms) != 1)
|
||||
return {};
|
||||
const char32_t ucs = xkb_keysym_to_utf32(syms[0]); // 0 for dead keys and functions
|
||||
return ucs ? QString::fromUcs4(&ucs, 1) : QString();
|
||||
}
|
||||
|
||||
bool isLetter(const QString &text)
|
||||
{
|
||||
return !text.isEmpty() && QChar::isLetter(text.toUcs4().first());
|
||||
}
|
||||
|
||||
std::optional<Letter> letterAt(xkb_keymap *keymap, xkb_keycode_t key, xkb_layout_index_t layout)
|
||||
{
|
||||
const QString lower = symbolText(keymap, key, layout, 0);
|
||||
if (!isLetter(lower))
|
||||
return std::nullopt;
|
||||
const QString shifted = symbolText(keymap, key, layout, 1);
|
||||
return Letter{lower, isLetter(shifted) ? shifted : lower.toUpper()};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
KeymapLetters readKeymapLetters(xkb_keymap *keymap, quint32 layoutIndex)
|
||||
{
|
||||
KeymapLetters result;
|
||||
if (!keymap || layoutIndex >= xkb_keymap_num_layouts(keymap))
|
||||
return result;
|
||||
result.name = QString::fromUtf8(xkb_keymap_layout_get_name(keymap, layoutIndex));
|
||||
|
||||
const auto collect = [&](KeyRange range, QList<Letter> &into) {
|
||||
for (int code : std::views::iota(range.first, range.last + 1)) {
|
||||
if (const auto letter = letterAt(keymap, fromEvdev(code), layoutIndex))
|
||||
into.append(*letter);
|
||||
}
|
||||
};
|
||||
for (KeyRange range : LetterRows)
|
||||
collect(range, result.rows.emplace_back());
|
||||
for (KeyRange range : ExtraKeys)
|
||||
collect(range, result.extras);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
struct xkb_keymap;
|
||||
|
||||
struct Letter {
|
||||
QString lower;
|
||||
QString upper;
|
||||
};
|
||||
|
||||
// The letters one layout of an XKB keymap puts where a physical keyboard has
|
||||
// its three letter rows (Q…, A…, Z… positions). Letters on other keys, e.g.
|
||||
// Slovak ľščťž on the number row, are collected as extras.
|
||||
struct KeymapLetters {
|
||||
QString name; // e.g. "Slovak"
|
||||
QList<QList<Letter>> rows;
|
||||
QList<Letter> extras;
|
||||
};
|
||||
|
||||
KeymapLetters readKeymapLetters(xkb_keymap *keymap, quint32 layoutIndex);
|
||||
+11
-4
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
@@ -14,6 +15,7 @@ enum class KeyType {
|
||||
Globe,
|
||||
Dismiss,
|
||||
Spacer, // empty room, e.g. the half-key indent of the home row
|
||||
Letters, // template only: a row of letters from the system keymap
|
||||
};
|
||||
|
||||
struct KeyDef {
|
||||
@@ -24,6 +26,11 @@ struct KeyDef {
|
||||
QStringList alternates; // long-press choices
|
||||
QString target; // layer to switch to (LayerSwitch only)
|
||||
qreal width = 1.0; // in key units
|
||||
bool stretch = false; // grows so the row is as wide as the layer's widest
|
||||
|
||||
// Letters placeholders only.
|
||||
int lettersRow = -1; // which keymap row (0 = top)
|
||||
QStringList flicks; // flick symbol per letter, left to right
|
||||
|
||||
// Text this key shows/types; only character keys change case.
|
||||
QString text(bool uppercase) const
|
||||
@@ -42,11 +49,11 @@ struct Layer {
|
||||
};
|
||||
|
||||
struct Layout {
|
||||
QString id; // e.g. "en-US"
|
||||
QString name; // human readable
|
||||
QString locale; // e.g. "en_US"
|
||||
QString base; // id of a layout to take missing layers from
|
||||
QString id;
|
||||
QString name; // human readable, e.g. "Slovak"
|
||||
QList<Layer> layers; // the first one is shown by default
|
||||
// Template only: long-press choices for keymap letters, by lowercase letter.
|
||||
QHash<QString, QStringList> alternates;
|
||||
|
||||
const Layer *layer(const QString &layerName) const
|
||||
{
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "LayoutBuilder.h"
|
||||
#include "KeyGeometry.h"
|
||||
|
||||
#include <QSet>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
|
||||
// Letters without a Unicode decomposition, and the key they belong under.
|
||||
const QHash<QString, QString> &baseOverrides()
|
||||
{
|
||||
static const QHash<QString, QString> bases = {
|
||||
{QStringLiteral("ß"), QStringLiteral("s")}, {QStringLiteral("ł"), QStringLiteral("l")},
|
||||
{QStringLiteral("ø"), QStringLiteral("o")}, {QStringLiteral("æ"), QStringLiteral("a")},
|
||||
{QStringLiteral("œ"), QStringLiteral("o")}, {QStringLiteral("đ"), QStringLiteral("d")},
|
||||
{QStringLiteral("ð"), QStringLiteral("d")}, {QStringLiteral("þ"), QStringLiteral("t")},
|
||||
{QStringLiteral("ħ"), QStringLiteral("h")}, {QStringLiteral("ı"), QStringLiteral("i")},
|
||||
};
|
||||
return bases;
|
||||
}
|
||||
|
||||
// "š" → "s": the letter an accented letter is a variant of.
|
||||
QString baseLetter(const QString &letter)
|
||||
{
|
||||
if (const auto it = baseOverrides().constFind(letter); it != baseOverrides().cend())
|
||||
return *it;
|
||||
return letter.normalized(QString::NormalizationForm_D).left(1);
|
||||
}
|
||||
|
||||
// Long-press choices per key letter: the keymap's extras first (they're what
|
||||
// this language types), then the template's, minus letters that have a key.
|
||||
QHash<QString, QStringList> alternatesByLetter(const Layout &tmpl, const KeymapLetters &letters)
|
||||
{
|
||||
QSet<QString> onKeys;
|
||||
for (const auto &row : letters.rows) {
|
||||
for (const Letter &letter : row)
|
||||
onKeys.insert(letter.lower);
|
||||
}
|
||||
|
||||
QHash<QString, QStringList> result;
|
||||
const auto add = [&](const QString &base, const QString &choice) {
|
||||
QStringList &list = result[base];
|
||||
if (!onKeys.contains(choice) && !list.contains(choice))
|
||||
list.append(choice);
|
||||
};
|
||||
for (const Letter &extra : letters.extras)
|
||||
add(baseLetter(extra.lower), extra.lower);
|
||||
for (auto it = tmpl.alternates.cbegin(); it != tmpl.alternates.cend(); ++it) {
|
||||
for (const QString &choice : *it)
|
||||
add(it.key(), choice);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Row expandLetters(const Row &row, const KeymapLetters &letters, const QHash<QString, QStringList> &alternates)
|
||||
{
|
||||
Row result;
|
||||
for (const KeyDef &key : row) {
|
||||
if (key.type != KeyType::Letters) {
|
||||
result.append(key);
|
||||
continue;
|
||||
}
|
||||
const QList<Letter> source = letters.rows.value(key.lettersRow);
|
||||
for (qsizetype i = 0; i < source.size(); ++i) {
|
||||
KeyDef letter;
|
||||
letter.label = source[i].lower;
|
||||
letter.shifted = source[i].upper;
|
||||
letter.flick = key.flicks.value(i);
|
||||
letter.alternates = alternates.value(source[i].lower);
|
||||
result.append(letter);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void stretchRows(Layer &layer)
|
||||
{
|
||||
qreal widest = 0;
|
||||
for (const Row &row : layer.rows)
|
||||
widest = std::max(widest, rowUnits(row));
|
||||
|
||||
for (Row &row : layer.rows) {
|
||||
const auto stretchy = std::ranges::count_if(row, &KeyDef::stretch);
|
||||
if (stretchy == 0)
|
||||
continue;
|
||||
const qreal extra = (widest - rowUnits(row)) / qreal(stretchy);
|
||||
for (KeyDef &key : row) {
|
||||
if (key.stretch)
|
||||
key.width += extra;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters)
|
||||
{
|
||||
Layout layout = tmpl;
|
||||
layout.name = letters.name.isEmpty() ? tmpl.name : letters.name;
|
||||
layout.alternates.clear();
|
||||
|
||||
const QHash<QString, QStringList> alternates = alternatesByLetter(tmpl, letters);
|
||||
for (Layer &layer : layout.layers) {
|
||||
for (Row &row : layer.rows)
|
||||
row = expandLetters(row, letters, alternates);
|
||||
stretchRows(layer);
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "KeymapLetters.h"
|
||||
#include "Layout.h"
|
||||
|
||||
// Turns the template into the layout shown: fills its letters placeholders
|
||||
// from the keymap, gives each letter its flick and long-press choices, and
|
||||
// stretches the marked keys so every row of a layer is equally wide.
|
||||
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters);
|
||||
@@ -9,6 +9,16 @@ namespace LayoutLoader {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int LetterRows = 3;
|
||||
|
||||
QStringList toStringList(const QJsonValue &value)
|
||||
{
|
||||
QStringList list;
|
||||
for (const QJsonValue &item : value.toArray())
|
||||
list.append(item.toString());
|
||||
return list;
|
||||
}
|
||||
|
||||
const QHash<QString, KeyType> &keyTypesByName()
|
||||
{
|
||||
static const QHash<QString, KeyType> types = {
|
||||
@@ -21,6 +31,7 @@ const QHash<QString, KeyType> &keyTypesByName()
|
||||
{QStringLiteral("globe"), KeyType::Globe},
|
||||
{QStringLiteral("dismiss"), KeyType::Dismiss},
|
||||
{QStringLiteral("spacer"), KeyType::Spacer},
|
||||
{QStringLiteral("letters"), KeyType::Letters},
|
||||
};
|
||||
return types;
|
||||
}
|
||||
@@ -51,13 +62,19 @@ std::optional<KeyDef> parseKey(const QJsonValue &value, QString *error)
|
||||
key.flick = obj.value(u"flick").toString();
|
||||
key.target = obj.value(u"target").toString();
|
||||
key.width = obj.value(u"width").toDouble(1.0);
|
||||
for (const QJsonValue &alt : obj.value(u"alternates").toArray())
|
||||
key.alternates.append(alt.toString());
|
||||
key.stretch = obj.value(u"stretch").toBool();
|
||||
key.alternates = toStringList(obj.value(u"alternates"));
|
||||
key.lettersRow = obj.value(u"row").toInt(-1);
|
||||
key.flicks = toStringList(obj.value(u"flicks"));
|
||||
|
||||
if (key.type == KeyType::Character && key.label.isEmpty()) {
|
||||
*error = QStringLiteral("character key without a label");
|
||||
return std::nullopt;
|
||||
}
|
||||
if (key.type == KeyType::Letters && (key.lettersRow < 0 || key.lettersRow >= LetterRows)) {
|
||||
*error = QStringLiteral("letters need a row from 0 to %1").arg(LetterRows - 1);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (key.width <= 0) {
|
||||
*error = QStringLiteral("key \"%1\" has a non-positive width").arg(key.label);
|
||||
return std::nullopt;
|
||||
@@ -110,8 +127,9 @@ std::optional<Layout> parse(const QByteArray &json, QString *error)
|
||||
Layout layout;
|
||||
layout.id = root.value(u"id").toString();
|
||||
layout.name = root.value(u"name").toString(layout.id);
|
||||
layout.locale = root.value(u"locale").toString();
|
||||
layout.base = root.value(u"base").toString();
|
||||
const QJsonObject alternates = root.value(u"alternates").toObject();
|
||||
for (auto it = alternates.begin(); it != alternates.end(); ++it)
|
||||
layout.alternates.insert(it.key(), toStringList(it.value()));
|
||||
if (layout.id.isEmpty()) {
|
||||
*error = QStringLiteral("layout without an id");
|
||||
return std::nullopt;
|
||||
@@ -124,8 +142,8 @@ std::optional<Layout> parse(const QByteArray &json, QString *error)
|
||||
layout.layers.append(*layer);
|
||||
}
|
||||
|
||||
if (layout.layers.isEmpty() && layout.base.isEmpty()) {
|
||||
*error = QStringLiteral("layout has no layers and no base");
|
||||
if (layout.layers.isEmpty()) {
|
||||
*error = QStringLiteral("layout has no layers");
|
||||
return std::nullopt;
|
||||
}
|
||||
return layout;
|
||||
|
||||
@@ -6,16 +6,17 @@
|
||||
|
||||
class QJsonValue;
|
||||
|
||||
// Parses the JSON layout format (see layouts/en-US.json).
|
||||
// Parses the JSON template format (see layouts/template.json).
|
||||
// A key is either a plain string (a character key) or an object:
|
||||
// {"type": "backspace", "width": 1.5}
|
||||
// {"label": "e", "flick": "3", "alternates": ["é", "è"]}
|
||||
// {"type": "backspace", "width": 1.5, "stretch": true}
|
||||
// {"label": ",", "flick": "!", "alternates": ["…"]}
|
||||
// {"type": "letters", "row": 0, "flicks": ["1", "2"]} (filled from the keymap)
|
||||
namespace LayoutLoader {
|
||||
|
||||
std::optional<Layout> parse(const QByteArray &json, QString *error);
|
||||
std::optional<Layout> load(const QString &path, QString *error);
|
||||
|
||||
// Checks references that can only be resolved once bases are merged in.
|
||||
// Checks references between layers.
|
||||
bool validate(const Layout &layout, QString *error);
|
||||
|
||||
} // namespace LayoutLoader
|
||||
|
||||
@@ -16,6 +16,7 @@ QString keyTypeName(KeyType type)
|
||||
case KeyType::Globe: return QStringLiteral("globe");
|
||||
case KeyType::Dismiss: return QStringLiteral("dismiss");
|
||||
case KeyType::Spacer: return QStringLiteral("spacer");
|
||||
case KeyType::Letters: break; // expanded before it reaches the model
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#include "LayoutRepository.h"
|
||||
#include "LayoutLoader.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
Q_LOGGING_CATEGORY(lcLayouts, "sloposk.layouts")
|
||||
|
||||
void LayoutRepository::loadDirectory(const QString &dir)
|
||||
{
|
||||
const QDir directory(dir);
|
||||
for (const QString &file : directory.entryList({QStringLiteral("*.json")}, QDir::Files, QDir::Name)) {
|
||||
QString error;
|
||||
const auto layout = LayoutLoader::load(directory.filePath(file), &error);
|
||||
if (layout)
|
||||
add(*layout);
|
||||
else
|
||||
qCWarning(lcLayouts) << "Skipping" << directory.filePath(file) << ":" << error;
|
||||
}
|
||||
}
|
||||
|
||||
void LayoutRepository::finalize(const QString &preferredLocale)
|
||||
{
|
||||
resolveBases();
|
||||
|
||||
m_layouts.removeIf([](const Layout &layout) {
|
||||
QString error;
|
||||
if (LayoutLoader::validate(layout, &error))
|
||||
return false;
|
||||
qCWarning(lcLayouts) << "Dropping layout" << layout.id << ":" << error;
|
||||
return true;
|
||||
});
|
||||
|
||||
std::stable_partition(m_layouts.begin(), m_layouts.end(), [&](const Layout &layout) {
|
||||
return layout.locale == preferredLocale;
|
||||
});
|
||||
}
|
||||
|
||||
int LayoutRepository::indexOf(const QString &id) const
|
||||
{
|
||||
for (qsizetype i = 0; i < m_layouts.size(); ++i) {
|
||||
if (m_layouts[i].id == id)
|
||||
return int(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void LayoutRepository::add(const Layout &layout)
|
||||
{
|
||||
const int existing = indexOf(layout.id);
|
||||
if (existing >= 0)
|
||||
m_layouts[existing] = layout;
|
||||
else
|
||||
m_layouts.append(layout);
|
||||
}
|
||||
|
||||
void LayoutRepository::resolveBases()
|
||||
{
|
||||
// One level of inheritance keeps this simple and cycle-free.
|
||||
for (Layout &layout : m_layouts) {
|
||||
if (layout.base.isEmpty())
|
||||
continue;
|
||||
const int baseIndex = indexOf(layout.base);
|
||||
if (baseIndex < 0) {
|
||||
qCWarning(lcLayouts) << "Layout" << layout.id << "has unknown base" << layout.base;
|
||||
continue;
|
||||
}
|
||||
for (const Layer &layer : m_layouts[baseIndex].layers) {
|
||||
if (!layout.layer(layer.name))
|
||||
layout.layers.append(layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Layout.h"
|
||||
|
||||
// All usable layouts: built-ins from resources plus user overrides on disk.
|
||||
// A later layout with the same id replaces an earlier one.
|
||||
class LayoutRepository
|
||||
{
|
||||
public:
|
||||
// Loads every *.json in dir. Invalid files are skipped with a warning.
|
||||
void loadDirectory(const QString &dir);
|
||||
|
||||
// Merges base layers in, drops invalid layouts, puts `locale` first.
|
||||
void finalize(const QString &preferredLocale);
|
||||
|
||||
const QList<Layout> &layouts() const { return m_layouts; }
|
||||
int indexOf(const QString &id) const;
|
||||
|
||||
private:
|
||||
void add(const Layout &layout);
|
||||
void resolveBases();
|
||||
|
||||
QList<Layout> m_layouts;
|
||||
};
|
||||
+48
-15
@@ -1,16 +1,19 @@
|
||||
#include "app/DBusControl.h"
|
||||
#include "app/DevHooks.h"
|
||||
#include "app/SystemLayouts.h"
|
||||
#include "backend/KWinInputMethod.h"
|
||||
#include "backend/WaylandKeymap.h"
|
||||
#include "engine/DirectEngine.h"
|
||||
#include "input/KeyboardController.h"
|
||||
#include "layout/LayoutBuilder.h"
|
||||
#include "layout/LayoutLoader.h"
|
||||
#include "layout/LayoutModel.h"
|
||||
#include "layout/LayoutRepository.h"
|
||||
#include "settings/Settings.h"
|
||||
#include "window/FloatingWindow.h"
|
||||
#include "window/VisibilityController.h"
|
||||
#include "window/WindowSettings.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QLocale>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlExtensionPlugin>
|
||||
#include <QQuickWindow>
|
||||
@@ -20,13 +23,33 @@ Q_IMPORT_QML_PLUGIN(SloposkPlugin)
|
||||
|
||||
namespace {
|
||||
|
||||
void loadLayouts(LayoutRepository &layouts)
|
||||
std::optional<Layout> loadTemplate()
|
||||
{
|
||||
layouts.loadDirectory(QStringLiteral(":/layouts"));
|
||||
// User layouts (~/.local/share/sloposk/layouts) override built-ins by id.
|
||||
layouts.loadDirectory(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
|
||||
+ QStringLiteral("/layouts"));
|
||||
layouts.finalize(QLocale::system().name());
|
||||
// ~/.local/share/sloposk/template.json replaces the built-in one.
|
||||
const QString user = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("template.json"));
|
||||
for (const QString &path : {user, QStringLiteral(":/layouts/template.json")}) {
|
||||
if (path.isEmpty())
|
||||
continue;
|
||||
QString error;
|
||||
if (auto layout = LayoutLoader::load(path, &error); layout && LayoutLoader::validate(*layout, &error))
|
||||
return layout;
|
||||
qWarning("Skipping layout template %s: %s", qPrintable(path), qPrintable(error));
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// The shown layout is the template filled with the active system layout's letters.
|
||||
void connectLayouts(const Layout &tmpl, WaylandKeymap &keymap, SystemLayouts &systemLayouts,
|
||||
KeyboardController &controller, LayoutModel &model)
|
||||
{
|
||||
const auto rebuild = [&] {
|
||||
model.setLayout(buildLayout(tmpl, readKeymapLetters(keymap.keymap(), systemLayouts.current())));
|
||||
};
|
||||
QObject::connect(&keymap, &WaylandKeymap::changed, &model, rebuild);
|
||||
QObject::connect(&systemLayouts, &SystemLayouts::currentChanged, &model, rebuild);
|
||||
QObject::connect(&controller, &KeyboardController::nextLayoutRequested, &systemLayouts,
|
||||
[&] { systemLayouts.next(keymap.layoutCount()); });
|
||||
rebuild();
|
||||
}
|
||||
|
||||
void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings)
|
||||
@@ -65,25 +88,35 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
QGuiApplication app(argc, argv);
|
||||
QGuiApplication::setApplicationName(QStringLiteral("sloposk"));
|
||||
QGuiApplication::setOrganizationName(QStringLiteral("sloposk")); // ~/.config/sloposk/sloposk.conf
|
||||
QGuiApplication::setDesktopFileName(QStringLiteral("org.sloposk.keyboard"));
|
||||
|
||||
LayoutRepository layouts;
|
||||
loadLayouts(layouts);
|
||||
if (layouts.layouts().isEmpty()) {
|
||||
qCritical("No usable keyboard layouts found");
|
||||
const std::optional<Layout> layoutTemplate = loadTemplate();
|
||||
if (!layoutTemplate) {
|
||||
qCritical("No usable layout template found");
|
||||
return 1;
|
||||
}
|
||||
|
||||
KWinInputMethod backend;
|
||||
DirectEngine engine(backend);
|
||||
LayoutModel model;
|
||||
KeyboardController controller(engine, model, layouts);
|
||||
KeyboardController controller(engine, model);
|
||||
connectTextContext(backend, controller);
|
||||
|
||||
WaylandKeymap keymap;
|
||||
SystemLayouts systemLayouts;
|
||||
connectLayouts(*layoutTemplate, keymap, systemLayouts, controller, model);
|
||||
|
||||
Settings settings;
|
||||
controller.setOptions(settings.typingOptions());
|
||||
QObject::connect(&settings, &Settings::typingOptionsChanged, &controller,
|
||||
[&] { controller.setOptions(settings.typingOptions()); });
|
||||
|
||||
QQmlApplicationEngine qml;
|
||||
qml.setInitialProperties({
|
||||
{QStringLiteral("layoutModel"), QVariant::fromValue(&model)},
|
||||
{QStringLiteral("controller"), QVariant::fromValue(&controller)},
|
||||
{QStringLiteral("settings"), QVariant::fromValue(&settings)},
|
||||
});
|
||||
qml.loadFromModule("Sloposk", "Main");
|
||||
auto *window = qml.rootObjects().isEmpty() ? nullptr : qobject_cast<QQuickWindow *>(qml.rootObjects().first());
|
||||
@@ -94,8 +127,8 @@ int main(int argc, char *argv[])
|
||||
|
||||
FloatingWindow placement(window);
|
||||
window->setProperty("placement", QVariant::fromValue(&placement));
|
||||
WindowSettings settings;
|
||||
restoreAndPersistGeometry(placement, settings);
|
||||
WindowSettings windowSettings;
|
||||
restoreAndPersistGeometry(placement, windowSettings);
|
||||
|
||||
VisibilityController visibility;
|
||||
connectVisibility(visibility, backend, controller, placement);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "Settings.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
namespace {
|
||||
constexpr auto AutoCapitalizeKey = "typing/autoCapitalize";
|
||||
constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod";
|
||||
}
|
||||
|
||||
Settings::Settings(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
const QSettings store;
|
||||
const TypingOptions defaults;
|
||||
m_typing.autoCapitalize = store.value(AutoCapitalizeKey, defaults.autoCapitalize).toBool();
|
||||
m_typing.doubleSpacePeriod = store.value(DoubleSpacePeriodKey, defaults.doubleSpacePeriod).toBool();
|
||||
}
|
||||
|
||||
void Settings::setAutoCapitalize(bool enabled)
|
||||
{
|
||||
TypingOptions options = m_typing;
|
||||
options.autoCapitalize = enabled;
|
||||
setTypingOptions(options);
|
||||
}
|
||||
|
||||
void Settings::setDoubleSpacePeriod(bool enabled)
|
||||
{
|
||||
TypingOptions options = m_typing;
|
||||
options.doubleSpacePeriod = enabled;
|
||||
setTypingOptions(options);
|
||||
}
|
||||
|
||||
void Settings::setTypingOptions(const TypingOptions &options)
|
||||
{
|
||||
if (options == m_typing)
|
||||
return;
|
||||
m_typing = options;
|
||||
QSettings store;
|
||||
store.setValue(AutoCapitalizeKey, m_typing.autoCapitalize);
|
||||
store.setValue(DoubleSpacePeriodKey, m_typing.doubleSpacePeriod);
|
||||
Q_EMIT typingOptionsChanged();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "input/TypingOptions.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QtQml/qqmlregistration.h>
|
||||
|
||||
// User preferences, persisted with QSettings (~/.config/sloposk/sloposk.conf)
|
||||
// and editable from the keyboard's settings panel.
|
||||
class Settings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("Created in C++")
|
||||
Q_PROPERTY(bool autoCapitalize READ autoCapitalize WRITE setAutoCapitalize NOTIFY typingOptionsChanged)
|
||||
Q_PROPERTY(bool doubleSpacePeriod READ doubleSpacePeriod WRITE setDoubleSpacePeriod NOTIFY typingOptionsChanged)
|
||||
|
||||
public:
|
||||
explicit Settings(QObject *parent = nullptr);
|
||||
|
||||
TypingOptions typingOptions() const { return m_typing; }
|
||||
|
||||
bool autoCapitalize() const { return m_typing.autoCapitalize; }
|
||||
void setAutoCapitalize(bool enabled);
|
||||
bool doubleSpacePeriod() const { return m_typing.doubleSpacePeriod; }
|
||||
void setDoubleSpacePeriod(bool enabled);
|
||||
|
||||
Q_SIGNALS:
|
||||
void typingOptionsChanged();
|
||||
|
||||
private:
|
||||
void setTypingOptions(const TypingOptions &options);
|
||||
|
||||
TypingOptions m_typing;
|
||||
};
|
||||
Reference in New Issue
Block a user