#include "sloppyfocus.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(SloppyFocusPluginFactory, "metadata.json", registerPlugin();) static const char *CONFIG_GROUP = "SloppyFocus"; static const char *KEY_FOCUS_DELAY = "FocusDelay"; static const char *KEY_MIN_STAY = "MinimumStayTime"; static const char *KEY_RAISE = "RaiseOnFocus"; static const char *KEY_IGNORE_DIALOGS = "IgnoreDialogs"; static const char *KEY_IGNORE_FLOATING = "IgnoreFloatingWidgets"; // ============================================================ // 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(), CONFIG_GROUP); 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(), CONFIG_GROUP); 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::activatePendingView); 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 KTextEditor::View ancestor of the entered widget KTextEditor::View *view = viewForWidget(widget); if (!view) { return false; } // Don't re-trigger for the view that already has focus if (view == m_currentView) { m_focusTimer.stop(); m_pendingView = 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_pendingView = view; m_focusTimer.start(m_plugin->focusDelay()); return false; } void SloppyFocusView::activatePendingView() { if (!m_pendingView) { return; } // Final check: is the mouse still over this view? // (The view might have been destroyed or mouse moved away) KTextEditor::View *view = m_pendingView; m_pendingView = nullptr; if (!view->isVisible()) { return; } // Verify mouse is still within the view's geometry QPoint globalPos = QCursor::pos(); QRect viewRect = view->rect(); QPoint viewTopLeft = view->mapToGlobal(viewRect.topLeft()); QRect globalRect(viewTopLeft, viewRect.size()); if (!globalRect.contains(globalPos)) { return; } // Perform the focus switch — just move keyboard focus to the view // in its current split pane. Do NOT use activateView(document) as that // can open the document in a different view space. view->setFocus(); m_currentView = view; m_lastFocusChange.restart(); if (m_plugin->raiseOnFocus()) { view->raise(); } } KTextEditor::View *SloppyFocusView::viewForWidget(QWidget *widget) const { // Walk up the widget hierarchy looking for a KTextEditor::View QWidget *w = widget; while (w) { if (auto *view = qobject_cast(w)) { return view; } w = w->parentWidget(); } return nullptr; } 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()); } #include "sloppyfocus.moc"