-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Clipboard: Cross-application copy-paste support #4499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
6d41257
2581e7d
b897e65
b660117
eedb39e
32fbb64
a3dabc0
c06bfac
e96a66e
04dcb84
9ef419f
491a7ac
3f07cbc
6b351a6
be7c143
c54b027
f85130e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -79,21 +79,18 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard | |||||||||||||
| responses.add(ClipboardMessage::CopyLayers); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| ClipboardMessage::Write { content } => { | ||||||||||||||
| let text = match content { | ||||||||||||||
| ClipboardContent::Svg(_) => { | ||||||||||||||
| log::error!("SVG copying is not yet supported"); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
| ClipboardContent::Image { .. } => { | ||||||||||||||
| log::error!("Image copying is not yet supported"); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
| ClipboardContent::Graphite(graphite) => format!("{CLIPBOARD_PREFIX}{graphite}"), | ||||||||||||||
| ClipboardContent::Text(text) => text, | ||||||||||||||
| }; | ||||||||||||||
| responses.add(FrontendMessage::TriggerClipboardWrite { content: text }); | ||||||||||||||
| } | ||||||||||||||
| ClipboardMessage::Write { content } => match content { | ||||||||||||||
| ClipboardContent::Image { .. } => { | ||||||||||||||
| log::error!("Image copying is not yet supported"); | ||||||||||||||
| } | ||||||||||||||
| ClipboardContent::Graphite(graphite) => { | ||||||||||||||
| let graphite_json = format!("{CLIPBOARD_PREFIX}{graphite}"); | ||||||||||||||
| responses.add(PortfolioMessage::RequestSvgTextCopy { graphite_json }); | ||||||||||||||
| } | ||||||||||||||
| ClipboardContent::Text(graphite_json) => { | ||||||||||||||
| responses.add(FrontendMessage::TriggerClipboardSvgAndJsonWrite { svg_string: None, graphite_json }); | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Copying a text selection breaks on Chrome/Edge: the frontend receives Prompt for AI agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+90
to
+92
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Prompt for AI agents
Suggested change
|
||||||||||||||
| }, | ||||||||||||||
|
|
||||||||||||||
| ClipboardMessage::CopyLayers => { | ||||||||||||||
| if current_tool == &ToolType::Path { | ||||||||||||||
|
|
@@ -526,7 +523,7 @@ mod test { | |||||||||||||
| .await | ||||||||||||||
| .into_iter() | ||||||||||||||
| .find_map(|message| match message { | ||||||||||||||
| FrontendMessage::TriggerClipboardWrite { content } => Some(content), | ||||||||||||||
| FrontendMessage::TriggerClipboardSvgAndJsonWrite { graphite_json, .. } => Some(graphite_json), | ||||||||||||||
| _ => None, | ||||||||||||||
| }) | ||||||||||||||
| .expect("copying layers should write a payload to the clipboard") | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -70,6 +70,7 @@ pub enum GraphRuntimeRequest { | |||||||||||||||||||||||||||||||||||||
| GraphUpdate(GraphUpdate), | ||||||||||||||||||||||||||||||||||||||
| ExecutionRequest(ExecutionRequest), | ||||||||||||||||||||||||||||||||||||||
| EditorPreferencesUpdate(EditorPreferences), | ||||||||||||||||||||||||||||||||||||||
| CopySvgTextClipboard(String, Vec<NodeId>), | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| #[derive(Debug, serde::Serialize, serde::Deserialize)] | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -108,6 +109,10 @@ impl InternalNodeGraphUpdateSender { | |||||||||||||||||||||||||||||||||||||
| fn send_eyedropper_preview(&self, raster: Raster<CPU>) { | ||||||||||||||||||||||||||||||||||||||
| self.0.send(NodeGraphUpdate::EyedropperPreview(raster)).expect("Failed to send response") | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| fn send_svg_text_clipboard(&self, svg_string: String, graphite_json: String) { | ||||||||||||||||||||||||||||||||||||||
| self.0.send(NodeGraphUpdate::SvgTextCopyClipboard { svg_string, graphite_json }).expect("Failed to send response") | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| impl NodeGraphUpdateSender for InternalNodeGraphUpdateSender { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -162,6 +167,7 @@ impl NodeRuntime { | |||||||||||||||||||||||||||||||||||||
| let mut graph = None; | ||||||||||||||||||||||||||||||||||||||
| let mut eyedropper = None; | ||||||||||||||||||||||||||||||||||||||
| let mut execution = None; | ||||||||||||||||||||||||||||||||||||||
| let mut svg_clipboard = None; | ||||||||||||||||||||||||||||||||||||||
| for request in self.receiver.try_iter() { | ||||||||||||||||||||||||||||||||||||||
| match request { | ||||||||||||||||||||||||||||||||||||||
| GraphRuntimeRequest::GraphUpdate(_) => graph = Some(request), | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -182,6 +188,7 @@ impl NodeRuntime { | |||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| GraphRuntimeRequest::EditorPreferencesUpdate(_) => preferences = Some(request), | ||||||||||||||||||||||||||||||||||||||
| GraphRuntimeRequest::CopySvgTextClipboard(..) => svg_clipboard = Some(request), | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -193,7 +200,7 @@ impl NodeRuntime { | |||||||||||||||||||||||||||||||||||||
| eyedropper.render_config.pointer = execution.render_config.pointer; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let requests = [preferences, graph, eyedropper, execution].into_iter().flatten(); | ||||||||||||||||||||||||||||||||||||||
| let requests = [preferences, graph, eyedropper, svg_clipboard, execution].into_iter().flatten(); | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When a copy request and a graph evaluation are coalesced in one runtime pass, this line introspects monitor nodes before the pending execution refreshes them, so the clipboard can contain stale or empty SVG. Ensure the copy path reads output from the relevant completed execution, and avoid relying only on request reordering because the execution branch currently returns before later requests. Prompt for AI agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is technically true however it probably isn't very relevant since the user would be copying an SVG that is not yet rendered into the viewport. You could fix this by having a result variable instead of an early return:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Moving Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| for request in requests { | ||||||||||||||||||||||||||||||||||||||
| match request { | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the control flow would be much easier if these were made into functions: match request {
GraphRuntimeRequest::EditorPreferencesUpdate(preferences) => self.editor_preferences_update(preferences),
GraphRuntimeRequest::GraphUpdate(graph_update) => self.graph_update(graph_update),
...
} |
||||||||||||||||||||||||||||||||||||||
|
|
@@ -340,6 +347,28 @@ impl NodeRuntime { | |||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| return texture; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| GraphRuntimeRequest::CopySvgTextClipboard(text_string_clipboard, selected_node_ids) => { | ||||||||||||||||||||||||||||||||||||||
| let combined_graphics = self.collect_graphics(&selected_node_ids); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if combined_graphics.is_empty() { | ||||||||||||||||||||||||||||||||||||||
| self.sender.send_svg_text_clipboard(String::new(), text_string_clipboard); | ||||||||||||||||||||||||||||||||||||||
| return None; | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let bounds = graphene_std::renderer::graphic_list_bounding_box(&combined_graphics, DAffine2::IDENTITY); | ||||||||||||||||||||||||||||||||||||||
| let final_bounds = match bounds { | ||||||||||||||||||||||||||||||||||||||
| RenderBoundingBox::Rectangle(bounds) if (bounds[1] - bounds[0]) != DVec2::ZERO => bounds, | ||||||||||||||||||||||||||||||||||||||
| _ => [DVec2::ZERO, DVec2::ONE], | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let footprint = Footprint::from_bounds(final_bounds, RenderQuality::Full); | ||||||||||||||||||||||||||||||||||||||
| let render_params = RenderParams { footprint, ..Default::default() }; | ||||||||||||||||||||||||||||||||||||||
| let mut render = SvgRender::new(); | ||||||||||||||||||||||||||||||||||||||
| combined_graphics.render_svg(&mut render, &render_params); | ||||||||||||||||||||||||||||||||||||||
| render.format_svg(final_bounds[0], final_bounds[1]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self.sender.send_svg_text_clipboard(render.svg.to_svg_string(), text_string_clipboard); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| None | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -392,11 +421,7 @@ impl NodeRuntime { | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| for monitor_node_path in &self.monitor_nodes { | ||||||||||||||||||||||||||||||||||||||
| // Skip the inspect monitor node | ||||||||||||||||||||||||||||||||||||||
| if self | ||||||||||||||||||||||||||||||||||||||
| .inspect_state | ||||||||||||||||||||||||||||||||||||||
| .as_ref() | ||||||||||||||||||||||||||||||||||||||
| .is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node)) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| if self.is_insepect_monitor_node(monitor_node_path) { | ||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -498,11 +523,7 @@ impl NodeRuntime { | |||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| let bounds = expand_to_thumbnail_aspect(raw_bounds); | ||||||||||||||||||||||||||||||||||||||
| let new_thumbnail_svg = { | ||||||||||||||||||||||||||||||||||||||
| let footprint = Footprint { | ||||||||||||||||||||||||||||||||||||||
| transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)), | ||||||||||||||||||||||||||||||||||||||
| resolution: UVec2::new((bounds[1].x - bounds[0].x).abs() as u32, (bounds[1].y - bounds[0].y).abs() as u32), | ||||||||||||||||||||||||||||||||||||||
| quality: RenderQuality::Full, | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| let footprint = Footprint::from_bounds(bounds, RenderQuality::Full); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Render the thumbnail from a `Graphic` into an SVG string | ||||||||||||||||||||||||||||||||||||||
| let render_params = RenderParams { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -529,6 +550,36 @@ impl NodeRuntime { | |||||||||||||||||||||||||||||||||||||
| *old_thumbnail_svg = new_thumbnail_svg; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| fn collect_graphics(&self, selected_node_ids: &Vec<NodeId>) -> List<Graphic> { | ||||||||||||||||||||||||||||||||||||||
| let mut combined_graphics = List::<Graphic>::new(); | ||||||||||||||||||||||||||||||||||||||
| for monitor_node_path in &self.monitor_nodes { | ||||||||||||||||||||||||||||||||||||||
| // Skip inspect monitor node if active | ||||||||||||||||||||||||||||||||||||||
| if self.is_insepect_monitor_node(monitor_node_path) { | ||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let Some(parent_network_node_id) = monitor_node_path.len().checked_sub(2).and_then(|index| monitor_node_path.get(index)).copied() else { | ||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if selected_node_ids.contains(&parent_network_node_id) { | ||||||||||||||||||||||||||||||||||||||
| // Introspect using the full monitor node path | ||||||||||||||||||||||||||||||||||||||
| if let Ok(introspected_data) = self.executor.introspect(monitor_node_path) | ||||||||||||||||||||||||||||||||||||||
| && let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>() | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| combined_graphics.extend(io.output.clone()); | ||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||
| warn!("No graphic type is matched while extracting svg"); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+568
to
+574
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: When a selected layer's monitor output is Prompt for AI agents
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't happen as it will always be made into a group. |
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| combined_graphics | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| fn is_insepect_monitor_node(&self, monitor_node_path: &Vec<NodeId>) -> bool { | ||||||||||||||||||||||||||||||||||||||
| self.inspect_state.as_ref().is_some_and(|state| monitor_node_path.last().copied() == Some(state.monitor_node)) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /// Returns the union of the artboards' clipping rectangles, used as the thumbnail bounds for an artboard layer so the | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.