Extend sloppy focus to all panes (tool views, terminals, etc.)

Generalize focus tracking from KTextEditor::View-only to any
focusable widget pane. paneForWidget() walks up the hierarchy:
- KTextEditor::View takes priority (editor splits)
- Falls back to any widget with StrongFocus/WheelFocus policy
  (terminals, file browser, build output, plugin panels, etc.)

This means hovering over any Kate pane will give it keyboard
focus, not just editor splits.
This commit is contained in:
Levi Neely 2026-07-20 15:01:25 +02:00
parent a641cbfc00
commit be18599c9b
2 changed files with 46 additions and 35 deletions

View File

@ -116,7 +116,7 @@ SloppyFocusView::SloppyFocusView(SloppyFocusPlugin *plugin, KTextEditor::MainWin
, m_mainWindow(mainWindow)
{
m_focusTimer.setSingleShot(true);
connect(&m_focusTimer, &QTimer::timeout, this, &SloppyFocusView::activatePendingView);
connect(&m_focusTimer, &QTimer::timeout, this, &SloppyFocusView::activatePendingPane);
connect(m_plugin, &SloppyFocusPlugin::configChanged, this, &SloppyFocusView::onConfigChanged);
m_lastFocusChange.start();
@ -153,16 +153,16 @@ bool SloppyFocusView::eventFilter(QObject *obj, QEvent *event)
return false;
}
// Find the KTextEditor::View ancestor of the entered widget
KTextEditor::View *view = viewForWidget(widget);
if (!view) {
// Find the focusable pane (editor view, tool view, terminal, etc.)
QWidget *pane = paneForWidget(widget);
if (!pane) {
return false;
}
// Don't re-trigger for the view that already has focus
if (view == m_currentView) {
// Don't re-trigger for the pane that already has focus
if (pane == m_currentPane) {
m_focusTimer.stop();
m_pendingView = nullptr;
m_pendingPane = nullptr;
return false;
}
@ -178,59 +178,70 @@ bool SloppyFocusView::eventFilter(QObject *obj, QEvent *event)
}
// Schedule focus change after delay
m_pendingView = view;
m_pendingPane = pane;
m_focusTimer.start(m_plugin->focusDelay());
return false;
}
void SloppyFocusView::activatePendingView()
void SloppyFocusView::activatePendingPane()
{
if (!m_pendingView) {
if (!m_pendingPane) {
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;
QWidget *pane = m_pendingPane;
m_pendingPane = nullptr;
if (!view->isVisible()) {
if (!pane->isVisible()) {
return;
}
// Verify mouse is still within the view's geometry
// Verify mouse is still within the pane's geometry
QPoint globalPos = QCursor::pos();
QRect viewRect = view->rect();
QPoint viewTopLeft = view->mapToGlobal(viewRect.topLeft());
QRect globalRect(viewTopLeft, viewRect.size());
QPoint paneTopLeft = pane->mapToGlobal(QPoint(0, 0));
QRect globalRect(paneTopLeft, pane->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;
// Move keyboard focus to the pane
pane->setFocus();
m_currentPane = pane;
m_lastFocusChange.restart();
if (m_plugin->raiseOnFocus()) {
view->raise();
pane->raise();
}
}
KTextEditor::View *SloppyFocusView::viewForWidget(QWidget *widget) const
QWidget *SloppyFocusView::paneForWidget(QWidget *widget) const
{
// Walk up the widget hierarchy looking for a KTextEditor::View
// 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;
while (w) {
if (auto *view = qobject_cast<KTextEditor::View *>(w)) {
return view;
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 nullptr;
return focusCandidate;
}
bool SloppyFocusView::shouldIgnoreFocus() const

View File

@ -62,19 +62,19 @@ protected:
bool eventFilter(QObject *obj, QEvent *event) override;
private Q_SLOTS:
void activatePendingView();
void activatePendingPane();
void onConfigChanged();
private:
KTextEditor::View *viewForWidget(QWidget *widget) const;
QWidget *paneForWidget(QWidget *widget) const;
bool shouldIgnoreFocus() const;
SloppyFocusPlugin *m_plugin;
KTextEditor::MainWindow *m_mainWindow;
QTimer m_focusTimer;
QElapsedTimer m_lastFocusChange;
QPointer<KTextEditor::View> m_pendingView;
QPointer<KTextEditor::View> m_currentView;
QPointer<QWidget> m_pendingPane;
QPointer<QWidget> m_currentPane;
};
class SloppyFocusConfigPage : public KTextEditor::ConfigPage