Rework clipboard handling to carry embedded resources across documents (#4296)

* Move clipboard code to its dedicated handler

* Adapt clipboard format to work with resources

* Restore visibility, lock, and collapse state when pasting layers

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Timon
2026-07-01 09:31:55 +00:00
committed by GitHub
co-authored by Keavon Chambers
parent 7c870d9426
commit 8a38af5be7
19 changed files with 829 additions and 638 deletions
@@ -112,7 +112,7 @@ pub enum DocumentMessage {
resize: Key,
resize_opposite: Key,
},
PasteImage {
InsertImage {
name: Option<String>,
image: Image<Color>,
mouse: Option<(f64, f64)>,
@@ -121,7 +121,7 @@ pub enum DocumentMessage {
/// can wrap it without a content Transform node. When false, place at the cursor or viewport center.
place_at_origin: bool,
},
PasteSvg {
InsertSvg {
name: Option<String>,
svg: String,
mouse: Option<(f64, f64)>,
@@ -753,7 +753,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
let resize_opposite = ipp.keyboard.key(resize_opposite);
self.nudge_selected_layers(delta_x, delta_y, resize, resize_opposite, responses);
}
DocumentMessage::PasteImage {
DocumentMessage::InsertImage {
name,
image,
mouse,
@@ -817,7 +817,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
// Force chosen tool to be Select Tool after importing image.
responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select });
}
DocumentMessage::PasteSvg {
DocumentMessage::InsertSvg {
name,
svg,
mouse,
@@ -97,8 +97,8 @@ pub enum NodeGraphMessage {
SetChainPosition {
node_id: NodeId,
},
PasteNodes {
serialized_nodes: String,
InsertNodes {
nodes: Vec<(NodeId, NodeTemplate)>,
},
PointerDown {
shift_click: bool,
@@ -1,7 +1,7 @@
use super::node_properties;
use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendNode};
use crate::consts::GRID_SIZE;
use crate::messages::clipboard::utility_types::ClipboardContent;
use crate::messages::clipboard::utility_types::ClipboardItem;
use crate::messages::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual};
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::document_message_handler::navigation_controls;
@@ -12,9 +12,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, NodeGraphErrorDiagnostic};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
use crate::messages::portfolio::document::utility_types::network_interface::{
self, FlowType, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing,
};
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, NodeTypePersistentMetadata, OutputConnector, Previewing};
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry};
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
use crate::messages::prelude::*;
@@ -248,12 +246,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
let new_ids = &all_selected_nodes.iter().enumerate().map(|(new, old)| (*old, NodeId(new as u64))).collect();
let copied_nodes = network_interface.copy_nodes(new_ids, selection_network_path).collect::<Vec<_>>();
let Ok(data) = serde_json::to_string(&copied_nodes) else {
log::error!("Failed to serialize nodes for clipboard");
return;
};
responses.add(ClipboardMessage::Write {
content: ClipboardContent::Nodes(data),
responses.add(ClipboardMessage::WriteItems {
items: vec![ClipboardItem::Nodes(copied_nodes)],
});
}
NodeGraphMessage::CreateNodeInLayerNoTransaction { node_type, layer } => {
@@ -762,26 +756,18 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
NodeGraphMessage::SetChainPosition { node_id } => {
network_interface.set_chain_position(&node_id, selection_network_path);
}
NodeGraphMessage::PasteNodes { serialized_nodes } => {
let data = match serde_json::from_str::<Vec<(NodeId, NodeTemplate)>>(&serialized_nodes) {
Ok(d) => d,
Err(e) => {
warn!("Invalid node data {e:?}");
return;
}
};
if data.is_empty() {
NodeGraphMessage::InsertNodes { nodes } => {
if nodes.is_empty() {
return;
}
responses.add(DocumentMessage::AddTransaction);
let new_ids: HashMap<_, _> = data.iter().map(|(id, _)| (*id, NodeId::new())).collect();
let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() });
let nodes: Vec<_> = new_ids.values().copied().collect();
responses.add(NodeGraphMessage::AddNodes {
nodes: data,
new_ids: new_ids.clone(),
});
responses.add(NodeGraphMessage::SelectedNodesSet { nodes })
}
NodeGraphMessage::PointerDown {
@@ -1,23 +0,0 @@
use super::network_interface::NodeTemplate;
use graph_craft::document::NodeId;
#[repr(u8)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
pub enum Clipboard {
Internal,
Device,
_InternalClipboardCount, // Keep this as the last entry of **internal** clipboards since it is used for counting the number of enum variants
}
pub const INTERNAL_CLIPBOARD_COUNT: u8 = Clipboard::_InternalClipboardCount as u8;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CopyBufferEntry {
pub nodes: Vec<(NodeId, NodeTemplate)>,
pub selected: bool,
pub visible: bool,
pub locked: bool,
pub collapsed: bool,
}
@@ -1,4 +1,3 @@
pub mod clipboards;
pub mod document_metadata;
pub mod error;
pub mod misc;
@@ -6832,17 +6832,22 @@ pub enum TransactionStatus {
fn collect_network_resources(network: &NodeNetwork, out: &mut HashSet<ResourceId>) {
for node in network.nodes.values() {
for input in &node.inputs {
if let NodeInput::Value { tagged_value, .. } = input
&& let TaggedValue::Resource(id) = &**tagged_value
{
out.insert(*id);
}
}
if let DocumentNodeImplementation::Network(nested) = &node.implementation {
collect_network_resources(nested, out);
collect_node_resources(node, out);
}
}
/// Collects resource IDs referenced by a node and its nested networks.
pub fn collect_node_resources(node: &DocumentNode, out: &mut HashSet<ResourceId>) {
for input in &node.inputs {
if let NodeInput::Value { tagged_value, .. } = input
&& let TaggedValue::Resource(id) = &**tagged_value
{
out.insert(*id);
}
}
if let DocumentNodeImplementation::Network(nested) = &node.implementation {
collect_network_resources(nested, out);
}
}
#[cfg(test)]
@@ -6857,18 +6862,19 @@ mod network_interface_tests {
.await;
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![rectangle] }).await;
let frontend_messages = editor.handle_message(NodeGraphMessage::Copy).await;
let serialized_nodes = frontend_messages
let clipboard = frontend_messages
.into_iter()
.find_map(|msg| match msg {
FrontendMessage::TriggerClipboardWrite { content } => Some(content),
_ => None,
})
.expect("copy message should be dispatched")
.strip_prefix("graphite/nodes: ")
.expect("should start with magic string")
.to_string();
println!("Serialized: {serialized_nodes}");
editor.handle_message(NodeGraphMessage::PasteNodes { serialized_nodes }).await;
.expect("copy message should be dispatched");
println!("Clipboard: {clipboard}");
editor
.handle_message(ClipboardMessage::ReadClipboard {
content: ClipboardContentRaw::Text(clipboard),
})
.await;
let nodes = &mut editor.active_document_mut().network_interface.network_mut(&[]).unwrap().nodes;
let orignal = nodes.remove(&rectangle).expect("original node should exist");
assert!(
@@ -2,7 +2,6 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::persistent_state::PersistentStateMessage;
use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType};
use crate::messages::frontend::utility_types::{ExportBounds, FileType, PersistedState};
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::prelude::*;
use graphene_std::Color;
use graphene_std::raster::Image;
@@ -39,12 +38,6 @@ pub enum PortfolioMessage {
CloseDocumentWithConfirmation {
document_id: DocumentId,
},
Copy {
clipboard: Clipboard,
},
Cut {
clipboard: Clipboard,
},
DeleteDocument {
document_id: DocumentId,
},
@@ -113,31 +106,19 @@ pub enum PortfolioMessage {
name: Option<String>,
svg: String,
},
PasteSerializedData {
data: String,
},
PasteSerializedVector {
data: String,
},
PasteImage {
InsertImage {
name: Option<String>,
image: Image<Color>,
mouse: Option<(f64, f64)>,
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
},
PasteSvg {
InsertSvg {
name: Option<String>,
svg: String,
mouse: Option<(f64, f64)>,
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
},
// TODO: Unused except by tests, remove?
PasteIntoFolder {
clipboard: Clipboard,
parent: LayerNodeIdentifier,
insert_index: usize,
},
CenterPastedLayers {
CenterLayers {
layers: Vec<LayerNodeIdentifier>,
},
PrevDocument,
@@ -1,11 +1,9 @@
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::document::utility_types::network_interface;
use super::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler};
use super::utility_types::{PanelLayoutSubdivision, PanelType, WorkspacePanelLayout};
use crate::application::{Editor, generate_uuid};
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
use crate::consts::{DEFAULT_DOCUMENT_NAME, FILE_EXTENSION};
use crate::messages::animation::TimingInformation;
use crate::messages::clipboard::utility_types::ClipboardContent;
use crate::messages::dialog::simple_dialogs;
use crate::messages::frontend::utility_types::{DocumentInfo, PersistedState};
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
@@ -13,15 +11,12 @@ use crate::messages::input_mapper::utility_types::macros::{action_shortcut, acti
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::DocumentMessageContext;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::{self, resolve_network_node_type};
use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::document::node_graph::document_node_definitions;
use crate::messages::portfolio::document::utility_types::network_interface::OutputConnector;
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
use crate::messages::portfolio::document_migration::*;
use crate::messages::portfolio::utility_types::FileContent;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::utility_types::{HintData, ToolType};
use crate::messages::viewport::ToPhysical;
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
@@ -31,9 +26,6 @@ use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::raster_types::Image;
use graphene_std::renderer::Quad;
use graphene_std::subpath::BezierHandles;
use graphene_std::vector::misc::HandleId;
use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType};
use std::path::PathBuf;
use std::sync::Arc;
use std::vec;
@@ -68,7 +60,6 @@ pub struct PortfolioMessageHandler {
pub(crate) active_document_id: Option<DocumentId>,
persistent_state: PersistentStateMessageHandler,
pub fonts: FontsMessageHandler,
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
pub executor: NodeGraphExecutor,
pub selection_mode: SelectionMode,
pub reset_node_definitions_on_open: bool,
@@ -281,86 +272,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(PortfolioMessage::SelectDocument { document_id });
}
}
PortfolioMessage::Copy { clipboard } => {
if context.current_tool == &ToolType::Path {
responses.add(PathToolMessage::Copy { clipboard });
return;
}
// We can't use `self.active_document()` because it counts as an immutable borrow of the entirety of `self`
let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) else {
return;
};
if active_document.graph_view_overlay_open() {
responses.add(NodeGraphMessage::Copy);
return;
}
let mut copy_val = |buffer: &mut Vec<CopyBufferEntry>| {
let mut ordered_last_elements = active_document.network_interface.shallowest_unique_layers(&[]).collect::<Vec<_>>();
ordered_last_elements.sort_by_key(|layer| {
let Some(parent) = layer.parent(active_document.metadata()) else { return usize::MAX };
DocumentMessageHandler::get_calculated_insert_index(active_document.metadata(), &SelectedNodes(vec![layer.to_node()]), parent)
});
for layer in ordered_last_elements.into_iter() {
let layer_node_id = layer.to_node();
let mut copy_ids = HashMap::new();
copy_ids.insert(layer_node_id, NodeId(0));
active_document
.network_interface
.upstream_flow_back_from_nodes(vec![layer_node_id], &[], network_interface::FlowType::LayerChildrenUpstreamFlow)
.enumerate()
.for_each(|(index, node_id)| {
copy_ids.insert(node_id, NodeId((index + 1) as u64));
});
buffer.push(CopyBufferEntry {
nodes: active_document.network_interface.copy_nodes(&copy_ids, &[]).collect(),
selected: active_document.network_interface.selected_nodes().selected_layers_contains(layer, active_document.metadata()),
visible: active_document.network_interface.selected_nodes().layer_visible(layer, &active_document.network_interface),
locked: active_document.network_interface.selected_nodes().layer_locked(layer, &active_document.network_interface),
collapsed: false,
});
}
};
if clipboard == Clipboard::Device {
let mut buffer = Vec::new();
copy_val(&mut buffer);
let Ok(data) = serde_json::to_string(&buffer) else {
log::error!("Failed to serialize nodes for clipboard");
return;
};
responses.add(ClipboardMessage::Write {
content: ClipboardContent::Layer(data),
});
} else {
let copy_buffer = &mut self.copy_buffer;
copy_buffer[clipboard as usize].clear();
copy_val(&mut copy_buffer[clipboard as usize]);
}
}
PortfolioMessage::Cut { clipboard } => {
if context.current_tool == &ToolType::Path {
responses.add(PathToolMessage::Cut { clipboard });
return;
}
if let Some(active_document) = self.active_document()
&& active_document.graph_view_overlay_open()
{
responses.add(NodeGraphMessage::Cut);
return;
}
responses.add(PortfolioMessage::Copy { clipboard });
responses.add(DocumentMessage::DeleteSelectedLayers);
}
PortfolioMessage::DeleteDocument { document_id } => {
let document_index = self.document_index(document_id);
self.documents.remove(&document_id);
@@ -759,7 +670,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
});
}
FileContent::Svg(svg) => {
responses.add(PortfolioMessage::PasteSvg {
responses.add(PortfolioMessage::InsertSvg {
name,
svg,
mouse: None,
@@ -767,7 +678,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
});
}
FileContent::Image(image) => {
responses.add(PortfolioMessage::PasteImage {
responses.add(PortfolioMessage::InsertImage {
name,
image,
mouse: None,
@@ -959,7 +870,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
name: name.clone().unwrap_or_default(),
});
responses.add(DocumentMessage::PasteImage {
responses.add(DocumentMessage::InsertImage {
name,
image,
mouse: None,
@@ -1014,7 +925,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
})
});
responses.add(DocumentMessage::PasteSvg {
responses.add(DocumentMessage::InsertSvg {
name,
svg,
mouse: None,
@@ -1036,153 +947,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()],
});
}
// TODO: Unused except by tests, remove?
PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => {
let mut all_new_ids = Vec::new();
let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>, all_new_ids: &mut Vec<NodeId>| {
if self.active_document().is_some() {
trace!("Pasting into folder {parent:?} as index: {insert_index}");
let nodes = entry.clone().nodes;
let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]);
all_new_ids.extend(new_ids.values().cloned());
responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() });
responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index });
}
};
responses.add(DocumentMessage::DeselectAllLayers);
for entry in self.copy_buffer[clipboard as usize].iter().rev() {
paste(entry, responses, &mut all_new_ids)
}
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids });
}
PortfolioMessage::PasteSerializedData { data } => {
if let Some(document) = self.active_document() {
let mut all_new_ids = Vec::new();
if let Ok(data) = serde_json::from_str::<Vec<CopyBufferEntry>>(&data) {
let parent = document.new_layer_parent(false);
let mut layers = Vec::new();
let mut added_nodes = false;
for entry in data.into_iter().rev() {
if !added_nodes {
responses.add(DocumentMessage::DeselectAllLayers);
responses.add(DocumentMessage::AddTransaction);
added_nodes = true;
}
let new_ids: HashMap<_, _> = entry.nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]);
all_new_ids.extend(new_ids.values().cloned());
responses.add(NodeGraphMessage::AddNodes { nodes: entry.nodes, new_ids });
responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index: 0 });
layers.push(layer);
}
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids });
responses.add(DeferMessage::AfterGraphRun {
messages: vec![PortfolioMessage::CenterPastedLayers { layers }.into()],
});
}
}
}
// Custom paste implementation for Path tool
PortfolioMessage::PasteSerializedVector { data } => {
// If using Path tool then send the operation to Path tool
if *current_tool == ToolType::Path {
responses.add(PathToolMessage::Paste { data });
return;
}
// If not using Path tool, create new layers and add paths into those
if let Some(document) = self.active_document() {
let Ok(data) = serde_json::from_str::<Vec<(LayerNodeIdentifier, Vector, DAffine2)>>(&data) else {
return;
};
let mut layers = Vec::new();
for (_, new_vector, transform) in data {
let Some(node_type) = resolve_network_node_type("Path") else {
error!("Path node does not exist");
continue;
};
let nodes = vec![(NodeId(0), node_type.default_node_template())];
let parent = document.new_layer_parent(false);
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
layers.push(layer);
// Adding the transform back into the layer
responses.add(GraphOperationMessage::TransformSet {
layer,
transform,
transform_in: TransformIn::Local,
skip_rerender: false,
});
// Add default fill and stroke to the layer
let fill = graphene_std::vector::style::Fill::solid(Color::WHITE);
responses.add(GraphOperationMessage::FillSet { layer, fill });
let stroke = graphene_std::vector::style::Stroke::new(Some(Color::BLACK), DEFAULT_STROKE_WIDTH);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
// Create new point ids and add those into the existing Vector path
let mut points_map = HashMap::new();
for (point, position) in new_vector.point_domain.iter() {
let new_point_id = PointId::generate();
points_map.insert(point, new_point_id);
let modification_type = VectorModificationType::InsertPoint { id: new_point_id, position };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
// Create new segment ids and add the segments into the existing Vector path
let mut segments_map = HashMap::new();
for (segment_id, bezier, start, end) in new_vector.segment_bezier_iter() {
let new_segment_id = SegmentId::generate();
segments_map.insert(segment_id, new_segment_id);
let handles = match bezier.handles {
BezierHandles::Linear => [None, None],
BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None],
BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)],
};
let points = [points_map[&start], points_map[&end]];
let modification_type = VectorModificationType::InsertSegment { id: new_segment_id, points, handles };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
// Set G1 continuity
for handles in new_vector.colinear_manipulators {
let to_new_handle = |handle: HandleId| -> HandleId {
HandleId {
ty: handle.ty,
segment: segments_map[&handle.segment],
}
};
let new_handles = [to_new_handle(handles[0]), to_new_handle(handles[1])];
let modification_type = VectorModificationType::SetG1Continuous { handles: new_handles, enabled: true };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
}
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(Message::Defer(DeferMessage::AfterGraphRun {
messages: vec![PortfolioMessage::CenterPastedLayers { layers }.into()],
}));
}
}
PortfolioMessage::CenterPastedLayers { layers } => {
PortfolioMessage::CenterLayers { layers } => {
if let Some(document) = self.active_document_mut() {
let viewport_bounds_quad_pixels = Quad::from_box([DVec2::ZERO, viewport.size().into_dvec2()]); // In viewport pixel coordinates
let viewport_center_pixels = viewport_bounds_quad_pixels.center(); // In viewport pixel coordinates
@@ -1287,7 +1052,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
PortfolioMessage::PasteImage {
PortfolioMessage::InsertImage {
name,
image,
mouse,
@@ -1296,7 +1061,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
if self.document_ids.is_empty() {
responses.add(PortfolioMessage::OpenImage { name, image });
} else {
responses.add(DocumentMessage::PasteImage {
responses.add(DocumentMessage::InsertImage {
name,
image,
mouse,
@@ -1305,7 +1070,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
});
}
}
PortfolioMessage::PasteSvg {
PortfolioMessage::InsertSvg {
name,
svg,
mouse,
@@ -1314,7 +1079,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
if self.document_ids.is_empty() {
responses.add(PortfolioMessage::OpenSvg { name, svg });
} else {
responses.add(DocumentMessage::PasteSvg {
responses.add(DocumentMessage::InsertSvg {
name,
svg,
mouse,
@@ -1816,14 +1581,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
PrevDocument,
Import,
));
// Extend with actions that must have a selected layer
if document.network_interface.selected_nodes().selected_layers(document.metadata()).next().is_some() {
common.extend(actions!(PortfolioMessageDiscriminant;
Copy,
Cut,
));
}
}
// Extend with actions that are disabled when focusing the document