diff --git a/dialog.go b/dialog.go index b4b38c1..66c06b1 100644 --- a/dialog.go +++ b/dialog.go @@ -368,7 +368,7 @@ func (d *FormDialog) Show(parent ...fyne.Window) { d.items = d.itemsFn() d.concreteDialog.OnShow() - d.dlg = dialog.NewForm( + frm := dialog.NewForm( d.concreteDialog.Title(), d.confirmLabel, d.dismissLabel, @@ -376,6 +376,7 @@ func (d *FormDialog) Show(parent ...fyne.Window) { d.concreteDialog.OnClose, d.window, ) + d.dlg = frm d.dlg.Show() d.dlg.Resize(fyne.NewSize(640, 480)) if d.focusItem != nil { diff --git a/screen/element.go b/screen/element.go index 3ade1a9..8d98e9e 100644 --- a/screen/element.go +++ b/screen/element.go @@ -55,15 +55,17 @@ type Element struct { Disabled bool `yaml:"disabled"` Hidden bool `yaml:"hidden"` + // Properties shared by most widgets + Placeholder string `yaml:"placeholder"` + PlaceholderLocalized bool `yaml:"placeholderLocalized"` + // Calendar Properties Time *string `yaml:"time"` TimeFormat *string `yaml:"timeFormat"` // Entry Properties - MultiLine bool `yaml:"multiLine"` - Placeholder string `yaml:"placeholder"` - PlaceholderLocalized bool `yaml:"placeholderLocalized"` - Validator string `yaml:"validator"` + MultiLine bool `yaml:"multiLine"` + Validator string `yaml:"validator"` // List Properties ItemTemplate string `yaml:"itemTemplate"` @@ -73,6 +75,10 @@ type Element struct { // Check Properties Checked bool `yaml:"checked"` + // Select Properties + SelectOptionsBinding string `yaml:"selectOptionsBinding"` + SelectOptions []string `yaml:"selectOptions"` + // Spacer Properties FixHorizontal bool `yaml:"fixHorizontal"` FixVertical bool `yaml:"fixVertical"` @@ -85,6 +91,7 @@ type Element struct { OnSelected string `yaml:"onSelected"` OnUnselected string `yaml:"onUnselected"` OnDateSelected string `yaml:"onDateSelected"` + OnOptionSelected string `yaml:"onOptionSelected"` OnValidationChanged string `yaml:"onValidationChanged"` } diff --git a/screen/layout.go b/screen/layout.go index 9252bdc..154e299 100644 --- a/screen/layout.go +++ b/screen/layout.go @@ -219,6 +219,7 @@ func (l Layout) buildFormLayout( children = append(children, label, field) } + c = container.New(layout.NewFormLayout(), children...) return } diff --git a/screen/screen.go b/screen/screen.go index 8138247..86a4b40 100644 --- a/screen/screen.go +++ b/screen/screen.go @@ -24,6 +24,7 @@ type ListItemHandlerFn func(widget.ListItemID) type ClickHandlerFn func() type CheckHandlerFn func(bool) type DateSelectedHandlerFn func(time.Time) +type OptionSelectedHandlerFn func(string) type ValidationChangedHandlerFn func(error) type UIElement struct { @@ -48,6 +49,7 @@ type ScreenHandler interface { GetOnClickedHandler(string) ClickHandlerFn GetOnCheckChangedHandler(string) CheckHandlerFn GetOnDateSelectedHandler(string) DateSelectedHandlerFn + GetOnOptionSelectedHandler(string) OptionSelectedHandlerFn GetOnValidationChangedHandler(string) ValidationChangedHandlerFn } @@ -195,6 +197,10 @@ func (*TemplateScreenHandler) GetOnDateSelectedHandler(string) DateSelectedHandl return func(time.Time) {} } +func (*TemplateScreenHandler) GetOnOptionSelectedHandler(string) OptionSelectedHandlerFn { + return func(string) {} +} + func (*TemplateScreenHandler) GetOnValidationChangedHandler(string) ValidationChangedHandlerFn { return func(error) {} } diff --git a/screen/widget.go b/screen/widget.go index 49c6c37..f786700 100644 --- a/screen/widget.go +++ b/screen/widget.go @@ -55,6 +55,8 @@ const ( // - onChanged: The function to call when the checkbox is changed Check Widget = "Check" + DateEntry Widget = "DateEntry" + // Entry Widget // // Available yaml options: @@ -165,6 +167,12 @@ const ( // - onUnselect: The on unselect function for the list List Widget = "List" + // Select Widget + Select Widget = "Select" + + // SelectEntry Widget + SelectEntry Widget = "SelectEntry" + // Separator Widget Separator Widget = "Separator" @@ -207,6 +215,8 @@ func (w Widget) Build( widget, err = w.buildCalendarWidget(e, s) case Check: widget, err = w.buildCheckWidget(e, s) + case DateEntry: + widget, err = w.buildDateEntryWidget(e, s) case Entry: widget, err = w.buildEntryWidget(e, s) case H1, H2, H3, H4, H5, H6: @@ -217,6 +227,10 @@ func (w Widget) Build( widget, err = w.buildLabelWidget(e, s) case List: widget, err = w.buildListWidget(e, s) + case Select: + widget, err = w.buildSelectWidget(e, s) + case SelectEntry: + widget, err = w.buildSelectEntryWidget(e, s) case Separator: widget, err = w.buildSeparatorWidget(e, s) case Spacer: @@ -317,6 +331,23 @@ func (w Widget) buildCheckWidget( return } +func (w Widget) buildDateEntryWidget( + e *Element, + s ScreenHandler, +) (c fyne.CanvasObject, err error) { + ent := uiwidget.NewDateEntry() + + if e.OnDateSelected != "" { + ent.OnDateChanged = s.GetOnDateSelectedHandler(e.OnDateSelected) + } + + if e.Disabled { + ent.Disable() + } + c = ent + return +} + func (w Widget) buildEntryWidget( e *Element, s ScreenHandler, @@ -452,6 +483,82 @@ func (w Widget) buildListWidget( return } +func (w Widget) buildSelectWidget( + e *Element, + s ScreenHandler, +) (c fyne.CanvasObject, err error) { + var slc *widget.Select + var options []string + + if e.Binding != "" { + if lst, ok := s.GetBinding(e.SelectOptionsBinding).(binding.StringList); ok { + options, _ = lst.Get() + } + } else { + options = e.SelectOptions + } + + slc = widget.NewSelect(options, + s.GetOnOptionSelectedHandler(e.OnOptionSelected)) + slc.PlaceHolder = e.Placeholder + + for opt, val := range e.Options { + switch opt { + case "alignment": + slc.Alignment = getTextAlignment(val) + } + } + + if e.Disabled { + slc.Disable() + } + c = slc + return +} + +func (w Widget) buildSelectEntryWidget( + e *Element, + s ScreenHandler, +) (c fyne.CanvasObject, err error) { + var slc *widget.SelectEntry + var options []string + + if e.SelectOptionsBinding != "" { + if lst, ok := s.GetBinding(e.SelectOptionsBinding).(binding.StringList); ok { + options, _ = lst.Get() + lst.AddListener(binding.NewDataListener(func() { + options, _ = lst.Get() + if slc != nil { + slc.SetOptions(options) + } + })) + } + } else { + options = e.SelectOptions + } + + slc = widget.NewSelectEntry(options) + + if e.Binding != "" { + slc.Bind(s.GetBinding(e.Binding).(binding.String)) + } else { + slc.Text = e.Text + } + slc.PlaceHolder = e.Placeholder + if e.Validator != "" { + slc.Validator = s.GetValidator(e.Validator) + } + if e.OnValidationChanged != "" { + slc.SetOnValidationChanged(s.GetOnValidationChangedHandler(e.OnValidationChanged)) + } + + if e.Disabled { + slc.Disable() + } + c = slc + return +} + func (w Widget) buildSeparatorWidget( _ *Element, _ ScreenHandler, @@ -477,7 +584,12 @@ func (w Widget) buildUpDownLabelWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { + var minSize *fyne.Size + if e.MinSize != nil { + minSize = &fyne.Size{Width: e.MinSize.Width, Height: e.MinSize.Height} + } btn := uiwidget.NewUpDownLabelWithData( + minSize, s.GetBinding(e.Binding).(binding.String), s.GetOnClickedHandler(e.OnUpClicked), s.GetOnClickedHandler(e.OnDownClicked)) diff --git a/screenhandler.go b/screenhandler.go index 1ffaad6..05492c7 100644 --- a/screenhandler.go +++ b/screenhandler.go @@ -40,6 +40,7 @@ type ScreenHandler struct { onClickedHandlers map[string]screen.ClickHandlerFn onCheckChangedHandlers map[string]screen.CheckHandlerFn onDateSelectedHandlers map[string]screen.DateSelectedHandlerFn + onOptionSelectedHandlers map[string]screen.OptionSelectedHandlerFn onValidationChangedHandlers map[string]screen.ValidationChangedHandlerFn } @@ -82,6 +83,7 @@ func NewScreenHandler( h.onClickedHandlers = make(map[string]screen.ClickHandlerFn) h.onCheckChangedHandlers = make(map[string]screen.CheckHandlerFn) h.onDateSelectedHandlers = make(map[string]screen.DateSelectedHandlerFn) + h.onOptionSelectedHandlers = make(map[string]screen.OptionSelectedHandlerFn) h.onValidationChangedHandlers = make(map[string]screen.ValidationChangedHandlerFn) return } @@ -202,6 +204,17 @@ func (h *ScreenHandler) RegisterOnDateSelectedHandler( h.onDateSelectedHandlers[name] = fn } +// RegisterOnOptionSelectedHandler registers a OnOptionSelectedHandler with the ScreenHandler. +// +// Use this function to register the handler functions used in the screen +// definition. +func (h *ScreenHandler) RegisterOnOptionSelectedHandler( + name string, + fn screen.OptionSelectedHandlerFn, +) { + h.onOptionSelectedHandlers[name] = fn +} + // RegisterOnValidationChangedHandler registers a OnValidationChangedHandler with the ScreenHandler. // // Use this function to register the handler functions used in the screen @@ -245,6 +258,42 @@ func (h *ScreenHandler) GetElement(id string) *screen.UIElement { return elem } +func (h *ScreenHandler) ReplaceElementObject( + containerId string, + id string, + newDecorator fyne.CanvasObject, + newObject fyne.CanvasObject, +) { + containerElement := h.GetElement(containerId) + if containerElement == nil { + return + } + containerObj := containerElement.Object.(*fyne.Container) + + element := h.GetElement(id) + if element == nil { + return + } + decorator := element.Decorator + + allObjs := containerObj.Objects + replaced := false + for i, obj := range allObjs { + if obj == decorator { + allObjs[i] = newDecorator + replaced = true + break + } + } + + if replaced { + h.exportedElements[id].Decorator = newDecorator + h.exportedElements[id].Object = newObject + } + + containerObj.Refresh() +} + // GetLocalizer returns the i18n.Localizer used for localization. func (h *ScreenHandler) GetLocalizer() *i18n.Localizer { return h.localizer @@ -354,6 +403,15 @@ func (h *ScreenHandler) GetOnDateSelectedHandler(name string) screen.DateSelecte return fn } +func (h *ScreenHandler) GetOnOptionSelectedHandler(name string) screen.OptionSelectedHandlerFn { + fn, ok := h.onOptionSelectedHandlers[name] + if !ok { + log.Printf("option selected handler %s not found", name) + return nil + } + return fn +} + // GetOnValidationChangedHandler returns the OnValidationChangedHandler with the given name. func (h *ScreenHandler) GetOnValidationChangedHandler(name string) screen.ValidationChangedHandlerFn { fn, ok := h.onValidationChangedHandlers[name] diff --git a/uiwidget/dateentry.go b/uiwidget/dateentry.go new file mode 100644 index 0000000..ff5159f --- /dev/null +++ b/uiwidget/dateentry.go @@ -0,0 +1,131 @@ +package uiwidget + +import ( + "math" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + + xwidget "fyne.io/x/fyne/widget" +) + +type DateEntry struct { + widget.Entry + calendar *xwidget.Calendar + popUp *widget.PopUp + + OnDateChanged func(time.Time) + + date time.Time +} + +func NewDateEntry() *DateEntry { + e := &DateEntry{} + e.Validator = func(s string) error { + _, err := time.Parse("02/01/2006", s) + return err + } + e.OnChanged = func(s string) { + if e.Validate() != nil { + if e.OnDateChanged != nil { + time, _ := time.Parse("02/01/2006", s) + e.OnDateChanged(time) + } + } + } + e.ExtendBaseWidget(e) + e.SetDate(time.Now()) + return e +} + +func (e *DateEntry) CreateRenderer() fyne.WidgetRenderer { + e.ExtendBaseWidget(e) + if e.ActionItem == nil { + e.ActionItem = e.setupDropdown() + if e.Disabled() { + e.ActionItem.(fyne.Disableable).Disable() + } + } + return e.Entry.CreateRenderer() +} + +func (e *DateEntry) Enable() { + if e.ActionItem != nil { + e.ActionItem.(fyne.Disableable).Enable() + } + e.Entry.Enable() +} + +func (e *DateEntry) Disable() { + if e.ActionItem != nil { + e.ActionItem.(fyne.Disableable).Disable() + } + e.Entry.Disable() +} + +func (e *DateEntry) MinSize() fyne.Size { + e.ExtendBaseWidget(e) + return e.Entry.MinSize() +} + +func (e *DateEntry) Move(pos fyne.Position) { + e.Entry.Move(pos) + if e.popUp != nil { + e.popUp.Move(e.popUpPos()) + } +} + +func (e *DateEntry) Resize(size fyne.Size) { + e.Entry.Resize(size) + if e.popUp != nil { + e.popUp.Resize(fyne.NewSize(size.Width, e.popUp.Size().Height)) + } +} + +func (e *DateEntry) SetDate(date time.Time) { + e.date = date + e.calendar = xwidget.NewCalendar(date, e.onDateSelected) + e.SetText(date.Format("02/01/2006")) + + if e.ActionItem == nil { + e.ActionItem = e.setupDropdown() + if e.Disabled() { + e.ActionItem.(fyne.Disableable).Disable() + } + } +} + +func (e *DateEntry) GetDate() time.Time { + return e.date +} + +func (e *DateEntry) popUpPos() fyne.Position { + entryPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(e) + return entryPos.Add(fyne.NewPos(0, e.Size().Height-theme.InputBorderSize())) +} + +func (e *DateEntry) setupDropdown() *widget.Button { + dropdownButton := widget.NewButton("", func() { + c := fyne.CurrentApp().Driver().CanvasForObject(e) + + e.popUp = widget.NewPopUp(e.calendar, c) + e.popUp.ShowAtPosition(e.popUpPos()) + e.popUp.Resize( + fyne.NewSize( + float32(math.Max(float64(e.Size().Width), float64(e.popUp.MinSize().Width))), + e.popUp.MinSize().Height)) + }) + dropdownButton.Importance = widget.LowImportance + dropdownButton.SetIcon(theme.MenuDropDownIcon()) + return dropdownButton +} + +func (e *DateEntry) onDateSelected(date time.Time) { + e.date = date + e.SetText(e.date.Format("02/01/2006")) + if e.popUp != nil { + e.popUp.Hide() + } +} diff --git a/uiwidget/label.go b/uiwidget/label.go index cd00685..094dc2c 100644 --- a/uiwidget/label.go +++ b/uiwidget/label.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "bitbucket.org/hevanto/ui/uilayout" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" @@ -125,11 +126,11 @@ func NewLabelWithData(data binding.String, opts ...LabelOpts) *widget.Label { // // Returns fyne.CanvasObject: the created widget. func NewUpDownLabelWithData( + minSize *fyne.Size, data binding.String, upHandler func(), downHandler func(), ) fyne.CanvasObject { - c1 := container.NewWithoutLayout() c2 := container.NewWithoutLayout() w := NewWidgetBorder( widget.NewLabelWithData(data), @@ -137,15 +138,19 @@ func NewUpDownLabelWithData( bu := widget.NewButtonWithIcon("", theme.MoveUpIcon(), upHandler) bd := widget.NewButtonWithIcon("", theme.MoveDownIcon(), downHandler) w.Resize(fyne.NewSize(100, w.MinSize().Height)) - c1.Add(w) c2.Move(fyne.NewPos(w.Size().Width, 0)) - c1.Add(c2) bu.Resize(fyne.NewSize(30, w.Size().Height*0.5)) bd.Resize(fyne.NewSize(30, w.Size().Height*0.5)) bd.Move(fyne.NewPos(0, w.Size().Height*0.5)) c2.Add(bu) c2.Add(bd) - return c1 + + if minSize == nil { + minSize = &fyne.Size{Width: 100, Height: 0} + } + return container.NewHBox( + container.New(uilayout.NewMinSizeLayout(*minSize), + w), c2) } // NewH creates a RichText widget formatted as a header of the provided level.