Skip to content

Commit

Permalink
Fix issue with display items model when changed from thread.
Browse files Browse the repository at this point in the history
  • Loading branch information
cmeyer committed Jan 27, 2025
1 parent 63db95b commit 3c4ac43
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 10 deletions.
14 changes: 6 additions & 8 deletions nion/swift/DataPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def audited_func() -> None:
class ItemExplorerController:
def __init__(self, ui: UserInterface.UserInterface,
canvas_item: CanvasItem.AbstractCanvasItem,
display_item_adapters_model: ListModel.MappedListModel, selection: Selection.IndexedSelection,
display_item_adapters_model: Panel.MappedListModelLike, selection: Selection.IndexedSelection,
direction: GridCanvasItem.Direction = GridCanvasItem.Direction.Row, wrap: bool = True) -> None:
self.__pending_tasks: typing.List[asyncio.Task[None]] = list()
self.ui = ui
Expand Down Expand Up @@ -317,7 +317,7 @@ def selection_changed() -> None:
self.__selection_changed_listener: typing.Optional[Event.EventListener] = self.__selection.changed_event.listen(selection_changed)
self.selected_indexes = list()

for index, display_item_adapter in enumerate(self.__display_item_adapters_model.display_item_adapters):
for index, display_item_adapter in enumerate(self.__display_item_adapters_model.items):
self.__display_item_adapter_inserted("display_item_adapters", display_item_adapter, index)

self.__closed = False
Expand Down Expand Up @@ -493,7 +493,7 @@ def drag_started(self, index: int, x: int, y: int, modifiers: UserInterface.Keyb


class DataGridController(ItemExplorerController):
def __init__(self, ui: UserInterface.UserInterface, display_item_adapters_model: ListModel.MappedListModel,
def __init__(self, ui: UserInterface.UserInterface, display_item_adapters_model: Panel.MappedListModelLike,
selection: Selection.IndexedSelection,
direction: GridCanvasItem.Direction = GridCanvasItem.Direction.Row, wrap: bool = True) -> None:
canvas_item = GridCanvasItem.GridCanvasItem(GridCanvasItemDelegate(self), selection, direction, wrap)
Expand Down Expand Up @@ -675,7 +675,7 @@ def unmap_display_item_to_display_item_adapter(display_item_adapter: DisplayItem

display_items_model = document_controller.filtered_display_items_model

self.__filtered_display_item_adapters_model = ListModel.MappedListModel(container=display_items_model, master_items_key="display_items", items_key="display_item_adapters", map_fn=map_display_item_to_display_item_adapter, unmap_fn=unmap_display_item_to_display_item_adapter)
filtered_display_item_adapters_model = ListModel.MappedListModel(container=display_items_model, master_items_key="display_items", items_key="display_item_adapters", map_fn=map_display_item_to_display_item_adapter, unmap_fn=unmap_display_item_to_display_item_adapter)

self.__selection = self.document_controller.selection

Expand All @@ -693,7 +693,7 @@ def focus_changed(focused: bool) -> None:
def delete_display_item_adapters(display_item_adapters: typing.List[DisplayItemAdapter]) -> None:
document_controller.delete_display_items([display_item_adapter.display_item for display_item_adapter in display_item_adapters if display_item_adapter.display_item])

self.data_grid_controller = DataGridController(ui, self.__filtered_display_item_adapters_model, self.__selection)
self.data_grid_controller = DataGridController(ui, Panel.ThreadSafeListModel(filtered_display_item_adapters_model, document_controller.event_loop), self.__selection)
self.data_grid_controller.on_context_menu_event = show_context_menu
self.data_grid_controller.on_focus_changed = focus_changed
self.data_grid_controller.on_delete_display_item_adapters = delete_display_item_adapters
Expand Down Expand Up @@ -758,7 +758,7 @@ def _get_mime_data_and_thumbnail_data(self, drag_started_event: ListCanvasItem.L
return mime_data, thumbnail_data

list_item_delegate = ListItemDelegate(self, self.__selection)
list_canvas_item = ListCanvasItem.ListCanvasItem2(display_items_model, self.__selection, list_item_factory, list_item_delegate, item_height=80, key="display_items")
list_canvas_item = ListCanvasItem.ListCanvasItem2(Panel.ThreadSafeListModel(display_items_model, document_controller.event_loop), self.__selection, list_item_factory, list_item_delegate, item_height=80, key="display_items")
scroll_area_canvas_item = CanvasItem.ScrollAreaCanvasItem(list_canvas_item)
scroll_bar_canvas_item = CanvasItem.ScrollBarCanvasItem(scroll_area_canvas_item, CanvasItem.Orientation.Vertical)
scroll_group_canvas_item = CanvasItem.CanvasItemComposition()
Expand Down Expand Up @@ -873,8 +873,6 @@ def close(self) -> None:
# finish closing
self.data_grid_controller.close()
self.data_grid_controller = typing.cast(DataGridController, None)
self.__filtered_display_item_adapters_model.close()
self.__filtered_display_item_adapters_model = typing.cast(ListModel.MappedListModel, None)
# button group
self.__view_button_group.close()
self.__view_button_group = typing.cast(CanvasItem.RadioButtonGroup, None)
Expand Down
4 changes: 2 additions & 2 deletions nion/swift/FilterPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def display_item_removed(key: str, display_item: _ValueType, index: int) -> None
# connect the display_items_model from the document controller to self.
# when data items are inserted or removed from the document controller, the inserter and remover methods
# will be called.
self.__display_items_model = document_controller.display_items_model
self.__display_items_model = Panel.ThreadSafeListModel(document_controller.display_items_model, document_controller.event_loop)

self.__display_item_inserted_listener = self.__display_items_model.item_inserted_event.listen(display_item_inserted)
self.__display_item_removed_listener = self.__display_items_model.item_removed_event.listen(display_item_removed)
Expand All @@ -111,7 +111,7 @@ def display_item_removed(key: str, display_item: _ValueType, index: int) -> None
self.__date_filter: typing.Optional[ListModel.Filter] = None
self.__text_filter: typing.Optional[ListModel.Filter] = None

for index, display_item in enumerate(self.__display_items_model.display_items):
for index, display_item in enumerate(self.__display_items_model.items):
display_item_inserted("display_items", display_item, index)

def close(self) -> None:
Expand Down
51 changes: 51 additions & 0 deletions nion/swift/Panel.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

# standard libraries
import asyncio
import collections
import dataclasses
import functools
Expand All @@ -20,7 +21,9 @@
from nion.ui import CanvasItem
from nion.ui import Declarative
from nion.ui import UserInterface
from nion.utils import Event
from nion.utils import Geometry
from nion.utils import ReferenceCounting
from nion.utils import Registry

if typing.TYPE_CHECKING:
Expand Down Expand Up @@ -607,3 +610,51 @@ def is_panel_section_valid(self, panel_section_id: str) -> bool:
if panel_section_id in panel_section_factory.panel_section_ids:
return True
return False


# useful utility class

class MappedListModelLike(typing.Protocol):
item_inserted_event: Event.Event
item_removed_event: Event.Event
end_changes_event: Event.Event

@property
def items(self) -> typing.Sequence[typing.Any]:
raise NotImplementedError()


class ThreadSafeListModel(MappedListModelLike):
def __init__(self, list_model: MappedListModelLike, event_loop: asyncio.AbstractEventLoop) -> None:
self.__items = list(list_model.items)
self.__event_loop = event_loop

self.__list_model = list_model
self.item_inserted_event = Event.Event()
self.item_removed_event = Event.Event()
self.end_changes_event = Event.Event()

self.__item_inserted_listener = self.__list_model.item_inserted_event.listen(ReferenceCounting.weak_partial(ThreadSafeListModel.__list_model_item_inserted, self))
self.__item_removed_listener = self.__list_model.item_removed_event.listen(ReferenceCounting.weak_partial(ThreadSafeListModel.__list_model_item_removed, self))
self.__end_changes_listener = self.__list_model.end_changes_event.listen(ReferenceCounting.weak_partial(ThreadSafeListModel.__end_changes, self))

def __list_model_item_inserted(self, key: str, item: typing.Any, before_index: int) -> None:
if threading.current_thread() != threading.main_thread():
self.__event_loop.call_soon_threadsafe(self.__list_model_item_inserted, key, item, before_index)
else:
self.__items.insert(before_index, item)
self.item_inserted_event.fire(key, item, before_index)

def __list_model_item_removed(self, key: str, item: typing.Any, index: int) -> None:
if threading.current_thread() != threading.main_thread():
self.__event_loop.call_soon_threadsafe(self.__list_model_item_removed, key, item, index)
else:
del self.__items[index]
self.item_removed_event.fire(key, item, index)

def __end_changes(self, key: str) -> None:
self.end_changes_event.fire(key)

@property
def items(self) -> typing.Sequence[typing.Any]:
return list(self.__items)

0 comments on commit 3c4ac43

Please sign in to comment.