400 lines
10 KiB
Go
400 lines
10 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
|
|
|
|
// Partial check is when there is an indeterminate state (usually meaning that child items are some-what checked).
|
|
// Turning this on will override the checked state and show a dash icon (neither checked nor unchecked).
|
|
// The user interaction cannot turn this on, tapping a partial check state will set `Checked` to true.
|
|
//
|
|
// Since: 2.6
|
|
Partial 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 checked state and refreshes widget
|
|
// If the `Partial` state is set this will be turned off to respect the `checked` bool passed in here.
|
|
func (c *Check) SetChecked(checked bool) {
|
|
if checked == c.Checked && !c.Partial {
|
|
return
|
|
}
|
|
|
|
c.Partial = false
|
|
c.Checked = checked
|
|
onChanged := c.OnChanged
|
|
|
|
if onChanged != nil {
|
|
onChanged(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
|
|
}
|
|
|
|
minHeight := c.minSize.Height
|
|
minY := (c.Size().Height - minHeight) / 2
|
|
if !c.minSize.IsZero() &&
|
|
(pe.Position.X > c.minSize.Width || pe.Position.Y < minY || pe.Position.Y > minY+minHeight) {
|
|
// tapped outside the active area of the widget
|
|
return
|
|
}
|
|
|
|
if !c.focused {
|
|
focusIfNotMobile(c.super())
|
|
}
|
|
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 {
|
|
th := c.Theme()
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
|
|
c.ExtendBaseWidget(c)
|
|
bg := canvas.NewImageFromResource(th.Icon(theme.IconNameCheckButtonFill))
|
|
icon := canvas.NewImageFromResource(th.Icon(theme.IconNameCheckButton))
|
|
|
|
text := canvas.NewText(c.Text, th.Color(theme.ColorNameForeground, v))
|
|
text.Alignment = fyne.TextAlignLeading
|
|
|
|
focusIndicator := canvas.NewCircle(th.Color(theme.ColorNameBackground, v))
|
|
r := &checkRenderer{
|
|
widget.NewBaseRenderer([]fyne.CanvasObject{focusIndicator, bg, icon, text}),
|
|
bg,
|
|
icon,
|
|
text,
|
|
focusIndicator,
|
|
c,
|
|
}
|
|
r.applyTheme(th, v)
|
|
r.updateLabel()
|
|
r.updateResource(th)
|
|
r.updateFocusIndicator(th, v)
|
|
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 {
|
|
th := c.check.Theme()
|
|
|
|
pad4 := th.Size(theme.SizeNameInnerPadding) * 2
|
|
min := c.label.MinSize().Add(fyne.NewSize(th.Size(theme.SizeNameInlineIcon)+pad4, pad4))
|
|
|
|
if c.check.Text != "" {
|
|
min.Add(fyne.NewSize(th.Size(theme.SizeNamePadding), 0))
|
|
}
|
|
|
|
return min
|
|
}
|
|
|
|
// Layout the components of the check widget
|
|
func (c *checkRenderer) Layout(size fyne.Size) {
|
|
th := c.check.Theme()
|
|
innerPadding := th.Size(theme.SizeNameInnerPadding)
|
|
borderSize := th.Size(theme.SizeNameInputBorder)
|
|
iconInlineSize := th.Size(theme.SizeNameInlineIcon)
|
|
|
|
focusIndicatorSize := fyne.NewSquareSize(iconInlineSize + innerPadding)
|
|
c.focusIndicator.Resize(focusIndicatorSize)
|
|
c.focusIndicator.Move(fyne.NewPos(borderSize, (size.Height-focusIndicatorSize.Height)/2))
|
|
|
|
xOff := focusIndicatorSize.Width + borderSize*2
|
|
labelSize := size.SubtractWidthHeight(xOff, 0)
|
|
c.label.Resize(labelSize)
|
|
c.label.Move(fyne.NewPos(xOff, 0))
|
|
|
|
iconPos := fyne.NewPos(innerPadding/2+borderSize, (size.Height-iconInlineSize)/2)
|
|
iconSize := fyne.NewSquareSize(iconInlineSize)
|
|
c.bg.Move(iconPos)
|
|
c.bg.Resize(iconSize)
|
|
c.icon.Move(iconPos)
|
|
c.icon.Resize(iconSize)
|
|
}
|
|
|
|
// applyTheme updates this Check to the current theme
|
|
func (c *checkRenderer) applyTheme(th fyne.Theme, v fyne.ThemeVariant) {
|
|
c.label.Color = th.Color(theme.ColorNameForeground, v)
|
|
c.label.TextSize = th.Size(theme.SizeNameText)
|
|
if c.check.Disabled() {
|
|
c.label.Color = th.Color(theme.ColorNameDisabled, v)
|
|
}
|
|
}
|
|
|
|
func (c *checkRenderer) Refresh() {
|
|
th := c.check.Theme()
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
|
|
c.applyTheme(th, v)
|
|
c.updateLabel()
|
|
c.updateResource(th)
|
|
c.updateFocusIndicator(th, v)
|
|
canvas.Refresh(c.check.super())
|
|
}
|
|
|
|
// must be called while holding c.check.propertyLock for reading
|
|
func (c *checkRenderer) updateLabel() {
|
|
c.label.Text = c.check.Text
|
|
}
|
|
|
|
// must be called while holding c.check.propertyLock for reading
|
|
func (c *checkRenderer) updateResource(th fyne.Theme) {
|
|
res := theme.NewThemedResource(th.Icon(theme.IconNameCheckButton))
|
|
res.ColorName = theme.ColorNameInputBorder
|
|
bgRes := theme.NewThemedResource(th.Icon(theme.IconNameCheckButtonFill))
|
|
bgRes.ColorName = theme.ColorNameInputBackground
|
|
|
|
if c.check.Partial {
|
|
res = theme.NewThemedResource(th.Icon(theme.IconNameCheckButtonPartial))
|
|
res.ColorName = theme.ColorNamePrimary
|
|
bgRes.ColorName = theme.ColorNameBackground
|
|
} else if c.check.Checked {
|
|
res = theme.NewThemedResource(th.Icon(theme.IconNameCheckButtonChecked))
|
|
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.icon.Refresh()
|
|
c.bg.Resource = bgRes
|
|
c.bg.Refresh()
|
|
}
|
|
|
|
// must be called while holding c.check.propertyLock for reading
|
|
func (c *checkRenderer) updateFocusIndicator(th fyne.Theme, v fyne.ThemeVariant) {
|
|
if c.check.Disabled() {
|
|
c.focusIndicator.FillColor = color.Transparent
|
|
} else if c.check.focused {
|
|
c.focusIndicator.FillColor = th.Color(theme.ColorNameFocus, v)
|
|
} else if c.check.hovered {
|
|
c.focusIndicator.FillColor = th.Color(theme.ColorNameHover, v)
|
|
} else {
|
|
c.focusIndicator.FillColor = color.Transparent
|
|
}
|
|
}
|
|
|
|
func focusIfNotMobile(w fyne.Widget) {
|
|
if w == nil {
|
|
return
|
|
}
|
|
|
|
if !fyne.CurrentDevice().IsMobile() {
|
|
if c := fyne.CurrentApp().Driver().CanvasForObject(w); c != nil {
|
|
c.Focus(w.(fyne.Focusable))
|
|
}
|
|
}
|
|
|
|
}
|