Scott M
Scott M

Reputation: 744

Failing at extending fyne's widgets

(Many new details here.)

I am trying (very hard) to extend the functionality of a handful of Fyne's widgets. Basically I need to implement or "override" right click on things like Select, Button, Edit and so on. I need minor changes to some other behaviours as well.

I've tried extending existing widgets in various ways. Everything I do results in a compile failure, a panic, or bizarre and incorrect behaviour. The answer below provides a claim of a solution for Button, but it's incomplete. And when I apply the same technique to Select, it panics when left click is tried.

The panic is

panic: interface conversion: fyne.Canvas is nil, not *glfw.glCanvas

In retrospect, this is where I failed originally and is what prompted me to try increasingly complex approaches, none of which worked. Button and Select clearly have different requirements.

(Sorry to "unaccept", but the alternative is hours of trying things and having no confidence that what I tried - even if it avoids the panic - is the right and long term solution. The claim is that Fyne widgets are extensible - it seems reasonable to ask that the documentation be improved to show how, because naive attempts don't uniformly work.)

Here I try to extend the widgets I care about using the technique in the answer. I'd have thought that all these widgets would have acted identically, modulo the override of TappedSecondary. They generally don't. The right answer is obviously far more complex than presented in the answer below. Here's my findings based on the code given below:

Extending button (as shown in answer): widget.Button changes color when the mouse enters the area. The extended version does not. Neither form changes the appearance of the button when clicked (which I'd expect in general from any GUI, maybe something else is missing?) The extended version does catch TappedSecondary.

Extending Select (as shown above): left click panics. Right click is caught properly.

Extending Label: you have to add a do-nothing Tapped function to get it to match the interface needed, but right click can then be caught and all seems well.

Extending Entry: it catches TappedSecondary, which as expected means I don't get the usual dropdown menu. Left click (to move the cursor) doesn't redraw the cursor in the new location, but the internal cursor position does change (backspace will remove the character at the new position, and then redraw everything correctly).

Extending Check: TappedSecondary is caught. left-clicking causes OnChanged to be called (and the boolean passed properly alternates each time) but the check mark is never drawn.

As far as I know today these are the only widgets I need to extend, so I'd consider the answer complete when it makes the listed widgets work as they normally do except I get TappedSecondary. The ideal answer would point to new documentation online somewhere that gives complete examples, for all widget types, in a way that preserves the existing behaviour (unless you deliberately override that behaviour.)

Overall impression: extending a widget in the most trivial way possible can alter or break some aspects of drawing the widget when it responds to mouse events, including a panic from Select and missing graphical changes in Entry, Button and Check. I'm not overriding any existing Tapped() function so I don't know why this is the case.


//About contains name string, value string and other fields we don't care about

type HasAbout interface {
    GetAbout() *About
}

//loses reaction to mouse movement
type myButton struct {
    widget.Button
    about *About
}

func (m *myButton) TappedSecondary(*fyne.PointEvent) {
    log.Println("Right Click") //works
}

func (m *myButton) GetAbout() *About {
    return m.about
}

func newMyButton(label string, tapped func()) *myButton {
    ret := &myButton{}
    ret.ExtendBaseWidget(ret)
    ret.Text = label
    ret.OnTapped = tapped

    return ret
}

//panic on left click
type mySelect struct {
    widget.Select
    about *About
}

func (m *mySelect) TappedSecondary(at *fyne.PointEvent) {
    log.Println("Right Click", m.about.name, *at) //works
}

func (m *mySelect) GetAbout() *About {
    return m.about
}

func newMySelect(about *About) *mySelect {
    ret := &mySelect{}

    ret.ExtendBaseWidget(ret)
    ret.about = about
    //widget.Renderer(ret).Layout(ret.MinSize()) //from NewSelect, but did not fix panic
    return ret
}

//must override Tapped, and then all seems well
type myLabel struct {
    widget.Label
    about *About
}

func (m *myLabel) TappedSecondary(at *fyne.PointEvent) {
    log.Println("Right Click Label", m.GetAbout().name, *at)
}
//must also implement this or we don't get TappedSecondary
func (m *myLabel) Tapped(at *fyne.PointEvent) {
}

func (m *myLabel) GetAbout() *About {
    return m.about
}

func newMyLabel(about *About) *myLabel {
    ret := &myLabel{}

    ret.ExtendBaseWidget(ret)
    ret.about = about
    ret.Text = about.value
    return ret
}

//lose the ability to see cursor changes on left click.
//correctly, lose the usual dropdown menu on right click
type myEntry struct {
    widget.Entry
    about *About
}

func (m *myEntry) TappedSecondary(at *fyne.PointEvent) {
    log.Println("Right Click Entry", m.Text, *at)
}

func (m *myEntry) GetAbout() *About {
    return m.about
}

func newMyEntry(about *About) *myEntry {
    ret := &myEntry{}

    ret.ExtendBaseWidget(ret)
    ret.about = about
    return ret
}


//lose the ability to see check changes you made with left click.
type myCheck struct {
    widget.Check
    about *About
}

func (m *myCheck) TappedSecondary(at *fyne.PointEvent) {
    log.Println("Right Click myCheck", m.Text, *at) //works
}


func (m *myCheck) GetAbout() *About {
    return m.about
}

func newMyCheck(about *About) *myCheck {
    ret := &myCheck{}

    ret.ExtendBaseWidget(ret)
    ret.about = about
    ret.Text = about.value
    ret.OnChanged = func(v bool) {fmt.Println("Check is ", v)} //works
    return ret
}

//driver, from original answer, given new tests
func main() {
    about := About{}
    about.name = "hi"
    about.value = "Whee"

    a := app.New()

    w := a.NewWindow("Hello")
    w.SetContent(widget.NewVBox(
            newMyButton("Right tap me", func() {
                    log.Println("Normal callback")
            }),
            newMySelect(&about),
            newMyLabel(&about),
            newMyEntry(&about),
            newMyCheck(&about),
            widget.NewButton("X", func(){}) , //to compare against
        ),
)

    w.ShowAndRun()
}

Upvotes: 2

Views: 1698

Answers (1)

andy.xyz
andy.xyz

Reputation: 3265

Copying code out of the library is a distraction here - it is entirely possible to extend the widgets, so I will answer the original intent rather than fix the errors...

You can extend a core widget by embedding it in a struct then adding or overriding functionality. The key is to call ExtendBaseWidget so that the internals register your overriding type correctly. The following code shows how to make a new button that responds to right click.

package main

import (
    "log"

    "fyne.io/fyne"
    "fyne.io/fyne/app"
    "fyne.io/fyne/widget"
)

type myButton struct {
    widget.Button
}

func (m *myButton) TappedSecondary(*fyne.PointEvent) {
    log.Println("Right Click")
}

func newMyButton(label string, tapped func()) *myButton {
    ret := &myButton{}
    ret.ExtendBaseWidget(ret)
    ret.Text = label
    ret.OnTapped = tapped

    return ret
}

func main() {
    a := app.New()

    w := a.NewWindow("Hello")
    w.SetContent(widget.NewVBox(
        newMyButton("Right tap me", func() {
            log.Println("Normal callback")
        }),
    ))

    w.ShowAndRun()
}

Upvotes: 2

Related Questions