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

Fixed App.current_window on macOS, for when a dialog is in focus #2926

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b031b18
Fixed macOS
proneon267 Oct 22, 2024
a8405fc
Added changelog
proneon267 Oct 22, 2024
c32d164
Separated dialogs for both windows
proneon267 Oct 22, 2024
02bc50a
pre-commit fix
proneon267 Oct 22, 2024
d143d19
Modified testbed::app::test_desktop::test_current_window
proneon267 Oct 22, 2024
8a42a91
Restart CI for intermittent iOS failure
proneon267 Oct 22, 2024
c667c3e
Test Run 1
proneon267 Oct 23, 2024
75f6e41
Test Run 2
proneon267 Oct 23, 2024
1e04814
Test Run 3
proneon267 Oct 23, 2024
43d1005
Cleaning up
proneon267 Oct 25, 2024
a373f26
Modified setup_info_dialog_result
proneon267 Nov 1, 2024
b984fbd
Merge branch 'beeware:main' into macOS_current_window_fix
proneon267 Nov 1, 2024
67d2fde
Modified setup_info_dialog_result
proneon267 Nov 1, 2024
6307ac6
Modified setup_info_dialog_result
proneon267 Nov 1, 2024
33a9694
Added note to docs
proneon267 Nov 2, 2024
beb3011
Fixed bug and probe on winforms
proneon267 Nov 5, 2024
9c6e99e
Removed unnecessary note for macOS.
proneon267 Nov 5, 2024
3d47599
Added pragma no branch
proneon267 Nov 5, 2024
dc62b34
Restart CI for intermittent readthedocs failures
proneon267 Nov 5, 2024
28880fa
Implemented on GTK
proneon267 Nov 6, 2024
f4020d5
Implemented on Android
proneon267 Nov 7, 2024
53d6f71
Implemented on iOS
proneon267 Nov 7, 2024
a479725
Update 2926.bugfix.rst
proneon267 Nov 7, 2024
3919067
Reimplemented on Winforms
proneon267 Nov 10, 2024
a21c30f
Correct probe method on cocoa
proneon267 Nov 10, 2024
dd97a81
Reimplemented on gtk
proneon267 Nov 10, 2024
fe18276
Correct behavior on cocoa
proneon267 Nov 10, 2024
d3bb915
Correct behavior on gtk
proneon267 Nov 10, 2024
e43f923
Correct behavior on winforms
proneon267 Nov 11, 2024
85ad15d
Reimplemented across platforms
proneon267 Nov 11, 2024
cdc052b
Merge branch 'main' into macOS_current_window_fix
proneon267 Nov 11, 2024
a1cd156
Minor fix
proneon267 Nov 11, 2024
91b944d
Merge branch 'macOS_current_window_fix' of https://github.com/proneon…
proneon267 Nov 11, 2024
4c81695
Correct test setup on cocoa
proneon267 Nov 12, 2024
378b5ce
Modified mobile test setup
proneon267 Nov 12, 2024
f4d6c64
Modified changenote
proneon267 Nov 12, 2024
f10bdc8
Fix implementation on macOS
proneon267 Nov 14, 2024
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
17 changes: 10 additions & 7 deletions android/src/toga_android/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,27 @@ def __init__(
):
super().__init__()

self.native = AlertDialog.Builder(toga.App.app.current_window._impl.app.native)
self.native.setCancelable(False)
self.native.setTitle(title)
self.native.setMessage(message)
self.native_builder = AlertDialog.Builder(
toga.App.app.current_window._impl.app.native
)
self.native_builder.setCancelable(False)
self.native_builder.setTitle(title)
self.native_builder.setMessage(message)
if icon is not None:
self.native.setIcon(icon)
self.native_builder.setIcon(icon)

self.native.setPositiveButton(
self.native_builder.setPositiveButton(
positive_text,
OnClickListener(
self.completion_handler,
True if (negative_text is not None) else None,
),
)
if negative_text is not None:
self.native.setNegativeButton(
self.native_builder.setNegativeButton(
negative_text, OnClickListener(self.completion_handler, False)
)
self.native = self.native_builder.create()

def completion_handler(self, return_value: bool) -> None:
self.future.set_result(return_value)
Expand Down
3 changes: 3 additions & 0 deletions android/tests_backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ def logs_path(self):
def assert_app_icon(self, icon):
pytest.xfail("Android apps don't have app icons at runtime")

def assert_dialog_in_focus(self, dialog):
freakboy3742 marked this conversation as resolved.
Show resolved Hide resolved
assert dialog._impl.native.isShowing() is True, "The dialog is not in focus"

def _menu_item(self, path):
menu = self.main_window_probe._native_menu()
for i_path, label in enumerate(path):
Expand Down
31 changes: 21 additions & 10 deletions android/tests_backend/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@


class DialogsMixin:
def _setup_alert_dialog_result(self, dialog, buttons, selected_index):
def _setup_alert_dialog_result(
self, dialog, buttons, selected_index, pre_close_test_method=None
):
# Install an overridden show method that invokes the original,
# but then closes the open dialog.
orig_show = dialog._impl.show
Expand All @@ -15,21 +17,30 @@ def automated_show(host_window, future):
async def _close_dialog():
# Inject a small pause without blocking the event loop
await asyncio.sleep(1.0 if self.app.run_slow else 0.2)

try:
dialog_view = self.get_dialog_view()
self.assert_dialog_buttons(dialog_view, buttons)
await self.press_dialog_button(dialog_view, buttons[selected_index])
except Exception as e:
# An error occurred closing the dialog; that means the dialog
# isn't what as expected, so record that in the future.
future.set_exception(e)
if pre_close_test_method:
pre_close_test_method()
finally:
try:
dialog_view = self.get_dialog_view()
self.assert_dialog_buttons(dialog_view, buttons)
await self.press_dialog_button(
dialog_view, buttons[selected_index]
)
except Exception as e:
# An error occurred closing the dialog; that means the dialog
# isn't what as expected, so record that in the future.
future.set_exception(e)

asyncio.create_task(_close_dialog(), name="close-dialog")

dialog._impl.show = automated_show

def setup_info_dialog_result(self, dialog):
self._setup_alert_dialog_result(dialog, ["OK"], 0)
def setup_info_dialog_result(self, dialog, pre_close_test_method=None):
freakboy3742 marked this conversation as resolved.
Show resolved Hide resolved
self._setup_alert_dialog_result(
dialog, ["OK"], 0, pre_close_test_method=pre_close_test_method
)

def setup_question_dialog_result(self, dialog, result):
self._setup_alert_dialog_result(dialog, ["No", "Yes"], 1 if result else 0)
Expand Down
1 change: 1 addition & 0 deletions changes/2926.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
On macOS, when a dialog is in focus, `App.current_window` now returns the host window, instead of raising an `AttributeError`.
7 changes: 6 additions & 1 deletion cocoa/src/toga_cocoa/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
NSMenu,
NSMenuItem,
NSNumber,
NSPanel,
NSScreen,
)
from .screens import Screen as ScreenImpl
Expand Down Expand Up @@ -366,7 +367,11 @@ def show_cursor(self):
######################################################################

def get_current_window(self):
return self.native.keyWindow
key_window = self.native.keyWindow
if isinstance(key_window, NSPanel):
return key_window.sheetParent
else:
return key_window

def set_current_window(self, window):
window._impl.native.makeKeyAndOrderFront(window._impl.native)
Expand Down
1 change: 1 addition & 0 deletions cocoa/src/toga_cocoa/libs/appkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@ class NSLineBreakMode(Enum):

######################################################################
# NSPanel.h
NSPanel = ObjCClass("NSPanel")

NSUtilityWindowMask = 1 << 4

Expand Down
15 changes: 12 additions & 3 deletions cocoa/tests_backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
NSEvent,
NSEventModifierFlagShift,
NSEventType,
NSPanel,
NSWindow,
)

from .dialogs import DialogsMixin
from .probe import BaseProbe, NSRunLoop

NSPanel = ObjCClass("NSPanel")
NSDate = ObjCClass("NSDate")


Expand Down Expand Up @@ -190,6 +190,11 @@ def activate_menu_close_all_windows(self):
def activate_menu_minimize(self):
self._activate_menu_item(["Window", "Minimize"])

def assert_dialog_in_focus(self, dialog):
assert (
dialog._impl.native.window == self.app._impl.native.keyWindow
), "The dialog is not in focus"
freakboy3742 marked this conversation as resolved.
Show resolved Hide resolved

def assert_menu_item(self, path, enabled):
item = self._menu_item(path)
assert item.isEnabled() == enabled
Expand Down Expand Up @@ -247,7 +252,7 @@ async def restore_standard_app(self):
self.app._impl.native.activateIgnoringOtherApps(True)
await self.redraw("Restore to standard app", delay=0.1)

def _setup_alert_dialog_result(self, dialog, result):
def _setup_alert_dialog_result(self, dialog, result, pre_close_test_method=None):
# Replace the dialog polling mechanism with an implementation that polls
# 5 times, then returns the required result.
_poll_modal_session = dialog._impl._poll_modal_session
Expand All @@ -258,7 +263,11 @@ def auto_poll_modal_session(nsapp, session):
if count < 5:
count += 1
return _poll_modal_session(nsapp, session)
return result
try:
if pre_close_test_method:
pre_close_test_method(dialog)
finally:
return result

dialog._impl._poll_modal_session = auto_poll_modal_session

Expand Down
8 changes: 6 additions & 2 deletions cocoa/tests_backend/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
class DialogsMixin:
supports_multiple_select_folder = True

def setup_info_dialog_result(self, dialog):
self._setup_alert_dialog_result(dialog, NSAlertFirstButtonReturn)
def setup_info_dialog_result(self, dialog, pre_close_test_method=None):
self._setup_alert_dialog_result(
dialog,
NSAlertFirstButtonReturn,
pre_close_test_method=pre_close_test_method,
)

def setup_question_dialog_result(self, dialog, result):
if result:
Expand Down
20 changes: 14 additions & 6 deletions cocoa/tests_backend/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,26 @@ def press_toolbar_button(self, index):
argtypes=[objc_id],
)

def _setup_alert_dialog_result(self, dialog, result):
def _setup_alert_dialog_result(self, dialog, result, pre_close_test_method=None):
# Install an overridden show method that invokes the original,
# but then closes the open dialog.
orig_show = dialog._impl.show

def automated_show(host_window, future):
orig_show(host_window, future)

dialog._impl.host_window.endSheet(
dialog._impl.host_window.attachedSheet,
returnCode=result,
)
try:
if pre_close_test_method:
pre_close_test_method(dialog)
finally:
try:
dialog._impl.host_window.endSheet(
dialog._impl.host_window.attachedSheet,
returnCode=result,
)
except Exception as e:
# An error occurred closing the dialog; that means the dialog
# isn't what as expected, so record that in the future.
future.set_exception(e)

dialog._impl.show = automated_show

Expand Down
5 changes: 5 additions & 0 deletions gtk/tests_backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ def assert_app_icon(self, icon):
mid_color = img.getpixel((img.size[0] // 2, img.size[1] // 2))
assert mid_color == (149, 119, 73, 255)

def assert_dialog_in_focus(self, dialog):
# Gtk.Dialog's methods - is_active(), has_focus() both return False, even
# when the dialog is in focus. Hence, they cannot be used to determine focus.
assert dialog._impl.native.is_visible(), "The dialog is not in focus"

def _menu_item(self, path):
main_menu = self.app._impl.native.get_menubar()
menu = main_menu
Expand Down
33 changes: 26 additions & 7 deletions gtk/tests_backend/dialogs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
from datetime import datetime, timedelta
from pathlib import Path
from unittest.mock import Mock
Expand All @@ -21,23 +22,41 @@ def _default_close_handler(self, dialog, gtk_result):
dialog._impl.native.response(gtk_result)
self._wait_for_dialog("Wait for dialog to disappear")

def _setup_dialog_result(self, dialog, gtk_result, close_handler=None):
def _setup_dialog_result(
self, dialog, gtk_result, close_handler=None, pre_close_test_method=None
):
# Install an overridden show method that invokes the original,
# but then closes the open dialog.
orig_show = dialog._impl.show

def automated_show(host_window, future):
orig_show(host_window, future)

if close_handler:
close_handler(dialog, gtk_result)
else:
self._default_close_handler(dialog, gtk_result)
async def _close_dialog():
# Add a slight delay for the dialog to show up
await asyncio.sleep(0.05)
try:
if pre_close_test_method:
pre_close_test_method(dialog)
finally:
try:
if close_handler:
close_handler(dialog, gtk_result)
else:
self._default_close_handler(dialog, gtk_result)
except Exception as e:
# An error occurred closing the dialog; that means the dialog
# isn't what as expected, so record that in the future.
future.set_exception(e)

asyncio.create_task(_close_dialog(), name="close-dialog")
freakboy3742 marked this conversation as resolved.
Show resolved Hide resolved

dialog._impl.show = automated_show

def setup_info_dialog_result(self, dialog):
self._setup_dialog_result(dialog, Gtk.ResponseType.OK)
def setup_info_dialog_result(self, dialog, pre_close_test_method=None):
self._setup_dialog_result(
dialog, Gtk.ResponseType.OK, pre_close_test_method=pre_close_test_method
)

def setup_question_dialog_result(self, dialog, result):
self._setup_dialog_result(
Expand Down
6 changes: 6 additions & 0 deletions iOS/tests_backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ def logs_path(self):
def assert_app_icon(self, icon):
pytest.xfail("iOS apps don't have app icons at runtime")

def assert_dialog_in_focus(self, dialog):
root_view_controller = self.native.keyWindow.rootViewController
assert (
root_view_controller.presentedViewController == dialog._impl.native
), "The dialog is not in focus"

def assert_system_menus(self):
pytest.skip("Menus not implemented on iOS")

Expand Down
27 changes: 19 additions & 8 deletions iOS/tests_backend/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class DialogsMixin:
def dialog_view_controller(self):
return self.app.current_window._impl.native.rootViewController

def _setup_alert_dialog(self, dialog, action_index):
def _setup_alert_dialog(self, dialog, action_index, pre_close_test_method=None):
# Install an overridden show method that invokes the original,
# but then closes the open dialog.
orig_show = dialog._impl.show
Expand All @@ -20,16 +20,27 @@ def automated_show(host_window, future):
NSRunLoop.currentRunLoop.runUntilDate(
NSDate.dateWithTimeIntervalSinceNow(1.0 if self.app.run_slow else 0.2)
)
# Close the dialog and trigger the completion handler
self.dialog_view_controller.dismissViewControllerAnimated(
False, completion=None
)
dialog._impl.native.actions[action_index].handler(dialog._impl.native)
try:
if pre_close_test_method:
pre_close_test_method(dialog)
finally:
try:
# Close the dialog and trigger the completion handler
self.dialog_view_controller.dismissViewControllerAnimated(
False, completion=None
)
dialog._impl.native.actions[action_index].handler(
dialog._impl.native
)
except Exception as e:
# An error occurred closing the dialog; that means the dialog
# isn't what as expected, so record that in the future.
future.set_exception(e)

dialog._impl.show = automated_show

def setup_info_dialog_result(self, dialog):
self._setup_alert_dialog(dialog, 0)
def setup_info_dialog_result(self, dialog, pre_close_test_method=None):
self._setup_alert_dialog(dialog, 0, pre_close_test_method=pre_close_test_method)

def setup_question_dialog_result(self, dialog, result):
self._setup_alert_dialog(dialog, 0 if result else 1)
Expand Down
Loading