fix: localize multi-window input and permission UI
- commit
- 81d21608c4e04259312e4174a096adc4a729ed8e
- author
- brice · · 2026-04-15 02:56 UTC
- parent
- 201b740
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +330 -115
M cmd/dumber/main_test.go +15 -0
| @@ -161,3 +161,18 @@ func TestTryForwardBrowseURLToRunningInstance_ReturnsFalseOnRelayMiss(t *testing | |||
| 161 | 161 | t.Fatal("expected browse URL to remain unforwarded") | |
| 162 | 162 | } | |
| 163 | 163 | } | |
| 164 | + | ||
| 165 | + | func TestTryForwardBrowseURLToRunningInstance_PropagatesError(t *testing.T) { | |
| 166 | + | relay := mocks.NewMockBrowserLaunchRelay(t) | |
| 167 | + | wantErr := errors.New("relay error") | |
| 168 | + | relay.EXPECT().DeliverOpenFreshWindow(context.Background(), "https://example.com").Return(false, wantErr) | |
| 169 | + | ||
| 170 | + | forwarded, err := tryForwardBrowseURLToRunningInstance(context.Background(), relay, "https://example.com") | |
| 171 | + | ||
| 172 | + | if !errors.Is(err, wantErr) { | |
| 173 | + | t.Fatalf("expected error %v, got %v", wantErr, err) | |
| 174 | + | } | |
| 175 | + | if forwarded { | |
| 176 | + | t.Fatal("expected forwarded to be false on error") | |
| 177 | + | } | |
| 178 | + | } | |
| @@ -161,3 +161,18 @@ func TestTryForwardBrowseURLToRunningInstance_ReturnsFalseOnRelayMiss(t *testing | |||||
| 161 | t.Fatal("expected browse URL to remain unforwarded") | 161 | t.Fatal("expected browse URL to remain unforwarded") | ||
| 162 | } | 162 | } | ||
| 163 | } | 163 | } | ||
| 164 | + | ||||
| 165 | + | func TestTryForwardBrowseURLToRunningInstance_PropagatesError(t *testing.T) { | |||
| 166 | + | relay := mocks.NewMockBrowserLaunchRelay(t) | |||
| 167 | + | wantErr := errors.New("relay error") | |||
| 168 | + | relay.EXPECT().DeliverOpenFreshWindow(context.Background(), "https://example.com").Return(false, wantErr) | |||
| 169 | + | ||||
| 170 | + | forwarded, err := tryForwardBrowseURLToRunningInstance(context.Background(), relay, "https://example.com") | |||
| 171 | + | ||||
| 172 | + | if !errors.Is(err, wantErr) { | |||
| 173 | + | t.Fatalf("expected error %v, got %v", wantErr, err) | |||
| 174 | + | } | |||
| 175 | + | if forwarded { | |||
| 176 | + | t.Fatal("expected forwarded to be false on error") | |||
| 177 | + | } | |||
| 178 | + | } | |||
M internal/infrastructure/cef/app_test.go +21 -0
| @@ -122,3 +122,24 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsEmptyBrowseURLAndReturns1 | |||
| 122 | 122 | t.Fatalf("forwarded browse url = %q, want empty string", gotURL) | |
| 123 | 123 | } | |
| 124 | 124 | } | |
| 125 | + | ||
| 126 | + | func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardNonBrowse(t *testing.T) { | |
| 127 | + | t.Parallel() | |
| 128 | + | ||
| 129 | + | called := false | |
| 130 | + | eng := &Engine{} | |
| 131 | + | eng.SetAlreadyRunningAppRelaunchHandler(func(string) { | |
| 132 | + | called = true | |
| 133 | + | }) | |
| 134 | + | ||
| 135 | + | ret := (&dumberBPH{engine: eng}).OnAlreadyRunningAppRelaunch(relaunchCommandLineStub{ | |
| 136 | + | commandLineString: "dumber", | |
| 137 | + | }, "") | |
| 138 | + | ||
| 139 | + | if ret != 1 { | |
| 140 | + | t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret) | |
| 141 | + | } | |
| 142 | + | if called { | |
| 143 | + | t.Fatal("handler should not be called for non-browse relaunch") | |
| 144 | + | } | |
| 145 | + | } | |
| @@ -122,3 +122,24 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsEmptyBrowseURLAndReturns1 | |||||
| 122 | t.Fatalf("forwarded browse url = %q, want empty string", gotURL) | 122 | t.Fatalf("forwarded browse url = %q, want empty string", gotURL) | ||
| 123 | } | 123 | } | ||
| 124 | } | 124 | } | ||
| 125 | + | ||||
| 126 | + | func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardNonBrowse(t *testing.T) { | |||
| 127 | + | t.Parallel() | |||
| 128 | + | ||||
| 129 | + | called := false | |||
| 130 | + | eng := &Engine{} | |||
| 131 | + | eng.SetAlreadyRunningAppRelaunchHandler(func(string) { | |||
| 132 | + | called = true | |||
| 133 | + | }) | |||
| 134 | + | ||||
| 135 | + | ret := (&dumberBPH{engine: eng}).OnAlreadyRunningAppRelaunch(relaunchCommandLineStub{ | |||
| 136 | + | commandLineString: "dumber", | |||
| 137 | + | }, "") | |||
| 138 | + | ||||
| 139 | + | if ret != 1 { | |||
| 140 | + | t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret) | |||
| 141 | + | } | |||
| 142 | + | if called { | |||
| 143 | + | t.Fatal("handler should not be called for non-browse relaunch") | |||
| 144 | + | } | |||
| 145 | + | } | |||
M internal/infrastructure/desktop/browser_launch_relay.go +1 -1
| @@ -198,7 +198,7 @@ func (l *browserLaunchRelayListener) serve(ctx context.Context, opener port.Brow | |||
| 198 | 198 | continue | |
| 199 | 199 | } | |
| 200 | 200 | ||
| 201 | - | l.handleConnection(ctx, conn, opener) | |
| 201 | + | go l.handleConnection(ctx, conn, opener) | |
| 202 | 202 | } | |
| 203 | 203 | } | |
| 204 | 204 | ||
| @@ -198,7 +198,7 @@ func (l *browserLaunchRelayListener) serve(ctx context.Context, opener port.Brow | |||||
| 198 | continue | 198 | continue | ||
| 199 | } | 199 | } | ||
| 200 | 200 | ||||
| 201 | - | l.handleConnection(ctx, conn, opener) | 201 | + | go l.handleConnection(ctx, conn, opener) |
| 202 | } | 202 | } | ||
| 203 | } | 203 | } | ||
| 204 | 204 | ||||
M internal/ui/app.go +187 -99
| @@ -117,8 +117,6 @@ type App struct { | |||
| 117 | 117 | appToaster *component.Toaster | |
| 118 | 118 | // Mode indicator toaster for modal mode notifications (pane, tab, session, resize) | |
| 119 | 119 | modeToaster *component.Toaster | |
| 120 | - | // Top-right WebRTC permission activity indicator | |
| 121 | - | webrtcIndicator *component.WebRTCPermissionIndicator | |
| 122 | 120 | ||
| 123 | 121 | // Session management | |
| 124 | 122 | sessionManager *component.SessionManager | |
| @@ -433,8 +431,7 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow | |||
| 433 | 431 | if w := permPopup.Widget(); w != nil { | |
| 434 | 432 | mainWindow.AddOverlay(w) | |
| 435 | 433 | } | |
| 436 | - | permDialog := dialog.NewPermissionDialog(permPopup) | |
| 437 | - | a.deps.PermissionUC.SetDialogPresenter(permDialog) | |
| 434 | + | browserWindow.permissionDialog = dialog.NewPermissionDialog(permPopup) | |
| 438 | 435 | } | |
| 439 | 436 | } | |
| 440 | 437 | ||
| @@ -443,8 +440,15 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow | |||
| 443 | 440 | if indicator != nil { | |
| 444 | 441 | if w := indicator.Widget(); w != nil { | |
| 445 | 442 | mainWindow.AddOverlay(w) | |
| 446 | - | a.webrtcIndicator = indicator | |
| 447 | 443 | } | |
| 444 | + | browserWindow.webrtcIndicator = indicator | |
| 445 | + | } | |
| 446 | + | a.wireBrowserWindowPermissionIndicator(browserWindow) | |
| 447 | + | if a.kbDispatcher != nil { | |
| 448 | + | a.initBrowserWindowInput(ctx, browserWindow) | |
| 449 | + | } | |
| 450 | + | if a.tabCoord != nil { | |
| 451 | + | a.wireBrowserWindowTabBar(ctx, browserWindow) | |
| 448 | 452 | } | |
| 449 | 453 | ||
| 450 | 454 | // Apply GTK CSS styling from theme manager. | |
| @@ -487,12 +491,47 @@ func (a *App) browserWindowForTab(tabID entity.TabID) *browserWindow { | |||
| 487 | 491 | return a.browserWindowForMainWindow(a.mainWindow) | |
| 488 | 492 | } | |
| 489 | 493 | ||
| 494 | + | func (a *App) browserWindowForPane(paneID entity.PaneID) *browserWindow { | |
| 495 | + | if paneID == "" || a.tabs == nil { | |
| 496 | + | return nil | |
| 497 | + | } | |
| 498 | + | for _, tab := range a.tabs.Tabs { | |
| 499 | + | if tab == nil || tab.Workspace == nil { | |
| 500 | + | continue | |
| 501 | + | } | |
| 502 | + | if tab.Workspace.FindPane(paneID) != nil { | |
| 503 | + | return a.browserWindowForTab(tab.ID) | |
| 504 | + | } | |
| 505 | + | } | |
| 506 | + | return nil | |
| 507 | + | } | |
| 508 | + | ||
| 509 | + | func (a *App) activateBrowserWindow(bw *browserWindow) { | |
| 510 | + | if bw == nil { | |
| 511 | + | return | |
| 512 | + | } | |
| 513 | + | if bw.mainWindow != nil { | |
| 514 | + | a.mainWindow = bw.mainWindow | |
| 515 | + | } | |
| 516 | + | a.keyboardHandler = bw.keyboardHandler | |
| 517 | + | a.globalShortcutHandler = bw.globalShortcutHandler | |
| 518 | + | if a.tabCoord != nil && bw.mainWindow != nil { | |
| 519 | + | a.tabCoord.SetMainWindow(bw.mainWindow) | |
| 520 | + | } | |
| 521 | + | if a.tabs != nil && bw.activeTabID != "" && a.tabs.Find(bw.activeTabID) != nil { | |
| 522 | + | a.tabs.SetActive(bw.activeTabID) | |
| 523 | + | } | |
| 524 | + | } | |
| 525 | + | ||
| 490 | 526 | func (a *App) setBrowserWindowForTab(tabID entity.TabID, bw *browserWindow) { | |
| 491 | 527 | if bw == nil { | |
| 492 | 528 | return | |
| 493 | 529 | } | |
| 494 | 530 | a.ensureWindowForTabMap() | |
| 495 | 531 | a.windowForTab[tabID] = bw | |
| 532 | + | if bw.activeTabID == "" { | |
| 533 | + | bw.activeTabID = tabID | |
| 534 | + | } | |
| 496 | 535 | } | |
| 497 | 536 | ||
| 498 | 537 | func (a *App) startBrowserLaunchRelayListener(ctx context.Context) { | |
| @@ -525,18 +564,24 @@ func (a *App) openFreshWindow(ctx context.Context, url string) error { | |||
| 525 | 564 | a.registerBrowserWindow(created) | |
| 526 | 565 | ||
| 527 | 566 | if a.tabs == nil || a.tabs.Count() == 0 { | |
| 567 | + | a.activateBrowserWindow(created) | |
| 528 | 568 | return nil | |
| 529 | 569 | } | |
| 530 | 570 | ||
| 571 | + | var openErr error | |
| 531 | 572 | if a.tabCoord != nil { | |
| 532 | - | return a.openFreshWindowWithTabCoord(ctx, url, created) | |
| 533 | - | } | |
| 534 | - | ||
| 535 | - | if a.tabsUC == nil { | |
| 573 | + | openErr = a.openFreshWindowWithTabCoord(ctx, url, created) | |
| 574 | + | } else if a.tabsUC == nil { | |
| 575 | + | a.activateBrowserWindow(created) | |
| 536 | 576 | return nil | |
| 577 | + | } else { | |
| 578 | + | openErr = a.openFreshWindowWithTabsUC(ctx, url, created) | |
| 537 | 579 | } | |
| 538 | - | ||
| 539 | - | return a.openFreshWindowWithTabsUC(ctx, url, created) | |
| 580 | + | if openErr != nil { | |
| 581 | + | return openErr | |
| 582 | + | } | |
| 583 | + | a.activateBrowserWindow(created) | |
| 584 | + | return nil | |
| 540 | 585 | } | |
| 541 | 586 | ||
| 542 | 587 | func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error { | |
| @@ -579,6 +624,7 @@ func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, creat | |||
| 579 | 624 | return fmt.Errorf("workspace view not created for tab %s", createdTab.ID) | |
| 580 | 625 | } | |
| 581 | 626 | a.setBrowserWindowForTab(createdTab.ID, created) | |
| 627 | + | created.activeTabID = createdTab.ID | |
| 582 | 628 | if created.mainWindow != nil { | |
| 583 | 629 | created.mainWindow.Show() | |
| 584 | 630 | } | |
| @@ -591,9 +637,14 @@ func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created | |||
| 591 | 637 | InitialURL: url, | |
| 592 | 638 | }) | |
| 593 | 639 | if err != nil { | |
| 640 | + | a.removeBrowserWindow(created.id) | |
| 641 | + | if created.mainWindow != nil { | |
| 642 | + | created.mainWindow.Destroy() | |
| 643 | + | } | |
| 594 | 644 | return err | |
| 595 | 645 | } | |
| 596 | 646 | a.setBrowserWindowForTab(output.Tab.ID, created) | |
| 647 | + | created.activeTabID = output.Tab.ID | |
| 597 | 648 | if a.widgetFactory != nil { | |
| 598 | 649 | a.tabCreationWindow = created | |
| 599 | 650 | defer func() { | |
| @@ -832,37 +883,39 @@ func (a *autoCopyConfigFn) IsAutoCopyEnabled() bool { | |||
| 832 | 883 | } | |
| 833 | 884 | ||
| 834 | 885 | func (a *App) initKeyboardHandler(ctx context.Context) { | |
| 886 | + | bw := a.browserWindowForMainWindow(a.mainWindow) | |
| 887 | + | if bw == nil { | |
| 888 | + | return | |
| 889 | + | } | |
| 890 | + | a.initBrowserWindowInput(ctx, bw) | |
| 891 | + | } | |
| 892 | + | ||
| 893 | + | func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) { | |
| 835 | 894 | log := logging.FromContext(ctx) | |
| 836 | 895 | ||
| 837 | - | if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil { | |
| 896 | + | if bw == nil || bw.mainWindow == nil || a.deps == nil || a.deps.Config == nil || a.kbDispatcher == nil { | |
| 838 | 897 | return | |
| 839 | 898 | } | |
| 840 | 899 | ||
| 841 | - | // Create keyboard handler and wire to dispatcher. | |
| 842 | - | a.keyboardHandler = input.NewKeyboardHandler(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session) | |
| 843 | - | a.keyboardHandler.SetOnAction(func(ctx context.Context, action input.Action) error { | |
| 900 | + | bw.keyboardHandler = input.NewKeyboardHandler(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session) | |
| 901 | + | bw.keyboardHandler.SetOnAction(func(actionCtx context.Context, action input.Action) error { | |
| 902 | + | a.activateBrowserWindow(bw) | |
| 844 | 903 | if action == input.ActionClosePane { | |
| 845 | - | if a.closeAndReleaseActiveFloatingPane(ctx) { | |
| 904 | + | if a.closeAndReleaseActiveFloatingPane(actionCtx) { | |
| 846 | 905 | return nil | |
| 847 | 906 | } | |
| 848 | 907 | } | |
| 849 | - | return a.kbDispatcher.Dispatch(ctx, action) | |
| 908 | + | return a.kbDispatcher.Dispatch(actionCtx, action) | |
| 850 | 909 | }) | |
| 851 | - | a.keyboardHandler.SetOnEscape(func(ctx context.Context) bool { | |
| 852 | - | return a.handleGlobalEscape(ctx) | |
| 910 | + | bw.keyboardHandler.SetOnEscape(func(escapeCtx context.Context) bool { | |
| 911 | + | a.activateBrowserWindow(bw) | |
| 912 | + | return a.handleGlobalEscape(escapeCtx) | |
| 853 | 913 | }) | |
| 854 | - | a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) { | |
| 914 | + | bw.keyboardHandler.SetOnModeChange(func(from, to input.Mode) { | |
| 915 | + | a.activateBrowserWindow(bw) | |
| 855 | 916 | a.handleModeChange(ctx, from, to) | |
| 856 | 917 | }) | |
| 857 | - | // Wire key routing policy. | |
| 858 | - | // Determines how each key event should be handled based on UI state: | |
| 859 | - | // - Overlays visible (session manager, tab picker): pass all keys to widget | |
| 860 | - | // - Omnibox visible: accent detection for text keys, shortcuts for modified keys | |
| 861 | - | // - WebView focused: pass text/dead keys to WebKit IM for native compose, | |
| 862 | - | // intercept shortcut-modified keys (Ctrl+, Alt+) | |
| 863 | - | // - Default: handle through shortcut system | |
| 864 | - | a.keyboardHandler.SetRouteKey(func(kc input.KeyContext) input.KeyRoute { | |
| 865 | - | // Overlays take full keyboard control | |
| 918 | + | bw.keyboardHandler.SetRouteKey(func(kc input.KeyContext) input.KeyRoute { | |
| 866 | 919 | if a.sessionManager != nil && a.sessionManager.IsVisible() { | |
| 867 | 920 | return input.RoutePassToWidget | |
| 868 | 921 | } | |
| @@ -870,19 +923,8 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||
| 870 | 923 | return input.RoutePassToWidget | |
| 871 | 924 | } | |
| 872 | 925 | ||
| 873 | - | wsView := a.activeWorkspaceView() | |
| 874 | - | if wsView == nil { | |
| 875 | - | return input.RouteHandleShortcuts | |
| 876 | - | } | |
| 877 | - | ||
| 878 | - | // Check actual focus rather than visibility: find bar can remain | |
| 879 | - | // visible after focus returns to the WebView, so visibility alone | |
| 880 | - | // would misroute keys. | |
| 881 | 926 | if a.accentFocusProvider != nil { | |
| 882 | 927 | if _, ok := a.accentFocusProvider.GetFocusedInput().(port.EntryInputTarget); ok { | |
| 883 | - | // GTK Entry focused (omnibox or find bar): Alt-modified keys go to | |
| 884 | - | // shortcuts (pane navigation), other keys pass through to the widget's | |
| 885 | - | // own capture-phase key controller (which handles accent detection) | |
| 886 | 928 | if kc.Modifiers&input.ModAlt != 0 { | |
| 887 | 929 | return input.RouteHandleShortcuts | |
| 888 | 930 | } | |
| @@ -890,8 +932,6 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||
| 890 | 932 | } | |
| 891 | 933 | } | |
| 892 | 934 | ||
| 893 | - | // WebView focused: text/dead keys pass through for native IM compose, | |
| 894 | - | // shortcut-modified keys (Ctrl+, Alt+) are handled by shortcut system | |
| 895 | 935 | if input.IsShortcutModified(kc.Modifiers) { | |
| 896 | 936 | return input.RouteHandleShortcuts | |
| 897 | 937 | } | |
| @@ -899,31 +939,43 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||
| 899 | 939 | return input.RoutePassToWidget | |
| 900 | 940 | } | |
| 901 | 941 | ||
| 902 | - | // Non-text, non-shortcut keys (F-keys, arrows, etc.): shortcut system | |
| 903 | 942 | return input.RouteHandleShortcuts | |
| 904 | 943 | }) | |
| 905 | - | // Wire accent handler for dead keys support | |
| 906 | 944 | if a.insertAccentUC != nil { | |
| 907 | - | a.keyboardHandler.SetAccentHandler(a.insertAccentUC) | |
| 945 | + | bw.keyboardHandler.SetAccentHandler(a.insertAccentUC) | |
| 908 | 946 | } | |
| 909 | - | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | |
| 947 | + | bw.keyboardHandler.AttachTo(bw.mainWindow.Window()) | |
| 910 | 948 | ||
| 911 | - | // Create global shortcut handler for Alt+1-9 tab switching. | |
| 912 | - | // This uses GtkShortcutController with GTK_SHORTCUT_SCOPE_GLOBAL | |
| 913 | - | // to intercept shortcuts even when WebView has focus. | |
| 914 | - | a.globalShortcutHandler = input.NewGlobalShortcutHandler( | |
| 949 | + | bw.globalShortcutHandler = input.NewGlobalShortcutHandler( | |
| 915 | 950 | ctx, | |
| 916 | - | a.mainWindow.Window(), | |
| 951 | + | bw.mainWindow.Window(), | |
| 917 | 952 | &a.deps.Config.Workspace, | |
| 918 | 953 | &a.deps.Config.Session, | |
| 919 | - | a.keyboardHandler, | |
| 920 | - | func(ctx context.Context, action input.Action) error { | |
| 921 | - | return a.kbDispatcher.Dispatch(ctx, action) | |
| 954 | + | bw.keyboardHandler, | |
| 955 | + | func(actionCtx context.Context, action input.Action) error { | |
| 956 | + | a.activateBrowserWindow(bw) | |
| 957 | + | return a.kbDispatcher.Dispatch(actionCtx, action) | |
| 922 | 958 | }, | |
| 923 | 959 | ) | |
| 924 | - | if a.globalShortcutHandler == nil { | |
| 925 | - | log.Warn().Msg("global shortcut handler creation failed, shortcuts may not work when WebView has focus") | |
| 960 | + | if bw.globalShortcutHandler == nil { | |
| 961 | + | log.Warn().Str("window_id", bw.id).Msg("global shortcut handler creation failed, shortcuts may not work when WebView has focus") | |
| 926 | 962 | } | |
| 963 | + | ||
| 964 | + | a.activateBrowserWindow(bw) | |
| 965 | + | } | |
| 966 | + | ||
| 967 | + | func (a *App) wireBrowserWindowTabBar(ctx context.Context, bw *browserWindow) { | |
| 968 | + | if bw == nil || bw.mainWindow == nil || bw.mainWindow.TabBar() == nil || a.tabCoord == nil { | |
| 969 | + | return | |
| 970 | + | } | |
| 971 | + | bw.mainWindow.TabBar().SetOnSwitch(func(tabID entity.TabID) { | |
| 972 | + | a.activateBrowserWindow(bw) | |
| 973 | + | if err := a.tabCoord.Switch(ctx, tabID); err != nil { | |
| 974 | + | logging.FromContext(ctx).Error().Err(err).Str("tab_id", string(tabID)).Str("window_id", bw.id).Msg("tab switch failed") | |
| 975 | + | return | |
| 976 | + | } | |
| 977 | + | bw.activeTabID = tabID | |
| 978 | + | }) | |
| 927 | 979 | } | |
| 928 | 980 | ||
| 929 | 981 | // handleAccentKeyPress handles accent key press events for GTK entry widgets | |
| @@ -1849,6 +1901,10 @@ func (a *App) initCoordinators(ctx context.Context) { | |||
| 1849 | 1901 | a.createWorkspaceView(ctx, tab) | |
| 1850 | 1902 | }) | |
| 1851 | 1903 | a.tabCoord.SetOnTabSwitched(func(ctx context.Context, tab *entity.Tab) { | |
| 1904 | + | if bw := a.browserWindowForTab(tab.ID); bw != nil { | |
| 1905 | + | bw.activeTabID = tab.ID | |
| 1906 | + | a.activateBrowserWindow(bw) | |
| 1907 | + | } | |
| 1852 | 1908 | a.switchWorkspaceView(ctx, tab.ID) | |
| 1853 | 1909 | }) | |
| 1854 | 1910 | a.tabCoord.SetOnQuit(a.Quit) | |
| @@ -1858,13 +1914,8 @@ func (a *App) initCoordinators(ctx context.Context) { | |||
| 1858 | 1914 | a.attachPopupToTab(ctx, tabID, pane, wv) | |
| 1859 | 1915 | }) | |
| 1860 | 1916 | ||
| 1861 | - | // Wire tab bar click handling to coordinator | |
| 1862 | - | if a.mainWindow != nil && a.mainWindow.TabBar() != nil { | |
| 1863 | - | a.mainWindow.TabBar().SetOnSwitch(func(tabID entity.TabID) { | |
| 1864 | - | if err := a.tabCoord.Switch(ctx, tabID); err != nil { | |
| 1865 | - | log.Error().Err(err).Str("tab_id", string(tabID)).Msg("tab switch failed") | |
| 1866 | - | } | |
| 1867 | - | }) | |
| 1917 | + | for _, bw := range a.browserWindows { | |
| 1918 | + | a.wireBrowserWindowTabBar(ctx, bw) | |
| 1868 | 1919 | } | |
| 1869 | 1920 | ||
| 1870 | 1921 | // Set fullscreen callback to hide/show tab bar (after tabCoord is initialized) | |
| @@ -1999,6 +2050,9 @@ func (a *App) initCoordinators(ctx context.Context) { | |||
| 1999 | 2050 | a.deps.Config.Workspace.NewPaneURL, | |
| 2000 | 2051 | ) | |
| 2001 | 2052 | a.wireKeyboardActions() | |
| 2053 | + | for _, bw := range a.browserWindows { | |
| 2054 | + | a.initBrowserWindowInput(ctx, bw) | |
| 2055 | + | } | |
| 2002 | 2056 | ||
| 2003 | 2057 | log.Debug().Msg("coordinators initialized") | |
| 2004 | 2058 | } | |
| @@ -2041,33 +2095,73 @@ func (a *App) wireKeyboardActions() { | |||
| 2041 | 2095 | } | |
| 2042 | 2096 | ||
| 2043 | 2097 | func (a *App) wireWebRTCPermissionIndicator() { | |
| 2044 | - | if a.webrtcIndicator == nil || a.contentCoord == nil { | |
| 2098 | + | if a.contentCoord == nil { | |
| 2045 | 2099 | return | |
| 2046 | 2100 | } | |
| 2047 | 2101 | ctx := context.Background() | |
| 2048 | 2102 | if a.deps != nil && a.deps.Ctx != nil { | |
| 2049 | 2103 | ctx = a.deps.Ctx | |
| 2050 | 2104 | } | |
| 2051 | - | log := logging.FromContext(ctx) | |
| 2052 | 2105 | ||
| 2053 | - | a.contentCoord.SetOnPermissionActivity(func(origin string, permTypes []entity.PermissionType, state content.PermissionActivityState) { | |
| 2054 | - | a.webrtcIndicator.SetOrigin(origin) | |
| 2106 | + | a.contentCoord.SetOnPermissionActivity(func( | |
| 2107 | + | paneID entity.PaneID, | |
| 2108 | + | origin string, | |
| 2109 | + | permTypes []entity.PermissionType, | |
| 2110 | + | state content.PermissionActivityState, | |
| 2111 | + | ) { | |
| 2112 | + | bw := a.browserWindowForPane(paneID) | |
| 2113 | + | if bw == nil || bw.webrtcIndicator == nil { | |
| 2114 | + | return | |
| 2115 | + | } | |
| 2116 | + | if state == content.PermissionActivityRequesting && a.deps != nil && a.deps.PermissionUC != nil && bw.permissionDialog != nil { | |
| 2117 | + | a.deps.PermissionUC.SetDialogPresenter(bw.permissionDialog) | |
| 2118 | + | } | |
| 2119 | + | bw.webrtcIndicator.SetOrigin(origin) | |
| 2055 | 2120 | ||
| 2056 | 2121 | switch state { | |
| 2057 | 2122 | case content.PermissionActivityRequesting: | |
| 2058 | - | a.webrtcIndicator.MarkRequesting(permTypes) | |
| 2123 | + | bw.webrtcIndicator.MarkRequesting(permTypes) | |
| 2059 | 2124 | case content.PermissionActivityAllowed: | |
| 2060 | - | a.webrtcIndicator.MarkAllowed(permTypes) | |
| 2125 | + | bw.webrtcIndicator.MarkAllowed(permTypes) | |
| 2061 | 2126 | case content.PermissionActivityBlocked: | |
| 2062 | - | a.webrtcIndicator.MarkBlocked(permTypes) | |
| 2127 | + | bw.webrtcIndicator.MarkBlocked(permTypes) | |
| 2063 | 2128 | } | |
| 2064 | 2129 | ||
| 2065 | 2130 | for _, permType := range permTypes { | |
| 2066 | - | a.syncWebRTCPermissionLockState(ctx, origin, permType) | |
| 2131 | + | a.syncWebRTCPermissionLockState(ctx, bw.webrtcIndicator, origin, permType) | |
| 2067 | 2132 | } | |
| 2068 | 2133 | }) | |
| 2069 | 2134 | ||
| 2070 | - | a.webrtcIndicator.SetOnToggleLock(func(origin string, permType entity.PermissionType, state string, hasStored bool) { | |
| 2135 | + | // Reset the owning window's indicator when that pane navigates away. | |
| 2136 | + | a.contentCoord.SetOnActiveNavigationCommitted(func(paneID entity.PaneID, uri string) { | |
| 2137 | + | bw := a.browserWindowForPane(paneID) | |
| 2138 | + | if bw == nil || bw.webrtcIndicator == nil { | |
| 2139 | + | return | |
| 2140 | + | } | |
| 2141 | + | newOrigin, err := urlutil.ExtractOrigin(uri) | |
| 2142 | + | if err != nil { | |
| 2143 | + | bw.webrtcIndicator.Reset() | |
| 2144 | + | return | |
| 2145 | + | } | |
| 2146 | + | ||
| 2147 | + | currentOrigin := bw.webrtcIndicator.Origin() | |
| 2148 | + | if currentOrigin != "" && currentOrigin != newOrigin { | |
| 2149 | + | bw.webrtcIndicator.Reset() | |
| 2150 | + | } | |
| 2151 | + | }) | |
| 2152 | + | } | |
| 2153 | + | ||
| 2154 | + | func (a *App) wireBrowserWindowPermissionIndicator(bw *browserWindow) { | |
| 2155 | + | if bw == nil || bw.webrtcIndicator == nil { | |
| 2156 | + | return | |
| 2157 | + | } | |
| 2158 | + | ctx := context.Background() | |
| 2159 | + | if a.deps != nil && a.deps.Ctx != nil { | |
| 2160 | + | ctx = a.deps.Ctx | |
| 2161 | + | } | |
| 2162 | + | log := logging.FromContext(ctx) | |
| 2163 | + | ||
| 2164 | + | bw.webrtcIndicator.SetOnToggleLock(func(origin string, permType entity.PermissionType, state string, hasStored bool) { | |
| 2071 | 2165 | if a.deps == nil || a.deps.PermissionUC == nil || origin == "" { | |
| 2072 | 2166 | return | |
| 2073 | 2167 | } | |
| @@ -2092,27 +2186,17 @@ func (a *App) wireWebRTCPermissionIndicator() { | |||
| 2092 | 2186 | } | |
| 2093 | 2187 | } | |
| 2094 | 2188 | ||
| 2095 | - | a.syncWebRTCPermissionLockState(ctx, origin, permType) | |
| 2096 | - | }) | |
| 2097 | - | ||
| 2098 | - | // Reset indicator when the active pane navigates away from the current origin. | |
| 2099 | - | a.contentCoord.SetOnActiveNavigationCommitted(func(uri string) { | |
| 2100 | - | newOrigin, err := urlutil.ExtractOrigin(uri) | |
| 2101 | - | if err != nil { | |
| 2102 | - | // Internal pages (dumb://, about:) — clear the indicator. | |
| 2103 | - | a.webrtcIndicator.Reset() | |
| 2104 | - | return | |
| 2105 | - | } | |
| 2106 | - | ||
| 2107 | - | currentOrigin := a.webrtcIndicator.Origin() | |
| 2108 | - | if currentOrigin != "" && currentOrigin != newOrigin { | |
| 2109 | - | a.webrtcIndicator.Reset() | |
| 2110 | - | } | |
| 2189 | + | a.syncWebRTCPermissionLockState(ctx, bw.webrtcIndicator, origin, permType) | |
| 2111 | 2190 | }) | |
| 2112 | 2191 | } | |
| 2113 | 2192 | ||
| 2114 | - | func (a *App) syncWebRTCPermissionLockState(ctx context.Context, origin string, permType entity.PermissionType) { | |
| 2115 | - | if a.webrtcIndicator == nil || a.deps == nil || a.deps.PermissionUC == nil || origin == "" { | |
| 2193 | + | func (a *App) syncWebRTCPermissionLockState( | |
| 2194 | + | ctx context.Context, | |
| 2195 | + | indicator *component.WebRTCPermissionIndicator, | |
| 2196 | + | origin string, | |
| 2197 | + | permType entity.PermissionType, | |
| 2198 | + | ) { | |
| 2199 | + | if indicator == nil || a.deps == nil || a.deps.PermissionUC == nil || origin == "" { | |
| 2116 | 2200 | return | |
| 2117 | 2201 | } | |
| 2118 | 2202 | ||
| @@ -2127,11 +2211,11 @@ func (a *App) syncWebRTCPermissionLockState(ctx context.Context, origin string, | |||
| 2127 | 2211 | } | |
| 2128 | 2212 | ||
| 2129 | 2213 | if record == nil { | |
| 2130 | - | a.webrtcIndicator.SetStoredDecision(permType, entity.PermissionPrompt, false) | |
| 2214 | + | indicator.SetStoredDecision(permType, entity.PermissionPrompt, false) | |
| 2131 | 2215 | return | |
| 2132 | 2216 | } | |
| 2133 | 2217 | ||
| 2134 | - | a.webrtcIndicator.SetStoredDecision(permType, record.Decision, true) | |
| 2218 | + | indicator.SetStoredDecision(permType, record.Decision, true) | |
| 2135 | 2219 | } | |
| 2136 | 2220 | ||
| 2137 | 2221 | // generateWindowID generates a unique ID for top-level browser windows. | |
| @@ -2535,6 +2619,7 @@ func (a *App) switchWorkspaceView(ctx context.Context, tabID entity.TabID) { | |||
| 2535 | 2619 | ||
| 2536 | 2620 | // Swap content (MainWindow.SetContent now properly removes old content) | |
| 2537 | 2621 | if target := a.browserWindowForTab(tabID); target != nil && target.mainWindow != nil { | |
| 2622 | + | target.activeTabID = tabID | |
| 2538 | 2623 | target.mainWindow.SetContent(gtkWidget) | |
| 2539 | 2624 | } else if a.mainWindow != nil { | |
| 2540 | 2625 | a.mainWindow.SetContent(gtkWidget) | |
| @@ -3411,13 +3496,16 @@ func (a *App) initConfigWatcher(ctx context.Context) { | |||
| 3411 | 3496 | a.deps.OnConfigChange(func() { | |
| 3412 | 3497 | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 3413 | 3498 | a.applyAppearanceConfig(ctx) | |
| 3414 | - | // Reload keyboard shortcuts | |
| 3415 | - | if a.keyboardHandler != nil { | |
| 3416 | - | a.keyboardHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session) | |
| 3417 | - | } | |
| 3418 | - | // Reload global shortcuts (removes stale, re-registers from config) | |
| 3419 | - | if a.globalShortcutHandler != nil { | |
| 3420 | - | a.globalShortcutHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session) | |
| 3499 | + | for _, bw := range a.browserWindows { | |
| 3500 | + | if bw == nil { | |
| 3501 | + | continue | |
| 3502 | + | } | |
| 3503 | + | if bw.keyboardHandler != nil { | |
| 3504 | + | bw.keyboardHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session) | |
| 3505 | + | } | |
| 3506 | + | if bw.globalShortcutHandler != nil { | |
| 3507 | + | bw.globalShortcutHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session) | |
| 3508 | + | } | |
| 3421 | 3509 | } | |
| 3422 | 3510 | return false | |
| 3423 | 3511 | }) | |
| @@ -117,8 +117,6 @@ type App struct { | |||||
| 117 | appToaster *component.Toaster | 117 | appToaster *component.Toaster | ||
| 118 | // Mode indicator toaster for modal mode notifications (pane, tab, session, resize) | 118 | // Mode indicator toaster for modal mode notifications (pane, tab, session, resize) | ||
| 119 | modeToaster *component.Toaster | 119 | modeToaster *component.Toaster | ||
| 120 | - | // Top-right WebRTC permission activity indicator | |||
| 121 | - | webrtcIndicator *component.WebRTCPermissionIndicator | |||
| 122 | 120 | ||||
| 123 | // Session management | 121 | // Session management | ||
| 124 | sessionManager *component.SessionManager | 122 | sessionManager *component.SessionManager | ||
| @@ -433,8 +431,7 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow | |||||
| 433 | if w := permPopup.Widget(); w != nil { | 431 | if w := permPopup.Widget(); w != nil { | ||
| 434 | mainWindow.AddOverlay(w) | 432 | mainWindow.AddOverlay(w) | ||
| 435 | } | 433 | } | ||
| 436 | - | permDialog := dialog.NewPermissionDialog(permPopup) | 434 | + | browserWindow.permissionDialog = dialog.NewPermissionDialog(permPopup) |
| 437 | - | a.deps.PermissionUC.SetDialogPresenter(permDialog) | |||
| 438 | } | 435 | } | ||
| 439 | } | 436 | } | ||
| 440 | 437 | ||||
| @@ -443,8 +440,15 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow | |||||
| 443 | if indicator != nil { | 440 | if indicator != nil { | ||
| 444 | if w := indicator.Widget(); w != nil { | 441 | if w := indicator.Widget(); w != nil { | ||
| 445 | mainWindow.AddOverlay(w) | 442 | mainWindow.AddOverlay(w) | ||
| 446 | - | a.webrtcIndicator = indicator | |||
| 447 | } | 443 | } | ||
| 444 | + | browserWindow.webrtcIndicator = indicator | |||
| 445 | + | } | |||
| 446 | + | a.wireBrowserWindowPermissionIndicator(browserWindow) | |||
| 447 | + | if a.kbDispatcher != nil { | |||
| 448 | + | a.initBrowserWindowInput(ctx, browserWindow) | |||
| 449 | + | } | |||
| 450 | + | if a.tabCoord != nil { | |||
| 451 | + | a.wireBrowserWindowTabBar(ctx, browserWindow) | |||
| 448 | } | 452 | } | ||
| 449 | 453 | ||||
| 450 | // Apply GTK CSS styling from theme manager. | 454 | // Apply GTK CSS styling from theme manager. | ||
| @@ -487,12 +491,47 @@ func (a *App) browserWindowForTab(tabID entity.TabID) *browserWindow { | |||||
| 487 | return a.browserWindowForMainWindow(a.mainWindow) | 491 | return a.browserWindowForMainWindow(a.mainWindow) | ||
| 488 | } | 492 | } | ||
| 489 | 493 | ||||
| 494 | + | func (a *App) browserWindowForPane(paneID entity.PaneID) *browserWindow { | |||
| 495 | + | if paneID == "" || a.tabs == nil { | |||
| 496 | + | return nil | |||
| 497 | + | } | |||
| 498 | + | for _, tab := range a.tabs.Tabs { | |||
| 499 | + | if tab == nil || tab.Workspace == nil { | |||
| 500 | + | continue | |||
| 501 | + | } | |||
| 502 | + | if tab.Workspace.FindPane(paneID) != nil { | |||
| 503 | + | return a.browserWindowForTab(tab.ID) | |||
| 504 | + | } | |||
| 505 | + | } | |||
| 506 | + | return nil | |||
| 507 | + | } | |||
| 508 | + | ||||
| 509 | + | func (a *App) activateBrowserWindow(bw *browserWindow) { | |||
| 510 | + | if bw == nil { | |||
| 511 | + | return | |||
| 512 | + | } | |||
| 513 | + | if bw.mainWindow != nil { | |||
| 514 | + | a.mainWindow = bw.mainWindow | |||
| 515 | + | } | |||
| 516 | + | a.keyboardHandler = bw.keyboardHandler | |||
| 517 | + | a.globalShortcutHandler = bw.globalShortcutHandler | |||
| 518 | + | if a.tabCoord != nil && bw.mainWindow != nil { | |||
| 519 | + | a.tabCoord.SetMainWindow(bw.mainWindow) | |||
| 520 | + | } | |||
| 521 | + | if a.tabs != nil && bw.activeTabID != "" && a.tabs.Find(bw.activeTabID) != nil { | |||
| 522 | + | a.tabs.SetActive(bw.activeTabID) | |||
| 523 | + | } | |||
| 524 | + | } | |||
| 525 | + | ||||
| 490 | func (a *App) setBrowserWindowForTab(tabID entity.TabID, bw *browserWindow) { | 526 | func (a *App) setBrowserWindowForTab(tabID entity.TabID, bw *browserWindow) { | ||
| 491 | if bw == nil { | 527 | if bw == nil { | ||
| 492 | return | 528 | return | ||
| 493 | } | 529 | } | ||
| 494 | a.ensureWindowForTabMap() | 530 | a.ensureWindowForTabMap() | ||
| 495 | a.windowForTab[tabID] = bw | 531 | a.windowForTab[tabID] = bw | ||
| 532 | + | if bw.activeTabID == "" { | |||
| 533 | + | bw.activeTabID = tabID | |||
| 534 | + | } | |||
| 496 | } | 535 | } | ||
| 497 | 536 | ||||
| 498 | func (a *App) startBrowserLaunchRelayListener(ctx context.Context) { | 537 | func (a *App) startBrowserLaunchRelayListener(ctx context.Context) { | ||
| @@ -525,18 +564,24 @@ func (a *App) openFreshWindow(ctx context.Context, url string) error { | |||||
| 525 | a.registerBrowserWindow(created) | 564 | a.registerBrowserWindow(created) | ||
| 526 | 565 | ||||
| 527 | if a.tabs == nil || a.tabs.Count() == 0 { | 566 | if a.tabs == nil || a.tabs.Count() == 0 { | ||
| 567 | + | a.activateBrowserWindow(created) | |||
| 528 | return nil | 568 | return nil | ||
| 529 | } | 569 | } | ||
| 530 | 570 | ||||
| 571 | + | var openErr error | |||
| 531 | if a.tabCoord != nil { | 572 | if a.tabCoord != nil { | ||
| 532 | - | return a.openFreshWindowWithTabCoord(ctx, url, created) | 573 | + | openErr = a.openFreshWindowWithTabCoord(ctx, url, created) |
| 533 | - | } | 574 | + | } else if a.tabsUC == nil { |
| 534 | - | 575 | + | a.activateBrowserWindow(created) | |
| 535 | - | if a.tabsUC == nil { | |||
| 536 | return nil | 576 | return nil | ||
| 577 | + | } else { | |||
| 578 | + | openErr = a.openFreshWindowWithTabsUC(ctx, url, created) | |||
| 537 | } | 579 | } | ||
| 538 | - | 580 | + | if openErr != nil { | |
| 539 | - | return a.openFreshWindowWithTabsUC(ctx, url, created) | 581 | + | return openErr |
| 582 | + | } | |||
| 583 | + | a.activateBrowserWindow(created) | |||
| 584 | + | return nil | |||
| 540 | } | 585 | } | ||
| 541 | 586 | ||||
| 542 | func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error { | 587 | func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error { | ||
| @@ -579,6 +624,7 @@ func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, creat | |||||
| 579 | return fmt.Errorf("workspace view not created for tab %s", createdTab.ID) | 624 | return fmt.Errorf("workspace view not created for tab %s", createdTab.ID) | ||
| 580 | } | 625 | } | ||
| 581 | a.setBrowserWindowForTab(createdTab.ID, created) | 626 | a.setBrowserWindowForTab(createdTab.ID, created) | ||
| 627 | + | created.activeTabID = createdTab.ID | |||
| 582 | if created.mainWindow != nil { | 628 | if created.mainWindow != nil { | ||
| 583 | created.mainWindow.Show() | 629 | created.mainWindow.Show() | ||
| 584 | } | 630 | } | ||
| @@ -591,9 +637,14 @@ func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created | |||||
| 591 | InitialURL: url, | 637 | InitialURL: url, | ||
| 592 | }) | 638 | }) | ||
| 593 | if err != nil { | 639 | if err != nil { | ||
| 640 | + | a.removeBrowserWindow(created.id) | |||
| 641 | + | if created.mainWindow != nil { | |||
| 642 | + | created.mainWindow.Destroy() | |||
| 643 | + | } | |||
| 594 | return err | 644 | return err | ||
| 595 | } | 645 | } | ||
| 596 | a.setBrowserWindowForTab(output.Tab.ID, created) | 646 | a.setBrowserWindowForTab(output.Tab.ID, created) | ||
| 647 | + | created.activeTabID = output.Tab.ID | |||
| 597 | if a.widgetFactory != nil { | 648 | if a.widgetFactory != nil { | ||
| 598 | a.tabCreationWindow = created | 649 | a.tabCreationWindow = created | ||
| 599 | defer func() { | 650 | defer func() { | ||
| @@ -832,37 +883,39 @@ func (a *autoCopyConfigFn) IsAutoCopyEnabled() bool { | |||||
| 832 | } | 883 | } | ||
| 833 | 884 | ||||
| 834 | func (a *App) initKeyboardHandler(ctx context.Context) { | 885 | func (a *App) initKeyboardHandler(ctx context.Context) { | ||
| 886 | + | bw := a.browserWindowForMainWindow(a.mainWindow) | |||
| 887 | + | if bw == nil { | |||
| 888 | + | return | |||
| 889 | + | } | |||
| 890 | + | a.initBrowserWindowInput(ctx, bw) | |||
| 891 | + | } | |||
| 892 | + | ||||
| 893 | + | func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) { | |||
| 835 | log := logging.FromContext(ctx) | 894 | log := logging.FromContext(ctx) | ||
| 836 | 895 | ||||
| 837 | - | if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil { | 896 | + | if bw == nil || bw.mainWindow == nil || a.deps == nil || a.deps.Config == nil || a.kbDispatcher == nil { |
| 838 | return | 897 | return | ||
| 839 | } | 898 | } | ||
| 840 | 899 | ||||
| 841 | - | // Create keyboard handler and wire to dispatcher. | 900 | + | bw.keyboardHandler = input.NewKeyboardHandler(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session) |
| 842 | - | a.keyboardHandler = input.NewKeyboardHandler(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session) | 901 | + | bw.keyboardHandler.SetOnAction(func(actionCtx context.Context, action input.Action) error { |
| 843 | - | a.keyboardHandler.SetOnAction(func(ctx context.Context, action input.Action) error { | 902 | + | a.activateBrowserWindow(bw) |
| 844 | if action == input.ActionClosePane { | 903 | if action == input.ActionClosePane { | ||
| 845 | - | if a.closeAndReleaseActiveFloatingPane(ctx) { | 904 | + | if a.closeAndReleaseActiveFloatingPane(actionCtx) { |
| 846 | return nil | 905 | return nil | ||
| 847 | } | 906 | } | ||
| 848 | } | 907 | } | ||
| 849 | - | return a.kbDispatcher.Dispatch(ctx, action) | 908 | + | return a.kbDispatcher.Dispatch(actionCtx, action) |
| 850 | }) | 909 | }) | ||
| 851 | - | a.keyboardHandler.SetOnEscape(func(ctx context.Context) bool { | 910 | + | bw.keyboardHandler.SetOnEscape(func(escapeCtx context.Context) bool { |
| 852 | - | return a.handleGlobalEscape(ctx) | 911 | + | a.activateBrowserWindow(bw) |
| 912 | + | return a.handleGlobalEscape(escapeCtx) | |||
| 853 | }) | 913 | }) | ||
| 854 | - | a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) { | 914 | + | bw.keyboardHandler.SetOnModeChange(func(from, to input.Mode) { |
| 915 | + | a.activateBrowserWindow(bw) | |||
| 855 | a.handleModeChange(ctx, from, to) | 916 | a.handleModeChange(ctx, from, to) | ||
| 856 | }) | 917 | }) | ||
| 857 | - | // Wire key routing policy. | 918 | + | bw.keyboardHandler.SetRouteKey(func(kc input.KeyContext) input.KeyRoute { |
| 858 | - | // Determines how each key event should be handled based on UI state: | |||
| 859 | - | // - Overlays visible (session manager, tab picker): pass all keys to widget | |||
| 860 | - | // - Omnibox visible: accent detection for text keys, shortcuts for modified keys | |||
| 861 | - | // - WebView focused: pass text/dead keys to WebKit IM for native compose, | |||
| 862 | - | // intercept shortcut-modified keys (Ctrl+, Alt+) | |||
| 863 | - | // - Default: handle through shortcut system | |||
| 864 | - | a.keyboardHandler.SetRouteKey(func(kc input.KeyContext) input.KeyRoute { | |||
| 865 | - | // Overlays take full keyboard control | |||
| 866 | if a.sessionManager != nil && a.sessionManager.IsVisible() { | 919 | if a.sessionManager != nil && a.sessionManager.IsVisible() { | ||
| 867 | return input.RoutePassToWidget | 920 | return input.RoutePassToWidget | ||
| 868 | } | 921 | } | ||
| @@ -870,19 +923,8 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||||
| 870 | return input.RoutePassToWidget | 923 | return input.RoutePassToWidget | ||
| 871 | } | 924 | } | ||
| 872 | 925 | ||||
| 873 | - | wsView := a.activeWorkspaceView() | |||
| 874 | - | if wsView == nil { | |||
| 875 | - | return input.RouteHandleShortcuts | |||
| 876 | - | } | |||
| 877 | - | ||||
| 878 | - | // Check actual focus rather than visibility: find bar can remain | |||
| 879 | - | // visible after focus returns to the WebView, so visibility alone | |||
| 880 | - | // would misroute keys. | |||
| 881 | if a.accentFocusProvider != nil { | 926 | if a.accentFocusProvider != nil { | ||
| 882 | if _, ok := a.accentFocusProvider.GetFocusedInput().(port.EntryInputTarget); ok { | 927 | if _, ok := a.accentFocusProvider.GetFocusedInput().(port.EntryInputTarget); ok { | ||
| 883 | - | // GTK Entry focused (omnibox or find bar): Alt-modified keys go to | |||
| 884 | - | // shortcuts (pane navigation), other keys pass through to the widget's | |||
| 885 | - | // own capture-phase key controller (which handles accent detection) | |||
| 886 | if kc.Modifiers&input.ModAlt != 0 { | 928 | if kc.Modifiers&input.ModAlt != 0 { | ||
| 887 | return input.RouteHandleShortcuts | 929 | return input.RouteHandleShortcuts | ||
| 888 | } | 930 | } | ||
| @@ -890,8 +932,6 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||||
| 890 | } | 932 | } | ||
| 891 | } | 933 | } | ||
| 892 | 934 | ||||
| 893 | - | // WebView focused: text/dead keys pass through for native IM compose, | |||
| 894 | - | // shortcut-modified keys (Ctrl+, Alt+) are handled by shortcut system | |||
| 895 | if input.IsShortcutModified(kc.Modifiers) { | 935 | if input.IsShortcutModified(kc.Modifiers) { | ||
| 896 | return input.RouteHandleShortcuts | 936 | return input.RouteHandleShortcuts | ||
| 897 | } | 937 | } | ||
| @@ -899,31 +939,43 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||||
| 899 | return input.RoutePassToWidget | 939 | return input.RoutePassToWidget | ||
| 900 | } | 940 | } | ||
| 901 | 941 | ||||
| 902 | - | // Non-text, non-shortcut keys (F-keys, arrows, etc.): shortcut system | |||
| 903 | return input.RouteHandleShortcuts | 942 | return input.RouteHandleShortcuts | ||
| 904 | }) | 943 | }) | ||
| 905 | - | // Wire accent handler for dead keys support | |||
| 906 | if a.insertAccentUC != nil { | 944 | if a.insertAccentUC != nil { | ||
| 907 | - | a.keyboardHandler.SetAccentHandler(a.insertAccentUC) | 945 | + | bw.keyboardHandler.SetAccentHandler(a.insertAccentUC) |
| 908 | } | 946 | } | ||
| 909 | - | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | 947 | + | bw.keyboardHandler.AttachTo(bw.mainWindow.Window()) |
| 910 | 948 | ||||
| 911 | - | // Create global shortcut handler for Alt+1-9 tab switching. | 949 | + | bw.globalShortcutHandler = input.NewGlobalShortcutHandler( |
| 912 | - | // This uses GtkShortcutController with GTK_SHORTCUT_SCOPE_GLOBAL | |||
| 913 | - | // to intercept shortcuts even when WebView has focus. | |||
| 914 | - | a.globalShortcutHandler = input.NewGlobalShortcutHandler( | |||
| 915 | ctx, | 950 | ctx, | ||
| 916 | - | a.mainWindow.Window(), | 951 | + | bw.mainWindow.Window(), |
| 917 | &a.deps.Config.Workspace, | 952 | &a.deps.Config.Workspace, | ||
| 918 | &a.deps.Config.Session, | 953 | &a.deps.Config.Session, | ||
| 919 | - | a.keyboardHandler, | 954 | + | bw.keyboardHandler, |
| 920 | - | func(ctx context.Context, action input.Action) error { | 955 | + | func(actionCtx context.Context, action input.Action) error { |
| 921 | - | return a.kbDispatcher.Dispatch(ctx, action) | 956 | + | a.activateBrowserWindow(bw) |
| 957 | + | return a.kbDispatcher.Dispatch(actionCtx, action) | |||
| 922 | }, | 958 | }, | ||
| 923 | ) | 959 | ) | ||
| 924 | - | if a.globalShortcutHandler == nil { | 960 | + | if bw.globalShortcutHandler == nil { |
| 925 | - | log.Warn().Msg("global shortcut handler creation failed, shortcuts may not work when WebView has focus") | 961 | + | log.Warn().Str("window_id", bw.id).Msg("global shortcut handler creation failed, shortcuts may not work when WebView has focus") |
| 926 | } | 962 | } | ||
| 963 | + | ||||
| 964 | + | a.activateBrowserWindow(bw) | |||
| 965 | + | } | |||
| 966 | + | ||||
| 967 | + | func (a *App) wireBrowserWindowTabBar(ctx context.Context, bw *browserWindow) { | |||
| 968 | + | if bw == nil || bw.mainWindow == nil || bw.mainWindow.TabBar() == nil || a.tabCoord == nil { | |||
| 969 | + | return | |||
| 970 | + | } | |||
| 971 | + | bw.mainWindow.TabBar().SetOnSwitch(func(tabID entity.TabID) { | |||
| 972 | + | a.activateBrowserWindow(bw) | |||
| 973 | + | if err := a.tabCoord.Switch(ctx, tabID); err != nil { | |||
| 974 | + | logging.FromContext(ctx).Error().Err(err).Str("tab_id", string(tabID)).Str("window_id", bw.id).Msg("tab switch failed") | |||
| 975 | + | return | |||
| 976 | + | } | |||
| 977 | + | bw.activeTabID = tabID | |||
| 978 | + | }) | |||
| 927 | } | 979 | } | ||
| 928 | 980 | ||||
| 929 | // handleAccentKeyPress handles accent key press events for GTK entry widgets | 981 | // handleAccentKeyPress handles accent key press events for GTK entry widgets | ||
| @@ -1849,6 +1901,10 @@ func (a *App) initCoordinators(ctx context.Context) { | |||||
| 1849 | a.createWorkspaceView(ctx, tab) | 1901 | a.createWorkspaceView(ctx, tab) | ||
| 1850 | }) | 1902 | }) | ||
| 1851 | a.tabCoord.SetOnTabSwitched(func(ctx context.Context, tab *entity.Tab) { | 1903 | a.tabCoord.SetOnTabSwitched(func(ctx context.Context, tab *entity.Tab) { | ||
| 1904 | + | if bw := a.browserWindowForTab(tab.ID); bw != nil { | |||
| 1905 | + | bw.activeTabID = tab.ID | |||
| 1906 | + | a.activateBrowserWindow(bw) | |||
| 1907 | + | } | |||
| 1852 | a.switchWorkspaceView(ctx, tab.ID) | 1908 | a.switchWorkspaceView(ctx, tab.ID) | ||
| 1853 | }) | 1909 | }) | ||
| 1854 | a.tabCoord.SetOnQuit(a.Quit) | 1910 | a.tabCoord.SetOnQuit(a.Quit) | ||
| @@ -1858,13 +1914,8 @@ func (a *App) initCoordinators(ctx context.Context) { | |||||
| 1858 | a.attachPopupToTab(ctx, tabID, pane, wv) | 1914 | a.attachPopupToTab(ctx, tabID, pane, wv) | ||
| 1859 | }) | 1915 | }) | ||
| 1860 | 1916 | ||||
| 1861 | - | // Wire tab bar click handling to coordinator | 1917 | + | for _, bw := range a.browserWindows { |
| 1862 | - | if a.mainWindow != nil && a.mainWindow.TabBar() != nil { | 1918 | + | a.wireBrowserWindowTabBar(ctx, bw) |
| 1863 | - | a.mainWindow.TabBar().SetOnSwitch(func(tabID entity.TabID) { | |||
| 1864 | - | if err := a.tabCoord.Switch(ctx, tabID); err != nil { | |||
| 1865 | - | log.Error().Err(err).Str("tab_id", string(tabID)).Msg("tab switch failed") | |||
| 1866 | - | } | |||
| 1867 | - | }) | |||
| 1868 | } | 1919 | } | ||
| 1869 | 1920 | ||||
| 1870 | // Set fullscreen callback to hide/show tab bar (after tabCoord is initialized) | 1921 | // Set fullscreen callback to hide/show tab bar (after tabCoord is initialized) | ||
| @@ -1999,6 +2050,9 @@ func (a *App) initCoordinators(ctx context.Context) { | |||||
| 1999 | a.deps.Config.Workspace.NewPaneURL, | 2050 | a.deps.Config.Workspace.NewPaneURL, | ||
| 2000 | ) | 2051 | ) | ||
| 2001 | a.wireKeyboardActions() | 2052 | a.wireKeyboardActions() | ||
| 2053 | + | for _, bw := range a.browserWindows { | |||
| 2054 | + | a.initBrowserWindowInput(ctx, bw) | |||
| 2055 | + | } | |||
| 2002 | 2056 | ||||
| 2003 | log.Debug().Msg("coordinators initialized") | 2057 | log.Debug().Msg("coordinators initialized") | ||
| 2004 | } | 2058 | } | ||
| @@ -2041,33 +2095,73 @@ func (a *App) wireKeyboardActions() { | |||||
| 2041 | } | 2095 | } | ||
| 2042 | 2096 | ||||
| 2043 | func (a *App) wireWebRTCPermissionIndicator() { | 2097 | func (a *App) wireWebRTCPermissionIndicator() { | ||
| 2044 | - | if a.webrtcIndicator == nil || a.contentCoord == nil { | 2098 | + | if a.contentCoord == nil { |
| 2045 | return | 2099 | return | ||
| 2046 | } | 2100 | } | ||
| 2047 | ctx := context.Background() | 2101 | ctx := context.Background() | ||
| 2048 | if a.deps != nil && a.deps.Ctx != nil { | 2102 | if a.deps != nil && a.deps.Ctx != nil { | ||
| 2049 | ctx = a.deps.Ctx | 2103 | ctx = a.deps.Ctx | ||
| 2050 | } | 2104 | } | ||
| 2051 | - | log := logging.FromContext(ctx) | |||
| 2052 | 2105 | ||||
| 2053 | - | a.contentCoord.SetOnPermissionActivity(func(origin string, permTypes []entity.PermissionType, state content.PermissionActivityState) { | 2106 | + | a.contentCoord.SetOnPermissionActivity(func( |
| 2054 | - | a.webrtcIndicator.SetOrigin(origin) | 2107 | + | paneID entity.PaneID, |
| 2108 | + | origin string, | |||
| 2109 | + | permTypes []entity.PermissionType, | |||
| 2110 | + | state content.PermissionActivityState, | |||
| 2111 | + | ) { | |||
| 2112 | + | bw := a.browserWindowForPane(paneID) | |||
| 2113 | + | if bw == nil || bw.webrtcIndicator == nil { | |||
| 2114 | + | return | |||
| 2115 | + | } | |||
| 2116 | + | if state == content.PermissionActivityRequesting && a.deps != nil && a.deps.PermissionUC != nil && bw.permissionDialog != nil { | |||
| 2117 | + | a.deps.PermissionUC.SetDialogPresenter(bw.permissionDialog) | |||
| 2118 | + | } | |||
| 2119 | + | bw.webrtcIndicator.SetOrigin(origin) | |||
| 2055 | 2120 | ||||
| 2056 | switch state { | 2121 | switch state { | ||
| 2057 | case content.PermissionActivityRequesting: | 2122 | case content.PermissionActivityRequesting: | ||
| 2058 | - | a.webrtcIndicator.MarkRequesting(permTypes) | 2123 | + | bw.webrtcIndicator.MarkRequesting(permTypes) |
| 2059 | case content.PermissionActivityAllowed: | 2124 | case content.PermissionActivityAllowed: | ||
| 2060 | - | a.webrtcIndicator.MarkAllowed(permTypes) | 2125 | + | bw.webrtcIndicator.MarkAllowed(permTypes) |
| 2061 | case content.PermissionActivityBlocked: | 2126 | case content.PermissionActivityBlocked: | ||
| 2062 | - | a.webrtcIndicator.MarkBlocked(permTypes) | 2127 | + | bw.webrtcIndicator.MarkBlocked(permTypes) |
| 2063 | } | 2128 | } | ||
| 2064 | 2129 | ||||
| 2065 | for _, permType := range permTypes { | 2130 | for _, permType := range permTypes { | ||
| 2066 | - | a.syncWebRTCPermissionLockState(ctx, origin, permType) | 2131 | + | a.syncWebRTCPermissionLockState(ctx, bw.webrtcIndicator, origin, permType) |
| 2067 | } | 2132 | } | ||
| 2068 | }) | 2133 | }) | ||
| 2069 | 2134 | ||||
| 2070 | - | a.webrtcIndicator.SetOnToggleLock(func(origin string, permType entity.PermissionType, state string, hasStored bool) { | 2135 | + | // Reset the owning window's indicator when that pane navigates away. |
| 2136 | + | a.contentCoord.SetOnActiveNavigationCommitted(func(paneID entity.PaneID, uri string) { | |||
| 2137 | + | bw := a.browserWindowForPane(paneID) | |||
| 2138 | + | if bw == nil || bw.webrtcIndicator == nil { | |||
| 2139 | + | return | |||
| 2140 | + | } | |||
| 2141 | + | newOrigin, err := urlutil.ExtractOrigin(uri) | |||
| 2142 | + | if err != nil { | |||
| 2143 | + | bw.webrtcIndicator.Reset() | |||
| 2144 | + | return | |||
| 2145 | + | } | |||
| 2146 | + | ||||
| 2147 | + | currentOrigin := bw.webrtcIndicator.Origin() | |||
| 2148 | + | if currentOrigin != "" && currentOrigin != newOrigin { | |||
| 2149 | + | bw.webrtcIndicator.Reset() | |||
| 2150 | + | } | |||
| 2151 | + | }) | |||
| 2152 | + | } | |||
| 2153 | + | ||||
| 2154 | + | func (a *App) wireBrowserWindowPermissionIndicator(bw *browserWindow) { | |||
| 2155 | + | if bw == nil || bw.webrtcIndicator == nil { | |||
| 2156 | + | return | |||
| 2157 | + | } | |||
| 2158 | + | ctx := context.Background() | |||
| 2159 | + | if a.deps != nil && a.deps.Ctx != nil { | |||
| 2160 | + | ctx = a.deps.Ctx | |||
| 2161 | + | } | |||
| 2162 | + | log := logging.FromContext(ctx) | |||
| 2163 | + | ||||
| 2164 | + | bw.webrtcIndicator.SetOnToggleLock(func(origin string, permType entity.PermissionType, state string, hasStored bool) { | |||
| 2071 | if a.deps == nil || a.deps.PermissionUC == nil || origin == "" { | 2165 | if a.deps == nil || a.deps.PermissionUC == nil || origin == "" { | ||
| 2072 | return | 2166 | return | ||
| 2073 | } | 2167 | } | ||
| @@ -2092,27 +2186,17 @@ func (a *App) wireWebRTCPermissionIndicator() { | |||||
| 2092 | } | 2186 | } | ||
| 2093 | } | 2187 | } | ||
| 2094 | 2188 | ||||
| 2095 | - | a.syncWebRTCPermissionLockState(ctx, origin, permType) | 2189 | + | a.syncWebRTCPermissionLockState(ctx, bw.webrtcIndicator, origin, permType) |
| 2096 | - | }) | |||
| 2097 | - | ||||
| 2098 | - | // Reset indicator when the active pane navigates away from the current origin. | |||
| 2099 | - | a.contentCoord.SetOnActiveNavigationCommitted(func(uri string) { | |||
| 2100 | - | newOrigin, err := urlutil.ExtractOrigin(uri) | |||
| 2101 | - | if err != nil { | |||
| 2102 | - | // Internal pages (dumb://, about:) — clear the indicator. | |||
| 2103 | - | a.webrtcIndicator.Reset() | |||
| 2104 | - | return | |||
| 2105 | - | } | |||
| 2106 | - | ||||
| 2107 | - | currentOrigin := a.webrtcIndicator.Origin() | |||
| 2108 | - | if currentOrigin != "" && currentOrigin != newOrigin { | |||
| 2109 | - | a.webrtcIndicator.Reset() | |||
| 2110 | - | } | |||
| 2111 | }) | 2190 | }) | ||
| 2112 | } | 2191 | } | ||
| 2113 | 2192 | ||||
| 2114 | - | func (a *App) syncWebRTCPermissionLockState(ctx context.Context, origin string, permType entity.PermissionType) { | 2193 | + | func (a *App) syncWebRTCPermissionLockState( |
| 2115 | - | if a.webrtcIndicator == nil || a.deps == nil || a.deps.PermissionUC == nil || origin == "" { | 2194 | + | ctx context.Context, |
| 2195 | + | indicator *component.WebRTCPermissionIndicator, | |||
| 2196 | + | origin string, | |||
| 2197 | + | permType entity.PermissionType, | |||
| 2198 | + | ) { | |||
| 2199 | + | if indicator == nil || a.deps == nil || a.deps.PermissionUC == nil || origin == "" { | |||
| 2116 | return | 2200 | return | ||
| 2117 | } | 2201 | } | ||
| 2118 | 2202 | ||||
| @@ -2127,11 +2211,11 @@ func (a *App) syncWebRTCPermissionLockState(ctx context.Context, origin string, | |||||
| 2127 | } | 2211 | } | ||
| 2128 | 2212 | ||||
| 2129 | if record == nil { | 2213 | if record == nil { | ||
| 2130 | - | a.webrtcIndicator.SetStoredDecision(permType, entity.PermissionPrompt, false) | 2214 | + | indicator.SetStoredDecision(permType, entity.PermissionPrompt, false) |
| 2131 | return | 2215 | return | ||
| 2132 | } | 2216 | } | ||
| 2133 | 2217 | ||||
| 2134 | - | a.webrtcIndicator.SetStoredDecision(permType, record.Decision, true) | 2218 | + | indicator.SetStoredDecision(permType, record.Decision, true) |
| 2135 | } | 2219 | } | ||
| 2136 | 2220 | ||||
| 2137 | // generateWindowID generates a unique ID for top-level browser windows. | 2221 | // generateWindowID generates a unique ID for top-level browser windows. | ||
| @@ -2535,6 +2619,7 @@ func (a *App) switchWorkspaceView(ctx context.Context, tabID entity.TabID) { | |||||
| 2535 | 2619 | ||||
| 2536 | // Swap content (MainWindow.SetContent now properly removes old content) | 2620 | // Swap content (MainWindow.SetContent now properly removes old content) | ||
| 2537 | if target := a.browserWindowForTab(tabID); target != nil && target.mainWindow != nil { | 2621 | if target := a.browserWindowForTab(tabID); target != nil && target.mainWindow != nil { | ||
| 2622 | + | target.activeTabID = tabID | |||
| 2538 | target.mainWindow.SetContent(gtkWidget) | 2623 | target.mainWindow.SetContent(gtkWidget) | ||
| 2539 | } else if a.mainWindow != nil { | 2624 | } else if a.mainWindow != nil { | ||
| 2540 | a.mainWindow.SetContent(gtkWidget) | 2625 | a.mainWindow.SetContent(gtkWidget) | ||
| @@ -3411,13 +3496,16 @@ func (a *App) initConfigWatcher(ctx context.Context) { | |||||
| 3411 | a.deps.OnConfigChange(func() { | 3496 | a.deps.OnConfigChange(func() { | ||
| 3412 | cb := glib.SourceFunc(func(_ uintptr) bool { | 3497 | cb := glib.SourceFunc(func(_ uintptr) bool { | ||
| 3413 | a.applyAppearanceConfig(ctx) | 3498 | a.applyAppearanceConfig(ctx) | ||
| 3414 | - | // Reload keyboard shortcuts | 3499 | + | for _, bw := range a.browserWindows { |
| 3415 | - | if a.keyboardHandler != nil { | 3500 | + | if bw == nil { |
| 3416 | - | a.keyboardHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session) | 3501 | + | continue |
| 3417 | - | } | 3502 | + | } |
| 3418 | - | // Reload global shortcuts (removes stale, re-registers from config) | 3503 | + | if bw.keyboardHandler != nil { |
| 3419 | - | if a.globalShortcutHandler != nil { | 3504 | + | bw.keyboardHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session) |
| 3420 | - | a.globalShortcutHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session) | 3505 | + | } |
| 3506 | + | if bw.globalShortcutHandler != nil { | |||
| 3507 | + | bw.globalShortcutHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session) | |||
| 3508 | + | } | |||
| 3421 | } | 3509 | } | ||
| 3422 | return false | 3510 | return false | ||
| 3423 | }) | 3511 | }) | ||
M internal/ui/app_browser_launch_test.go +63 -0
| @@ -12,6 +12,7 @@ import ( | |||
| 12 | 12 | "github.com/bnema/dumber/internal/domain/entity" | |
| 13 | 13 | "github.com/bnema/dumber/internal/ui/component" | |
| 14 | 14 | "github.com/bnema/dumber/internal/ui/coordinator" | |
| 15 | + | "github.com/bnema/dumber/internal/ui/input" | |
| 15 | 16 | "github.com/bnema/dumber/internal/ui/window" | |
| 16 | 17 | "github.com/bnema/puregotk/v4/gio" | |
| 17 | 18 | "github.com/bnema/puregotk/v4/gtk" | |
| @@ -254,3 +255,65 @@ func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) { | |||
| 254 | 255 | t.Fatalf("new window tab bar visible = %v, want false", got) | |
| 255 | 256 | } | |
| 256 | 257 | } | |
| 258 | + | ||
| 259 | + | func TestApp_ActivateBrowserWindowSwitchesActiveWorkspace(t *testing.T) { | |
| 260 | + | tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1"))) | |
| 261 | + | tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2"))) | |
| 262 | + | tabs := entity.NewTabList() | |
| 263 | + | tabs.Add(tab1) | |
| 264 | + | tabs.Add(tab2) | |
| 265 | + | ws1 := &component.WorkspaceView{} | |
| 266 | + | ws2 := &component.WorkspaceView{} | |
| 267 | + | first := &browserWindow{id: "window-1", activeTabID: tab1.ID, mainWindow: &window.MainWindow{}} | |
| 268 | + | second := &browserWindow{id: "window-2", activeTabID: tab2.ID, mainWindow: &window.MainWindow{}} | |
| 269 | + | kh := &input.KeyboardHandler{} | |
| 270 | + | gs := &input.GlobalShortcutHandler{} | |
| 271 | + | second.keyboardHandler = kh | |
| 272 | + | second.globalShortcutHandler = gs | |
| 273 | + | app := &App{ | |
| 274 | + | tabs: tabs, | |
| 275 | + | mainWindow: first.mainWindow, | |
| 276 | + | browserWindows: map[string]*browserWindow{first.id: first, second.id: second}, | |
| 277 | + | windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second}, | |
| 278 | + | workspaceViews: map[entity.TabID]*component.WorkspaceView{tab1.ID: ws1, tab2.ID: ws2}, | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | app.activateBrowserWindow(second) | |
| 282 | + | ||
| 283 | + | if app.mainWindow != second.mainWindow { | |
| 284 | + | t.Fatalf("mainWindow = %p, want %p", app.mainWindow, second.mainWindow) | |
| 285 | + | } | |
| 286 | + | if app.tabs.ActiveTabID != tab2.ID { | |
| 287 | + | t.Fatalf("active tab = %q, want %q", app.tabs.ActiveTabID, tab2.ID) | |
| 288 | + | } | |
| 289 | + | if app.activeWorkspaceView() != ws2 { | |
| 290 | + | t.Fatalf("active workspace view = %p, want %p", app.activeWorkspaceView(), ws2) | |
| 291 | + | } | |
| 292 | + | if app.keyboardHandler != kh { | |
| 293 | + | t.Fatalf("keyboardHandler = %p, want %p", app.keyboardHandler, kh) | |
| 294 | + | } | |
| 295 | + | if app.globalShortcutHandler != gs { | |
| 296 | + | t.Fatalf("globalShortcutHandler = %p, want %p", app.globalShortcutHandler, gs) | |
| 297 | + | } | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | func TestApp_BrowserWindowForPaneFindsOwningWindow(t *testing.T) { | |
| 301 | + | tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1"))) | |
| 302 | + | tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2"))) | |
| 303 | + | tabs := entity.NewTabList() | |
| 304 | + | tabs.Add(tab1) | |
| 305 | + | tabs.Add(tab2) | |
| 306 | + | first := &browserWindow{id: "window-1"} | |
| 307 | + | second := &browserWindow{id: "window-2"} | |
| 308 | + | app := &App{ | |
| 309 | + | tabs: tabs, | |
| 310 | + | browserWindows: map[string]*browserWindow{first.id: first, second.id: second}, | |
| 311 | + | windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second}, | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | got := app.browserWindowForPane(entity.PaneID("pane-2")) | |
| 315 | + | ||
| 316 | + | if got != second { | |
| 317 | + | t.Fatalf("browserWindowForPane = %p, want %p", got, second) | |
| 318 | + | } | |
| 319 | + | } | |
| @@ -12,6 +12,7 @@ import ( | |||||
| 12 | "github.com/bnema/dumber/internal/domain/entity" | 12 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 13 | "github.com/bnema/dumber/internal/ui/component" | 13 | "github.com/bnema/dumber/internal/ui/component" | ||
| 14 | "github.com/bnema/dumber/internal/ui/coordinator" | 14 | "github.com/bnema/dumber/internal/ui/coordinator" | ||
| 15 | + | "github.com/bnema/dumber/internal/ui/input" | |||
| 15 | "github.com/bnema/dumber/internal/ui/window" | 16 | "github.com/bnema/dumber/internal/ui/window" | ||
| 16 | "github.com/bnema/puregotk/v4/gio" | 17 | "github.com/bnema/puregotk/v4/gio" | ||
| 17 | "github.com/bnema/puregotk/v4/gtk" | 18 | "github.com/bnema/puregotk/v4/gtk" | ||
| @@ -254,3 +255,65 @@ func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) { | |||||
| 254 | t.Fatalf("new window tab bar visible = %v, want false", got) | 255 | t.Fatalf("new window tab bar visible = %v, want false", got) | ||
| 255 | } | 256 | } | ||
| 256 | } | 257 | } | ||
| 258 | + | ||||
| 259 | + | func TestApp_ActivateBrowserWindowSwitchesActiveWorkspace(t *testing.T) { | |||
| 260 | + | tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1"))) | |||
| 261 | + | tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2"))) | |||
| 262 | + | tabs := entity.NewTabList() | |||
| 263 | + | tabs.Add(tab1) | |||
| 264 | + | tabs.Add(tab2) | |||
| 265 | + | ws1 := &component.WorkspaceView{} | |||
| 266 | + | ws2 := &component.WorkspaceView{} | |||
| 267 | + | first := &browserWindow{id: "window-1", activeTabID: tab1.ID, mainWindow: &window.MainWindow{}} | |||
| 268 | + | second := &browserWindow{id: "window-2", activeTabID: tab2.ID, mainWindow: &window.MainWindow{}} | |||
| 269 | + | kh := &input.KeyboardHandler{} | |||
| 270 | + | gs := &input.GlobalShortcutHandler{} | |||
| 271 | + | second.keyboardHandler = kh | |||
| 272 | + | second.globalShortcutHandler = gs | |||
| 273 | + | app := &App{ | |||
| 274 | + | tabs: tabs, | |||
| 275 | + | mainWindow: first.mainWindow, | |||
| 276 | + | browserWindows: map[string]*browserWindow{first.id: first, second.id: second}, | |||
| 277 | + | windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second}, | |||
| 278 | + | workspaceViews: map[entity.TabID]*component.WorkspaceView{tab1.ID: ws1, tab2.ID: ws2}, | |||
| 279 | + | } | |||
| 280 | + | ||||
| 281 | + | app.activateBrowserWindow(second) | |||
| 282 | + | ||||
| 283 | + | if app.mainWindow != second.mainWindow { | |||
| 284 | + | t.Fatalf("mainWindow = %p, want %p", app.mainWindow, second.mainWindow) | |||
| 285 | + | } | |||
| 286 | + | if app.tabs.ActiveTabID != tab2.ID { | |||
| 287 | + | t.Fatalf("active tab = %q, want %q", app.tabs.ActiveTabID, tab2.ID) | |||
| 288 | + | } | |||
| 289 | + | if app.activeWorkspaceView() != ws2 { | |||
| 290 | + | t.Fatalf("active workspace view = %p, want %p", app.activeWorkspaceView(), ws2) | |||
| 291 | + | } | |||
| 292 | + | if app.keyboardHandler != kh { | |||
| 293 | + | t.Fatalf("keyboardHandler = %p, want %p", app.keyboardHandler, kh) | |||
| 294 | + | } | |||
| 295 | + | if app.globalShortcutHandler != gs { | |||
| 296 | + | t.Fatalf("globalShortcutHandler = %p, want %p", app.globalShortcutHandler, gs) | |||
| 297 | + | } | |||
| 298 | + | } | |||
| 299 | + | ||||
| 300 | + | func TestApp_BrowserWindowForPaneFindsOwningWindow(t *testing.T) { | |||
| 301 | + | tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1"))) | |||
| 302 | + | tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2"))) | |||
| 303 | + | tabs := entity.NewTabList() | |||
| 304 | + | tabs.Add(tab1) | |||
| 305 | + | tabs.Add(tab2) | |||
| 306 | + | first := &browserWindow{id: "window-1"} | |||
| 307 | + | second := &browserWindow{id: "window-2"} | |||
| 308 | + | app := &App{ | |||
| 309 | + | tabs: tabs, | |||
| 310 | + | browserWindows: map[string]*browserWindow{first.id: first, second.id: second}, | |||
| 311 | + | windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second}, | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | got := app.browserWindowForPane(entity.PaneID("pane-2")) | |||
| 315 | + | ||||
| 316 | + | if got != second { | |||
| 317 | + | t.Fatalf("browserWindowForPane = %p, want %p", got, second) | |||
| 318 | + | } | |||
| 319 | + | } | |||
M internal/ui/browser_window.go +15 -7
| @@ -3,13 +3,22 @@ package ui | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | ||
| 6 | + | "github.com/bnema/dumber/internal/application/port" | |
| 7 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 8 | + | "github.com/bnema/dumber/internal/ui/component" | |
| 9 | + | "github.com/bnema/dumber/internal/ui/input" | |
| 6 | 10 | "github.com/bnema/dumber/internal/ui/window" | |
| 7 | 11 | ) | |
| 8 | 12 | ||
| 9 | 13 | type browserWindow struct { | |
| 10 | - | id string | |
| 11 | - | initialURL string | |
| 12 | - | mainWindow *window.MainWindow | |
| 14 | + | id string | |
| 15 | + | initialURL string | |
| 16 | + | activeTabID entity.TabID | |
| 17 | + | mainWindow *window.MainWindow | |
| 18 | + | keyboardHandler *input.KeyboardHandler | |
| 19 | + | globalShortcutHandler *input.GlobalShortcutHandler | |
| 20 | + | permissionDialog port.PermissionDialogPresenter | |
| 21 | + | webrtcIndicator *component.WebRTCPermissionIndicator | |
| 13 | 22 | } | |
| 14 | 23 | ||
| 15 | 24 | func (a *App) registerBrowserWindow(bw *browserWindow) { | |
| @@ -35,14 +44,13 @@ func (a *App) removeBrowserWindow(id string) { | |||
| 35 | 44 | delete(a.browserWindows, id) | |
| 36 | 45 | for _, remaining := range a.browserWindows { | |
| 37 | 46 | if remaining != nil && remaining.mainWindow != nil { | |
| 38 | - | a.mainWindow = remaining.mainWindow | |
| 39 | - | if a.tabCoord != nil { | |
| 40 | - | a.tabCoord.SetMainWindow(remaining.mainWindow) | |
| 41 | - | } | |
| 47 | + | a.activateBrowserWindow(remaining) | |
| 42 | 48 | return | |
| 43 | 49 | } | |
| 44 | 50 | } | |
| 45 | 51 | a.mainWindow = nil | |
| 52 | + | a.keyboardHandler = nil | |
| 53 | + | a.globalShortcutHandler = nil | |
| 46 | 54 | if a.tabCoord != nil { | |
| 47 | 55 | a.tabCoord.SetMainWindow(nil) | |
| 48 | 56 | } | |
| @@ -3,13 +3,22 @@ package ui | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | 5 | ||||
| 6 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 7 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 8 | + | "github.com/bnema/dumber/internal/ui/component" | |||
| 9 | + | "github.com/bnema/dumber/internal/ui/input" | |||
| 6 | "github.com/bnema/dumber/internal/ui/window" | 10 | "github.com/bnema/dumber/internal/ui/window" | ||
| 7 | ) | 11 | ) | ||
| 8 | 12 | ||||
| 9 | type browserWindow struct { | 13 | type browserWindow struct { | ||
| 10 | - | id string | 14 | + | id string |
| 11 | - | initialURL string | 15 | + | initialURL string |
| 12 | - | mainWindow *window.MainWindow | 16 | + | activeTabID entity.TabID |
| 17 | + | mainWindow *window.MainWindow | |||
| 18 | + | keyboardHandler *input.KeyboardHandler | |||
| 19 | + | globalShortcutHandler *input.GlobalShortcutHandler | |||
| 20 | + | permissionDialog port.PermissionDialogPresenter | |||
| 21 | + | webrtcIndicator *component.WebRTCPermissionIndicator | |||
| 13 | } | 22 | } | ||
| 14 | 23 | ||||
| 15 | func (a *App) registerBrowserWindow(bw *browserWindow) { | 24 | func (a *App) registerBrowserWindow(bw *browserWindow) { | ||
| @@ -35,14 +44,13 @@ func (a *App) removeBrowserWindow(id string) { | |||||
| 35 | delete(a.browserWindows, id) | 44 | delete(a.browserWindows, id) | ||
| 36 | for _, remaining := range a.browserWindows { | 45 | for _, remaining := range a.browserWindows { | ||
| 37 | if remaining != nil && remaining.mainWindow != nil { | 46 | if remaining != nil && remaining.mainWindow != nil { | ||
| 38 | - | a.mainWindow = remaining.mainWindow | 47 | + | a.activateBrowserWindow(remaining) |
| 39 | - | if a.tabCoord != nil { | |||
| 40 | - | a.tabCoord.SetMainWindow(remaining.mainWindow) | |||
| 41 | - | } | |||
| 42 | return | 48 | return | ||
| 43 | } | 49 | } | ||
| 44 | } | 50 | } | ||
| 45 | a.mainWindow = nil | 51 | a.mainWindow = nil | ||
| 52 | + | a.keyboardHandler = nil | |||
| 53 | + | a.globalShortcutHandler = nil | |||
| 46 | if a.tabCoord != nil { | 54 | if a.tabCoord != nil { | ||
| 47 | a.tabCoord.SetMainWindow(nil) | 55 | a.tabCoord.SetMainWindow(nil) | ||
| 48 | } | 56 | } | ||
M internal/ui/browser_window_test.go +19 -0
| @@ -2,6 +2,7 @@ package ui | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "errors" | |
| 5 | 6 | "testing" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/dumber/internal/ui/window" | |
| @@ -109,3 +110,21 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) { | |||
| 109 | 110 | t.Fatalf("browser window initialURL = %q, want %q", app.browserWindows["window-1"].initialURL, "https://example.com") | |
| 110 | 111 | } | |
| 111 | 112 | } | |
| 113 | + | ||
| 114 | + | func TestOpenFreshWindow_PropagatesFactoryError(t *testing.T) { | |
| 115 | + | app := &App{browserWindows: make(map[string]*browserWindow)} | |
| 116 | + | app.dispatchOnMainThread = func(fn func()) { fn() } | |
| 117 | + | wantErr := errors.New("factory error") | |
| 118 | + | app.browserWindowFactory = func(context.Context, string) (*browserWindow, error) { | |
| 119 | + | return nil, wantErr | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | err := app.OpenFreshWindow(context.Background(), "https://example.com") | |
| 123 | + | ||
| 124 | + | if !errors.Is(err, wantErr) { | |
| 125 | + | t.Fatalf("expected error %v, got %v", wantErr, err) | |
| 126 | + | } | |
| 127 | + | if got := len(app.browserWindows); got != 0 { | |
| 128 | + | t.Fatalf("browserWindows length = %d, want 0", got) | |
| 129 | + | } | |
| 130 | + | } | |
| @@ -2,6 +2,7 @@ package ui | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "errors" | |||
| 5 | "testing" | 6 | "testing" | ||
| 6 | 7 | ||||
| 7 | "github.com/bnema/dumber/internal/ui/window" | 8 | "github.com/bnema/dumber/internal/ui/window" | ||
| @@ -109,3 +110,21 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) { | |||||
| 109 | t.Fatalf("browser window initialURL = %q, want %q", app.browserWindows["window-1"].initialURL, "https://example.com") | 110 | t.Fatalf("browser window initialURL = %q, want %q", app.browserWindows["window-1"].initialURL, "https://example.com") | ||
| 110 | } | 111 | } | ||
| 111 | } | 112 | } | ||
| 113 | + | ||||
| 114 | + | func TestOpenFreshWindow_PropagatesFactoryError(t *testing.T) { | |||
| 115 | + | app := &App{browserWindows: make(map[string]*browserWindow)} | |||
| 116 | + | app.dispatchOnMainThread = func(fn func()) { fn() } | |||
| 117 | + | wantErr := errors.New("factory error") | |||
| 118 | + | app.browserWindowFactory = func(context.Context, string) (*browserWindow, error) { | |||
| 119 | + | return nil, wantErr | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | err := app.OpenFreshWindow(context.Background(), "https://example.com") | |||
| 123 | + | ||||
| 124 | + | if !errors.Is(err, wantErr) { | |||
| 125 | + | t.Fatalf("expected error %v, got %v", wantErr, err) | |||
| 126 | + | } | |||
| 127 | + | if got := len(app.browserWindows); got != 0 { | |||
| 128 | + | t.Fatalf("browserWindows length = %d, want 0", got) | |||
| 129 | + | } | |||
| 130 | + | } | |||
M internal/ui/coordinator/content/callbacks.go +3 -2
| @@ -118,7 +118,7 @@ func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.P | |||
| 118 | 118 | } | |
| 119 | 119 | }, | |
| 120 | 120 | OnPermissionRequest: func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool { | |
| 121 | - | return c.handlePermissionRequest(ctx, origin, permTypes, metadata, allow, deny) | |
| 121 | + | return c.handlePermissionRequest(ctx, paneID, origin, permTypes, metadata, allow, deny) | |
| 122 | 122 | }, | |
| 123 | 123 | } | |
| 124 | 124 | ||
| @@ -189,6 +189,7 @@ func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.P | |||
| 189 | 189 | // It delegates to the permission use case which handles auto-allow, stored permissions, and dialogs. | |
| 190 | 190 | func (c *Coordinator) handlePermissionRequest( | |
| 191 | 191 | ctx context.Context, | |
| 192 | + | paneID entity.PaneID, | |
| 192 | 193 | origin string, | |
| 193 | 194 | permTypes []string, | |
| 194 | 195 | metadata map[string]string, | |
| @@ -226,7 +227,7 @@ func (c *Coordinator) handlePermissionRequest( | |||
| 226 | 227 | if c.onPermissionActivity == nil || len(trackedTypes) == 0 { | |
| 227 | 228 | return | |
| 228 | 229 | } | |
| 229 | - | c.onPermissionActivity(origin, trackedTypes, state) | |
| 230 | + | c.onPermissionActivity(paneID, origin, trackedTypes, state) | |
| 230 | 231 | } | |
| 231 | 232 | ||
| 232 | 233 | notifyActivity(PermissionActivityRequesting) | |
| @@ -118,7 +118,7 @@ func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.P | |||||
| 118 | } | 118 | } | ||
| 119 | }, | 119 | }, | ||
| 120 | OnPermissionRequest: func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool { | 120 | OnPermissionRequest: func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool { | ||
| 121 | - | return c.handlePermissionRequest(ctx, origin, permTypes, metadata, allow, deny) | 121 | + | return c.handlePermissionRequest(ctx, paneID, origin, permTypes, metadata, allow, deny) |
| 122 | }, | 122 | }, | ||
| 123 | } | 123 | } | ||
| 124 | 124 | ||||
| @@ -189,6 +189,7 @@ func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.P | |||||
| 189 | // It delegates to the permission use case which handles auto-allow, stored permissions, and dialogs. | 189 | // It delegates to the permission use case which handles auto-allow, stored permissions, and dialogs. | ||
| 190 | func (c *Coordinator) handlePermissionRequest( | 190 | func (c *Coordinator) handlePermissionRequest( | ||
| 191 | ctx context.Context, | 191 | ctx context.Context, | ||
| 192 | + | paneID entity.PaneID, | |||
| 192 | origin string, | 193 | origin string, | ||
| 193 | permTypes []string, | 194 | permTypes []string, | ||
| 194 | metadata map[string]string, | 195 | metadata map[string]string, | ||
| @@ -226,7 +227,7 @@ func (c *Coordinator) handlePermissionRequest( | |||||
| 226 | if c.onPermissionActivity == nil || len(trackedTypes) == 0 { | 227 | if c.onPermissionActivity == nil || len(trackedTypes) == 0 { | ||
| 227 | return | 228 | return | ||
| 228 | } | 229 | } | ||
| 229 | - | c.onPermissionActivity(origin, trackedTypes, state) | 230 | + | c.onPermissionActivity(paneID, origin, trackedTypes, state) |
| 230 | } | 231 | } | ||
| 231 | 232 | ||||
| 232 | notifyActivity(PermissionActivityRequesting) | 233 | notifyActivity(PermissionActivityRequesting) | ||
M internal/ui/coordinator/content/coordinator.go +4 -4
| @@ -59,10 +59,10 @@ type Coordinator struct { | |||
| 59 | 59 | onWindowTitleChanged func(title string) | |
| 60 | 60 | ||
| 61 | 61 | // Callback when media permission activity changes (requesting/allowed/blocked). | |
| 62 | - | onPermissionActivity func(origin string, permTypes []entity.PermissionType, state PermissionActivityState) | |
| 62 | + | onPermissionActivity func(paneID entity.PaneID, origin string, permTypes []entity.PermissionType, state PermissionActivityState) | |
| 63 | 63 | ||
| 64 | 64 | // Callback when the active pane commits a navigation (new page loading). | |
| 65 | - | onActiveNavigationCommitted func(uri string) | |
| 65 | + | onActiveNavigationCommitted func(paneID entity.PaneID, uri string) | |
| 66 | 66 | ||
| 67 | 67 | // Callback when the WebView becomes visible (first real commit) | |
| 68 | 68 | onWebViewShown func(paneID entity.PaneID) | |
| @@ -187,13 +187,13 @@ func (c *Coordinator) SetOnHistoryRecord(fn func(ctx context.Context, paneID ent | |||
| 187 | 187 | ||
| 188 | 188 | // SetOnPermissionActivity sets a callback for WebRTC permission activity changes. | |
| 189 | 189 | func (c *Coordinator) SetOnPermissionActivity( | |
| 190 | - | fn func(origin string, permTypes []entity.PermissionType, state PermissionActivityState), | |
| 190 | + | fn func(paneID entity.PaneID, origin string, permTypes []entity.PermissionType, state PermissionActivityState), | |
| 191 | 191 | ) { | |
| 192 | 192 | c.onPermissionActivity = fn | |
| 193 | 193 | } | |
| 194 | 194 | ||
| 195 | 195 | // SetOnActiveNavigationCommitted sets a callback fired when the active pane commits a navigation. | |
| 196 | - | func (c *Coordinator) SetOnActiveNavigationCommitted(fn func(uri string)) { | |
| 196 | + | func (c *Coordinator) SetOnActiveNavigationCommitted(fn func(paneID entity.PaneID, uri string)) { | |
| 197 | 197 | c.onActiveNavigationCommitted = fn | |
| 198 | 198 | } | |
| 199 | 199 | ||
| @@ -59,10 +59,10 @@ type Coordinator struct { | |||||
| 59 | onWindowTitleChanged func(title string) | 59 | onWindowTitleChanged func(title string) | ||
| 60 | 60 | ||||
| 61 | // Callback when media permission activity changes (requesting/allowed/blocked). | 61 | // Callback when media permission activity changes (requesting/allowed/blocked). | ||
| 62 | - | onPermissionActivity func(origin string, permTypes []entity.PermissionType, state PermissionActivityState) | 62 | + | onPermissionActivity func(paneID entity.PaneID, origin string, permTypes []entity.PermissionType, state PermissionActivityState) |
| 63 | 63 | ||||
| 64 | // Callback when the active pane commits a navigation (new page loading). | 64 | // Callback when the active pane commits a navigation (new page loading). | ||
| 65 | - | onActiveNavigationCommitted func(uri string) | 65 | + | onActiveNavigationCommitted func(paneID entity.PaneID, uri string) |
| 66 | 66 | ||||
| 67 | // Callback when the WebView becomes visible (first real commit) | 67 | // Callback when the WebView becomes visible (first real commit) | ||
| 68 | onWebViewShown func(paneID entity.PaneID) | 68 | onWebViewShown func(paneID entity.PaneID) | ||
| @@ -187,13 +187,13 @@ func (c *Coordinator) SetOnHistoryRecord(fn func(ctx context.Context, paneID ent | |||||
| 187 | 187 | ||||
| 188 | // SetOnPermissionActivity sets a callback for WebRTC permission activity changes. | 188 | // SetOnPermissionActivity sets a callback for WebRTC permission activity changes. | ||
| 189 | func (c *Coordinator) SetOnPermissionActivity( | 189 | func (c *Coordinator) SetOnPermissionActivity( | ||
| 190 | - | fn func(origin string, permTypes []entity.PermissionType, state PermissionActivityState), | 190 | + | fn func(paneID entity.PaneID, origin string, permTypes []entity.PermissionType, state PermissionActivityState), |
| 191 | ) { | 191 | ) { | ||
| 192 | c.onPermissionActivity = fn | 192 | c.onPermissionActivity = fn | ||
| 193 | } | 193 | } | ||
| 194 | 194 | ||||
| 195 | // SetOnActiveNavigationCommitted sets a callback fired when the active pane commits a navigation. | 195 | // SetOnActiveNavigationCommitted sets a callback fired when the active pane commits a navigation. | ||
| 196 | - | func (c *Coordinator) SetOnActiveNavigationCommitted(fn func(uri string)) { | 196 | + | func (c *Coordinator) SetOnActiveNavigationCommitted(fn func(paneID entity.PaneID, uri string)) { |
| 197 | c.onActiveNavigationCommitted = fn | 197 | c.onActiveNavigationCommitted = fn | ||
| 198 | } | 198 | } | ||
| 199 | 199 | ||||
M internal/ui/coordinator/content/navigation.go +2 -2
| @@ -123,13 +123,13 @@ func (c *Coordinator) notifyActiveNavigation(paneID entity.PaneID, uri string) { | |||
| 123 | 123 | c.activePaneOverrideMu.RUnlock() | |
| 124 | 124 | if override != "" { | |
| 125 | 125 | if override == paneID { | |
| 126 | - | c.onActiveNavigationCommitted(uri) | |
| 126 | + | c.onActiveNavigationCommitted(paneID, uri) | |
| 127 | 127 | } | |
| 128 | 128 | return | |
| 129 | 129 | } | |
| 130 | 130 | ws, _ := c.getActiveWS() | |
| 131 | 131 | if ws != nil && ws.ActivePaneID == paneID { | |
| 132 | - | c.onActiveNavigationCommitted(uri) | |
| 132 | + | c.onActiveNavigationCommitted(paneID, uri) | |
| 133 | 133 | } | |
| 134 | 134 | } | |
| 135 | 135 | ||
| @@ -123,13 +123,13 @@ func (c *Coordinator) notifyActiveNavigation(paneID entity.PaneID, uri string) { | |||||
| 123 | c.activePaneOverrideMu.RUnlock() | 123 | c.activePaneOverrideMu.RUnlock() | ||
| 124 | if override != "" { | 124 | if override != "" { | ||
| 125 | if override == paneID { | 125 | if override == paneID { | ||
| 126 | - | c.onActiveNavigationCommitted(uri) | 126 | + | c.onActiveNavigationCommitted(paneID, uri) |
| 127 | } | 127 | } | ||
| 128 | return | 128 | return | ||
| 129 | } | 129 | } | ||
| 130 | ws, _ := c.getActiveWS() | 130 | ws, _ := c.getActiveWS() | ||
| 131 | if ws != nil && ws.ActivePaneID == paneID { | 131 | if ws != nil && ws.ActivePaneID == paneID { | ||
| 132 | - | c.onActiveNavigationCommitted(uri) | 132 | + | c.onActiveNavigationCommitted(paneID, uri) |
| 133 | } | 133 | } | ||
| 134 | } | 134 | } | ||
| 135 | 135 | ||||