361 lines
8.7 KiB
Go
361 lines
8.7 KiB
Go
|
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
|
||
|
}
|
||
|
}
|