Add Qt mouse press handling
ober
c16c41920e5952c922350a907574d4783eb0555c
--- a/Makefile +++ b/Makefile @@ -300,6 +300,8 @@ vendor-deps: @sh scripts/patch-jerboa-std-compat.sh "$(abspath $(JERBOA)/lib)" @sh scripts/patch-jerboa-shell-compat.sh "$(CURDIR)" @scripts/patch-jerboa-aws-compat.sh "$(CURDIR)" + @echo "=== Transpiling vendored jerboa-qt ===" + @$(MAKE) -C "$(CQT_ROOT)" JERBUILD="$(abspath $(VENDORED_JERBUILD))" transpile @sh scripts/patch-jerboa-qt-compat.sh "$(CURDIR)" @echo "=== Vendor complete ===" --- a/dependencies.lock +++ b/dependencies.lock @@ -4,7 +4,7 @@ JERBOA_REV := aecb3f873c58569f19ec62d8ac34901c49c496f9 JERBOA_SHELL_REV := a861227afb4a6e343a358b5bfec1700989ee479d JERBOA_PCRE2_REV := 00ed6cba49c8b6a477da575e8acd1eea993c911e JERBOA_SCINTILLA_REV := 96745c5ea69c95f1c4fa2ce80f43e9cdf59a55a6 -JERBOA_QT_REV := dc86e8354ed3406066e9ce7482c0d1d43e4c6272 +JERBOA_QT_REV := cc28fd393b82625fcbe5af26866ff2461c5e6ccd JERBOA_SSL_REV := f466a3149f6c9a0e6f8dd202e00bb746a9bacd2c JERBOA_HTTPS_REV := 966c2f83e746e0153c7a843b235d8a2e466e1560 JERBOA_SQLITE_REV := 51d9ca3f186094ab492ac98e4a07637947e51091 --- a/scripts/patch-jerboa-qt-compat.sh +++ b/scripts/patch-jerboa-qt-compat.sh @@ -15,8 +15,8 @@ # string-split/string-join from the prelude when vendored qt.sls carries local # helper definitions for those names. Idempotent. # -# Patches the transpiled .sls (the build input — jerboa-qt's lib/ is used as-is, -# not re-transpiled). Run from vendor-deps. +# Patches the transpiled .sls generated by vendor-deps from the locked +# jerboa-qt source revision. set -eu repo="${1:-.}" --- a/src/jerboa-emacs/qt/app.ss +++ b/src/jerboa-emacs/qt/app.ss @@ -839,6 +839,7 @@ (def *qt-edit-version-counter* 0) (def *qt-buffer-edit-versions* (make-hash-table)) (def *qt-text-change-listener-editors* (make-hash-table)) +(def *qt-mouse-listener-editors* (make-hash-table)) (def (qt-buffer-edit-version buf) "Return BUF's monotonic edit version for debounce checks." @@ -859,6 +860,29 @@ (qt-on-plain-text-edit-text-changed! editor (lambda () (qt-bump-edit-version! editor))))) +(def (qt-install-mouse-listener! app editor) + "Install one mouse-press callback on EDITOR for point/modeline refresh." + (when (and editor (not (hash-get *qt-mouse-listener-editors* editor))) + (hash-put! *qt-mouse-listener-editors* editor #t) + (qt-on-mouse-press! editor + (lambda () + (with-key-handler-errors app + (when (= (qt-last-mouse-button) QT_MOUSE_LEFT) + (let* ((fr (app-state-frame app)) + (widget (qt-last-mouse-widget)) + (x (qt-last-mouse-x)) + (y (qt-last-mouse-y)) + (pos (sci-send editor SCI_POSITIONFROMPOINT x y))) + (let loop ((wins (qt-frame-windows fr)) (idx 0)) + (cond + ((null? wins) (void)) + ((eq? editor (qt-edit-window-editor (car wins))) + (set! (qt-frame-current-idx fr) idx)) + (else (loop (cdr wins) (+ idx 1))))) + (when (and (eq? widget editor) (>= pos 0)) + (qt-plain-text-edit-set-cursor-position! editor pos)) + (qt-widget-set-focus! editor) + (qt-refresh-after-key! app (app-state-echo app) "mouse-press")))))))) (def (qt-refresh-after-key! app echo-label origin) (let* ((fr (app-state-frame app)) (trace? (eq? (buffer-lexer-lang (qt-current-buffer fr)) 'dired))) @@ -1613,12 +1637,14 @@ ;; Install on the initial editor (consuming — editor doesn't see keys) (qt-install-text-change-listener! (qt-current-editor fr)) + (qt-install-mouse-listener! app (qt-current-editor fr)) (qt-on-key-press-consuming! (qt-current-editor fr) key-handler) ;; Store installer so split-window can install on new editors (set! (app-state-key-handler app) (lambda (editor) (qt-install-text-change-listener! editor) + (qt-install-mouse-listener! app editor) (qt-on-key-press-consuming! editor key-handler))) ;; Tell automation which widget to target for key events. When a terminal --- a/src/jerboa-emacs/qt/sci-shim.ss +++ b/src/jerboa-emacs/qt/sci-shim.ss @@ -73,8 +73,10 @@ qt-layout-set-spacing! qt-layout-set-stretch-factor! ;; Label qt-label-create qt-label-set-alignment! qt-label-set-pixmap! qt-label-set-text! qt-label-text - ;; Key events + ;; Key and mouse events qt-last-key-code qt-last-key-modifiers qt-last-key-text qt-last-key-autorepeat? qt-last-key-widget + qt-on-mouse-press! qt-last-mouse-x qt-last-mouse-y qt-last-mouse-button + qt-last-mouse-modifiers qt-last-mouse-widget QT_MOUSE_LEFT ;; Line edit qt-line-edit-create qt-line-edit-set-completer! qt-line-edit-set-text! qt-line-edit-text ;; List widget @@ -197,6 +199,9 @@ QT_CURSOR_PREVIOUS_CHAR QT_CURSOR_PREVIOUS_WORD) :std/sugar) +(def (qt-app-process-events! app) + (qt-pump-events! app)) + ;; SCI_REPLACESEL constant ;; Qt modifier/key/cursor constants re-exported for modules that only import sci-shim. @@ -263,6 +268,8 @@ (def (qt-stacked-widget-set-current-widget! sw widget) (ffi-qt-stacked-widget-set-current-widget sw widget)) +(def QT_MOUSE_LEFT 1) + (def ffi-qt-last-key-widget (foreign-procedure "qt_last_key_widget" () void*)) --- a/support/vendor-overrides/qt_chez_shim.c +++ b/support/vendor-overrides/qt_chez_shim.c @@ -21,6 +21,9 @@ #include <stdatomic.h> #include <stdio.h> +/* Loader ABI canary: "JQT1". */ +unsigned int jerboa_qt_abi_version(void) { return 0x4a515431u; } + /* ---- Stored callback function pointers (set from Chez at init time) ---- */ static qt_callback_void jerboa_void_callback = NULL; static qt_callback_string jerboa_string_callback = NULL; @@ -45,6 +48,7 @@ static struct cb_entry cb_queue[CB_QUEUE_SIZE]; static _Atomic unsigned int cb_head = 0; /* written by producer (Qt thread) */ static _Atomic unsigned int cb_tail = 0; /* written by consumer (primordial) */ static _Atomic unsigned int cb_dropped = 0; +static struct cb_entry cb_dispatch; static void cb_log_drop_to_file(unsigned int n) { const char *home = getenv("HOME"); @@ -139,6 +143,76 @@ static void deferred_string_callback(long id, const char* s) { cb_push_string(id static void deferred_int_callback(long id, int v) { cb_push_int(id, v); } static void deferred_bool_callback(long id, int v) { cb_push_bool(id, v); } +/* ---- Polling API expected by current jerboa-qt ---- */ +int jerboa_qt_next_callback_event(void) { + unsigned int t = atomic_load_explicit(&cb_tail, memory_order_relaxed); + unsigned int h = atomic_load_explicit(&cb_head, memory_order_acquire); + int type = 0; + + memset(&cb_dispatch, 0, sizeof(cb_dispatch)); + if (t == h) { + return 0; + } + + cb_dispatch = cb_queue[t]; + t = (t + 1) & (CB_QUEUE_SIZE - 1); + atomic_store_explicit(&cb_tail, t, memory_order_release); + + switch (cb_dispatch.type) { + case CB_VOID: type = 1; break; + case CB_STRING: type = 2; break; + case CB_INT: type = 3; break; + case CB_BOOL: type = 4; break; + } + return type; +} + +long jerboa_qt_callback_event_id(void) { + return cb_dispatch.id; +} + +int jerboa_qt_callback_event_value(void) { + return cb_dispatch.int_value; +} + +const char *jerboa_qt_callback_event_string(void) { + return cb_dispatch.str_value; +} + +unsigned long jerboa_qt_callback_pending_count(void) { + unsigned int h = atomic_load_explicit(&cb_head, memory_order_acquire); + unsigned int t = atomic_load_explicit(&cb_tail, memory_order_relaxed); + return (unsigned long)((h - t) & (CB_QUEUE_SIZE - 1)); +} + +unsigned long jerboa_qt_callback_dropped_count(void) { + return (unsigned long)atomic_load_explicit(&cb_dropped, memory_order_relaxed); +} + +void jerboa_qt_clear_callback_events(void) { + atomic_store_explicit(&cb_head, 0, memory_order_release); + atomic_store_explicit(&cb_tail, 0, memory_order_release); + atomic_store_explicit(&cb_dropped, 0, memory_order_relaxed); + memset(&cb_dispatch, 0, sizeof(cb_dispatch)); +} + +void jerboa_qt_watch_destroyed(void* object, long callback_id) { + (void)object; + (void)callback_id; +} + +void jerboa_qt_unwatch_destroyed(void* object, long callback_id) { + (void)object; + (void)callback_id; +} + +void jerboa_qt_clear_destroyed_watches(void) { +} + +void jerboa_qt_notify_destroying_tree(void* object) { + (void)object; +} + /* ---- Drain function: called from primordial Chez thread ---- */ void jerboa_qt_drain_pending_callbacks(void) { unsigned int t = atomic_load_explicit(&cb_tail, memory_order_relaxed); @@ -177,6 +251,22 @@ void* jerboa_qt_application_create(void) { return qt_application_create(0, NULL); } +/* NULL-safe string getters for current jerboa-qt FFI names. */ +const char *jerboa_qt_label_text(void *label) { + const char *s = qt_label_text(label); + return s ? s : ""; +} + +const char *jerboa_qt_push_button_text(void *button) { + const char *s = qt_push_button_text(button); + return s ? s : ""; +} + +const char *jerboa_qt_line_edit_text(void *edit) { + const char *s = qt_line_edit_text(edit); + return s ? s : ""; +} + /* ---- Signal connection wrappers ---- */ /* Each wrapper inserts the deferred callback as the trampoline. * The Qt thread only writes to the ring buffer — never calls Scheme. */ @@ -302,6 +392,10 @@ void jerboa_qt_install_key_handler_consuming(void* w, long callback_id) { if (jerboa_void_callback) qt_widget_install_key_handler_consuming(w, deferred_void_callback, callback_id); } +void jerboa_qt_install_mouse_handler(void* w, long callback_id) { + if (jerboa_void_callback) + qt_widget_install_mouse_handler(w, deferred_void_callback, callback_id); +} /* Radio Button */ void jerboa_qt_radio_button_on_toggled(void* r, long callback_id) { --- a/support/vendor-overrides/qt_shim.cpp +++ b/support/vendor-overrides/qt_shim.cpp @@ -40,6 +40,7 @@ #include <QSplitter> #include <QEvent> #include <QKeyEvent> +#include <QMouseEvent> #include <QPixmap> #include <QIcon> #include <QRadioButton> @@ -582,6 +583,11 @@ static int s_last_key_modifiers = 0; static std::string s_last_key_text; static int s_last_key_autorepeat = 0; static QObject* s_last_key_widget = nullptr; // which widget fired the last key event +static int s_last_mouse_x = 0; +static int s_last_mouse_y = 0; +static int s_last_mouse_button = 0; +static int s_last_mouse_modifiers = 0; +static QObject* s_last_mouse_widget = nullptr; // Storage for QInputDialog ok/cancel flag static bool s_last_input_ok = false; @@ -643,6 +649,29 @@ private: long m_callback_id; }; +class MousePressFilter : public QObject { +public: + MousePressFilter(QObject* parent, qt_callback_void callback, long callback_id) + : QObject(parent), m_callback(callback), m_callback_id(callback_id) {} + + bool eventFilter(QObject* obj, QEvent* event) override { + if (event->type() == QEvent::MouseButtonPress) { + auto* me = static_cast<QMouseEvent*>(event); + s_last_mouse_x = me->position().toPoint().x(); + s_last_mouse_y = me->position().toPoint().y(); + s_last_mouse_button = static_cast<int>(me->button()); + s_last_mouse_modifiers = static_cast<int>(me->modifiers()); + s_last_mouse_widget = obj; + m_callback(m_callback_id); + } + return QObject::eventFilter(obj, event); + } + +private: + qt_callback_void m_callback; + long m_callback_id; +}; + // ============================================================ // Application lifecycle // ============================================================ @@ -2487,6 +2516,17 @@ extern "C" void qt_widget_install_key_handler_consuming(qt_widget_t w, ); } +extern "C" void qt_widget_install_mouse_handler(qt_widget_t w, + qt_callback_void callback, + long callback_id) { + QT_NULL_CHECK_VOID(w); + QT_VOID( + auto* widget = static_cast<QWidget*>(w); + auto* filter = new MousePressFilter(widget, callback, callback_id); + widget->installEventFilter(filter) + ); +} + extern "C" int qt_last_key_code(void) { QT_RETURN(int, s_last_key_code); } @@ -2509,6 +2549,26 @@ extern "C" void* qt_last_key_widget(void) { return s_last_key_widget; } +extern "C" int qt_last_mouse_x(void) { + QT_RETURN(int, s_last_mouse_x); +} + +extern "C" int qt_last_mouse_y(void) { + QT_RETURN(int, s_last_mouse_y); +} + +extern "C" int qt_last_mouse_button(void) { + QT_RETURN(int, s_last_mouse_button); +} + +extern "C" int qt_last_mouse_modifiers(void) { + QT_RETURN(int, s_last_mouse_modifiers); +} + +extern "C" void* qt_last_mouse_widget(void) { + return s_last_mouse_widget; +} + extern "C" void qt_send_key_event(qt_widget_t w, int type, int key, int modifiers, const char* text) { QT_NULL_CHECK_VOID(w); // Capture primitives by value; construct QKeyEvent inside the lambda --- a/support/vendor-overrides/qt_shim.h +++ b/support/vendor-overrides/qt_shim.h @@ -351,11 +351,19 @@ void qt_widget_install_key_handler(qt_widget_t w, void qt_widget_install_key_handler_consuming(qt_widget_t w, qt_callback_void callback, long callback_id); +void qt_widget_install_mouse_handler(qt_widget_t w, + qt_callback_void callback, + long callback_id); int qt_last_key_code(void); int qt_last_key_modifiers(void); const char* qt_last_key_text(void); int qt_last_key_autorepeat(void); void* qt_last_key_widget(void); +int qt_last_mouse_x(void); +int qt_last_mouse_y(void); +int qt_last_mouse_button(void); +int qt_last_mouse_modifiers(void); +void* qt_last_mouse_widget(void); void qt_send_key_event(qt_widget_t w, int type, int key, int modifiers, const char* text); /* ========== Phase 7: Images, Icons, Radio Buttons, GroupBox ========== */ --- a/tests/test-qt-part2.ss +++ b/tests/test-qt-part2.ss @@ -93,11 +93,13 @@ (display "\n=== Qt Part2 Groups 44-55 ===\n") -(test-case "group44 qt key fidelity" +(test-case "group44 qt key/mouse fidelity" (check (qt-key-event->string QT_KEY_RETURN 0 "") => "C-m") (check (qt-key-event->string QT_KEY_RETURN QT_MOD_CTRL "") => "C-<return>") (check (qt-key-event->string QT_KEY_SPACE QT_MOD_CTRL "") => "C-@") - (check (qt-key-event->string QT_KEY_BACKSPACE 0 "") => "DEL")) + (check (qt-key-event->string QT_KEY_BACKSPACE 0 "") => "DEL") + (check QT_MOUSE_LEFT => 1) + (check qt-on-mouse-press! ? procedure?)) (test-case "group45 command registry shard smoke" (check (find-command 'scroll-up) ? procedure?)