Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resize ignored in popup containing wrapped text. #1570

Open
AlbinoGeek opened this issue Nov 17, 2020 · 18 comments
Open

Resize ignored in popup containing wrapped text. #1570

AlbinoGeek opened this issue Nov 17, 2020 · 18 comments
Labels
question A question has been asked

Comments

@AlbinoGeek
Copy link
Contributor

So, I have code that creates a widget.PopUp using the widget.NewModalPopUp function. However, one of the buttons in the model closes it out (modal.Hide()) and creates a new model (because simply replacing the Content does not have the size re-calculated, due to the current shadow bug + the wordwrap bug...) --

However, the second modal cannot be resized (???) -- I would assume it has something to do with the first one not being disposed, as there is no modal.Close() or equiv. function to trigger it to happen.

Am I missing something?

@andydotxyz
Copy link
Member

Hidden widgets should not be included in any calculations and disposal is not needed.
The Hide() call will remove the dialog from the stack so it should not impact future calculations.

You may have noticed that NewModalPopup is deprecated and so you may have hit a bug related to the old popup code.
Perhaps ShowModalPopup works? the New...Popup calls will be removed / replaced in 2.0.

@andydotxyz andydotxyz added the question A question has been asked label Nov 17, 2020
@AlbinoGeek
Copy link
Contributor Author

AlbinoGeek commented Nov 17, 2020

Same issue, and ShowModalPopUp doesn't give me a way to programmatically hide it, since I have no reference.

The work-around for now is to "resize all the things"

The following works, but exemplifies the bug:

Reproduction

  1. Call w.Step1() after showing the window / running the app
  2. Note how the label/wrap bug is not visible here, everything fits neatly.
  3. Click Next (which calls w.Step2())
  4. Observe modal that is suddenly skinny and tall with a broken shadow (resize ignored, label/wrap bug.)
  5. Uncomment modal.Resize and observe no broken modals (yay!)

Code

func (w *windowMain) WizardModal(skipText, nextText string, skipFn, nextFn func(*widget.PopUp), contents ...fyne.CanvasObject) {
	var modal *widget.PopUp

	if skipFn == nil {
		skipFn = func(*widget.PopUp) { modal.Hide() }
	}
	if nextFn == nil {
		nextFn = func(*widget.PopUp) { modal.Hide() }
	}

	buttons := make([]fyne.CanvasObject, 0)
	if skipText != "" {
		btn := widget.NewButtonWithIcon(skipText, theme.CancelIcon(), func() { skipFn(modal) })
		btn.Importance = widget.LowImportance
		buttons = append(buttons, btn)
	}
	if nextText != "" {
		btn := widget.NewButtonWithIcon(nextText, theme.NavigateNextIcon(), func() { nextFn(modal) })
		btn.Importance = widget.HighImportance
		buttons = append(buttons, btn)
	}

	box := widget.NewVBox(
		widget.NewCard(title, summary, widget.NewVBox(contents...)),
		layout.NewSpacer(),
		widget.NewSeparator(),
		fyne.NewContainerWithLayout(
			layout.NewGridLayout(len(buttons)),
			buttons...,
		),
	)

	modal = widget.NewModalPopUp(box, w.Canvas())
	modal.Show()

	// modal.Resize(fyne.NewSize(320, 220)) // required to resize subsequent modals
	box.Resize(fyne.NewSize(320, 220)) // required to resize the first shown modal
}

func (w *windowMain) Step1() {
	w.WizardModal("Skip", "Next", nil, func(modal *widget.PopUp) {
		modal.Hide()
		w.Step2()
	},
		labelWithWrapping("You are only two steps away from having your replays automatically uploaded!"),
		labelWithWrapping("We will guide you through both of the steps (don't worry, they're easy!"),
	)
}

func (w *windowMain) Step2() {
	w.WizardModal("Back", "Next", func(modal *widget.PopUp() {
		modal.Hide()
		w.Step1()
	}, func(modal *widget.PopUp) {
		modal.Hide()
	},
		labelWithWrapping("First thing's first. We need to find your StarCraft II Replays Directory!"),
		labelWithWrapping("More Lorem Ipsum text because I haven't written this yet. Shows off the bug."),
	)
}

@AlbinoGeek
Copy link
Contributor Author

Given that I have a work-around for this, I will be finishing up sc2-rsu interface before going keep into this.

However, once I've done that, I'll create a reproduction repository for this (and the linked) bug, to make it easier for you guys.

Sorry about creating all these issues, haha.

@andydotxyz
Copy link
Member

Thanks for all the information.
Could you please include the usage code for WizardModal? A simple main() function passing data that will replicate the issue. A full repository is very helpful, but really just making the code snippet above runnable would be sufficient.

@andydotxyz
Copy link
Member

I know it might be a daft thing to ask - but why not use a single modal dialog and pack a container inside it that can switch out contents instead? You could show/hide panels like the container.AppTabs does. Might be less code for you.

@andydotxyz andydotxyz changed the title [QUESTION] How to dispose of a widget.PopUp? Inconsistent resizing for modal popups Nov 17, 2020
@AlbinoGeek
Copy link
Contributor Author

In the above example, one need only call w.Step1() and the rest plays out through user interaction.

The following code is very pared-down, but is what was used to reproduce this bug.

Probably more scaffolding/structure than is required, hence I'm making a repository soon.

// Window represents a managed fyne.Window
type Window interface {
	GetWindow() fyne.Window
	SetWindow(fyne.Window)
	Init()
	Hide()
	Show()
}

type windowBase struct {
	fyne.Window
	app fyne.App
	ui  *graphicalInterface
}

// GetWindow returns the managed fyne.Window
func (b *windowBase) GetWindow() fyne.Window {
	return b.Window
}

// SetWindow is not used in this bug replication
func (b *windowBase) SetWindow(w fyne.Window) {
	b.Window = w
}

type windowMain struct {
	*windowBase
	// other stuff unrelated to bug
}

func (w *windowMain) Init() {
	w.windowBase.Window = w.windowBase.app.NewWindow("Main Window")
	w.SetContent(widget.NewVBox(
		widget.NewButton("Hi!", func() {
			w.Step1()
		}),
	))

	w.Resize(fyne.NewSize(420, 360))
	w.CenterOnScreen()
}

const (
	// WindowMain is an index string used in graphicalInterface.windows
	WindowMain = "Main"
)

func newUI() *graphicalInterface {
	ui := new(graphicalInterface)
	ui.app = app.New()

	ui.app.Settings().SetTheme(theme.DarkTheme())

	ui.windows = make(map[string]Window)
	ui.windows[WindowMain] = &windowMain{
		windowBase: &windowBase{app: ui.app, ui: ui}}
	// preparation of other windows
	ui.windows[WindowMain].Init()

	return ui
}

type graphicalInterface struct {
	app     fyne.App
	windows map[string]Window
}

// OpenWindow shows a given window, initializing it first if needed
func (ui *graphicalInterface) OpenWindow(windowName string) {
	if w, ok := ui.windows[windowName]; ok {
		if w.GetWindow() == nil {
			w.Init()
		} else {
			w.Show()
		}
	}
}

// Run starts the fyne.App and shows the main window
func (ui *graphicalInterface) Run() {
	ui.windows[WindowMain].Show()
	ui.app.Run()
}

func main() {
	GUI = newUI()
	GUI.Run()
}

I may be following through with that suggestion soon, keeping the modal around and managing its' lifecycle instead of trying to dispose it or otherwise. Part of the issue, is that some actions from the Modal can trigger Dialogs, which currently do not play nice together (it is more reliable to close the modal before showing a dialog, then returning the modal after the dialog has been dismissed) which requires a lot of state management.

@andydotxyz
Copy link
Member

There is no definition here for labelWithWrapping.
You might be interested to know that if I replace that with widget.NewLabel it seems to work as expected.

@AlbinoGeek
Copy link
Contributor Author

Apologies, that method is:

func labelWithWrapping(text string) *widget.Label {
	label := widget.NewLabel(text)
	label.Wrapping = fyne.TextWrapWord
	return label
}

Alright, I tried to refactor changing the contents, however:

  1. When a Modal's contents are replaced, the modal does not refresh
  2. Calling refresh on the modal redraws the model with the old contents, and does not re-draw the shadow

I am trying with a container now as you mentioned.

@andydotxyz
Copy link
Member

I will try with that code, but for note:

  1. If you change content manually you will need to call Refresh() to check it updates.
  2. I think the shadow issue is this one: Dialog shadow does not resize after Refresh #1370

@andydotxyz
Copy link
Member

andydotxyz commented Nov 18, 2020

When running the complete code I cannot see any issue. Can you please confirm the exact steps and show the bug outcome so we can verify?

I have tried with and without your calls to Resize() but cannot see whats wrong.

@AlbinoGeek
Copy link
Contributor Author

AlbinoGeek commented Nov 18, 2020

All four of the below gifs were recording using the new re-used Modal/Container code (attached at the bottom of this message)

With the 3 Resizes

Peek 2020-11-18 12-36

Issue: Border and shadow are is not correctly drawn on the 1st or 3rd modals (only the 2nd ???)


Without any Resizes

Peek 2020-11-18 12-38

With only a modal.Resize upon initial creation:

Marked in code with // 1

Peek 2020-11-18 12-39

Issue: Resize is ignored entirely (has to do with box containing the Label with wrapping.)

With 2 Resize upon initial creation:

Marked in code with // 1 and // 2

Peek 2020-11-18 12-41

Issue: Broken shadows just like with 3 resizes.

The modal also remains large when the content shrinks, but this is expected when not resizing (the Box doesn't realize its contents shrunk.)

Code for examples

func (w *windowMain) WizardModal(skipText, nextText string, skipFn, nextFn func(), contents ...fyne.CanvasObject) {
	if skipFn == nil {
		skipFn = func() { w.modal.Hide() }
	}
	if nextFn == nil {
		nextFn = func() { w.modal.Hide() }
	}

	buttons := make([]fyne.CanvasObject, 0)
	if skipText != "" {
		btn := widget.NewButtonWithIcon(skipText, theme.CancelIcon(), skipFn)
		btn.Importance = widget.LowImportance
		buttons = append(buttons, btn)
	}
	if nextText != "" {
		btn := widget.NewButtonWithIcon(nextText, theme.NavigateNextIcon(), nextFn)
		btn.Importance = widget.HighImportance
		buttons = append(buttons, btn)
	}

	// Re-use existing Modal
	if w.modal != nil {
		box := w.modal.Content.(*widget.Box)
		box.Children[0].(*widget.Card).Content.(*fyne.Container).Objects = contents
		box.Children[len(box.Children)-1] = fyne.NewContainerWithLayout(
			layout.NewGridLayout(len(buttons)),
			buttons...,
		)

		w.modal.Show()
		// box.Resize(fyne.NewSize(320, 220)) // 3
		w.modal.Refresh()
		return
	}

	// Create Fresh Modal
	container := fyne.NewContainerWithLayout(
		layout.NewVBoxLayout(),
		contents...,
	)

	box := widget.NewVBox(
		widget.NewCard("Welcome!", "First-Time Setup",
			container,
		),
		layout.NewSpacer(),
		widget.NewSeparator(),
		fyne.NewContainerWithLayout(
			layout.NewGridLayout(len(buttons)),
			buttons...,
		),
	)

	w.modal = widget.NewModalPopUp(box, w.Canvas())
	w.modal.Show()

	w.modal.Resize(fyne.NewSize(320, 220)) // 1
	box.Resize(fyne.NewSize(320, 220))     // 2
}

func (w *windowMain) openGettingStarted1() {
	w.WizardModal("Skip", "Next", nil, func() {
		if viper.GetString("replaysroot") == "" {
			w.openGettingStarted2()
		} else {
			w.modal.Hide()
		}
	},
		labelWithWrapping("You are only two steps away from having your replays automatically uploaded!"),
		labelWithWrapping("1) We will find your Replays Directory"),
		labelWithWrapping("2) We will find your sc2replaystats API Key"),
	)
}

func (w *windowMain) openGettingStarted2() {
	btnSettings := widget.NewButtonWithIcon("Open Settings", theme.SettingsIcon(), func() {
		w.ui.OpenWindow(WindowSettings)
	})
	btnSettings.Importance = widget.HighImportance

	// TODO: Refactor this to actually have the settings UI, not just direct the user to settings
	w.WizardModal("", "Next", nil, func() {
		if viper.GetString("apikey") == "" {
			w.openGettingStarted3()
		} else {
			w.modal.Hide()
		}
	},
		labelWithWrapping("First thing's first. We need to find your StarCraft II Replays Directory!"),
		btnSettings,
	)
}

func (w *windowMain) openGettingStarted3() {
	btnSettings := widget.NewButtonWithIcon("Open Settings", theme.SettingsIcon(), func() {
		w.ui.OpenWindow(WindowSettings)
	})
	btnSettings.Importance = widget.HighImportance

	// TODO: Refactor this to actually have the settings UI, not just direct the user to settings
	w.WizardModal("", "Next", nil, func() {
		w.modal.Hide()
	},
		labelWithWrapping("Lastly, please set your sc2replaystats API key. If you do not know how to find this, use the \"Login and find it for me\" button to have us login to your account and generate one on your behalf."),
		btnSettings,
	)
}

The interaction starts by calling w.openGettingStarted1()


I realize part of the issue now!

If you call modal.Resize with a size smaller than its MinSize, the modal is drawn at the MinSize, but the border/shadow are drawn at your smaller requested size -- leading to a shadow mismatching the dimensions of the modal. I would expect instead for the shadows to be drawn at whatever size the modal was drawn, since that already overrides what I asked for it to resize to.

@AlbinoGeek
Copy link
Contributor Author

This issue can be seen in the application AlbinoGeek/sc2-rsu@475bac24

Most of the GUI code pertaining to this can be found in the windowMain.go file:

https://github.com/AlbinoGeek/sc2-rsu/blob/475bac24fc0ce3079cefb6f38fde4ef08222e101/cmd/windowMain.go

@andydotxyz
Copy link
Member

If you call modal.Resize with a size smaller than its MinSize, the modal is drawn at the MinSize,

Yes - nothing will draw smaller than it's minSize.
The shadow being the wrong size is the same as the bug I linked before I think.

If we take shadows out of the equasion (as they are being tracked in #1370), is the issue that the sizes you were using are too small, or is there another issue in here that we still need to fix?

@andydotxyz
Copy link
Member

P.S. if you do want smaller dialogs than the size being presented, then in combination with your requested size in Resize() you could put the large text in a ScrollContainer which is the way to present large items in a smaller space.

@AlbinoGeek
Copy link
Contributor Author

AlbinoGeek commented Nov 18, 2020

If we ignore the shadows, then this particular issue boils down to:

With only a modal.Resize upon initial creation:

Issue: Resize is ignored entirely (has to do with box containing the Label with wrapping.)

The modal retains it's "skyscraper" default shape, instead of expanding to the requested width, or contracting to the requested height. It's simply as if the Resize command were ignored entirely.

@andydotxyz andydotxyz changed the title Inconsistent resizing for modal popups Resize ignored in popup containing wrapped text. Nov 18, 2020
@andydotxyz
Copy link
Member

andydotxyz commented Nov 18, 2020

Ok, updated the title accordingly.
This may relate to what was being worked on in PR #1001

@AlbinoGeek
Copy link
Contributor Author

Was a new PR for this issue made since the cleanup of issues/PR that "pre-date an infrastructure move"

@andydotxyz
Copy link
Member

Nobody has figured out how to solve this, so there is no PR open (the previous one was a work in progress that was not complete).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question A question has been asked
Projects
None yet
Development

No branches or pull requests

2 participants