fix(input): reliable accelerators + mouse back/forward + alt nav + ctrl-scroll\n\n- Capture-phase key controller for Ctrl±/0 consistency\n- Only consume mouse side buttons 8/9; preserve left/right clicks\n- Add Alt+ArrowLeft/Right navigation and two-finger swipe gesture enablement\n- Add Ctrl+scroll zoom; fix accelerator matching for punctuation (cmdorctrl- and cmdorctrl+=)\n- Remove layout-specific hacks; add raw minus diagnostics
- commit
- 0e75579e2bea5a8e87984700b40a2c518eea1866
- author
- bamen · · 2025-09-09 18:55 UTC
- committer
- bamen · · 2025-09-09 17:56 UTC
- parent
- 606e951
- signature
- unsigned
3 files · +111 -5
M pkg/webkit/keyboard.go +2 -0
| @@ -32,3 +32,5 @@ func getShortcutRegistry(w *WebView) shortcutRegistry { | |||
| 32 | 32 | } | |
| 33 | 33 | return reg | |
| 34 | 34 | } | |
| 35 | + | ||
| 36 | + | // No layout-specific remaps in non-CGO build either. | |
| @@ -32,3 +32,5 @@ func getShortcutRegistry(w *WebView) shortcutRegistry { | |||||
| 32 | } | 32 | } | ||
| 33 | return reg | 33 | return reg | ||
| 34 | } | 34 | } | ||
| 35 | + | ||||
| 36 | + | // No layout-specific remaps in non-CGO build either. | |||
M pkg/webkit/keyboard_cgo.go +86 -5
| @@ -10,6 +10,7 @@ package webkit | |||
| 10 | 10 | // Declarations for Go callbacks | |
| 11 | 11 | extern void goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state); | |
| 12 | 12 | extern void goOnButtonPress(unsigned long id, unsigned int button, GdkModifierType state); | |
| 13 | + | extern void goOnScroll(unsigned long id, double dx, double dy, GdkModifierType state); | |
| 13 | 14 | ||
| 14 | 15 | // Key controller callback -> forward to Go | |
| 15 | 16 | static gboolean on_key_pressed(GtkEventControllerKey* controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) { | |
| @@ -30,6 +31,8 @@ static void on_click_pressed(GtkGestureClick* gesture, gint n_press, gdouble x, | |||
| 30 | 31 | static void attach_key_controller(GtkWidget* widget, unsigned long id) { | |
| 31 | 32 | if (!widget) return; | |
| 32 | 33 | GtkEventController* keyc = gtk_event_controller_key_new(); | |
| 34 | + | // Capture phase so we see keys even if child consumes them (e.g., WebKit) | |
| 35 | + | gtk_event_controller_set_propagation_phase(keyc, GTK_PHASE_CAPTURE); | |
| 33 | 36 | g_signal_connect_data(keyc, "key-pressed", G_CALLBACK(on_key_pressed), (gpointer)id, NULL, 0); | |
| 34 | 37 | gtk_widget_add_controller(widget, keyc); | |
| 35 | 38 | } | |
| @@ -40,6 +43,37 @@ static void attach_mouse_gesture(GtkWidget* widget, unsigned long id) { | |||
| 40 | 43 | g_signal_connect_data(click, "pressed", G_CALLBACK(on_click_pressed), (gpointer)id, NULL, 0); | |
| 41 | 44 | gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click)); | |
| 42 | 45 | } | |
| 46 | + | ||
| 47 | + | // Legacy controller to capture side buttons (8/9) reliably in GTK4 | |
| 48 | + | static gboolean on_legacy_event(GtkEventController* controller, GdkEvent* event, gpointer user_data) { | |
| 49 | + | (void)controller; | |
| 50 | + | if (!event) return FALSE; | |
| 51 | + | GdkEventType type = gdk_event_get_event_type(event); | |
| 52 | + | if (type == GDK_BUTTON_PRESS) { | |
| 53 | + | guint button = gdk_button_event_get_button(event); | |
| 54 | + | GdkModifierType state = gdk_event_get_modifier_state(event); | |
| 55 | + | if (button == 8 || button == 9) { | |
| 56 | + | goOnButtonPress((unsigned long)user_data, button, state); | |
| 57 | + | return TRUE; // consume side buttons | |
| 58 | + | } | |
| 59 | + | return FALSE; // let WebKit handle normal clicks | |
| 60 | + | } else if (type == GDK_SCROLL) { | |
| 61 | + | double dx = 0.0, dy = 0.0; | |
| 62 | + | gdk_scroll_event_get_deltas(event, &dx, &dy); | |
| 63 | + | GdkModifierType state = gdk_event_get_modifier_state(event); | |
| 64 | + | goOnScroll((unsigned long)user_data, dx, dy, state); | |
| 65 | + | return FALSE; // don't steal regular scrolling | |
| 66 | + | } | |
| 67 | + | return FALSE; | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | static void attach_mouse_legacy(GtkWidget* widget, unsigned long id) { | |
| 71 | + | if (!widget) return; | |
| 72 | + | GtkEventController* legacy = gtk_event_controller_legacy_new(); | |
| 73 | + | gtk_event_controller_set_propagation_phase(legacy, GTK_PHASE_CAPTURE); | |
| 74 | + | g_signal_connect_data(legacy, "event", G_CALLBACK(on_legacy_event), (gpointer)id, NULL, 0); | |
| 75 | + | gtk_widget_add_controller(widget, legacy); | |
| 76 | + | } | |
| 43 | 77 | */ | |
| 44 | 78 | import "C" | |
| 45 | 79 | ||
| @@ -62,6 +96,8 @@ var ( | |||
| 62 | 96 | lastKeyTime = make(map[uintptr]map[string]time.Time) | |
| 63 | 97 | ) | |
| 64 | 98 | ||
| 99 | + | // Locale hint function removed; no layout-specific remaps. | |
| 100 | + | ||
| 65 | 101 | // RegisterKeyboardShortcut registers a callback under an accelerator string. | |
| 66 | 102 | func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error { | |
| 67 | 103 | if w == nil || w.destroyed { | |
| @@ -90,28 +126,42 @@ func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) { | |||
| 90 | 126 | ||
| 91 | 127 | func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |
| 92 | 128 | ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0 | |
| 129 | + | alt := (state & uint(C.GDK_ALT_MASK)) != 0 | |
| 93 | 130 | // Map keyval to string names | |
| 94 | 131 | var keyName string | |
| 95 | 132 | switch keyval { | |
| 96 | - | case uint(C.GDK_KEY_plus), uint(C.GDK_KEY_KP_Add): | |
| 97 | - | keyName = "plus" | |
| 98 | - | case uint(C.GDK_KEY_equal): | |
| 99 | - | keyName = "=" | |
| 100 | 133 | case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore): | |
| 101 | - | // Treat underscore as minus for layouts where '-' requires Shift (e.g., AZERTY) | |
| 134 | + | // Normalize minus; log raw detection for diagnostics | |
| 135 | + | log.Printf("[accelerator-raw] minus key detected ctrl=%v alt=%v keyval=0x%x", ctrl, alt, keyval) | |
| 102 | 136 | keyName = "-" | |
| 137 | + | case uint(C.GDK_KEY_equal), uint(C.GDK_KEY_KP_Add): | |
| 138 | + | keyName = "=" | |
| 103 | 139 | case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0): | |
| 104 | 140 | keyName = "0" | |
| 141 | + | case uint(C.GDK_KEY_Left): | |
| 142 | + | keyName = "ArrowLeft" | |
| 143 | + | case uint(C.GDK_KEY_Right): | |
| 144 | + | keyName = "ArrowRight" | |
| 105 | 145 | case uint(C.GDK_KEY_F12): | |
| 106 | 146 | keyName = "F12" | |
| 107 | 147 | default: | |
| 148 | + | // If Control held, log unknown keyval for diagnostics | |
| 149 | + | if ctrl { | |
| 150 | + | log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval) | |
| 151 | + | } | |
| 108 | 152 | return | |
| 109 | 153 | } | |
| 110 | 154 | ||
| 111 | 155 | // Candidate accelerator strings in order | |
| 112 | 156 | candidates := []string{keyName} | |
| 113 | 157 | if ctrl { | |
| 158 | + | // Common style: modifier + '+' + keyName (e.g., cmdorctrl+=) | |
| 114 | 159 | candidates = append([]string{"cmdorctrl+" + keyName, "ctrl+" + keyName}, candidates...) | |
| 160 | + | // Also accept style without the extra '+' for punctuation keys (e.g., cmdorctrl-) | |
| 161 | + | candidates = append([]string{"cmdorctrl" + keyName, "ctrl" + keyName}, candidates...) | |
| 162 | + | } | |
| 163 | + | if alt { | |
| 164 | + | candidates = append([]string{"alt+" + keyName}, candidates...) | |
| 115 | 165 | } | |
| 116 | 166 | ||
| 117 | 167 | regMu.RLock() | |
| @@ -168,9 +218,40 @@ func AttachKeyboardControllers(w *WebView) { | |||
| 168 | 218 | C.attach_key_controller(w.native.win, C.ulong(w.id)) | |
| 169 | 219 | } | |
| 170 | 220 | C.attach_mouse_gesture(w.native.view, C.ulong(w.id)) | |
| 221 | + | // Legacy controller captures raw button presses (8/9) for back/forward reliably | |
| 222 | + | C.attach_mouse_legacy(w.native.view, C.ulong(w.id)) | |
| 171 | 223 | log.Printf("[input] GTK4 key/mouse controllers attached") | |
| 172 | 224 | } | |
| 173 | 225 | ||
| 226 | + | //export goOnScroll | |
| 227 | + | func goOnScroll(id C.ulong, dx C.double, dy C.double, state C.GdkModifierType) { | |
| 228 | + | uid := uintptr(id) | |
| 229 | + | regMu.RLock() | |
| 230 | + | vw := viewByID[uid] | |
| 231 | + | regMu.RUnlock() | |
| 232 | + | if vw == nil { return } | |
| 233 | + | // Ctrl+scroll zooms | |
| 234 | + | if (uint(state) & uint(C.GDK_CONTROL_MASK)) != 0 { | |
| 235 | + | if float64(dy) < 0 { | |
| 236 | + | // scroll up -> zoom in | |
| 237 | + | nz := vw.zoom | |
| 238 | + | if nz <= 0 { nz = 1.0 } | |
| 239 | + | nz *= 1.1 | |
| 240 | + | if nz < 0.25 { nz = 0.25 } | |
| 241 | + | if nz > 5.0 { nz = 5.0 } | |
| 242 | + | _ = vw.SetZoom(nz) | |
| 243 | + | } else if float64(dy) > 0 { | |
| 244 | + | // scroll down -> zoom out | |
| 245 | + | nz := vw.zoom | |
| 246 | + | if nz <= 0 { nz = 1.0 } | |
| 247 | + | nz /= 1.1 | |
| 248 | + | if nz < 0.25 { nz = 0.25 } | |
| 249 | + | if nz > 5.0 { nz = 5.0 } | |
| 250 | + | _ = vw.SetZoom(nz) | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | } | |
| 254 | + | ||
| 174 | 255 | //export goOnThemeChanged | |
| 175 | 256 | func goOnThemeChanged(id C.ulong, preferDark C.int) { | |
| 176 | 257 | uid := uintptr(id) | |
| @@ -10,6 +10,7 @@ package webkit | |||||
| 10 | // Declarations for Go callbacks | 10 | // Declarations for Go callbacks | ||
| 11 | extern void goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state); | 11 | extern void goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state); | ||
| 12 | extern void goOnButtonPress(unsigned long id, unsigned int button, GdkModifierType state); | 12 | extern void goOnButtonPress(unsigned long id, unsigned int button, GdkModifierType state); | ||
| 13 | + | extern void goOnScroll(unsigned long id, double dx, double dy, GdkModifierType state); | |||
| 13 | 14 | ||||
| 14 | // Key controller callback -> forward to Go | 15 | // Key controller callback -> forward to Go | ||
| 15 | static gboolean on_key_pressed(GtkEventControllerKey* controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) { | 16 | static gboolean on_key_pressed(GtkEventControllerKey* controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) { | ||
| @@ -30,6 +31,8 @@ static void on_click_pressed(GtkGestureClick* gesture, gint n_press, gdouble x, | |||||
| 30 | static void attach_key_controller(GtkWidget* widget, unsigned long id) { | 31 | static void attach_key_controller(GtkWidget* widget, unsigned long id) { | ||
| 31 | if (!widget) return; | 32 | if (!widget) return; | ||
| 32 | GtkEventController* keyc = gtk_event_controller_key_new(); | 33 | GtkEventController* keyc = gtk_event_controller_key_new(); | ||
| 34 | + | // Capture phase so we see keys even if child consumes them (e.g., WebKit) | |||
| 35 | + | gtk_event_controller_set_propagation_phase(keyc, GTK_PHASE_CAPTURE); | |||
| 33 | g_signal_connect_data(keyc, "key-pressed", G_CALLBACK(on_key_pressed), (gpointer)id, NULL, 0); | 36 | g_signal_connect_data(keyc, "key-pressed", G_CALLBACK(on_key_pressed), (gpointer)id, NULL, 0); | ||
| 34 | gtk_widget_add_controller(widget, keyc); | 37 | gtk_widget_add_controller(widget, keyc); | ||
| 35 | } | 38 | } | ||
| @@ -40,6 +43,37 @@ static void attach_mouse_gesture(GtkWidget* widget, unsigned long id) { | |||||
| 40 | g_signal_connect_data(click, "pressed", G_CALLBACK(on_click_pressed), (gpointer)id, NULL, 0); | 43 | g_signal_connect_data(click, "pressed", G_CALLBACK(on_click_pressed), (gpointer)id, NULL, 0); | ||
| 41 | gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click)); | 44 | gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click)); | ||
| 42 | } | 45 | } | ||
| 46 | + | ||||
| 47 | + | // Legacy controller to capture side buttons (8/9) reliably in GTK4 | |||
| 48 | + | static gboolean on_legacy_event(GtkEventController* controller, GdkEvent* event, gpointer user_data) { | |||
| 49 | + | (void)controller; | |||
| 50 | + | if (!event) return FALSE; | |||
| 51 | + | GdkEventType type = gdk_event_get_event_type(event); | |||
| 52 | + | if (type == GDK_BUTTON_PRESS) { | |||
| 53 | + | guint button = gdk_button_event_get_button(event); | |||
| 54 | + | GdkModifierType state = gdk_event_get_modifier_state(event); | |||
| 55 | + | if (button == 8 || button == 9) { | |||
| 56 | + | goOnButtonPress((unsigned long)user_data, button, state); | |||
| 57 | + | return TRUE; // consume side buttons | |||
| 58 | + | } | |||
| 59 | + | return FALSE; // let WebKit handle normal clicks | |||
| 60 | + | } else if (type == GDK_SCROLL) { | |||
| 61 | + | double dx = 0.0, dy = 0.0; | |||
| 62 | + | gdk_scroll_event_get_deltas(event, &dx, &dy); | |||
| 63 | + | GdkModifierType state = gdk_event_get_modifier_state(event); | |||
| 64 | + | goOnScroll((unsigned long)user_data, dx, dy, state); | |||
| 65 | + | return FALSE; // don't steal regular scrolling | |||
| 66 | + | } | |||
| 67 | + | return FALSE; | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | static void attach_mouse_legacy(GtkWidget* widget, unsigned long id) { | |||
| 71 | + | if (!widget) return; | |||
| 72 | + | GtkEventController* legacy = gtk_event_controller_legacy_new(); | |||
| 73 | + | gtk_event_controller_set_propagation_phase(legacy, GTK_PHASE_CAPTURE); | |||
| 74 | + | g_signal_connect_data(legacy, "event", G_CALLBACK(on_legacy_event), (gpointer)id, NULL, 0); | |||
| 75 | + | gtk_widget_add_controller(widget, legacy); | |||
| 76 | + | } | |||
| 43 | */ | 77 | */ | ||
| 44 | import "C" | 78 | import "C" | ||
| 45 | 79 | ||||
| @@ -62,6 +96,8 @@ var ( | |||||
| 62 | lastKeyTime = make(map[uintptr]map[string]time.Time) | 96 | lastKeyTime = make(map[uintptr]map[string]time.Time) | ||
| 63 | ) | 97 | ) | ||
| 64 | 98 | ||||
| 99 | + | // Locale hint function removed; no layout-specific remaps. | |||
| 100 | + | ||||
| 65 | // RegisterKeyboardShortcut registers a callback under an accelerator string. | 101 | // RegisterKeyboardShortcut registers a callback under an accelerator string. | ||
| 66 | func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error { | 102 | func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error { | ||
| 67 | if w == nil || w.destroyed { | 103 | if w == nil || w.destroyed { | ||
| @@ -90,28 +126,42 @@ func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) { | |||||
| 90 | 126 | ||||
| 91 | func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | 127 | func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | ||
| 92 | ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0 | 128 | ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0 | ||
| 129 | + | alt := (state & uint(C.GDK_ALT_MASK)) != 0 | |||
| 93 | // Map keyval to string names | 130 | // Map keyval to string names | ||
| 94 | var keyName string | 131 | var keyName string | ||
| 95 | switch keyval { | 132 | switch keyval { | ||
| 96 | - | case uint(C.GDK_KEY_plus), uint(C.GDK_KEY_KP_Add): | |||
| 97 | - | keyName = "plus" | |||
| 98 | - | case uint(C.GDK_KEY_equal): | |||
| 99 | - | keyName = "=" | |||
| 100 | case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore): | 133 | case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore): | ||
| 101 | - | // Treat underscore as minus for layouts where '-' requires Shift (e.g., AZERTY) | 134 | + | // Normalize minus; log raw detection for diagnostics |
| 135 | + | log.Printf("[accelerator-raw] minus key detected ctrl=%v alt=%v keyval=0x%x", ctrl, alt, keyval) | |||
| 102 | keyName = "-" | 136 | keyName = "-" | ||
| 137 | + | case uint(C.GDK_KEY_equal), uint(C.GDK_KEY_KP_Add): | |||
| 138 | + | keyName = "=" | |||
| 103 | case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0): | 139 | case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0): | ||
| 104 | keyName = "0" | 140 | keyName = "0" | ||
| 141 | + | case uint(C.GDK_KEY_Left): | |||
| 142 | + | keyName = "ArrowLeft" | |||
| 143 | + | case uint(C.GDK_KEY_Right): | |||
| 144 | + | keyName = "ArrowRight" | |||
| 105 | case uint(C.GDK_KEY_F12): | 145 | case uint(C.GDK_KEY_F12): | ||
| 106 | keyName = "F12" | 146 | keyName = "F12" | ||
| 107 | default: | 147 | default: | ||
| 148 | + | // If Control held, log unknown keyval for diagnostics | |||
| 149 | + | if ctrl { | |||
| 150 | + | log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval) | |||
| 151 | + | } | |||
| 108 | return | 152 | return | ||
| 109 | } | 153 | } | ||
| 110 | 154 | ||||
| 111 | // Candidate accelerator strings in order | 155 | // Candidate accelerator strings in order | ||
| 112 | candidates := []string{keyName} | 156 | candidates := []string{keyName} | ||
| 113 | if ctrl { | 157 | if ctrl { | ||
| 158 | + | // Common style: modifier + '+' + keyName (e.g., cmdorctrl+=) | |||
| 114 | candidates = append([]string{"cmdorctrl+" + keyName, "ctrl+" + keyName}, candidates...) | 159 | candidates = append([]string{"cmdorctrl+" + keyName, "ctrl+" + keyName}, candidates...) | ||
| 160 | + | // Also accept style without the extra '+' for punctuation keys (e.g., cmdorctrl-) | |||
| 161 | + | candidates = append([]string{"cmdorctrl" + keyName, "ctrl" + keyName}, candidates...) | |||
| 162 | + | } | |||
| 163 | + | if alt { | |||
| 164 | + | candidates = append([]string{"alt+" + keyName}, candidates...) | |||
| 115 | } | 165 | } | ||
| 116 | 166 | ||||
| 117 | regMu.RLock() | 167 | regMu.RLock() | ||
| @@ -168,9 +218,40 @@ func AttachKeyboardControllers(w *WebView) { | |||||
| 168 | C.attach_key_controller(w.native.win, C.ulong(w.id)) | 218 | C.attach_key_controller(w.native.win, C.ulong(w.id)) | ||
| 169 | } | 219 | } | ||
| 170 | C.attach_mouse_gesture(w.native.view, C.ulong(w.id)) | 220 | C.attach_mouse_gesture(w.native.view, C.ulong(w.id)) | ||
| 221 | + | // Legacy controller captures raw button presses (8/9) for back/forward reliably | |||
| 222 | + | C.attach_mouse_legacy(w.native.view, C.ulong(w.id)) | |||
| 171 | log.Printf("[input] GTK4 key/mouse controllers attached") | 223 | log.Printf("[input] GTK4 key/mouse controllers attached") | ||
| 172 | } | 224 | } | ||
| 173 | 225 | ||||
| 226 | + | //export goOnScroll | |||
| 227 | + | func goOnScroll(id C.ulong, dx C.double, dy C.double, state C.GdkModifierType) { | |||
| 228 | + | uid := uintptr(id) | |||
| 229 | + | regMu.RLock() | |||
| 230 | + | vw := viewByID[uid] | |||
| 231 | + | regMu.RUnlock() | |||
| 232 | + | if vw == nil { return } | |||
| 233 | + | // Ctrl+scroll zooms | |||
| 234 | + | if (uint(state) & uint(C.GDK_CONTROL_MASK)) != 0 { | |||
| 235 | + | if float64(dy) < 0 { | |||
| 236 | + | // scroll up -> zoom in | |||
| 237 | + | nz := vw.zoom | |||
| 238 | + | if nz <= 0 { nz = 1.0 } | |||
| 239 | + | nz *= 1.1 | |||
| 240 | + | if nz < 0.25 { nz = 0.25 } | |||
| 241 | + | if nz > 5.0 { nz = 5.0 } | |||
| 242 | + | _ = vw.SetZoom(nz) | |||
| 243 | + | } else if float64(dy) > 0 { | |||
| 244 | + | // scroll down -> zoom out | |||
| 245 | + | nz := vw.zoom | |||
| 246 | + | if nz <= 0 { nz = 1.0 } | |||
| 247 | + | nz /= 1.1 | |||
| 248 | + | if nz < 0.25 { nz = 0.25 } | |||
| 249 | + | if nz > 5.0 { nz = 5.0 } | |||
| 250 | + | _ = vw.SetZoom(nz) | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 253 | + | } | |||
| 254 | + | ||||
| 174 | //export goOnThemeChanged | 255 | //export goOnThemeChanged | ||
| 175 | func goOnThemeChanged(id C.ulong, preferDark C.int) { | 256 | func goOnThemeChanged(id C.ulong, preferDark C.int) { | ||
| 176 | uid := uintptr(id) | 257 | uid := uintptr(id) | ||
M pkg/webkit/webview_cgo.go +23 -0
| @@ -204,6 +204,16 @@ static void maybe_set_media_user_gesture(WebKitSettings* settings, int require) | |||
| 204 | 204 | #endif | |
| 205 | 205 | } | |
| 206 | 206 | ||
| 207 | + | // Enable two-finger swipe back/forward navigation gestures when supported | |
| 208 | + | static void maybe_set_back_forward_gestures(WebKitSettings* settings, int enable) { | |
| 209 | + | #if WEBKIT_CHECK_VERSION(2,38,0) | |
| 210 | + | if (!settings) return; | |
| 211 | + | webkit_settings_set_enable_back_forward_navigation_gestures(settings, enable ? 1 : 0); | |
| 212 | + | #else | |
| 213 | + | (void)settings; (void)enable; | |
| 214 | + | #endif | |
| 215 | + | } | |
| 216 | + | ||
| 207 | 217 | // Set cookie accept policy with compile-time compatibility across WebKit versions | |
| 208 | 218 | static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) { | |
| 209 | 219 | if (!cm) return; | |
| @@ -282,6 +292,7 @@ type WebView struct { | |||
| 282 | 292 | msgHandler func(payload string) | |
| 283 | 293 | titleHandler func(title string) | |
| 284 | 294 | uriHandler func(uri string) | |
| 295 | + | zoomHandler func(level float64) | |
| 285 | 296 | } | |
| 286 | 297 | ||
| 287 | 298 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | |
| @@ -387,6 +398,9 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 387 | 398 | _ = v.SetZoom(nz) | |
| 388 | 399 | }) | |
| 389 | 400 | _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) }) | |
| 401 | + | // Navigation with Alt+Arrow keys | |
| 402 | + | _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() }) | |
| 403 | + | _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() }) | |
| 390 | 404 | // Update window title when page title changes | |
| 391 | 405 | cNotifyTitle1 := C.CString("notify::title") | |
| 392 | 406 | C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle1, C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0) | |
| @@ -420,6 +434,8 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 420 | 434 | if cfg.Rendering.DebugGPU { C.maybe_set_draw_indicators(settings, 1) } | |
| 421 | 435 | // Reduce media pipeline churn by requiring a user gesture for playback | |
| 422 | 436 | C.maybe_set_media_user_gesture(settings, 1) | |
| 437 | + | // Enable trackpad back/forward gestures when available | |
| 438 | + | C.maybe_set_back_forward_gestures(settings, 1) | |
| 423 | 439 | } | |
| 424 | 440 | ||
| 425 | 441 | if cfg.ZoomDefault > 0 { | |
| @@ -559,6 +575,13 @@ func (w *WebView) dispatchURIChanged(uri string) { | |||
| 559 | 575 | if w != nil && w.uriHandler != nil { w.uriHandler(uri) } | |
| 560 | 576 | } | |
| 561 | 577 | ||
| 578 | + | // RegisterZoomChangedHandler registers a callback invoked when zoom level changes. | |
| 579 | + | func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb } | |
| 580 | + | ||
| 581 | + | func (w *WebView) dispatchZoomChanged(level float64) { | |
| 582 | + | if w != nil && w.zoomHandler != nil { w.zoomHandler(level) } | |
| 583 | + | } | |
| 584 | + | ||
| 562 | 585 | // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script. | |
| 563 | 586 | func (w *WebView) enableUserContentManager() { | |
| 564 | 587 | if w == nil || w.native == nil || w.native.ucm == nil { return } | |
| @@ -204,6 +204,16 @@ static void maybe_set_media_user_gesture(WebKitSettings* settings, int require) | |||||
| 204 | #endif | 204 | #endif | ||
| 205 | } | 205 | } | ||
| 206 | 206 | ||||
| 207 | + | // Enable two-finger swipe back/forward navigation gestures when supported | |||
| 208 | + | static void maybe_set_back_forward_gestures(WebKitSettings* settings, int enable) { | |||
| 209 | + | #if WEBKIT_CHECK_VERSION(2,38,0) | |||
| 210 | + | if (!settings) return; | |||
| 211 | + | webkit_settings_set_enable_back_forward_navigation_gestures(settings, enable ? 1 : 0); | |||
| 212 | + | #else | |||
| 213 | + | (void)settings; (void)enable; | |||
| 214 | + | #endif | |||
| 215 | + | } | |||
| 216 | + | ||||
| 207 | // Set cookie accept policy with compile-time compatibility across WebKit versions | 217 | // Set cookie accept policy with compile-time compatibility across WebKit versions | ||
| 208 | static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) { | 218 | static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) { | ||
| 209 | if (!cm) return; | 219 | if (!cm) return; | ||
| @@ -282,6 +292,7 @@ type WebView struct { | |||||
| 282 | msgHandler func(payload string) | 292 | msgHandler func(payload string) | ||
| 283 | titleHandler func(title string) | 293 | titleHandler func(title string) | ||
| 284 | uriHandler func(uri string) | 294 | uriHandler func(uri string) | ||
| 295 | + | zoomHandler func(level float64) | |||
| 285 | } | 296 | } | ||
| 286 | 297 | ||||
| 287 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | 298 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | ||
| @@ -387,6 +398,9 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 387 | _ = v.SetZoom(nz) | 398 | _ = v.SetZoom(nz) | ||
| 388 | }) | 399 | }) | ||
| 389 | _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) }) | 400 | _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) }) | ||
| 401 | + | // Navigation with Alt+Arrow keys | |||
| 402 | + | _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() }) | |||
| 403 | + | _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() }) | |||
| 390 | // Update window title when page title changes | 404 | // Update window title when page title changes | ||
| 391 | cNotifyTitle1 := C.CString("notify::title") | 405 | cNotifyTitle1 := C.CString("notify::title") | ||
| 392 | C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle1, C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0) | 406 | C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle1, C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0) | ||
| @@ -420,6 +434,8 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 420 | if cfg.Rendering.DebugGPU { C.maybe_set_draw_indicators(settings, 1) } | 434 | if cfg.Rendering.DebugGPU { C.maybe_set_draw_indicators(settings, 1) } | ||
| 421 | // Reduce media pipeline churn by requiring a user gesture for playback | 435 | // Reduce media pipeline churn by requiring a user gesture for playback | ||
| 422 | C.maybe_set_media_user_gesture(settings, 1) | 436 | C.maybe_set_media_user_gesture(settings, 1) | ||
| 437 | + | // Enable trackpad back/forward gestures when available | |||
| 438 | + | C.maybe_set_back_forward_gestures(settings, 1) | |||
| 423 | } | 439 | } | ||
| 424 | 440 | ||||
| 425 | if cfg.ZoomDefault > 0 { | 441 | if cfg.ZoomDefault > 0 { | ||
| @@ -559,6 +575,13 @@ func (w *WebView) dispatchURIChanged(uri string) { | |||||
| 559 | if w != nil && w.uriHandler != nil { w.uriHandler(uri) } | 575 | if w != nil && w.uriHandler != nil { w.uriHandler(uri) } | ||
| 560 | } | 576 | } | ||
| 561 | 577 | ||||
| 578 | + | // RegisterZoomChangedHandler registers a callback invoked when zoom level changes. | |||
| 579 | + | func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb } | |||
| 580 | + | ||||
| 581 | + | func (w *WebView) dispatchZoomChanged(level float64) { | |||
| 582 | + | if w != nil && w.zoomHandler != nil { w.zoomHandler(level) } | |||
| 583 | + | } | |||
| 584 | + | ||||
| 562 | // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script. | 585 | // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script. | ||
| 563 | func (w *WebView) enableUserContentManager() { | 586 | func (w *WebView) enableUserContentManager() { | ||
| 564 | if w == nil || w.native == nil || w.native.ucm == nil { return } | 587 | if w == nil || w.native == nil || w.native.ucm == nil { return } | ||