Skip to content

Commit

Permalink
TwoStateToolbarAction (#93)
Browse files Browse the repository at this point in the history
Displayed icon in widget alternates between 2 different icons.
Simple demo app provided.
  • Loading branch information
jimorc authored Dec 24, 2024
1 parent b37145a commit ce61268
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 0 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,29 @@ m := NewMap()

![](img/map.png)

### TwoStateToolbarAction

A TwoStateToolbarAction displays one of two icons based on the stored state. It is similar
to a regular ToolbarAction except that the icon and state are toggled each time the toolbar
action is activated. The current (new) state is passed to the `onActivated` function.

One potential use of this toolbar action is displaying the MediaPlayIcon when a media
file is not being played, and the MediaPauseIcon or MediaStopIcon when a media file is
being played. A second use may be seen in an application where a left or right panel is
displayed or not. For example, show the left panel open icon when the left panel is
closed, and the left panel close icon when the panel is open.


```go
action := NewTwoStateToolBar(theme.MediaPlayIcon(),
theme.MediaPauseIcon(),
func(on bool) {
// Do something with state. For example, if on is true, start playback.
})
```

* [Demo App](cmd/twostatetoolbaraction_demo/main.go)

## Dialogs

### About
Expand Down
38 changes: 38 additions & 0 deletions cmd/twostatetoolbaraction_demo/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"fmt"

"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
xwidget "fyne.io/x/fyne/widget"
)

func main() {
a := app.New()
w := a.NewWindow("Two State Demo")

twoState0 := xwidget.NewTwoStateToolbarAction(nil,
nil, func(on bool) {
fmt.Println(on)
})
sep := widget.NewToolbarSeparator()
tb := widget.NewToolbar(twoState0, sep)

toggleButton := widget.NewButton("Toggle State", func() {
on := twoState0.GetOn()
twoState0.SetOn(!on)
})
offIconButton := widget.NewButton("Set OffIcon", func() {
twoState0.SetOffIcon(theme.MediaPlayIcon())
})
onIconButton := widget.NewButton("Set OnIcon", func() {
twoState0.SetOnIcon(theme.MediaStopIcon())
})
vc := container.NewVBox(toggleButton, offIconButton, onIconButton)
c := container.NewBorder(tb, vc, nil, nil)
w.SetContent(c)
w.ShowAndRun()
}
Binary file added widget/testdata/twostatetoolbaraction/offstate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added widget/testdata/twostatetoolbaraction/onstate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
93 changes: 93 additions & 0 deletions widget/twostatetoolbaraction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package widget

import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)

// TwoStateToolbarAction is a push button style of ToolbarItem that displays a different
// icon depending on its state.
//
// state is a boolean indicating off and on. The actual meaning of the boolean depends on how it is used. For
// example, in a media play app, false might indicate that the medium file is not being played, and true might
// indicate that the file is being played.
// Similarly, the two states could be used to indicate that a panel is being hidden or shown.
type TwoStateToolbarAction struct {
on bool
offIcon fyne.Resource
onIcon fyne.Resource
OnActivated func(bool) `json:"-"`

button widget.Button
}

// NewTwoStateToolbarAction returns a new push button style of Toolbar item that displays
// a different icon for each of its two states
func NewTwoStateToolbarAction(offStateIcon fyne.Resource,
onStateIcon fyne.Resource,
onTapped func(bool)) *TwoStateToolbarAction {
t := &TwoStateToolbarAction{offIcon: offStateIcon, onIcon: onStateIcon, OnActivated: onTapped}
t.button.SetIcon(t.offIcon)
t.button.OnTapped = t.activated
return t
}

// GetOn returns the current state of the toolbaraction
func (t *TwoStateToolbarAction) GetOn() bool {
return t.on
}

// SetOn sets the state of the toolbaraction
func (t *TwoStateToolbarAction) SetOn(on bool) {
t.on = on
if t.OnActivated != nil {
t.OnActivated(t.on)
}
t.setButtonIcon()
t.button.Refresh()
}

// SetOffIcon sets the icon that is displayed when the state is false
func (t *TwoStateToolbarAction) SetOffIcon(icon fyne.Resource) {
t.offIcon = icon
t.setButtonIcon()
t.button.Refresh()
}

// SetOnIcon sets the icon that is displayed when the state is true
func (t *TwoStateToolbarAction) SetOnIcon(icon fyne.Resource) {
t.onIcon = icon
t.setButtonIcon()
t.button.Refresh()
}

// ToolbarObject gets a button to render this ToolbarAction
func (t *TwoStateToolbarAction) ToolbarObject() fyne.CanvasObject {
t.button.Importance = widget.LowImportance

// synchronize properties
t.setButtonIcon()
t.button.OnTapped = t.activated
return &t.button
}

func (t *TwoStateToolbarAction) activated() {
if !t.on {
t.on = true
} else {
t.on = false
}
t.setButtonIcon()
if t.OnActivated != nil {
t.OnActivated(t.on)
}
t.button.Refresh()
}

func (t *TwoStateToolbarAction) setButtonIcon() {
if !t.on {
t.button.Icon = t.offIcon
} else {
t.button.Icon = t.onIcon
}
}
87 changes: 87 additions & 0 deletions widget/twostatetoolbaraction_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package widget

import (
"testing"

"fyne.io/fyne/v2/test"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewTwoStateToolbarAction(t *testing.T) {
action := NewTwoStateToolbarAction(theme.MediaPlayIcon(),
theme.MediaPauseIcon(),
func(_ bool) {})
assert.Equal(t, theme.MediaPlayIcon().Name(), action.offIcon.Name())
assert.Equal(t, theme.MediaPauseIcon().Name(), action.onIcon.Name())
assert.Equal(t, action.offIcon.Name(), action.button.Icon.Name())
}

func TestTwoStateToolbarAction_Activated(t *testing.T) {
action := NewTwoStateToolbarAction(theme.MediaPlayIcon(),
theme.MediaPauseIcon(),
func(_ bool) {})
require.Equal(t, action.offIcon.Name(), action.button.Icon.Name())
action.button.Tapped(nil)
assert.Equal(t, action.onIcon.Name(), action.button.Icon.Name())
}

func TestTwoStateToolbarAction_Tapped(t *testing.T) {
test.NewApp()
action := NewTwoStateToolbarAction(theme.MediaPlayIcon(),
theme.MediaPauseIcon(),
func(_ bool) {})
tb := widget.NewToolbar(action)
w := test.NewWindow(tb)
defer w.Close()
test.AssertRendersToImage(t, "twostatetoolbaraction/offstate.png", w.Canvas())
action.button.Tapped(nil)
test.AssertRendersToImage(t, "twostatetoolbaraction/onstate.png", w.Canvas())
}

func TestTwoStateToolbarAction_GetSetState(t *testing.T) {
var ts bool
playState := false
test.NewApp()
action := NewTwoStateToolbarAction(theme.MediaPlayIcon(),
theme.MediaPauseIcon(),
func(on bool) {
ts = on
})
tb := widget.NewToolbar(action)
w := test.NewWindow(tb)
defer w.Close()
assert.Equal(t, playState, action.GetOn())
action.SetOn(true)
assert.Equal(t, true, action.GetOn())
assert.Equal(t, true, ts)
test.AssertRendersToImage(t, "twostatetoolbaraction/onstate.png", w.Canvas())
}

func TestTwoStateToolbarAction_SetOffStateIcon(t *testing.T) {
test.NewApp()
action := NewTwoStateToolbarAction(nil,
theme.MediaPauseIcon(),
func(staone bool) {})
tb := widget.NewToolbar(action)
w := test.NewWindow(tb)
defer w.Close()

action.SetOffIcon(theme.MediaPlayIcon())
assert.Equal(t, theme.MediaPlayIcon().Name(), action.offIcon.Name())
}

func TestTwoStateToolbarAction_SetOnStateIcon(t *testing.T) {
test.NewApp()
action := NewTwoStateToolbarAction(theme.MediaPlayIcon(),
nil,
func(on bool) {})
tb := widget.NewToolbar(action)
w := test.NewWindow(tb)
defer w.Close()

action.SetOnIcon(theme.MediaPauseIcon())
assert.Equal(t, theme.MediaPauseIcon().Name(), action.onIcon.Name())
}

0 comments on commit ce61268

Please sign in to comment.