Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 95 additions & 19 deletions widget/calendar.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package widget

import (
"fmt"
"math"
"strconv"
"strings"
"time"
"unicode"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/lang"
Expand All @@ -27,14 +29,18 @@ var minCellContent = NewLabel("22")
// Since: 2.6
type Calendar struct {
BaseWidget
currentTime time.Time
displayedMonth time.Time

monthPrevious *Button
monthNext *Button
monthLabel *Label

dates *fyne.Container

SelectedDate time.Time // currently selected date

// TODO discuss a config variable WeekStart time.Weekday to override week's first day

OnChanged func(time.Time) `json:"-"`
}

Expand All @@ -43,30 +49,49 @@ type Calendar struct {
// Since: 2.6
func NewCalendar(cT time.Time, changed func(time.Time)) *Calendar {
c := &Calendar{
currentTime: cT,
OnChanged: changed,
displayedMonth: cT,
OnChanged: changed,
}

c.ExtendBaseWidget(c)
return c
}

// SetDisplayedDate sets the currently displayed year and month
func (c *Calendar) SetDisplayedDate(date time.Time) {
if date.IsZero() {
date = time.Now()
}

// Dates are 'normalised', forcing date to start from the start of the month ensures move from March to February
c.displayedMonth = time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, time.Local)

// check if label has been created to avoid nil pointer access panic if function is called before
// widget instanciation
if c.monthLabel != nil {
c.monthLabel.SetText(c.monthYear())
c.dates.Objects = c.calendarObjects()
}
}

// SetSelectedDate sets the currently selected date
//
// Date selection works only if .Selectable = true
func (c *Calendar) SetSelectedDate(date time.Time) {
c.SelectedDate = date
c.updateSelection()
}

// CreateRenderer returns a new WidgetRenderer for this widget.
// This should not be called by regular code, it is used internally to render a widget.
func (c *Calendar) CreateRenderer() fyne.WidgetRenderer {
c.monthPrevious = NewButtonWithIcon("", theme.NavigateBackIcon(), func() {
c.currentTime = c.currentTime.AddDate(0, -1, 0)
// Dates are 'normalised', forcing date to start from the start of the month ensures move from March to February
c.currentTime = time.Date(c.currentTime.Year(), c.currentTime.Month(), 1, 0, 0, 0, 0, c.currentTime.Location())
c.monthLabel.SetText(c.monthYear())
c.dates.Objects = c.calendarObjects()
c.SetDisplayedDate(c.displayedMonth.AddDate(0, -1, 0))
})
c.monthPrevious.Importance = LowImportance

c.monthNext = NewButtonWithIcon("", theme.NavigateNextIcon(), func() {
c.currentTime = c.currentTime.AddDate(0, 1, 0)
c.monthLabel.SetText(c.monthYear())
c.dates.Objects = c.calendarObjects()
c.SetDisplayedDate(c.displayedMonth.AddDate(0, 1, 0))
})
c.monthNext.Importance = LowImportance

Expand Down Expand Up @@ -110,12 +135,12 @@ func (c *Calendar) calendarObjects() []fyne.CanvasObject {
}

func (c *Calendar) dateForButton(dayNum int) time.Time {
oldName, off := c.currentTime.Zone()
return time.Date(c.currentTime.Year(), c.currentTime.Month(), dayNum, c.currentTime.Hour(), c.currentTime.Minute(), 0, 0, time.FixedZone(oldName, off)).In(c.currentTime.Location())
oldName, off := c.displayedMonth.Zone()
return time.Date(c.displayedMonth.Year(), c.displayedMonth.Month(), dayNum, c.displayedMonth.Hour(), c.displayedMonth.Minute(), 0, 0, time.FixedZone(oldName, off)).In(c.displayedMonth.Location())
}

func (c *Calendar) daysOfMonth() []fyne.CanvasObject {
start := time.Date(c.currentTime.Year(), c.currentTime.Month(), 1, 0, 0, 0, 0, c.currentTime.Location())
start := time.Date(c.displayedMonth.Year(), c.displayedMonth.Month(), 1, 0, 0, 0, 0, c.displayedMonth.Location())
var buttons []fyne.CanvasObject

dayIndex := int(start.Weekday())
Expand Down Expand Up @@ -145,20 +170,65 @@ func (c *Calendar) daysOfMonth() []fyne.CanvasObject {
dayNum := d.Day()
s := strconv.Itoa(dayNum)
b := NewButton(s, func() {
selectedDate := c.dateForButton(dayNum)

c.OnChanged(selectedDate)
// TODO discuss if it is a good idea to unset selection when tapping the selected date
/*if selDate := c.dateForButton(dayNum); selDate.Format("02012006") != c.SelectedDate.Format("02012006") {
c.SelectedDate = selDate
} else {
c.SelectedDate = time.Time{}
}*/

c.SelectedDate = c.dateForButton(dayNum)
c.updateSelection()

if c.OnChanged != nil {
c.OnChanged(c.SelectedDate)
}
})
b.Importance = LowImportance

if !c.SelectedDate.IsZero() && c.SelectedDate.Year() == c.displayedMonth.Year() && c.SelectedDate.Month() == c.displayedMonth.Month() && c.SelectedDate.Day() == dayNum {
b.Importance = HighImportance
} else {
b.Importance = LowImportance
}

buttons = append(buttons, b)
}

return buttons
}

func (c *Calendar) updateSelection() {
if c.dates == nil || len(c.dates.Objects) <= 0 {
return
}

defer c.dates.Refresh()

if c.SelectedDate.IsZero() || c.SelectedDate.Month() != c.displayedMonth.Month() || c.SelectedDate.Year() != c.displayedMonth.Year() {
for i := 0; i < len(c.dates.Objects); i++ {
if b, ok := c.dates.Objects[i].(*Button); ok {
b.Importance = LowImportance
b.Refresh()
}
}
return
}

for i := 0; i < len(c.dates.Objects); i++ {
if b, ok := c.dates.Objects[i].(*Button); ok {
dayNum, _ := strconv.Atoi(b.Text)
if dayNum == c.SelectedDate.Day() {
b.Importance = HighImportance
} else {
b.Importance = LowImportance
}
b.Refresh()
}
}
}

func (c *Calendar) monthYear() string {
return c.currentTime.Format("January 2006")
return monthName(c.displayedMonth.Format("January")) + fmt.Sprintf(" %04d", c.displayedMonth.Year())
}

type calendarLayout struct {
Expand Down Expand Up @@ -238,3 +308,9 @@ func shortDayName(in string) string {
long := lang.X(lower, in)
return strings.ToUpper(lang.X(key, long[:3]))
}

func monthName(in string) string {
r := []rune(lang.X(strings.ToLower(in), in))
r[0] = unicode.ToUpper(r[0])
return string(r)
}
6 changes: 3 additions & 3 deletions widget/calendar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import (
func TestNewCalendar(t *testing.T) {
now := time.Now()
c := NewCalendar(now, func(time.Time) {})
assert.Equal(t, now.Day(), c.currentTime.Day())
assert.Equal(t, int(now.Month()), int(c.currentTime.Month()))
assert.Equal(t, now.Year(), c.currentTime.Year())
assert.Equal(t, now.Day(), c.displayedMonth.Day())
assert.Equal(t, int(now.Month()), int(c.displayedMonth.Month()))
assert.Equal(t, now.Year(), c.displayedMonth.Year())

_ = test.WidgetRenderer(c) // and render
assert.Equal(t, now.Format("January 2006"), c.monthLabel.Text)
Expand Down
34 changes: 28 additions & 6 deletions widget/date_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"time"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
)

Expand All @@ -12,7 +14,7 @@ import (
// Since: 2.6
type DateEntry struct {
Entry
Date *time.Time
Date *time.Time // TODO discuss: why not just use time.Time{} (Zero-time) ?
OnChanged func(*time.Time) `json:"-"`

dropDown *Calendar
Expand Down Expand Up @@ -117,9 +119,9 @@ func (e *DateEntry) Move(pos fyne.Position) {
// Implements: fyne.Widget
func (e *DateEntry) Resize(size fyne.Size) {
e.Entry.Resize(size)
if e.popUp != nil {
/*if e.popUp != nil {
e.popUp.Resize(fyne.NewSize(size.Width, e.popUp.Size().Height))
}
}*/
}

// SetDate will update the widget to a specific date.
Expand All @@ -141,7 +143,11 @@ func (e *DateEntry) popUpPos() fyne.Position {
}

func (e *DateEntry) setDate(d time.Time) {
e.Date = &d
if !d.IsZero() {
e.Date = &d
} else {
e.Date = nil
}
if e.popUp != nil {
e.popUp.Hide()
}
Expand All @@ -154,11 +160,27 @@ func (e *DateEntry) setupDropDown() *Button {
e.dropDown = NewCalendar(time.Now(), e.setDate)
}
dropDownButton := NewButton("", func() {
if e.Date != nil && !e.Date.IsZero() {
e.dropDown.SetSelectedDate(*e.Date)
e.dropDown.SetDisplayedDate(*e.Date)
} else {
e.dropDown.SetSelectedDate(time.Time{})
e.dropDown.SetDisplayedDate(time.Time{})
}

c := fyne.CurrentApp().Driver().CanvasForObject(e.super())

e.popUp = NewPopUp(e.dropDown, c)
e.popUp = NewPopUp(&fyne.Container{
Layout: layout.NewVBoxLayout(),
Objects: []fyne.CanvasObject{
e.dropDown,
&Button{Text: lang.X("today", "Today"), OnTapped: func() {
e.setDate(time.Now())
}},
},
}, c)
e.popUp.ShowAtPosition(e.popUpPos())
e.popUp.Resize(fyne.NewSize(e.Size().Width, e.popUp.MinSize().Height))
//e.popUp.Resize(fyne.NewSize(e.Size().Width, e.popUp.MinSize().Height))
})
dropDownButton.Importance = LowImportance
dropDownButton.SetIcon(e.Theme().Icon(theme.IconNameCalendar))
Expand Down
Loading