-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add right click menu and styling (#29)
* add group layer delegate * rename group layer delegate * add context for right click menu * working minimal right click menu * fix syncing of selection * update docstrings and remove unused functions * fix double click edit * simplify right click actions and context to fix tests * update docstrings * Force widget in tests to have a parent (#35) * Allows toggling visibility of group layers (#30) * toggle visiblity for group layers * add tests for visiblity * update docstrings * fix failing tests * remove logger and thumbnail role * fix view controls when switching from group to layer --------- Co-authored-by: Will Graham <[email protected]>
- Loading branch information
1 parent
5bb3858
commit 42acf51
Showing
12 changed files
with
640 additions
and
10 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
This file contains 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,105 @@ | ||
from __future__ import annotations | ||
|
||
from typing import TYPE_CHECKING, List | ||
|
||
from app_model.types import Action | ||
from qtpy.QtCore import QPoint | ||
from qtpy.QtWidgets import QAction, QMenu | ||
|
||
from napari_experimental.group_layer import GroupLayer | ||
|
||
if TYPE_CHECKING: | ||
from qtpy.QtWidgets import QWidget | ||
|
||
|
||
class GroupLayerActions: | ||
"""Class holding all GroupLayerActions to be shown in the right click | ||
context menu. Based on structure in napari/layers/_layer_actions and | ||
napari/app_model/actions/_layerlist_context_actions | ||
Parameters | ||
---------- | ||
group_layers: GroupLayer | ||
Group layers to apply actions to | ||
""" | ||
|
||
def __init__(self, group_layers: GroupLayer) -> None: | ||
self.group_layers = group_layers | ||
|
||
self.actions: List[Action] = [ | ||
Action( | ||
id="napari:grouplayer:toggle_visibility", | ||
title="toggle_visibility", | ||
callback=self._toggle_visibility, | ||
) | ||
] | ||
|
||
def _toggle_visibility(self): | ||
"""Toggle the visibility of all selected groups and layers. If some | ||
selected groups/layers are inside others, then prioritise those | ||
highest in the tree.""" | ||
|
||
# Remove any selected items that are inside other selected groups | ||
# e.g. if a group is selected and also a layer inside it, toggling the | ||
# visibility will give odd results as toggling the group will toggle | ||
# the layer, then toggling the layer will toggle it again. We're | ||
# assuming groups higher up the tree have priority. | ||
items_to_toggle = self.group_layers.selection.copy() | ||
items_to_keep = [] | ||
for sel_item in self.group_layers.selection: | ||
if sel_item.is_group(): | ||
for item in items_to_toggle: | ||
if item not in sel_item or item == sel_item: | ||
items_to_keep.append(item) | ||
items_to_toggle = items_to_keep | ||
items_to_keep = [] | ||
|
||
# Toggle the visibility of the relevant selection | ||
for item in items_to_toggle: | ||
if not item.is_group(): | ||
visibility = item.layer.visible | ||
item.layer.visible = not visibility | ||
else: | ||
item.visible = not item.visible | ||
|
||
|
||
class ContextMenu(QMenu): | ||
"""Simplified context menu for the right click options. All actions are | ||
populated from GroupLayerActions. | ||
Parameters | ||
---------- | ||
group_layer_actions: GroupLayerActions | ||
Group layer actions used to populate actions in this menu | ||
title: str, optional | ||
Optional title for the menu | ||
parent: QWidget, optional | ||
Optional parent widget | ||
""" | ||
|
||
def __init__( | ||
self, | ||
group_layer_actions: GroupLayerActions, | ||
title: str | None = None, | ||
parent: QWidget | None = None, | ||
): | ||
QMenu.__init__(self, parent) | ||
self.group_layer_actions = group_layer_actions | ||
if title is not None: | ||
self.setTitle(title) | ||
self._populate_actions() | ||
|
||
def _populate_actions(self): | ||
"""Populate menu actions from GroupLayerActions""" | ||
for gl_action in self.group_layer_actions.actions: | ||
action = QAction(gl_action.title, parent=self) | ||
action.triggered.connect(gl_action.callback) | ||
self.addAction(action) | ||
|
||
def exec_(self, pos: QPoint): | ||
"""For now, rebuild actions every time the menu is shown. Otherwise, | ||
it doesn't react properly when items have been added/removed from | ||
the group_layer root""" | ||
self.clear() | ||
self._populate_actions() | ||
super().exec_(pos) | ||
This file contains 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
This file contains 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,118 @@ | ||
from __future__ import annotations | ||
|
||
from pathlib import Path | ||
from typing import TYPE_CHECKING | ||
|
||
from napari._qt.containers._base_item_model import ItemRole | ||
from napari._qt.containers._layer_delegate import LayerDelegate | ||
from napari._qt.containers.qt_layer_model import ThumbnailRole | ||
from napari._qt.qt_resources import QColoredSVGIcon | ||
from qtpy.QtCore import QPoint, QSize, Qt | ||
from qtpy.QtGui import QMouseEvent, QPainter, QPixmap | ||
|
||
from napari_experimental.group_layer_actions import ( | ||
ContextMenu, | ||
GroupLayerActions, | ||
) | ||
|
||
if TYPE_CHECKING: | ||
from qtpy import QtCore | ||
from qtpy.QtWidgets import QStyleOptionViewItem | ||
|
||
from napari_experimental.group_layer_qt import ( | ||
QtGroupLayerModel, | ||
QtGroupLayerView, | ||
) | ||
|
||
|
||
class GroupLayerDelegate(LayerDelegate): | ||
"""A QItemDelegate specialized for painting group layer objects.""" | ||
|
||
def get_layer_icon( | ||
self, option: QStyleOptionViewItem, index: QtCore.QModelIndex | ||
): | ||
"""Add the appropriate QIcon to the item based on the layer type. | ||
Same as LayerDelegate, but pulls folder icons from inside this plugin. | ||
""" | ||
item = index.data(ItemRole) | ||
if item is None: | ||
return | ||
if item.is_group(): | ||
expanded = option.widget.isExpanded(index) | ||
icon_name = "folder-open" if expanded else "folder" | ||
icon_path = ( | ||
Path(__file__).parent / "resources" / f"{icon_name}.svg" | ||
) | ||
icon = QColoredSVGIcon(str(icon_path)) | ||
else: | ||
icon_name = f"new_{item.layer._type_string}" | ||
try: | ||
icon = QColoredSVGIcon.from_resources(icon_name) | ||
except ValueError: | ||
return | ||
# guessing theme rather than passing it through. | ||
bg = option.palette.color(option.palette.ColorRole.Window).red() | ||
option.icon = icon.colored(theme="dark" if bg < 128 else "light") | ||
option.decorationSize = QSize(18, 18) | ||
option.decorationPosition = ( | ||
option.Position.Right | ||
) # put icon on the right | ||
option.features |= option.ViewItemFeature.HasDecoration | ||
|
||
def _paint_thumbnail( | ||
self, | ||
painter: QPainter, | ||
option: QStyleOptionViewItem, | ||
index: QtCore.QModelIndex, | ||
): | ||
"""paint the layer thumbnail - same as in LayerDelegate, but allows | ||
there to be no thumbnail for group layers""" | ||
thumb_rect = option.rect.translated(-2, 2) | ||
h = index.data(Qt.ItemDataRole.SizeHintRole).height() - 4 | ||
thumb_rect.setWidth(h) | ||
thumb_rect.setHeight(h) | ||
image = index.data(ThumbnailRole) | ||
if image is not None: | ||
painter.drawPixmap(thumb_rect, QPixmap.fromImage(image)) | ||
|
||
def editorEvent( | ||
self, | ||
event: QtCore.QEvent, | ||
model: QtCore.QAbstractItemModel, | ||
option: QStyleOptionViewItem, | ||
index: QtCore.QModelIndex, | ||
) -> bool: | ||
"""Called when an event has occurred in the editor""" | ||
# if the user clicks quickly on the visibility checkbox, we *don't* | ||
# want it to be interpreted as a double-click. Ignore this event. | ||
if event.type() == QMouseEvent.MouseButtonDblClick: | ||
self.initStyleOption(option, index) | ||
style = option.widget.style() | ||
check_rect = style.subElementRect( | ||
style.SubElement.SE_ItemViewItemCheckIndicator, | ||
option, | ||
option.widget, | ||
) | ||
if check_rect.contains(event.pos()): | ||
return True | ||
|
||
# refer all other events to LayerDelegate | ||
return super().editorEvent(event, model, option, index) | ||
|
||
def show_context_menu( | ||
self, | ||
index: QtCore.QModelIndex, | ||
model: QtGroupLayerModel, | ||
pos: QPoint, | ||
parent: QtGroupLayerView, | ||
): | ||
"""Show the group layer context menu. | ||
To add a new item to the menu, update the GroupLayerActions. | ||
""" | ||
if not hasattr(self, "_context_menu"): | ||
self._group_layer_actions = GroupLayerActions(model._root) | ||
self._context_menu = ContextMenu( | ||
self._group_layer_actions, parent=parent | ||
) | ||
|
||
self._context_menu.exec_(pos) |
Oops, something went wrong.