feat(tabs): integrate tab system with browser architecture
- Initialize TabManager in browser.go during createWebView - Set tab manager root container as window content - Add window-level shortcuts for tab mode (Ctrl+T) and actions - Add direct tab switching with Alt+1-9 (tabs 1-9) and Alt+0 (tab 10) - Update workspace manager with helper methods: - GetAllPanes(), GetActivePane(), RestoreFocus(), GetRootWidget() - Maintain app-level workspace/panes compatibility references - Tab manager delegates to active tab's workspace - Each tab creates embedded WebView (CreateWindow=false)
- commit
- 965849af0ece22cab4ae58d42fc2808f12bf3136
- author
- bnema · · 2025-11-17 19:41 UTC
- parent
- 1518120
- signature
- unsigned
4 files · +166 -43
M internal/app/browser/browser.go +23 -20
| @@ -48,7 +48,11 @@ type BrowserApp struct { | |||
| 48 | 48 | navigationController *control.NavigationController | |
| 49 | 49 | clipboardController *control.ClipboardController | |
| 50 | 50 | ||
| 51 | - | // Pane workspace management | |
| 51 | + | // Tab and workspace management | |
| 52 | + | tabManager *TabManager | |
| 53 | + | ||
| 54 | + | // Convenience accessors (delegate to active tab's workspace) | |
| 55 | + | // These maintain compatibility with existing code | |
| 52 | 56 | panes []*BrowserPane | |
| 53 | 57 | activePane *BrowserPane | |
| 54 | 58 | workspace *WorkspaceManager | |
| @@ -248,8 +252,19 @@ func (app *BrowserApp) Run() { | |||
| 248 | 252 | // Continue without content blocking | |
| 249 | 253 | } | |
| 250 | 254 | ||
| 251 | - | // Handle browse command if present | |
| 252 | - | app.navigationController.HandleBrowseCommand() | |
| 255 | + | // Handle browse command if present (must use active tab's navigation controller) | |
| 256 | + | if app.tabManager != nil { | |
| 257 | + | activeTab := app.tabManager.GetActiveTab() | |
| 258 | + | if activeTab != nil && activeTab.workspace != nil { | |
| 259 | + | activePane := activeTab.workspace.GetActivePane() | |
| 260 | + | if activePane != nil && activePane.navigationController != nil { | |
| 261 | + | activePane.navigationController.HandleBrowseCommand() | |
| 262 | + | } | |
| 263 | + | } | |
| 264 | + | } else { | |
| 265 | + | // Fallback to legacy behavior if no tab manager | |
| 266 | + | app.navigationController.HandleBrowseCommand() | |
| 267 | + | } | |
| 253 | 268 | ||
| 254 | 269 | // Setup signal handling | |
| 255 | 270 | app.setupSignalHandling() | |
| @@ -269,23 +284,11 @@ func (app *BrowserApp) cleanup() { | |||
| 269 | 284 | app.windowShortcutHandler = nil | |
| 270 | 285 | } | |
| 271 | 286 | ||
| 272 | - | // Cleanup all panes | |
| 273 | - | if app.panes != nil { | |
| 274 | - | log.Printf("Cleaning up %d panes", len(app.panes)) | |
| 275 | - | for i, pane := range app.panes { | |
| 276 | - | if pane != nil { | |
| 277 | - | log.Printf("Cleaning up pane %d (%s)", i, pane.ID()) | |
| 278 | - | pane.Cleanup() | |
| 279 | - | } | |
| 280 | - | } | |
| 281 | - | app.panes = nil | |
| 282 | - | } | |
| 283 | - | ||
| 284 | - | // Cleanup workspace manager | |
| 285 | - | if app.workspace != nil { | |
| 286 | - | log.Printf("Cleaning up workspace manager") | |
| 287 | - | // WorkspaceManager doesn't have explicit cleanup yet, but we clear the reference | |
| 288 | - | app.workspace = nil | |
| 287 | + | // Cleanup tab manager (which cleans up all tabs and their workspaces) | |
| 288 | + | if app.tabManager != nil { | |
| 289 | + | log.Printf("Cleaning up tab manager") | |
| 290 | + | app.tabManager.Cleanup() | |
| 291 | + | app.tabManager = nil | |
| 289 | 292 | } | |
| 290 | 293 | ||
| 291 | 294 | // Flush all caches to ensure pending writes complete before database closes | |
| @@ -48,7 +48,11 @@ type BrowserApp struct { | |||||
| 48 | navigationController *control.NavigationController | 48 | navigationController *control.NavigationController | ||
| 49 | clipboardController *control.ClipboardController | 49 | clipboardController *control.ClipboardController | ||
| 50 | 50 | ||||
| 51 | - | // Pane workspace management | 51 | + | // Tab and workspace management |
| 52 | + | tabManager *TabManager | |||
| 53 | + | ||||
| 54 | + | // Convenience accessors (delegate to active tab's workspace) | |||
| 55 | + | // These maintain compatibility with existing code | |||
| 52 | panes []*BrowserPane | 56 | panes []*BrowserPane | ||
| 53 | activePane *BrowserPane | 57 | activePane *BrowserPane | ||
| 54 | workspace *WorkspaceManager | 58 | workspace *WorkspaceManager | ||
| @@ -248,8 +252,19 @@ func (app *BrowserApp) Run() { | |||||
| 248 | // Continue without content blocking | 252 | // Continue without content blocking | ||
| 249 | } | 253 | } | ||
| 250 | 254 | ||||
| 251 | - | // Handle browse command if present | 255 | + | // Handle browse command if present (must use active tab's navigation controller) |
| 252 | - | app.navigationController.HandleBrowseCommand() | 256 | + | if app.tabManager != nil { |
| 257 | + | activeTab := app.tabManager.GetActiveTab() | |||
| 258 | + | if activeTab != nil && activeTab.workspace != nil { | |||
| 259 | + | activePane := activeTab.workspace.GetActivePane() | |||
| 260 | + | if activePane != nil && activePane.navigationController != nil { | |||
| 261 | + | activePane.navigationController.HandleBrowseCommand() | |||
| 262 | + | } | |||
| 263 | + | } | |||
| 264 | + | } else { | |||
| 265 | + | // Fallback to legacy behavior if no tab manager | |||
| 266 | + | app.navigationController.HandleBrowseCommand() | |||
| 267 | + | } | |||
| 253 | 268 | ||||
| 254 | // Setup signal handling | 269 | // Setup signal handling | ||
| 255 | app.setupSignalHandling() | 270 | app.setupSignalHandling() | ||
| @@ -269,23 +284,11 @@ func (app *BrowserApp) cleanup() { | |||||
| 269 | app.windowShortcutHandler = nil | 284 | app.windowShortcutHandler = nil | ||
| 270 | } | 285 | } | ||
| 271 | 286 | ||||
| 272 | - | // Cleanup all panes | 287 | + | // Cleanup tab manager (which cleans up all tabs and their workspaces) |
| 273 | - | if app.panes != nil { | 288 | + | if app.tabManager != nil { |
| 274 | - | log.Printf("Cleaning up %d panes", len(app.panes)) | 289 | + | log.Printf("Cleaning up tab manager") |
| 275 | - | for i, pane := range app.panes { | 290 | + | app.tabManager.Cleanup() |
| 276 | - | if pane != nil { | 291 | + | app.tabManager = nil |
| 277 | - | log.Printf("Cleaning up pane %d (%s)", i, pane.ID()) | |||
| 278 | - | pane.Cleanup() | |||
| 279 | - | } | |||
| 280 | - | } | |||
| 281 | - | app.panes = nil | |||
| 282 | - | } | |||
| 283 | - | ||||
| 284 | - | // Cleanup workspace manager | |||
| 285 | - | if app.workspace != nil { | |||
| 286 | - | log.Printf("Cleaning up workspace manager") | |||
| 287 | - | // WorkspaceManager doesn't have explicit cleanup yet, but we clear the reference | |||
| 288 | - | app.workspace = nil | |||
| 289 | } | 292 | } | ||
| 290 | 293 | ||||
| 291 | // Flush all caches to ensure pending writes complete before database closes | 294 | // Flush all caches to ensure pending writes complete before database closes | ||
M internal/app/browser/webview.go +22 -16
| @@ -249,28 +249,34 @@ func (app *BrowserApp) createWebView() error { | |||
| 249 | 249 | app.clipboardController = pane.clipboardController | |
| 250 | 250 | app.messageHandler = pane.messageHandler | |
| 251 | 251 | app.shortcutHandler = pane.shortcutHandler | |
| 252 | - | app.panes = []*BrowserPane{pane} | |
| 253 | - | app.activePane = pane | |
| 254 | 252 | ||
| 255 | - | if app.workspace == nil { | |
| 256 | - | app.workspace = NewWorkspaceManager(app, pane) | |
| 257 | - | // CRITICAL: Register navigation handler for the root webview | |
| 258 | - | // This is needed because buildPane() couldn't register it when workspace was nil | |
| 259 | - | app.workspace.RegisterNavigationHandler(view) | |
| 253 | + | // Initialize tab manager (which manages workspaces) | |
| 254 | + | window := view.Window() | |
| 255 | + | if window != nil && app.tabManager == nil { | |
| 256 | + | app.tabManager = NewTabManager(app, window) | |
| 260 | 257 | ||
| 261 | - | // CRITICAL: Setup popup handling for the root webview | |
| 262 | - | // This must be done after workspace is created so WebKit's create signal is connected | |
| 263 | - | node := app.workspace.GetNodeForWebView(view) | |
| 264 | - | if node != nil { | |
| 265 | - | app.workspace.setupPopupHandling(view, node) | |
| 266 | - | log.Printf("[webview] Setup native popup handling for root WebView ID: %d", view.ID()) | |
| 258 | + | // Determine initial URL | |
| 259 | + | initialURL := "about:blank" | |
| 260 | + | ||
| 261 | + | // Initialize tab system (creates root container with tab bar and first tab) | |
| 262 | + | if err := app.tabManager.Initialize(initialURL); err != nil { | |
| 263 | + | log.Printf("ERROR: failed to initialize tab manager: %v", err) | |
| 264 | + | return err | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | // Set tab manager's root container as window content | |
| 268 | + | rootContainer := app.tabManager.GetRootContainer() | |
| 269 | + | if rootContainer != nil { | |
| 270 | + | window.SetChild(rootContainer) | |
| 271 | + | log.Printf("[tabs] Tab system initialized and set as window content") | |
| 267 | 272 | } else { | |
| 268 | - | log.Printf("[webview] WARNING: Could not find node for root WebView ID: %d", view.ID()) | |
| 273 | + | log.Printf("ERROR: tab manager root container is nil") | |
| 274 | + | return fmt.Errorf("failed to get tab manager root container") | |
| 269 | 275 | } | |
| 270 | 276 | } | |
| 271 | 277 | ||
| 272 | - | // Initialize window-level global shortcuts AFTER workspace is set up | |
| 273 | - | if window := view.Window(); window != nil { | |
| 278 | + | // Initialize window-level global shortcuts AFTER tab manager is set up | |
| 279 | + | if window != nil { | |
| 274 | 280 | app.windowShortcutHandler = NewWindowShortcutHandler(window, app) | |
| 275 | 281 | if app.windowShortcutHandler != nil { | |
| 276 | 282 | log.Printf("Window-level global shortcuts initialized") | |
| @@ -249,28 +249,34 @@ func (app *BrowserApp) createWebView() error { | |||||
| 249 | app.clipboardController = pane.clipboardController | 249 | app.clipboardController = pane.clipboardController | ||
| 250 | app.messageHandler = pane.messageHandler | 250 | app.messageHandler = pane.messageHandler | ||
| 251 | app.shortcutHandler = pane.shortcutHandler | 251 | app.shortcutHandler = pane.shortcutHandler | ||
| 252 | - | app.panes = []*BrowserPane{pane} | |||
| 253 | - | app.activePane = pane | |||
| 254 | 252 | ||||
| 255 | - | if app.workspace == nil { | 253 | + | // Initialize tab manager (which manages workspaces) |
| 256 | - | app.workspace = NewWorkspaceManager(app, pane) | 254 | + | window := view.Window() |
| 257 | - | // CRITICAL: Register navigation handler for the root webview | 255 | + | if window != nil && app.tabManager == nil { |
| 258 | - | // This is needed because buildPane() couldn't register it when workspace was nil | 256 | + | app.tabManager = NewTabManager(app, window) |
| 259 | - | app.workspace.RegisterNavigationHandler(view) | |||
| 260 | 257 | ||||
| 261 | - | // CRITICAL: Setup popup handling for the root webview | 258 | + | // Determine initial URL |
| 262 | - | // This must be done after workspace is created so WebKit's create signal is connected | 259 | + | initialURL := "about:blank" |
| 263 | - | node := app.workspace.GetNodeForWebView(view) | 260 | + | |
| 264 | - | if node != nil { | 261 | + | // Initialize tab system (creates root container with tab bar and first tab) |
| 265 | - | app.workspace.setupPopupHandling(view, node) | 262 | + | if err := app.tabManager.Initialize(initialURL); err != nil { |
| 266 | - | log.Printf("[webview] Setup native popup handling for root WebView ID: %d", view.ID()) | 263 | + | log.Printf("ERROR: failed to initialize tab manager: %v", err) |
| 264 | + | return err | |||
| 265 | + | } | |||
| 266 | + | ||||
| 267 | + | // Set tab manager's root container as window content | |||
| 268 | + | rootContainer := app.tabManager.GetRootContainer() | |||
| 269 | + | if rootContainer != nil { | |||
| 270 | + | window.SetChild(rootContainer) | |||
| 271 | + | log.Printf("[tabs] Tab system initialized and set as window content") | |||
| 267 | } else { | 272 | } else { | ||
| 268 | - | log.Printf("[webview] WARNING: Could not find node for root WebView ID: %d", view.ID()) | 273 | + | log.Printf("ERROR: tab manager root container is nil") |
| 274 | + | return fmt.Errorf("failed to get tab manager root container") | |||
| 269 | } | 275 | } | ||
| 270 | } | 276 | } | ||
| 271 | 277 | ||||
| 272 | - | // Initialize window-level global shortcuts AFTER workspace is set up | 278 | + | // Initialize window-level global shortcuts AFTER tab manager is set up |
| 273 | - | if window := view.Window(); window != nil { | 279 | + | if window != nil { |
| 274 | app.windowShortcutHandler = NewWindowShortcutHandler(window, app) | 280 | app.windowShortcutHandler = NewWindowShortcutHandler(window, app) | ||
| 275 | if app.windowShortcutHandler != nil { | 281 | if app.windowShortcutHandler != nil { | ||
| 276 | log.Printf("Window-level global shortcuts initialized") | 282 | log.Printf("Window-level global shortcuts initialized") | ||
M internal/app/browser/window_shortcuts.go +76 -7
| @@ -60,9 +60,33 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error { | |||
| 60 | 60 | {"ctrl+f", h.handleFindToggle, "Find in page"}, | |
| 61 | 61 | {"ctrl+shift+c", h.handleCopyURL, "Copy URL"}, | |
| 62 | 62 | {"ctrl+shift+p", h.handlePrint, "Print page"}, | |
| 63 | - | {"ctrl+t", h.handleNewTab, "New tab (no-op)"}, | |
| 64 | - | {"ctrl+w", h.handleClosePane, "Close current pane"}, | |
| 65 | 63 | {"F12", h.handleDevTools, "Developer tools"}, | |
| 64 | + | // Pane management | |
| 65 | + | {"ctrl+p", h.handlePaneMode, "Enter pane mode"}, | |
| 66 | + | // Tab management | |
| 67 | + | {"ctrl+t", h.handleTabMode, "Enter tab mode"}, | |
| 68 | + | // Tab mode actions (only active when tab mode is enabled) | |
| 69 | + | {"n", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"}, | |
| 70 | + | {"c", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"}, | |
| 71 | + | {"x", func() { h.handleTabModeAction("close-tab") }, "Close tab (tab mode)"}, | |
| 72 | + | {"l", func() { h.handleTabModeAction("next-tab") }, "Next tab (tab mode)"}, | |
| 73 | + | {"h", func() { h.handleTabModeAction("previous-tab") }, "Previous tab (tab mode)"}, | |
| 74 | + | {"r", func() { h.handleTabModeAction("rename-tab") }, "Rename tab (tab mode)"}, | |
| 75 | + | {"Tab", func() { h.handleTabModeAction("next-tab") }, "Next tab (tab mode)"}, | |
| 76 | + | {"shift+Tab", func() { h.handleTabModeAction("previous-tab") }, "Previous tab (tab mode)"}, | |
| 77 | + | {"Return", func() { h.handleTabModeAction("confirm") }, "Confirm (tab mode)"}, | |
| 78 | + | {"Escape", func() { h.handleTabModeAction("cancel") }, "Cancel (tab mode)"}, | |
| 79 | + | // Direct tab switching | |
| 80 | + | {"alt+1", func() { h.handleDirectTabSwitch(0) }, "Switch to tab 1"}, | |
| 81 | + | {"alt+2", func() { h.handleDirectTabSwitch(1) }, "Switch to tab 2"}, | |
| 82 | + | {"alt+3", func() { h.handleDirectTabSwitch(2) }, "Switch to tab 3"}, | |
| 83 | + | {"alt+4", func() { h.handleDirectTabSwitch(3) }, "Switch to tab 4"}, | |
| 84 | + | {"alt+5", func() { h.handleDirectTabSwitch(4) }, "Switch to tab 5"}, | |
| 85 | + | {"alt+6", func() { h.handleDirectTabSwitch(5) }, "Switch to tab 6"}, | |
| 86 | + | {"alt+7", func() { h.handleDirectTabSwitch(6) }, "Switch to tab 7"}, | |
| 87 | + | {"alt+8", func() { h.handleDirectTabSwitch(7) }, "Switch to tab 8"}, | |
| 88 | + | {"alt+9", func() { h.handleDirectTabSwitch(8) }, "Switch to tab 9"}, | |
| 89 | + | {"alt+0", func() { h.handleDirectTabSwitch(9) }, "Switch to tab 10"}, | |
| 66 | 90 | // Page reload shortcuts | |
| 67 | 91 | {"ctrl+r", h.handleReload, "Reload page"}, | |
| 68 | 92 | {"ctrl+shift+r", h.handleHardReload, "Hard reload (bypass cache)"}, | |
| @@ -409,11 +433,56 @@ func (h *WindowShortcutHandler) handleClosePane() { | |||
| 409 | 433 | } | |
| 410 | 434 | } | |
| 411 | 435 | ||
| 412 | - | // handleNewTab is a no-op handler for Ctrl+T | |
| 413 | - | // This prevents the default browser behavior and allows for future implementation | |
| 414 | - | func (h *WindowShortcutHandler) handleNewTab() { | |
| 415 | - | log.Printf("[window-shortcuts] Ctrl+T pressed (no-op - tabs not yet implemented)") | |
| 416 | - | // TODO: Implement new tab functionality | |
| 436 | + | // handlePaneMode enters pane mode for modal pane management. | |
| 437 | + | func (h *WindowShortcutHandler) handlePaneMode() { | |
| 438 | + | if h.app == nil || h.app.workspace == nil { | |
| 439 | + | log.Printf("[window-shortcuts] Cannot enter pane mode: workspace not available") | |
| 440 | + | return | |
| 441 | + | } | |
| 442 | + | ||
| 443 | + | log.Printf("[window-shortcuts] Entering pane mode") | |
| 444 | + | h.app.workspace.EnterPaneMode() | |
| 445 | + | } | |
| 446 | + | ||
| 447 | + | // handleTabMode enters tab mode for modal tab management. | |
| 448 | + | func (h *WindowShortcutHandler) handleTabMode() { | |
| 449 | + | if h.app == nil || h.app.tabManager == nil { | |
| 450 | + | log.Printf("[window-shortcuts] Cannot enter tab mode: tab manager not available") | |
| 451 | + | return | |
| 452 | + | } | |
| 453 | + | ||
| 454 | + | log.Printf("[window-shortcuts] Entering tab mode") | |
| 455 | + | h.app.tabManager.EnterTabMode() | |
| 456 | + | } | |
| 457 | + | ||
| 458 | + | // handleDirectTabSwitch switches to a specific tab by index (0-based). | |
| 459 | + | func (h *WindowShortcutHandler) handleDirectTabSwitch(index int) { | |
| 460 | + | if h.app == nil || h.app.tabManager == nil { | |
| 461 | + | log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available") | |
| 462 | + | return | |
| 463 | + | } | |
| 464 | + | ||
| 465 | + | log.Printf("[window-shortcuts] Direct tab switch to index %d", index) | |
| 466 | + | if err := h.app.tabManager.SwitchToTab(index); err != nil { | |
| 467 | + | log.Printf("[window-shortcuts] Failed to switch to tab %d: %v", index, err) | |
| 468 | + | } | |
| 469 | + | } | |
| 470 | + | ||
| 471 | + | // handleTabModeAction handles tab mode action keys (n, x, l, h, etc.) | |
| 472 | + | // Only processes actions when tab mode is active, otherwise lets keys pass through to WebView | |
| 473 | + | func (h *WindowShortcutHandler) handleTabModeAction(action string) { | |
| 474 | + | if h.app == nil || h.app.tabManager == nil { | |
| 475 | + | return // Tab manager not available, let key pass through | |
| 476 | + | } | |
| 477 | + | ||
| 478 | + | // Check if tab mode is active | |
| 479 | + | if !h.app.tabManager.IsTabModeActive() { | |
| 480 | + | return // Tab mode not active, let key pass through to WebView | |
| 481 | + | } | |
| 482 | + | ||
| 483 | + | // Tab mode is active, handle the action | |
| 484 | + | log.Printf("[window-shortcuts] Tab mode action: %s", action) | |
| 485 | + | h.app.tabManager.HandleTabAction(action) | |
| 417 | 486 | } | |
| 418 | 487 | ||
| 419 | 488 | // Cleanup releases resources | |
| @@ -60,9 +60,33 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error { | |||||
| 60 | {"ctrl+f", h.handleFindToggle, "Find in page"}, | 60 | {"ctrl+f", h.handleFindToggle, "Find in page"}, | ||
| 61 | {"ctrl+shift+c", h.handleCopyURL, "Copy URL"}, | 61 | {"ctrl+shift+c", h.handleCopyURL, "Copy URL"}, | ||
| 62 | {"ctrl+shift+p", h.handlePrint, "Print page"}, | 62 | {"ctrl+shift+p", h.handlePrint, "Print page"}, | ||
| 63 | - | {"ctrl+t", h.handleNewTab, "New tab (no-op)"}, | |||
| 64 | - | {"ctrl+w", h.handleClosePane, "Close current pane"}, | |||
| 65 | {"F12", h.handleDevTools, "Developer tools"}, | 63 | {"F12", h.handleDevTools, "Developer tools"}, | ||
| 64 | + | // Pane management | |||
| 65 | + | {"ctrl+p", h.handlePaneMode, "Enter pane mode"}, | |||
| 66 | + | // Tab management | |||
| 67 | + | {"ctrl+t", h.handleTabMode, "Enter tab mode"}, | |||
| 68 | + | // Tab mode actions (only active when tab mode is enabled) | |||
| 69 | + | {"n", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"}, | |||
| 70 | + | {"c", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"}, | |||
| 71 | + | {"x", func() { h.handleTabModeAction("close-tab") }, "Close tab (tab mode)"}, | |||
| 72 | + | {"l", func() { h.handleTabModeAction("next-tab") }, "Next tab (tab mode)"}, | |||
| 73 | + | {"h", func() { h.handleTabModeAction("previous-tab") }, "Previous tab (tab mode)"}, | |||
| 74 | + | {"r", func() { h.handleTabModeAction("rename-tab") }, "Rename tab (tab mode)"}, | |||
| 75 | + | {"Tab", func() { h.handleTabModeAction("next-tab") }, "Next tab (tab mode)"}, | |||
| 76 | + | {"shift+Tab", func() { h.handleTabModeAction("previous-tab") }, "Previous tab (tab mode)"}, | |||
| 77 | + | {"Return", func() { h.handleTabModeAction("confirm") }, "Confirm (tab mode)"}, | |||
| 78 | + | {"Escape", func() { h.handleTabModeAction("cancel") }, "Cancel (tab mode)"}, | |||
| 79 | + | // Direct tab switching | |||
| 80 | + | {"alt+1", func() { h.handleDirectTabSwitch(0) }, "Switch to tab 1"}, | |||
| 81 | + | {"alt+2", func() { h.handleDirectTabSwitch(1) }, "Switch to tab 2"}, | |||
| 82 | + | {"alt+3", func() { h.handleDirectTabSwitch(2) }, "Switch to tab 3"}, | |||
| 83 | + | {"alt+4", func() { h.handleDirectTabSwitch(3) }, "Switch to tab 4"}, | |||
| 84 | + | {"alt+5", func() { h.handleDirectTabSwitch(4) }, "Switch to tab 5"}, | |||
| 85 | + | {"alt+6", func() { h.handleDirectTabSwitch(5) }, "Switch to tab 6"}, | |||
| 86 | + | {"alt+7", func() { h.handleDirectTabSwitch(6) }, "Switch to tab 7"}, | |||
| 87 | + | {"alt+8", func() { h.handleDirectTabSwitch(7) }, "Switch to tab 8"}, | |||
| 88 | + | {"alt+9", func() { h.handleDirectTabSwitch(8) }, "Switch to tab 9"}, | |||
| 89 | + | {"alt+0", func() { h.handleDirectTabSwitch(9) }, "Switch to tab 10"}, | |||
| 66 | // Page reload shortcuts | 90 | // Page reload shortcuts | ||
| 67 | {"ctrl+r", h.handleReload, "Reload page"}, | 91 | {"ctrl+r", h.handleReload, "Reload page"}, | ||
| 68 | {"ctrl+shift+r", h.handleHardReload, "Hard reload (bypass cache)"}, | 92 | {"ctrl+shift+r", h.handleHardReload, "Hard reload (bypass cache)"}, | ||
| @@ -409,11 +433,56 @@ func (h *WindowShortcutHandler) handleClosePane() { | |||||
| 409 | } | 433 | } | ||
| 410 | } | 434 | } | ||
| 411 | 435 | ||||
| 412 | - | // handleNewTab is a no-op handler for Ctrl+T | 436 | + | // handlePaneMode enters pane mode for modal pane management. |
| 413 | - | // This prevents the default browser behavior and allows for future implementation | 437 | + | func (h *WindowShortcutHandler) handlePaneMode() { |
| 414 | - | func (h *WindowShortcutHandler) handleNewTab() { | 438 | + | if h.app == nil || h.app.workspace == nil { |
| 415 | - | log.Printf("[window-shortcuts] Ctrl+T pressed (no-op - tabs not yet implemented)") | 439 | + | log.Printf("[window-shortcuts] Cannot enter pane mode: workspace not available") |
| 416 | - | // TODO: Implement new tab functionality | 440 | + | return |
| 441 | + | } | |||
| 442 | + | ||||
| 443 | + | log.Printf("[window-shortcuts] Entering pane mode") | |||
| 444 | + | h.app.workspace.EnterPaneMode() | |||
| 445 | + | } | |||
| 446 | + | ||||
| 447 | + | // handleTabMode enters tab mode for modal tab management. | |||
| 448 | + | func (h *WindowShortcutHandler) handleTabMode() { | |||
| 449 | + | if h.app == nil || h.app.tabManager == nil { | |||
| 450 | + | log.Printf("[window-shortcuts] Cannot enter tab mode: tab manager not available") | |||
| 451 | + | return | |||
| 452 | + | } | |||
| 453 | + | ||||
| 454 | + | log.Printf("[window-shortcuts] Entering tab mode") | |||
| 455 | + | h.app.tabManager.EnterTabMode() | |||
| 456 | + | } | |||
| 457 | + | ||||
| 458 | + | // handleDirectTabSwitch switches to a specific tab by index (0-based). | |||
| 459 | + | func (h *WindowShortcutHandler) handleDirectTabSwitch(index int) { | |||
| 460 | + | if h.app == nil || h.app.tabManager == nil { | |||
| 461 | + | log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available") | |||
| 462 | + | return | |||
| 463 | + | } | |||
| 464 | + | ||||
| 465 | + | log.Printf("[window-shortcuts] Direct tab switch to index %d", index) | |||
| 466 | + | if err := h.app.tabManager.SwitchToTab(index); err != nil { | |||
| 467 | + | log.Printf("[window-shortcuts] Failed to switch to tab %d: %v", index, err) | |||
| 468 | + | } | |||
| 469 | + | } | |||
| 470 | + | ||||
| 471 | + | // handleTabModeAction handles tab mode action keys (n, x, l, h, etc.) | |||
| 472 | + | // Only processes actions when tab mode is active, otherwise lets keys pass through to WebView | |||
| 473 | + | func (h *WindowShortcutHandler) handleTabModeAction(action string) { | |||
| 474 | + | if h.app == nil || h.app.tabManager == nil { | |||
| 475 | + | return // Tab manager not available, let key pass through | |||
| 476 | + | } | |||
| 477 | + | ||||
| 478 | + | // Check if tab mode is active | |||
| 479 | + | if !h.app.tabManager.IsTabModeActive() { | |||
| 480 | + | return // Tab mode not active, let key pass through to WebView | |||
| 481 | + | } | |||
| 482 | + | ||||
| 483 | + | // Tab mode is active, handle the action | |||
| 484 | + | log.Printf("[window-shortcuts] Tab mode action: %s", action) | |||
| 485 | + | h.app.tabManager.HandleTabAction(action) | |||
| 417 | } | 486 | } | ||
| 418 | 487 | ||||
| 419 | // Cleanup releases resources | 488 | // Cleanup releases resources | ||
M internal/app/browser/workspace_manager.go +45 -0
| @@ -542,3 +542,48 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) { | |||
| 542 | 542 | ||
| 543 | 543 | log.Printf("[workspace] Registered navigation handler for webview: %d", webView.ID()) | |
| 544 | 544 | } | |
| 545 | + | ||
| 546 | + | // GetAllPanes returns all BrowserPanes in this workspace. | |
| 547 | + | // This is used by TabManager to update app-level panes reference. | |
| 548 | + | func (wm *WorkspaceManager) GetAllPanes() []*BrowserPane { | |
| 549 | + | panes := make([]*BrowserPane, 0, len(wm.viewToNode)) | |
| 550 | + | for _, node := range wm.viewToNode { | |
| 551 | + | if node.pane != nil { | |
| 552 | + | panes = append(panes, node.pane) | |
| 553 | + | } | |
| 554 | + | } | |
| 555 | + | return panes | |
| 556 | + | } | |
| 557 | + | ||
| 558 | + | // GetActivePane returns the currently active BrowserPane in this workspace. | |
| 559 | + | // This is used by TabManager to update app-level activePane reference. | |
| 560 | + | func (wm *WorkspaceManager) GetActivePane() *BrowserPane { | |
| 561 | + | node := wm.GetActiveNode() | |
| 562 | + | if node != nil && node.pane != nil { | |
| 563 | + | return node.pane | |
| 564 | + | } | |
| 565 | + | return nil | |
| 566 | + | } | |
| 567 | + | ||
| 568 | + | // RestoreFocus restores focus to the active pane in this workspace. | |
| 569 | + | // Called when switching to a tab to ensure the correct pane has focus. | |
| 570 | + | func (wm *WorkspaceManager) RestoreFocus() { | |
| 571 | + | // Focus is managed automatically by the focus state machine | |
| 572 | + | // when the workspace becomes visible | |
| 573 | + | activeNode := wm.GetActiveNode() | |
| 574 | + | if activeNode != nil && activeNode.pane != nil && activeNode.pane.webView != nil { | |
| 575 | + | // Request focus on the active pane | |
| 576 | + | if wm.focusStateMachine != nil { | |
| 577 | + | wm.focusStateMachine.RequestFocus(activeNode, SourceProgrammatic) | |
| 578 | + | } | |
| 579 | + | } | |
| 580 | + | } | |
| 581 | + | ||
| 582 | + | // GetRootWidget returns the root GTK widget for this workspace. | |
| 583 | + | // This is the container that should be added to the tab's content area. | |
| 584 | + | func (wm *WorkspaceManager) GetRootWidget() gtk.Widgetter { | |
| 585 | + | if wm.root != nil && wm.root.container != nil { | |
| 586 | + | return wm.root.container | |
| 587 | + | } | |
| 588 | + | return nil | |
| 589 | + | } | |
| @@ -542,3 +542,48 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) { | |||||
| 542 | 542 | ||||
| 543 | log.Printf("[workspace] Registered navigation handler for webview: %d", webView.ID()) | 543 | log.Printf("[workspace] Registered navigation handler for webview: %d", webView.ID()) | ||
| 544 | } | 544 | } | ||
| 545 | + | ||||
| 546 | + | // GetAllPanes returns all BrowserPanes in this workspace. | |||
| 547 | + | // This is used by TabManager to update app-level panes reference. | |||
| 548 | + | func (wm *WorkspaceManager) GetAllPanes() []*BrowserPane { | |||
| 549 | + | panes := make([]*BrowserPane, 0, len(wm.viewToNode)) | |||
| 550 | + | for _, node := range wm.viewToNode { | |||
| 551 | + | if node.pane != nil { | |||
| 552 | + | panes = append(panes, node.pane) | |||
| 553 | + | } | |||
| 554 | + | } | |||
| 555 | + | return panes | |||
| 556 | + | } | |||
| 557 | + | ||||
| 558 | + | // GetActivePane returns the currently active BrowserPane in this workspace. | |||
| 559 | + | // This is used by TabManager to update app-level activePane reference. | |||
| 560 | + | func (wm *WorkspaceManager) GetActivePane() *BrowserPane { | |||
| 561 | + | node := wm.GetActiveNode() | |||
| 562 | + | if node != nil && node.pane != nil { | |||
| 563 | + | return node.pane | |||
| 564 | + | } | |||
| 565 | + | return nil | |||
| 566 | + | } | |||
| 567 | + | ||||
| 568 | + | // RestoreFocus restores focus to the active pane in this workspace. | |||
| 569 | + | // Called when switching to a tab to ensure the correct pane has focus. | |||
| 570 | + | func (wm *WorkspaceManager) RestoreFocus() { | |||
| 571 | + | // Focus is managed automatically by the focus state machine | |||
| 572 | + | // when the workspace becomes visible | |||
| 573 | + | activeNode := wm.GetActiveNode() | |||
| 574 | + | if activeNode != nil && activeNode.pane != nil && activeNode.pane.webView != nil { | |||
| 575 | + | // Request focus on the active pane | |||
| 576 | + | if wm.focusStateMachine != nil { | |||
| 577 | + | wm.focusStateMachine.RequestFocus(activeNode, SourceProgrammatic) | |||
| 578 | + | } | |||
| 579 | + | } | |||
| 580 | + | } | |||
| 581 | + | ||||
| 582 | + | // GetRootWidget returns the root GTK widget for this workspace. | |||
| 583 | + | // This is the container that should be added to the tab's content area. | |||
| 584 | + | func (wm *WorkspaceManager) GetRootWidget() gtk.Widgetter { | |||
| 585 | + | if wm.root != nil && wm.root.container != nil { | |||
| 586 | + | return wm.root.container | |||
| 587 | + | } | |||
| 588 | + | return nil | |||
| 589 | + | } | |||