Option to use IME for input detection

This commit is contained in:
Chad Kouse 2025-01-03 17:12:53 -05:00
parent 3934d727e4
commit 600a5c7cfc
No known key found for this signature in database
GPG Key ID: 2BBC602A2578C7A2
4 changed files with 162 additions and 4 deletions

View File

@ -281,6 +281,7 @@
|<<input.mouse.rocker_gestures,input.mouse.rocker_gestures>>|Enable Opera-like mouse rocker gestures.
|<<input.partial_timeout,input.partial_timeout>>|Timeout (in milliseconds) for partially typed key bindings.
|<<input.spatial_navigation,input.spatial_navigation>>|Enable spatial navigation.
|<<input.use_ime,input.use_ime>>|Use Input Method Editor for input detection.
|<<keyhint.blacklist,keyhint.blacklist>>|Keychains that shouldn't be shown in the keyhint dialog.
|<<keyhint.delay,keyhint.delay>>|Time (in milliseconds) from pressing a key to seeing the keyhint dialog.
|<<keyhint.radius,keyhint.radius>>|Rounding radius (in pixels) for the edges of the keyhint dialog.
@ -3695,6 +3696,15 @@ Type: <<types,Bool>>
Default: +pass:[false]+
[[input.use_ime]]
=== input.use_ime
Use Input Method Editor for input detection.
Try setting this to `true` if you sometimes click into input elements in a web page and qutebrowser doesn't automatically switch to input mode.
Type: <<types,Bool>>
Default: +pass:[false]+
[[keyhint.blacklist]]
=== keyhint.blacklist
Keychains that shouldn't be shown in the keyhint dialog.

View File

@ -193,6 +193,9 @@ class TabEventFilter(QObject):
self._check_insertmode_on_release = True
return
if config.val.input.use_ime:
return
if elem.is_editable():
log.mouse.debug("Clicked editable element!")
if config.val.input.insert_mode.auto_enter:

View File

@ -1998,6 +1998,16 @@ input.mode_override:
supports_pattern: true
desc: Mode to change to when focusing on a tab/URL changes.
input.use_ime:
default: false
type: Bool
desc: >-
Use Input Method Editor for input detection.
Try setting this to `true` if
you sometimes click into input elements in a web page and qutebrowser
doesn't automatically switch to input mode.
## keyhint
keyhint.blacklist:

View File

@ -6,12 +6,14 @@
from typing import cast, Optional
from qutebrowser.qt.core import pyqtSlot, QObject, QEvent, qVersion
from qutebrowser.qt.gui import QKeyEvent, QWindow
from qutebrowser.qt.core import pyqtSlot, QObject, QEvent, qVersion, Qt, QRectF
from qutebrowser.qt.gui import QKeyEvent, QWindow, QInputMethodQueryEvent
from qutebrowser.qt.widgets import QApplication
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.misc import quitter, objects
from qutebrowser.utils import objreg, debug, log, qtutils
from qutebrowser.utils import objreg, debug, log, qtutils, message, usertypes
class EventFilter(QObject):
@ -33,6 +35,7 @@ class EventFilter(QObject):
QEvent.Type.ShortcutOverride: self._handle_key_event,
}
self._log_qt_events = "log-qt-events" in objects.debug_flags
self._ime_event_handler: Optional[IMEEventHandler] = None
def install(self) -> None:
objects.qapp.installEventFilter(self)
@ -41,6 +44,15 @@ class EventFilter(QObject):
def shutdown(self) -> None:
objects.qapp.removeEventFilter(self)
@pyqtSlot(str)
def on_config_changed(self, option: str) -> None:
"""Handle changes in the configuration."""
if option != 'input.use_ime':
return
log.config.debug("Option: {}".format(option))
self.update_ime_registration()
def _handle_key_event(self, event: QKeyEvent) -> bool:
"""Handle a key press/release event.
@ -82,7 +94,11 @@ class EventFilter(QObject):
source = type(obj).__name__
ev_type_str = debug.qenum_key(QEvent, ev_type)
log.misc.debug(f"{source} got event: {ev_type_str}")
ignored_events = ["Timer", "MetaCall", "Paint", "UpdateRequest",
"MouseMove", "CursorChange", "ToolTipChange", "PaletteChange", "FontChange",
"StyleChange", "HoverMove"]
if ev_type_str not in ignored_events:
log.misc.debug(f"{source} got event: {ev_type_str}")
if (
ev_type == QEvent.Type.DragEnter and
@ -117,9 +133,128 @@ class EventFilter(QObject):
self._activated = False
raise
def update_ime_registration(self) -> None:
"""Updates the IME registration based on current config value."""
# Use IME for input detection if input.use_ime is true
if config.val.input.use_ime:
if self._ime_event_handler is not None:
log.misc.debug("IME Event handler already registered")
return
log.misc.debug("Registering IME Event handler")
self._ime_event_handler = IMEEventHandler(parent=QApplication.instance())
return
if self._ime_event_handler is None:
return
log.misc.debug("Deregistering IME Event handler")
self._ime_event_handler.deregister()
self._ime_event_handler = None
class IMEEventHandler(QObject):
"""Handles the cursorRectangleChanged event from the inputMethod."""
def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
self._input_method = QApplication.inputMethod()
assert self._input_method is not None
self._input_method.cursorRectangleChanged.connect(
self.cursor_rectangle_changed
)
self._last_seen_rect: Optional[QRectF] = None
message.global_bridge.mode_left.connect(self._on_mode_left)
def deregister(self) -> None:
"""Deregister the event handler."""
try:
message.global_bridge.mode_left.disconnect(self._on_mode_left)
except Exception:
log.misc.debug("Tried to disconnect mode_left but was not connected")
try:
if self._input_method is not None:
self._input_method.cursorRectangleChanged.disconnect(
self.cursor_rectangle_changed
)
except Exception:
log.misc.debug("Tried to disconnect cursor_rectangle_changed was not connected")
self._last_seen_rect = None
@pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self, mode: usertypes.KeyMode) -> None:
"""Event handler for when a mode is left."""
# Clear last_seen_rect when leaving insert mode
log.modes.debug("Left mode {}".format(
mode))
if mode == usertypes.KeyMode.insert:
self._last_seen_rect = None
log.misc.debug("Cleared last seen rect")
@pyqtSlot()
def cursor_rectangle_changed(self) -> None:
"""Event handler for when the rectangle the cursor is in changes."""
# May want to maintain last seen rect per window or tab.
# Tab might work better with remembering focus for tabs
# However, remembering modes per tab doesn't really seem to be implemented.
# Could be confusing to the user to change modes for them and
# this isn't something stock vim does.
# some input examples here https://www.javatpoint.com/html-form-input-types
# <input type="date">: doesn't report as having an input method enabled,
# although the existing heuristics pick it up
# if insert_mode_auto_load is false but there is a blinking cursor on
# load clicking the scroll bar will enter insert mode
assert self._input_method is not None
new_rect = self._input_method.cursorRectangle()
if self._last_seen_rect and self._last_seen_rect.contains(new_rect):
log.misc.debug("Rect changed but not handling because last seen rect already contains")
return
self._last_seen_rect = new_rect
# implementation detail: qtwebengine doesn't set anchor for input
# fields in a web page, qt widgets do, I haven't found any cases where
# it doesn't work yet. Would like to compare with a "get focused thing
# and examine" check first and compare across versions.
anchor_rect = self._input_method.anchorRectangle()
if anchor_rect:
log.misc.debug("Rectangle changed but not handling because anchor rect is set")
return
focused_window = objreg.last_focused_window()
focus_object = QApplication.focusObject()
query = None
if not new_rect and focus_object:
# sometimes we get a rectangle changed event and the queried
# rectangle is empty but we are still in an editable element. For
# instance when pressing enter in a text box on confluence or jira
# (including comment on the Qt instance) and tabbing between cells
# on https://html-online.com/editor/
# Checking ImEnabled helps in these cases.
query = QInputMethodQueryEvent(Qt.InputMethodQuery.ImEnabled)
QApplication.sendEvent(focus_object, query)
if new_rect or (query and query.value(Qt.InputMethodQuery.ImEnabled)):
log.mouse.debug("Clicked editable element!")
if config.val.input.insert_mode.auto_enter:
modeman.enter(focused_window.win_id, usertypes.KeyMode.insert,
'click', only_if_normal=True)
else:
log.mouse.debug("Clicked non-editable element!")
if config.val.input.insert_mode.auto_leave:
modeman.leave(focused_window.win_id, usertypes.KeyMode.insert,
'click', maybe=True)
def init() -> None:
"""Initialize the global EventFilter instance."""
event_filter = EventFilter(parent=objects.qapp)
event_filter.install()
quitter.instance.shutting_down.connect(event_filter.shutdown)
config.instance.changed.connect(event_filter.on_config_changed)
event_filter.update_ime_registration()