-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
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 | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -52,6 +52,7 @@ pub enum NodeGraphUpdate { | |||||||||||||||||||
| CompilationResponse(CompilationResponse), | ||||||||||||||||||||
| EyedropperPreview(Raster<CPU>), | ||||||||||||||||||||
| NodeGraphUpdateMessage(NodeGraphUpdateMessage), | ||||||||||||||||||||
| SvgTextCopyClipboard(String, String), | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[derive(Debug, Default)] | ||||||||||||||||||||
|
|
@@ -466,6 +467,9 @@ impl NodeGraphExecutor { | |||||||||||||||||||
| responses.add(EyedropperToolMessage::PreviewImage { data, width, height }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| NodeGraphUpdate::NodeGraphUpdateMessage(_) => {} | ||||||||||||||||||||
| NodeGraphUpdate::SvgTextCopyClipboard(svg_string, graphite_json) => { | ||||||||||||||||||||
| responses.add(FrontendMessage::TriggerClipboardSvgWrite { svg_string, graphite_json }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -812,6 +816,12 @@ impl NodeGraphExecutor { | |||||||||||||||||||
|
|
||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub fn copy_svg_clipboard(&self, graphite_json: String, selected_nodes: Vec<NodeId>) { | ||||||||||||||||||||
| self.runtime_io | ||||||||||||||||||||
| .send(GraphRuntimeRequest::CopySvgTextClipboard(graphite_json, selected_nodes)) | ||||||||||||||||||||
| .expect("Failed to send runtime request"); | ||||||||||||||||||||
|
Comment on lines
+820
to
+823
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
|
||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // TODO: Eventually remove this document upgrade code | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 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, text_string: String) { | ||||||
| self.0.send(NodeGraphUpdate::SvgTextCopyClipboard(svg_string, text_string)).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, execution, svg_clipboard].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. P1: When a Prompt for AI agents
Suggested change
|
||||||
|
|
||||||
| for request in requests { | ||||||
| match request { | ||||||
|
|
@@ -340,6 +347,59 @@ impl NodeRuntime { | |||||
| }); | ||||||
| return texture; | ||||||
| } | ||||||
| GraphRuntimeRequest::CopySvgTextClipboard(text_string_clipboard, selected_node_ids) => { | ||||||
| let mut combined_graphics = List::<Graphic>::new(); | ||||||
|
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 the selection contains an artboard, this branch ignores its Prompt for AI agents |
||||||
|
|
||||||
| for monitor_node_path in &self.monitor_nodes { | ||||||
| // Skip inspect monitor node if active | ||||||
| if self.inspect_state.as_ref().is_some_and(|state| monitor_node_path.last().copied() == Some(state.monitor_node)) { | ||||||
| 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) { | ||||||
| if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>() { | ||||||
| combined_graphics.extend(io.output.clone()); | ||||||
| } else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Item<Graphic>>>() { | ||||||
| combined_graphics.push(io.output.clone()); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if combined_graphics.is_empty() { | ||||||
| self.sender.send_svg_text_clipboard(String::new(), text_string_clipboard); | ||||||
| return None; | ||||||
| } | ||||||
|
|
||||||
| let bounds = graphene_std::renderer::graphic_list_bounding_box(&combined_graphics, DAffine2::IDENTITY); | ||||||
| let raw_bounds = match bounds { | ||||||
| RenderBoundingBox::Rectangle(bounds) if (bounds[1] - bounds[0]) != DVec2::ZERO => bounds, | ||||||
| _ => [DVec2::ZERO, DVec2::ONE], | ||||||
| }; | ||||||
|
|
||||||
| let footprint = Footprint { | ||||||
| transform: DAffine2::from_translation(DVec2::new(raw_bounds[0].x, raw_bounds[0].y)), | ||||||
| resolution: UVec2::new((raw_bounds[1].x - raw_bounds[0].x).abs().ceil() as u32, (raw_bounds[1].y - raw_bounds[0].y).abs().ceil() as u32).max(UVec2::ONE), | ||||||
| quality: RenderQuality::Full, | ||||||
| }; | ||||||
|
|
||||||
| let render_params = RenderParams { | ||||||
| footprint, | ||||||
| thumbnail: false, | ||||||
| ..Default::default() | ||||||
| }; | ||||||
| let mut render = SvgRender::new(); | ||||||
| combined_graphics.render_svg(&mut render, &render_params); | ||||||
| render.format_svg(raw_bounds[0], raw_bounds[1]); | ||||||
|
|
||||||
| self.sender.send_svg_text_clipboard(render.svg.to_svg_string(), text_string_clipboard); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| None | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,13 +23,28 @@ export function createClipboardManager(subscriptions: SubscriptionsRouter, edito | |
| subscriptions.subscribeFrontendMessage("TriggerSelectionWrite", async (data) => { | ||
| insertAtCaret(data.content); | ||
| }); | ||
|
|
||
| subscriptions.subscribeFrontendMessage("TriggerClipboardSvgWrite", (data) => { | ||
| // Adopted from https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem#browser_compatibility | ||
| if (ClipboardItem.supports("image/svg+xml")) { | ||
|
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: On browsers where the Prompt for AI agents |
||
| navigator.clipboard?.write?.([ | ||
| new ClipboardItem({ | ||
| "image/svg+xml": data.svg_string, | ||
| "text/plain": data.graphite_json, | ||
| }), | ||
| ]); | ||
| } else { | ||
| navigator.clipboard?.writeText?.(data.graphite_json); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| export function destroyClipboardManager() { | ||
| const subscriptions = subscriptionsRouter; | ||
| if (!subscriptions) return; | ||
|
|
||
| subscriptions.unsubscribeFrontendMessage("TriggerClipboardWrite"); | ||
| subscriptions.unsubscribeFrontendMessage("TriggerClipboardSvgWrite"); | ||
| subscriptions.unsubscribeFrontendMessage("TriggerSelectionRead"); | ||
| subscriptions.unsubscribeFrontendMessage("TriggerSelectionWrite"); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: This changes layer copy from a synchronous
TriggerClipboardWriteinto a runtime request whose generated frontend message is discarded byEditorTestUtils::handle_message. As a result, the existing clipboard copy tests fail at their.expect; propagate the asynchronous clipboard response through the test/runtime API or preserve a directly observable write result.Prompt for AI agents