clui/ctrlutil.go
Leandro Dorileo 1fc1fdd776 base_control: fix set visible event dispatching
Currently when setting a control visible no matter if this control has
active children new KeyTab events will be dispatched. This results in
errors when an application has explicitly set the active control -
i.e with clui.ActivateControl().

In this case the expected behavior is to have that control active
instead of the next one, since we are not considering the the children
state then we always emit the KeyTab event resulting into a focus/active
status change.

This patch changes this behavior by querying the children's status and
checking if there is a child with active state set and only triggering
the KeyTab event when no child is active.

Signed-off-by: Leandro Dorileo <leandro.maciel.dorileo@intel.com>
2018-06-21 17:22:26 -07:00

270 lines
5.9 KiB
Go

package clui
import (
term "github.com/nsf/termbox-go"
)
// ThumbPosition returns a scrollbar thumb position depending
// on currently active item(itemNo), total number of items
// (itemCount), and length/height of the scrollbar(length)
// including arrows. Returns position in interval of (1..lenght-2)
// or -1 if the thumb is not visible
func ThumbPosition(itemNo, itemCount, length int) int {
length -= 2
if itemNo < 0 {
return -1
}
if itemNo >= itemCount-1 {
return length - 1
}
if length < 4 {
return 0
}
ydiff := int(float32(itemNo) / float32(itemCount-1.0) * float32(length-1))
return ydiff
}
// ItemByThumbPosition calculates item number by scrollbar
// thumb position. Position - thumb position inside scrollbar,
// itemCount - total number of items, lenght - lenght or heigth
// of scrollbar. Return -1 if it is not possible to calculate:
// e.g, itemCount equals zero
func ItemByThumbPosition(position, itemCount, length int) int {
length -= 2
if position < 1 {
return -1
}
if itemCount < 1 {
return -1
}
if itemCount == 1 {
return 0
}
newPos := int(float32(itemCount-1)*float32(position-1)/float32(length-1) + 0.9)
if newPos < 0 {
newPos = 0
} else if newPos >= itemCount {
newPos = itemCount - 1
}
return newPos
}
// ChildAt returns the children of parent control that is at absolute
// coordinates x, y. Returns nil if x, y are outside parent control and
// returns parent if no child is at x, y
func ChildAt(parent Control, x, y int) Control {
px, py := parent.Pos()
pw, ph := parent.Size()
if px > x || py > y || px+pw <= x || py+ph <= y {
return nil
}
if len(parent.Children()) == 0 {
return parent
}
var ctrl Control
ctrl = parent
for _, child := range parent.Children() {
check := ChildAt(child, x, y)
if check != nil {
ctrl = check
break
}
}
return ctrl
}
// DeactivateControls makes all children of parent inactive
func DeactivateControls(parent Control) {
for _, ctrl := range parent.Children() {
if ctrl.Active() {
ctrl.SetActive(false)
ctrl.ProcessEvent(Event{Type: EventActivate, X: 0})
}
DeactivateControls(ctrl)
}
}
// ActivateControl makes control active and disables all other children of
// the parent. Returns true if control was found and activated
func ActivateControl(parent, control Control) bool {
DeactivateControls(parent)
res := false
ctrl := FindChild(parent, control)
if ctrl != nil {
res = true
if !ctrl.Active() {
ctrl.ProcessEvent(Event{Type: EventActivate, X: 1})
ctrl.SetActive(true)
}
}
return res
}
// FindChild returns control if it is a child of the parent and nil otherwise
func FindChild(parent, control Control) Control {
var res Control
if parent == control {
return parent
}
for _, ctrl := range parent.Children() {
if ctrl == control {
res = ctrl
break
}
res = FindChild(ctrl, control)
if res != nil {
break
}
}
return res
}
// IsMouseClickEvent returns if a user action can be treated as mouse click.
func IsMouseClickEvent(ev Event) bool {
if ev.Type == EventClick {
return true
}
if ev.Type == EventMouse && ev.Key == term.MouseLeft {
return true
}
return false
}
// FindFirstControl returns the first child for that fn returns true.
// The function is used to find active or tab-stop control
func FindFirstControl(parent Control, fn func(Control) bool) Control {
linear := getLinearControlList(parent, fn)
if len(linear) == 0 {
return nil
}
return linear[0]
}
// FindLastControl returns the first child for that fn returns true.
// The function is used by TAB processing method if a user goes backwards
// with TAB key - not supported now
func FindLastControl(parent Control, fn func(Control) bool) Control {
linear := getLinearControlList(parent, fn)
if len(linear) == 0 {
return nil
}
return linear[len(linear)-1]
}
// ActiveControl returns the active child of the parent or nil if no child is
// active
func ActiveControl(parent Control) Control {
fnActive := func(c Control) bool {
return c.Active()
}
return FindFirstControl(parent, fnActive)
}
// FindFirstActiveControl returns the first active control of a parent
func FindFirstActiveControl(parent Control) Control {
for _, curr := range getLinearControlList(parent, nil) {
if curr.Active() {
return curr
}
}
return nil
}
func getLinearControlList(parent Control, fn func(Control) bool) []Control {
result := []Control{}
for _, curr := range parent.Children() {
if fn(curr) {
result = append(result, curr)
}
if len(curr.Children()) == 0 {
continue
}
ch := getLinearControlList(curr, fn)
if len(ch) != 0 {
result = append(result, ch...)
}
}
return result
}
// NextControl returns the next or previous child (depends on next parameter)
// that has tab-stop feature on. Used by library when processing TAB key
func NextControl(parent Control, curr Control, next bool) Control {
fnTab := func(c Control) bool {
return c.TabStop() && c.Visible() && c.Enabled()
}
linear := getLinearControlList(parent, fnTab)
var pIndex, nIndex int
for i, ch := range linear {
if ch != curr {
continue
}
pIndex = i - 1
nIndex = i + 1
break
}
if nIndex > len(linear)-1 {
nIndex = 0
}
if pIndex < 0 {
pIndex = len(linear) - 1
}
if next {
return linear[nIndex]
} else {
return linear[pIndex]
}
}
// SendEventToChild tries to find a child control that should recieve the evetn
// For mouse click events it looks for a control at coordinates of event,
// makes it active, and then sends the event to it.
// If it is not mouse click event then it looks for the first active child and
// sends the event to it if it is not nil
func SendEventToChild(parent Control, ev Event) bool {
var child Control
if IsMouseClickEvent(ev) {
child = ChildAt(parent, ev.X, ev.Y)
if child != nil && !child.Active() {
ActivateControl(parent, child)
}
} else {
child = ActiveControl(parent)
}
if child != nil && child != parent {
return child.ProcessEvent(ev)
}
return false
}