Source code for zooui.objects.scene.sceneutils.clipboard
## ZooUI - Zooming User Interface
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License
## as published by the Free Software Foundation; either version 3
## of the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, see <https://www.gnu.org/licenses/>.
"""SceneClipboardManager - Clipboard functionality for Scene class.
This class manages copy/paste operations for SVG objects within a scene.
It was extracted from the Scene class to improve modularity and maintainability.
"""
import logging
from typing import Any
from zooui.objects.mediaobjects.svgmediaobject import SVGMediaObject
[docs]
class SceneClipboardManager:
"""Manager for scene clipboard functionality.
This class handles copy and paste operations for SVG objects within a scene.
It maintains an internal clipboard and provides methods for copying selected
objects and pasting them with optional offset.
Attributes:
_scene: Reference to the parent Scene object
_logger: Logger instance for clipboard operations
_clipboard: List of serialized object data for clipboard storage
"""
def __init__(self, scene):
"""Initialize SceneClipboardManager.
Args:
scene: Parent Scene object
"""
self._scene = scene
self._logger = logging.getLogger(__name__)
self._clipboard: list[dict[str, Any]] = []
[docs]
def copy_selection(self) -> None:
"""Copy selected SVG objects to internal clipboard.
Only SVGMediaObjects are supported for copy/paste.
Deselects copied objects after copying.
"""
if not hasattr(self._scene, "selection") or not self._scene.selection:
return
self._clipboard.clear()
# Handle both single object and list selection
objects = self._scene.selection if isinstance(self._scene.selection, list) else [self._scene.selection]
svg_count = 0
for obj in objects:
# Only copy SVGMediaObjects
if isinstance(obj, SVGMediaObject):
self._clipboard.append(obj.to_dict())
svg_count += 1
else:
# Silent fail for non-SVG objects as requested
self._logger.debug(f"Skipping non-SVG object in copy: {type(obj).__name__}")
# Deselect copied objects as requested
self._scene.selection = None
# Log at debug level for developer visibility
if svg_count > 0:
self._logger.debug(f"Copied {svg_count} SVG object(s) to clipboard")
[docs]
def paste(self, offset_position: tuple[float, float] | None = None) -> list["SVGMediaObject"]:
"""Paste SVG objects from clipboard with fixed offset.
Only SVGMediaObjects are supported for copy/paste.
Selects pasted objects after pasting.
Logs warning for unsupported object types.
Args:
offset_position: Optional offset position for pasted objects
Returns:
List of pasted SVGMediaObject instances
"""
if not self._clipboard:
return []
pasted_objects: list[SVGMediaObject] = []
has_unsupported_types = False
# First pass: collect SVG positions and create objects
svg_objects = []
svg_positions = []
for obj_data in self._clipboard:
try:
obj_type = obj_data["type"]
if obj_type == "SVGMediaObject":
obj = SVGMediaObject.from_dict(obj_data, self._scene)
svg_objects.append(obj)
pos = obj_data["position"]
svg_positions.append((pos[0], pos[1]))
else:
has_unsupported_types = True
self._logger.warning(f"Unsupported object type in clipboard: {obj_type}")
except Exception as e:
self._logger.warning(f"Failed to deserialize object from clipboard: {e}")
continue
# Calculate centroid if we have positions and offset
centroid_x = 0.0
centroid_y = 0.0
if svg_positions and offset_position is not None:
centroid_x = sum(p[0] for p in svg_positions) / len(svg_positions)
centroid_y = sum(p[1] for p in svg_positions) / len(svg_positions)
# Second pass: apply offset and add to scene
for i, obj in enumerate(svg_objects):
try:
if offset_position is not None and svg_positions:
# Apply offset relative to centroid
old_x, old_y = svg_positions[i]
offset_x, offset_y = offset_position
new_x = old_x + (offset_x - centroid_x)
new_y = old_y + (offset_y - centroid_y)
obj.pos = (new_x, new_y)
# Add to scene using public add method
if hasattr(self._scene, "add"):
self._scene.add(obj)
pasted_objects.append(obj)
except Exception as e:
self._logger.warning(f"Failed to paste object: {e}")
continue
# Select pasted objects
if pasted_objects:
self._scene.selection = pasted_objects[0] if len(pasted_objects) == 1 else pasted_objects
# Log warnings if needed
if has_unsupported_types:
self._logger.warning("Clipboard contains unsupported object types (only SVG supported)")
return pasted_objects
[docs]
def clear(self) -> None:
"""Clear the clipboard."""
self._clipboard.clear()
self._logger.debug("Clipboard cleared")
[docs]
def has_content(self) -> bool:
"""Check if clipboard has content.
Returns:
True if clipboard has content, False otherwise
"""
return len(self._clipboard) > 0
[docs]
def get_content_count(self) -> int:
"""Get number of items in clipboard.
Returns:
Number of items in clipboard
"""
return len(self._clipboard)