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

Refactor window closing and app exit handling #980

Merged
merged 50 commits into from
May 29, 2021
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
ea28928
Re-add on_exit handling and showing second window to winforms backend
obulat Jul 22, 2020
c5c8232
Add Secondary window and on_exit testing to dialogs app
obulat Jul 23, 2020
7f88423
Fix incorrect question dialog result
obulat Jul 23, 2020
452aa49
Add cancel_exit parameter to app.on_exit
obulat Jul 23, 2020
7b2bdac
Fix dialogs app
obulat Jul 23, 2020
c77bfbe
Fix flake8 error
obulat Jul 23, 2020
c015b2c
Add window on_close handler for secondary windows
obulat Jul 23, 2020
64806ed
Fix pep8 violation
obulat Jul 23, 2020
1987af2
Add initial value for window._on_close
obulat Jul 23, 2020
70540fb
Change window_count to secondary_window_count in Dialogs example app
obulat Jul 27, 2020
c214df8
Add `windows` set-like object to app
obulat Jul 29, 2020
8dcf638
Update window tests to add `app.windows +=` for adding window to app
obulat Aug 6, 2020
9a175c7
Rework handling of window closing
obulat Aug 7, 2020
189945f
Update tests, replace window on_close with toga_on_close
obulat Aug 7, 2020
36b2252
Transfer `on_close` handler logic (removing window from app.windows a…
obulat Aug 10, 2020
2842bff
Remove stray print and obsolete TODO
obulat Aug 10, 2020
7dfabf0
Fix pep8 violation
obulat Aug 10, 2020
7ced499
Add methods to pass tests
obulat Aug 19, 2020
246ef5c
Merge branch 'master' into winforms-on-exit
saroad2 Sep 27, 2020
cd5996a
Call on_exit when exiting an app using the exit() method
saroad2 Sep 27, 2020
42ab90c
Revert "Call on_exit when exiting an app using the exit() method"
saroad2 Sep 27, 2020
f3643a8
Merge remote-tracking branch 'origin/master' into winforms-on-exit
Mar 27, 2021
33d4921
Merge branch 'master' of https://github.com/beeware/toga into winform…
obulat Apr 14, 2021
cf6d32b
Add tests for window and app
obulat Apr 14, 2021
5d64c23
Fix flake8 error
obulat Apr 14, 2021
6d0c7d3
Remove interface window when closing it in cocoa
obulat Apr 14, 2021
fe53938
Add dummy factory to tests
obulat Apr 14, 2021
9b945cf
Fix app window iteration tests
obulat Apr 14, 2021
beea045
Refactor window close / app exit handling in cocoa
obulat May 8, 2021
f8ac0b1
Refactor winforms on_close / on_exit handlers
obulat May 8, 2021
dc12978
Readd on_close to MainWindow to pass tests
obulat May 8, 2021
d937748
Rename toga.Window.on_close to toga_on_close
obulat May 8, 2021
5fa9a35
Rename on_close to toga_on_close for MainWindow
obulat May 8, 2021
b36ce6b
Rename on_close to toga_on_close for MainWindow in gtk
obulat May 8, 2021
01e6cec
Rename on_close to toga_on_close for MainWindow in winforms
obulat May 8, 2021
17ff511
Add test_app_contains_window test
obulat May 8, 2021
62313e5
Fix exit handling; make on_close/on_exit return Boolean
obulat May 10, 2021
3099f96
Fix Winforms cancelling exit/window close events
obulat May 13, 2021
fa3891b
Improve demonstrations for on_close behavior.
freakboy3742 May 15, 2021
105c388
Enable on_close handler for main window.
freakboy3742 May 15, 2021
a8dbfc9
Correct behavior for cocoa on_close handling.
freakboy3742 May 15, 2021
e2a2593
Corrected impl on_close handling on gtk and winforms.
freakboy3742 May 15, 2021
ebf3c88
Disable setting on_close for main window
obulat May 23, 2021
2b871ec
Fix Winforms windows cancelling closing
obulat May 23, 2021
c791b9b
Fix failing test
obulat May 23, 2021
6b21942
Cleanups and clarifications of dialogs example app.
freakboy3742 May 29, 2021
4fc1e1d
Added inline documentation to explain logic.
freakboy3742 May 29, 2021
a61f936
Factored on_exit handling into the interface layer to ensure consiste…
freakboy3742 May 29, 2021
04ed041
Use platform-prefix naming for event handlers.
freakboy3742 May 29, 2021
cabef46
toga_on_close is an implementation detail.
freakboy3742 May 29, 2021
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
76 changes: 75 additions & 1 deletion examples/dialogs/dialogs/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,73 @@ def action_save_file_dialog(self, widget):
except ValueError:
self.label.text = "Save file dialog was canceled"

def window_close_handler(self, window):
# This handler is called before the window is closed, so there
# still are 1 more windows than the number of secondary windows
# after it is closed
# Return False if the window should stay open

# Check to see if there has been a previous close attempt.
if window in self.close_attempts:
# If there has, allow the close to proceed
self.set_window_label_text(len(self.windows)-2)
return True
else:
window.info_dialog(f'Abort {window.title}!', 'Maybe try that again...')
self.close_attempts.add(window)
return False

def action_open_secondary_window(self, widget):
self.window_counter += 1
window = toga.Window(title=f"New Window {self.window_counter}")
# Both self.windows.add() and self.windows += work:
self.windows += window

self.set_window_label_text()
secondary_label = toga.Label(text="You are in a secondary window!")
window.content = toga.Box(
children=[
secondary_label
],
style=Pack(
flex=1,
direction=COLUMN,
padding=10
)
)
window.on_close = self.window_close_handler
window.show()

def action_close_secondary_windows(self, widget):
for window in list(self.windows):
if window._WINDOW_CLASS != 'MainWindow':
window.close()

def exit_handler(self, app):
# Return True if app should close, and False if it should remain open
if self.main_window.confirm_dialog('Toga', 'Are you sure you want to quit?'):
print(f"Label text was '{self.label.text}' when you quit the app")
return True
else:
self.label.text = 'Exit canceled'
return False

def set_window_label_text(self, num_windows=None):
if num_windows is None:
num_windows = len(self.windows) - 1
self.window_label.text = f"{num_windows} secondary window(s) open"

def startup(self):
# Set up main window
self.main_window = toga.MainWindow(title=self.name)
freakboy3742 marked this conversation as resolved.
Show resolved Hide resolved
self.on_exit = self.exit_handler

# Label to show responses.
self.label = toga.Label('Ready.', style=Pack(padding_top=20))
self.window_label = toga.Label('', style=Pack(padding_top=20))
self.window_counter = 0
self.close_attempts = set()
self.set_window_label_text()

# Buttons
btn_style = Pack(flex=1)
Expand All @@ -135,6 +196,16 @@ def startup(self):
on_press=self.action_select_folder_dialog_multi,
style=btn_style
)
btn_open_secondary_window = toga.Button(
'Open Secondary Window',
on_press=self.action_open_secondary_window,
style=btn_style
)
btn_close_secondary_window = toga.Button(
'Close All Secondary Windows',
on_press=self.action_close_secondary_windows,
style=btn_style
)

btn_clear = toga.Button('Clear', on_press=self.do_clear, style=btn_style)

Expand All @@ -151,8 +222,11 @@ def startup(self):
btn_select,
btn_select_multi,
btn_open_multi,
btn_open_secondary_window,
btn_close_secondary_window,
btn_clear,
self.label
self.label,
self.window_label
],
style=Pack(
flex=1,
Expand Down
11 changes: 4 additions & 7 deletions src/cocoa/toga_cocoa/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,11 @@


class MainWindow(Window):
def on_close(self):
def toga_on_close(self):
self.interface.app.exit()


class AppDelegate(NSObject):
@objc_method
def applicationWillTerminate_(self, sender):
if self.interface.app.on_exit:
self.interface.app.on_exit(self.interface.app)

@objc_method
def applicationDidFinishLaunching_(self, notification):
self.native.activateIgnoringOtherApps(True)
Expand Down Expand Up @@ -261,7 +256,9 @@ def show_about_dialog(self):
self.native.orderFrontStandardAboutPanelWithOptions(options)

def exit(self):
self.native.terminate(None)
should_exit = self.interface.on_exit(self)
if should_exit:
self.native.terminate(self.native)

def set_on_exit(self, value):
pass
Expand Down
21 changes: 17 additions & 4 deletions src/cocoa/toga_cocoa/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ def height(self):

class WindowDelegate(NSObject):
@objc_method
def windowWillClose_(self, notification) -> None:
self.interface.on_close()
def windowShouldClose_(self, notification) -> bool:
return self.impl.toga_on_close()

@objc_method
def windowDidResize_(self, notification) -> None:
Expand Down Expand Up @@ -246,11 +246,24 @@ def show(self):
def set_full_screen(self, is_full_screen):
self.interface.factory.not_implemented('Window.set_full_screen()')

def on_close(self):
def set_on_close(self, handler):
pass

def toga_on_close(self):
if self.interface.on_close:
should_close = self.interface.on_close(self)
else:
should_close = True

if should_close:
self.interface.app.windows -= self.interface

return should_close

def close(self):
self.native.close()
# Calling performClose instead of close ensures that the on_close
# handlers in the delegates will be called
self.native.performClose(self.native)

def info_dialog(self, title, message):
return dialogs.info(self.interface, title, message)
Expand Down
52 changes: 52 additions & 0 deletions src/core/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ def test_is_full_screen(self):
self.assertFalse(self.app.is_full_screen)

def test_app_exit(self):
def exit_handler():
return
self.app.on_exit = exit_handler
self.assertIs(self.app.on_exit._raw, exit_handler)
self.app.exit()

self.assertActionPerformed(self.app, 'exit')
Expand All @@ -104,6 +108,54 @@ def test_full_screen(self):
self.app.set_full_screen()
self.assertFalse(self.app.is_full_screen)

def test_add_window(self):
test_window = toga.Window(factory=toga_dummy.factory)

self.assertEqual(len(self.app.windows), 0)
self.app.windows += test_window
self.assertEqual(len(self.app.windows), 1)
self.app.windows += test_window
self.assertEqual(len(self.app.windows), 1)
self.assertIs(test_window.app, self.app)

not_a_window = 'not_a_window'
with self.assertRaises(TypeError):
self.app.windows += not_a_window

def test_remove_window(self):
test_window = toga.Window(factory=toga_dummy.factory)
self.app.windows += test_window
self.assertEqual(len(self.app.windows), 1)
self.app.windows -= test_window
self.assertEqual(len(self.app.windows), 0)

not_a_window = 'not_a_window'
with self.assertRaises(TypeError):
self.app.windows -= not_a_window

test_window_not_in_app = toga.Window(factory=toga_dummy.factory)
with self.assertRaises(AttributeError):
self.app.windows -= test_window_not_in_app

def test_app_contains_window(self):
test_window = toga.Window(factory=toga_dummy.factory)
self.assertFalse(test_window in self.app.windows)
self.app.windows += test_window
self.assertTrue(test_window in self.app.windows)

def test_window_iteration(self):
test_windows = [
toga.Window(id=1, factory=toga_dummy.factory),
toga.Window(id=2, factory=toga_dummy.factory),
toga.Window(id=3, factory=toga_dummy.factory),
]
for window in test_windows:
self.app.windows += window
self.assertEqual(len(self.app.windows), 3)

for window in self.app.windows:
self.assertIn(window, test_windows)


class DocumentAppTests(TestCase):
def setUp(self):
Expand Down
24 changes: 20 additions & 4 deletions src/core/tests/test_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ class TestWindow(TestCase):
def setUp(self):
super().setUp()
self.window = toga.Window(factory=toga_dummy.factory)
self.app = toga.App('test_name', 'id.app', factory=toga_dummy.factory)

def test_raises_error_when_app_not_set(self):
self.app = None
with self.assertRaises(AttributeError):
self.window.show()

def test_widget_created(self):
self.assertIsNotNone(self.window.id)
app = toga.App('test_name', 'id.app', factory=toga_dummy.factory)
new_app = toga.App('error_name', 'id.error', factory=toga_dummy.factory)
self.window.app = app
self.window.app = self.app
with self.assertRaises(Exception):
self.window.app = new_app

Expand Down Expand Up @@ -62,8 +67,19 @@ def test_full_screen_set(self):

def test_on_close(self):
with patch.object(self.window, '_impl'):
self.window.on_close()
self.window._impl.on_close.assert_called_once_with()
self.app.windows += self.window
self.assertIsNone(self.window._on_close)

# set a new callback
def callback(window, **extra):
return 'called {} with {}'.format(type(window), extra)

self.window.on_close = callback
self.assertEqual(self.window.on_close._raw, callback)
self.assertEqual(
self.window.on_close('widget', a=1),
"called <class 'toga.window.Window'> with {'a': 1}"
)

def test_close(self):
with patch.object(self.window, "_impl"):
Expand Down
Loading