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.
This commit is contained in:
Levi Neely 2026-10-09 11:31:30 +02:00
parent 7de4f8b549
commit fd84f59f14
7 changed files with 568 additions and 1 deletions

View File

@ -15,6 +15,7 @@ per `.so`.
| `deft_palette` | `[deft:util] palette` | `src/palette/` | `palette`, `deftcommands_lib` | | `deft_palette` | `[deft:util] palette` | `src/palette/` | `palette`, `deftcommands_lib` |
| `deft_switch` | `[deft:util] switch` | `src/switch/` | `palette`, `project_lib` | | `deft_switch` | `[deft:util] switch` | `src/switch/` | `palette`, `project_lib` |
| `deft_radial` | `[deft:util] radial` | `src/radial/` | `radial` | | `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_acme` | `[deft:nineify] acme` | `src/acme/` | self-contained |
| `deft_sam` | `[deft:nineify] sam` | `src/sam/` | `sam_lib` | | `deft_sam` | `[deft:nineify] sam` | `src/sam/` | `sam_lib` |
| `deft_plumb` | `[deft:nineify] plumb`| `src/plumb/` | `plumb_lib` | | `deft_plumb` | `[deft:nineify] plumb`| `src/plumb/` | `plumb_lib` |

View File

@ -64,7 +64,7 @@ ctest --test-dir build # or via ctest
## Install the plugins into Kate ## 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/`: KTextEditor plugins under `build/bin/kf6/ktexteditor/`:
| Plugin file | Display name | Feature | | 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_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_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_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_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_sam.so` | `[deft:nineify] sam` | `sam`-style structural regular expressions |
| `deft_plumb.so` | `[deft:nineify] plumb`| Plan 9 plumber (open under cursor) | | `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) default is used. See [docs/radials.example.json](docs/radials.example.json)
for the format. 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 ### `[deft:nineify] acme` — acme editing gestures
![Acme-style bracket/quote pair selection in Kate](docs/img/deft-acme-select.png) ![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.{h,cpp} [deft:util] radial — caret menu plugin
radialplugin.json plugin metadata radialplugin.json plugin metadata
test_*.cpp QTest suites test_*.cpp QTest suites
sloppyfocus/
sloppyfocusplugin.{h,cpp,json} [deft:util] sloppyfocus — focus follows mouse
commands/ commands/
texttransforms.{h,cpp} pure text transforms (headless) texttransforms.{h,cpp} pure text transforms (headless)
deftcommands.{h,cpp} ":"-verb KTextEditor::Command pack (used by palette) deftcommands.{h,cpp} ":"-verb KTextEditor::Command pack (used by palette)

View File

@ -8,4 +8,5 @@ if(KF6TextEditor_FOUND)
add_subdirectory(acme) add_subdirectory(acme)
add_subdirectory(commands) add_subdirectory(commands)
add_subdirectory(switch) add_subdirectory(switch)
add_subdirectory(sloppyfocus)
endif() endif()

View File

@ -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
)

View File

@ -0,0 +1,387 @@
/*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "sloppyfocusplugin.h"
#include <KConfigGroup>
#include <KLocalizedString>
#include <KPluginFactory>
#include <KSharedConfig>
#include <QApplication>
#include <QCheckBox>
#include <QDialog>
#include <QEvent>
#include <QFormLayout>
#include <QGroupBox>
#include <QIcon>
#include <QLabel>
#include <QSpinBox>
#include <QVBoxLayout>
#include <QWidget>
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<QWidget *>(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<KTextEditor::View *>(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<int>::of(&QSpinBox::valueChanged),
this, &SloppyFocusConfigPage::changed);
connect(m_minimumStaySpin, QOverload<int>::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<deft::SloppyFocusPlugin>();)
#include "sloppyfocusplugin.moc"

View File

@ -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 <KTextEditor/ConfigPage>
#include <KTextEditor/MainWindow>
#include <KTextEditor/Plugin>
#include <KTextEditor/View>
#include <QElapsedTimer>
#include <QObject>
#include <QPointer>
#include <QTimer>
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<QWidget> m_pendingPane;
QPointer<QWidget> 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

View File

@ -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"
}
}