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:
parent
7de4f8b549
commit
fd84f59f14
|
|
@ -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` |
|
||||
|
|
|
|||
20
README.md
20
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
|
||||
|
||||

|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ if(KF6TextEditor_FOUND)
|
|||
add_subdirectory(acme)
|
||||
add_subdirectory(commands)
|
||||
add_subdirectory(switch)
|
||||
add_subdirectory(sloppyfocus)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue