Skip to content

Commit

Permalink
Make updating/drawing related icons more efficient. Avoids unnecessar…
Browse files Browse the repository at this point in the history
…y layouts.
  • Loading branch information
cmeyer committed Jun 1, 2024
1 parent 4a3e35d commit b87fcc3
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 31 deletions.
76 changes: 58 additions & 18 deletions nion/swift/DataItemThumbnailWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from nion.ui import Widgets
from nion.utils import Geometry
from nion.utils import Process
from nion.utils import ReferenceCounting

if typing.TYPE_CHECKING:
from nion.swift.model import Persistence
Expand Down Expand Up @@ -152,52 +153,91 @@ def mouse_position_changed(self, x: int, y: int, modifiers: UserInterface.Keyboa


class ThumbnailCanvasItem(CanvasItem.CanvasItemComposition):
"""A canvas item that displays a thumbnail and allows dragging the thumbnail.
To facilitate reusing existing canvas items, set the thumbnail source using set_thumbnail_source.
This class keeps a bitmap and an overlay canvas item. The overlay canvas item is used for drag and drop events.
Callers can set the on_drag, on_drop_mime_data, and on_delete callbacks to handle these events.
"""

def __init__(self, ui: UserInterface.UserInterface, thumbnail_source: AbstractThumbnailSource, size: typing.Optional[Geometry.IntSize] = None) -> None:
super().__init__()
self.__ui = ui
self.__thumbnail_source = thumbnail_source
self.__thumbnail_size = size

# define the callbacks
self.on_drag: typing.Optional[typing.Callable[[UserInterface.MimeData, typing.Optional[_ImageDataType], int, int], None]] = None
self.on_drop_mime_data: typing.Optional[typing.Callable[[UserInterface.MimeData, int, int], str]] = None
self.on_delete: typing.Optional[typing.Callable[[], None]] = None

# set up the initial bitmap and overlay canvas items.
bitmap_overlay_canvas_item = BitmapOverlayCanvasItem()
bitmap_canvas_item = CanvasItem.BitmapCanvasItem(background_color="#CCC", border_color="#444")
bitmap_overlay_canvas_item.add_canvas_item(bitmap_canvas_item)
if size is not None:
bitmap_canvas_item.update_sizing(bitmap_canvas_item.sizing.with_fixed_size(size))
thumbnail_source.overlay_canvas_item.update_sizing(thumbnail_source.overlay_canvas_item.sizing.with_fixed_size(size))
bitmap_overlay_canvas_item.add_canvas_item(thumbnail_source.overlay_canvas_item)
self.__thumbnail_source = thumbnail_source
self.on_drag: typing.Optional[typing.Callable[[UserInterface.MimeData, typing.Optional[_ImageDataType], int, int], None]] = None
self.on_drop_mime_data: typing.Optional[typing.Callable[[UserInterface.MimeData, int, int], str]] = None
self.on_delete: typing.Optional[typing.Callable[[], None]] = None

def drag_pressed(x: int, y: int, modifiers: UserInterface.KeyboardModifiers) -> None:
on_drag = self.on_drag
if callable(on_drag):
mime_data = ui.create_mime_data()
valid, thumbnail = thumbnail_source.populate_mime_data_for_drag(mime_data, Geometry.IntSize(width=80, height=80))
if valid:
on_drag(mime_data, thumbnail, x, y)

# handle overlay drop callback by forwarding to the callback set by the caller.
def drop_mime_data(mime_data: UserInterface.MimeData, x: int, y: int) -> str:
if callable(self.on_drop_mime_data):
return self.on_drop_mime_data(mime_data, x, y)
return "ignore"

# handle overlay drag callback by forwarding to the callback set by the caller.
def delete() -> None:
on_delete = self.on_delete
if callable(on_delete):
on_delete()

bitmap_overlay_canvas_item.on_drag_pressed = drag_pressed
# connect the handlers to the overlay canvas item.
bitmap_overlay_canvas_item.on_drag_pressed = ReferenceCounting.weak_partial(ThumbnailCanvasItem.__drag_pressed, self)
bitmap_overlay_canvas_item.on_drop_mime_data = drop_mime_data
bitmap_overlay_canvas_item.on_delete = delete

def thumbnail_data_changed(thumbnail_data: typing.Optional[_NDArray]) -> None:
bitmap_canvas_item.rgba_bitmap_data = thumbnail_data
# store these for later use.
self.__bitmap_canvas_item = bitmap_canvas_item
self.__bitmap_overlay_canvas_item = bitmap_overlay_canvas_item

self.__thumbnail_source.on_thumbnail_data_changed = thumbnail_data_changed

bitmap_canvas_item.rgba_bitmap_data = self.__thumbnail_source.thumbnail_data
# connect the thumbnail source data changed event to a handler and call the handler to initialize.
self.__thumbnail_source.on_thumbnail_data_changed = ReferenceCounting.weak_partial(ThumbnailCanvasItem.__thumbnail_data_changed, self)
self.__thumbnail_data_changed(self.__thumbnail_source.thumbnail_data)

# add the overlay canvas item (with the bitmap canvas item) to this canvas item.
self.add_canvas_item(bitmap_overlay_canvas_item)

def __drag_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifiers) -> None:
# handle drag by forwarding to the callback set by the caller.
on_drag = self.on_drag
if callable(on_drag):
mime_data = self.__ui.create_mime_data()
valid, thumbnail = self.__thumbnail_source.populate_mime_data_for_drag(mime_data, Geometry.IntSize(width=80, height=80))
if valid:
on_drag(mime_data, thumbnail, x, y)

def __thumbnail_data_changed(self, thumbnail_data: typing.Optional[_NDArray]) -> None:
# update the bitmap canvas item with the new thumbnail data.
self.__bitmap_canvas_item.rgba_bitmap_data = thumbnail_data

@property
def thumbnail_source(self) -> AbstractThumbnailSource:
return self.__thumbnail_source

def set_thumbnail_source(self, thumbnail_source: AbstractThumbnailSource) -> None:
# reconfigure with the new thumbnail source.
self.__thumbnail_source.close()
self.__thumbnail_source = thumbnail_source
if self.__thumbnail_size is not None:
thumbnail_source.overlay_canvas_item.update_sizing(thumbnail_source.overlay_canvas_item.sizing.with_fixed_size(self.__thumbnail_size))
self.__bitmap_overlay_canvas_item.remove_canvas_item(self.__bitmap_overlay_canvas_item.canvas_items[-1])
self.__bitmap_overlay_canvas_item.add_canvas_item(self.__thumbnail_source.overlay_canvas_item)
self.__thumbnail_source.on_thumbnail_data_changed = ReferenceCounting.weak_partial(ThumbnailCanvasItem.__thumbnail_data_changed, self)
self.__thumbnail_data_changed(self.__thumbnail_source.thumbnail_data)

def close(self) -> None:
self.__thumbnail_source.close()
self.__thumbnail_source = typing.cast(typing.Any, None)
Expand Down
62 changes: 49 additions & 13 deletions nion/swift/DisplayPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,8 @@ def __init__(self, ui: UserInterface.UserInterface, document_model: DocumentMode
self.__source_thumbnails.layout = CanvasItem.CanvasItemRowLayout(spacing=8)
self.__dependent_thumbnails = CanvasItem.CanvasItemComposition()
self.__dependent_thumbnails.layout = CanvasItem.CanvasItemRowLayout(spacing=8)
self.__source_display_items = list[DisplayItem.DisplayItem]()
self.__dependent_display_items = list[DisplayItem.DisplayItem]()
self.add_spacing(12)
self.add_canvas_item(self.__source_thumbnails)
self.add_stretch()
Expand All @@ -837,6 +839,8 @@ def __init__(self, ui: UserInterface.UserInterface, document_model: DocumentMode
def close(self) -> None:
self.__related_items_stream_action.close()
self.__related_items_stream_action = typing.cast(typing.Any, None)
self.__source_display_items.clear()
self.__dependent_display_items.clear()
super().close()

@property
Expand All @@ -850,19 +854,51 @@ def _dependent_thumbnails(self) -> CanvasItem.CanvasItemComposition:
def __related_items_changed(self, items: typing.Optional[_RelatedItemsTuple]) -> None:
assert items is not None
source_display_items, dependent_display_items = items
self.__source_thumbnails.remove_all_canvas_items()
self.__dependent_thumbnails.remove_all_canvas_items()
for source_display_item in source_display_items:
thumbnail_source = DataItemThumbnailWidget.DataItemThumbnailSource(self.ui, display_item=source_display_item)
thumbnail_canvas_item = DataItemThumbnailWidget.ThumbnailCanvasItem(self.ui, thumbnail_source, self.__thumbnail_size)
thumbnail_canvas_item.update_sizing(thumbnail_canvas_item.sizing.with_fixed_height(self.__thumbnail_size.height))
thumbnail_canvas_item.on_drag = self.__drag_fn
self.__source_thumbnails.add_canvas_item(thumbnail_canvas_item)
for dependent_display_item in dependent_display_items:
thumbnail_source = DataItemThumbnailWidget.DataItemThumbnailSource(self.ui, display_item=dependent_display_item)
thumbnail_canvas_item = DataItemThumbnailWidget.ThumbnailCanvasItem(self.ui, thumbnail_source, self.__thumbnail_size)
thumbnail_canvas_item.on_drag = self.__drag_fn
self.__dependent_thumbnails.add_canvas_item(thumbnail_canvas_item)

# try to reuse thumbnail canvas items if possible. the algorithm runs through the max of source display items
# and canvas items. for iteration, if both source and canvas item exist, update it if it is changed. if only
# the source item exists, create a new canvas item and add it to the canvas. if only the canvas item exists,
# remove it from the canvas. this is repeated for the dependent display items. the canvas items for the list
# of canvas items are stored in the source_display_items and dependent_display_items lists respectively, to
# allow for quick comparison and updating.

for index in range(max(len(source_display_items), self.__source_thumbnails.canvas_items_count)):
source_display_item = source_display_items[index] if index < len(source_display_items) else None
thumbnail_canvas_item = typing.cast(DataItemThumbnailWidget.ThumbnailCanvasItem, self.__source_thumbnails.canvas_items[index]) if index < self.__source_thumbnails.canvas_items_count else None
if source_display_item and thumbnail_canvas_item:
thumbnail_canvas_item = typing.cast(DataItemThumbnailWidget.ThumbnailCanvasItem, self.__source_thumbnails.canvas_items[index])
if self.__source_display_items[index] != source_display_item:
thumbnail_canvas_item.set_thumbnail_source(DataItemThumbnailWidget.DataItemThumbnailSource(self.ui, display_item=source_display_item))
self.__source_display_items[index] = source_display_item
elif not source_display_item and thumbnail_canvas_item:
self.__source_thumbnails.remove_canvas_item(self.__source_thumbnails.canvas_items[-1])
self.__source_display_items.pop(-1)
elif source_display_item and not thumbnail_canvas_item:
thumbnail_source = DataItemThumbnailWidget.DataItemThumbnailSource(self.ui, display_item=source_display_items[index])
thumbnail_canvas_item = DataItemThumbnailWidget.ThumbnailCanvasItem(self.ui, thumbnail_source, self.__thumbnail_size)
thumbnail_canvas_item.update_sizing(thumbnail_canvas_item.sizing.with_fixed_height(self.__thumbnail_size.height))
thumbnail_canvas_item.on_drag = self.__drag_fn
self.__source_thumbnails.add_canvas_item(thumbnail_canvas_item)
self.__source_display_items.append(source_display_item)

for index in range(max(len(dependent_display_items), self.__dependent_thumbnails.canvas_items_count)):
dependent_display_item = dependent_display_items[index] if index < len(dependent_display_items) else None
thumbnail_canvas_item = typing.cast(DataItemThumbnailWidget.ThumbnailCanvasItem, self.__dependent_thumbnails.canvas_items[index]) if index < self.__dependent_thumbnails.canvas_items_count else None
if dependent_display_item and thumbnail_canvas_item:
thumbnail_canvas_item = typing.cast(DataItemThumbnailWidget.ThumbnailCanvasItem, self.__dependent_thumbnails.canvas_items[index])
if self.__dependent_display_items[index] != dependent_display_item:
thumbnail_canvas_item.set_thumbnail_source(DataItemThumbnailWidget.DataItemThumbnailSource(self.ui, display_item=dependent_display_item))
self.__dependent_display_items[index] = dependent_display_item
elif not dependent_display_item and thumbnail_canvas_item:
self.__dependent_thumbnails.remove_canvas_item(self.__dependent_thumbnails.canvas_items[-1])
self.__dependent_display_items.pop(-1)
elif dependent_display_item and not thumbnail_canvas_item:
thumbnail_dependent = DataItemThumbnailWidget.DataItemThumbnailSource(self.ui, display_item=dependent_display_items[index])
thumbnail_canvas_item = DataItemThumbnailWidget.ThumbnailCanvasItem(self.ui, thumbnail_dependent, self.__thumbnail_size)
thumbnail_canvas_item.update_sizing(thumbnail_canvas_item.sizing.with_fixed_height(self.__thumbnail_size.height))
thumbnail_canvas_item.on_drag = self.__drag_fn
self.__dependent_thumbnails.add_canvas_item(thumbnail_canvas_item)
self.__dependent_display_items.append(dependent_display_item)


class MissingDataCanvasItem(DisplayCanvasItem.DisplayCanvasItem):
Expand Down

0 comments on commit b87fcc3

Please sign in to comment.