-
Notifications
You must be signed in to change notification settings - Fork 54
Autogenerate images of parts of the viewer #621
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
Merged
+377
−0
Merged
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
2fb68c1
autogeneration-preliminary-work
TimMonko 63e8387
clean up
TimMonko 11a111a
barely working popups skeleton
TimMonko fe5f1b7
im lost this popup sometimes works but usually doesn't
TimMonko 8072f05
cleaner working popups
TimMonko d0e69d4
gui includes both viewer and popups -- but popups sometimes don't work
TimMonko 456cdb8
seems to work less but code is better
TimMonko 91cde7a
remove popups file -- outdated
TimMonko 31e81d0
clean up widget and menu popup capturing
TimMonko b2df727
got popups working consistently
TimMonko 70e7e03
add more popups, clean up code more
TimMonko 0995949
speed up slightly
TimMonko 3d8aec9
lengthen menu QTimer so as not to cause issue with popup generation
TimMonko 4326a74
Merge branch 'main' into autogenerate-gui
TimMonko 70b3373
Refactor autogenerate_images function and modularize widget, menu, an…
TimMonko 73c5ce1
add mouse over for status bar
TimMonko e267262
improve docstrings
TimMonko c20d526
Add functionality to capture viewer regions
TimMonko 49f456f
Merge branch 'main' into autogenerate-gui
TimMonko 8f9b694
Merge branch 'main' into autogenerate-gui
melissawm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,238 @@ | ||
| from pathlib import Path | ||
|
|
||
| from qtpy.QtCore import QTimer, QPoint | ||
| import napari | ||
|
|
||
| from napari._qt.qt_event_loop import get_qapp | ||
| from napari._qt.qt_resources import get_stylesheet | ||
| from napari._qt.dialogs.qt_modal import QtPopup | ||
| from qtpy.QtWidgets import QApplication | ||
|
|
||
| DOCS = REPO_ROOT_PATH = Path(__file__).resolve().parent.parent | ||
| IMAGES_PATH = DOCS / "images" / "_autogenerated" | ||
| IMAGES_PATH.mkdir(parents=True, exist_ok=True) | ||
| WIDGETS_PATH = IMAGES_PATH / "widgets" | ||
| WIDGETS_PATH.mkdir(parents=True, exist_ok=True) | ||
| MENUS_PATH = IMAGES_PATH / "menus" | ||
| MENUS_PATH.mkdir(parents=True, exist_ok=True) | ||
| POPUPS_PATH = IMAGES_PATH / "popups" | ||
| POPUPS_PATH.mkdir(parents=True, exist_ok=True) | ||
|
|
||
| def autogenerate_images(): | ||
| app = get_qapp() | ||
|
|
||
| # Create viewer with visible window | ||
| viewer = napari.Viewer(show=True) | ||
| viewer.window._qt_window.resize(1000, 800) | ||
| viewer.window._qt_window.setStyleSheet(get_stylesheet("dark")) | ||
|
|
||
| # Ensure window is active | ||
| viewer.window._qt_window.activateWindow() | ||
| viewer.window._qt_window.raise_() | ||
| app.processEvents() | ||
|
|
||
| viewer.screenshot(str(IMAGES_PATH / "viewer_empty.png"), canvas_only=False) | ||
| # Add sample data | ||
| viewer.open_sample(plugin='napari', sample='cells3d') | ||
|
|
||
| app.processEvents() # Ensure viewer is fully initialized | ||
| viewer.screenshot(str(IMAGES_PATH / "viewer_cells3d.png"), canvas_only=False) | ||
|
|
||
| # Open the console | ||
| viewer_buttons = find_widget_by_class(viewer.window._qt_window, "QtViewerButtons") | ||
| viewer_buttons.consoleButton.click() | ||
|
|
||
| # Print Qt widget hierarchy | ||
| # print_widget_hierarchy(viewer.window._qt_window) | ||
|
|
||
| # Wait for viewer to fully initialize and render | ||
| QTimer.singleShot(200, lambda: capture_elements(viewer)) | ||
|
|
||
| app.exec_() | ||
|
|
||
| def capture_elements(viewer): | ||
| """Capture specific UI elements based on the widget hierarchy.""" | ||
| qt_window = viewer.window._qt_window | ||
|
|
||
| # Main components - using the hierarchy you provided | ||
| viewer_components = { | ||
| "welcome_widget": find_widget_by_class(qt_window, "QtWelcomeWidget"), | ||
|
|
||
| "console_dock": find_widget_by_name(qt_window, "console"), | ||
|
|
||
| "dimension_slider": find_widget_by_class(qt_window, "QtDims"), | ||
|
|
||
| # Layer list components | ||
| "layer_list_dock": find_widget_by_name(qt_window, "layer list"), | ||
| "layer_buttons": find_widget_by_class(qt_window, "QtLayerButtons"), | ||
| "layer_list": find_widget_by_class(qt_window, "QtLayerList"), | ||
| "viewer_buttons": find_widget_by_class(qt_window, "QtViewerButtons"), | ||
|
|
||
| # Layer controls | ||
| "layer_controls_dock": find_widget_by_name(qt_window, "layer controls"), | ||
|
|
||
| # TODO: mouse over part of the image to show intensity stuff | ||
| "status_bar": viewer.window._status_bar, | ||
| } | ||
|
|
||
| # Capture each component | ||
| for name, widget in viewer_components.items(): | ||
| capture_widget(widget, name) | ||
|
|
||
| menu_components = { | ||
| "file_menu": find_widget_by_name(qt_window, "napari/file"), | ||
| "samples_menu": find_widget_by_name(qt_window, "napari/file/samples/napari"), | ||
| "view_menu": find_widget_by_name(qt_window, "napari/view"), | ||
| "layers_menu": find_widget_by_name(qt_window, "napari/layers"), | ||
| "plugins_menu": find_widget_by_name(qt_window, "napari/plugins"), | ||
| "window_menu": find_widget_by_name(qt_window, "napari/window"), | ||
| "help_menu": find_widget_by_name(qt_window, "napari/help"), | ||
| } | ||
|
|
||
| for name, menu in menu_components.items(): | ||
| capture_menu(menu, name) | ||
|
|
||
|
|
||
| viewer_buttons = viewer_components["viewer_buttons"] | ||
|
|
||
| popups_configs = [ | ||
| { | ||
| "name": "ndisplay_2D_popup", | ||
| "prep": lambda: setattr(viewer.dims, "ndisplay", 2), | ||
| "button": viewer_buttons.ndisplayButton, | ||
| }, | ||
| { | ||
| "name": "roll_dims_popup", | ||
| "prep": lambda: setattr(viewer.dims, "ndisplay", 2), | ||
| "button": viewer_buttons.rollDimsButton, | ||
| }, | ||
| { | ||
| "name": "ndisplay_3D_popup", | ||
| "prep": lambda: setattr(viewer.dims, "ndisplay", 3), | ||
| "button": viewer_buttons.ndisplayButton, | ||
| }, | ||
| { | ||
| "name": "grid_popup", | ||
| "prep": None, | ||
| "button": viewer_buttons.gridViewButton, | ||
| } | ||
| ] | ||
|
|
||
| for config in popups_configs: | ||
| capture_popups(config) | ||
|
|
||
| QTimer.singleShot(100, lambda: close_all(viewer)) | ||
|
|
||
| def capture_popups(config): | ||
| """Capture popups that appear when clicking on viewer buttons.""" | ||
| app = get_qapp() | ||
| close_existing_popups() | ||
|
|
||
| if config["prep"] is not None: | ||
| config["prep"]() | ||
|
|
||
| app.processEvents() | ||
| config["button"].customContextMenuRequested.emit(QPoint()) | ||
| app.processEvents() | ||
| popups = [w for w in QApplication.topLevelWidgets() if isinstance(w, QtPopup) and w.isVisible()] | ||
|
|
||
| if not popups: | ||
| return print(f"No popup found for {config['name']}") | ||
|
|
||
| popup = popups[-1] # grab the most recent popup, just in case | ||
|
|
||
| app.processEvents() | ||
|
|
||
| pixmap = popup.grab() | ||
| pixmap.save(str(POPUPS_PATH / f"{config['name']}.png")) | ||
| popup.close() | ||
| app.processEvents() | ||
|
|
||
| def capture_widget(widget, name): | ||
| """Capture a widget and save it to a file.""" | ||
| if widget is None: | ||
| return print(f"Could not find {name}") | ||
|
|
||
| pixmap = widget.grab() | ||
| pixmap.save(str(WIDGETS_PATH / f"{name}.png")) | ||
| return | ||
|
|
||
| def capture_menu(menu, name): | ||
| """Show a menu and take screenshot of it.""" | ||
| if menu is None: | ||
| return print(f"Could not find menu {name}") | ||
|
|
||
| menu.popup(menu.parent().mapToGlobal(menu.pos())) | ||
|
|
||
| # Give menu time to appear | ||
| def grab_menu(): | ||
| pixmap = menu.grab() | ||
| pixmap.save(str(MENUS_PATH / f"{name}.png")) | ||
| menu.hide() | ||
|
|
||
| QTimer.singleShot(300, grab_menu) | ||
| return | ||
|
|
||
| def close_all(viewer): | ||
| viewer.close() | ||
| QTimer.singleShot(100, lambda: get_qapp().quit()) | ||
|
|
||
| def close_existing_popups(): | ||
| """Close any existing popups.""" | ||
| for widget in QApplication.topLevelWidgets(): | ||
| if isinstance(widget, QtPopup): | ||
| widget.close() | ||
|
|
||
| get_qapp().processEvents() | ||
|
|
||
| def find_widget_by_name(parent, name): | ||
| """Find a widget by its object name.""" | ||
| if parent.objectName() == name: | ||
| return parent | ||
|
|
||
| for child in parent.children(): | ||
| if hasattr(child, 'objectName') and child.objectName() == name: | ||
| return child | ||
|
|
||
| if hasattr(child, 'children'): | ||
| found = find_widget_by_name(child, name) | ||
| if found: | ||
| return found | ||
|
|
||
| return None | ||
|
|
||
| def find_widget_by_class(parent, class_name): | ||
| """Find a child widget by its class name.""" | ||
| if parent.__class__.__name__ == class_name: | ||
| return parent | ||
|
|
||
| for child in parent.children(): | ||
| if child.__class__.__name__ == class_name: | ||
| return child | ||
|
|
||
| if hasattr(child, 'children'): | ||
| found = find_widget_by_class(child, class_name) | ||
| if found: | ||
| return found | ||
|
|
||
| return None | ||
|
|
||
|
|
||
| def print_widget_hierarchy(widget, indent=0, max_depth=None): | ||
| """Print a hierarchy of child widgets with their class names and object names.""" | ||
|
|
||
| if max_depth is not None and indent > max_depth: | ||
| return | ||
|
|
||
| class_name = widget.__class__.__name__ | ||
| object_name = widget.objectName() | ||
| name_str = f" (name: '{object_name}')" if object_name else "" | ||
| print(" " * indent + f"- {class_name}{name_str}") | ||
|
|
||
| for child in widget.children(): | ||
| if hasattr(child, "children"): | ||
| print_widget_hierarchy(child, indent + 4, max_depth) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| autogenerate_images() | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.