|
@@ -0,0 +1,277 @@
|
|
|
|
|
+// Copyright 2016 The G3N Authors. All rights reserved.
|
|
|
|
|
+// Use of this source code is governed by a BSD-style
|
|
|
|
|
+// license that can be found in the LICENSE file.
|
|
|
|
|
+
|
|
|
|
|
+package gui
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "github.com/g3n/engine/core"
|
|
|
|
|
+ "github.com/g3n/engine/window"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// manager singleton
|
|
|
|
|
+var gm *manager
|
|
|
|
|
+
|
|
|
|
|
+// manager routes GUI events to the appropriate panels.
|
|
|
|
|
+type manager struct {
|
|
|
|
|
+ core.Dispatcher // Embedded Dispatcher
|
|
|
|
|
+ core.TimerManager // Embedded TimerManager
|
|
|
|
|
+ win window.IWindow // The current IWindow
|
|
|
|
|
+ scene core.INode // INode containing IPanels to dispatch events to (can contain non-IPanels as well)
|
|
|
|
|
+ modal IPanel // Panel which along its descendants will exclusively receive all events
|
|
|
|
|
+ target IPanel // Panel immediately under the cursor
|
|
|
|
|
+ keyFocus core.IDispatcher // IDispatcher which will exclusively receive all key and char events
|
|
|
|
|
+ cursorFocus core.IDispatcher // IDispatcher which will exclusively receive all OnCursor events
|
|
|
|
|
+ cev *window.CursorEvent // IDispatcher which will exclusively receive all OnCursor events
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Manager returns the GUI manager singleton (creating it the first time)
|
|
|
|
|
+func Manager() *manager {
|
|
|
|
|
+
|
|
|
|
|
+ // Return singleton if already created
|
|
|
|
|
+ if gm != nil {
|
|
|
|
|
+ return gm
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ gm = new(manager)
|
|
|
|
|
+ gm.Dispatcher.Initialize()
|
|
|
|
|
+ gm.TimerManager.Initialize()
|
|
|
|
|
+
|
|
|
|
|
+ // Subscribe to window events
|
|
|
|
|
+ gm.win = window.Get()
|
|
|
|
|
+ gm.win.Subscribe(window.OnKeyUp, gm.onKeyboard)
|
|
|
|
|
+ gm.win.Subscribe(window.OnKeyDown, gm.onKeyboard)
|
|
|
|
|
+ gm.win.Subscribe(window.OnKeyRepeat, gm.onKeyboard)
|
|
|
|
|
+ gm.win.Subscribe(window.OnChar, gm.onKeyboard)
|
|
|
|
|
+ gm.win.Subscribe(window.OnCursor, gm.onCursor)
|
|
|
|
|
+ gm.win.Subscribe(window.OnMouseUp, gm.onMouse)
|
|
|
|
|
+ gm.win.Subscribe(window.OnMouseDown, gm.onMouse)
|
|
|
|
|
+ gm.win.Subscribe(window.OnScroll, gm.onScroll)
|
|
|
|
|
+
|
|
|
|
|
+ return gm
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Set sets the INode to watch for events.
|
|
|
|
|
+// It's usually a scene containing a hierarchy of INodes.
|
|
|
|
|
+// The manager only cares about IPanels inside that hierarchy.
|
|
|
|
|
+func (gm *manager) Set(scene core.INode) {
|
|
|
|
|
+
|
|
|
|
|
+ gm.scene = scene
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// SetModal sets the specified panel and its descendants to be the exclusive receivers of events.
|
|
|
|
|
+func (gm *manager) SetModal(ipan IPanel) {
|
|
|
|
|
+
|
|
|
|
|
+ gm.modal = ipan
|
|
|
|
|
+ gm.SetKeyFocus(nil)
|
|
|
|
|
+ gm.SetCursorFocus(nil)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// SetKeyFocus sets the key-focused IDispatcher, which will exclusively receive key and char events.
|
|
|
|
|
+func (gm *manager) SetKeyFocus(disp core.IDispatcher) {
|
|
|
|
|
+
|
|
|
|
|
+ if gm.keyFocus == disp {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if gm.keyFocus != nil {
|
|
|
|
|
+ gm.keyFocus.Dispatch(OnFocus, nil)
|
|
|
|
|
+ }
|
|
|
|
|
+ gm.keyFocus = disp
|
|
|
|
|
+ if gm.keyFocus != nil {
|
|
|
|
|
+ gm.keyFocus.Dispatch(OnFocusLost, nil)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// SetCursorFocus sets the cursor-focused IDispatcher, which will exclusively receive OnCursor events.
|
|
|
|
|
+func (gm *manager) SetCursorFocus(disp core.IDispatcher) {
|
|
|
|
|
+
|
|
|
|
|
+ if gm.cursorFocus == disp {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ gm.cursorFocus = disp
|
|
|
|
|
+ if gm.cursorFocus == nil {
|
|
|
|
|
+ gm.onCursor(OnCursor, gm.cev)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// onKeyboard is called when char or key events are received.
|
|
|
|
|
+// The events are dispatched to the focused IDispatcher or to non-GUI.
|
|
|
|
|
+func (gm *manager) onKeyboard(evname string, ev interface{}) {
|
|
|
|
|
+
|
|
|
|
|
+ if gm.keyFocus != nil {
|
|
|
|
|
+ if gm.modal == nil {
|
|
|
|
|
+ gm.keyFocus.Dispatch(evname, ev)
|
|
|
|
|
+ } else if ipan, ok := gm.keyFocus.(IPanel); ok && gm.modal.IsAncestorOf(ipan) {
|
|
|
|
|
+ gm.keyFocus.Dispatch(evname, ev)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ gm.Dispatch(evname, ev)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// onMouse is called when mouse events are received.
|
|
|
|
|
+// OnMouseDown/OnMouseUp are dispatched to gm.target or to non-GUI, while
|
|
|
|
|
+// OnMouseDownOut/OnMouseUpOut are dispatched to all non-target panels.
|
|
|
|
|
+func (gm *manager) onMouse(evname string, ev interface{}) {
|
|
|
|
|
+
|
|
|
|
|
+ // Check if gm.scene is nil and if so then there are no IPanels to send events to
|
|
|
|
|
+ if gm.scene == nil {
|
|
|
|
|
+ gm.Dispatch(evname, ev) // Dispatch event to non-GUI since event was not filtered by any GUI component
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Dispatch OnMouseDownOut/OnMouseUpOut to all panels except ancestors of target
|
|
|
|
|
+ gm.forEachIPanel(func(ipan IPanel) {
|
|
|
|
|
+ if gm.target == nil || !ipan.IsAncestorOf(gm.target) {
|
|
|
|
|
+ switch evname {
|
|
|
|
|
+ case OnMouseDown:
|
|
|
|
|
+ ipan.Dispatch(OnMouseDownOut, ev)
|
|
|
|
|
+ case OnMouseUp:
|
|
|
|
|
+ ipan.Dispatch(OnMouseUpOut, ev)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // Appropriately dispatch the event to target panel's lowest subscribed ancestor or to non-GUI or not at all
|
|
|
|
|
+ if gm.target != nil {
|
|
|
|
|
+ if gm.modal == nil || gm.modal.IsAncestorOf(gm.target) {
|
|
|
|
|
+ sendAncestry(gm.target, false, nil, gm.modal, evname, ev)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if gm.modal == nil {
|
|
|
|
|
+ gm.Dispatch(evname, ev)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// onScroll is called when scroll events are received.
|
|
|
|
|
+// The events are dispatched to the target panel or to non-GUI.
|
|
|
|
|
+func (gm *manager) onScroll(evname string, ev interface{}) {
|
|
|
|
|
+
|
|
|
|
|
+ // Check if gm.scene is nil and if so then there are no IPanels to send events to
|
|
|
|
|
+ if gm.scene == nil {
|
|
|
|
|
+ gm.Dispatch(evname, ev) // Dispatch event to non-GUI since event was not filtered by any GUI component
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Appropriately dispatch the event to target panel's lowest subscribed ancestor or to non-GUI or not at all
|
|
|
|
|
+ if gm.target != nil {
|
|
|
|
|
+ if gm.modal == nil || gm.modal.IsAncestorOf(gm.target) {
|
|
|
|
|
+ sendAncestry(gm.target, false, nil, gm.modal, evname, ev)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if gm.modal == nil {
|
|
|
|
|
+ gm.Dispatch(evname, ev)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// onCursor is called when (mouse) cursor events are received.
|
|
|
|
|
+// Updates the target/click panels and dispatches OnCursor, OnCursorEnter, OnCursorLeave events.
|
|
|
|
|
+func (gm *manager) onCursor(evname string, ev interface{}) {
|
|
|
|
|
+
|
|
|
|
|
+ // If an IDispatcher is capturing cursor events dispatch to it and return
|
|
|
|
|
+ if gm.cursorFocus != nil {
|
|
|
|
|
+ gm.cursorFocus.Dispatch(evname, ev)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // If gm.scene is nil then there are no IPanels to send events to
|
|
|
|
|
+ if gm.scene == nil {
|
|
|
|
|
+ gm.Dispatch(evname, ev) // Dispatch event to non-GUI since event was not filtered by any GUI component
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Get and store CursorEvent
|
|
|
|
|
+ gm.cev = ev.(*window.CursorEvent)
|
|
|
|
|
+
|
|
|
|
|
+ // Temporarily store last target and clear current one
|
|
|
|
|
+ oldTarget := gm.target
|
|
|
|
|
+ gm.target = nil
|
|
|
|
|
+
|
|
|
|
|
+ // Find IPanel immediately under the cursor and store it in gm.target
|
|
|
|
|
+ gm.forEachIPanel(func(ipan IPanel) {
|
|
|
|
|
+ if ipan.InsideBorders(gm.cev.Xpos, gm.cev.Ypos) && (gm.target == nil || ipan.Position().Z < gm.target.GetPanel().Position().Z) {
|
|
|
|
|
+ gm.target = ipan
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // If the cursor is now over a different panel, dispatch OnCursorLeave/OnCursorEnter
|
|
|
|
|
+ if gm.target != oldTarget {
|
|
|
|
|
+ // We are only interested in sending events up to the lowest common ancestor of target and oldTarget
|
|
|
|
|
+ var commonAnc IPanel
|
|
|
|
|
+ if gm.target != nil && oldTarget != nil {
|
|
|
|
|
+ commonAnc, _ = gm.target.LowestCommonAncestor(oldTarget).(IPanel)
|
|
|
|
|
+ }
|
|
|
|
|
+ // If just left a panel and the new panel is not a descendant of the old panel
|
|
|
|
|
+ if oldTarget != nil && !oldTarget.IsAncestorOf(gm.target) && (gm.modal == nil || gm.modal.IsAncestorOf(oldTarget)) {
|
|
|
|
|
+ sendAncestry(oldTarget, true, commonAnc, gm.modal, OnCursorLeave, ev)
|
|
|
|
|
+ }
|
|
|
|
|
+ // If just entered a panel and it's not an ancestor of the old panel
|
|
|
|
|
+ if gm.target != nil && !gm.target.IsAncestorOf(oldTarget) && (gm.modal == nil || gm.modal.IsAncestorOf(gm.target)) {
|
|
|
|
|
+ sendAncestry(gm.target, true, commonAnc, gm.modal, OnCursorEnter, ev)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Appropriately dispatch the event to target panel's lowest subscribed ancestor or to non-GUI or not at all
|
|
|
|
|
+ if gm.target != nil {
|
|
|
|
|
+ if gm.modal == nil || gm.modal.IsAncestorOf(gm.target) {
|
|
|
|
|
+ sendAncestry(gm.target, false, nil, gm.modal, evname, ev)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if gm.modal == nil {
|
|
|
|
|
+ gm.Dispatch(evname, ev)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// sendAncestry sends the specified event (evname/ev) to the specified target panel and its ancestors.
|
|
|
|
|
+// If all is false, then the event is only sent to the lowest subscribed ancestor.
|
|
|
|
|
+// If uptoEx (i.e. excluding) is not nil then the event will not be dispatched to that ancestor nor any higher ancestors.
|
|
|
|
|
+// If uptoIn (i.e. including) is not nil then the event will be dispatched to that ancestor but not to any higher ancestors.
|
|
|
|
|
+// uptoEx and uptoIn can both be defined.
|
|
|
|
|
+func sendAncestry(ipan IPanel, all bool, uptoEx IPanel, uptoIn IPanel, evname string, ev interface{}) {
|
|
|
|
|
+
|
|
|
|
|
+ var ok bool
|
|
|
|
|
+ for ipan != nil {
|
|
|
|
|
+ if uptoEx != nil && ipan == uptoEx {
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ count := ipan.Dispatch(evname, ev)
|
|
|
|
|
+ if (uptoIn != nil && ipan == uptoIn) || (!all && count > 0) {
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ ipan, ok = ipan.Parent().(IPanel)
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// traverseIPanel traverses the descendants of the provided IPanel,
|
|
|
|
|
+// executing the specified function for each IPanel.
|
|
|
|
|
+func traverseIPanel(ipan IPanel, f func(ipan IPanel)) {
|
|
|
|
|
+
|
|
|
|
|
+ // If panel not visible or not enabled, ignore entire hierarchy below this point
|
|
|
|
|
+ if !ipan.Visible() || !ipan.Enabled() {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ f(ipan) // Call specified function
|
|
|
|
|
+ // Check descendants (can assume they are IPanels)
|
|
|
|
|
+ for _, child := range ipan.Children() {
|
|
|
|
|
+ traverseIPanel(child.(IPanel), f)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// traverseINode traverses the descendants of the specified INode,
|
|
|
|
|
+// executing the specified function for each IPanel.
|
|
|
|
|
+func traverseINode(inode core.INode, f func(ipan IPanel)) {
|
|
|
|
|
+
|
|
|
|
|
+ if ipan, ok := inode.(IPanel); ok {
|
|
|
|
|
+ traverseIPanel(ipan, f)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ for _, child := range inode.Children() {
|
|
|
|
|
+ traverseINode(child, f)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// forEachIPanel executes the specified function for each enabled and visible IPanel in gm.scene.
|
|
|
|
|
+func (gm *manager) forEachIPanel(f func(ipan IPanel)) {
|
|
|
|
|
+
|
|
|
|
|
+ traverseINode(gm.scene, f)
|
|
|
|
|
+}
|