adam-gui/vendor/fyne.io/fyne/v2/widget/check.go

361 lines
8.7 KiB
Go
Raw Permalink Normal View History

2024-04-29 19:13:50 +02:00
package widget
import (
"fmt"
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/theme"
)
// Check widget has a text label and a checked (or unchecked) icon and triggers an event func when toggled
type Check struct {
DisableableWidget
Text string
Checked bool
OnChanged func(bool) `json:"-"`
focused bool
hovered bool
binder basicBinder
minSize fyne.Size // cached for hover/tap position calculations
}
// NewCheck creates a new check widget with the set label and change handler
func NewCheck(label string, changed func(bool)) *Check {
c := &Check{
Text: label,
OnChanged: changed,
}
c.ExtendBaseWidget(c)
return c
}
// NewCheckWithData returns a check widget connected with the specified data source.
//
// Since: 2.0
func NewCheckWithData(label string, data binding.Bool) *Check {
check := NewCheck(label, nil)
check.Bind(data)
return check
}
// Bind connects the specified data source to this Check.
// The current value will be displayed and any changes in the data will cause the widget to update.
// User interactions with this Check will set the value into the data source.
//
// Since: 2.0
func (c *Check) Bind(data binding.Bool) {
c.binder.SetCallback(c.updateFromData)
c.binder.Bind(data)
c.OnChanged = func(_ bool) {
c.binder.CallWithData(c.writeData)
}
}
// SetChecked sets the the checked state and refreshes widget
func (c *Check) SetChecked(checked bool) {
if checked == c.Checked {
return
}
c.Checked = checked
if c.OnChanged != nil {
c.OnChanged(c.Checked)
}
c.Refresh()
}
// Hide this widget, if it was previously visible
func (c *Check) Hide() {
if c.focused {
c.FocusLost()
impl := c.super()
if c := fyne.CurrentApp().Driver().CanvasForObject(impl); c != nil {
c.Focus(nil)
}
}
c.BaseWidget.Hide()
}
// MouseIn is called when a desktop pointer enters the widget
func (c *Check) MouseIn(me *desktop.MouseEvent) {
c.MouseMoved(me)
}
// MouseOut is called when a desktop pointer exits the widget
func (c *Check) MouseOut() {
if c.hovered {
c.hovered = false
c.Refresh()
}
}
// MouseMoved is called when a desktop pointer hovers over the widget
func (c *Check) MouseMoved(me *desktop.MouseEvent) {
if c.Disabled() {
return
}
oldHovered := c.hovered
// only hovered if cached minSize has not been initialized (test code)
// or the pointer is within the "active" area of the widget (its minSize)
c.hovered = c.minSize.IsZero() ||
(me.Position.X <= c.minSize.Width && me.Position.Y <= c.minSize.Height)
if oldHovered != c.hovered {
c.Refresh()
}
}
// Tapped is called when a pointer tapped event is captured and triggers any change handler
func (c *Check) Tapped(pe *fyne.PointEvent) {
if c.Disabled() {
return
}
if !c.minSize.IsZero() &&
(pe.Position.X > c.minSize.Width || pe.Position.Y > c.minSize.Height) {
// tapped outside the active area of the widget
return
}
if !c.focused && !fyne.CurrentDevice().IsMobile() {
impl := c.super()
if c := fyne.CurrentApp().Driver().CanvasForObject(impl); c != nil {
c.Focus(impl.(fyne.Focusable))
}
}
c.SetChecked(!c.Checked)
}
// MinSize returns the size that this widget should not shrink below
func (c *Check) MinSize() fyne.Size {
c.ExtendBaseWidget(c)
c.minSize = c.BaseWidget.MinSize()
return c.minSize
}
// CreateRenderer is a private method to Fyne which links this widget to its renderer
func (c *Check) CreateRenderer() fyne.WidgetRenderer {
c.ExtendBaseWidget(c)
c.propertyLock.RLock()
defer c.propertyLock.RUnlock()
// TODO move to `theme.CheckButtonFillIcon()` when we add it in 2.4
bg := canvas.NewImageFromResource(fyne.CurrentApp().Settings().Theme().Icon("iconNameCheckButtonFill"))
icon := canvas.NewImageFromResource(theme.CheckButtonIcon())
text := canvas.NewText(c.Text, theme.ForegroundColor())
text.Alignment = fyne.TextAlignLeading
focusIndicator := canvas.NewCircle(theme.BackgroundColor())
r := &checkRenderer{
widget.NewBaseRenderer([]fyne.CanvasObject{focusIndicator, bg, icon, text}),
bg,
icon,
text,
focusIndicator,
c,
}
r.applyTheme()
r.updateLabel()
r.updateResource()
r.updateFocusIndicator()
return r
}
// FocusGained is called when the Check has been given focus.
func (c *Check) FocusGained() {
if c.Disabled() {
return
}
c.focused = true
c.Refresh()
}
// FocusLost is called when the Check has had focus removed.
func (c *Check) FocusLost() {
c.focused = false
c.Refresh()
}
// TypedRune receives text input events when the Check is focused.
func (c *Check) TypedRune(r rune) {
if c.Disabled() {
return
}
if r == ' ' {
c.SetChecked(!c.Checked)
}
}
// TypedKey receives key input events when the Check is focused.
func (c *Check) TypedKey(key *fyne.KeyEvent) {}
// SetText sets the text of the Check
//
// Since: 2.4
func (c *Check) SetText(text string) {
c.Text = text
c.Refresh()
}
// Unbind disconnects any configured data source from this Check.
// The current value will remain at the last value of the data source.
//
// Since: 2.0
func (c *Check) Unbind() {
c.OnChanged = nil
c.binder.Unbind()
}
func (c *Check) updateFromData(data binding.DataItem) {
if data == nil {
return
}
boolSource, ok := data.(binding.Bool)
if !ok {
return
}
val, err := boolSource.Get()
if err != nil {
fyne.LogError("Error getting current data value", err)
return
}
c.SetChecked(val) // if val != c.Checked, this will call updateFromData again, but only once
}
func (c *Check) writeData(data binding.DataItem) {
if data == nil {
return
}
boolTarget, ok := data.(binding.Bool)
if !ok {
return
}
currentValue, err := boolTarget.Get()
if err != nil {
return
}
if currentValue != c.Checked {
err := boolTarget.Set(c.Checked)
if err != nil {
fyne.LogError(fmt.Sprintf("Failed to set binding value to %t", c.Checked), err)
}
}
}
type checkRenderer struct {
widget.BaseRenderer
bg, icon *canvas.Image
label *canvas.Text
focusIndicator *canvas.Circle
check *Check
}
// MinSize calculates the minimum size of a check.
// This is based on the contained text, the check icon and a standard amount of padding added.
func (c *checkRenderer) MinSize() fyne.Size {
pad4 := theme.InnerPadding() * 2
min := c.label.MinSize().Add(fyne.NewSize(theme.IconInlineSize()+pad4, pad4))
if c.check.Text != "" {
min.Add(fyne.NewSize(theme.Padding(), 0))
}
return min
}
// Layout the components of the check widget
func (c *checkRenderer) Layout(size fyne.Size) {
focusIndicatorSize := fyne.NewSquareSize(theme.IconInlineSize() + theme.InnerPadding())
c.focusIndicator.Resize(focusIndicatorSize)
c.focusIndicator.Move(fyne.NewPos(theme.InputBorderSize(), (size.Height-focusIndicatorSize.Height)/2))
xOff := focusIndicatorSize.Width + theme.InputBorderSize()*2
labelSize := size.SubtractWidthHeight(xOff, 0)
c.label.Resize(labelSize)
c.label.Move(fyne.NewPos(xOff, 0))
iconPos := fyne.NewPos(theme.InnerPadding()/2+theme.InputBorderSize(), (size.Height-theme.IconInlineSize())/2)
iconSize := fyne.NewSquareSize(theme.IconInlineSize())
c.bg.Move(iconPos)
c.bg.Resize(iconSize)
c.icon.Resize(iconSize)
c.icon.Move(iconPos)
}
// applyTheme updates this Check to the current theme
func (c *checkRenderer) applyTheme() {
c.label.Color = theme.ForegroundColor()
c.label.TextSize = theme.TextSize()
if c.check.disabled {
c.label.Color = theme.DisabledColor()
}
}
func (c *checkRenderer) Refresh() {
c.check.propertyLock.RLock()
c.applyTheme()
c.updateLabel()
c.updateResource()
c.updateFocusIndicator()
c.check.propertyLock.RUnlock()
canvas.Refresh(c.check.super())
}
func (c *checkRenderer) updateLabel() {
c.label.Text = c.check.Text
}
func (c *checkRenderer) updateResource() {
res := theme.NewThemedResource(theme.CheckButtonIcon())
res.ColorName = theme.ColorNameInputBorder
// TODO move to `theme.CheckButtonFillIcon()` when we add it in 2.4
bgRes := theme.NewThemedResource(fyne.CurrentApp().Settings().Theme().Icon("iconNameCheckButtonFill"))
bgRes.ColorName = theme.ColorNameInputBackground
if c.check.Checked {
res = theme.NewThemedResource(theme.CheckButtonCheckedIcon())
res.ColorName = theme.ColorNamePrimary
bgRes.ColorName = theme.ColorNameBackground
}
if c.check.disabled {
if c.check.Checked {
res = theme.NewThemedResource(theme.CheckButtonCheckedIcon())
}
res.ColorName = theme.ColorNameDisabled
bgRes.ColorName = theme.ColorNameBackground
}
c.icon.Resource = res
c.bg.Resource = bgRes
}
func (c *checkRenderer) updateFocusIndicator() {
if c.check.disabled {
c.focusIndicator.FillColor = color.Transparent
} else if c.check.focused {
c.focusIndicator.FillColor = theme.FocusColor()
} else if c.check.hovered {
c.focusIndicator.FillColor = theme.HoverColor()
} else {
c.focusIndicator.FillColor = color.Transparent
}
}