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

window. add close button interceptor to react on the window closing before anything is done #1180

Merged
merged 9 commits into from Aug 14, 2020

Conversation

ghost
Copy link

@ghost ghost commented Jul 10, 2020

Description:

adding wrapper to closing handler so that we can react on the window closing before the window is destroyed.
for example it can serve window closing confirmation

Fixes #467

Checklist:

  • Tests included.
  • Lint and formatter run with no errors.
  • Tests all pass on win10

Where applicable:

  • Public APIs match existing style.
  • Any breaking changes have a deprecation path or have been discussed.

example:

func main() {
	app := app.New()
	win := app.NewWindow("window title")
	win.Resize(fyne.NewSize(200, 100))

	win.SetCloseIntercept(func() {
		dialog.ShowConfirm("title", "close?",
			func(response bool) {
				if response {
					win.Close()
				}
			}, win)
	})

	win.SetContent(widget.NewHBox(
		widget.NewLabel("long text to exceed initial window width"),
	))

	win.ShowAndRun()
}

Copy link
Member

@andydotxyz andydotxyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the idea of ‘WillClose’ is to allow vetoing then perhaps it could be named more accordingly?

The glfw implementation seems complex - does it need a ‘closing’ bool and a goroutine? Isn’t it a case of calling the Function to verify, then continuing if ok? If the code was rearranged to exit early the diff could be much smaller.

Also why is the gomobile driver a no-op? Should we not be able to veto closing windows there?

@ghost
Copy link
Author

ghost commented Jul 12, 2020

Yes, the main benefit is to get the place to prevent the window closing, but there could be different cases where onClose is too late because window is already closed. I took name from the original discussion and if you see there's better option just write it.

closing yes it should be there, otherwise it's possible to get several handlers running that can create races. For example if you build my example and try to close the window several times without this protection it will show several confirmation dialogs.

Simple function call without goroutine is ok for a plain go code but using ShowConfirm in the handler is taking the app to a deadlock (checked on linux and windows). I'll keep old closed as doClose to simplify the diff

The only reason of no-op is i'm not sure how to check that case. I'll add the code there as well

@ghost
Copy link
Author

ghost commented Jul 12, 2020

  • rebased
  • old close is renamed to doCLose to reuse in willClose and make the diff easier to read
  • mobile part is implemented, checked on pixel 2 simulator.

There's an issue related to the ShowConfirm call already in the codebase (for mobile).
Calling it from the button click handler for example is not showing the dialog so i was checking my code using new window with two buttons.
My bad, set wrong parent window for ShowConfirm, everything is OK

@andydotxyz
Copy link
Member

Isn’t the race condition that requires ‘closing’ only present because of the goroutine?

If a confirm is required I would
Imagine that showing a confirm then vetoing the close, followed by a programmatic close if the user confirmed is a pretty standard workflow - this should not require goroutines should it?

@ghost
Copy link
Author

ghost commented Jul 12, 2020

I had no issues with the goroutine present but every time i've tried to remove it i'm experiencing a deadlock.
This could be related to the viewport. Closed is connected to it and looks like these handlers should not block the flow.

The easiest way to see the closing case is the window close [X] button that i'm not sure can be blocked

@andydotxyz
Copy link
Member

andydotxyz commented Jul 12, 2020

Isn't the deadlock related to the example code which blocks waiting for the interface to do something?

By moving to a goroutine you add the need to check for concurrent runs. Maybe this relates to the design - "WillClose" sounds like a notification or event but really it's supporting, or requiring, a long blocking wait whilst the user is asked of something. This is counter to how other callbacks are setup whereby they use the event thread and blocking would be a bad idea.

@andydotxyz
Copy link
Member

Thinking out loud a simpler design might require that if WillClose is set then the developer must call Window.Close() if that is what they want? That way threading is not required as the dialog callback can simply call that method if appropriate...

@ghost
Copy link
Author

ghost commented Jul 12, 2020

willClose is replacing the internal closing procedure if set and developer should call Close() explicitly in the willClose
It's not solving the deadlock and closing need, just making it client code responsibility. And only the external close command is covered by willClose

Copy link
Member

@andydotxyz andydotxyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few inline comments, it looks cleaner but would like a third opinion

test/testwindow.go Outdated Show resolved Hide resolved
window.go Outdated Show resolved Hide resolved
internal/driver/gomobile/window.go Outdated Show resolved Hide resolved
@andydotxyz
Copy link
Member

I think it looks cleaner this way - but perhaps others could feed in?
@toaster @stuartmscott @okratitan @lucor

@ghost
Copy link
Author

ghost commented Jul 13, 2020

closing this PR to reopen and get additional commit after recent guthub issues

@ghost ghost closed this Jul 13, 2020
@ghost ghost reopened this Jul 13, 2020
@ghost
Copy link
Author

ghost commented Jul 13, 2020

one of the text selection tests failed on one node, not sure it's related:

?   	fyne.io/fyne/tools/playground	[no test files]
--- FAIL: TestEntry_Select (0.50s)
    --- FAIL: TestEntry_Select/delete_select_down_with_Shift_still_hold (0.11s)
        util.go:60: 
            	Error Trace:	util.go:60
            	            				entry_test.go:1287
            	Error:      	Not equal: 
            	Test:       	TestEntry_Select/delete_select_down_with_Shift_still_hold
            	Messages:   	Image did not match master. Actual image written to file:///home/travis/gopath/src/fyne.io/fyne/widget/testdata/failed/entry/selection_initial.png.
2020/07/13 08:17:46 Fyne error:  Entry does not allow Truncation

@andydotxyz
Copy link
Member

I have restarted in case it was a temporary fault

internal/driver/glfw/window.go Outdated Show resolved Hide resolved
internal/driver/glfw/window_test.go Outdated Show resolved Hide resolved
@@ -404,6 +409,13 @@ func (w *window) Close() {
w.closed(w.viewport)
}

func (w *window) DoClose() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need that to be exported?

Copy link
Author

@ghost ghost Jul 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the idea is that user should explicitly close the window in this new handler

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO a difference between Close() and DoClose()isn't clear intuitively. I would stick with TryClose() and onCloseTry naming scheme. @andydotxyz What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DoClose is expected to close window without additional checks so TryClose is exactly opposite name

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that what I meant. Don't these names look more intuitive?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RustamGamidov Can you do as @andydotxyz suggests, i.e. rename willClose into onCloseIntercept, SetWillClose() into SetCloseIntercept() and get rid of DoClose()? Then the only thing that will remain is an approve from another contributor.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah... made it if i understood the idea. I'm just confused with how many things are moved to the client code like checking for deadlocks or loops. Especially because there was a comment regarding possible loop

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should not be neccessary to do any checking for loops or deadlocks in client code. The simplicity we are looking for is simplicity for the developers using the API.
They should not have to worry about threading at all, that is one of our design principles.

Copy link
Author

@ghost ghost Jul 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then i believe onCloseIntercepted should be called in goroutine like it was initially but it probably worth to wait for comments from other repo owners because we already used our arguments and most likely we'll stuck arguing

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if perhaps the event queue is the right place to put it? Just launching goroutines to work around possible deadlocks seems like it's not fixing the underlying issue? We used to do that with button taps and key presses and it caused a jumble of out-of-order events, so we don't do that any more.

internal/driver/gomobile/window.go Outdated Show resolved Hide resolved
window.go Outdated Show resolved Hide resolved
@ghost
Copy link
Author

ghost commented Jul 15, 2020

same test failed on the same node as far as i remember that still looks not related

?   	fyne.io/fyne/tools/playground	[no test files]
--- FAIL: TestEntry_Select (0.25s)
    --- FAIL: TestEntry_Select/delete_select_down_with_Shift_still_hold (0.06s)
        util.go:60: 
            	Error Trace:	util.go:60
            	            				entry_test.go:1287
            	Error:      	Not equal: 
            	Test:       	TestEntry_Select/delete_select_down_with_Shift_still_hold
            	Messages:   	Image did not match master. Actual image written to file:///home/travis/gopath/src/fyne.io/fyne/widget/testdata/failed/entry/selection_initial.png.

btw, locally on win10 i'm experiencing not stable clipboard test

Copy link
Member

@andydotxyz andydotxyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the when-to-check-intercept logic is reversed in this latest version, which is probably why you are seeing loops?

internal/driver/glfw/window.go Show resolved Hide resolved
internal/driver/glfw/window_test.go Outdated Show resolved Hide resolved
internal/driver/glfw/window_test.go Outdated Show resolved Hide resolved
internal/driver/glfw/window_test.go Show resolved Hide resolved
internal/driver/gomobile/window.go Show resolved Hide resolved
test/testwindow.go Outdated Show resolved Hide resolved
@okratitan okratitan self-requested a review July 15, 2020 14:54
@okratitan
Copy link
Member

So I think my feeling on this is - I like the style of WillClose with a return boolean. The Intercept requiring the dev to call Close() is a little confusing/not obvious in my opinion. However - WillClose is probably the wrong name and the name of that Method will need to be revisited.

On the flip side the implementation will be easier/cleaner with Intercept - as WillClose will have to involve some goroutines etc... So there is a trade off either way. I will be happy going with what the majority decides.

Copy link
Member

@Jacalz Jacalz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that I quite like the intercept behaviour that the PR implements currently. It looks like the example isn't updated though.

Rustam Gamidov added 6 commits August 11, 2020 08:40
   window.Close() should be called explicitly to close the window
   to keep Close for the programmatic closing with pre-checks
   to break the pre-checks loop without status variable
   be aware client code should close the window, check for loops and
   ensure there are locks
@ghost
Copy link
Author

ghost commented Aug 11, 2020

fixed the example and rebased over the current develop

Jacalz
Jacalz previously approved these changes Aug 11, 2020
Copy link
Member

@Jacalz Jacalz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. Thanks for rebasing and updating the example 👍

Copy link
Member

@andydotxyz andydotxyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great and is very close :) two comments inline that I think were missed from earlier discussion threads.
Last thought - in previous version there were tests, is it possible to re-add testing to this version?

if w.onClosed != nil {
w.queueEvent(w.onClosed)
if w.onCloseIntercepted != nil {
w.onCloseIntercepted()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Earlier you mentioned that this could block - should it be passed to w.queueEvent instead?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've tried that, and it's not helping unfortunately
i can do this to follow the pattern

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm what the issue is please?
Looking at the example above I can't help but feel the channel should not be needed - just calling Window.Close() in the callback should work.

Copy link
Author

@ghost ghost Aug 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the issue is not reacting ShowConfirm
Yes, it's solved with removing the channel with and without using queueEvent. Changed the example accordingly

window.go Outdated
@@ -64,6 +64,9 @@ type Window interface {
// SetOnClosed sets a function that runs when the window is closed.
SetOnClosed(func())

// SetCloseIntercept sets a function that runs instead of closing if defined
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a "." to this, and add another line instructing people to call Close() to confirm window closure?

@ghost
Copy link
Author

ghost commented Aug 11, 2020

is it possible to re-add testing to this version?

i don't see the way how to do this as the current solution is catching window manager closing only that is private

   no specific reason for that just following the pattern to decrease
   deadlock chance
@ghost
Copy link
Author

ghost commented Aug 11, 2020

# cd .; git clone -- https://github.com/fzipp/gocyclo /Users/travis/gopath/src/github.com/fzipp/gocyclo
Cloning into '/Users/travis/gopath/src/github.com/fzipp/gocyclo'...
fatal: unable to access 'https://github.com/fzipp/gocyclo/': Failed to connect to github.com port 443: Operation timed out
package github.com/fzipp/gocyclo: exit status 128

quite unlikely related to the PR

@ghost ghost changed the title window. add willClose handler to react on the window closing before anything is done window. add close button interceptor to react on the window closing before anything is done Aug 11, 2020
@andydotxyz
Copy link
Member

i don't see the way how to do this as the current solution is catching window manager closing only that is private

But from tests you can call private methods...

@ghost
Copy link
Author

ghost commented Aug 11, 2020

oh indeed! missed they are in the same package
made it in one test that makes tests dependent on the previous code but i see it's the common practice in the project

@ghost
Copy link
Author

ghost commented Aug 11, 2020

ci failed on one of the nodes

--- FAIL: TestFileWatcher_FileDeleted (0.19s)
    settings_desktop_test.go:92: File watcher callback was not called

Copy link
Member

@andydotxyz andydotxyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great thanks so much for getting this the whole way

@andydotxyz
Copy link
Member

@okratitan I think that @toaster and I are happy here, can you approve the latest version too please?

@andydotxyz andydotxyz merged commit b210665 into fyne-io:develop Aug 14, 2020
@andydotxyz
Copy link
Member

Many thanks, this will be in 1.4 release later this year.

@ghost
Copy link
Author

ghost commented Aug 14, 2020

🙏 thanks a lot for all your patience and comments! That was the long way!

@ghost ghost deleted the window-willClose branch August 14, 2020 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants