feat(ui): add page refresh shortcuts and fix zoom toaster behavior
- Add Ctrl+R, Ctrl+Shift+R, and F5 keyboard shortcuts for page refresh - Implement Reload() and ReloadBypassCache() WebKit methods - Fix zoom toaster showing on every same-domain navigation - Add _blank link handler to load popups in current window - Update README with new keyboard shortcuts
- commit
- 2b27f3776ce548529e1835d53ab4f1c590d87a70
- author
- bamen · · 2025-09-12 07:09 UTC
- parent
- db5f019
- signature
- unsigned
6 files · +131 -13
M CHANGELOG.md +17 -0
| @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. | |||
| 4 | 4 | ||
| 5 | 5 | ## [Unreleased] | |
| 6 | 6 | ||
| 7 | + | ### Added | |
| 8 | + | - Page refresh functionality with standard keyboard shortcuts: | |
| 9 | + | - **Ctrl+R** / **Cmd+R**: Reload current page | |
| 10 | + | - **Ctrl+Shift+R** / **Cmd+Shift+R**: Hard reload (bypass cache) | |
| 11 | + | - **F5**: Alternative reload key | |
| 12 | + | - WebKit integration with proper `webkit_web_view_reload()` and `webkit_web_view_reload_bypass_cache()` calls | |
| 13 | + | ||
| 14 | + | ### Fixed | |
| 15 | + | - Zoom level toast no longer shows on every page navigation within the same domain: | |
| 16 | + | - Added smart detection to differentiate user-initiated vs programmatic zoom changes | |
| 17 | + | - Toast now only appears when manually adjusting zoom or entering a new domain | |
| 18 | + | - Prevents annoying notifications during regular browsing within the same site | |
| 19 | + | - Links with `target="_blank"` now load in current window instead of being ignored: | |
| 20 | + | - Added WebKit `create` signal handler to intercept new window requests | |
| 21 | + | - Redirects popup/new tab links to current window for seamless single-window browsing | |
| 22 | + | - Maintains user experience consistency in tab-less browser architecture | |
| 23 | + | ||
| 7 | 24 | ## [0.5.0] - 2025-09-12 | |
| 8 | 25 | ||
| 9 | 26 | ### Added | |
| @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. | |||||
| 4 | 4 | ||||
| 5 | ## [Unreleased] | 5 | ## [Unreleased] | ||
| 6 | 6 | ||||
| 7 | + | ### Added | |||
| 8 | + | - Page refresh functionality with standard keyboard shortcuts: | |||
| 9 | + | - **Ctrl+R** / **Cmd+R**: Reload current page | |||
| 10 | + | - **Ctrl+Shift+R** / **Cmd+Shift+R**: Hard reload (bypass cache) | |||
| 11 | + | - **F5**: Alternative reload key | |||
| 12 | + | - WebKit integration with proper `webkit_web_view_reload()` and `webkit_web_view_reload_bypass_cache()` calls | |||
| 13 | + | ||||
| 14 | + | ### Fixed | |||
| 15 | + | - Zoom level toast no longer shows on every page navigation within the same domain: | |||
| 16 | + | - Added smart detection to differentiate user-initiated vs programmatic zoom changes | |||
| 17 | + | - Toast now only appears when manually adjusting zoom or entering a new domain | |||
| 18 | + | - Prevents annoying notifications during regular browsing within the same site | |||
| 19 | + | - Links with `target="_blank"` now load in current window instead of being ignored: | |||
| 20 | + | - Added WebKit `create` signal handler to intercept new window requests | |||
| 21 | + | - Redirects popup/new tab links to current window for seamless single-window browsing | |||
| 22 | + | - Maintains user experience consistency in tab-less browser architecture | |||
| 23 | + | ||||
| 7 | ## [0.5.0] - 2025-09-12 | 24 | ## [0.5.0] - 2025-09-12 | ||
| 8 | 25 | ||||
| 9 | ### Added | 26 | ### Added | ||
M README.md +3 -0
| @@ -41,6 +41,9 @@ Dumber is a minimalist browser and launcher companion focused on speed and simpl | |||
| 41 | 41 | | **Ctrl/Cmd+-** | Zoom Out | Works across keyboard layouts | | |
| 42 | 42 | | **Ctrl/Cmd+0** | Reset Zoom | Return to 100% zoom | | |
| 43 | 43 | | **Ctrl/Cmd+Shift+C** | Copy URL | Copy current URL to clipboard with toast | | |
| 44 | + | | **Ctrl/Cmd+R** | Reload Page | Refresh current page | | |
| 45 | + | | **Ctrl/Cmd+Shift+R** | Hard Reload | Refresh ignoring cache | | |
| 46 | + | | **F5** | Reload Page | Alternative reload key | | |
| 44 | 47 | | **Alt+←** | Navigate Back | Go to previous page | | |
| 45 | 48 | | **Alt+→** | Navigate Forward | Go to next page | | |
| 46 | 49 | ||
| @@ -41,6 +41,9 @@ Dumber is a minimalist browser and launcher companion focused on speed and simpl | |||||
| 41 | | **Ctrl/Cmd+-** | Zoom Out | Works across keyboard layouts | | 41 | | **Ctrl/Cmd+-** | Zoom Out | Works across keyboard layouts | | ||
| 42 | | **Ctrl/Cmd+0** | Reset Zoom | Return to 100% zoom | | 42 | | **Ctrl/Cmd+0** | Reset Zoom | Return to 100% zoom | | ||
| 43 | | **Ctrl/Cmd+Shift+C** | Copy URL | Copy current URL to clipboard with toast | | 43 | | **Ctrl/Cmd+Shift+C** | Copy URL | Copy current URL to clipboard with toast | | ||
| 44 | + | | **Ctrl/Cmd+R** | Reload Page | Refresh current page | | |||
| 45 | + | | **Ctrl/Cmd+Shift+R** | Hard Reload | Refresh ignoring cache | | |||
| 46 | + | | **F5** | Reload Page | Alternative reload key | | |||
| 44 | | **Alt+←** | Navigate Back | Go to previous page | | 47 | | **Alt+←** | Navigate Back | Go to previous page | | ||
| 45 | | **Alt+→** | Navigate Forward | Go to next page | | 48 | | **Alt+→** | Navigate Forward | Go to next page | | ||
| 46 | 49 | ||||
M main.go +58 -13
| @@ -260,6 +260,12 @@ func runBrowser() { | |||
| 260 | 260 | // Track current URL for zoom persistence | |
| 261 | 261 | // Initialize to the initial homepage so shortcuts work immediately | |
| 262 | 262 | var currentURL string = "dumb://homepage" | |
| 263 | + | ||
| 264 | + | // Track last zoom domain to avoid showing toaster on every navigation within same domain | |
| 265 | + | var lastZoomDomain string | |
| 266 | + | ||
| 267 | + | // Flag to prevent showing toast when programmatically setting zoom (not user-initiated) | |
| 268 | + | var programmaticZoomChange bool | |
| 263 | 269 | ||
| 264 | 270 | // Register custom scheme resolver for dumb:// | |
| 265 | 271 | webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) { | |
| @@ -471,10 +477,28 @@ func runBrowser() { | |||
| 471 | 477 | return | |
| 472 | 478 | } | |
| 473 | 479 | ctx := context.Background() | |
| 480 | + | currentDomain := services.ZoomKeyForLog(u) | |
| 481 | + | ||
| 474 | 482 | if z, err := browserService.GetZoomLevel(ctx, u); err == nil { | |
| 483 | + | programmaticZoomChange = true | |
| 475 | 484 | _ = view.SetZoom(z) | |
| 476 | - | key := services.ZoomKeyForLog(u) | |
| 477 | - | log.Printf("[zoom] loaded %.2f for %s", z, key) | |
| 485 | + | programmaticZoomChange = false | |
| 486 | + | log.Printf("[zoom] loaded %.2f for %s", z, currentDomain) | |
| 487 | + | ||
| 488 | + | // Only show toast when entering a new domain (not on first load) | |
| 489 | + | if currentDomain != lastZoomDomain && lastZoomDomain != "" { | |
| 490 | + | js := fmt.Sprintf(`try { | |
| 491 | + | if (typeof window.__dumber_showZoomToast === 'function') { | |
| 492 | + | window.__dumber_showZoomToast(%f); | |
| 493 | + | } | |
| 494 | + | } catch(e) { | |
| 495 | + | console.error('[dumber] Zoom toast error:', e); | |
| 496 | + | }`, z) | |
| 497 | + | if err := view.InjectScript(js); err != nil { | |
| 498 | + | log.Printf("[zoom] failed to show domain change toast: %v", err) | |
| 499 | + | } | |
| 500 | + | } | |
| 501 | + | lastZoomDomain = currentDomain | |
| 478 | 502 | } | |
| 479 | 503 | }) | |
| 480 | 504 | // Persist zoom changes per-domain | |
| @@ -491,18 +515,20 @@ func runBrowser() { | |||
| 491 | 515 | key := services.ZoomKeyForLog(url) | |
| 492 | 516 | log.Printf("[zoom] saved %.2f for %s", level, key) | |
| 493 | 517 | ||
| 494 | - | // Show zoom toast notification | |
| 495 | - | js := fmt.Sprintf(`try { | |
| 496 | - | if (typeof window.__dumber_showZoomToast === 'function') { | |
| 497 | - | window.__dumber_showZoomToast(%f); | |
| 498 | - | } else { | |
| 499 | - | console.warn('[dumber] Zoom toast function not available yet'); | |
| 518 | + | // Only show toast for user-initiated zoom changes, not programmatic ones | |
| 519 | + | if !programmaticZoomChange { | |
| 520 | + | js := fmt.Sprintf(`try { | |
| 521 | + | if (typeof window.__dumber_showZoomToast === 'function') { | |
| 522 | + | window.__dumber_showZoomToast(%f); | |
| 523 | + | } else { | |
| 524 | + | console.warn('[dumber] Zoom toast function not available yet'); | |
| 525 | + | } | |
| 526 | + | } catch(e) { | |
| 527 | + | console.error('[dumber] Zoom toast error:', e); | |
| 528 | + | }`, level) | |
| 529 | + | if err := view.InjectScript(js); err != nil { | |
| 530 | + | log.Printf("[zoom] failed to show toast: %v", err) | |
| 500 | 531 | } | |
| 501 | - | } catch(e) { | |
| 502 | - | console.error('[dumber] Zoom toast error:', e); | |
| 503 | - | }`, level) | |
| 504 | - | if err := view.InjectScript(js); err != nil { | |
| 505 | - | log.Printf("[zoom] failed to show toast: %v", err) | |
| 506 | 532 | } | |
| 507 | 533 | }) | |
| 508 | 534 | // Bridge UCM messages: navigate + history queries | |
| @@ -532,7 +558,9 @@ func runBrowser() { | |||
| 532 | 558 | } | |
| 533 | 559 | _ = view.LoadURL(currentURL) | |
| 534 | 560 | if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil { | |
| 561 | + | programmaticZoomChange = true | |
| 535 | 562 | _ = view.SetZoom(z) | |
| 563 | + | programmaticZoomChange = false | |
| 536 | 564 | } | |
| 537 | 565 | } | |
| 538 | 566 | case "query": | |
| @@ -627,12 +655,14 @@ func runBrowser() { | |||
| 627 | 655 | u = "dumb://homepage" | |
| 628 | 656 | } | |
| 629 | 657 | if z, zerr := browserService.GetZoomLevel(ctx, u); zerr == nil { | |
| 658 | + | programmaticZoomChange = true | |
| 630 | 659 | if err := view.SetZoom(z); err != nil { | |
| 631 | 660 | log.Printf("Warning: failed to set initial zoom: %v", err) | |
| 632 | 661 | } else { | |
| 633 | 662 | key := services.ZoomKeyForLog(u) | |
| 634 | 663 | log.Printf("[zoom] loaded %.2f for %s", z, key) | |
| 635 | 664 | } | |
| 665 | + | programmaticZoomChange = false | |
| 636 | 666 | } | |
| 637 | 667 | } | |
| 638 | 668 | ||
| @@ -661,12 +691,14 @@ func runBrowser() { | |||
| 661 | 691 | log.Printf("Warning: failed to load URL: %v", err) | |
| 662 | 692 | } | |
| 663 | 693 | if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil { | |
| 694 | + | programmaticZoomChange = true | |
| 664 | 695 | if err := view.SetZoom(z); err != nil { | |
| 665 | 696 | log.Printf("Warning: failed to set zoom: %v", err) | |
| 666 | 697 | } else { | |
| 667 | 698 | key := services.ZoomKeyForLog(currentURL) | |
| 668 | 699 | log.Printf("[zoom] loaded %.2f for %s", z, key) | |
| 669 | 700 | } | |
| 701 | + | programmaticZoomChange = false | |
| 670 | 702 | } | |
| 671 | 703 | // No JS injection; native accelerators handle zoom/devtools. | |
| 672 | 704 | } | |
| @@ -700,6 +732,19 @@ func runBrowser() { | |||
| 700 | 732 | _ = view.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)`) | |
| 701 | 733 | } | |
| 702 | 734 | }) | |
| 735 | + | // Page refresh shortcuts | |
| 736 | + | _ = view.RegisterKeyboardShortcut("cmdorctrl+r", func() { | |
| 737 | + | log.Printf("Shortcut: Reload page") | |
| 738 | + | _ = view.Reload() | |
| 739 | + | }) | |
| 740 | + | _ = view.RegisterKeyboardShortcut("cmdorctrl+shift+r", func() { | |
| 741 | + | log.Printf("Shortcut: Hard reload page") | |
| 742 | + | _ = view.ReloadBypassCache() | |
| 743 | + | }) | |
| 744 | + | _ = view.RegisterKeyboardShortcut("F5", func() { | |
| 745 | + | log.Printf("Shortcut: F5 reload") | |
| 746 | + | _ = view.Reload() | |
| 747 | + | }) | |
| 703 | 748 | // Zoom handled natively in webkit package (built-in shortcuts) | |
| 704 | 749 | } | |
| 705 | 750 | ||
| @@ -260,6 +260,12 @@ func runBrowser() { | |||||
| 260 | // Track current URL for zoom persistence | 260 | // Track current URL for zoom persistence | ||
| 261 | // Initialize to the initial homepage so shortcuts work immediately | 261 | // Initialize to the initial homepage so shortcuts work immediately | ||
| 262 | var currentURL string = "dumb://homepage" | 262 | var currentURL string = "dumb://homepage" | ||
| 263 | + | ||||
| 264 | + | // Track last zoom domain to avoid showing toaster on every navigation within same domain | |||
| 265 | + | var lastZoomDomain string | |||
| 266 | + | ||||
| 267 | + | // Flag to prevent showing toast when programmatically setting zoom (not user-initiated) | |||
| 268 | + | var programmaticZoomChange bool | |||
| 263 | 269 | ||||
| 264 | // Register custom scheme resolver for dumb:// | 270 | // Register custom scheme resolver for dumb:// | ||
| 265 | webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) { | 271 | webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) { | ||
| @@ -471,10 +477,28 @@ func runBrowser() { | |||||
| 471 | return | 477 | return | ||
| 472 | } | 478 | } | ||
| 473 | ctx := context.Background() | 479 | ctx := context.Background() | ||
| 480 | + | currentDomain := services.ZoomKeyForLog(u) | |||
| 481 | + | ||||
| 474 | if z, err := browserService.GetZoomLevel(ctx, u); err == nil { | 482 | if z, err := browserService.GetZoomLevel(ctx, u); err == nil { | ||
| 483 | + | programmaticZoomChange = true | |||
| 475 | _ = view.SetZoom(z) | 484 | _ = view.SetZoom(z) | ||
| 476 | - | key := services.ZoomKeyForLog(u) | 485 | + | programmaticZoomChange = false |
| 477 | - | log.Printf("[zoom] loaded %.2f for %s", z, key) | 486 | + | log.Printf("[zoom] loaded %.2f for %s", z, currentDomain) |
| 487 | + | ||||
| 488 | + | // Only show toast when entering a new domain (not on first load) | |||
| 489 | + | if currentDomain != lastZoomDomain && lastZoomDomain != "" { | |||
| 490 | + | js := fmt.Sprintf(`try { | |||
| 491 | + | if (typeof window.__dumber_showZoomToast === 'function') { | |||
| 492 | + | window.__dumber_showZoomToast(%f); | |||
| 493 | + | } | |||
| 494 | + | } catch(e) { | |||
| 495 | + | console.error('[dumber] Zoom toast error:', e); | |||
| 496 | + | }`, z) | |||
| 497 | + | if err := view.InjectScript(js); err != nil { | |||
| 498 | + | log.Printf("[zoom] failed to show domain change toast: %v", err) | |||
| 499 | + | } | |||
| 500 | + | } | |||
| 501 | + | lastZoomDomain = currentDomain | |||
| 478 | } | 502 | } | ||
| 479 | }) | 503 | }) | ||
| 480 | // Persist zoom changes per-domain | 504 | // Persist zoom changes per-domain | ||
| @@ -491,18 +515,20 @@ func runBrowser() { | |||||
| 491 | key := services.ZoomKeyForLog(url) | 515 | key := services.ZoomKeyForLog(url) | ||
| 492 | log.Printf("[zoom] saved %.2f for %s", level, key) | 516 | log.Printf("[zoom] saved %.2f for %s", level, key) | ||
| 493 | 517 | ||||
| 494 | - | // Show zoom toast notification | 518 | + | // Only show toast for user-initiated zoom changes, not programmatic ones |
| 495 | - | js := fmt.Sprintf(`try { | 519 | + | if !programmaticZoomChange { |
| 496 | - | if (typeof window.__dumber_showZoomToast === 'function') { | 520 | + | js := fmt.Sprintf(`try { |
| 497 | - | window.__dumber_showZoomToast(%f); | 521 | + | if (typeof window.__dumber_showZoomToast === 'function') { |
| 498 | - | } else { | 522 | + | window.__dumber_showZoomToast(%f); |
| 499 | - | console.warn('[dumber] Zoom toast function not available yet'); | 523 | + | } else { |
| 524 | + | console.warn('[dumber] Zoom toast function not available yet'); | |||
| 525 | + | } | |||
| 526 | + | } catch(e) { | |||
| 527 | + | console.error('[dumber] Zoom toast error:', e); | |||
| 528 | + | }`, level) | |||
| 529 | + | if err := view.InjectScript(js); err != nil { | |||
| 530 | + | log.Printf("[zoom] failed to show toast: %v", err) | |||
| 500 | } | 531 | } | ||
| 501 | - | } catch(e) { | |||
| 502 | - | console.error('[dumber] Zoom toast error:', e); | |||
| 503 | - | }`, level) | |||
| 504 | - | if err := view.InjectScript(js); err != nil { | |||
| 505 | - | log.Printf("[zoom] failed to show toast: %v", err) | |||
| 506 | } | 532 | } | ||
| 507 | }) | 533 | }) | ||
| 508 | // Bridge UCM messages: navigate + history queries | 534 | // Bridge UCM messages: navigate + history queries | ||
| @@ -532,7 +558,9 @@ func runBrowser() { | |||||
| 532 | } | 558 | } | ||
| 533 | _ = view.LoadURL(currentURL) | 559 | _ = view.LoadURL(currentURL) | ||
| 534 | if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil { | 560 | if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil { | ||
| 561 | + | programmaticZoomChange = true | |||
| 535 | _ = view.SetZoom(z) | 562 | _ = view.SetZoom(z) | ||
| 563 | + | programmaticZoomChange = false | |||
| 536 | } | 564 | } | ||
| 537 | } | 565 | } | ||
| 538 | case "query": | 566 | case "query": | ||
| @@ -627,12 +655,14 @@ func runBrowser() { | |||||
| 627 | u = "dumb://homepage" | 655 | u = "dumb://homepage" | ||
| 628 | } | 656 | } | ||
| 629 | if z, zerr := browserService.GetZoomLevel(ctx, u); zerr == nil { | 657 | if z, zerr := browserService.GetZoomLevel(ctx, u); zerr == nil { | ||
| 658 | + | programmaticZoomChange = true | |||
| 630 | if err := view.SetZoom(z); err != nil { | 659 | if err := view.SetZoom(z); err != nil { | ||
| 631 | log.Printf("Warning: failed to set initial zoom: %v", err) | 660 | log.Printf("Warning: failed to set initial zoom: %v", err) | ||
| 632 | } else { | 661 | } else { | ||
| 633 | key := services.ZoomKeyForLog(u) | 662 | key := services.ZoomKeyForLog(u) | ||
| 634 | log.Printf("[zoom] loaded %.2f for %s", z, key) | 663 | log.Printf("[zoom] loaded %.2f for %s", z, key) | ||
| 635 | } | 664 | } | ||
| 665 | + | programmaticZoomChange = false | |||
| 636 | } | 666 | } | ||
| 637 | } | 667 | } | ||
| 638 | 668 | ||||
| @@ -661,12 +691,14 @@ func runBrowser() { | |||||
| 661 | log.Printf("Warning: failed to load URL: %v", err) | 691 | log.Printf("Warning: failed to load URL: %v", err) | ||
| 662 | } | 692 | } | ||
| 663 | if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil { | 693 | if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil { | ||
| 694 | + | programmaticZoomChange = true | |||
| 664 | if err := view.SetZoom(z); err != nil { | 695 | if err := view.SetZoom(z); err != nil { | ||
| 665 | log.Printf("Warning: failed to set zoom: %v", err) | 696 | log.Printf("Warning: failed to set zoom: %v", err) | ||
| 666 | } else { | 697 | } else { | ||
| 667 | key := services.ZoomKeyForLog(currentURL) | 698 | key := services.ZoomKeyForLog(currentURL) | ||
| 668 | log.Printf("[zoom] loaded %.2f for %s", z, key) | 699 | log.Printf("[zoom] loaded %.2f for %s", z, key) | ||
| 669 | } | 700 | } | ||
| 701 | + | programmaticZoomChange = false | |||
| 670 | } | 702 | } | ||
| 671 | // No JS injection; native accelerators handle zoom/devtools. | 703 | // No JS injection; native accelerators handle zoom/devtools. | ||
| 672 | } | 704 | } | ||
| @@ -700,6 +732,19 @@ func runBrowser() { | |||||
| 700 | _ = view.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)`) | 732 | _ = view.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)`) | ||
| 701 | } | 733 | } | ||
| 702 | }) | 734 | }) | ||
| 735 | + | // Page refresh shortcuts | |||
| 736 | + | _ = view.RegisterKeyboardShortcut("cmdorctrl+r", func() { | |||
| 737 | + | log.Printf("Shortcut: Reload page") | |||
| 738 | + | _ = view.Reload() | |||
| 739 | + | }) | |||
| 740 | + | _ = view.RegisterKeyboardShortcut("cmdorctrl+shift+r", func() { | |||
| 741 | + | log.Printf("Shortcut: Hard reload page") | |||
| 742 | + | _ = view.ReloadBypassCache() | |||
| 743 | + | }) | |||
| 744 | + | _ = view.RegisterKeyboardShortcut("F5", func() { | |||
| 745 | + | log.Printf("Shortcut: F5 reload") | |||
| 746 | + | _ = view.Reload() | |||
| 747 | + | }) | |||
| 703 | // Zoom handled natively in webkit package (built-in shortcuts) | 748 | // Zoom handled natively in webkit package (built-in shortcuts) | ||
| 704 | } | 749 | } | ||
| 705 | 750 | ||||
M pkg/webkit/keyboard_cgo.go +6 -0
| @@ -145,8 +145,14 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||
| 145 | 145 | keyName = "ArrowRight" | |
| 146 | 146 | case uint(C.GDK_KEY_F12): | |
| 147 | 147 | keyName = "F12" | |
| 148 | + | case uint(C.GDK_KEY_F5): | |
| 149 | + | keyName = "F5" | |
| 148 | 150 | case uint(C.GDK_KEY_c), uint(C.GDK_KEY_C): | |
| 149 | 151 | keyName = "c" | |
| 152 | + | case uint(C.GDK_KEY_r), uint(C.GDK_KEY_R): | |
| 153 | + | keyName = "r" | |
| 154 | + | case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L): | |
| 155 | + | keyName = "l" | |
| 150 | 156 | default: | |
| 151 | 157 | // If Control held, log unknown keyval for diagnostics | |
| 152 | 158 | if ctrl { | |
| @@ -145,8 +145,14 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||||
| 145 | keyName = "ArrowRight" | 145 | keyName = "ArrowRight" | ||
| 146 | case uint(C.GDK_KEY_F12): | 146 | case uint(C.GDK_KEY_F12): | ||
| 147 | keyName = "F12" | 147 | keyName = "F12" | ||
| 148 | + | case uint(C.GDK_KEY_F5): | |||
| 149 | + | keyName = "F5" | |||
| 148 | case uint(C.GDK_KEY_c), uint(C.GDK_KEY_C): | 150 | case uint(C.GDK_KEY_c), uint(C.GDK_KEY_C): | ||
| 149 | keyName = "c" | 151 | keyName = "c" | ||
| 152 | + | case uint(C.GDK_KEY_r), uint(C.GDK_KEY_R): | |||
| 153 | + | keyName = "r" | |||
| 154 | + | case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L): | |||
| 155 | + | keyName = "l" | |||
| 150 | default: | 156 | default: | ||
| 151 | // If Control held, log unknown keyval for diagnostics | 157 | // If Control held, log unknown keyval for diagnostics | ||
| 152 | if ctrl { | 158 | if ctrl { | ||
M pkg/webkit/webview.go +6 -0
| @@ -93,6 +93,12 @@ func (w *WebView) GoBack() error { return ErrNotImplemented } | |||
| 93 | 93 | // GoForward is not supported in the non-CGO stub. | |
| 94 | 94 | func (w *WebView) GoForward() error { return ErrNotImplemented } | |
| 95 | 95 | ||
| 96 | + | // Reload is not supported in the non-CGO stub. | |
| 97 | + | func (w *WebView) Reload() error { return ErrNotImplemented } | |
| 98 | + | ||
| 99 | + | // ReloadBypassCache is not supported in the non-CGO stub. | |
| 100 | + | func (w *WebView) ReloadBypassCache() error { return ErrNotImplemented } | |
| 101 | + | ||
| 96 | 102 | // ShowDevTools is a no-op in the non-CGO build. | |
| 97 | 103 | func (w *WebView) ShowDevTools() error { return nil } | |
| 98 | 104 | ||
| @@ -93,6 +93,12 @@ func (w *WebView) GoBack() error { return ErrNotImplemented } | |||||
| 93 | // GoForward is not supported in the non-CGO stub. | 93 | // GoForward is not supported in the non-CGO stub. | ||
| 94 | func (w *WebView) GoForward() error { return ErrNotImplemented } | 94 | func (w *WebView) GoForward() error { return ErrNotImplemented } | ||
| 95 | 95 | ||||
| 96 | + | // Reload is not supported in the non-CGO stub. | |||
| 97 | + | func (w *WebView) Reload() error { return ErrNotImplemented } | |||
| 98 | + | ||||
| 99 | + | // ReloadBypassCache is not supported in the non-CGO stub. | |||
| 100 | + | func (w *WebView) ReloadBypassCache() error { return ErrNotImplemented } | |||
| 101 | + | ||||
| 96 | // ShowDevTools is a no-op in the non-CGO build. | 102 | // ShowDevTools is a no-op in the non-CGO build. | ||
| 97 | func (w *WebView) ShowDevTools() error { return nil } | 103 | func (w *WebView) ShowDevTools() error { return nil } | ||
| 98 | 104 | ||||
M pkg/webkit/webview_cgo.go +41 -0
| @@ -38,6 +38,26 @@ static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view, | |||
| 38 | 38 | GTlsCertificate *certificate, | |
| 39 | 39 | GTlsCertificateFlags errors, | |
| 40 | 40 | gpointer user_data); | |
| 41 | + | ||
| 42 | + | // Handle create signal for _blank links - redirect to current window | |
| 43 | + | static WebKitWebView* on_create_new_window(WebKitWebView* web_view, | |
| 44 | + | WebKitNavigationAction* navigation_action, | |
| 45 | + | gpointer user_data) { | |
| 46 | + | (void)user_data; | |
| 47 | + | ||
| 48 | + | // Get the request from navigation action | |
| 49 | + | WebKitURIRequest* request = webkit_navigation_action_get_request(navigation_action); | |
| 50 | + | if (request) { | |
| 51 | + | const gchar* uri = webkit_uri_request_get_uri(request); | |
| 52 | + | if (uri) { | |
| 53 | + | // Load in current window instead of creating new one | |
| 54 | + | webkit_web_view_load_uri(web_view, uri); | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // Return NULL to prevent new window creation | |
| 59 | + | return NULL; | |
| 60 | + | } | |
| 41 | 61 | static void connect_tls_error_handler(WebKitWebView* wv); | |
| 42 | 62 | ||
| 43 | 63 | // Dialog callback structure for TLS warnings | |
| @@ -116,6 +136,9 @@ static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const c | |||
| 116 | 136 | GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 }; | |
| 117 | 137 | webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color); | |
| 118 | 138 | ||
| 139 | + | // Handle create signal to redirect _blank links to current window | |
| 140 | + | g_signal_connect(G_OBJECT(w), "create", G_CALLBACK(on_create_new_window), NULL); | |
| 141 | + | ||
| 119 | 142 | // Connect TLS error handler | |
| 120 | 143 | connect_tls_error_handler(WEBKIT_WEB_VIEW(w)); | |
| 121 | 144 | if (out_ucm) { *out_ucm = u; } | |
| @@ -1034,6 +1057,24 @@ func (w *WebView) GoForward() error { | |||
| 1034 | 1057 | return nil | |
| 1035 | 1058 | } | |
| 1036 | 1059 | ||
| 1060 | + | func (w *WebView) Reload() error { | |
| 1061 | + | if w == nil || w.destroyed || w.native == nil || w.native.wv == nil { | |
| 1062 | + | return ErrNotImplemented | |
| 1063 | + | } | |
| 1064 | + | C.webkit_web_view_reload(w.native.wv) | |
| 1065 | + | log.Printf("[webkit] Reload page") | |
| 1066 | + | return nil | |
| 1067 | + | } | |
| 1068 | + | ||
| 1069 | + | func (w *WebView) ReloadBypassCache() error { | |
| 1070 | + | if w == nil || w.destroyed || w.native == nil || w.native.wv == nil { | |
| 1071 | + | return ErrNotImplemented | |
| 1072 | + | } | |
| 1073 | + | C.webkit_web_view_reload_bypass_cache(w.native.wv) | |
| 1074 | + | log.Printf("[webkit] Hard reload page (bypass cache)") | |
| 1075 | + | return nil | |
| 1076 | + | } | |
| 1077 | + | ||
| 1037 | 1078 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | |
| 1038 | 1079 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | |
| 1039 | 1080 | ||
| @@ -38,6 +38,26 @@ static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view, | |||||
| 38 | GTlsCertificate *certificate, | 38 | GTlsCertificate *certificate, | ||
| 39 | GTlsCertificateFlags errors, | 39 | GTlsCertificateFlags errors, | ||
| 40 | gpointer user_data); | 40 | gpointer user_data); | ||
| 41 | + | ||||
| 42 | + | // Handle create signal for _blank links - redirect to current window | |||
| 43 | + | static WebKitWebView* on_create_new_window(WebKitWebView* web_view, | |||
| 44 | + | WebKitNavigationAction* navigation_action, | |||
| 45 | + | gpointer user_data) { | |||
| 46 | + | (void)user_data; | |||
| 47 | + | ||||
| 48 | + | // Get the request from navigation action | |||
| 49 | + | WebKitURIRequest* request = webkit_navigation_action_get_request(navigation_action); | |||
| 50 | + | if (request) { | |||
| 51 | + | const gchar* uri = webkit_uri_request_get_uri(request); | |||
| 52 | + | if (uri) { | |||
| 53 | + | // Load in current window instead of creating new one | |||
| 54 | + | webkit_web_view_load_uri(web_view, uri); | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // Return NULL to prevent new window creation | |||
| 59 | + | return NULL; | |||
| 60 | + | } | |||
| 41 | static void connect_tls_error_handler(WebKitWebView* wv); | 61 | static void connect_tls_error_handler(WebKitWebView* wv); | ||
| 42 | 62 | ||||
| 43 | // Dialog callback structure for TLS warnings | 63 | // Dialog callback structure for TLS warnings | ||
| @@ -116,6 +136,9 @@ static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const c | |||||
| 116 | GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 }; | 136 | GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 }; | ||
| 117 | webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color); | 137 | webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color); | ||
| 118 | 138 | ||||
| 139 | + | // Handle create signal to redirect _blank links to current window | |||
| 140 | + | g_signal_connect(G_OBJECT(w), "create", G_CALLBACK(on_create_new_window), NULL); | |||
| 141 | + | ||||
| 119 | // Connect TLS error handler | 142 | // Connect TLS error handler | ||
| 120 | connect_tls_error_handler(WEBKIT_WEB_VIEW(w)); | 143 | connect_tls_error_handler(WEBKIT_WEB_VIEW(w)); | ||
| 121 | if (out_ucm) { *out_ucm = u; } | 144 | if (out_ucm) { *out_ucm = u; } | ||
| @@ -1034,6 +1057,24 @@ func (w *WebView) GoForward() error { | |||||
| 1034 | return nil | 1057 | return nil | ||
| 1035 | } | 1058 | } | ||
| 1036 | 1059 | ||||
| 1060 | + | func (w *WebView) Reload() error { | |||
| 1061 | + | if w == nil || w.destroyed || w.native == nil || w.native.wv == nil { | |||
| 1062 | + | return ErrNotImplemented | |||
| 1063 | + | } | |||
| 1064 | + | C.webkit_web_view_reload(w.native.wv) | |||
| 1065 | + | log.Printf("[webkit] Reload page") | |||
| 1066 | + | return nil | |||
| 1067 | + | } | |||
| 1068 | + | ||||
| 1069 | + | func (w *WebView) ReloadBypassCache() error { | |||
| 1070 | + | if w == nil || w.destroyed || w.native == nil || w.native.wv == nil { | |||
| 1071 | + | return ErrNotImplemented | |||
| 1072 | + | } | |||
| 1073 | + | C.webkit_web_view_reload_bypass_cache(w.native.wv) | |||
| 1074 | + | log.Printf("[webkit] Hard reload page (bypass cache)") | |||
| 1075 | + | return nil | |||
| 1076 | + | } | |||
| 1077 | + | ||||
| 1037 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | 1078 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | ||
| 1038 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | 1079 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | ||
| 1039 | 1080 | ||||