refactor(browser): adapt webview and webkit architecture for multi-pane workspaces
- commit
- 62604950e4863839f9a2f06cfa96f09adce21dc1
- author
- bamen · · 2025-09-19 03:42 UTC
- parent
- f7997f7
- signature
- unsigned
9 files · +599 -146
M internal/app/browser/browser.go +42 -4
| @@ -17,8 +17,8 @@ import ( | |||
| 17 | 17 | "github.com/bnema/dumber/internal/config" | |
| 18 | 18 | "github.com/bnema/dumber/internal/db" | |
| 19 | 19 | "github.com/bnema/dumber/internal/filtering" | |
| 20 | - | "github.com/bnema/dumber/internal/migrations" | |
| 21 | 20 | "github.com/bnema/dumber/internal/logging" | |
| 21 | + | "github.com/bnema/dumber/internal/migrations" | |
| 22 | 22 | "github.com/bnema/dumber/pkg/webkit" | |
| 23 | 23 | "github.com/bnema/dumber/services" | |
| 24 | 24 | ) | |
| @@ -45,13 +45,19 @@ type BrowserApp struct { | |||
| 45 | 45 | navigationController *control.NavigationController | |
| 46 | 46 | clipboardController *control.ClipboardController | |
| 47 | 47 | ||
| 48 | + | // Pane workspace management | |
| 49 | + | panes []*BrowserPane | |
| 50 | + | activePane *BrowserPane | |
| 51 | + | workspace *WorkspaceManager | |
| 52 | + | ||
| 48 | 53 | // Content filtering | |
| 49 | 54 | filterManager *filtering.FilterManager | |
| 50 | 55 | ||
| 51 | 56 | // Handlers | |
| 52 | - | schemeHandler *api.SchemeHandler | |
| 53 | - | messageHandler *messaging.Handler | |
| 54 | - | shortcutHandler *ShortcutHandler | |
| 57 | + | schemeHandler *api.SchemeHandler | |
| 58 | + | messageHandler *messaging.Handler | |
| 59 | + | shortcutHandler *ShortcutHandler | |
| 60 | + | windowShortcutHandler *WindowShortcutHandler | |
| 55 | 61 | } | |
| 56 | 62 | ||
| 57 | 63 | // Run starts the browser application | |
| @@ -172,11 +178,43 @@ func (app *BrowserApp) Run() { | |||
| 172 | 178 | ||
| 173 | 179 | // cleanup handles cleanup on shutdown | |
| 174 | 180 | func (app *BrowserApp) cleanup() { | |
| 181 | + | log.Printf("Starting browser cleanup...") | |
| 182 | + | ||
| 183 | + | // Cleanup window shortcuts first | |
| 184 | + | if app.windowShortcutHandler != nil { | |
| 185 | + | log.Printf("Cleaning up window-level shortcuts") | |
| 186 | + | app.windowShortcutHandler.Cleanup() | |
| 187 | + | app.windowShortcutHandler = nil | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // Cleanup all panes | |
| 191 | + | if app.panes != nil { | |
| 192 | + | log.Printf("Cleaning up %d panes", len(app.panes)) | |
| 193 | + | for i, pane := range app.panes { | |
| 194 | + | if pane != nil { | |
| 195 | + | log.Printf("Cleaning up pane %d (%s)", i, pane.ID()) | |
| 196 | + | pane.Cleanup() | |
| 197 | + | } | |
| 198 | + | } | |
| 199 | + | app.panes = nil | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | // Cleanup workspace manager | |
| 203 | + | if app.workspace != nil { | |
| 204 | + | log.Printf("Cleaning up workspace manager") | |
| 205 | + | // WorkspaceManager doesn't have explicit cleanup yet, but we clear the reference | |
| 206 | + | app.workspace = nil | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | // Close database last | |
| 175 | 210 | if app.database != nil { | |
| 211 | + | log.Printf("Closing database") | |
| 176 | 212 | if closeErr := app.database.Close(); closeErr != nil { | |
| 177 | 213 | log.Printf("Warning: failed to close database: %v", closeErr) | |
| 178 | 214 | } | |
| 179 | 215 | } | |
| 216 | + | ||
| 217 | + | log.Printf("Browser cleanup completed") | |
| 180 | 218 | } | |
| 181 | 219 | ||
| 182 | 220 | // setupOutputCapture initializes stdout/stderr capture if configured | |
| @@ -17,8 +17,8 @@ import ( | |||||
| 17 | "github.com/bnema/dumber/internal/config" | 17 | "github.com/bnema/dumber/internal/config" | ||
| 18 | "github.com/bnema/dumber/internal/db" | 18 | "github.com/bnema/dumber/internal/db" | ||
| 19 | "github.com/bnema/dumber/internal/filtering" | 19 | "github.com/bnema/dumber/internal/filtering" | ||
| 20 | - | "github.com/bnema/dumber/internal/migrations" | |||
| 21 | "github.com/bnema/dumber/internal/logging" | 20 | "github.com/bnema/dumber/internal/logging" | ||
| 21 | + | "github.com/bnema/dumber/internal/migrations" | |||
| 22 | "github.com/bnema/dumber/pkg/webkit" | 22 | "github.com/bnema/dumber/pkg/webkit" | ||
| 23 | "github.com/bnema/dumber/services" | 23 | "github.com/bnema/dumber/services" | ||
| 24 | ) | 24 | ) | ||
| @@ -45,13 +45,19 @@ type BrowserApp struct { | |||||
| 45 | navigationController *control.NavigationController | 45 | navigationController *control.NavigationController | ||
| 46 | clipboardController *control.ClipboardController | 46 | clipboardController *control.ClipboardController | ||
| 47 | 47 | ||||
| 48 | + | // Pane workspace management | |||
| 49 | + | panes []*BrowserPane | |||
| 50 | + | activePane *BrowserPane | |||
| 51 | + | workspace *WorkspaceManager | |||
| 52 | + | ||||
| 48 | // Content filtering | 53 | // Content filtering | ||
| 49 | filterManager *filtering.FilterManager | 54 | filterManager *filtering.FilterManager | ||
| 50 | 55 | ||||
| 51 | // Handlers | 56 | // Handlers | ||
| 52 | - | schemeHandler *api.SchemeHandler | 57 | + | schemeHandler *api.SchemeHandler |
| 53 | - | messageHandler *messaging.Handler | 58 | + | messageHandler *messaging.Handler |
| 54 | - | shortcutHandler *ShortcutHandler | 59 | + | shortcutHandler *ShortcutHandler |
| 60 | + | windowShortcutHandler *WindowShortcutHandler | |||
| 55 | } | 61 | } | ||
| 56 | 62 | ||||
| 57 | // Run starts the browser application | 63 | // Run starts the browser application | ||
| @@ -172,11 +178,43 @@ func (app *BrowserApp) Run() { | |||||
| 172 | 178 | ||||
| 173 | // cleanup handles cleanup on shutdown | 179 | // cleanup handles cleanup on shutdown | ||
| 174 | func (app *BrowserApp) cleanup() { | 180 | func (app *BrowserApp) cleanup() { | ||
| 181 | + | log.Printf("Starting browser cleanup...") | |||
| 182 | + | ||||
| 183 | + | // Cleanup window shortcuts first | |||
| 184 | + | if app.windowShortcutHandler != nil { | |||
| 185 | + | log.Printf("Cleaning up window-level shortcuts") | |||
| 186 | + | app.windowShortcutHandler.Cleanup() | |||
| 187 | + | app.windowShortcutHandler = nil | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // Cleanup all panes | |||
| 191 | + | if app.panes != nil { | |||
| 192 | + | log.Printf("Cleaning up %d panes", len(app.panes)) | |||
| 193 | + | for i, pane := range app.panes { | |||
| 194 | + | if pane != nil { | |||
| 195 | + | log.Printf("Cleaning up pane %d (%s)", i, pane.ID()) | |||
| 196 | + | pane.Cleanup() | |||
| 197 | + | } | |||
| 198 | + | } | |||
| 199 | + | app.panes = nil | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | // Cleanup workspace manager | |||
| 203 | + | if app.workspace != nil { | |||
| 204 | + | log.Printf("Cleaning up workspace manager") | |||
| 205 | + | // WorkspaceManager doesn't have explicit cleanup yet, but we clear the reference | |||
| 206 | + | app.workspace = nil | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | // Close database last | |||
| 175 | if app.database != nil { | 210 | if app.database != nil { | ||
| 211 | + | log.Printf("Closing database") | |||
| 176 | if closeErr := app.database.Close(); closeErr != nil { | 212 | if closeErr := app.database.Close(); closeErr != nil { | ||
| 177 | log.Printf("Warning: failed to close database: %v", closeErr) | 213 | log.Printf("Warning: failed to close database: %v", closeErr) | ||
| 178 | } | 214 | } | ||
| 179 | } | 215 | } | ||
| 216 | + | ||||
| 217 | + | log.Printf("Browser cleanup completed") | |||
| 180 | } | 218 | } | ||
| 181 | 219 | ||||
| 182 | // setupOutputCapture initializes stdout/stderr capture if configured | 220 | // setupOutputCapture initializes stdout/stderr capture if configured | ||
M internal/app/browser/shortcuts.go +75 -24
| @@ -1,9 +1,12 @@ | |||
| 1 | 1 | package browser | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "encoding/json" | |
| 5 | + | "fmt" | |
| 4 | 6 | "log" | |
| 5 | 7 | ||
| 6 | 8 | "github.com/bnema/dumber/internal/app/control" | |
| 9 | + | "github.com/bnema/dumber/internal/config" | |
| 7 | 10 | "github.com/bnema/dumber/pkg/webkit" | |
| 8 | 11 | ) | |
| 9 | 12 | ||
| @@ -11,57 +14,105 @@ import ( | |||
| 11 | 14 | type ShortcutHandler struct { | |
| 12 | 15 | webView *webkit.WebView | |
| 13 | 16 | clipboardController *control.ClipboardController | |
| 17 | + | config *config.Config | |
| 18 | + | app *BrowserApp | |
| 14 | 19 | } | |
| 15 | 20 | ||
| 16 | 21 | // NewShortcutHandler creates a new shortcut handler | |
| 17 | - | func NewShortcutHandler(webView *webkit.WebView, clipboardController *control.ClipboardController) *ShortcutHandler { | |
| 22 | + | func NewShortcutHandler(webView *webkit.WebView, clipboardController *control.ClipboardController, cfg *config.Config, app *BrowserApp) *ShortcutHandler { | |
| 18 | 23 | return &ShortcutHandler{ | |
| 19 | 24 | webView: webView, | |
| 20 | 25 | clipboardController: clipboardController, | |
| 26 | + | config: cfg, | |
| 27 | + | app: app, | |
| 21 | 28 | } | |
| 22 | 29 | } | |
| 23 | 30 | ||
| 24 | - | // RegisterShortcuts registers all keyboard shortcuts | |
| 25 | - | func (s *ShortcutHandler) RegisterShortcuts() { | |
| 26 | - | // DevTools | |
| 27 | - | _ = s.webView.RegisterKeyboardShortcut("F12", func() { | |
| 28 | - | log.Printf("Shortcut: F12 (devtools)") | |
| 29 | - | _ = s.webView.ShowDevTools() | |
| 30 | - | }) | |
| 31 | - | ||
| 32 | - | // Omnibox (Ctrl+L): use new keyboard service bridge | |
| 33 | - | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+l", func() { | |
| 34 | - | log.Printf("Shortcut: Omnibox toggle") | |
| 35 | - | // Handled in GUI KeyboardService via unified 'dumber:key' event | |
| 36 | - | }) | |
| 31 | + | func (s *ShortcutHandler) isActivePane() bool { | |
| 32 | + | if s == nil || s.webView == nil { | |
| 33 | + | log.Printf("[shortcut] isActivePane: handler or webView is nil") | |
| 34 | + | return false | |
| 35 | + | } | |
| 36 | + | if s.app == nil || s.app.activePane == nil { | |
| 37 | + | log.Printf("[shortcut] isActivePane: app or activePane is nil, returning true") | |
| 38 | + | return true | |
| 39 | + | } | |
| 40 | + | isActive := s.app.activePane.webView == s.webView | |
| 41 | + | log.Printf("[shortcut] isActivePane: handler webView=%p activePane webView=%p isActive=%v", s.webView, s.app.activePane.webView, isActive) | |
| 42 | + | return isActive | |
| 43 | + | } | |
| 37 | 44 | ||
| 38 | - | // Find in page (Ctrl+F): use keyboard service bridge | |
| 39 | - | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+f", func() { | |
| 40 | - | log.Printf("Shortcut: Find in page") | |
| 41 | - | // Handled in GUI KeyboardService via unified 'dumber:key' event | |
| 42 | - | }) | |
| 45 | + | // RegisterShortcuts registers pane-specific keyboard shortcuts with focus guards | |
| 46 | + | func (s *ShortcutHandler) RegisterShortcuts() { | |
| 47 | + | s.exposeWorkspaceConfig() | |
| 43 | 48 | ||
| 44 | - | // Copy URL (Ctrl+Shift+C): use keyboard service with native fallback | |
| 45 | - | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+shift+c", func() { | |
| 46 | - | log.Printf("Shortcut: Copy URL") | |
| 47 | - | // Handled in GUI KeyboardService via unified 'dumber:key' event | |
| 48 | - | }) | |
| 49 | + | // NOTE: Global shortcuts (Ctrl+L, Ctrl+F, Ctrl+Shift+C, F12) are now handled | |
| 50 | + | // by WindowShortcutHandler at the window level to prevent duplicates | |
| 49 | 51 | ||
| 50 | 52 | // Page refresh shortcuts | |
| 51 | 53 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+r", func() { | |
| 54 | + | if !s.isActivePane() { | |
| 55 | + | return | |
| 56 | + | } | |
| 52 | 57 | log.Printf("Shortcut: Reload page") | |
| 53 | 58 | _ = s.webView.Reload() | |
| 54 | 59 | }) | |
| 55 | 60 | ||
| 56 | 61 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+shift+r", func() { | |
| 62 | + | if !s.isActivePane() { | |
| 63 | + | return | |
| 64 | + | } | |
| 57 | 65 | log.Printf("Shortcut: Hard reload page") | |
| 58 | 66 | _ = s.webView.ReloadBypassCache() | |
| 59 | 67 | }) | |
| 60 | 68 | ||
| 61 | 69 | _ = s.webView.RegisterKeyboardShortcut("F5", func() { | |
| 70 | + | if !s.isActivePane() { | |
| 71 | + | return | |
| 72 | + | } | |
| 62 | 73 | log.Printf("Shortcut: F5 reload") | |
| 63 | 74 | _ = s.webView.Reload() | |
| 64 | 75 | }) | |
| 65 | 76 | ||
| 77 | + | // Note: Workspace pane navigation shortcuts (Alt + Arrow keys) are now handled | |
| 78 | + | // by the WorkspaceManager to ensure they work properly across all panes | |
| 79 | + | ||
| 66 | 80 | // Zoom handled natively in webkit package (built-in shortcuts) | |
| 67 | 81 | } | |
| 82 | + | ||
| 83 | + | func (s *ShortcutHandler) dispatchUIShortcut(action string, extra map[string]any) error { | |
| 84 | + | if s == nil || s.webView == nil { | |
| 85 | + | return fmt.Errorf("webview unavailable for action %s", action) | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | detail := map[string]any{ | |
| 89 | + | "action": action, | |
| 90 | + | } | |
| 91 | + | for k, v := range extra { | |
| 92 | + | detail[k] = v | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | return s.webView.DispatchCustomEvent("dumber:ui:shortcut", detail) | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | func (s *ShortcutHandler) exposeWorkspaceConfig() { | |
| 99 | + | if s.webView == nil || s.config == nil { | |
| 100 | + | return | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | payload, err := json.Marshal(s.config.Workspace) | |
| 104 | + | if err != nil { | |
| 105 | + | log.Printf("Shortcut: failed to marshal workspace config: %v", err) | |
| 106 | + | return | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | script := fmt.Sprintf(`(function(){ | |
| 110 | + | const cfg = %s; | |
| 111 | + | window.__dumber_workspace_config = cfg; | |
| 112 | + | document.dispatchEvent(new CustomEvent('dumber:workspace-config',{detail:cfg})); | |
| 113 | + | })();`, string(payload)) | |
| 114 | + | ||
| 115 | + | if err := s.webView.InjectScript(script); err != nil { | |
| 116 | + | log.Printf("Shortcut: failed to expose workspace config: %v", err) | |
| 117 | + | } | |
| 118 | + | } | |
| @@ -1,9 +1,12 @@ | |||||
| 1 | package browser | 1 | package browser | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "encoding/json" | |||
| 5 | + | "fmt" | |||
| 4 | "log" | 6 | "log" | ||
| 5 | 7 | ||||
| 6 | "github.com/bnema/dumber/internal/app/control" | 8 | "github.com/bnema/dumber/internal/app/control" | ||
| 9 | + | "github.com/bnema/dumber/internal/config" | |||
| 7 | "github.com/bnema/dumber/pkg/webkit" | 10 | "github.com/bnema/dumber/pkg/webkit" | ||
| 8 | ) | 11 | ) | ||
| 9 | 12 | ||||
| @@ -11,57 +14,105 @@ import ( | |||||
| 11 | type ShortcutHandler struct { | 14 | type ShortcutHandler struct { | ||
| 12 | webView *webkit.WebView | 15 | webView *webkit.WebView | ||
| 13 | clipboardController *control.ClipboardController | 16 | clipboardController *control.ClipboardController | ||
| 17 | + | config *config.Config | |||
| 18 | + | app *BrowserApp | |||
| 14 | } | 19 | } | ||
| 15 | 20 | ||||
| 16 | // NewShortcutHandler creates a new shortcut handler | 21 | // NewShortcutHandler creates a new shortcut handler | ||
| 17 | - | func NewShortcutHandler(webView *webkit.WebView, clipboardController *control.ClipboardController) *ShortcutHandler { | 22 | + | func NewShortcutHandler(webView *webkit.WebView, clipboardController *control.ClipboardController, cfg *config.Config, app *BrowserApp) *ShortcutHandler { |
| 18 | return &ShortcutHandler{ | 23 | return &ShortcutHandler{ | ||
| 19 | webView: webView, | 24 | webView: webView, | ||
| 20 | clipboardController: clipboardController, | 25 | clipboardController: clipboardController, | ||
| 26 | + | config: cfg, | |||
| 27 | + | app: app, | |||
| 21 | } | 28 | } | ||
| 22 | } | 29 | } | ||
| 23 | 30 | ||||
| 24 | - | // RegisterShortcuts registers all keyboard shortcuts | 31 | + | func (s *ShortcutHandler) isActivePane() bool { |
| 25 | - | func (s *ShortcutHandler) RegisterShortcuts() { | 32 | + | if s == nil || s.webView == nil { |
| 26 | - | // DevTools | 33 | + | log.Printf("[shortcut] isActivePane: handler or webView is nil") |
| 27 | - | _ = s.webView.RegisterKeyboardShortcut("F12", func() { | 34 | + | return false |
| 28 | - | log.Printf("Shortcut: F12 (devtools)") | 35 | + | } |
| 29 | - | _ = s.webView.ShowDevTools() | 36 | + | if s.app == nil || s.app.activePane == nil { |
| 30 | - | }) | 37 | + | log.Printf("[shortcut] isActivePane: app or activePane is nil, returning true") |
| 31 | - | 38 | + | return true | |
| 32 | - | // Omnibox (Ctrl+L): use new keyboard service bridge | 39 | + | } |
| 33 | - | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+l", func() { | 40 | + | isActive := s.app.activePane.webView == s.webView |
| 34 | - | log.Printf("Shortcut: Omnibox toggle") | 41 | + | log.Printf("[shortcut] isActivePane: handler webView=%p activePane webView=%p isActive=%v", s.webView, s.app.activePane.webView, isActive) |
| 35 | - | // Handled in GUI KeyboardService via unified 'dumber:key' event | 42 | + | return isActive |
| 36 | - | }) | 43 | + | } |
| 37 | 44 | ||||
| 38 | - | // Find in page (Ctrl+F): use keyboard service bridge | 45 | + | // RegisterShortcuts registers pane-specific keyboard shortcuts with focus guards |
| 39 | - | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+f", func() { | 46 | + | func (s *ShortcutHandler) RegisterShortcuts() { |
| 40 | - | log.Printf("Shortcut: Find in page") | 47 | + | s.exposeWorkspaceConfig() |
| 41 | - | // Handled in GUI KeyboardService via unified 'dumber:key' event | |||
| 42 | - | }) | |||
| 43 | 48 | ||||
| 44 | - | // Copy URL (Ctrl+Shift+C): use keyboard service with native fallback | 49 | + | // NOTE: Global shortcuts (Ctrl+L, Ctrl+F, Ctrl+Shift+C, F12) are now handled |
| 45 | - | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+shift+c", func() { | 50 | + | // by WindowShortcutHandler at the window level to prevent duplicates |
| 46 | - | log.Printf("Shortcut: Copy URL") | |||
| 47 | - | // Handled in GUI KeyboardService via unified 'dumber:key' event | |||
| 48 | - | }) | |||
| 49 | 51 | ||||
| 50 | // Page refresh shortcuts | 52 | // Page refresh shortcuts | ||
| 51 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+r", func() { | 53 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+r", func() { | ||
| 54 | + | if !s.isActivePane() { | |||
| 55 | + | return | |||
| 56 | + | } | |||
| 52 | log.Printf("Shortcut: Reload page") | 57 | log.Printf("Shortcut: Reload page") | ||
| 53 | _ = s.webView.Reload() | 58 | _ = s.webView.Reload() | ||
| 54 | }) | 59 | }) | ||
| 55 | 60 | ||||
| 56 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+shift+r", func() { | 61 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+shift+r", func() { | ||
| 62 | + | if !s.isActivePane() { | |||
| 63 | + | return | |||
| 64 | + | } | |||
| 57 | log.Printf("Shortcut: Hard reload page") | 65 | log.Printf("Shortcut: Hard reload page") | ||
| 58 | _ = s.webView.ReloadBypassCache() | 66 | _ = s.webView.ReloadBypassCache() | ||
| 59 | }) | 67 | }) | ||
| 60 | 68 | ||||
| 61 | _ = s.webView.RegisterKeyboardShortcut("F5", func() { | 69 | _ = s.webView.RegisterKeyboardShortcut("F5", func() { | ||
| 70 | + | if !s.isActivePane() { | |||
| 71 | + | return | |||
| 72 | + | } | |||
| 62 | log.Printf("Shortcut: F5 reload") | 73 | log.Printf("Shortcut: F5 reload") | ||
| 63 | _ = s.webView.Reload() | 74 | _ = s.webView.Reload() | ||
| 64 | }) | 75 | }) | ||
| 65 | 76 | ||||
| 77 | + | // Note: Workspace pane navigation shortcuts (Alt + Arrow keys) are now handled | |||
| 78 | + | // by the WorkspaceManager to ensure they work properly across all panes | |||
| 79 | + | ||||
| 66 | // Zoom handled natively in webkit package (built-in shortcuts) | 80 | // Zoom handled natively in webkit package (built-in shortcuts) | ||
| 67 | } | 81 | } | ||
| 82 | + | ||||
| 83 | + | func (s *ShortcutHandler) dispatchUIShortcut(action string, extra map[string]any) error { | |||
| 84 | + | if s == nil || s.webView == nil { | |||
| 85 | + | return fmt.Errorf("webview unavailable for action %s", action) | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | detail := map[string]any{ | |||
| 89 | + | "action": action, | |||
| 90 | + | } | |||
| 91 | + | for k, v := range extra { | |||
| 92 | + | detail[k] = v | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | return s.webView.DispatchCustomEvent("dumber:ui:shortcut", detail) | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | func (s *ShortcutHandler) exposeWorkspaceConfig() { | |||
| 99 | + | if s.webView == nil || s.config == nil { | |||
| 100 | + | return | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | payload, err := json.Marshal(s.config.Workspace) | |||
| 104 | + | if err != nil { | |||
| 105 | + | log.Printf("Shortcut: failed to marshal workspace config: %v", err) | |||
| 106 | + | return | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | script := fmt.Sprintf(`(function(){ | |||
| 110 | + | const cfg = %s; | |||
| 111 | + | window.__dumber_workspace_config = cfg; | |||
| 112 | + | document.dispatchEvent(new CustomEvent('dumber:workspace-config',{detail:cfg})); | |||
| 113 | + | })();`, string(payload)) | |||
| 114 | + | ||||
| 115 | + | if err := s.webView.InjectScript(script); err != nil { | |||
| 116 | + | log.Printf("Shortcut: failed to expose workspace config: %v", err) | |||
| 117 | + | } | |||
| 118 | + | } | |||
M internal/app/browser/webview.go +181 -60
| @@ -2,6 +2,7 @@ package browser | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "fmt" | |
| 5 | 6 | "log" | |
| 6 | 7 | "os" | |
| 7 | 8 | "path/filepath" | |
| @@ -11,6 +12,7 @@ import ( | |||
| 11 | 12 | "github.com/bnema/dumber/internal/app/constants" | |
| 12 | 13 | "github.com/bnema/dumber/internal/app/control" | |
| 13 | 14 | "github.com/bnema/dumber/internal/app/environment" | |
| 15 | + | "github.com/bnema/dumber/internal/app/messaging" | |
| 14 | 16 | "github.com/bnema/dumber/internal/config" | |
| 15 | 17 | "github.com/bnema/dumber/internal/filtering" | |
| 16 | 18 | "github.com/bnema/dumber/pkg/webkit" | |
| @@ -43,17 +45,26 @@ func buildMemoryConfig(cfg config.WebkitMemoryConfig) webkit.MemoryConfig { | |||
| 43 | 45 | return mc | |
| 44 | 46 | } | |
| 45 | 47 | ||
| 46 | - | // createWebView creates and configures the WebView | |
| 47 | - | func (app *BrowserApp) createWebView() error { | |
| 48 | - | log.Printf("Creating WebView (native backend expected: %v)", webkit.IsNativeAvailable()) | |
| 49 | - | dataDir, _ := config.GetDataDir() | |
| 50 | - | stateDir, _ := config.GetStateDir() | |
| 48 | + | func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) { | |
| 49 | + | dataDir, err := config.GetDataDir() | |
| 50 | + | if err != nil { | |
| 51 | + | return nil, err | |
| 52 | + | } | |
| 53 | + | stateDir, err := config.GetStateDir() | |
| 54 | + | if err != nil { | |
| 55 | + | return nil, err | |
| 56 | + | } | |
| 57 | + | ||
| 51 | 58 | webkitData := filepath.Join(dataDir, "webkit") | |
| 52 | 59 | webkitCache := filepath.Join(stateDir, "webkit-cache") | |
| 53 | - | _ = os.MkdirAll(webkitData, constants.DirPerm) | |
| 54 | - | _ = os.MkdirAll(webkitCache, constants.DirPerm) | |
| 60 | + | if err := os.MkdirAll(webkitData, constants.DirPerm); err != nil { | |
| 61 | + | return nil, err | |
| 62 | + | } | |
| 63 | + | if err := os.MkdirAll(webkitCache, constants.DirPerm); err != nil { | |
| 64 | + | return nil, err | |
| 65 | + | } | |
| 55 | 66 | ||
| 56 | - | view, err := webkit.NewWebView(&webkit.Config{ | |
| 67 | + | cfg := &webkit.Config{ | |
| 57 | 68 | Assets: app.assets, | |
| 58 | 69 | InitialURL: "dumb://homepage", | |
| 59 | 70 | ZoomDefault: 1.0, | |
| @@ -82,52 +93,77 @@ func (app *BrowserApp) createWebView() error { | |||
| 82 | 93 | CustomUserAgent: app.config.CodecPreferences.CustomUserAgent, | |
| 83 | 94 | DisableTwitchCodecControl: app.config.CodecPreferences.DisableTwitchCodecControl, | |
| 84 | 95 | }, | |
| 85 | - | }) | |
| 96 | + | } | |
| 86 | 97 | ||
| 87 | - | if err != nil { | |
| 88 | - | return err | |
| 98 | + | return cfg, nil | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | func (app *BrowserApp) buildPane(view *webkit.WebView) (*BrowserPane, error) { | |
| 102 | + | pane := &BrowserPane{ | |
| 103 | + | webView: view, | |
| 89 | 104 | } | |
| 90 | 105 | ||
| 91 | - | app.webView = view | |
| 92 | - | app.setupWebViewIntegration() | |
| 93 | - | app.setupWebViewHandlers() | |
| 94 | - | app.setupControllers() | |
| 95 | - | app.setupKeyboardShortcuts() | |
| 106 | + | pane.clipboardController = control.NewClipboardController(view) | |
| 107 | + | pane.zoomController = control.NewZoomController(app.browserService, view) | |
| 108 | + | pane.navigationController = control.NewNavigationController( | |
| 109 | + | app.parserService, | |
| 110 | + | app.browserService, | |
| 111 | + | view, | |
| 112 | + | pane.zoomController, | |
| 113 | + | ) | |
| 114 | + | pane.messageHandler = messaging.NewHandler(app.parserService, app.browserService) | |
| 115 | + | pane.messageHandler.SetWebView(view) | |
| 116 | + | pane.messageHandler.SetNavigationController(pane.navigationController) | |
| 117 | + | pane.shortcutHandler = NewShortcutHandler(view, pane.clipboardController, app.config, app) | |
| 96 | 118 | ||
| 97 | - | // Apply initial zoom and show window | |
| 98 | - | app.zoomController.ApplyInitialZoom() | |
| 119 | + | pane.zoomController.RegisterHandlers() | |
| 120 | + | pane.shortcutHandler.RegisterShortcuts() | |
| 99 | 121 | ||
| 100 | - | log.Printf("Showing WebView window…") | |
| 101 | - | if err := view.Show(); err != nil { | |
| 102 | - | log.Printf("Warning: failed to show WebView: %v", err) | |
| 103 | - | } else if !webkit.IsNativeAvailable() { | |
| 104 | - | log.Printf("Notice: running without webkit_cgo tag — no native window will be displayed.") | |
| 122 | + | return pane, nil | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | func (app *BrowserApp) createPaneForView(view *webkit.WebView) (*BrowserPane, error) { | |
| 126 | + | pane, err := app.buildPane(view) | |
| 127 | + | if err != nil { | |
| 128 | + | return nil, err | |
| 105 | 129 | } | |
| 106 | 130 | ||
| 107 | - | return nil | |
| 108 | - | } | |
| 131 | + | // Generate unique pane ID | |
| 132 | + | pane.SetID(fmt.Sprintf("pane-%d-%p", time.Now().Unix(), view)) | |
| 133 | + | pane.initializeGUITracking() | |
| 109 | 134 | ||
| 110 | - | // setupWebViewIntegration connects the WebView to browser services | |
| 111 | - | func (app *BrowserApp) setupWebViewIntegration() { | |
| 112 | - | app.browserService.AttachWebView(app.webView) | |
| 135 | + | // Inject minimal bootstrap script for pane initialization | |
| 136 | + | bootstrapScript := fmt.Sprintf(` | |
| 137 | + | window.__dumber_pane = { | |
| 138 | + | id: '%s', | |
| 139 | + | created: %d, | |
| 140 | + | active: false | |
| 141 | + | }; | |
| 113 | 142 | ||
| 114 | - | // GUI bundle is loaded via WebKit user scripts in enableUserContentManager | |
| 115 | - | // No need to load separately in browser service | |
| 143 | + | // GUI manager will be loaded on-demand | |
| 144 | + | window.__dumber_gui_ready = true; | |
| 116 | 145 | ||
| 117 | - | // Use native window as title updater | |
| 118 | - | if win := app.webView.Window(); win != nil { | |
| 119 | - | app.browserService.SetWindowTitleUpdater(win) | |
| 146 | + | console.log('[pane] Initialized pane %s'); | |
| 147 | + | `, pane.ID(), time.Now().UnixMilli(), pane.ID()) | |
| 148 | + | ||
| 149 | + | if err := view.InjectScript(bootstrapScript); err != nil { | |
| 150 | + | log.Printf("[pane-%s] Failed to inject bootstrap: %v", pane.ID(), err) | |
| 120 | 151 | } | |
| 152 | + | ||
| 153 | + | app.attachPaneHandlers(pane) | |
| 154 | + | return pane, nil | |
| 121 | 155 | } | |
| 122 | 156 | ||
| 123 | - | // setupWebViewHandlers configures WebView event handlers | |
| 124 | - | func (app *BrowserApp) setupWebViewHandlers() { | |
| 125 | - | // Persist page titles to DB when they change | |
| 126 | - | app.webView.RegisterTitleChangedHandler(func(title string) { | |
| 157 | + | func (app *BrowserApp) attachPaneHandlers(pane *BrowserPane) { | |
| 158 | + | if pane == nil || pane.webView == nil { | |
| 159 | + | return | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | pane.webView.RegisterTitleChangedHandler(func(title string) { | |
| 127 | 163 | if title == "" { | |
| 128 | 164 | return | |
| 129 | 165 | } | |
| 130 | - | url := app.webView.GetCurrentURL() | |
| 166 | + | url := pane.webView.GetCurrentURL() | |
| 131 | 167 | if url == "" { | |
| 132 | 168 | return | |
| 133 | 169 | } | |
| @@ -139,36 +175,121 @@ func (app *BrowserApp) setupWebViewHandlers() { | |||
| 139 | 175 | }(url, title) | |
| 140 | 176 | }) | |
| 141 | 177 | ||
| 142 | - | // Set WebView reference for message handler and register script messages | |
| 143 | - | app.messageHandler.SetWebView(app.webView) | |
| 144 | - | app.webView.RegisterScriptMessageHandler(app.messageHandler.Handle) | |
| 178 | + | pane.webView.RegisterScriptMessageHandler(func(payload string) { | |
| 179 | + | if app.workspace != nil { | |
| 180 | + | app.workspace.focusByView(pane.webView) | |
| 181 | + | } | |
| 182 | + | pane.messageHandler.Handle(payload) | |
| 183 | + | }) | |
| 184 | + | ||
| 185 | + | if pane.messageHandler != nil && pane.navigationController != nil { | |
| 186 | + | pane.messageHandler.SetNavigationController(pane.navigationController) | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | pane.webView.RegisterPopupHandler(func(uri string) bool { | |
| 190 | + | handled := false | |
| 191 | + | if app.workspace != nil { | |
| 192 | + | handled = app.workspace.HandlePopup(pane.webView, uri) | |
| 193 | + | } | |
| 194 | + | if handled { | |
| 195 | + | return true | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | if pane.navigationController != nil && uri != "" { | |
| 199 | + | if err := pane.navigationController.NavigateToURL(uri); err != nil { | |
| 200 | + | log.Printf("[workspace] popup fallback navigation failed: %v", err) | |
| 201 | + | if pane.webView != nil { | |
| 202 | + | if loadErr := pane.webView.LoadURL(uri); loadErr != nil { | |
| 203 | + | log.Printf("[workspace] popup fallback load failed: %v", loadErr) | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | return true | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | if pane.webView != nil && uri != "" { | |
| 211 | + | if err := pane.webView.LoadURL(uri); err != nil { | |
| 212 | + | log.Printf("[workspace] popup direct load failed: %v", err) | |
| 213 | + | } | |
| 214 | + | return true | |
| 215 | + | } | |
| 145 | 216 | ||
| 217 | + | return false | |
| 218 | + | }) | |
| 146 | 219 | } | |
| 147 | 220 | ||
| 148 | - | // setupControllers initializes and configures controller objects | |
| 149 | - | func (app *BrowserApp) setupControllers() { | |
| 150 | - | // Initialize controllers | |
| 151 | - | app.zoomController = control.NewZoomController(app.browserService, app.webView) | |
| 152 | - | app.navigationController = control.NewNavigationController( | |
| 153 | - | app.parserService, | |
| 154 | - | app.browserService, | |
| 155 | - | app.webView, | |
| 156 | - | app.zoomController, | |
| 157 | - | ) | |
| 158 | - | app.clipboardController = control.NewClipboardController(app.webView) | |
| 221 | + | // createWebView creates and configures the WebView | |
| 222 | + | func (app *BrowserApp) createWebView() error { | |
| 223 | + | log.Printf("Creating WebView (native backend expected: %v)", webkit.IsNativeAvailable()) | |
| 224 | + | ||
| 225 | + | cfg, err := app.buildWebkitConfig() | |
| 226 | + | if err != nil { | |
| 227 | + | return err | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | // Main window needs a top-level window | |
| 231 | + | cfg.CreateWindow = true | |
| 232 | + | ||
| 233 | + | view, err := webkit.NewWebView(cfg) | |
| 234 | + | ||
| 235 | + | if err != nil { | |
| 236 | + | return err | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | pane, err := app.createPaneForView(view) | |
| 240 | + | if err != nil { | |
| 241 | + | return err | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | app.webView = view | |
| 245 | + | app.zoomController = pane.zoomController | |
| 246 | + | app.navigationController = pane.navigationController | |
| 247 | + | app.clipboardController = pane.clipboardController | |
| 248 | + | app.messageHandler = pane.messageHandler | |
| 249 | + | app.shortcutHandler = pane.shortcutHandler | |
| 250 | + | app.panes = []*BrowserPane{pane} | |
| 251 | + | app.activePane = pane | |
| 252 | + | ||
| 253 | + | if app.workspace == nil { | |
| 254 | + | app.workspace = NewWorkspaceManager(app, pane) | |
| 255 | + | } | |
| 159 | 256 | ||
| 160 | - | if app.messageHandler != nil && app.navigationController != nil { | |
| 161 | - | app.messageHandler.SetNavigationController(app.navigationController) | |
| 257 | + | // Initialize window-level global shortcuts AFTER workspace is set up | |
| 258 | + | if window := view.Window(); window != nil { | |
| 259 | + | app.windowShortcutHandler = NewWindowShortcutHandler(window, app) | |
| 260 | + | if app.windowShortcutHandler != nil { | |
| 261 | + | log.Printf("Window-level global shortcuts initialized") | |
| 262 | + | } else { | |
| 263 | + | log.Printf("Warning: Failed to initialize window-level shortcuts") | |
| 264 | + | } | |
| 162 | 265 | } | |
| 163 | 266 | ||
| 164 | - | // Register controller handlers | |
| 165 | - | app.zoomController.RegisterHandlers() | |
| 267 | + | app.setupWebViewIntegration() | |
| 268 | + | ||
| 269 | + | // Apply initial zoom and show window | |
| 270 | + | app.zoomController.ApplyInitialZoom() | |
| 271 | + | ||
| 272 | + | log.Printf("Showing WebView window…") | |
| 273 | + | if err := view.Show(); err != nil { | |
| 274 | + | log.Printf("Warning: failed to show WebView: %v", err) | |
| 275 | + | } else if !webkit.IsNativeAvailable() { | |
| 276 | + | log.Printf("Notice: running without webkit_cgo tag — no native window will be displayed.") | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | return nil | |
| 166 | 280 | } | |
| 167 | 281 | ||
| 168 | - | // setupKeyboardShortcuts registers keyboard shortcuts | |
| 169 | - | func (app *BrowserApp) setupKeyboardShortcuts() { | |
| 170 | - | app.shortcutHandler = NewShortcutHandler(app.webView, app.clipboardController) | |
| 171 | - | app.shortcutHandler.RegisterShortcuts() | |
| 282 | + | // setupWebViewIntegration connects the WebView to browser services | |
| 283 | + | func (app *BrowserApp) setupWebViewIntegration() { | |
| 284 | + | app.browserService.AttachWebView(app.webView) | |
| 285 | + | ||
| 286 | + | // GUI bundle is loaded via WebKit user scripts in enableUserContentManager | |
| 287 | + | // No need to load separately in browser service | |
| 288 | + | ||
| 289 | + | // Use native window as title updater | |
| 290 | + | if win := app.webView.Window(); win != nil { | |
| 291 | + | app.browserService.SetWindowTitleUpdater(win) | |
| 292 | + | } | |
| 172 | 293 | } | |
| 173 | 294 | ||
| 174 | 295 | // setupContentBlocking initializes the content blocking system with proper timing | |
| @@ -2,6 +2,7 @@ package browser | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "fmt" | |||
| 5 | "log" | 6 | "log" | ||
| 6 | "os" | 7 | "os" | ||
| 7 | "path/filepath" | 8 | "path/filepath" | ||
| @@ -11,6 +12,7 @@ import ( | |||||
| 11 | "github.com/bnema/dumber/internal/app/constants" | 12 | "github.com/bnema/dumber/internal/app/constants" | ||
| 12 | "github.com/bnema/dumber/internal/app/control" | 13 | "github.com/bnema/dumber/internal/app/control" | ||
| 13 | "github.com/bnema/dumber/internal/app/environment" | 14 | "github.com/bnema/dumber/internal/app/environment" | ||
| 15 | + | "github.com/bnema/dumber/internal/app/messaging" | |||
| 14 | "github.com/bnema/dumber/internal/config" | 16 | "github.com/bnema/dumber/internal/config" | ||
| 15 | "github.com/bnema/dumber/internal/filtering" | 17 | "github.com/bnema/dumber/internal/filtering" | ||
| 16 | "github.com/bnema/dumber/pkg/webkit" | 18 | "github.com/bnema/dumber/pkg/webkit" | ||
| @@ -43,17 +45,26 @@ func buildMemoryConfig(cfg config.WebkitMemoryConfig) webkit.MemoryConfig { | |||||
| 43 | return mc | 45 | return mc | ||
| 44 | } | 46 | } | ||
| 45 | 47 | ||||
| 46 | - | // createWebView creates and configures the WebView | 48 | + | func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) { |
| 47 | - | func (app *BrowserApp) createWebView() error { | 49 | + | dataDir, err := config.GetDataDir() |
| 48 | - | log.Printf("Creating WebView (native backend expected: %v)", webkit.IsNativeAvailable()) | 50 | + | if err != nil { |
| 49 | - | dataDir, _ := config.GetDataDir() | 51 | + | return nil, err |
| 50 | - | stateDir, _ := config.GetStateDir() | 52 | + | } |
| 53 | + | stateDir, err := config.GetStateDir() | |||
| 54 | + | if err != nil { | |||
| 55 | + | return nil, err | |||
| 56 | + | } | |||
| 57 | + | ||||
| 51 | webkitData := filepath.Join(dataDir, "webkit") | 58 | webkitData := filepath.Join(dataDir, "webkit") | ||
| 52 | webkitCache := filepath.Join(stateDir, "webkit-cache") | 59 | webkitCache := filepath.Join(stateDir, "webkit-cache") | ||
| 53 | - | _ = os.MkdirAll(webkitData, constants.DirPerm) | 60 | + | if err := os.MkdirAll(webkitData, constants.DirPerm); err != nil { |
| 54 | - | _ = os.MkdirAll(webkitCache, constants.DirPerm) | 61 | + | return nil, err |
| 62 | + | } | |||
| 63 | + | if err := os.MkdirAll(webkitCache, constants.DirPerm); err != nil { | |||
| 64 | + | return nil, err | |||
| 65 | + | } | |||
| 55 | 66 | ||||
| 56 | - | view, err := webkit.NewWebView(&webkit.Config{ | 67 | + | cfg := &webkit.Config{ |
| 57 | Assets: app.assets, | 68 | Assets: app.assets, | ||
| 58 | InitialURL: "dumb://homepage", | 69 | InitialURL: "dumb://homepage", | ||
| 59 | ZoomDefault: 1.0, | 70 | ZoomDefault: 1.0, | ||
| @@ -82,52 +93,77 @@ func (app *BrowserApp) createWebView() error { | |||||
| 82 | CustomUserAgent: app.config.CodecPreferences.CustomUserAgent, | 93 | CustomUserAgent: app.config.CodecPreferences.CustomUserAgent, | ||
| 83 | DisableTwitchCodecControl: app.config.CodecPreferences.DisableTwitchCodecControl, | 94 | DisableTwitchCodecControl: app.config.CodecPreferences.DisableTwitchCodecControl, | ||
| 84 | }, | 95 | }, | ||
| 85 | - | }) | 96 | + | } |
| 86 | 97 | ||||
| 87 | - | if err != nil { | 98 | + | return cfg, nil |
| 88 | - | return err | 99 | + | } |
| 100 | + | ||||
| 101 | + | func (app *BrowserApp) buildPane(view *webkit.WebView) (*BrowserPane, error) { | |||
| 102 | + | pane := &BrowserPane{ | |||
| 103 | + | webView: view, | |||
| 89 | } | 104 | } | ||
| 90 | 105 | ||||
| 91 | - | app.webView = view | 106 | + | pane.clipboardController = control.NewClipboardController(view) |
| 92 | - | app.setupWebViewIntegration() | 107 | + | pane.zoomController = control.NewZoomController(app.browserService, view) |
| 93 | - | app.setupWebViewHandlers() | 108 | + | pane.navigationController = control.NewNavigationController( |
| 94 | - | app.setupControllers() | 109 | + | app.parserService, |
| 95 | - | app.setupKeyboardShortcuts() | 110 | + | app.browserService, |
| 111 | + | view, | |||
| 112 | + | pane.zoomController, | |||
| 113 | + | ) | |||
| 114 | + | pane.messageHandler = messaging.NewHandler(app.parserService, app.browserService) | |||
| 115 | + | pane.messageHandler.SetWebView(view) | |||
| 116 | + | pane.messageHandler.SetNavigationController(pane.navigationController) | |||
| 117 | + | pane.shortcutHandler = NewShortcutHandler(view, pane.clipboardController, app.config, app) | |||
| 96 | 118 | ||||
| 97 | - | // Apply initial zoom and show window | 119 | + | pane.zoomController.RegisterHandlers() |
| 98 | - | app.zoomController.ApplyInitialZoom() | 120 | + | pane.shortcutHandler.RegisterShortcuts() |
| 99 | 121 | ||||
| 100 | - | log.Printf("Showing WebView window…") | 122 | + | return pane, nil |
| 101 | - | if err := view.Show(); err != nil { | 123 | + | } |
| 102 | - | log.Printf("Warning: failed to show WebView: %v", err) | 124 | + | |
| 103 | - | } else if !webkit.IsNativeAvailable() { | 125 | + | func (app *BrowserApp) createPaneForView(view *webkit.WebView) (*BrowserPane, error) { |
| 104 | - | log.Printf("Notice: running without webkit_cgo tag — no native window will be displayed.") | 126 | + | pane, err := app.buildPane(view) |
| 127 | + | if err != nil { | |||
| 128 | + | return nil, err | |||
| 105 | } | 129 | } | ||
| 106 | 130 | ||||
| 107 | - | return nil | 131 | + | // Generate unique pane ID |
| 108 | - | } | 132 | + | pane.SetID(fmt.Sprintf("pane-%d-%p", time.Now().Unix(), view)) |
| 133 | + | pane.initializeGUITracking() | |||
| 109 | 134 | ||||
| 110 | - | // setupWebViewIntegration connects the WebView to browser services | 135 | + | // Inject minimal bootstrap script for pane initialization |
| 111 | - | func (app *BrowserApp) setupWebViewIntegration() { | 136 | + | bootstrapScript := fmt.Sprintf(` |
| 112 | - | app.browserService.AttachWebView(app.webView) | 137 | + | window.__dumber_pane = { |
| 138 | + | id: '%s', | |||
| 139 | + | created: %d, | |||
| 140 | + | active: false | |||
| 141 | + | }; | |||
| 113 | 142 | ||||
| 114 | - | // GUI bundle is loaded via WebKit user scripts in enableUserContentManager | 143 | + | // GUI manager will be loaded on-demand |
| 115 | - | // No need to load separately in browser service | 144 | + | window.__dumber_gui_ready = true; |
| 116 | 145 | ||||
| 117 | - | // Use native window as title updater | 146 | + | console.log('[pane] Initialized pane %s'); |
| 118 | - | if win := app.webView.Window(); win != nil { | 147 | + | `, pane.ID(), time.Now().UnixMilli(), pane.ID()) |
| 119 | - | app.browserService.SetWindowTitleUpdater(win) | 148 | + | |
| 149 | + | if err := view.InjectScript(bootstrapScript); err != nil { | |||
| 150 | + | log.Printf("[pane-%s] Failed to inject bootstrap: %v", pane.ID(), err) | |||
| 120 | } | 151 | } | ||
| 152 | + | ||||
| 153 | + | app.attachPaneHandlers(pane) | |||
| 154 | + | return pane, nil | |||
| 121 | } | 155 | } | ||
| 122 | 156 | ||||
| 123 | - | // setupWebViewHandlers configures WebView event handlers | 157 | + | func (app *BrowserApp) attachPaneHandlers(pane *BrowserPane) { |
| 124 | - | func (app *BrowserApp) setupWebViewHandlers() { | 158 | + | if pane == nil || pane.webView == nil { |
| 125 | - | // Persist page titles to DB when they change | 159 | + | return |
| 126 | - | app.webView.RegisterTitleChangedHandler(func(title string) { | 160 | + | } |
| 161 | + | ||||
| 162 | + | pane.webView.RegisterTitleChangedHandler(func(title string) { | |||
| 127 | if title == "" { | 163 | if title == "" { | ||
| 128 | return | 164 | return | ||
| 129 | } | 165 | } | ||
| 130 | - | url := app.webView.GetCurrentURL() | 166 | + | url := pane.webView.GetCurrentURL() |
| 131 | if url == "" { | 167 | if url == "" { | ||
| 132 | return | 168 | return | ||
| 133 | } | 169 | } | ||
| @@ -139,36 +175,121 @@ func (app *BrowserApp) setupWebViewHandlers() { | |||||
| 139 | }(url, title) | 175 | }(url, title) | ||
| 140 | }) | 176 | }) | ||
| 141 | 177 | ||||
| 142 | - | // Set WebView reference for message handler and register script messages | 178 | + | pane.webView.RegisterScriptMessageHandler(func(payload string) { |
| 143 | - | app.messageHandler.SetWebView(app.webView) | 179 | + | if app.workspace != nil { |
| 144 | - | app.webView.RegisterScriptMessageHandler(app.messageHandler.Handle) | 180 | + | app.workspace.focusByView(pane.webView) |
| 181 | + | } | |||
| 182 | + | pane.messageHandler.Handle(payload) | |||
| 183 | + | }) | |||
| 184 | + | ||||
| 185 | + | if pane.messageHandler != nil && pane.navigationController != nil { | |||
| 186 | + | pane.messageHandler.SetNavigationController(pane.navigationController) | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | pane.webView.RegisterPopupHandler(func(uri string) bool { | |||
| 190 | + | handled := false | |||
| 191 | + | if app.workspace != nil { | |||
| 192 | + | handled = app.workspace.HandlePopup(pane.webView, uri) | |||
| 193 | + | } | |||
| 194 | + | if handled { | |||
| 195 | + | return true | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | if pane.navigationController != nil && uri != "" { | |||
| 199 | + | if err := pane.navigationController.NavigateToURL(uri); err != nil { | |||
| 200 | + | log.Printf("[workspace] popup fallback navigation failed: %v", err) | |||
| 201 | + | if pane.webView != nil { | |||
| 202 | + | if loadErr := pane.webView.LoadURL(uri); loadErr != nil { | |||
| 203 | + | log.Printf("[workspace] popup fallback load failed: %v", loadErr) | |||
| 204 | + | } | |||
| 205 | + | } | |||
| 206 | + | } | |||
| 207 | + | return true | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | if pane.webView != nil && uri != "" { | |||
| 211 | + | if err := pane.webView.LoadURL(uri); err != nil { | |||
| 212 | + | log.Printf("[workspace] popup direct load failed: %v", err) | |||
| 213 | + | } | |||
| 214 | + | return true | |||
| 215 | + | } | |||
| 145 | 216 | ||||
| 217 | + | return false | |||
| 218 | + | }) | |||
| 146 | } | 219 | } | ||
| 147 | 220 | ||||
| 148 | - | // setupControllers initializes and configures controller objects | 221 | + | // createWebView creates and configures the WebView |
| 149 | - | func (app *BrowserApp) setupControllers() { | 222 | + | func (app *BrowserApp) createWebView() error { |
| 150 | - | // Initialize controllers | 223 | + | log.Printf("Creating WebView (native backend expected: %v)", webkit.IsNativeAvailable()) |
| 151 | - | app.zoomController = control.NewZoomController(app.browserService, app.webView) | 224 | + | |
| 152 | - | app.navigationController = control.NewNavigationController( | 225 | + | cfg, err := app.buildWebkitConfig() |
| 153 | - | app.parserService, | 226 | + | if err != nil { |
| 154 | - | app.browserService, | 227 | + | return err |
| 155 | - | app.webView, | 228 | + | } |
| 156 | - | app.zoomController, | 229 | + | |
| 157 | - | ) | 230 | + | // Main window needs a top-level window |
| 158 | - | app.clipboardController = control.NewClipboardController(app.webView) | 231 | + | cfg.CreateWindow = true |
| 232 | + | ||||
| 233 | + | view, err := webkit.NewWebView(cfg) | |||
| 234 | + | ||||
| 235 | + | if err != nil { | |||
| 236 | + | return err | |||
| 237 | + | } | |||
| 238 | + | ||||
| 239 | + | pane, err := app.createPaneForView(view) | |||
| 240 | + | if err != nil { | |||
| 241 | + | return err | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | app.webView = view | |||
| 245 | + | app.zoomController = pane.zoomController | |||
| 246 | + | app.navigationController = pane.navigationController | |||
| 247 | + | app.clipboardController = pane.clipboardController | |||
| 248 | + | app.messageHandler = pane.messageHandler | |||
| 249 | + | app.shortcutHandler = pane.shortcutHandler | |||
| 250 | + | app.panes = []*BrowserPane{pane} | |||
| 251 | + | app.activePane = pane | |||
| 252 | + | ||||
| 253 | + | if app.workspace == nil { | |||
| 254 | + | app.workspace = NewWorkspaceManager(app, pane) | |||
| 255 | + | } | |||
| 159 | 256 | ||||
| 160 | - | if app.messageHandler != nil && app.navigationController != nil { | 257 | + | // Initialize window-level global shortcuts AFTER workspace is set up |
| 161 | - | app.messageHandler.SetNavigationController(app.navigationController) | 258 | + | if window := view.Window(); window != nil { |
| 259 | + | app.windowShortcutHandler = NewWindowShortcutHandler(window, app) | |||
| 260 | + | if app.windowShortcutHandler != nil { | |||
| 261 | + | log.Printf("Window-level global shortcuts initialized") | |||
| 262 | + | } else { | |||
| 263 | + | log.Printf("Warning: Failed to initialize window-level shortcuts") | |||
| 264 | + | } | |||
| 162 | } | 265 | } | ||
| 163 | 266 | ||||
| 164 | - | // Register controller handlers | 267 | + | app.setupWebViewIntegration() |
| 165 | - | app.zoomController.RegisterHandlers() | 268 | + | |
| 269 | + | // Apply initial zoom and show window | |||
| 270 | + | app.zoomController.ApplyInitialZoom() | |||
| 271 | + | ||||
| 272 | + | log.Printf("Showing WebView window…") | |||
| 273 | + | if err := view.Show(); err != nil { | |||
| 274 | + | log.Printf("Warning: failed to show WebView: %v", err) | |||
| 275 | + | } else if !webkit.IsNativeAvailable() { | |||
| 276 | + | log.Printf("Notice: running without webkit_cgo tag — no native window will be displayed.") | |||
| 277 | + | } | |||
| 278 | + | ||||
| 279 | + | return nil | |||
| 166 | } | 280 | } | ||
| 167 | 281 | ||||
| 168 | - | // setupKeyboardShortcuts registers keyboard shortcuts | 282 | + | // setupWebViewIntegration connects the WebView to browser services |
| 169 | - | func (app *BrowserApp) setupKeyboardShortcuts() { | 283 | + | func (app *BrowserApp) setupWebViewIntegration() { |
| 170 | - | app.shortcutHandler = NewShortcutHandler(app.webView, app.clipboardController) | 284 | + | app.browserService.AttachWebView(app.webView) |
| 171 | - | app.shortcutHandler.RegisterShortcuts() | 285 | + | |
| 286 | + | // GUI bundle is loaded via WebKit user scripts in enableUserContentManager | |||
| 287 | + | // No need to load separately in browser service | |||
| 288 | + | ||||
| 289 | + | // Use native window as title updater | |||
| 290 | + | if win := app.webView.Window(); win != nil { | |||
| 291 | + | app.browserService.SetWindowTitleUpdater(win) | |||
| 292 | + | } | |||
| 172 | } | 293 | } | ||
| 173 | 294 | ||||
| 174 | // setupContentBlocking initializes the content blocking system with proper timing | 295 | // setupContentBlocking initializes the content blocking system with proper timing | ||
M pkg/webkit/keyboard_cgo.go +49 -10
| @@ -8,15 +8,14 @@ package webkit | |||
| 8 | 8 | #include <gdk/gdk.h> | |
| 9 | 9 | ||
| 10 | 10 | // Declarations for Go callbacks | |
| 11 | - | extern void goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state); | |
| 11 | + | extern gboolean goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state); | |
| 12 | 12 | extern void goOnButtonPress(unsigned long id, unsigned int button, GdkModifierType state); | |
| 13 | 13 | extern void goOnScroll(unsigned long id, double dx, double dy, GdkModifierType state); | |
| 14 | 14 | ||
| 15 | 15 | // Key controller callback -> forward to Go | |
| 16 | 16 | static gboolean on_key_pressed(GtkEventControllerKey* controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) { | |
| 17 | 17 | (void)controller; (void)keycode; | |
| 18 | - | goOnKeyPress((unsigned long)user_data, keyval, state); | |
| 19 | - | return FALSE; // do not stop other handlers | |
| 18 | + | return goOnKeyPress((unsigned long)user_data, keyval, state); | |
| 20 | 19 | } | |
| 21 | 20 | ||
| 22 | 21 | // Mouse click gesture -> forward side buttons to Go | |
| @@ -137,15 +136,18 @@ func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error | |||
| 137 | 136 | } | |
| 138 | 137 | ||
| 139 | 138 | //export goOnKeyPress | |
| 140 | - | func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) { | |
| 139 | + | func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) C.gboolean { | |
| 141 | 140 | // Normalize and dispatch | |
| 142 | 141 | uid := uintptr(id) | |
| 143 | 142 | kv := uint(keyval) | |
| 144 | 143 | st := uint(state) | |
| 145 | - | dispatchAccelerator(uid, kv, st) | |
| 144 | + | if dispatchAccelerator(uid, kv, st) { | |
| 145 | + | return C.gboolean(1) | |
| 146 | + | } | |
| 147 | + | return C.gboolean(0) | |
| 146 | 148 | } | |
| 147 | 149 | ||
| 148 | - | func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |
| 150 | + | func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool { | |
| 149 | 151 | ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0 | |
| 150 | 152 | alt := (state & uint(C.GDK_ALT_MASK)) != 0 | |
| 151 | 153 | shift := (state & uint(C.GDK_SHIFT_MASK)) != 0 | |
| @@ -165,6 +167,18 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||
| 165 | 167 | keyName = "ArrowLeft" | |
| 166 | 168 | case uint(C.GDK_KEY_Right): | |
| 167 | 169 | keyName = "ArrowRight" | |
| 170 | + | case uint(C.GDK_KEY_Up): | |
| 171 | + | keyName = "ArrowUp" | |
| 172 | + | case uint(C.GDK_KEY_Down): | |
| 173 | + | keyName = "ArrowDown" | |
| 174 | + | case uint(C.GDK_KEY_KP_Left): | |
| 175 | + | keyName = "ArrowLeft" | |
| 176 | + | case uint(C.GDK_KEY_KP_Right): | |
| 177 | + | keyName = "ArrowRight" | |
| 178 | + | case uint(C.GDK_KEY_KP_Up): | |
| 179 | + | keyName = "ArrowUp" | |
| 180 | + | case uint(C.GDK_KEY_KP_Down): | |
| 181 | + | keyName = "ArrowDown" | |
| 168 | 182 | case uint(C.GDK_KEY_F12): | |
| 169 | 183 | keyName = "F12" | |
| 170 | 184 | case uint(C.GDK_KEY_F5): | |
| @@ -177,18 +191,20 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||
| 177 | 191 | keyName = "l" | |
| 178 | 192 | case uint(C.GDK_KEY_f), uint(C.GDK_KEY_F): | |
| 179 | 193 | keyName = "f" | |
| 194 | + | case uint(C.GDK_KEY_p), uint(C.GDK_KEY_P): | |
| 195 | + | keyName = "p" | |
| 180 | 196 | default: | |
| 181 | 197 | // If Control held, log unknown keyval for diagnostics | |
| 182 | 198 | if ctrl { | |
| 183 | 199 | log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval) | |
| 184 | 200 | } | |
| 185 | - | return | |
| 201 | + | return false | |
| 186 | 202 | } | |
| 187 | 203 | ||
| 188 | 204 | now := time.Now() | |
| 189 | 205 | rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName) | |
| 190 | 206 | if shouldDebounceShortcut(uid, rawKey, now) { | |
| 191 | - | return | |
| 207 | + | return false | |
| 192 | 208 | } | |
| 193 | 209 | ||
| 194 | 210 | if logMinusEvent { | |
| @@ -196,6 +212,7 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||
| 196 | 212 | } | |
| 197 | 213 | ||
| 198 | 214 | // Candidate accelerator strings in order | |
| 215 | + | handled := false | |
| 199 | 216 | candidates := []string{keyName} | |
| 200 | 217 | if ctrl && shift { | |
| 201 | 218 | // Ctrl+Shift combinations | |
| @@ -238,18 +255,21 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||
| 238 | 255 | reg := viewShortcuts[uid] | |
| 239 | 256 | regMu.RUnlock() | |
| 240 | 257 | if reg == nil { | |
| 241 | - | return | |
| 258 | + | return handled | |
| 242 | 259 | } | |
| 243 | 260 | for _, name := range candidates { | |
| 244 | 261 | if cb, ok := reg[name]; ok { | |
| 245 | 262 | if shouldDebounceShortcut(uid, "shortcut:"+name, now) { | |
| 246 | - | return | |
| 263 | + | return handled | |
| 247 | 264 | } | |
| 248 | 265 | log.Printf("[accelerator] %s", name) | |
| 249 | 266 | cb() | |
| 267 | + | handled = true | |
| 250 | 268 | break | |
| 251 | 269 | } | |
| 252 | 270 | } | |
| 271 | + | ||
| 272 | + | return handled | |
| 253 | 273 | } | |
| 254 | 274 | ||
| 255 | 275 | func nextViewID() uintptr { | |
| @@ -416,6 +436,25 @@ func goOnURIChanged(id C.ulong, curi *C.char) { | |||
| 416 | 436 | vw.dispatchURIChanged(uri) | |
| 417 | 437 | } | |
| 418 | 438 | ||
| 439 | + | //export goOnPopupRequest | |
| 440 | + | func goOnPopupRequest(id C.ulong, curi *C.char) C.gboolean { | |
| 441 | + | uid := uintptr(id) | |
| 442 | + | regMu.RLock() | |
| 443 | + | vw := viewByID[uid] | |
| 444 | + | regMu.RUnlock() | |
| 445 | + | if vw == nil { | |
| 446 | + | return C.gboolean(0) | |
| 447 | + | } | |
| 448 | + | var uri string | |
| 449 | + | if curi != nil { | |
| 450 | + | uri = C.GoString(curi) | |
| 451 | + | } | |
| 452 | + | if vw.dispatchPopupRequest(uri) { | |
| 453 | + | return C.gboolean(1) | |
| 454 | + | } | |
| 455 | + | return C.gboolean(0) | |
| 456 | + | } | |
| 457 | + | ||
| 419 | 458 | //export goOnButtonPress | |
| 420 | 459 | func goOnButtonPress(id C.ulong, button C.guint, state C.GdkModifierType) { | |
| 421 | 460 | uid := uintptr(id) | |
| @@ -8,15 +8,14 @@ package webkit | |||||
| 8 | #include <gdk/gdk.h> | 8 | #include <gdk/gdk.h> | ||
| 9 | 9 | ||||
| 10 | // Declarations for Go callbacks | 10 | // Declarations for Go callbacks | ||
| 11 | - | extern void goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state); | 11 | + | extern gboolean 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 | extern void goOnScroll(unsigned long id, double dx, double dy, GdkModifierType state); | ||
| 14 | 14 | ||||
| 15 | // Key controller callback -> forward to Go | 15 | // Key controller callback -> forward to Go | ||
| 16 | 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) { | ||
| 17 | (void)controller; (void)keycode; | 17 | (void)controller; (void)keycode; | ||
| 18 | - | goOnKeyPress((unsigned long)user_data, keyval, state); | 18 | + | return goOnKeyPress((unsigned long)user_data, keyval, state); |
| 19 | - | return FALSE; // do not stop other handlers | |||
| 20 | } | 19 | } | ||
| 21 | 20 | ||||
| 22 | // Mouse click gesture -> forward side buttons to Go | 21 | // Mouse click gesture -> forward side buttons to Go | ||
| @@ -137,15 +136,18 @@ func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error | |||||
| 137 | } | 136 | } | ||
| 138 | 137 | ||||
| 139 | //export goOnKeyPress | 138 | //export goOnKeyPress | ||
| 140 | - | func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) { | 139 | + | func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) C.gboolean { |
| 141 | // Normalize and dispatch | 140 | // Normalize and dispatch | ||
| 142 | uid := uintptr(id) | 141 | uid := uintptr(id) | ||
| 143 | kv := uint(keyval) | 142 | kv := uint(keyval) | ||
| 144 | st := uint(state) | 143 | st := uint(state) | ||
| 145 | - | dispatchAccelerator(uid, kv, st) | 144 | + | if dispatchAccelerator(uid, kv, st) { |
| 145 | + | return C.gboolean(1) | |||
| 146 | + | } | |||
| 147 | + | return C.gboolean(0) | |||
| 146 | } | 148 | } | ||
| 147 | 149 | ||||
| 148 | - | func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | 150 | + | func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool { |
| 149 | ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0 | 151 | ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0 | ||
| 150 | alt := (state & uint(C.GDK_ALT_MASK)) != 0 | 152 | alt := (state & uint(C.GDK_ALT_MASK)) != 0 | ||
| 151 | shift := (state & uint(C.GDK_SHIFT_MASK)) != 0 | 153 | shift := (state & uint(C.GDK_SHIFT_MASK)) != 0 | ||
| @@ -165,6 +167,18 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||||
| 165 | keyName = "ArrowLeft" | 167 | keyName = "ArrowLeft" | ||
| 166 | case uint(C.GDK_KEY_Right): | 168 | case uint(C.GDK_KEY_Right): | ||
| 167 | keyName = "ArrowRight" | 169 | keyName = "ArrowRight" | ||
| 170 | + | case uint(C.GDK_KEY_Up): | |||
| 171 | + | keyName = "ArrowUp" | |||
| 172 | + | case uint(C.GDK_KEY_Down): | |||
| 173 | + | keyName = "ArrowDown" | |||
| 174 | + | case uint(C.GDK_KEY_KP_Left): | |||
| 175 | + | keyName = "ArrowLeft" | |||
| 176 | + | case uint(C.GDK_KEY_KP_Right): | |||
| 177 | + | keyName = "ArrowRight" | |||
| 178 | + | case uint(C.GDK_KEY_KP_Up): | |||
| 179 | + | keyName = "ArrowUp" | |||
| 180 | + | case uint(C.GDK_KEY_KP_Down): | |||
| 181 | + | keyName = "ArrowDown" | |||
| 168 | case uint(C.GDK_KEY_F12): | 182 | case uint(C.GDK_KEY_F12): | ||
| 169 | keyName = "F12" | 183 | keyName = "F12" | ||
| 170 | case uint(C.GDK_KEY_F5): | 184 | case uint(C.GDK_KEY_F5): | ||
| @@ -177,18 +191,20 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||||
| 177 | keyName = "l" | 191 | keyName = "l" | ||
| 178 | case uint(C.GDK_KEY_f), uint(C.GDK_KEY_F): | 192 | case uint(C.GDK_KEY_f), uint(C.GDK_KEY_F): | ||
| 179 | keyName = "f" | 193 | keyName = "f" | ||
| 194 | + | case uint(C.GDK_KEY_p), uint(C.GDK_KEY_P): | |||
| 195 | + | keyName = "p" | |||
| 180 | default: | 196 | default: | ||
| 181 | // If Control held, log unknown keyval for diagnostics | 197 | // If Control held, log unknown keyval for diagnostics | ||
| 182 | if ctrl { | 198 | if ctrl { | ||
| 183 | log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval) | 199 | log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval) | ||
| 184 | } | 200 | } | ||
| 185 | - | return | 201 | + | return false |
| 186 | } | 202 | } | ||
| 187 | 203 | ||||
| 188 | now := time.Now() | 204 | now := time.Now() | ||
| 189 | rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName) | 205 | rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName) | ||
| 190 | if shouldDebounceShortcut(uid, rawKey, now) { | 206 | if shouldDebounceShortcut(uid, rawKey, now) { | ||
| 191 | - | return | 207 | + | return false |
| 192 | } | 208 | } | ||
| 193 | 209 | ||||
| 194 | if logMinusEvent { | 210 | if logMinusEvent { | ||
| @@ -196,6 +212,7 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||||
| 196 | } | 212 | } | ||
| 197 | 213 | ||||
| 198 | // Candidate accelerator strings in order | 214 | // Candidate accelerator strings in order | ||
| 215 | + | handled := false | |||
| 199 | candidates := []string{keyName} | 216 | candidates := []string{keyName} | ||
| 200 | if ctrl && shift { | 217 | if ctrl && shift { | ||
| 201 | // Ctrl+Shift combinations | 218 | // Ctrl+Shift combinations | ||
| @@ -238,18 +255,21 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||||
| 238 | reg := viewShortcuts[uid] | 255 | reg := viewShortcuts[uid] | ||
| 239 | regMu.RUnlock() | 256 | regMu.RUnlock() | ||
| 240 | if reg == nil { | 257 | if reg == nil { | ||
| 241 | - | return | 258 | + | return handled |
| 242 | } | 259 | } | ||
| 243 | for _, name := range candidates { | 260 | for _, name := range candidates { | ||
| 244 | if cb, ok := reg[name]; ok { | 261 | if cb, ok := reg[name]; ok { | ||
| 245 | if shouldDebounceShortcut(uid, "shortcut:"+name, now) { | 262 | if shouldDebounceShortcut(uid, "shortcut:"+name, now) { | ||
| 246 | - | return | 263 | + | return handled |
| 247 | } | 264 | } | ||
| 248 | log.Printf("[accelerator] %s", name) | 265 | log.Printf("[accelerator] %s", name) | ||
| 249 | cb() | 266 | cb() | ||
| 267 | + | handled = true | |||
| 250 | break | 268 | break | ||
| 251 | } | 269 | } | ||
| 252 | } | 270 | } | ||
| 271 | + | ||||
| 272 | + | return handled | |||
| 253 | } | 273 | } | ||
| 254 | 274 | ||||
| 255 | func nextViewID() uintptr { | 275 | func nextViewID() uintptr { | ||
| @@ -416,6 +436,25 @@ func goOnURIChanged(id C.ulong, curi *C.char) { | |||||
| 416 | vw.dispatchURIChanged(uri) | 436 | vw.dispatchURIChanged(uri) | ||
| 417 | } | 437 | } | ||
| 418 | 438 | ||||
| 439 | + | //export goOnPopupRequest | |||
| 440 | + | func goOnPopupRequest(id C.ulong, curi *C.char) C.gboolean { | |||
| 441 | + | uid := uintptr(id) | |||
| 442 | + | regMu.RLock() | |||
| 443 | + | vw := viewByID[uid] | |||
| 444 | + | regMu.RUnlock() | |||
| 445 | + | if vw == nil { | |||
| 446 | + | return C.gboolean(0) | |||
| 447 | + | } | |||
| 448 | + | var uri string | |||
| 449 | + | if curi != nil { | |||
| 450 | + | uri = C.GoString(curi) | |||
| 451 | + | } | |||
| 452 | + | if vw.dispatchPopupRequest(uri) { | |||
| 453 | + | return C.gboolean(1) | |||
| 454 | + | } | |||
| 455 | + | return C.gboolean(0) | |||
| 456 | + | } | |||
| 457 | + | ||||
| 419 | //export goOnButtonPress | 458 | //export goOnButtonPress | ||
| 420 | func goOnButtonPress(id C.ulong, button C.guint, state C.GdkModifierType) { | 459 | func goOnButtonPress(id C.ulong, button C.guint, state C.GdkModifierType) { | ||
| 421 | uid := uintptr(id) | 460 | uid := uintptr(id) | ||
M pkg/webkit/settings.go +4 -0
| @@ -47,6 +47,10 @@ type Config struct { | |||
| 47 | 47 | ||
| 48 | 48 | // UseDomZoom toggles CSS/DOM-based zoom instead of WebKit's native zoom. | |
| 49 | 49 | UseDomZoom bool | |
| 50 | + | ||
| 51 | + | // CreateWindow controls whether to create a window during WebView initialization. | |
| 52 | + | // Set to false when creating WebViews for workspace panes that will be reparented. | |
| 53 | + | CreateWindow bool | |
| 50 | 54 | } | |
| 51 | 55 | ||
| 52 | 56 | // CodecPreferencesConfig holds codec preferences for WebKit media playback | |
| @@ -47,6 +47,10 @@ type Config struct { | |||||
| 47 | 47 | ||||
| 48 | // UseDomZoom toggles CSS/DOM-based zoom instead of WebKit's native zoom. | 48 | // UseDomZoom toggles CSS/DOM-based zoom instead of WebKit's native zoom. | ||
| 49 | UseDomZoom bool | 49 | UseDomZoom bool | ||
| 50 | + | ||||
| 51 | + | // CreateWindow controls whether to create a window during WebView initialization. | |||
| 52 | + | // Set to false when creating WebViews for workspace panes that will be reparented. | |||
| 53 | + | CreateWindow bool | |||
| 50 | } | 54 | } | ||
| 51 | 55 | ||||
| 52 | // CodecPreferencesConfig holds codec preferences for WebKit media playback | 56 | // CodecPreferencesConfig holds codec preferences for WebKit media playback | ||
M pkg/webkit/webview.go +27 -0
| @@ -20,8 +20,10 @@ type WebView struct { | |||
| 20 | 20 | titleHandler func(title string) | |
| 21 | 21 | uriHandler func(uri string) | |
| 22 | 22 | zoomHandler func(level float64) | |
| 23 | + | popupHandler func(string) bool | |
| 23 | 24 | useDomZoom bool | |
| 24 | 25 | domZoomSeed float64 | |
| 26 | + | container uintptr | |
| 25 | 27 | } | |
| 26 | 28 | ||
| 27 | 29 | // NewWebView constructs a new WebView instance. | |
| @@ -39,6 +41,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 39 | 41 | wv.domZoomSeed = wv.zoom | |
| 40 | 42 | // Construction succeeds; create a logical window placeholder. | |
| 41 | 43 | wv.window = &Window{Title: "Dumber Browser"} | |
| 44 | + | wv.container = newWidgetHandle() | |
| 42 | 45 | return wv, nil | |
| 43 | 46 | } | |
| 44 | 47 | ||
| @@ -111,12 +114,21 @@ func (w *WebView) CloseDevTools() error { return nil } | |||
| 111 | 114 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | |
| 112 | 115 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | |
| 113 | 116 | ||
| 117 | + | func (w *WebView) RegisterPopupHandler(cb func(string) bool) { w.popupHandler = cb } | |
| 118 | + | ||
| 114 | 119 | func (w *WebView) dispatchScriptMessage(payload string) { //nolint:unused // Called from CGO WebKit callbacks | |
| 115 | 120 | if w != nil && w.msgHandler != nil { | |
| 116 | 121 | w.msgHandler(payload) | |
| 117 | 122 | } | |
| 118 | 123 | } | |
| 119 | 124 | ||
| 125 | + | func (w *WebView) dispatchPopupRequest(uri string) bool { //nolint:unused // Called from CGO WebKit callbacks | |
| 126 | + | if w != nil && w.popupHandler != nil { | |
| 127 | + | return w.popupHandler(uri) | |
| 128 | + | } | |
| 129 | + | return false | |
| 130 | + | } | |
| 131 | + | ||
| 120 | 132 | // RegisterTitleChangedHandler registers a callback invoked when the page title changes. | |
| 121 | 133 | func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb } | |
| 122 | 134 | ||
| @@ -144,6 +156,15 @@ func (w *WebView) dispatchZoomChanged(level float64) { | |||
| 144 | 156 | } | |
| 145 | 157 | } | |
| 146 | 158 | ||
| 159 | + | // Widget returns an empty handle in stub builds. | |
| 160 | + | func (w *WebView) Widget() uintptr { return 0 } | |
| 161 | + | ||
| 162 | + | // RootWidget returns the container widget handle in CGO builds. Stub returns 0. | |
| 163 | + | func (w *WebView) RootWidget() uintptr { return w.container } | |
| 164 | + | ||
| 165 | + | // DestroyWindow is a no-op in stub builds. | |
| 166 | + | func (w *WebView) DestroyWindow() {} | |
| 167 | + | ||
| 147 | 168 | // RunOnMainThread executes fn immediately in non-CGO builds. | |
| 148 | 169 | func (w *WebView) RunOnMainThread(fn func()) { | |
| 149 | 170 | if fn != nil { | |
| @@ -151,6 +172,12 @@ func (w *WebView) RunOnMainThread(fn func()) { | |||
| 151 | 172 | } | |
| 152 | 173 | } | |
| 153 | 174 | ||
| 175 | + | // PrepareForReparenting is a no-op in stub builds. | |
| 176 | + | func (w *WebView) PrepareForReparenting() {} | |
| 177 | + | ||
| 178 | + | // RefreshAfterReparenting is a no-op in stub builds. | |
| 179 | + | func (w *WebView) RefreshAfterReparenting() {} | |
| 180 | + | ||
| 154 | 181 | // UsesDomZoom reports whether DOM-based zoom is enabled in this WebView. | |
| 155 | 182 | func (w *WebView) UsesDomZoom() bool { | |
| 156 | 183 | return w != nil && w.useDomZoom | |
| @@ -20,8 +20,10 @@ type WebView struct { | |||||
| 20 | titleHandler func(title string) | 20 | titleHandler func(title string) | ||
| 21 | uriHandler func(uri string) | 21 | uriHandler func(uri string) | ||
| 22 | zoomHandler func(level float64) | 22 | zoomHandler func(level float64) | ||
| 23 | + | popupHandler func(string) bool | |||
| 23 | useDomZoom bool | 24 | useDomZoom bool | ||
| 24 | domZoomSeed float64 | 25 | domZoomSeed float64 | ||
| 26 | + | container uintptr | |||
| 25 | } | 27 | } | ||
| 26 | 28 | ||||
| 27 | // NewWebView constructs a new WebView instance. | 29 | // NewWebView constructs a new WebView instance. | ||
| @@ -39,6 +41,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 39 | wv.domZoomSeed = wv.zoom | 41 | wv.domZoomSeed = wv.zoom | ||
| 40 | // Construction succeeds; create a logical window placeholder. | 42 | // Construction succeeds; create a logical window placeholder. | ||
| 41 | wv.window = &Window{Title: "Dumber Browser"} | 43 | wv.window = &Window{Title: "Dumber Browser"} | ||
| 44 | + | wv.container = newWidgetHandle() | |||
| 42 | return wv, nil | 45 | return wv, nil | ||
| 43 | } | 46 | } | ||
| 44 | 47 | ||||
| @@ -111,12 +114,21 @@ func (w *WebView) CloseDevTools() error { return nil } | |||||
| 111 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | 114 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | ||
| 112 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | 115 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | ||
| 113 | 116 | ||||
| 117 | + | func (w *WebView) RegisterPopupHandler(cb func(string) bool) { w.popupHandler = cb } | |||
| 118 | + | ||||
| 114 | func (w *WebView) dispatchScriptMessage(payload string) { //nolint:unused // Called from CGO WebKit callbacks | 119 | func (w *WebView) dispatchScriptMessage(payload string) { //nolint:unused // Called from CGO WebKit callbacks | ||
| 115 | if w != nil && w.msgHandler != nil { | 120 | if w != nil && w.msgHandler != nil { | ||
| 116 | w.msgHandler(payload) | 121 | w.msgHandler(payload) | ||
| 117 | } | 122 | } | ||
| 118 | } | 123 | } | ||
| 119 | 124 | ||||
| 125 | + | func (w *WebView) dispatchPopupRequest(uri string) bool { //nolint:unused // Called from CGO WebKit callbacks | |||
| 126 | + | if w != nil && w.popupHandler != nil { | |||
| 127 | + | return w.popupHandler(uri) | |||
| 128 | + | } | |||
| 129 | + | return false | |||
| 130 | + | } | |||
| 131 | + | ||||
| 120 | // RegisterTitleChangedHandler registers a callback invoked when the page title changes. | 132 | // RegisterTitleChangedHandler registers a callback invoked when the page title changes. | ||
| 121 | func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb } | 133 | func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb } | ||
| 122 | 134 | ||||
| @@ -144,6 +156,15 @@ func (w *WebView) dispatchZoomChanged(level float64) { | |||||
| 144 | } | 156 | } | ||
| 145 | } | 157 | } | ||
| 146 | 158 | ||||
| 159 | + | // Widget returns an empty handle in stub builds. | |||
| 160 | + | func (w *WebView) Widget() uintptr { return 0 } | |||
| 161 | + | ||||
| 162 | + | // RootWidget returns the container widget handle in CGO builds. Stub returns 0. | |||
| 163 | + | func (w *WebView) RootWidget() uintptr { return w.container } | |||
| 164 | + | ||||
| 165 | + | // DestroyWindow is a no-op in stub builds. | |||
| 166 | + | func (w *WebView) DestroyWindow() {} | |||
| 167 | + | ||||
| 147 | // RunOnMainThread executes fn immediately in non-CGO builds. | 168 | // RunOnMainThread executes fn immediately in non-CGO builds. | ||
| 148 | func (w *WebView) RunOnMainThread(fn func()) { | 169 | func (w *WebView) RunOnMainThread(fn func()) { | ||
| 149 | if fn != nil { | 170 | if fn != nil { | ||
| @@ -151,6 +172,12 @@ func (w *WebView) RunOnMainThread(fn func()) { | |||||
| 151 | } | 172 | } | ||
| 152 | } | 173 | } | ||
| 153 | 174 | ||||
| 175 | + | // PrepareForReparenting is a no-op in stub builds. | |||
| 176 | + | func (w *WebView) PrepareForReparenting() {} | |||
| 177 | + | ||||
| 178 | + | // RefreshAfterReparenting is a no-op in stub builds. | |||
| 179 | + | func (w *WebView) RefreshAfterReparenting() {} | |||
| 180 | + | ||||
| 154 | // UsesDomZoom reports whether DOM-based zoom is enabled in this WebView. | 181 | // UsesDomZoom reports whether DOM-based zoom is enabled in this WebView. | ||
| 155 | func (w *WebView) UsesDomZoom() bool { | 182 | func (w *WebView) UsesDomZoom() bool { | ||
| 156 | return w != nil && w.useDomZoom | 183 | return w != nil && w.useDomZoom | ||
M pkg/webkit/webview_cgo.go +195 -46
| @@ -22,6 +22,7 @@ static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); } | |||
| 22 | 22 | // Forward declaration | |
| 23 | 23 | extern void goQuitMainLoop(); | |
| 24 | 24 | extern void goInvokeHandle(uintptr_t handle); | |
| 25 | + | extern gboolean goOnPopupRequest(unsigned long id, char* uri); | |
| 25 | 26 | ||
| 26 | 27 | static gboolean invoke_handle_cb(gpointer data) { | |
| 27 | 28 | goInvokeHandle((uintptr_t)data); | |
| @@ -51,20 +52,24 @@ static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view, | |||
| 51 | 52 | GTlsCertificateFlags errors, | |
| 52 | 53 | gpointer user_data); | |
| 53 | 54 | ||
| 54 | - | // Handle create signal for _blank links - redirect to current window | |
| 55 | + | // Handle create signal for _blank links - delegate to Go workspace manager | |
| 55 | 56 | static WebKitWebView* on_create_new_window(WebKitWebView* web_view, | |
| 56 | 57 | WebKitNavigationAction* navigation_action, | |
| 57 | 58 | gpointer user_data) { | |
| 58 | - | (void)user_data; | |
| 59 | + | unsigned long view_id = (unsigned long)user_data; | |
| 60 | + | const gchar* uri = NULL; | |
| 59 | 61 | ||
| 60 | 62 | // Get the request from navigation action | |
| 61 | 63 | WebKitURIRequest* request = webkit_navigation_action_get_request(navigation_action); | |
| 62 | 64 | if (request) { | |
| 63 | - | const gchar* uri = webkit_uri_request_get_uri(request); | |
| 64 | - | if (uri) { | |
| 65 | - | // Load in current window instead of creating new one | |
| 66 | - | webkit_web_view_load_uri(web_view, uri); | |
| 67 | - | } | |
| 65 | + | uri = webkit_uri_request_get_uri(request); | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | gboolean handled = goOnPopupRequest(view_id, (char*)uri); | |
| 69 | + | ||
| 70 | + | if (!handled && uri) { | |
| 71 | + | // Load in current window instead of creating new one | |
| 72 | + | webkit_web_view_load_uri(web_view, uri); | |
| 68 | 73 | } | |
| 69 | 74 | ||
| 70 | 75 | // Return NULL to prevent new window creation | |
| @@ -150,9 +155,6 @@ static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const c | |||
| 150 | 155 | GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 }; | |
| 151 | 156 | webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color); | |
| 152 | 157 | ||
| 153 | - | // Handle create signal to redirect _blank links to current window | |
| 154 | - | g_signal_connect(G_OBJECT(w), "create", G_CALLBACK(on_create_new_window), NULL); | |
| 155 | - | ||
| 156 | 158 | // Connect TLS error handler | |
| 157 | 159 | connect_tls_error_handler(WEBKIT_WEB_VIEW(w)); | |
| 158 | 160 | ||
| @@ -160,6 +162,11 @@ static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const c | |||
| 160 | 162 | return w; | |
| 161 | 163 | } | |
| 162 | 164 | ||
| 165 | + | static void connect_create_handler(WebKitWebView* web_view, unsigned long id) { | |
| 166 | + | if (!web_view) return; | |
| 167 | + | g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_new_window), (gpointer)id, NULL, 0); | |
| 168 | + | } | |
| 169 | + | ||
| 163 | 170 | static gboolean gtk_prefers_dark() { | |
| 164 | 171 | // Method 1: Check GNOME desktop interface color-scheme (primary method) | |
| 165 | 172 | GSettings* desktop_settings = g_settings_new("org.gnome.desktop.interface"); | |
| @@ -638,6 +645,7 @@ static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* host | |||
| 638 | 645 | // Return response code: 0=Go Back, 1=Proceed Once, 2=Always Accept | |
| 639 | 646 | return response_data.response; | |
| 640 | 647 | } | |
| 648 | + | ||
| 641 | 649 | */ | |
| 642 | 650 | import "C" | |
| 643 | 651 | ||
| @@ -655,6 +663,7 @@ import ( | |||
| 655 | 663 | "runtime/cgo" | |
| 656 | 664 | "strconv" | |
| 657 | 665 | "strings" | |
| 666 | + | "sync" | |
| 658 | 667 | "time" | |
| 659 | 668 | "unsafe" | |
| 660 | 669 | ||
| @@ -791,6 +800,21 @@ const domBridgeTemplate = `(() => { try { | |||
| 791 | 800 | }; | |
| 792 | 801 | } catch (e) { console.warn('[dumber] unified bridge init failed', e); } })();` | |
| 793 | 802 | ||
| 803 | + | var registerSchemeOnce sync.Once | |
| 804 | + | ||
| 805 | + | func registerDumbScheme(ctx *C.WebKitWebContext) { | |
| 806 | + | if ctx == nil { | |
| 807 | + | return | |
| 808 | + | } | |
| 809 | + | ||
| 810 | + | registerSchemeOnce.Do(func() { | |
| 811 | + | sch := C.CString("dumb") | |
| 812 | + | defer C.free(unsafe.Pointer(sch)) | |
| 813 | + | C.register_uri_scheme_handler(ctx, sch) | |
| 814 | + | C.register_uri_scheme_security(ctx, sch) | |
| 815 | + | }) | |
| 816 | + | } | |
| 817 | + | ||
| 794 | 818 | // Helper function to convert bool to int for C interop | |
| 795 | 819 | func boolToInt(b bool) int { | |
| 796 | 820 | if b { | |
| @@ -894,10 +918,11 @@ func storeCertificateDecision(hostname, certificateInfo, decision string, perman | |||
| 894 | 918 | } | |
| 895 | 919 | ||
| 896 | 920 | type nativeView struct { | |
| 897 | - | win *C.GtkWidget | |
| 898 | - | view *C.GtkWidget | |
| 899 | - | wv *C.WebKitWebView | |
| 900 | - | ucm *C.WebKitUserContentManager | |
| 921 | + | win *C.GtkWidget | |
| 922 | + | container *C.GtkWidget | |
| 923 | + | view *C.GtkWidget | |
| 924 | + | wv *C.WebKitWebView | |
| 925 | + | ucm *C.WebKitUserContentManager | |
| 901 | 926 | } | |
| 902 | 927 | ||
| 903 | 928 | type memoryStats struct { | |
| @@ -919,6 +944,7 @@ type WebView struct { | |||
| 919 | 944 | titleHandler func(title string) | |
| 920 | 945 | uriHandler func(uri string) | |
| 921 | 946 | zoomHandler func(level float64) | |
| 947 | + | popupHandler func(string) bool | |
| 922 | 948 | memStats *memoryStats | |
| 923 | 949 | gcTicker *time.Ticker | |
| 924 | 950 | gcDone chan struct{} | |
| @@ -926,6 +952,7 @@ type WebView struct { | |||
| 926 | 952 | useDomZoom bool | |
| 927 | 953 | domZoomSeed float64 | |
| 928 | 954 | domBridgeScript *C.WebKitUserScript | |
| 955 | + | wasReparented bool // Track if this WebView has been reparented | |
| 929 | 956 | } | |
| 930 | 957 | ||
| 931 | 958 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | |
| @@ -1018,13 +1045,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1018 | 1045 | } | |
| 1019 | 1046 | } | |
| 1020 | 1047 | ||
| 1021 | - | // Register custom URI scheme handler for "dumb://" | |
| 1022 | - | { | |
| 1023 | - | sch := C.CString("dumb") | |
| 1024 | - | C.register_uri_scheme_handler(ctx, sch) | |
| 1025 | - | C.register_uri_scheme_security(ctx, sch) | |
| 1026 | - | C.free(unsafe.Pointer(sch)) | |
| 1027 | - | } | |
| 1048 | + | registerDumbScheme(ctx) | |
| 1028 | 1049 | // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session | |
| 1029 | 1050 | ||
| 1030 | 1051 | // Create WebView through g_object_new with a fresh UCM (GTK4/WebKit 6 style) | |
| @@ -1034,17 +1055,35 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1034 | 1055 | return nil, errors.New("failed to create WebKitWebView") | |
| 1035 | 1056 | } | |
| 1036 | 1057 | ||
| 1037 | - | // Create a top-level window to host the view | |
| 1038 | - | win := C.new_window() | |
| 1039 | - | if win == nil { | |
| 1040 | - | return nil, errors.New("failed to create GtkWindow") | |
| 1041 | - | } | |
| 1058 | + | var win *C.GtkWidget | |
| 1059 | + | var window *Window | |
| 1060 | + | container := C.gtk_box_new(C.GtkOrientation(C.GTK_ORIENTATION_VERTICAL), 0) | |
| 1061 | + | if container == nil { | |
| 1062 | + | return nil, errors.New("failed to create GtkBox container") | |
| 1063 | + | } | |
| 1064 | + | // Hold a strong reference so the WebView keeps the container alive across reparenting. | |
| 1065 | + | C.g_object_ref_sink(C.gpointer(container)) | |
| 1066 | + | C.gtk_widget_set_hexpand(container, C.gboolean(1)) | |
| 1067 | + | C.gtk_widget_set_vexpand(container, C.gboolean(1)) | |
| 1068 | + | C.gtk_widget_set_hexpand(viewWidget, C.gboolean(1)) | |
| 1069 | + | C.gtk_widget_set_vexpand(viewWidget, C.gboolean(1)) | |
| 1070 | + | C.gtk_box_append((*C.GtkBox)(unsafe.Pointer(container)), viewWidget) | |
| 1071 | + | ||
| 1072 | + | // Only create window if requested | |
| 1073 | + | if cfg.CreateWindow { | |
| 1074 | + | // Create a top-level window to host the view | |
| 1075 | + | win = C.new_window() | |
| 1076 | + | if win == nil { | |
| 1077 | + | return nil, errors.New("failed to create GtkWindow") | |
| 1078 | + | } | |
| 1042 | 1079 | ||
| 1043 | - | // Pack view widget into the window | |
| 1044 | - | // GTK4: containers removed; use gtk_window_set_child | |
| 1045 | - | C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), viewWidget) | |
| 1046 | - | C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768) | |
| 1047 | - | C.connect_destroy_quit(win) | |
| 1080 | + | // Pack view widget into the window | |
| 1081 | + | // GTK4: containers removed; use gtk_window_set_child | |
| 1082 | + | C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), container) | |
| 1083 | + | C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768) | |
| 1084 | + | C.connect_destroy_quit(win) | |
| 1085 | + | window = &Window{win: win} | |
| 1086 | + | } | |
| 1048 | 1087 | ||
| 1049 | 1088 | seed := 1.0 | |
| 1050 | 1089 | if cfg.ZoomDefault > 0 { | |
| @@ -1056,8 +1095,8 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1056 | 1095 | zoom: 1.0, | |
| 1057 | 1096 | useDomZoom: cfg.UseDomZoom, | |
| 1058 | 1097 | domZoomSeed: seed, | |
| 1059 | - | native: &nativeView{win: win, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm}, | |
| 1060 | - | window: &Window{win: win}, | |
| 1098 | + | native: &nativeView{win: win, container: container, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm}, | |
| 1099 | + | window: window, | |
| 1061 | 1100 | memStats: &memoryStats{ | |
| 1062 | 1101 | pageLoadCount: 0, | |
| 1063 | 1102 | lastGCTime: time.Now(), | |
| @@ -1069,6 +1108,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1069 | 1108 | // Assign an ID for accelerator dispatch | |
| 1070 | 1109 | v.id = nextViewID() | |
| 1071 | 1110 | registerView(v.id, v) | |
| 1111 | + | C.connect_create_handler(v.native.wv, C.ulong(v.id)) | |
| 1072 | 1112 | ||
| 1073 | 1113 | // Register with global memory manager if monitoring is enabled | |
| 1074 | 1114 | if cfg.Memory.EnableMemoryMonitoring { | |
| @@ -1140,12 +1180,12 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1140 | 1180 | _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { | |
| 1141 | 1181 | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+f'}}))") | |
| 1142 | 1182 | }) | |
| 1143 | - | // Navigation with Alt+Arrow keys: use new keyboard service bridge | |
| 1144 | - | _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { | |
| 1145 | - | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'alt+arrowleft'}}))") | |
| 1183 | + | // Navigation with Ctrl/Cmd + Arrow keys: forward to keyboard service bridge | |
| 1184 | + | _ = v.RegisterKeyboardShortcut("cmdorctrl+ArrowLeft", func() { | |
| 1185 | + | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+arrowleft'}}))") | |
| 1146 | 1186 | }) | |
| 1147 | - | _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { | |
| 1148 | - | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'alt+arrowright'}}))") | |
| 1187 | + | _ = v.RegisterKeyboardShortcut("cmdorctrl+ArrowRight", func() { | |
| 1188 | + | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+arrowright'}}))") | |
| 1149 | 1189 | }) | |
| 1150 | 1190 | // Update window title when page title changes | |
| 1151 | 1191 | C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win))) | |
| @@ -1305,10 +1345,18 @@ func (w *WebView) LoadURL(rawURL string) error { | |||
| 1305 | 1345 | } | |
| 1306 | 1346 | ||
| 1307 | 1347 | func (w *WebView) Show() error { | |
| 1308 | - | if w == nil || w.destroyed || w.native == nil || w.native.win == nil { | |
| 1348 | + | if w == nil || w.destroyed || w.native == nil { | |
| 1349 | + | return ErrNotImplemented | |
| 1350 | + | } | |
| 1351 | + | if w.native.container != nil { | |
| 1352 | + | C.gtk_widget_set_visible(w.native.container, C.gboolean(1)) | |
| 1353 | + | } | |
| 1354 | + | if w.native.view != nil { | |
| 1355 | + | C.gtk_widget_set_visible(w.native.view, C.gboolean(1)) | |
| 1356 | + | } | |
| 1357 | + | if w.native.win == nil { | |
| 1309 | 1358 | return ErrNotImplemented | |
| 1310 | 1359 | } | |
| 1311 | - | C.gtk_widget_set_visible(w.native.view, C.gboolean(1)) | |
| 1312 | 1360 | C.gtk_widget_set_visible(w.native.win, C.gboolean(1)) | |
| 1313 | 1361 | C.gtk_window_present((*C.GtkWindow)(unsafe.Pointer(w.native.win))) | |
| 1314 | 1362 | log.Printf("[webkit] Show window") | |
| @@ -1316,16 +1364,24 @@ func (w *WebView) Show() error { | |||
| 1316 | 1364 | } | |
| 1317 | 1365 | ||
| 1318 | 1366 | func (w *WebView) Hide() error { | |
| 1319 | - | if w == nil || w.destroyed || w.native == nil || w.native.win == nil { | |
| 1367 | + | if w == nil || w.destroyed || w.native == nil { | |
| 1368 | + | return ErrNotImplemented | |
| 1369 | + | } | |
| 1370 | + | if w.native.container != nil { | |
| 1371 | + | C.gtk_widget_set_visible(w.native.container, C.gboolean(0)) | |
| 1372 | + | } | |
| 1373 | + | if w.native.view != nil { | |
| 1374 | + | C.gtk_widget_set_visible(w.native.view, C.gboolean(0)) | |
| 1375 | + | } | |
| 1376 | + | if w.native.win == nil { | |
| 1320 | 1377 | return ErrNotImplemented | |
| 1321 | 1378 | } | |
| 1322 | - | C.gtk_widget_set_visible(w.native.view, C.gboolean(0)) | |
| 1323 | 1379 | C.gtk_widget_set_visible(w.native.win, C.gboolean(0)) | |
| 1324 | 1380 | return nil | |
| 1325 | 1381 | } | |
| 1326 | 1382 | ||
| 1327 | 1383 | func (w *WebView) Destroy() error { | |
| 1328 | - | if w == nil || w.native == nil || w.native.win == nil { | |
| 1384 | + | if w == nil || w.native == nil { | |
| 1329 | 1385 | return ErrNotImplemented | |
| 1330 | 1386 | } | |
| 1331 | 1387 | ||
| @@ -1349,14 +1405,32 @@ func (w *WebView) Destroy() error { | |||
| 1349 | 1405 | w.domBridgeScript = nil | |
| 1350 | 1406 | } | |
| 1351 | 1407 | ||
| 1352 | - | // GTK4: destroy windows via gtk_window_destroy | |
| 1353 | - | C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win))) | |
| 1408 | + | // GTK4: destroy windows via gtk_window_destroy if we created one | |
| 1409 | + | if w.native.win != nil { | |
| 1410 | + | C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win))) | |
| 1411 | + | w.native.win = nil | |
| 1412 | + | if w.window != nil { | |
| 1413 | + | w.window.win = nil | |
| 1414 | + | } | |
| 1415 | + | log.Printf("[webkit] Destroy window") | |
| 1416 | + | } | |
| 1417 | + | ||
| 1418 | + | w.releaseNativeWidgets() | |
| 1354 | 1419 | w.destroyed = true | |
| 1355 | 1420 | unregisterView(w.id) | |
| 1356 | - | log.Printf("[webkit] Destroy window") | |
| 1357 | 1421 | return nil | |
| 1358 | 1422 | } | |
| 1359 | 1423 | ||
| 1424 | + | func (w *WebView) releaseNativeWidgets() { | |
| 1425 | + | if w == nil || w.native == nil { | |
| 1426 | + | return | |
| 1427 | + | } | |
| 1428 | + | if w.native.container != nil { | |
| 1429 | + | C.g_object_unref(C.gpointer(w.native.container)) | |
| 1430 | + | w.native.container = nil | |
| 1431 | + | } | |
| 1432 | + | } | |
| 1433 | + | ||
| 1360 | 1434 | // Window returns the associated native window. | |
| 1361 | 1435 | func (w *WebView) Window() *Window { return w.window } | |
| 1362 | 1436 | ||
| @@ -1409,12 +1483,21 @@ func (w *WebView) ReloadBypassCache() error { | |||
| 1409 | 1483 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | |
| 1410 | 1484 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | |
| 1411 | 1485 | ||
| 1486 | + | func (w *WebView) RegisterPopupHandler(cb func(string) bool) { w.popupHandler = cb } | |
| 1487 | + | ||
| 1412 | 1488 | func (w *WebView) dispatchScriptMessage(payload string) { | |
| 1413 | 1489 | if w != nil && w.msgHandler != nil { | |
| 1414 | 1490 | w.msgHandler(payload) | |
| 1415 | 1491 | } | |
| 1416 | 1492 | } | |
| 1417 | 1493 | ||
| 1494 | + | func (w *WebView) dispatchPopupRequest(uri string) bool { | |
| 1495 | + | if w != nil && w.popupHandler != nil { | |
| 1496 | + | return w.popupHandler(uri) | |
| 1497 | + | } | |
| 1498 | + | return false | |
| 1499 | + | } | |
| 1500 | + | ||
| 1418 | 1501 | // RegisterTitleChangedHandler registers a callback invoked when the page title changes. | |
| 1419 | 1502 | func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb } | |
| 1420 | 1503 | ||
| @@ -1451,6 +1534,72 @@ func (w *WebView) dispatchZoomChanged(level float64) { | |||
| 1451 | 1534 | } | |
| 1452 | 1535 | } | |
| 1453 | 1536 | ||
| 1537 | + | // Widget returns the underlying GtkWidget pointer for the WebView. | |
| 1538 | + | func (w *WebView) Widget() uintptr { | |
| 1539 | + | if w == nil || w.native == nil || w.native.view == nil { | |
| 1540 | + | return 0 | |
| 1541 | + | } | |
| 1542 | + | return uintptr(unsafe.Pointer(w.native.view)) | |
| 1543 | + | } | |
| 1544 | + | ||
| 1545 | + | // RootWidget returns the container widget that should be reparented for pane management. | |
| 1546 | + | func (w *WebView) RootWidget() uintptr { | |
| 1547 | + | if w == nil || w.native == nil || w.native.container == nil { | |
| 1548 | + | return 0 | |
| 1549 | + | } | |
| 1550 | + | return uintptr(unsafe.Pointer(w.native.container)) | |
| 1551 | + | } | |
| 1552 | + | ||
| 1553 | + | // DestroyWindow destroys the toplevel window hosting this WebView, if any. | |
| 1554 | + | func (w *WebView) DestroyWindow() { | |
| 1555 | + | if w == nil || w.native == nil || w.native.win == nil { | |
| 1556 | + | return | |
| 1557 | + | } | |
| 1558 | + | C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win))) | |
| 1559 | + | w.native.win = nil | |
| 1560 | + | if w.window != nil { | |
| 1561 | + | w.window.win = nil | |
| 1562 | + | } | |
| 1563 | + | } | |
| 1564 | + | ||
| 1565 | + | // PrepareForReparenting prepares the WebView widget for being moved to a new parent | |
| 1566 | + | func (w *WebView) PrepareForReparenting() { | |
| 1567 | + | if w == nil || w.native == nil || w.native.container == nil { | |
| 1568 | + | return | |
| 1569 | + | } | |
| 1570 | + | ||
| 1571 | + | // Mark that this view is being reparented | |
| 1572 | + | w.wasReparented = true | |
| 1573 | + | ||
| 1574 | + | // Ensure the widget is visible and properly sized for reparenting | |
| 1575 | + | C.gtk_widget_set_visible(w.native.container, C.gboolean(1)) | |
| 1576 | + | C.gtk_widget_set_size_request(w.native.container, 100, 100) | |
| 1577 | + | C.gtk_widget_queue_draw(w.native.container) | |
| 1578 | + | } | |
| 1579 | + | ||
| 1580 | + | // RefreshAfterReparenting refreshes the WebView after it has been reparented | |
| 1581 | + | func (w *WebView) RefreshAfterReparenting() { | |
| 1582 | + | if w == nil || w.native == nil || w.native.wv == nil { | |
| 1583 | + | return | |
| 1584 | + | } | |
| 1585 | + | ||
| 1586 | + | if w.wasReparented { | |
| 1587 | + | widgetHandle := w.RootWidget() | |
| 1588 | + | parentHandle := WidgetGetParent(widgetHandle) | |
| 1589 | + | if parentHandle == 0 { | |
| 1590 | + | log.Printf("[workspace] RefreshAfterReparenting deferred: widget=%#x parent=%#x url=%s", widgetHandle, parentHandle, w.url) | |
| 1591 | + | return | |
| 1592 | + | } | |
| 1593 | + | log.Printf("[workspace] RefreshAfterReparenting: widget=%#x parent=%#x url=%s", widgetHandle, parentHandle, w.url) | |
| 1594 | + | // Force WebKit to refresh its rendering context by triggering a reload | |
| 1595 | + | // This ensures the rendering context is properly reinitialized after reparenting | |
| 1596 | + | if w.url != "" { | |
| 1597 | + | w.LoadURL(w.url) // Reload current URL to refresh rendering context | |
| 1598 | + | } | |
| 1599 | + | w.wasReparented = false | |
| 1600 | + | } | |
| 1601 | + | } | |
| 1602 | + | ||
| 1454 | 1603 | // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script. | |
| 1455 | 1604 | func (w *WebView) enableUserContentManager(cfg *Config) { | |
| 1456 | 1605 | if w == nil || w.native == nil || w.native.ucm == nil { | |
| @@ -22,6 +22,7 @@ static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); } | |||||
| 22 | // Forward declaration | 22 | // Forward declaration | ||
| 23 | extern void goQuitMainLoop(); | 23 | extern void goQuitMainLoop(); | ||
| 24 | extern void goInvokeHandle(uintptr_t handle); | 24 | extern void goInvokeHandle(uintptr_t handle); | ||
| 25 | + | extern gboolean goOnPopupRequest(unsigned long id, char* uri); | |||
| 25 | 26 | ||||
| 26 | static gboolean invoke_handle_cb(gpointer data) { | 27 | static gboolean invoke_handle_cb(gpointer data) { | ||
| 27 | goInvokeHandle((uintptr_t)data); | 28 | goInvokeHandle((uintptr_t)data); | ||
| @@ -51,20 +52,24 @@ static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view, | |||||
| 51 | GTlsCertificateFlags errors, | 52 | GTlsCertificateFlags errors, | ||
| 52 | gpointer user_data); | 53 | gpointer user_data); | ||
| 53 | 54 | ||||
| 54 | - | // Handle create signal for _blank links - redirect to current window | 55 | + | // Handle create signal for _blank links - delegate to Go workspace manager |
| 55 | static WebKitWebView* on_create_new_window(WebKitWebView* web_view, | 56 | static WebKitWebView* on_create_new_window(WebKitWebView* web_view, | ||
| 56 | WebKitNavigationAction* navigation_action, | 57 | WebKitNavigationAction* navigation_action, | ||
| 57 | gpointer user_data) { | 58 | gpointer user_data) { | ||
| 58 | - | (void)user_data; | 59 | + | unsigned long view_id = (unsigned long)user_data; |
| 60 | + | const gchar* uri = NULL; | |||
| 59 | 61 | ||||
| 60 | // Get the request from navigation action | 62 | // Get the request from navigation action | ||
| 61 | WebKitURIRequest* request = webkit_navigation_action_get_request(navigation_action); | 63 | WebKitURIRequest* request = webkit_navigation_action_get_request(navigation_action); | ||
| 62 | if (request) { | 64 | if (request) { | ||
| 63 | - | const gchar* uri = webkit_uri_request_get_uri(request); | 65 | + | uri = webkit_uri_request_get_uri(request); |
| 64 | - | if (uri) { | 66 | + | } |
| 65 | - | // Load in current window instead of creating new one | 67 | + | |
| 66 | - | webkit_web_view_load_uri(web_view, uri); | 68 | + | gboolean handled = goOnPopupRequest(view_id, (char*)uri); |
| 67 | - | } | 69 | + | |
| 70 | + | if (!handled && uri) { | |||
| 71 | + | // Load in current window instead of creating new one | |||
| 72 | + | webkit_web_view_load_uri(web_view, uri); | |||
| 68 | } | 73 | } | ||
| 69 | 74 | ||||
| 70 | // Return NULL to prevent new window creation | 75 | // Return NULL to prevent new window creation | ||
| @@ -150,9 +155,6 @@ static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const c | |||||
| 150 | GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 }; | 155 | GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 }; | ||
| 151 | webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color); | 156 | webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color); | ||
| 152 | 157 | ||||
| 153 | - | // Handle create signal to redirect _blank links to current window | |||
| 154 | - | g_signal_connect(G_OBJECT(w), "create", G_CALLBACK(on_create_new_window), NULL); | |||
| 155 | - | ||||
| 156 | // Connect TLS error handler | 158 | // Connect TLS error handler | ||
| 157 | connect_tls_error_handler(WEBKIT_WEB_VIEW(w)); | 159 | connect_tls_error_handler(WEBKIT_WEB_VIEW(w)); | ||
| 158 | 160 | ||||
| @@ -160,6 +162,11 @@ static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const c | |||||
| 160 | return w; | 162 | return w; | ||
| 161 | } | 163 | } | ||
| 162 | 164 | ||||
| 165 | + | static void connect_create_handler(WebKitWebView* web_view, unsigned long id) { | |||
| 166 | + | if (!web_view) return; | |||
| 167 | + | g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_new_window), (gpointer)id, NULL, 0); | |||
| 168 | + | } | |||
| 169 | + | ||||
| 163 | static gboolean gtk_prefers_dark() { | 170 | static gboolean gtk_prefers_dark() { | ||
| 164 | // Method 1: Check GNOME desktop interface color-scheme (primary method) | 171 | // Method 1: Check GNOME desktop interface color-scheme (primary method) | ||
| 165 | GSettings* desktop_settings = g_settings_new("org.gnome.desktop.interface"); | 172 | GSettings* desktop_settings = g_settings_new("org.gnome.desktop.interface"); | ||
| @@ -638,6 +645,7 @@ static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* host | |||||
| 638 | // Return response code: 0=Go Back, 1=Proceed Once, 2=Always Accept | 645 | // Return response code: 0=Go Back, 1=Proceed Once, 2=Always Accept | ||
| 639 | return response_data.response; | 646 | return response_data.response; | ||
| 640 | } | 647 | } | ||
| 648 | + | ||||
| 641 | */ | 649 | */ | ||
| 642 | import "C" | 650 | import "C" | ||
| 643 | 651 | ||||
| @@ -655,6 +663,7 @@ import ( | |||||
| 655 | "runtime/cgo" | 663 | "runtime/cgo" | ||
| 656 | "strconv" | 664 | "strconv" | ||
| 657 | "strings" | 665 | "strings" | ||
| 666 | + | "sync" | |||
| 658 | "time" | 667 | "time" | ||
| 659 | "unsafe" | 668 | "unsafe" | ||
| 660 | 669 | ||||
| @@ -791,6 +800,21 @@ const domBridgeTemplate = `(() => { try { | |||||
| 791 | }; | 800 | }; | ||
| 792 | } catch (e) { console.warn('[dumber] unified bridge init failed', e); } })();` | 801 | } catch (e) { console.warn('[dumber] unified bridge init failed', e); } })();` | ||
| 793 | 802 | ||||
| 803 | + | var registerSchemeOnce sync.Once | |||
| 804 | + | ||||
| 805 | + | func registerDumbScheme(ctx *C.WebKitWebContext) { | |||
| 806 | + | if ctx == nil { | |||
| 807 | + | return | |||
| 808 | + | } | |||
| 809 | + | ||||
| 810 | + | registerSchemeOnce.Do(func() { | |||
| 811 | + | sch := C.CString("dumb") | |||
| 812 | + | defer C.free(unsafe.Pointer(sch)) | |||
| 813 | + | C.register_uri_scheme_handler(ctx, sch) | |||
| 814 | + | C.register_uri_scheme_security(ctx, sch) | |||
| 815 | + | }) | |||
| 816 | + | } | |||
| 817 | + | ||||
| 794 | // Helper function to convert bool to int for C interop | 818 | // Helper function to convert bool to int for C interop | ||
| 795 | func boolToInt(b bool) int { | 819 | func boolToInt(b bool) int { | ||
| 796 | if b { | 820 | if b { | ||
| @@ -894,10 +918,11 @@ func storeCertificateDecision(hostname, certificateInfo, decision string, perman | |||||
| 894 | } | 918 | } | ||
| 895 | 919 | ||||
| 896 | type nativeView struct { | 920 | type nativeView struct { | ||
| 897 | - | win *C.GtkWidget | 921 | + | win *C.GtkWidget |
| 898 | - | view *C.GtkWidget | 922 | + | container *C.GtkWidget |
| 899 | - | wv *C.WebKitWebView | 923 | + | view *C.GtkWidget |
| 900 | - | ucm *C.WebKitUserContentManager | 924 | + | wv *C.WebKitWebView |
| 925 | + | ucm *C.WebKitUserContentManager | |||
| 901 | } | 926 | } | ||
| 902 | 927 | ||||
| 903 | type memoryStats struct { | 928 | type memoryStats struct { | ||
| @@ -919,6 +944,7 @@ type WebView struct { | |||||
| 919 | titleHandler func(title string) | 944 | titleHandler func(title string) | ||
| 920 | uriHandler func(uri string) | 945 | uriHandler func(uri string) | ||
| 921 | zoomHandler func(level float64) | 946 | zoomHandler func(level float64) | ||
| 947 | + | popupHandler func(string) bool | |||
| 922 | memStats *memoryStats | 948 | memStats *memoryStats | ||
| 923 | gcTicker *time.Ticker | 949 | gcTicker *time.Ticker | ||
| 924 | gcDone chan struct{} | 950 | gcDone chan struct{} | ||
| @@ -926,6 +952,7 @@ type WebView struct { | |||||
| 926 | useDomZoom bool | 952 | useDomZoom bool | ||
| 927 | domZoomSeed float64 | 953 | domZoomSeed float64 | ||
| 928 | domBridgeScript *C.WebKitUserScript | 954 | domBridgeScript *C.WebKitUserScript | ||
| 955 | + | wasReparented bool // Track if this WebView has been reparented | |||
| 929 | } | 956 | } | ||
| 930 | 957 | ||||
| 931 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | 958 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | ||
| @@ -1018,13 +1045,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1018 | } | 1045 | } | ||
| 1019 | } | 1046 | } | ||
| 1020 | 1047 | ||||
| 1021 | - | // Register custom URI scheme handler for "dumb://" | 1048 | + | registerDumbScheme(ctx) |
| 1022 | - | { | |||
| 1023 | - | sch := C.CString("dumb") | |||
| 1024 | - | C.register_uri_scheme_handler(ctx, sch) | |||
| 1025 | - | C.register_uri_scheme_security(ctx, sch) | |||
| 1026 | - | C.free(unsafe.Pointer(sch)) | |||
| 1027 | - | } | |||
| 1028 | // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session | 1049 | // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session | ||
| 1029 | 1050 | ||||
| 1030 | // Create WebView through g_object_new with a fresh UCM (GTK4/WebKit 6 style) | 1051 | // Create WebView through g_object_new with a fresh UCM (GTK4/WebKit 6 style) | ||
| @@ -1034,17 +1055,35 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1034 | return nil, errors.New("failed to create WebKitWebView") | 1055 | return nil, errors.New("failed to create WebKitWebView") | ||
| 1035 | } | 1056 | } | ||
| 1036 | 1057 | ||||
| 1037 | - | // Create a top-level window to host the view | 1058 | + | var win *C.GtkWidget |
| 1038 | - | win := C.new_window() | 1059 | + | var window *Window |
| 1039 | - | if win == nil { | 1060 | + | container := C.gtk_box_new(C.GtkOrientation(C.GTK_ORIENTATION_VERTICAL), 0) |
| 1040 | - | return nil, errors.New("failed to create GtkWindow") | 1061 | + | if container == nil { |
| 1041 | - | } | 1062 | + | return nil, errors.New("failed to create GtkBox container") |
| 1063 | + | } | |||
| 1064 | + | // Hold a strong reference so the WebView keeps the container alive across reparenting. | |||
| 1065 | + | C.g_object_ref_sink(C.gpointer(container)) | |||
| 1066 | + | C.gtk_widget_set_hexpand(container, C.gboolean(1)) | |||
| 1067 | + | C.gtk_widget_set_vexpand(container, C.gboolean(1)) | |||
| 1068 | + | C.gtk_widget_set_hexpand(viewWidget, C.gboolean(1)) | |||
| 1069 | + | C.gtk_widget_set_vexpand(viewWidget, C.gboolean(1)) | |||
| 1070 | + | C.gtk_box_append((*C.GtkBox)(unsafe.Pointer(container)), viewWidget) | |||
| 1071 | + | ||||
| 1072 | + | // Only create window if requested | |||
| 1073 | + | if cfg.CreateWindow { | |||
| 1074 | + | // Create a top-level window to host the view | |||
| 1075 | + | win = C.new_window() | |||
| 1076 | + | if win == nil { | |||
| 1077 | + | return nil, errors.New("failed to create GtkWindow") | |||
| 1078 | + | } | |||
| 1042 | 1079 | ||||
| 1043 | - | // Pack view widget into the window | 1080 | + | // Pack view widget into the window |
| 1044 | - | // GTK4: containers removed; use gtk_window_set_child | 1081 | + | // GTK4: containers removed; use gtk_window_set_child |
| 1045 | - | C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), viewWidget) | 1082 | + | C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), container) |
| 1046 | - | C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768) | 1083 | + | C.gtk_window_set_default_size((*C.GtkWindow)(unsafe.Pointer(win)), 1024, 768) |
| 1047 | - | C.connect_destroy_quit(win) | 1084 | + | C.connect_destroy_quit(win) |
| 1085 | + | window = &Window{win: win} | |||
| 1086 | + | } | |||
| 1048 | 1087 | ||||
| 1049 | seed := 1.0 | 1088 | seed := 1.0 | ||
| 1050 | if cfg.ZoomDefault > 0 { | 1089 | if cfg.ZoomDefault > 0 { | ||
| @@ -1056,8 +1095,8 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1056 | zoom: 1.0, | 1095 | zoom: 1.0, | ||
| 1057 | useDomZoom: cfg.UseDomZoom, | 1096 | useDomZoom: cfg.UseDomZoom, | ||
| 1058 | domZoomSeed: seed, | 1097 | domZoomSeed: seed, | ||
| 1059 | - | native: &nativeView{win: win, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm}, | 1098 | + | native: &nativeView{win: win, container: container, view: viewWidget, wv: C.as_webview(viewWidget), ucm: createdUcm}, |
| 1060 | - | window: &Window{win: win}, | 1099 | + | window: window, |
| 1061 | memStats: &memoryStats{ | 1100 | memStats: &memoryStats{ | ||
| 1062 | pageLoadCount: 0, | 1101 | pageLoadCount: 0, | ||
| 1063 | lastGCTime: time.Now(), | 1102 | lastGCTime: time.Now(), | ||
| @@ -1069,6 +1108,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1069 | // Assign an ID for accelerator dispatch | 1108 | // Assign an ID for accelerator dispatch | ||
| 1070 | v.id = nextViewID() | 1109 | v.id = nextViewID() | ||
| 1071 | registerView(v.id, v) | 1110 | registerView(v.id, v) | ||
| 1111 | + | C.connect_create_handler(v.native.wv, C.ulong(v.id)) | |||
| 1072 | 1112 | ||||
| 1073 | // Register with global memory manager if monitoring is enabled | 1113 | // Register with global memory manager if monitoring is enabled | ||
| 1074 | if cfg.Memory.EnableMemoryMonitoring { | 1114 | if cfg.Memory.EnableMemoryMonitoring { | ||
| @@ -1140,12 +1180,12 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1140 | _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { | 1180 | _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { | ||
| 1141 | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+f'}}))") | 1181 | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+f'}}))") | ||
| 1142 | }) | 1182 | }) | ||
| 1143 | - | // Navigation with Alt+Arrow keys: use new keyboard service bridge | 1183 | + | // Navigation with Ctrl/Cmd + Arrow keys: forward to keyboard service bridge |
| 1144 | - | _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { | 1184 | + | _ = v.RegisterKeyboardShortcut("cmdorctrl+ArrowLeft", func() { |
| 1145 | - | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'alt+arrowleft'}}))") | 1185 | + | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+arrowleft'}}))") |
| 1146 | }) | 1186 | }) | ||
| 1147 | - | _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { | 1187 | + | _ = v.RegisterKeyboardShortcut("cmdorctrl+ArrowRight", func() { |
| 1148 | - | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'alt+arrowright'}}))") | 1188 | + | _ = v.InjectScript("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'cmdorctrl+arrowright'}}))") |
| 1149 | }) | 1189 | }) | ||
| 1150 | // Update window title when page title changes | 1190 | // Update window title when page title changes | ||
| 1151 | C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win))) | 1191 | C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win))) | ||
| @@ -1305,10 +1345,18 @@ func (w *WebView) LoadURL(rawURL string) error { | |||||
| 1305 | } | 1345 | } | ||
| 1306 | 1346 | ||||
| 1307 | func (w *WebView) Show() error { | 1347 | func (w *WebView) Show() error { | ||
| 1308 | - | if w == nil || w.destroyed || w.native == nil || w.native.win == nil { | 1348 | + | if w == nil || w.destroyed || w.native == nil { |
| 1349 | + | return ErrNotImplemented | |||
| 1350 | + | } | |||
| 1351 | + | if w.native.container != nil { | |||
| 1352 | + | C.gtk_widget_set_visible(w.native.container, C.gboolean(1)) | |||
| 1353 | + | } | |||
| 1354 | + | if w.native.view != nil { | |||
| 1355 | + | C.gtk_widget_set_visible(w.native.view, C.gboolean(1)) | |||
| 1356 | + | } | |||
| 1357 | + | if w.native.win == nil { | |||
| 1309 | return ErrNotImplemented | 1358 | return ErrNotImplemented | ||
| 1310 | } | 1359 | } | ||
| 1311 | - | C.gtk_widget_set_visible(w.native.view, C.gboolean(1)) | |||
| 1312 | C.gtk_widget_set_visible(w.native.win, C.gboolean(1)) | 1360 | C.gtk_widget_set_visible(w.native.win, C.gboolean(1)) | ||
| 1313 | C.gtk_window_present((*C.GtkWindow)(unsafe.Pointer(w.native.win))) | 1361 | C.gtk_window_present((*C.GtkWindow)(unsafe.Pointer(w.native.win))) | ||
| 1314 | log.Printf("[webkit] Show window") | 1362 | log.Printf("[webkit] Show window") | ||
| @@ -1316,16 +1364,24 @@ func (w *WebView) Show() error { | |||||
| 1316 | } | 1364 | } | ||
| 1317 | 1365 | ||||
| 1318 | func (w *WebView) Hide() error { | 1366 | func (w *WebView) Hide() error { | ||
| 1319 | - | if w == nil || w.destroyed || w.native == nil || w.native.win == nil { | 1367 | + | if w == nil || w.destroyed || w.native == nil { |
| 1368 | + | return ErrNotImplemented | |||
| 1369 | + | } | |||
| 1370 | + | if w.native.container != nil { | |||
| 1371 | + | C.gtk_widget_set_visible(w.native.container, C.gboolean(0)) | |||
| 1372 | + | } | |||
| 1373 | + | if w.native.view != nil { | |||
| 1374 | + | C.gtk_widget_set_visible(w.native.view, C.gboolean(0)) | |||
| 1375 | + | } | |||
| 1376 | + | if w.native.win == nil { | |||
| 1320 | return ErrNotImplemented | 1377 | return ErrNotImplemented | ||
| 1321 | } | 1378 | } | ||
| 1322 | - | C.gtk_widget_set_visible(w.native.view, C.gboolean(0)) | |||
| 1323 | C.gtk_widget_set_visible(w.native.win, C.gboolean(0)) | 1379 | C.gtk_widget_set_visible(w.native.win, C.gboolean(0)) | ||
| 1324 | return nil | 1380 | return nil | ||
| 1325 | } | 1381 | } | ||
| 1326 | 1382 | ||||
| 1327 | func (w *WebView) Destroy() error { | 1383 | func (w *WebView) Destroy() error { | ||
| 1328 | - | if w == nil || w.native == nil || w.native.win == nil { | 1384 | + | if w == nil || w.native == nil { |
| 1329 | return ErrNotImplemented | 1385 | return ErrNotImplemented | ||
| 1330 | } | 1386 | } | ||
| 1331 | 1387 | ||||
| @@ -1349,14 +1405,32 @@ func (w *WebView) Destroy() error { | |||||
| 1349 | w.domBridgeScript = nil | 1405 | w.domBridgeScript = nil | ||
| 1350 | } | 1406 | } | ||
| 1351 | 1407 | ||||
| 1352 | - | // GTK4: destroy windows via gtk_window_destroy | 1408 | + | // GTK4: destroy windows via gtk_window_destroy if we created one |
| 1353 | - | C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win))) | 1409 | + | if w.native.win != nil { |
| 1410 | + | C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win))) | |||
| 1411 | + | w.native.win = nil | |||
| 1412 | + | if w.window != nil { | |||
| 1413 | + | w.window.win = nil | |||
| 1414 | + | } | |||
| 1415 | + | log.Printf("[webkit] Destroy window") | |||
| 1416 | + | } | |||
| 1417 | + | ||||
| 1418 | + | w.releaseNativeWidgets() | |||
| 1354 | w.destroyed = true | 1419 | w.destroyed = true | ||
| 1355 | unregisterView(w.id) | 1420 | unregisterView(w.id) | ||
| 1356 | - | log.Printf("[webkit] Destroy window") | |||
| 1357 | return nil | 1421 | return nil | ||
| 1358 | } | 1422 | } | ||
| 1359 | 1423 | ||||
| 1424 | + | func (w *WebView) releaseNativeWidgets() { | |||
| 1425 | + | if w == nil || w.native == nil { | |||
| 1426 | + | return | |||
| 1427 | + | } | |||
| 1428 | + | if w.native.container != nil { | |||
| 1429 | + | C.g_object_unref(C.gpointer(w.native.container)) | |||
| 1430 | + | w.native.container = nil | |||
| 1431 | + | } | |||
| 1432 | + | } | |||
| 1433 | + | ||||
| 1360 | // Window returns the associated native window. | 1434 | // Window returns the associated native window. | ||
| 1361 | func (w *WebView) Window() *Window { return w.window } | 1435 | func (w *WebView) Window() *Window { return w.window } | ||
| 1362 | 1436 | ||||
| @@ -1409,12 +1483,21 @@ func (w *WebView) ReloadBypassCache() error { | |||||
| 1409 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | 1483 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | ||
| 1410 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | 1484 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | ||
| 1411 | 1485 | ||||
| 1486 | + | func (w *WebView) RegisterPopupHandler(cb func(string) bool) { w.popupHandler = cb } | |||
| 1487 | + | ||||
| 1412 | func (w *WebView) dispatchScriptMessage(payload string) { | 1488 | func (w *WebView) dispatchScriptMessage(payload string) { | ||
| 1413 | if w != nil && w.msgHandler != nil { | 1489 | if w != nil && w.msgHandler != nil { | ||
| 1414 | w.msgHandler(payload) | 1490 | w.msgHandler(payload) | ||
| 1415 | } | 1491 | } | ||
| 1416 | } | 1492 | } | ||
| 1417 | 1493 | ||||
| 1494 | + | func (w *WebView) dispatchPopupRequest(uri string) bool { | |||
| 1495 | + | if w != nil && w.popupHandler != nil { | |||
| 1496 | + | return w.popupHandler(uri) | |||
| 1497 | + | } | |||
| 1498 | + | return false | |||
| 1499 | + | } | |||
| 1500 | + | ||||
| 1418 | // RegisterTitleChangedHandler registers a callback invoked when the page title changes. | 1501 | // RegisterTitleChangedHandler registers a callback invoked when the page title changes. | ||
| 1419 | func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb } | 1502 | func (w *WebView) RegisterTitleChangedHandler(cb func(title string)) { w.titleHandler = cb } | ||
| 1420 | 1503 | ||||
| @@ -1451,6 +1534,72 @@ func (w *WebView) dispatchZoomChanged(level float64) { | |||||
| 1451 | } | 1534 | } | ||
| 1452 | } | 1535 | } | ||
| 1453 | 1536 | ||||
| 1537 | + | // Widget returns the underlying GtkWidget pointer for the WebView. | |||
| 1538 | + | func (w *WebView) Widget() uintptr { | |||
| 1539 | + | if w == nil || w.native == nil || w.native.view == nil { | |||
| 1540 | + | return 0 | |||
| 1541 | + | } | |||
| 1542 | + | return uintptr(unsafe.Pointer(w.native.view)) | |||
| 1543 | + | } | |||
| 1544 | + | ||||
| 1545 | + | // RootWidget returns the container widget that should be reparented for pane management. | |||
| 1546 | + | func (w *WebView) RootWidget() uintptr { | |||
| 1547 | + | if w == nil || w.native == nil || w.native.container == nil { | |||
| 1548 | + | return 0 | |||
| 1549 | + | } | |||
| 1550 | + | return uintptr(unsafe.Pointer(w.native.container)) | |||
| 1551 | + | } | |||
| 1552 | + | ||||
| 1553 | + | // DestroyWindow destroys the toplevel window hosting this WebView, if any. | |||
| 1554 | + | func (w *WebView) DestroyWindow() { | |||
| 1555 | + | if w == nil || w.native == nil || w.native.win == nil { | |||
| 1556 | + | return | |||
| 1557 | + | } | |||
| 1558 | + | C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win))) | |||
| 1559 | + | w.native.win = nil | |||
| 1560 | + | if w.window != nil { | |||
| 1561 | + | w.window.win = nil | |||
| 1562 | + | } | |||
| 1563 | + | } | |||
| 1564 | + | ||||
| 1565 | + | // PrepareForReparenting prepares the WebView widget for being moved to a new parent | |||
| 1566 | + | func (w *WebView) PrepareForReparenting() { | |||
| 1567 | + | if w == nil || w.native == nil || w.native.container == nil { | |||
| 1568 | + | return | |||
| 1569 | + | } | |||
| 1570 | + | ||||
| 1571 | + | // Mark that this view is being reparented | |||
| 1572 | + | w.wasReparented = true | |||
| 1573 | + | ||||
| 1574 | + | // Ensure the widget is visible and properly sized for reparenting | |||
| 1575 | + | C.gtk_widget_set_visible(w.native.container, C.gboolean(1)) | |||
| 1576 | + | C.gtk_widget_set_size_request(w.native.container, 100, 100) | |||
| 1577 | + | C.gtk_widget_queue_draw(w.native.container) | |||
| 1578 | + | } | |||
| 1579 | + | ||||
| 1580 | + | // RefreshAfterReparenting refreshes the WebView after it has been reparented | |||
| 1581 | + | func (w *WebView) RefreshAfterReparenting() { | |||
| 1582 | + | if w == nil || w.native == nil || w.native.wv == nil { | |||
| 1583 | + | return | |||
| 1584 | + | } | |||
| 1585 | + | ||||
| 1586 | + | if w.wasReparented { | |||
| 1587 | + | widgetHandle := w.RootWidget() | |||
| 1588 | + | parentHandle := WidgetGetParent(widgetHandle) | |||
| 1589 | + | if parentHandle == 0 { | |||
| 1590 | + | log.Printf("[workspace] RefreshAfterReparenting deferred: widget=%#x parent=%#x url=%s", widgetHandle, parentHandle, w.url) | |||
| 1591 | + | return | |||
| 1592 | + | } | |||
| 1593 | + | log.Printf("[workspace] RefreshAfterReparenting: widget=%#x parent=%#x url=%s", widgetHandle, parentHandle, w.url) | |||
| 1594 | + | // Force WebKit to refresh its rendering context by triggering a reload | |||
| 1595 | + | // This ensures the rendering context is properly reinitialized after reparenting | |||
| 1596 | + | if w.url != "" { | |||
| 1597 | + | w.LoadURL(w.url) // Reload current URL to refresh rendering context | |||
| 1598 | + | } | |||
| 1599 | + | w.wasReparented = false | |||
| 1600 | + | } | |||
| 1601 | + | } | |||
| 1602 | + | ||||
| 1454 | // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script. | 1603 | // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script. | ||
| 1455 | func (w *WebView) enableUserContentManager(cfg *Config) { | 1604 | func (w *WebView) enableUserContentManager(cfg *Config) { | ||
| 1456 | if w == nil || w.native == nil || w.native.ucm == nil { | 1605 | if w == nil || w.native == nil || w.native.ucm == nil { | ||
M pkg/webkit/window.go +5 -0
| @@ -18,3 +18,8 @@ func NewWindow(title string) (*Window, error) { | |||
| 18 | 18 | func (w *Window) SetTitle(title string) { | |
| 19 | 19 | w.Title = title | |
| 20 | 20 | } | |
| 21 | + | ||
| 22 | + | // SetChild is a stubbed window child setter for non-CGO builds. | |
| 23 | + | func (w *Window) SetChild(child uintptr) { | |
| 24 | + | _ = child | |
| 25 | + | } | |
| @@ -18,3 +18,8 @@ func NewWindow(title string) (*Window, error) { | |||||
| 18 | func (w *Window) SetTitle(title string) { | 18 | func (w *Window) SetTitle(title string) { | ||
| 19 | w.Title = title | 19 | w.Title = title | ||
| 20 | } | 20 | } | ||
| 21 | + | ||||
| 22 | + | // SetChild is a stubbed window child setter for non-CGO builds. | |||
| 23 | + | func (w *Window) SetChild(child uintptr) { | |||
| 24 | + | _ = child | |||
| 25 | + | } | |||
M pkg/webkit/window_cgo.go +21 -2
| @@ -10,10 +10,17 @@ package webkit | |||
| 10 | 10 | */ | |
| 11 | 11 | import "C" | |
| 12 | 12 | ||
| 13 | - | import "unsafe" | |
| 13 | + | import ( | |
| 14 | + | "sync" | |
| 15 | + | "unsafe" | |
| 16 | + | ) | |
| 14 | 17 | ||
| 15 | 18 | type Window struct { | |
| 16 | - | win *C.GtkWidget | |
| 19 | + | win *C.GtkWidget | |
| 20 | + | shortcutController *C.GtkShortcutController | |
| 21 | + | shortcutCallbacks map[string]func() | |
| 22 | + | shortcutHandles map[string]uintptr | |
| 23 | + | shortcutMu sync.Mutex | |
| 17 | 24 | } | |
| 18 | 25 | ||
| 19 | 26 | func NewWindow(title string) (*Window, error) { | |
| @@ -39,3 +46,15 @@ func (w *Window) SetTitle(title string) { | |||
| 39 | 46 | defer C.free(unsafe.Pointer(ctitle)) | |
| 40 | 47 | C.gtk_window_set_title((*C.GtkWindow)(unsafe.Pointer(w.win)), (*C.gchar)(ctitle)) | |
| 41 | 48 | } | |
| 49 | + | ||
| 50 | + | // SetChild assigns the given widget as the GtkWindow child. Passing 0 removes the child. | |
| 51 | + | func (w *Window) SetChild(child uintptr) { | |
| 52 | + | if w == nil || w.win == nil { | |
| 53 | + | return | |
| 54 | + | } | |
| 55 | + | var widget *C.GtkWidget | |
| 56 | + | if child != 0 { | |
| 57 | + | widget = (*C.GtkWidget)(unsafe.Pointer(child)) | |
| 58 | + | } | |
| 59 | + | C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(w.win)), widget) | |
| 60 | + | } | |
| @@ -10,10 +10,17 @@ package webkit | |||||
| 10 | */ | 10 | */ | ||
| 11 | import "C" | 11 | import "C" | ||
| 12 | 12 | ||||
| 13 | - | import "unsafe" | 13 | + | import ( |
| 14 | + | "sync" | |||
| 15 | + | "unsafe" | |||
| 16 | + | ) | |||
| 14 | 17 | ||||
| 15 | type Window struct { | 18 | type Window struct { | ||
| 16 | - | win *C.GtkWidget | 19 | + | win *C.GtkWidget |
| 20 | + | shortcutController *C.GtkShortcutController | |||
| 21 | + | shortcutCallbacks map[string]func() | |||
| 22 | + | shortcutHandles map[string]uintptr | |||
| 23 | + | shortcutMu sync.Mutex | |||
| 17 | } | 24 | } | ||
| 18 | 25 | ||||
| 19 | func NewWindow(title string) (*Window, error) { | 26 | func NewWindow(title string) (*Window, error) { | ||
| @@ -39,3 +46,15 @@ func (w *Window) SetTitle(title string) { | |||||
| 39 | defer C.free(unsafe.Pointer(ctitle)) | 46 | defer C.free(unsafe.Pointer(ctitle)) | ||
| 40 | C.gtk_window_set_title((*C.GtkWindow)(unsafe.Pointer(w.win)), (*C.gchar)(ctitle)) | 47 | C.gtk_window_set_title((*C.GtkWindow)(unsafe.Pointer(w.win)), (*C.gchar)(ctitle)) | ||
| 41 | } | 48 | } | ||
| 49 | + | ||||
| 50 | + | // SetChild assigns the given widget as the GtkWindow child. Passing 0 removes the child. | |||
| 51 | + | func (w *Window) SetChild(child uintptr) { | |||
| 52 | + | if w == nil || w.win == nil { | |||
| 53 | + | return | |||
| 54 | + | } | |||
| 55 | + | var widget *C.GtkWidget | |||
| 56 | + | if child != 0 { | |||
| 57 | + | widget = (*C.GtkWidget)(unsafe.Pointer(child)) | |||
| 58 | + | } | |||
| 59 | + | C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(w.win)), widget) | |||
| 60 | + | } | |||