Release stale native menu command items on rebuild - #4587
Conversation
fbd301e to
dc00219
Compare
|
CI note: the linux-wayland-qt testbed job failed only in the existing layout-sensitive tests/widgets/test_selection.py::test_flex_horizontal_widget_size (expected width at least 350; observed 73). The new command_rebuild_replaces_native_items test passed in that job. All other completed backend testbeds are green; the remaining jobs are still running. |
dc00219 to
2af2346
Compare
freakboy3742
left a comment
There was a problem hiding this comment.
As noted inline, the additional test isn't actually verifying anything; but the core of the implementation is on the right track. While the mechanics are correct, it's missing the "spirit" of the suggested change.
Cocoa is highlighted as a backend that doesn't have the problem - it has a matching pair of create_menu_item and remove_menu_item methods, and remove_menu_item does the full removal process. This means there's symmetry in the interface, and the abstracted create/remove method does the whole job.
However, in your PR, you've added remove_native methods... which are nothing more than a proxy to calling native.remove() (or the equivalent). We either need to work out how to abstract a full create_menu_item()/remove_menu_item() pair; or simplify the implementation to call the remove method directly.
|
|
||
|
|
||
| async def test_command_rebuild_replaces_native_items(app, app_probe): | ||
| """Rebuilding menus must release obsolete native command items.""" |
There was a problem hiding this comment.
How is this a test of the release behavior? The only thing it asserts is that a newly created menu item generates a new native item.
There was a problem hiding this comment.
The revised regression captures every pre-rebuild native item for an existing command, triggers a real application menu rebuild by adding another command, then asserts that none of the captured items remain registered on the original command. A second Qt/WinForms regression creates a temporary window with menu and toolbar items, closes it, and verifies those exact native objects are removed.
|
@lntutor It's great that you've made some progress on the issue. There is also the issue of deleting the menu items when the MainWindow is removed. Here's an example to highlight the problem: import gc
import toga
class MinimalApp(toga.App):
def __init__(self):
super().__init__(
formal_name="MinimalApp", app_id="com.example.minimal.app"
)
def startup(self):
def on_running(app=self):
all_commands = [cmd for cmd in app.commands._commands.values()]
cmd = all_commands[0]
for i in range(10):
temp_window = toga.MainWindow(title="Temp Window")
temp_window.show()
temp_window.close()
gc.collect()
print(f"The number of Toga windows is {len(self.windows)}.")
print(f"Native instances of `cmd`: {len(cmd._impl.native)}")
self.on_running = on_running
self.main_window = toga.MainWindow(title=self.formal_name)
self.main_window.show()
def main():
return MinimalApp()
if __name__ == "__main__":
app = main()
app.main_loop() |
|
Follow-up to the review: the commits after 2af2346 now take the direct-removal option. Qt and WinForms filter each window-owned item out of the associated command native list during window cleanup; no remove_native proxy interface was retained. The accompanying test selects only the items owned by the command being checked. |
|
The refreshed full CI matrix is now green, including both GTK3 Wayland and X11 testbeds. The requested temporary-window lifecycle scenario is covered by |
Oliver-Leigh
left a comment
There was a problem hiding this comment.
@lntutor Thanks for making changes to address the issue that I raised. This is getting a lot closer!
I want to raise a point in the spirit of something @freakboy3742 mentioned: We would like there to be as much consistency between the platforms as possible. It looks like you're getting good consistency between GTK, Qt and Windows, but not with macOS. For example, Command.native is a set on macOS, but a list elsewhere. Also, on macOS purge_toolbar uses a very different method to what you're using here.
I've highlighted a few other things in-line that need to be fixed too.
| temporary_window.toolbar.add(command) | ||
| await app_probe.redraw("Temporary window created") | ||
|
|
||
| if toga.backend == "toga_gtk": |
There was a problem hiding this comment.
We try not to add backend specifics in this way. If there are backend specific methodologies, then we prefer to use a probe and add the implementation specific code there. For example, in this test, you could create a window probe.
There was a problem hiding this comment.
Addressed in 74f1f1e. Backend-specific command-item discovery now lives in the Cocoa, GTK, Qt, and WinForms WindowProbe implementations; the shared test only performs backend-agnostic lifecycle assertions.
| or (toga.backend == "toga_gtk" and os.environ.get("TOGA_GTK") == "4"), | ||
| reason=("Window-local command items are only created by GTK3, Qt, and WinForms."), | ||
| ) | ||
| async def test_closed_window_releases_native_command_items(app, app_probe): |
There was a problem hiding this comment.
Since this is a window test, it's better to move it to test_window.py.
There was a problem hiding this comment.
Addressed in 74f1f1e. The closed-window lifecycle test now lives in testbed/tests/window/test_window.py.
| @pytest.mark.skipif( | ||
| toga.backend not in {"toga_gtk", "toga_qt", "toga_winforms"} | ||
| or (toga.backend == "toga_gtk" and os.environ.get("TOGA_GTK") == "4"), | ||
| reason=("Window-local command items are only created by GTK3, Qt, and WinForms."), | ||
| ) |
There was a problem hiding this comment.
We don't use this method to skip tests - if skipping is needed, you can find the functions in conftest.py. However, I don't think skipping is needed here. You write "Window-local command items are only created by GTK3, Qt, and WinForms", but the toolbar items are created on the other desktop backends.
There was a problem hiding this comment.
Addressed in 74f1f1e. The backend-specific skipif decorator was removed, so the test now covers Cocoa, GTK3, Qt, and WinForms. The unsupported GTK4 case is represented as a probe capability and routed through the existing skip_on_backends helper.
|
Following up on the current head What changed on this head:
The shared test no longer depends on the concrete container type of CI on this exact head is fully green, including the full testbed matrix. Please take another look when convenient. |
|
@Oliver-Leigh Current head |
|
@lntutor I’ve looked through the changes and I can’t see where you have addressed my concerns about the backend consistency. Can you have another look and let me know when these are addressed? Something else to keep in mind: It is highlighted in our contribution guide that the reviewer determines if an issue is resolved. If you mark an issue as resolved yourself, it makes the review process take considerably longer. |
Assisted-by: OpenAI Codex GPT-5 <noreply@openai.com>
74f1f1e to
6a5ba7d
Compare
|
@Oliver-Leigh I reworked the implementation on current Cocoa, GTK, Qt, and WinForms now use the same lifecycle model:
The backend probes now expose the same ownership mapping to the shared window lifecycle test. I also strengthened the app-menu and status-icon tests so they assert the old native objects are actually replaced or removed, avoiding the earlier vacuous-test problem. Verification on this exact commit:
I also reopened the four discussions I had previously marked resolved, so resolution remains with the reviewers as requested. |
Assisted-by: OpenAI Codex GPT-5 <noreply@openai.com>
Assisted-by: OpenAI Codex GPT-5 <noreply@openai.com>
Fixes #4580
Rebuilding application menus, window menus and toolbars, and status-icon menus now deregisters obsolete native command items on Cocoa, GTK, Qt, and WinForms.
The desktop backends use the same ownership model:
Command.nativeis a set of active native representations.The testbed covers application-menu rebuilds, status-icon menu rebuilds, and closing a temporary MainWindow with command items. Backend-specific discovery remains in WindowProbe implementations.
PR Checklist:
Assisted-by: OpenAI Codex