Skip to content

Commit

Permalink
Merge pull request #980 from obulat/winforms-on-exit
Browse files Browse the repository at this point in the history
Refactor window closing and app exit handling
  • Loading branch information
freakboy3742 authored May 29, 2021
2 parents 4890a8c + cabef46 commit c563450
Show file tree
Hide file tree
Showing 13 changed files with 343 additions and 52 deletions.
78 changes: 77 additions & 1 deletion examples/dialogs/dialogs/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,75 @@ 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, update the window label and allow
# the close to proceed. The count is -2 (rather than -1)
# because *this* window hasn't been removed from
# the window list.
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(len(self.windows) - 1)
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):
# Close all windows that aren't the main window.
for window in list(self.windows):
if not isinstance(window, toga.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):
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)
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(0)

# Buttons
btn_style = Pack(flex=1)
Expand All @@ -135,6 +198,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 +224,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
15 changes: 8 additions & 7 deletions src/cocoa/toga_cocoa/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,17 @@


class MainWindow(Window):
def on_close(self):
def cocoa_windowShouldClose(self):
# Main Window close is a proxy for "Exit app".
# Defer all handling to the app's exit method.
# As a result of calling that method, the app will either
# exit, or the user will cancel the exit; in which case
# the main window shouldn't close, either.
self.interface.app.exit()
return False


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 @@ -290,7 +291,7 @@ def show_about_dialog(self):
self.native.orderFrontStandardAboutPanelWithOptions(options)

def exit(self):
self.native.terminate(None)
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.cocoa_windowShouldClose()

@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 cocoa_windowShouldClose(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(widget):
return True
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

0 comments on commit c563450

Please sign in to comment.