323 lines
9.1 KiB
Go
323 lines
9.1 KiB
Go
// Copyright 2019 The Walk Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// +build windows
|
|
|
|
package walk
|
|
|
|
import (
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"github.com/lxn/win"
|
|
)
|
|
|
|
// The global window group manager instance.
|
|
var wgm windowGroupManager
|
|
|
|
// windowGroupManager manages window groups for each thread with one or
|
|
// more windows.
|
|
type windowGroupManager struct {
|
|
mutex sync.RWMutex
|
|
groups map[uint32]*WindowGroup
|
|
}
|
|
|
|
// Group returns a window group for the given thread ID, if one exists.
|
|
// If a group does not already exist it returns nil.
|
|
func (m *windowGroupManager) Group(threadID uint32) *WindowGroup {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
if m.groups == nil {
|
|
return nil
|
|
}
|
|
return m.groups[threadID]
|
|
}
|
|
|
|
// CreateGroup returns a window group for the given thread ID. If one does
|
|
// not already exist, it will be created.
|
|
//
|
|
// The group will have its counter incremented as a result of this call.
|
|
// It is the caller's responsibility to call Done when finished with the
|
|
// group.
|
|
func (m *windowGroupManager) CreateGroup(threadID uint32) *WindowGroup {
|
|
// Fast path with read lock
|
|
m.mutex.RLock()
|
|
if m.groups != nil {
|
|
if group := m.groups[threadID]; group != nil {
|
|
m.mutex.RUnlock()
|
|
group.Add(1)
|
|
return group
|
|
}
|
|
}
|
|
m.mutex.RUnlock()
|
|
|
|
// Slow path with write lock
|
|
m.mutex.Lock()
|
|
if m.groups == nil {
|
|
m.groups = make(map[uint32]*WindowGroup)
|
|
} else {
|
|
if group := m.groups[threadID]; group != nil {
|
|
// Another caller raced with our lock and beat us
|
|
m.mutex.Unlock()
|
|
group.Add(1)
|
|
return group
|
|
}
|
|
}
|
|
|
|
group := newWindowGroup(threadID, m.removeGroup)
|
|
group.Add(1)
|
|
m.groups[threadID] = group
|
|
m.mutex.Unlock()
|
|
|
|
return group
|
|
}
|
|
|
|
// removeGroup is called by window groups to remove themselves from
|
|
// the manager.
|
|
func (m *windowGroupManager) removeGroup(threadID uint32) {
|
|
m.mutex.Lock()
|
|
delete(m.groups, threadID)
|
|
m.mutex.Unlock()
|
|
}
|
|
|
|
// WindowGroup holds data common to windows that share a thread.
|
|
//
|
|
// Each WindowGroup keeps track of the number of references to
|
|
// the group. When the number of references reaches zero, the
|
|
// group is disposed of.
|
|
type WindowGroup struct {
|
|
refs int // Tracks the number of windows that rely on this group
|
|
ignored int // Tracks the number of refs created by the group itself
|
|
threadID uint32
|
|
completion func(uint32) // Used to tell the window group manager to remove this group
|
|
removed bool // Has this group been removed from its manager? (used for race detection)
|
|
toolTip *ToolTip
|
|
activeForm Form
|
|
oleInit bool
|
|
accPropServices *win.IAccPropServices
|
|
|
|
syncMutex sync.Mutex
|
|
syncFuncs []func() // Functions queued to run on the group's thread
|
|
layoutResultsByForm map[Form]*formLayoutResult // Layout computations queued for application on the group's thread
|
|
}
|
|
|
|
// newWindowGroup returns a new window group for the given thread ID.
|
|
//
|
|
// The completion function will be called when the group is disposed of.
|
|
func newWindowGroup(threadID uint32, completion func(uint32)) *WindowGroup {
|
|
hr := win.OleInitialize()
|
|
|
|
return &WindowGroup{
|
|
threadID: threadID,
|
|
completion: completion,
|
|
oleInit: hr == win.S_OK || hr == win.S_FALSE,
|
|
layoutResultsByForm: make(map[Form]*formLayoutResult),
|
|
}
|
|
}
|
|
|
|
// ThreadID identifies the thread that the group is affiliated with.
|
|
func (g *WindowGroup) ThreadID() uint32 {
|
|
return g.threadID
|
|
}
|
|
|
|
// Refs returns the current number of references to the group.
|
|
func (g *WindowGroup) Refs() int {
|
|
return g.refs
|
|
}
|
|
|
|
// AccessibilityServices returns an instance of CLSID_AccPropServices class.
|
|
func (g *WindowGroup) accessibilityServices() *win.IAccPropServices {
|
|
if g.accPropServices != nil {
|
|
return g.accPropServices
|
|
}
|
|
|
|
var accPropServices *win.IAccPropServices
|
|
hr := win.CoCreateInstance(&win.CLSID_AccPropServices, nil, win.CLSCTX_ALL, &win.IID_IAccPropServices, (*unsafe.Pointer)(unsafe.Pointer(&accPropServices)))
|
|
if win.FAILED(hr) {
|
|
return nil
|
|
}
|
|
|
|
g.accPropServices = accPropServices
|
|
return accPropServices
|
|
}
|
|
|
|
// accPropIds is a static list of accessibility properties user (may) set for a window
|
|
// and we should clear when the window is disposed.
|
|
var accPropIds = []win.MSAAPROPID{
|
|
win.PROPID_ACC_DEFAULTACTION,
|
|
win.PROPID_ACC_DESCRIPTION,
|
|
win.PROPID_ACC_HELP,
|
|
win.PROPID_ACC_KEYBOARDSHORTCUT,
|
|
win.PROPID_ACC_NAME,
|
|
win.PROPID_ACC_ROLE,
|
|
win.PROPID_ACC_ROLEMAP,
|
|
win.PROPID_ACC_STATE,
|
|
win.PROPID_ACC_STATEMAP,
|
|
win.PROPID_ACC_VALUEMAP,
|
|
}
|
|
|
|
// accClearHwndProps clears all window properties for Dynamic Annotation to release resources.
|
|
func (g *WindowGroup) accClearHwndProps(hwnd win.HWND) {
|
|
if g.accPropServices != nil {
|
|
g.accPropServices.ClearHwndProps(hwnd, win.OBJID_CLIENT, win.CHILDID_SELF, accPropIds)
|
|
}
|
|
}
|
|
|
|
// Add changes the group's reference counter by delta, which may be negative.
|
|
//
|
|
// If the reference counter becomes zero the group will be disposed of.
|
|
//
|
|
// If the reference counter goes negative Add will panic.
|
|
func (g *WindowGroup) Add(delta int) {
|
|
if g.removed {
|
|
panic("walk: add() called on a WindowGroup that has been removed from its manager")
|
|
}
|
|
|
|
g.refs += delta
|
|
if g.refs < 0 {
|
|
panic("walk: negative WindowGroup refs counter")
|
|
}
|
|
if g.refs-g.ignored == 0 {
|
|
g.dispose()
|
|
}
|
|
}
|
|
|
|
// Done decrements the group's reference counter by one.
|
|
func (g *WindowGroup) Done() {
|
|
g.Add(-1)
|
|
}
|
|
|
|
// Synchronize adds f to the group's function queue, to be executed
|
|
// by the message loop running on the the group's thread.
|
|
//
|
|
// Synchronize can be called from any thread.
|
|
func (g *WindowGroup) Synchronize(f func()) {
|
|
g.syncMutex.Lock()
|
|
defer g.syncMutex.Unlock()
|
|
g.syncFuncs = append(g.syncFuncs, f)
|
|
}
|
|
|
|
// synchronizeLayout causes the given layout computations to be applied
|
|
// later by the message loop running on the group's thread.
|
|
//
|
|
// Any previously queued layout computations for the affected form that
|
|
// have not yet been applied will be replaced.
|
|
//
|
|
// synchronizeLayout can be called from any thread.
|
|
func (g *WindowGroup) synchronizeLayout(result *formLayoutResult) {
|
|
g.syncMutex.Lock()
|
|
g.layoutResultsByForm[result.form] = result
|
|
g.syncMutex.Unlock()
|
|
}
|
|
|
|
// RunSynchronized runs all of the function calls queued by Synchronize
|
|
// and applies any layout changes queued by synchronizeLayout.
|
|
//
|
|
// RunSynchronized must be called by the group's thread.
|
|
func (g *WindowGroup) RunSynchronized() {
|
|
// Clear the list of callbacks first to avoid deadlock
|
|
// if a callback itself calls Synchronize()...
|
|
g.syncMutex.Lock()
|
|
funcs := g.syncFuncs
|
|
var results []*formLayoutResult
|
|
for _, result := range g.layoutResultsByForm {
|
|
results = append(results, result)
|
|
delete(g.layoutResultsByForm, result.form)
|
|
}
|
|
g.syncFuncs = nil
|
|
g.syncMutex.Unlock()
|
|
|
|
for _, result := range results {
|
|
applyLayoutResults(result.results, result.stopwatch)
|
|
}
|
|
for _, f := range funcs {
|
|
f()
|
|
}
|
|
}
|
|
|
|
// ToolTip returns the tool tip control for the group, if one exists.
|
|
func (g *WindowGroup) ToolTip() *ToolTip {
|
|
return g.toolTip
|
|
}
|
|
|
|
// CreateToolTip returns a tool tip control for the group.
|
|
//
|
|
// If a control has not already been prepared for the group one will be
|
|
// created.
|
|
func (g *WindowGroup) CreateToolTip() (*ToolTip, error) {
|
|
if g.toolTip != nil {
|
|
return g.toolTip, nil
|
|
}
|
|
|
|
tt, err := NewToolTip() // This must not call group.ToolTip()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
g.toolTip = tt
|
|
|
|
// At this point the ToolTip has already added a reference for itself
|
|
// to the group as part of the ToolTip's InitWindow process. We don't
|
|
// want it to count toward the group's liveness, however, because it
|
|
// would keep the group from cleaning up after itself.
|
|
//
|
|
// To solve this problem we also keep track of the number of
|
|
// references that each group should ignore. The ignored references
|
|
// are subtracted from the total number of references when evaluating
|
|
// liveness. The expectation is that ignored references will be
|
|
// removed as part of the group's disposal process.
|
|
g.ignore(1)
|
|
|
|
return tt, nil
|
|
}
|
|
|
|
// ActiveForm returns the currently active form for the group. If no
|
|
// form is active it returns nil.
|
|
func (g *WindowGroup) ActiveForm() Form {
|
|
return g.activeForm
|
|
}
|
|
|
|
// SetActiveForm updates the currently active form for the group.
|
|
func (g *WindowGroup) SetActiveForm(form Form) {
|
|
g.activeForm = form
|
|
}
|
|
|
|
// ignore changes the number of references that the group will ignore.
|
|
//
|
|
// ignore is used internally by WindowGroup to keep track of the number
|
|
// of references created by the group itself. When finished with a group,
|
|
// call Done() instead.
|
|
func (g *WindowGroup) ignore(delta int) {
|
|
if g.removed {
|
|
panic("walk: ignore() called on a WindowGroup that has been removed from its manager")
|
|
}
|
|
|
|
g.ignored += delta
|
|
if g.ignored < 0 {
|
|
panic("walk: negative WindowGroup ignored counter")
|
|
}
|
|
if g.refs-g.ignored == 0 {
|
|
g.dispose()
|
|
}
|
|
}
|
|
|
|
// dispose releases any resources consumed by the group.
|
|
func (g *WindowGroup) dispose() {
|
|
if g.accPropServices != nil {
|
|
g.accPropServices.Release()
|
|
g.accPropServices = nil
|
|
}
|
|
|
|
if g.oleInit {
|
|
win.OleUninitialize()
|
|
g.oleInit = false
|
|
}
|
|
|
|
if g.toolTip != nil {
|
|
g.toolTip.Dispose()
|
|
g.toolTip = nil
|
|
}
|
|
g.removed = true // race detection only
|
|
g.completion(g.threadID)
|
|
}
|