erm/vendor/github.com/lxn/walk/windowgroup.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)
}