mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
feat(v3): Modal windows (macOS) (#4839)
* modal window * thread safety * update docs * windows support --------- Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
This commit is contained in:
parent
972bb6faa3
commit
78e701fd54
10 changed files with 139 additions and 38 deletions
|
|
@ -288,7 +288,7 @@ func CreateToolPalette(app *application.Application) *application.WebviewWindow
|
|||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Modal dialogs
|
||||
### Pattern 4: Modal dialogs (macOS only)
|
||||
|
||||
Child windows that block parent:
|
||||
|
||||
|
|
@ -303,14 +303,7 @@ func ShowModaldialog(parent *application.WebviewWindow, title string) {
|
|||
Resizable: false,
|
||||
})
|
||||
|
||||
// Disable parent (platform-specific)
|
||||
parent.SetEnabled(false)
|
||||
|
||||
// Re-enable parent on close
|
||||
dialog.OnDestroy(func() {
|
||||
parent.SetEnabled(true)
|
||||
parent.SetFocus()
|
||||
})
|
||||
parent.AttachModal(dialog)
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -359,51 +352,38 @@ func (e *EditorApp) TogglePreview() {
|
|||
```go
|
||||
childWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Title: "Child Window",
|
||||
Parent: parentWindow,
|
||||
})
|
||||
|
||||
parentWindow.AttachModal(childWindow)
|
||||
```
|
||||
|
||||
**Behaviour:**
|
||||
- Child closes when parent closes
|
||||
- Child stays above parent (on some platforms)
|
||||
- Child minimises with parent (on some platforms)
|
||||
- Child stays above parent
|
||||
- Child moves with parent
|
||||
- Child blocks interaction to parent
|
||||
|
||||
**Platform support:**
|
||||
|
||||
| Feature | macOS | Windows | Linux |
|
||||
|---------|-------|---------|-------|
|
||||
| Auto-close | ✅ | ✅ | ⚠️ Varies |
|
||||
| Stay above | ✅ | ⚠️ Partial | ⚠️ Varies |
|
||||
| Minimise together | ✅ | ❌ | ⚠️ Varies |
|
||||
| macOS | Windows | Linux |
|
||||
|-------|---------|-------|
|
||||
| ✅ | ❌ | ❌ |
|
||||
|
||||
### Modal Behaviour
|
||||
|
||||
Create modal-like behaviour:
|
||||
|
||||
```go
|
||||
func ShowModal(parent *application.WebviewWindow) {
|
||||
modal := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Title: "Modal dialog",
|
||||
func ShowModaldialog(parent *application.WebviewWindow, title string) {
|
||||
dialog := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Title: title,
|
||||
Width: 400,
|
||||
Height: 200,
|
||||
Parent: parent,
|
||||
AlwaysOnTop: true,
|
||||
})
|
||||
|
||||
// Disable parent interaction
|
||||
parent.SetEnabled(false)
|
||||
|
||||
// Re-enable on close
|
||||
modal.OnClose(func() bool {
|
||||
parent.SetEnabled(true)
|
||||
parent.SetFocus()
|
||||
return true
|
||||
})
|
||||
parent.AttachModal(dialog)
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** True modal behaviour (blocking) varies by platform.
|
||||
|
||||
## Window Lifecycle Management
|
||||
|
||||
### Creation Callbacks
|
||||
|
|
|
|||
|
|
@ -597,12 +597,27 @@ if err != nil {
|
|||
}
|
||||
```
|
||||
|
||||
**Platform support:**
|
||||
- **macOS**: Full support
|
||||
- **Windows**: Full support
|
||||
- **Linux**: Full support
|
||||
### AttachModal()
|
||||
|
||||
**Note:** This triggers the native OS print dialog, allowing the user to select printer settings and print the current window content.
|
||||
Attaches a second Window as a sheet modal.
|
||||
|
||||
```go
|
||||
func (w *Window) AttachModal(modalWindow Window)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `modalWindow` - The window to attach as a modal
|
||||
|
||||
**Platform support:**
|
||||
- **macOS**: Full support (presents as a sheet)
|
||||
- **Windows**: No support
|
||||
- **Linux**: No support
|
||||
|
||||
**Example:**
|
||||
```go
|
||||
modalWindow := app.Window.New()
|
||||
window.AttachModal(modalWindow)
|
||||
```
|
||||
|
||||
## Platform-Specific Options
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ After processing, the content will be moved to the main changelog and this file
|
|||
|
||||
## Added
|
||||
<!-- New features, capabilities, or enhancements -->
|
||||
- Add support for modal sheets (macOS)
|
||||
|
||||
## Changed
|
||||
<!-- Changes in existing functionality -->
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ type (
|
|||
setMenu(menu *Menu)
|
||||
snapAssist()
|
||||
setContentProtection(enabled bool)
|
||||
attachModal(modalWindow *WebviewWindow)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -1284,6 +1285,22 @@ func (w *WebviewWindow) NativeWindow() unsafe.Pointer {
|
|||
return w.impl.nativeWindow()
|
||||
}
|
||||
|
||||
// AttachModal attaches a modal window to this window, presenting it as a sheet on macOS.
|
||||
func (w *WebviewWindow) AttachModal(modalWindow Window) {
|
||||
if w.impl == nil || w.isDestroyed() {
|
||||
return
|
||||
}
|
||||
|
||||
modalWebviewWindow, ok := modalWindow.(*WebviewWindow)
|
||||
if !ok || modalWebviewWindow == nil {
|
||||
return
|
||||
}
|
||||
|
||||
InvokeSync(func() {
|
||||
w.impl.attachModal(modalWebviewWindow)
|
||||
})
|
||||
}
|
||||
|
||||
// shouldUnconditionallyClose returns whether the window should close unconditionally
|
||||
func (w *WebviewWindow) shouldUnconditionallyClose() bool {
|
||||
return atomic.LoadUint32(&w.unconditionallyClose) != 0
|
||||
|
|
|
|||
|
|
@ -296,6 +296,10 @@ func (w *androidWebviewWindow) nativeWindow() unsafe.Pointer {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (w *androidWebviewWindow) attachModal(modalWindow *WebviewWindow) {
|
||||
// Modal windows are not supported on Android
|
||||
}
|
||||
|
||||
func (w *androidWebviewWindow) on(eventID uint) {
|
||||
// Android event handling
|
||||
}
|
||||
|
|
|
|||
|
|
@ -331,6 +331,24 @@ void windowZoomOut(void* nsWindow) {
|
|||
}
|
||||
}
|
||||
|
||||
// createModalWindow presents a modal window as a sheet attached to the parent window
|
||||
void createModalWindow(void* parentWindowPtr, void* modalWindowPtr) {
|
||||
if (parentWindowPtr == NULL || modalWindowPtr == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSWindow* parentWindow = (NSWindow*)parentWindowPtr;
|
||||
NSWindow* modalWindow = (NSWindow*)modalWindowPtr;
|
||||
|
||||
// Present the modal window as a sheet attached to the parent window
|
||||
// Must be dispatched to the main thread for UI thread safety
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[parentWindow beginSheet:modalWindow completionHandler:^(NSModalResponse returnCode) {
|
||||
// Sheet was dismissed - window will be released automatically
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
// set the window position relative to the screen
|
||||
void windowSetRelativePosition(void* nsWindow, int x, int y) {
|
||||
WebviewWindow* window = (WebviewWindow*)nsWindow;
|
||||
|
|
@ -1551,6 +1569,17 @@ func (w *macosWebviewWindow) setContentProtection(enabled bool) {
|
|||
C.setContentProtection(w.nsWindow, C.bool(enabled))
|
||||
}
|
||||
|
||||
func (w *macosWebviewWindow) attachModal(modalWindow *WebviewWindow) {
|
||||
if modalWindow == nil || modalWindow.impl == nil || modalWindow.isDestroyed() {
|
||||
return
|
||||
}
|
||||
modalNativeWindow := modalWindow.impl.nativeWindow()
|
||||
if modalNativeWindow == nil {
|
||||
return
|
||||
}
|
||||
C.createModalWindow(w.nsWindow, modalNativeWindow)
|
||||
}
|
||||
|
||||
func (w *macosWebviewWindow) cut() {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -330,6 +330,10 @@ func (w *iosWebviewWindow) nativeWindow() unsafe.Pointer {
|
|||
return w.nativeHandle
|
||||
}
|
||||
|
||||
func (w *iosWebviewWindow) attachModal(modalWindow *WebviewWindow) {
|
||||
// Modal windows are not supported on iOS
|
||||
}
|
||||
|
||||
func (w *iosWebviewWindow) on(eventID uint) {
|
||||
// iOS event handling
|
||||
}
|
||||
|
|
|
|||
|
|
@ -410,6 +410,10 @@ func (w *linuxWebviewWindow) nativeWindow() unsafe.Pointer {
|
|||
return unsafe.Pointer(w.window)
|
||||
}
|
||||
|
||||
func (w *linuxWebviewWindow) attachModal(modalWindow *WebviewWindow) {
|
||||
// Modal windows are not supported on Linux
|
||||
}
|
||||
|
||||
func (w *linuxWebviewWindow) print() error {
|
||||
w.execJS("window.print();")
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ type windowsWebviewWindow struct {
|
|||
|
||||
// menubarTheme is the theme for the menubar
|
||||
menubarTheme *w32.MenuBarTheme
|
||||
|
||||
// Modal window tracking
|
||||
parentHWND w32.HWND // Parent window HWND when this window is a modal
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) setMenu(menu *Menu) {
|
||||
|
|
@ -233,6 +236,37 @@ func (w *windowsWebviewWindow) startDrag() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) attachModal(modalWindow *WebviewWindow) {
|
||||
if modalWindow == nil || modalWindow.impl == nil || modalWindow.isDestroyed() {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the modal window's Windows implementation
|
||||
modalWindowsImpl, ok := modalWindow.impl.(*windowsWebviewWindow)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
parentHWND := w.hwnd
|
||||
modalHWND := modalWindowsImpl.hwnd
|
||||
|
||||
// Set parent-child relationship using GWLP_HWNDPARENT
|
||||
// This ensures the modal stays above parent and moves with it
|
||||
w32.SetWindowLongPtr(modalHWND, w32.GWLP_HWNDPARENT, uintptr(parentHWND))
|
||||
|
||||
// Track the parent HWND in the modal window for cleanup
|
||||
modalWindowsImpl.parentHWND = parentHWND
|
||||
|
||||
// Disable the parent window to block interaction (Microsoft's recommended approach)
|
||||
// This follows Windows modal dialog best practices
|
||||
w32.EnableWindow(parentHWND, false)
|
||||
|
||||
// Ensure modal window is shown and brought to front
|
||||
w32.ShowWindow(modalHWND, w32.SW_SHOW)
|
||||
w32.SetForegroundWindow(modalHWND)
|
||||
w32.BringWindowToTop(modalHWND)
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) nativeWindow() unsafe.Pointer {
|
||||
return unsafe.Pointer(w.hwnd)
|
||||
}
|
||||
|
|
@ -725,6 +759,12 @@ func (w *windowsWebviewWindow) setRelativePosition(x int, y int) {
|
|||
}
|
||||
|
||||
func (w *windowsWebviewWindow) destroy() {
|
||||
// Re-enable parent window if this was a modal window
|
||||
if w.parentHWND != 0 {
|
||||
w32.EnableWindow(w.parentHWND, true)
|
||||
w.parentHWND = 0
|
||||
}
|
||||
|
||||
w.parent.markAsDestroyed()
|
||||
// destroy the window
|
||||
w32.DestroyWindow(w.hwnd)
|
||||
|
|
@ -1420,6 +1460,12 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
|
|||
}
|
||||
|
||||
defer func() {
|
||||
// Re-enable parent window if this was a modal window
|
||||
if w.parentHWND != 0 {
|
||||
w32.EnableWindow(w.parentHWND, true)
|
||||
w.parentHWND = 0
|
||||
}
|
||||
|
||||
windowsApp := globalApplication.impl.(*windowsApp)
|
||||
windowsApp.unregisterWindow(w)
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ type Window interface {
|
|||
Flash(enabled bool)
|
||||
Print() error
|
||||
RegisterHook(eventType events.WindowEventType, callback func(event *WindowEvent)) func()
|
||||
AttachModal(modalWindow Window)
|
||||
shouldUnconditionallyClose() bool
|
||||
|
||||
// Editing methods
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue