From fd84f59f14dec88a49d9ae1a540b80de31a9c582 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 9 Oct 2026 11:31:30 +0200 Subject: [PATCH] sloppyfocus: fold kate-sloppyfocus into the suite as deft_sloppyfocus Bring the standalone kate-sloppyfocus plugin (focus-follows-mouse) into Deft as the seventh plugin, [deft:util] sloppyfocus, under src/sloppyfocus/. Ported to the suite's conventions: - Wrapped SloppyFocusPlugin/View/ConfigPage in namespace deft. - KF6-only CMake (dropped the KF5 fallback), target deft_sloppyfocus, installed to kf6/ktexteditor and guarded by KF6TextEditor_FOUND; links KF6::TextEditor/ CoreAddons/I18n/ConfigCore + Qt6::Widgets. - metadata.json -> sloppyfocusplugin.json with the [deft:util] sloppyfocus display name and author Levi Neely; factory JSON and .moc renamed to match. - Config group renamed to deft_sloppyfocus. Two port fixes required by the suite's stricter build: - forward-declare QCheckBox/QSpinBox at global scope (the original's inline 'class QSpinBox *' members became deft::QSpinBox once wrapped in the namespace). - the KConfigGroup group name must be a QString (QT_NO_CAST_FROM_ASCII is on here), so the group is a QStringLiteral rather than a const char *. README (plugin table, a new sloppyfocus section, layout tree) and AGENTS.md updated. The plugin already removes its qApp event filter in its destructor, so it is not subject to the reload crash fixed in 629a2da. 18/18 ctest green. --- AGENTS.md | 1 + README.md | 20 +- src/CMakeLists.txt | 1 + src/sloppyfocus/CMakeLists.txt | 21 ++ src/sloppyfocus/sloppyfocusplugin.cpp | 387 +++++++++++++++++++++++++ src/sloppyfocus/sloppyfocusplugin.h | 125 ++++++++ src/sloppyfocus/sloppyfocusplugin.json | 14 + 7 files changed, 568 insertions(+), 1 deletion(-) create mode 100644 src/sloppyfocus/CMakeLists.txt create mode 100644 src/sloppyfocus/sloppyfocusplugin.cpp create mode 100644 src/sloppyfocus/sloppyfocusplugin.h create mode 100644 src/sloppyfocus/sloppyfocusplugin.json diff --git a/AGENTS.md b/AGENTS.md index 60c9d3f..1c84277 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ per `.so`. | `deft_palette` | `[deft:util] palette` | `src/palette/` | `palette`, `deftcommands_lib` | | `deft_switch` | `[deft:util] switch` | `src/switch/` | `palette`, `project_lib` | | `deft_radial` | `[deft:util] radial` | `src/radial/` | `radial` | +| `deft_sloppyfocus` | `[deft:util] sloppyfocus` | `src/sloppyfocus/` | self-contained | | `deft_acme` | `[deft:nineify] acme` | `src/acme/` | self-contained | | `deft_sam` | `[deft:nineify] sam` | `src/sam/` | `sam_lib` | | `deft_plumb` | `[deft:nineify] plumb`| `src/plumb/` | `plumb_lib` | diff --git a/README.md b/README.md index 1df8afc..46a22ea 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ ctest --test-dir build # or via ctest ## Install the plugins into Kate -Deft is **one `.so` per feature**, not a monolith. The build produces six +Deft is **one `.so` per feature**, not a monolith. The build produces seven KTextEditor plugins under `build/bin/kf6/ktexteditor/`: | Plugin file | Display name | Feature | @@ -72,6 +72,7 @@ KTextEditor plugins under `build/bin/kf6/ktexteditor/`: | `deft_palette.so`| `[deft:util] palette` | Command palette (`Alt+X` / M-x) over every window action + the `:`-verbs + the switchers, frecency-ranked | | `deft_switch.so` | `[deft:util] switch` | Project **Go to File** (`Alt+P`) and **Go to Symbol** (`Alt+G`) | | `deft_radial.so` | `[deft:util] radial` | Radial caret menu (`Alt+R` / right-mouse) | +| `deft_sloppyfocus.so` | `[deft:util] sloppyfocus` | Focus follows the mouse into the view/pane under the pointer | | `deft_acme.so` | `[deft:nineify] acme` | Acme editing gestures (line keys, pair selection, mouse chords) | | `deft_sam.so` | `[deft:nineify] sam` | `sam`-style structural regular expressions | | `deft_plumb.so` | `[deft:nineify] plumb`| Plan 9 plumber (open under cursor) | @@ -233,6 +234,21 @@ through a list. default is used. See [docs/radials.example.json](docs/radials.example.json) for the format. +### `[deft:util] sloppyfocus` — focus follows mouse + +Keyboard focus follows the pointer: move the mouse into an editor view or tool +pane and, after a short hover delay, that pane gets keyboard focus — no click. +The acme way of working, where the mouse and the keyboard target the same place. + +- Works for editor views and focusable tool panes (terminal, file tree, etc.). +- **Focus-stealing prevention:** a configurable hover **delay** and a **minimum + stay time** stop rapid bouncing; focus is suppressed while a modal dialog is + open or when a floating widget in another window holds focus. +- Configure it in Settings → Configure Kate → Plugins → **Sloppy Focus** + (delay, minimum stay time, raise-on-focus, dialog/floating guards). +- Pairs naturally with `radial`'s pointer-follows-focus warping — one moves the + pointer to the focused pane, the other moves focus to the pointed-at pane. + ### `[deft:nineify] acme` — acme editing gestures ![Acme-style bracket/quote pair selection in Kate](docs/img/deft-acme-select.png) @@ -303,6 +319,8 @@ src/ radialplugin.{h,cpp} [deft:util] radial — caret menu plugin radialplugin.json plugin metadata test_*.cpp QTest suites + sloppyfocus/ + sloppyfocusplugin.{h,cpp,json} [deft:util] sloppyfocus — focus follows mouse commands/ texttransforms.{h,cpp} pure text transforms (headless) deftcommands.{h,cpp} ":"-verb KTextEditor::Command pack (used by palette) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0899773..936c774 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,4 +8,5 @@ if(KF6TextEditor_FOUND) add_subdirectory(acme) add_subdirectory(commands) add_subdirectory(switch) + add_subdirectory(sloppyfocus) endif() diff --git a/src/sloppyfocus/CMakeLists.txt b/src/sloppyfocus/CMakeLists.txt new file mode 100644 index 0000000..6fc25de --- /dev/null +++ b/src/sloppyfocus/CMakeLists.txt @@ -0,0 +1,21 @@ +# sloppyfocus — focus-follows-mouse ([deft:util] sloppyfocus). +# +# Self-contained plugin (no separate lib). Needs KF6::TextEditor; the config +# page persists settings via KF6::ConfigCore (KSharedConfig/KConfigGroup). +if(NOT KF6TextEditor_FOUND) + return() +endif() + +find_package(KF6Config QUIET CONFIG) + +kcoreaddons_add_plugin(deft_sloppyfocus + SOURCES sloppyfocusplugin.cpp sloppyfocusplugin.h + INSTALL_NAMESPACE "kf6/ktexteditor" +) +target_link_libraries(deft_sloppyfocus PRIVATE + KF6::TextEditor + KF6::CoreAddons + KF6::I18n + KF6::ConfigCore + Qt6::Widgets +) diff --git a/src/sloppyfocus/sloppyfocusplugin.cpp b/src/sloppyfocus/sloppyfocusplugin.cpp new file mode 100644 index 0000000..c17b2df --- /dev/null +++ b/src/sloppyfocus/sloppyfocusplugin.cpp @@ -0,0 +1,387 @@ +/* + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +#include "sloppyfocusplugin.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace deft +{ + +namespace +{ +const QString configGroup() { return QStringLiteral("deft_sloppyfocus"); } +const char *KEY_FOCUS_DELAY = "FocusDelay"; +const char *KEY_MIN_STAY = "MinimumStayTime"; +const char *KEY_RAISE = "RaiseOnFocus"; +const char *KEY_IGNORE_DIALOGS = "IgnoreDialogs"; +const char *KEY_IGNORE_FLOATING = "IgnoreFloatingWidgets"; +} // namespace + +// ============================================================ +// SloppyFocusPlugin +// ============================================================ + +SloppyFocusPlugin::SloppyFocusPlugin(QObject *parent, const QVariantList &args) + : KTextEditor::Plugin(parent) +{ + Q_UNUSED(args) + readConfig(); +} + +SloppyFocusPlugin::~SloppyFocusPlugin() = default; + +QObject *SloppyFocusPlugin::createView(KTextEditor::MainWindow *mainWindow) +{ + return new SloppyFocusView(this, mainWindow); +} + +int SloppyFocusPlugin::configPages() const +{ + return 1; +} + +KTextEditor::ConfigPage *SloppyFocusPlugin::configPage(int number, QWidget *parent) +{ + if (number == 0) { + return new SloppyFocusConfigPage(this, parent); + } + return nullptr; +} + +void SloppyFocusPlugin::setFocusDelay(int ms) +{ + m_focusDelay = qBound(0, ms, 5000); +} + +void SloppyFocusPlugin::setMinimumStayTime(int ms) +{ + m_minimumStayTime = qBound(0, ms, 5000); +} + +void SloppyFocusPlugin::setRaiseOnFocus(bool raise) +{ + m_raiseOnFocus = raise; +} + +void SloppyFocusPlugin::setIgnoreDialogs(bool ignore) +{ + m_ignoreDialogs = ignore; +} + +void SloppyFocusPlugin::setIgnoreFloatingWidgets(bool ignore) +{ + m_ignoreFloatingWidgets = ignore; +} + +void SloppyFocusPlugin::readConfig() +{ + KConfigGroup cg(KSharedConfig::openConfig(), configGroup()); + m_focusDelay = cg.readEntry(KEY_FOCUS_DELAY, 150); + m_minimumStayTime = cg.readEntry(KEY_MIN_STAY, 50); + m_raiseOnFocus = cg.readEntry(KEY_RAISE, false); + m_ignoreDialogs = cg.readEntry(KEY_IGNORE_DIALOGS, true); + m_ignoreFloatingWidgets = cg.readEntry(KEY_IGNORE_FLOATING, true); +} + +void SloppyFocusPlugin::writeConfig() +{ + KConfigGroup cg(KSharedConfig::openConfig(), configGroup()); + cg.writeEntry(KEY_FOCUS_DELAY, m_focusDelay); + cg.writeEntry(KEY_MIN_STAY, m_minimumStayTime); + cg.writeEntry(KEY_RAISE, m_raiseOnFocus); + cg.writeEntry(KEY_IGNORE_DIALOGS, m_ignoreDialogs); + cg.writeEntry(KEY_IGNORE_FLOATING, m_ignoreFloatingWidgets); + cg.sync(); + Q_EMIT configChanged(); +} + +// ============================================================ +// SloppyFocusView — event filter and focus switching logic +// ============================================================ + +SloppyFocusView::SloppyFocusView(SloppyFocusPlugin *plugin, KTextEditor::MainWindow *mainWindow) + : QObject(mainWindow->window()) + , m_plugin(plugin) + , m_mainWindow(mainWindow) +{ + m_focusTimer.setSingleShot(true); + connect(&m_focusTimer, &QTimer::timeout, this, &SloppyFocusView::activatePendingPane); + connect(m_plugin, &SloppyFocusPlugin::configChanged, this, &SloppyFocusView::onConfigChanged); + + m_lastFocusChange.start(); + + // Install event filter on QApplication to see Enter events for ALL widgets. + // A filter on just the main window won't propagate to children. + qApp->installEventFilter(this); +} + +SloppyFocusView::~SloppyFocusView() +{ + m_focusTimer.stop(); + qApp->removeEventFilter(this); +} + +void SloppyFocusView::onConfigChanged() +{ + // Timer interval is updated on next trigger; nothing else needed +} + +bool SloppyFocusView::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() != QEvent::Enter) { + return false; + } + + auto *widget = qobject_cast(obj); + if (!widget) { + return false; + } + + // Only handle widgets belonging to our main window + if (widget->window() != m_mainWindow->window()) { + return false; + } + + // Find the focusable pane (editor view, tool view, terminal, etc.) + QWidget *pane = paneForWidget(widget); + if (!pane) { + return false; + } + + // Don't re-trigger for the pane that already has focus + if (pane == m_currentPane) { + m_focusTimer.stop(); + m_pendingPane = nullptr; + return false; + } + + // Focus-stealing prevention: check minimum stay time + if (m_plugin->minimumStayTime() > 0 && + m_lastFocusChange.elapsed() < m_plugin->minimumStayTime()) { + return false; + } + + // Check if we should ignore due to active dialog + if (shouldIgnoreFocus()) { + return false; + } + + // Schedule focus change after delay + m_pendingPane = pane; + m_focusTimer.start(m_plugin->focusDelay()); + + return false; +} + +void SloppyFocusView::activatePendingPane() +{ + if (!m_pendingPane) { + return; + } + + QWidget *pane = m_pendingPane; + m_pendingPane = nullptr; + + if (!pane->isVisible()) { + return; + } + + // Verify mouse is still within the pane's geometry + QPoint globalPos = QCursor::pos(); + QPoint paneTopLeft = pane->mapToGlobal(QPoint(0, 0)); + QRect globalRect(paneTopLeft, pane->size()); + if (!globalRect.contains(globalPos)) { + return; + } + + // Move keyboard focus to the pane + pane->setFocus(); + m_currentPane = pane; + m_lastFocusChange.restart(); + + if (m_plugin->raiseOnFocus()) { + pane->raise(); + } +} + +QWidget *SloppyFocusView::paneForWidget(QWidget *widget) const +{ + // Walk up the widget hierarchy to find the nearest focusable pane. + // Priority: KTextEditor::View first, then any widget with a keyboard + // focus policy (tool views, terminals, file browsers, etc.). + QWidget *w = widget; + QWidget *focusCandidate = nullptr; + + while (w && w != m_mainWindow->window()) { + // KTextEditor::View is always a valid pane + if (qobject_cast(w)) { + return w; + } + + // Track the deepest widget that accepts keyboard focus as a + // fallback. This catches tool views, terminals, tree views, etc. + if (!focusCandidate) { + auto policy = w->focusPolicy(); + if (policy == Qt::StrongFocus || policy == Qt::WheelFocus) { + focusCandidate = w; + } + } + + w = w->parentWidget(); + } + + return focusCandidate; +} + +bool SloppyFocusView::shouldIgnoreFocus() const +{ + // Ignore if a modal dialog is active + if (m_plugin->ignoreDialogs()) { + if (QApplication::activeModalWidget()) { + return true; + } + } + + // Ignore if a floating/popup widget has focus + if (m_plugin->ignoreFloatingWidgets()) { + QWidget *focused = QApplication::focusWidget(); + if (focused && focused->window() != m_mainWindow->window()) { + return true; + } + } + + return false; +} + +// ============================================================ +// SloppyFocusConfigPage +// ============================================================ + +SloppyFocusConfigPage::SloppyFocusConfigPage(SloppyFocusPlugin *plugin, QWidget *parent) + : KTextEditor::ConfigPage(parent) + , m_plugin(plugin) +{ + auto *layout = new QVBoxLayout(this); + + // Focus timing group + auto *timingGroup = new QGroupBox(i18n("Focus Timing"), this); + auto *timingLayout = new QFormLayout(timingGroup); + + m_focusDelaySpin = new QSpinBox(this); + m_focusDelaySpin->setRange(0, 5000); + m_focusDelaySpin->setSuffix(i18n(" ms")); + m_focusDelaySpin->setToolTip(i18n("Time the mouse must hover over a view before focus switches. " + "Set to 0 for immediate focus change.")); + timingLayout->addRow(i18n("Focus delay:"), m_focusDelaySpin); + + m_minimumStaySpin = new QSpinBox(this); + m_minimumStaySpin->setRange(0, 5000); + m_minimumStaySpin->setSuffix(i18n(" ms")); + m_minimumStaySpin->setToolTip(i18n("Minimum time focus must remain on a view before another " + "switch is allowed. Prevents rapid bouncing.")); + timingLayout->addRow(i18n("Minimum stay time:"), m_minimumStaySpin); + + layout->addWidget(timingGroup); + + // Behavior group + auto *behaviorGroup = new QGroupBox(i18n("Focus-Stealing Prevention"), this); + auto *behaviorLayout = new QVBoxLayout(behaviorGroup); + + m_raiseOnFocusCheck = new QCheckBox(i18n("Raise view on focus"), this); + m_raiseOnFocusCheck->setToolTip(i18n("Raise the view widget when it receives sloppy focus")); + behaviorLayout->addWidget(m_raiseOnFocusCheck); + + m_ignoreDialogsCheck = new QCheckBox(i18n("Ignore when dialogs are open"), this); + m_ignoreDialogsCheck->setToolTip(i18n("Disable sloppy focus while a modal dialog is active")); + behaviorLayout->addWidget(m_ignoreDialogsCheck); + + m_ignoreFloatingCheck = new QCheckBox(i18n("Ignore when floating widgets have focus"), this); + m_ignoreFloatingCheck->setToolTip(i18n("Disable sloppy focus when focus is in a different " + "window (e.g., completion popups, tooltips)")); + behaviorLayout->addWidget(m_ignoreFloatingCheck); + + layout->addWidget(behaviorGroup); + layout->addStretch(); + + // Connect change signals + connect(m_focusDelaySpin, QOverload::of(&QSpinBox::valueChanged), + this, &SloppyFocusConfigPage::changed); + connect(m_minimumStaySpin, QOverload::of(&QSpinBox::valueChanged), + this, &SloppyFocusConfigPage::changed); + connect(m_raiseOnFocusCheck, &QCheckBox::toggled, + this, &SloppyFocusConfigPage::changed); + connect(m_ignoreDialogsCheck, &QCheckBox::toggled, + this, &SloppyFocusConfigPage::changed); + connect(m_ignoreFloatingCheck, &QCheckBox::toggled, + this, &SloppyFocusConfigPage::changed); + + reset(); +} + +SloppyFocusConfigPage::~SloppyFocusConfigPage() = default; + +QString SloppyFocusConfigPage::name() const +{ + return i18n("Sloppy Focus"); +} + +QString SloppyFocusConfigPage::fullName() const +{ + return i18n("Sloppy Focus (Focus Follows Mouse)"); +} + +QIcon SloppyFocusConfigPage::icon() const +{ + return QIcon::fromTheme(QStringLiteral("input-mouse")); +} + +void SloppyFocusConfigPage::apply() +{ + m_plugin->setFocusDelay(m_focusDelaySpin->value()); + m_plugin->setMinimumStayTime(m_minimumStaySpin->value()); + m_plugin->setRaiseOnFocus(m_raiseOnFocusCheck->isChecked()); + m_plugin->setIgnoreDialogs(m_ignoreDialogsCheck->isChecked()); + m_plugin->setIgnoreFloatingWidgets(m_ignoreFloatingCheck->isChecked()); + m_plugin->writeConfig(); +} + +void SloppyFocusConfigPage::defaults() +{ + m_focusDelaySpin->setValue(150); + m_minimumStaySpin->setValue(50); + m_raiseOnFocusCheck->setChecked(false); + m_ignoreDialogsCheck->setChecked(true); + m_ignoreFloatingCheck->setChecked(true); +} + +void SloppyFocusConfigPage::reset() +{ + m_focusDelaySpin->setValue(m_plugin->focusDelay()); + m_minimumStaySpin->setValue(m_plugin->minimumStayTime()); + m_raiseOnFocusCheck->setChecked(m_plugin->raiseOnFocus()); + m_ignoreDialogsCheck->setChecked(m_plugin->ignoreDialogs()); + m_ignoreFloatingCheck->setChecked(m_plugin->ignoreFloatingWidgets()); +} + +} // namespace deft + +K_PLUGIN_FACTORY_WITH_JSON(SloppyFocusPluginFactory, "sloppyfocusplugin.json", + registerPlugin();) + +#include "sloppyfocusplugin.moc" diff --git a/src/sloppyfocus/sloppyfocusplugin.h b/src/sloppyfocus/sloppyfocusplugin.h new file mode 100644 index 0000000..5db6945 --- /dev/null +++ b/src/sloppyfocus/sloppyfocusplugin.h @@ -0,0 +1,125 @@ +/* + * SPDX-License-Identifier: LGPL-2.0-or-later + * + * [deft:util] sloppyfocus — focus-follows-mouse for Kate. + * + * SloppyFocusPlugin installs an application event filter that moves keyboard + * focus to whatever editor view / tool pane the mouse enters, after a + * configurable hover delay, with focus-stealing prevention (minimum stay time, + * dialog / floating-widget guards). This is the acme-style sloppy focus that + * complements the radial's pointer-follows-focus warping: together the mouse + * and keyboard focus stay where your attention is. + */ +#ifndef DEFT_SLOPPYFOCUSPLUGIN_H +#define DEFT_SLOPPYFOCUSPLUGIN_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +class QCheckBox; +class QSpinBox; + +namespace deft +{ + +class SloppyFocusPlugin : public KTextEditor::Plugin +{ + Q_OBJECT + +public: + explicit SloppyFocusPlugin(QObject *parent, const QVariantList &args = {}); + ~SloppyFocusPlugin() override; + + QObject *createView(KTextEditor::MainWindow *mainWindow) override; + int configPages() const override; + KTextEditor::ConfigPage *configPage(int number, QWidget *parent) override; + + // Configuration + int focusDelay() const { return m_focusDelay; } + int minimumStayTime() const { return m_minimumStayTime; } + bool raiseOnFocus() const { return m_raiseOnFocus; } + bool ignoreDialogs() const { return m_ignoreDialogs; } + bool ignoreFloatingWidgets() const { return m_ignoreFloatingWidgets; } + + void setFocusDelay(int ms); + void setMinimumStayTime(int ms); + void setRaiseOnFocus(bool raise); + void setIgnoreDialogs(bool ignore); + void setIgnoreFloatingWidgets(bool ignore); + + void readConfig(); + void writeConfig(); + +Q_SIGNALS: + void configChanged(); + +private: + int m_focusDelay = 150; + int m_minimumStayTime = 50; + bool m_raiseOnFocus = false; + bool m_ignoreDialogs = true; + bool m_ignoreFloatingWidgets = true; +}; + +class SloppyFocusView : public QObject +{ + Q_OBJECT + +public: + explicit SloppyFocusView(SloppyFocusPlugin *plugin, KTextEditor::MainWindow *mainWindow); + ~SloppyFocusView() override; + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + +private Q_SLOTS: + void activatePendingPane(); + void onConfigChanged(); + +private: + QWidget *paneForWidget(QWidget *widget) const; + bool shouldIgnoreFocus() const; + + SloppyFocusPlugin *m_plugin; + KTextEditor::MainWindow *m_mainWindow; + QTimer m_focusTimer; + QElapsedTimer m_lastFocusChange; + QPointer m_pendingPane; + QPointer m_currentPane; +}; + +class SloppyFocusConfigPage : public KTextEditor::ConfigPage +{ + Q_OBJECT + +public: + explicit SloppyFocusConfigPage(SloppyFocusPlugin *plugin, QWidget *parent); + ~SloppyFocusConfigPage() override; + + QString name() const override; + QString fullName() const override; + QIcon icon() const override; + + void apply() override; + void defaults() override; + void reset() override; + +private: + SloppyFocusPlugin *m_plugin; + QSpinBox *m_focusDelaySpin; + QSpinBox *m_minimumStaySpin; + QCheckBox *m_raiseOnFocusCheck; + QCheckBox *m_ignoreDialogsCheck; + QCheckBox *m_ignoreFloatingCheck; +}; + +} // namespace deft + +#endif // DEFT_SLOPPYFOCUSPLUGIN_H diff --git a/src/sloppyfocus/sloppyfocusplugin.json b/src/sloppyfocus/sloppyfocusplugin.json new file mode 100644 index 0000000..77acc1f --- /dev/null +++ b/src/sloppyfocus/sloppyfocusplugin.json @@ -0,0 +1,14 @@ +{ + "KPlugin": { + "Authors": [ + { + "Name": "Levi Neely" + } + ], + "Description": "Focus follows mouse: keyboard focus moves to the editor view or tool pane under the pointer, with a configurable hover delay and focus-stealing prevention.", + "Icon": "input-mouse", + "License": "LGPL-2.0-or-later", + "Name": "[deft:util] sloppyfocus", + "Version": "0.1.0" + } +}