Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions nion/ui/CanvasUserInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,9 +919,10 @@ def drag_finished(action: str) -> None:
if drag_finished_fn:
drag_finished_fn(action)

# TODO
# drag = QtDrag(self.proxy, self.widget, typing.cast(QtMimeData, mime_data), thumbnail, hot_spot_x, hot_spot_y, drag_finished)
# drag.execute()
# a widget here is drawn by a canvas item within the hierarchy of the window displaying it, and that
# hierarchy is what carries a drag: it follows the mouse and delivers the mime data to whatever is under it,
# which may be another canvas item of the same window or something outside it entirely.
self.canvas_item.drag(mime_data, thumbnail, hot_spot_x, hot_spot_y, drag_finished)

def map_to_global(self, p: Geometry.IntPoint) -> Geometry.IntPoint:
# the canvas item knows where it is within the widget it is drawn in, and that widget knows where it is on the
Expand Down Expand Up @@ -2759,6 +2760,17 @@ def __window_size_changed(self, width: int, height: int) -> None:
self.__current_size = Geometry.IntSize(width=width, height=height)

def close(self) -> None:
# the widgets of this window are drawn by canvas items within one canvas widget of the window it wraps, so
# closing this window means closing both: the widgets first, which are what hold on to anything they were
# given, and then that canvas widget, which owns the canvas items drawing them.
canvas_widget = self.__canvas_widget
self.__canvas_widget = None
super().close()
# the canvas items draw on a thread, and closing them is what stops it. they have to stop before the window
# they draw in goes away, or the drawing carries on into a window which is no longer there.
if canvas_widget:
canvas_widget.close()
self.__canvas_item = typing.cast(typing.Any, None)
self.__ui.destroy_document_window(self.__window)
self.__window = typing.cast(typing.Any, None)

Expand Down
29 changes: 29 additions & 0 deletions nion/ui/Declarative.py
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,9 @@ def create_list_view(self, *,
on_escape_pressed: typing.Optional[UICallableIdentifier] = None,
on_return_pressed: typing.Optional[UICallableIdentifier] = None,
on_item_handle_context_menu: typing.Optional[UICallableIdentifier] = None,
on_item_drag_started: typing.Optional[UICallableIdentifier] = None,
on_can_drop_mime_data: typing.Optional[UICallableIdentifier] = None,
on_drop_mime_data: typing.Optional[UICallableIdentifier] = None,
**kwargs: typing.Any) -> UIDescriptionResult:
"""Create a list view UI description with items, an item component, and the item height.

Expand All @@ -1044,6 +1047,9 @@ def create_list_view(self, *,
on_escape_pressed: callback when escape is pressed, return true if handled (optional)
on_return_pressed: callback when return is pressed, return true if handled (optional)
on_item_handle_context_menu: callback to display context menu, passes gx, gy, index (optional)
on_item_drag_started: callback when the user starts dragging an item, passes index, x, y, modifiers (optional)
on_can_drop_mime_data: callback asking whether mime data can be dropped, passes mime_data, action, drop_index (optional)
on_drop_mime_data: callback to accept dropped mime data at drop_index, passes mime_data, action, drop_index (optional)

Returns:
UI description of the list view
Expand All @@ -1055,6 +1061,17 @@ def create_list_view(self, *,
Each item handler is given an `is_selected_model` property, a boolean property model tracking whether its item
is selected. An item component can bind to it, e.g. `@binding(is_selected_model.value)`, to display the item
differently when it is selected.

An item can also carry a `tool_tip` property, which is displayed while the mouse rests on that item. Unlike
the tool tip of the list view itself, which is the same wherever the mouse rests on it, this one belongs to
the one item and changes as the mouse moves from item to item.

The `on_item_drag_started` callback describes the item being dragged as mime data and starts the drag, by
calling `drag` on the widget it is passed. The list takes part in a drop only when it is given
`on_can_drop_mime_data` or `on_drop_mime_data`. A drop lands in the gap between two items, so `drop_index` is
the index the drop would be inserted at: zero for the gap before the first item, and the number of items for
the gap after the last one, which is also what a list with no items at all reports.
`on_drop_mime_data` returns the action taken, or "ignore".
"""
d: UIDescriptionResult = {"type": "list_view"}
if name is not None:
Expand All @@ -1079,6 +1096,12 @@ def create_list_view(self, *,
d["on_return_pressed"] = on_return_pressed
if on_item_handle_context_menu is not None:
d["on_item_handle_context_menu"] = on_item_handle_context_menu
if on_item_drag_started is not None:
d["on_item_drag_started"] = on_item_drag_started
if on_can_drop_mime_data is not None:
d["on_can_drop_mime_data"] = on_can_drop_mime_data
if on_drop_mime_data is not None:
d["on_drop_mime_data"] = on_drop_mime_data
self.__process_common_properties(d, **kwargs)
return d

Expand Down Expand Up @@ -1899,6 +1922,12 @@ def construct_list_view(ui: UserInterface.UserInterface, window: Window.Window,
connect_event(widget, widget, d, handler, "on_escape_pressed", [])
connect_event(widget, widget, d, handler, "on_return_pressed", [])
connect_event(widget, widget, d, handler, "on_item_handle_context_menu", ["x", "y", "gx", "gy", "index"])
connect_event(widget, widget, d, handler, "on_item_drag_started", ["index", "x", "y", "modifiers"])
connect_event(widget, widget, d, handler, "on_can_drop_mime_data", ["mime_data", "action", "drop_index"])
connect_event(widget, widget, d, handler, "on_drop_mime_data", ["mime_data", "action", "drop_index"])
# a list which cannot take a drop leaves the drag to whatever is drawn behind it, so only one which says how
# to handle a drop takes part in one.
widget.wants_drag_events = "on_can_drop_mime_data" in d or "on_drop_mime_data" in d
connect_attributes(widget, d, handler, finishes)
return widget

Expand Down
53 changes: 37 additions & 16 deletions nion/ui/GridFlowCanvasItem.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ def is_dropping(self) -> bool:
def is_dropping(self, value: bool) -> None:
if value != self.is_dropping:
self.__is_dropping_model.value = value
self.update()
# the adornments are what draw the drop highlight over the item.
self.__adornments_canvas_item.is_dropping = value

def context_menu_event(self, x: int, y: int, gx: int, gy: int) -> bool:
grid_flow_canvas_item = self.__grid_flow_canvas_item_ref()
Expand Down Expand Up @@ -308,8 +309,6 @@ def __init__(self, list_model: ListModel.ListModelLike, selection: Selection.Ind
self.__mouse_pressed_for_dragging = False
self.__mouse_position: Geometry.IntPoint | None = None
self.__mouse_dragging = False
self.__dropping = True
self.__drop_before_index: int | None = None
self.__drop_index: int | None = None
# initialize
with self.batch_update():
Expand Down Expand Up @@ -462,7 +461,7 @@ def mouse_position_changed(self, x: int, y: int, modifiers: UserInterface.Keyboa
if base_container:
base_container._bypass_request_focus()
selected_items = [self.__list_model.items[index] for index in self.__selection.indexes]
selected_items = selected_items if self.__mouse_canvas_item.item in selected_items else [self.__mouse_canvas_item]
selected_items = selected_items if self.__mouse_canvas_item.item in selected_items else [self.__mouse_canvas_item.item]
if self.__delegate.drag_started_event(GridFlowCanvasItemDragStartedEvent(self.__mouse_canvas_item.item, selected_items, Geometry.IntPoint(x=x, y=y), modifiers)):
# once a drag starts, mouse release will not be called; call it here instead
self.__mouse_released(x, y, modifiers, False)
Expand Down Expand Up @@ -611,20 +610,44 @@ def _drop_mime_data(self, mime_data: UserInterface.MimeData, action: str, drop_i
return "ignore"

def drag_enter(self, mime_data: UserInterface.MimeData) -> str:
self.__dropping = True
return "ignore"

def drag_move(self, mime_data: UserInterface.MimeData, x: int, y: int) -> str:
mouse_index = self.__get_mouse_index(x, y)
max_index = len(self.__list_model.items)
drop_index = None
if mouse_index >= 0 and mouse_index < max_index:
drop_index = mouse_index
if not self._can_drop_mime_data(mime_data, "move", drop_index):
drop_index = self._get_drop_index(x, y)
if drop_index is not None:
# the point is a possible drop target if either this canvas item can take the drop itself or the delegate
# can. the delegate is asked here, and not only when the drop arrives, so that it is the one deciding
# where a drop lands and so that the drop is shown while the drag is over it.
if not self._can_drop_mime_data(mime_data, "move", drop_index) and not self.__delegate.can_drop_mime_data(mime_data, "copy", drop_index):
drop_index = None
self.__set_drop_index(drop_index)
return "ignore"

def _get_drop_index(self, x: int, y: int) -> int | None:
"""Return the index a drop at the given point would land on, or None where it would land nowhere.

The index is the item the drop lands on. A subclass which lands a drop between its items instead returns the
index the drop would be inserted at, which runs from zero up to and including the number of items.
"""
mouse_index = self.__get_mouse_index(x, y)
return mouse_index if 0 <= mouse_index < len(self.__list_model.items) else None

def _show_drop_index(self, drop_index: int | None) -> None:
"""Show where a drop would land. The item it would land on is the one which shows it."""
grid_flow_item_canvas_items = self.__grid_flow_item_canvas_items
for index, grid_flow_item_canvas_item in enumerate(grid_flow_item_canvas_items):
grid_flow_item_canvas_item.is_dropping = index == drop_index

def __set_drop_index(self, drop_index: int | None) -> None:
# only one place shows the drop at a time: the one the drop would land on.
if drop_index != self.__drop_index:
self.__drop_index = drop_index
return "ignore"
self._show_drop_index(drop_index)

@property
def _drop_index(self) -> int | None:
"""The index a drop would land on while a drag is over this canvas item, or None."""
return self.__drop_index

def __get_mouse_index(self, x: int, y: int) -> int:
canvas_item = self.__grid_flow_item_at_point(Geometry.IntPoint(x=x, y=y))
Expand All @@ -637,14 +660,12 @@ def __get_mouse_index(self, x: int, y: int) -> int:
return mouse_index

def drag_leave(self) -> str:
self.__dropping = False
self.__drop_index = None
self.__set_drop_index(None)
return "ignore"

def drop(self, mime_data: UserInterface.MimeData, x: int, y: int) -> str:
drop_index = self.__drop_index
self.__dropping = False
self.__drop_index = None
self.__set_drop_index(None)
self.update()
if drop_index is not None:
internal_drop_result = self._drop_mime_data(mime_data, "move", drop_index)
Expand Down
Loading
Loading