mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
Merge branch 'v3-alpha' into v3-bugfix/4910-systray-highlight-and-popup-level
This commit is contained in:
commit
a36c1f9614
23 changed files with 272 additions and 184 deletions
|
|
@ -30,6 +30,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
*/
|
||||
## [Unreleased]
|
||||
|
||||
## v3.0.0-alpha.73 - 2026-02-27
|
||||
|
||||
## Fixed
|
||||
- Fix frameless window cannot be minimized on darwin (#4294)
|
||||
|
||||
## v3.0.0-alpha.72 - 2026-02-16
|
||||
|
||||
## Fixed
|
||||
|
|
|
|||
|
|
@ -269,9 +269,9 @@ app.Event.Emit("user-logged-in", user)
|
|||
|
||||
```javascript
|
||||
// JavaScript: Listen for event
|
||||
import { On } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
On('user-logged-in', (user) => {
|
||||
Events.On('user-logged-in', (user) => {
|
||||
console.log('User logged in:', user)
|
||||
})
|
||||
```
|
||||
|
|
|
|||
|
|
@ -549,11 +549,11 @@ func ProcessLargeFile(path string) error {
|
|||
```
|
||||
|
||||
```javascript
|
||||
import { On } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
import { ProcessLargeFile } from './bindings/FileService'
|
||||
|
||||
// Listen for progress
|
||||
On('file-progress', (data) => {
|
||||
Events.On('file-progress', (data) => {
|
||||
console.log(`Line ${data.line}: ${data.text}`)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -139,9 +139,9 @@ export namespace backend {
|
|||
### JavaScript Side
|
||||
|
||||
```ts
|
||||
import { invoke } from "@wailsio/runtime";
|
||||
import { System } from "@wailsio/runtime";
|
||||
|
||||
await invoke(0x7a1201d3 /* ChatService.Send */, ["Hello"]);
|
||||
await System.invoke(0x7a1201d3 /* ChatService.Send */, ["Hello"]);
|
||||
```
|
||||
|
||||
Implementation: `runtime/desktop/@wailsio/runtime/invoke.ts`
|
||||
|
|
|
|||
|
|
@ -199,14 +199,14 @@ func ShowConfirmdialog(message string) bool {
|
|||
</div>
|
||||
|
||||
<script>
|
||||
import { OnEvent, Emit } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
OnEvent("set-message", (message) => {
|
||||
Events.On("set-message", (message) => {
|
||||
document.getElementById("message").textContent = message
|
||||
})
|
||||
|
||||
function confirm(result) {
|
||||
Emit(result ? "confirm-yes" : "confirm-no")
|
||||
Events.Emit(result ? "confirm-yes" : "confirm-no")
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
|
@ -389,17 +389,17 @@ func (ld *Logindialog) Cancel() {
|
|||
</div>
|
||||
|
||||
<script>
|
||||
import { Emit } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault()
|
||||
const username = document.getElementById('username').value
|
||||
const password = document.getElementById('password').value
|
||||
Emit('login-submit', { username, password })
|
||||
Events.Emit('login-submit', { username, password })
|
||||
})
|
||||
|
||||
function cancel() {
|
||||
Emit('login-cancel')
|
||||
Events.Emit('login-cancel')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -129,13 +129,13 @@ window.EmitEvent("window-specific-event", data)
|
|||
### From JavaScript
|
||||
|
||||
```javascript
|
||||
import { Emit } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
// Emit to Go
|
||||
Emit("button-clicked", { buttonId: "submit" })
|
||||
Events.Emit("button-clicked", { buttonId: "submit" })
|
||||
|
||||
// Emit to all windows
|
||||
Emit("broadcast-message", "Hello everyone")
|
||||
Events.Emit("broadcast-message", "Hello everyone")
|
||||
```
|
||||
|
||||
## Listening to Events
|
||||
|
|
@ -506,7 +506,7 @@ func main() {
|
|||
**JavaScript:**
|
||||
|
||||
```javascript
|
||||
import { Events, Emit } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
// Listen for notifications
|
||||
Events.On("notification", (event) => {
|
||||
|
|
|
|||
|
|
@ -166,11 +166,11 @@ Use the `--wails-draggable` CSS property:
|
|||
**JavaScript for buttons:**
|
||||
|
||||
```javascript
|
||||
import { WindowMinimise, WindowMaximise, WindowClose } from '@wailsio/runtime'
|
||||
import { Window } from '@wailsio/runtime'
|
||||
|
||||
document.querySelector('.minimize').addEventListener('click', WindowMinimise)
|
||||
document.querySelector('.maximize').addEventListener('click', WindowMaximise)
|
||||
document.querySelector('.close').addEventListener('click', WindowClose)
|
||||
document.querySelector('.minimize').addEventListener('click', () => Window.Minimise())
|
||||
document.querySelector('.maximize').addEventListener('click', () => Window.Maximise())
|
||||
document.querySelector('.close').addEventListener('click', () => Window.Close())
|
||||
```
|
||||
|
||||
## System Buttons
|
||||
|
|
@ -214,15 +214,11 @@ document.querySelector('.close').addEventListener('click', Close)
|
|||
**Or use runtime methods:**
|
||||
|
||||
```javascript
|
||||
import {
|
||||
WindowMinimise,
|
||||
WindowMaximise,
|
||||
WindowClose
|
||||
} from '@wailsio/runtime'
|
||||
import { Window } from '@wailsio/runtime'
|
||||
|
||||
document.querySelector('.minimize').addEventListener('click', WindowMinimise)
|
||||
document.querySelector('.maximize').addEventListener('click', WindowMaximise)
|
||||
document.querySelector('.close').addEventListener('click', WindowClose)
|
||||
document.querySelector('.minimize').addEventListener('click', () => Window.Minimise())
|
||||
document.querySelector('.maximize').addEventListener('click', () => Window.Maximise())
|
||||
document.querySelector('.close').addEventListener('click', () => Window.Close())
|
||||
```
|
||||
|
||||
### Toggle Maximise State
|
||||
|
|
@ -230,22 +226,22 @@ document.querySelector('.close').addEventListener('click', WindowClose)
|
|||
Track maximise state for button icon:
|
||||
|
||||
```javascript
|
||||
import { WindowIsMaximised, WindowMaximise, WindowUnMaximise } from '@wailsio/runtime'
|
||||
import { Window } from '@wailsio/runtime'
|
||||
|
||||
async function toggleMaximise() {
|
||||
const isMaximised = await WindowIsMaximised()
|
||||
|
||||
const isMaximised = await Window.IsMaximised()
|
||||
|
||||
if (isMaximised) {
|
||||
await WindowUnMaximise()
|
||||
await Window.Restore()
|
||||
} else {
|
||||
await WindowMaximise()
|
||||
await Window.Maximise()
|
||||
}
|
||||
|
||||
|
||||
updateMaximiseButton()
|
||||
}
|
||||
|
||||
async function updateMaximiseButton() {
|
||||
const isMaximised = await WindowIsMaximised()
|
||||
const isMaximised = await Window.IsMaximised()
|
||||
const button = document.querySelector('.maximize')
|
||||
button.textContent = isMaximised ? '❐' : '□'
|
||||
}
|
||||
|
|
@ -737,41 +733,35 @@ body {
|
|||
**JavaScript:**
|
||||
|
||||
```javascript
|
||||
import {
|
||||
WindowMinimise,
|
||||
WindowMaximise,
|
||||
WindowUnMaximise,
|
||||
WindowIsMaximised,
|
||||
WindowClose
|
||||
} from '@wailsio/runtime'
|
||||
import { Window } from '@wailsio/runtime'
|
||||
|
||||
// Minimise button
|
||||
document.querySelector('.minimize').addEventListener('click', () => {
|
||||
WindowMinimise()
|
||||
Window.Minimise()
|
||||
})
|
||||
|
||||
// Maximise/restore button
|
||||
const maximiseBtn = document.querySelector('.maximize')
|
||||
maximiseBtn.addEventListener('click', async () => {
|
||||
const isMaximised = await WindowIsMaximised()
|
||||
|
||||
const isMaximised = await Window.IsMaximised()
|
||||
|
||||
if (isMaximised) {
|
||||
await WindowUnMaximise()
|
||||
await Window.Restore()
|
||||
} else {
|
||||
await WindowMaximise()
|
||||
await Window.Maximise()
|
||||
}
|
||||
|
||||
|
||||
updateMaximiseButton()
|
||||
})
|
||||
|
||||
// Close button
|
||||
document.querySelector('.close').addEventListener('click', () => {
|
||||
WindowClose()
|
||||
Window.Close()
|
||||
})
|
||||
|
||||
// Update maximise button icon
|
||||
async function updateMaximiseButton() {
|
||||
const isMaximised = await WindowIsMaximised()
|
||||
const isMaximised = await Window.IsMaximised()
|
||||
maximiseBtn.textContent = isMaximised ? '❐' : '□'
|
||||
maximiseBtn.title = isMaximised ? 'Restore' : 'Maximise'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -518,13 +518,12 @@ useEffect(() => {
|
|||
Check platform availability when using platform-specific events:
|
||||
|
||||
```javascript
|
||||
import { Platform, Events } from '@wailsio/runtime';
|
||||
import { Events } from '@wailsio/runtime';
|
||||
|
||||
if (Platform.isWindows) {
|
||||
Events.On('windows:APMSuspend', handleSuspend);
|
||||
} else if (Platform.isMac) {
|
||||
Events.On('mac:ApplicationWillTerminate', handleTerminate);
|
||||
}
|
||||
// Platform-specific events can be registered unconditionally;
|
||||
// they will simply never fire on unsupported platforms.
|
||||
Events.On('windows:APMSuspend', handleSuspend);
|
||||
Events.On('mac:ApplicationWillTerminate', handleTerminate);
|
||||
```
|
||||
|
||||
### 4. Don't Overuse Events
|
||||
|
|
|
|||
|
|
@ -456,13 +456,13 @@ EventsOn("update", (data) => {
|
|||
EventsEmit("action", data)
|
||||
|
||||
// v3
|
||||
import { OnEvent, Emit } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
OnEvent("update", (data) => {
|
||||
Events.On("update", (data) => {
|
||||
console.log(data)
|
||||
})
|
||||
|
||||
Emit("action", data)
|
||||
Events.Emit("action", data)
|
||||
```
|
||||
|
||||
### Step 6: Update Configuration
|
||||
|
|
|
|||
|
|
@ -97,9 +97,9 @@ app.Event.Emit("global-update", data)
|
|||
Listens for events from Go.
|
||||
|
||||
```javascript
|
||||
import { On } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
On(eventName, callback)
|
||||
Events.On(eventName, callback)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
|
@ -110,10 +110,10 @@ On(eventName, callback)
|
|||
|
||||
**Example:**
|
||||
```javascript
|
||||
import { On } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
// Listen for events
|
||||
const cleanup = On('data-updated', (data) => {
|
||||
const cleanup = Events.On('data-updated', (data) => {
|
||||
console.log('Count:', data.count)
|
||||
console.log('Status:', data.status)
|
||||
updateUI(data)
|
||||
|
|
@ -128,17 +128,17 @@ cleanup()
|
|||
Listens for a single event occurrence.
|
||||
|
||||
```javascript
|
||||
import { Once } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
Once(eventName, callback)
|
||||
Events.Once(eventName, callback)
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
import { Once } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
// Listen for first occurrence only
|
||||
Once('initialization-complete', (data) => {
|
||||
Events.Once('initialization-complete', (data) => {
|
||||
console.log('App initialized!', data)
|
||||
// This will only fire once
|
||||
})
|
||||
|
|
@ -149,24 +149,24 @@ Once('initialization-complete', (data) => {
|
|||
Removes an event listener.
|
||||
|
||||
```javascript
|
||||
import { Off } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
Off(eventName, callback)
|
||||
Events.Off(eventName, callback)
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
import { On, Off } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
const handler = (data) => {
|
||||
console.log('Event received:', data)
|
||||
}
|
||||
|
||||
// Start listening
|
||||
On('my-event', handler)
|
||||
Events.On('my-event', handler)
|
||||
|
||||
// Stop listening
|
||||
Off('my-event', handler)
|
||||
Events.Off('my-event', handler)
|
||||
```
|
||||
|
||||
### OffAll()
|
||||
|
|
@ -174,9 +174,9 @@ Off('my-event', handler)
|
|||
Removes all listeners for an event.
|
||||
|
||||
```javascript
|
||||
import { OffAll } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
OffAll(eventName)
|
||||
Events.OffAll(eventName)
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
|
@ -281,10 +281,10 @@ func (s *DataService) FetchData(query string) ([]Item, error) {
|
|||
|
||||
```javascript
|
||||
import { FetchData } from './bindings/DataService'
|
||||
import { On } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
// Listen for completion event
|
||||
On('data-fetched', (data) => {
|
||||
Events.On('data-fetched', (data) => {
|
||||
console.log(`Fetched ${data.count} items for query: ${data.query}`)
|
||||
showNotification(`Found ${data.count} results`)
|
||||
})
|
||||
|
|
@ -325,16 +325,16 @@ func (s *Service) ProcessFiles(files []string) error {
|
|||
**JavaScript:**
|
||||
|
||||
```javascript
|
||||
import { On, Once } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
// Update progress bar
|
||||
On('progress', (data) => {
|
||||
Events.On('progress', (data) => {
|
||||
progressBar.style.width = `${data.percent}%`
|
||||
statusText.textContent = `Processing ${data.file}... (${data.current}/${data.total})`
|
||||
})
|
||||
|
||||
// Handle completion
|
||||
Once('processing-complete', () => {
|
||||
Events.Once('processing-complete', () => {
|
||||
progressBar.style.width = '100%'
|
||||
statusText.textContent = 'Complete!'
|
||||
setTimeout(() => hideProgressBar(), 2000)
|
||||
|
|
@ -364,10 +364,10 @@ window1.OnEvent("request-data", func(e *application.CustomEvent) {
|
|||
**JavaScript:**
|
||||
|
||||
```javascript
|
||||
import { On } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
// Listen in any window
|
||||
On('theme-changed', (theme) => {
|
||||
Events.On('theme-changed', (theme) => {
|
||||
document.body.className = theme
|
||||
})
|
||||
```
|
||||
|
|
@ -407,13 +407,13 @@ func (s *StateService) GetState(key string) interface{} {
|
|||
**JavaScript:**
|
||||
|
||||
```javascript
|
||||
import { On } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
import { GetState } from './bindings/StateService'
|
||||
|
||||
// Keep local state in sync
|
||||
let localState = {}
|
||||
|
||||
On('state-updated', async (data) => {
|
||||
Events.On('state-updated', async (data) => {
|
||||
localState[data.key] = data.value
|
||||
updateUI(data.key, data.value)
|
||||
})
|
||||
|
|
@ -459,10 +459,10 @@ func (s *NotificationService) Info(message string) {
|
|||
**JavaScript:**
|
||||
|
||||
```javascript
|
||||
import { On } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
// Unified notification handler
|
||||
On('notification', (data) => {
|
||||
Events.On('notification', (data) => {
|
||||
const toast = document.createElement('div')
|
||||
toast.className = `toast toast-${data.type}`
|
||||
toast.textContent = data.message
|
||||
|
|
@ -568,34 +568,34 @@ func main() {
|
|||
**JavaScript:**
|
||||
|
||||
```javascript
|
||||
import { On, Once } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
import { StartLongTask, BroadcastMessage } from './bindings/EventDemoService'
|
||||
|
||||
// Task events
|
||||
On('task-started', () => {
|
||||
Events.On('task-started', () => {
|
||||
console.log('Task started...')
|
||||
document.getElementById('status').textContent = 'Running...'
|
||||
})
|
||||
|
||||
On('task-progress', (data) => {
|
||||
Events.On('task-progress', (data) => {
|
||||
const progressBar = document.getElementById('progress')
|
||||
progressBar.style.width = `${data.percent}%`
|
||||
console.log(`Step ${data.step} of ${data.total}`)
|
||||
})
|
||||
|
||||
Once('task-completed', (data) => {
|
||||
Events.Once('task-completed', (data) => {
|
||||
console.log('Task completed!', data.message)
|
||||
document.getElementById('status').textContent = data.message
|
||||
})
|
||||
|
||||
// Broadcast events
|
||||
On('broadcast', (message) => {
|
||||
Events.On('broadcast', (message) => {
|
||||
console.log('Broadcast:', message)
|
||||
alert(message)
|
||||
})
|
||||
|
||||
// Window state events
|
||||
On('window-state', (state) => {
|
||||
Events.On('window-state', (state) => {
|
||||
console.log('Window is now:', state)
|
||||
document.body.dataset.windowState = state
|
||||
})
|
||||
|
|
@ -784,12 +784,12 @@ func (s *Service) EmitWithDebounce(event string, data interface{}) {
|
|||
### Throttling Events
|
||||
|
||||
```javascript
|
||||
import { On } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
let lastUpdate = 0
|
||||
const throttleMs = 100
|
||||
|
||||
On('high-frequency-event', (data) => {
|
||||
Events.On('high-frequency-event', (data) => {
|
||||
const now = Date.now()
|
||||
if (now - lastUpdate < throttleMs) {
|
||||
return // Skip this update
|
||||
|
|
|
|||
|
|
@ -159,9 +159,7 @@ Events.Emit(UserUpdated({
|
|||
The runtime is organized into modules, each providing specific functionality. Import only what you need:
|
||||
|
||||
```javascript
|
||||
import { On, Emit } from '@wailsio/runtime/events'
|
||||
import Window from '@wailsio/runtime/window'
|
||||
import { SetText, Text } from '@wailsio/runtime/clipboard'
|
||||
import { Events, Window, Clipboard } from '@wailsio/runtime'
|
||||
```
|
||||
|
||||
### Events
|
||||
|
|
@ -182,17 +180,17 @@ function On<T>(eventType: EventType<T>, callback: (event: WailsEvent<T>) => void
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { On } from '@wailsio/runtime/events'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
// Basic event listening
|
||||
const unsubscribe = On('user-logged-in', (event) => {
|
||||
const unsubscribe = Events.On('user-logged-in', (event) => {
|
||||
console.log('User:', event.data.username)
|
||||
})
|
||||
|
||||
// With typed events (TypeScript)
|
||||
import { UserLogin } from './bindings/events'
|
||||
|
||||
On(UserLogin, (event) => {
|
||||
Events.On(UserLogin, (event) => {
|
||||
// event.data is typed as UserLoginData
|
||||
console.log('User:', event.data.username)
|
||||
})
|
||||
|
|
@ -212,9 +210,9 @@ function Once<T>(eventType: EventType<T>, callback: (event: WailsEvent<T>) => vo
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { Once } from '@wailsio/runtime/events'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
Once('app-ready', () => {
|
||||
Events.Once('app-ready', () => {
|
||||
console.log('App initialized')
|
||||
})
|
||||
```
|
||||
|
|
@ -233,15 +231,15 @@ function Emit<T>(event: Event<T>): Promise<boolean>
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { Emit } from '@wailsio/runtime/events'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
// Basic event emission
|
||||
const wasCancelled = await Emit('button-clicked', { buttonId: 'submit' })
|
||||
const wasCancelled = await Events.Emit('button-clicked', { buttonId: 'submit' })
|
||||
|
||||
// With typed events (TypeScript)
|
||||
import { UserLogin } from './bindings/events'
|
||||
|
||||
const cancelled = await Emit(UserLogin({
|
||||
const cancelled = await Events.Emit(UserLogin({
|
||||
UserID: "123",
|
||||
Username: "john_doe",
|
||||
LoginTime: new Date().toISOString()
|
||||
|
|
@ -267,9 +265,9 @@ function Off(...eventNames: string[]): void
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { Off } from '@wailsio/runtime/events'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
Off('user-logged-in', 'user-logged-out')
|
||||
Events.Off('user-logged-in', 'user-logged-out')
|
||||
```
|
||||
|
||||
#### OffAll()
|
||||
|
|
@ -285,7 +283,7 @@ function OffAll(): void
|
|||
Window management methods. The default export is the current window.
|
||||
|
||||
```javascript
|
||||
import Window from '@wailsio/runtime/window'
|
||||
import { Window } from '@wailsio/runtime'
|
||||
|
||||
// Current window
|
||||
await Window.SetTitle('New Title')
|
||||
|
|
@ -351,7 +349,7 @@ function Center(): Promise<void>
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import Window from '@wailsio/runtime/window'
|
||||
import { Window } from '@wailsio/runtime'
|
||||
|
||||
// Resize and center
|
||||
await Window.SetSize(800, 600)
|
||||
|
|
@ -514,7 +512,7 @@ function Print(): Promise<void>
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import Window from '@wailsio/runtime/window'
|
||||
import { Window } from '@wailsio/runtime'
|
||||
|
||||
// Open print dialog for current window
|
||||
await Window.Print()
|
||||
|
|
@ -537,9 +535,9 @@ function SetText(text: string): Promise<void>
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { SetText } from '@wailsio/runtime/clipboard'
|
||||
import { Clipboard } from '@wailsio/runtime'
|
||||
|
||||
await SetText('Hello from Wails!')
|
||||
await Clipboard.SetText('Hello from Wails!')
|
||||
```
|
||||
|
||||
#### Text()
|
||||
|
|
@ -553,9 +551,9 @@ function Text(): Promise<string>
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { Text } from '@wailsio/runtime/clipboard'
|
||||
import { Clipboard } from '@wailsio/runtime'
|
||||
|
||||
const clipboardText = await Text()
|
||||
const clipboardText = await Clipboard.Text()
|
||||
console.log('Clipboard:', clipboardText)
|
||||
```
|
||||
|
||||
|
|
@ -620,11 +618,11 @@ function Quit(): Promise<void>
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { Quit } from '@wailsio/runtime/application'
|
||||
import { Application } from '@wailsio/runtime'
|
||||
|
||||
// Add quit button
|
||||
document.getElementById('quit-btn').addEventListener('click', async () => {
|
||||
await Quit()
|
||||
await Application.Quit()
|
||||
})
|
||||
```
|
||||
|
||||
|
|
@ -643,9 +641,9 @@ function OpenURL(url: string | URL): Promise<void>
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { OpenURL } from '@wailsio/runtime/browser'
|
||||
import { Browser } from '@wailsio/runtime'
|
||||
|
||||
await OpenURL('https://wails.io')
|
||||
await Browser.OpenURL('https://wails.io')
|
||||
```
|
||||
|
||||
### Screens
|
||||
|
|
@ -696,16 +694,16 @@ interface Screen {
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { GetAll, GetPrimary } from '@wailsio/runtime/screens'
|
||||
import { Screens } from '@wailsio/runtime'
|
||||
|
||||
// List all screens
|
||||
const screens = await GetAll()
|
||||
const screens = await Screens.GetAll()
|
||||
screens.forEach(screen => {
|
||||
console.log(`${screen.Name}: ${screen.Size.Width}x${screen.Size.Height}`)
|
||||
})
|
||||
|
||||
// Get primary screen
|
||||
const primary = await GetPrimary()
|
||||
const primary = await Screens.GetPrimary()
|
||||
console.log('Primary screen:', primary.Name)
|
||||
```
|
||||
|
||||
|
|
@ -724,9 +722,9 @@ function Info(options: MessageDialogOptions): Promise<string>
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { Info } from '@wailsio/runtime/dialogs'
|
||||
import { Dialogs } from '@wailsio/runtime'
|
||||
|
||||
await Info({
|
||||
await Dialogs.Info({
|
||||
Title: 'Success',
|
||||
Message: 'Operation completed successfully!'
|
||||
})
|
||||
|
|
@ -759,9 +757,9 @@ function Question(options: QuestionDialogOptions): Promise<string>
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { Question } from '@wailsio/runtime/dialogs'
|
||||
import { Dialogs } from '@wailsio/runtime'
|
||||
|
||||
const result = await Question({
|
||||
const result = await Dialogs.Question({
|
||||
Title: 'Confirm Delete',
|
||||
Message: 'Are you sure you want to delete this file?',
|
||||
Buttons: [
|
||||
|
|
@ -786,9 +784,9 @@ function OpenFile(options: OpenFileDialogOptions): Promise<string | string[]>
|
|||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { OpenFile } from '@wailsio/runtime/dialogs'
|
||||
import { Dialogs } from '@wailsio/runtime'
|
||||
|
||||
const file = await OpenFile({
|
||||
const file = await Dialogs.OpenFile({
|
||||
Title: 'Select Image',
|
||||
Filters: [
|
||||
{ DisplayName: 'Images', Pattern: '*.png;*.jpg;*.jpeg' },
|
||||
|
|
@ -864,14 +862,10 @@ WML provides declarative attributes for common actions. Add attributes to HTML e
|
|||
## Complete Example
|
||||
|
||||
```javascript
|
||||
import { On, Emit } from '@wailsio/runtime/events'
|
||||
import Window from '@wailsio/runtime/window'
|
||||
import { SetText, Text } from '@wailsio/runtime/clipboard'
|
||||
import { Question } from '@wailsio/runtime/dialogs'
|
||||
import { GetAll } from '@wailsio/runtime/screens'
|
||||
import { Events, Window, Clipboard, Dialogs, Screens } from '@wailsio/runtime'
|
||||
|
||||
// Listen for events from Go
|
||||
On('data-updated', (event) => {
|
||||
Events.On('data-updated', (event) => {
|
||||
console.log('Data:', event.data)
|
||||
updateUI(event.data)
|
||||
})
|
||||
|
|
@ -892,12 +886,12 @@ document.getElementById('fullscreen-btn').addEventListener('click', async () =>
|
|||
|
||||
// Clipboard operations
|
||||
document.getElementById('copy-btn').addEventListener('click', async () => {
|
||||
await SetText('Copied from Wails!')
|
||||
await Clipboard.SetText('Copied from Wails!')
|
||||
})
|
||||
|
||||
// Dialog with confirmation
|
||||
document.getElementById('delete-btn').addEventListener('click', async () => {
|
||||
const result = await Question({
|
||||
const result = await Dialogs.Question({
|
||||
Title: 'Confirm',
|
||||
Message: 'Delete this item?',
|
||||
Buttons: [
|
||||
|
|
@ -907,12 +901,12 @@ document.getElementById('delete-btn').addEventListener('click', async () => {
|
|||
})
|
||||
|
||||
if (result === 'Delete') {
|
||||
await Emit('delete-item', { id: currentItemId })
|
||||
await Events.Emit('delete-item', { id: currentItemId })
|
||||
}
|
||||
})
|
||||
|
||||
// Screen information
|
||||
const screens = await GetAll()
|
||||
const screens = await Screens.GetAll()
|
||||
console.log(`Detected ${screens.length} screen(s)`)
|
||||
screens.forEach(screen => {
|
||||
console.log(`- ${screen.Name}: ${screen.Size.Width}x${screen.Size.Height}`)
|
||||
|
|
@ -940,10 +934,9 @@ screens.forEach(screen => {
|
|||
The runtime includes full TypeScript definitions:
|
||||
|
||||
```typescript
|
||||
import { On, WailsEvent } from '@wailsio/runtime/events'
|
||||
import Window from '@wailsio/runtime/window'
|
||||
import { Events, Window } from '@wailsio/runtime'
|
||||
|
||||
On('custom-event', (event: WailsEvent) => {
|
||||
Events.On('custom-event', (event) => {
|
||||
// TypeScript knows event.data, event.name, event.sender
|
||||
console.log(event.data)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -146,8 +146,7 @@ import (
|
|||
import { MyMethod } from './bindings/MyService'
|
||||
|
||||
// Runtime APIs
|
||||
import { On, Emit } from '@wailsio/runtime'
|
||||
import { WindowSetTitle } from '@wailsio/runtime/window'
|
||||
import { Events, Window } from '@wailsio/runtime'
|
||||
```
|
||||
|
||||
## Type Reference
|
||||
|
|
|
|||
|
|
@ -498,9 +498,9 @@ window.EmitEvent("data-updated", map[string]interface{}{
|
|||
|
||||
**Frontend (JavaScript):**
|
||||
```javascript
|
||||
import { On } from '@wailsio/runtime'
|
||||
import { Events } from '@wailsio/runtime'
|
||||
|
||||
On('data-updated', (data) => {
|
||||
Events.On('data-updated', (data) => {
|
||||
console.log('Count:', data.count)
|
||||
console.log('Status:', data.status)
|
||||
})
|
||||
|
|
@ -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 -->
|
||||
|
|
@ -25,6 +26,7 @@ After processing, the content will be moved to the main changelog and this file
|
|||
<!-- Bug fixes -->
|
||||
- Fix system tray menu highlight state on macOS - icon now shows selected state when menu is open (#4910)
|
||||
- Fix system tray attached window appearing behind other windows on macOS - now uses proper popup window level (#4910)
|
||||
- Fix incorrect `@wailsio/runtime` import examples across documentation (#4989)
|
||||
|
||||
## Deprecated
|
||||
<!-- Soon-to-be removed features -->
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
v3.0.0-alpha.72
|
||||
v3.0.0-alpha.73
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ extern void registerListener(unsigned int event);
|
|||
void* windowNew(unsigned int id, int width, int height, bool fraudulentWebsiteWarningEnabled, bool frameless, bool enableDragAndDrop, struct WebviewPreferences preferences) {
|
||||
NSWindowStyleMask styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
|
||||
if (frameless) {
|
||||
styleMask = NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable;
|
||||
styleMask = NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable;
|
||||
}
|
||||
WebviewWindow* window = [[WebviewWindow alloc] initWithContentRect:NSMakeRect(0, 0, width-1, height-1)
|
||||
styleMask:styleMask
|
||||
|
|
@ -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