Skip to content
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

[WIP] Copy-paste patch objects using system clipboard text buffer #2086

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft
68 changes: 68 additions & 0 deletions src/g_editor.c
Expand Up @@ -3,6 +3,7 @@
* WARRANTIES, see the file, "LICENSE.txt," in this distribution. */

#include <stdio.h>
#include <stdlib.h>
#include "m_pd.h"
#include "m_imp.h"
#include "s_stuff.h"
Expand Down Expand Up @@ -37,6 +38,11 @@ struct _instanceeditor

/* positional offset for duplicated items */
#define PASTE_OFFSET 10
#define CLIPBOARD_PATCH_TEXT_START 0
#define CLIPBOARD_PATCH_TEXT_LINE_END 1
#define CLIPBOARD_PATCH_TEXT_END 2
#define CLIPBOARD_PATCH_TEXT_LINE_PARTIAL 3
#define CLIPBOARD_PATCH_TEXT_LINE_END_APPENDIX 4

void glist_readfrombinbuf(t_glist *x, const t_binbuf *b, const char *filename,
int selectem);
Expand Down Expand Up @@ -3718,6 +3724,17 @@ static void canvas_copy(t_canvas *x)
}
}

static void canvas_copy_to_clipboard_as_text(t_canvas *x)
{
t_binbuf *bb = canvas_docopy(x);
if (!bb)
return;
t_atom *atoms = binbuf_getvec(bb);
int num_atoms = binbuf_getnatom(bb);
pdgui_vmess("pdtk_copy_to_clipboard_as_text", "ca", x, num_atoms, atoms);
binbuf_free(bb);
}

static void canvas_clearline(t_canvas *x)
{
if (x->gl_editor->e_selectedline)
Expand Down Expand Up @@ -4123,6 +4140,53 @@ static void canvas_paste(t_canvas *x)
}
}

static t_binbuf *clipboard_patch_bb = NULL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should go into (t_glist*)->gl_privatedata


void canvas_got_clipboard_contents(t_canvas *x, t_floatarg flagf, t_symbol *s) {
int flag = flagf;
switch (flag) {
case CLIPBOARD_PATCH_TEXT_START:
if (clipboard_patch_bb) {
binbuf_free(clipboard_patch_bb);
}
clipboard_patch_bb = binbuf_new();
break;
case CLIPBOARD_PATCH_TEXT_LINE_PARTIAL:
case CLIPBOARD_PATCH_TEXT_LINE_END:
case CLIPBOARD_PATCH_TEXT_LINE_END_APPENDIX:
Copy link
Contributor

@umlaeute umlaeute Sep 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not exactly sure about this.
since a Pd-file can be properly parsed by binbuf_text(), even if it contains message delimiting commas ([foo, bar() and gobj attributes (, f 10), I do not see why need special handling here.
ideally, the canvas_got_clipboard_contents() should get the raw text (as in the clipboard), and that's it (and all the necessary escaping happens in the GUI)

#X msg 78 108 foo \, bar, f 30;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(and sorry for not saying that before you started to implement it)

Copy link
Author

@eeropic eeropic Sep 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh no worries. I'm just having trouble transmitting the comma with pdsend, i.e. something, f32 gets interpreted as 2 messages. So perhaps a special escape sequence for the comma would suffice? So \, gets transmitted as-is, but , gets handled somehow differently (tcl->pd)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

void canvas_got_clipboard_contents(t_canvas *x, t_floatarg flagf, t_symbol *s) {
    const char *debug_str = "#X msg 78 108 foo \\, bar\, f 30;";
    t_binbuf *temp_bb = binbuf_new();
    binbuf_text(temp_bb, debug_str, strlen(debug_str));
    canvas_dopaste(x, temp_bb);
    binbuf_free(temp_bb);
}

Works just fine, but I can't get it transmitted via pdsend properly

Copy link
Contributor

@umlaeute umlaeute Sep 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's because a plain comma in a message is a message separator in Pd lingo.
also, a line in a patch file might not be terminated by a semicolon, in which case it is continued on the next line:

#X obj 79 136 print hello;

is really equivalent to

#X
obj
79
136
print
hello;

i think this is currently not handled correctly (but haven't actually checked)

this is currently not handled correctly, and i get:

bad arguments for message 'got-clipboard-contents' to object 'canvas'
bad arguments for message 'got-clipboard-contents' to object 'canvas'
#: no such object 
ob: no such object 
prin: no such object 
hello: no such object 

iirc, miller's suggestion was:

  1. send a start-message to initiate the binbuf build (aka got-clipboard-contents 0)
  2. send the text string
  3. send an end-message to stop the binbuf build (aka got-clipboard-contents 2)
  4. after that, the text buffer is complete, and can be converted into a binbuf and copied into the patch

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here's a super stupid (and simple idea):
do not attempt to send the data as a symbol (it's going to pollute the symbol table anyhow), but as a list of numbers (UTF-8-encoded).
This is trivial to reconstruct on the receiving side.

Copy link
Author

@eeropic eeropic Sep 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's stupid :-) it's like, stream of pure data... (eh) will try. Would love this to take as few lines as possible and maximize utilizing existing functionality.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great stuff!! Just need to implement the chunking, so it doesn't cut off the data in pdsend

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. It's just a poc.

I'm pretty sure there is some memory overallocation and what not.
On rethinking, I also would prefer "reset" and "submit" instead of "begin" and "end".

if (clipboard_patch_bb) {
t_binbuf *line_bb = binbuf_new();
t_binbuf *temp_bb = binbuf_new();
if (flag == CLIPBOARD_PATCH_TEXT_LINE_END_APPENDIX) {
t_atom a;
SETCOMMA(&a);
binbuf_add(line_bb, 1, &a);
}
binbuf_text(temp_bb, s->s_name, strlen(s->s_name));
if (flag == CLIPBOARD_PATCH_TEXT_LINE_END || flag == CLIPBOARD_PATCH_TEXT_LINE_END_APPENDIX) {
binbuf_addsemi(temp_bb);
}
binbuf_add(line_bb, binbuf_getnatom(temp_bb), binbuf_getvec(temp_bb));
binbuf_add(clipboard_patch_bb, binbuf_getnatom(line_bb), binbuf_getvec(line_bb));
binbuf_clear(line_bb);
binbuf_clear(temp_bb);
binbuf_free(line_bb);
binbuf_free(temp_bb);
}
break;
case CLIPBOARD_PATCH_TEXT_END:
if (clipboard_patch_bb) {
canvas_dopaste(x, clipboard_patch_bb);
binbuf_free(clipboard_patch_bb);
clipboard_patch_bb = NULL;
}
break;
default:
post("Invalid flag received in canvas_got_clipboard_contents.");
break;
}
}

static void canvas_duplicate(t_canvas *x)
{
if (!x->gl_editor)
Expand Down Expand Up @@ -4924,6 +4988,10 @@ void g_editor_setup(void)
gensym("cut"), A_NULL);
class_addmethod(canvas_class, (t_method)canvas_copy,
gensym("copy"), A_NULL);
class_addmethod(canvas_class, (t_method)canvas_copy_to_clipboard_as_text,
gensym("copy-to-clipboard-as-text"), A_NULL);
class_addmethod(canvas_class, (t_method)canvas_got_clipboard_contents,
gensym("got-clipboard-contents"), A_FLOAT, A_DEFSYMBOL, A_NULL);
class_addmethod(canvas_class, (t_method)canvas_paste,
gensym("paste"), A_NULL);
class_addmethod(canvas_class, (t_method)canvas_paste_replace,
Expand Down
5 changes: 5 additions & 0 deletions tcl/pd-gui.tcl
Expand Up @@ -200,6 +200,8 @@ set recentfiles_list {}
set total_recentfiles 5
# modifier for key commands (Ctrl/Control on most platforms, Cmd/Mod1 on MacOSX)
set modifier ""
# on Mac OS X/Aqua, the Alt/Option key is called Option in Tcl
set alt ""
# current state of the Edit Mode menu item
set editmode_button 0

Expand Down Expand Up @@ -294,6 +296,7 @@ proc init_for_platform {} {
switch -- $::windowingsystem {
"x11" {
set ::modifier "Control"
set ::alt "Alt"
option add *PatchWindow*Canvas.background "white" startupFile
# add control to show/hide hidden files in the open panel (load
# the tk_getOpenFile dialog once, otherwise it will not work)
Expand Down Expand Up @@ -334,6 +337,7 @@ proc init_for_platform {} {
# from the commandline incorporates the special mac event handling
package require apple_events
set ::modifier "Mod1"
set ::alt "Option"
if {$::tcl_version < 8.5} {
# old default font for Tk 8.4 on macOS
# since font detection requires 8.5+
Expand Down Expand Up @@ -371,6 +375,7 @@ proc init_for_platform {} {
}
"win32" {
set ::modifier "Control"
set ::alt "Alt"
option add *PatchWindow*Canvas.background "white" startupFile
# fix menu font size on Windows with tk scaling = 1
font create menufont -family Tahoma -size -11
Expand Down
9 changes: 9 additions & 0 deletions tcl/pd_bindings.tcl
Expand Up @@ -120,6 +120,12 @@ proc ::pd_bindings::global_bindings {} {
bind_capslock all $::modifier-Key m {::pd_menucommands::scheduleAction menu_minimize %W}
bind all <$::modifier-quoteleft> {::pd_menucommands::scheduleAction menu_raisenextwindow}
}

# On mac, the copy/paste menu command bindings also execute the commands, results in paste executed twice
# however, if we don't set this binding, the regular copy and paste gets called even with alt pressed
bind_capslock all $::modifier-$::alt-Key c {}
bind_capslock all $::modifier-$::alt-Key v {}

# BackSpace/Delete report the wrong isos (unicode representations) on OSX,
# so we set them to the empty string and let ::pd_bindings::sendkey guess the correct values
bind all <KeyPress-BackSpace> {::pd_bindings::sendkey %W 1 %K "" 1 %k}
Expand All @@ -134,6 +140,9 @@ proc ::pd_bindings::global_bindings {} {
bind_capslock all $::modifier-Key q {::pd_connect::menu_quit}
bind_capslock all $::modifier-Key m {menu_minimize %W}

bind_capslock all $::modifier-$::alt-Key c {menu_send %W copy-to-clipboard-as-text}
bind_capslock all $::modifier-$::alt-Key v {::pdtk_canvas::pdtk_get_clipboard_text $::focused_window}

bind all <$::modifier-Next> {menu_raisenextwindow} ;# PgUp
bind all <$::modifier-Prior> {menu_raisepreviouswindow};# PageDown
# these can conflict with CMD+comma & CMD+period bindings in Tk Cococa
Expand Down
10 changes: 10 additions & 0 deletions tcl/pd_menus.tcl
Expand Up @@ -64,6 +64,8 @@ proc ::pd_menus::configure_for_pdwindow {} {
$menubar.file entryconfigure [_ "Print..."] -state disabled

# Edit menu
$menubar.edit entryconfigure [_ "Copy to clipboard (text)"] -state disabled
$menubar.edit entryconfigure [_ "Paste from clipboard (text)"] -state disabled
$menubar.edit entryconfigure [_ "Paste Replace"] -state disabled
$menubar.edit entryconfigure [_ "Duplicate"] -state disabled
$menubar.edit entryconfigure [_ "Font"] -state normal
Expand Down Expand Up @@ -96,6 +98,8 @@ proc ::pd_menus::configure_for_canvas {mytoplevel} {
$menubar.file entryconfigure [_ "Save As..."] -state normal
$menubar.file entryconfigure [_ "Print..."] -state normal
# Edit menu
$menubar.edit entryconfigure [_ "Copy to clipboard (text)"] -state normal
$menubar.edit entryconfigure [_ "Paste from clipboard (text)"] -state normal
$menubar.edit entryconfigure [_ "Paste Replace"] -state normal
$menubar.edit entryconfigure [_ "Duplicate"] -state normal
$menubar.edit entryconfigure [_ "Font"] -state normal
Expand Down Expand Up @@ -142,6 +146,8 @@ proc ::pd_menus::configure_for_dialog {mytoplevel} {

# Edit menu
$menubar.edit entryconfigure [_ "Font"] -state disabled
$menubar.edit entryconfigure [_ "Copy to clipboard (text)"] -state disabled
$menubar.edit entryconfigure [_ "Paste from clipboard (text)"] -state disabled
$menubar.edit entryconfigure [_ "Paste Replace"] -state disabled
$menubar.edit entryconfigure [_ "Duplicate"] -state disabled
$menubar.edit entryconfigure [_ "Zoom In"] -state disabled
Expand Down Expand Up @@ -196,8 +202,12 @@ proc ::pd_menus::build_edit_menu {mymenu} {
-command {::pd_menucommands::scheduleAction menu_send $::focused_window cut}
$mymenu add command -label [_ "Copy"] -accelerator "$accelerator+C" \
-command {::pd_menucommands::scheduleAction menu_send $::focused_window copy}
$mymenu add command -label [_ "Copy to clipboard (text)"] -accelerator "$accelerator+$::alt+C" \
-command {::pd_menucommands::scheduleAction menu_send $::focused_window copy-to-clipboard-as-text}
$mymenu add command -label [_ "Paste"] -accelerator "$accelerator+V" \
-command {::pd_menucommands::scheduleAction menu_send $::focused_window paste}
$mymenu add command -label [_ "Paste from clipboard (text)"] -accelerator "$accelerator+$::alt+V" \
-command {::pdtk_canvas::pdtk_get_clipboard_text $::focused_window}
$mymenu add command -label [_ "Duplicate"] -accelerator "$accelerator+D" \
-command {::pd_menucommands::scheduleAction menu_send $::focused_window duplicate}
$mymenu add command -label [_ "Paste Replace" ] \
Expand Down
89 changes: 89 additions & 0 deletions tcl/pdtk_canvas.tcl
Expand Up @@ -250,6 +250,95 @@ proc pdtk_canvas_clickpaste {tkcanvas x y b} {
}
}

proc ::pdtk_canvas::pdtk_get_clipboard_text {tkcanvas} {
set CLIPBOARD_PATCH_TEXT_START 0;
set CLIPBOARD_PATCH_TEXT_LINE_END 1;
set CLIPBOARD_PATCH_TEXT_END 2;
set CLIPBOARD_PATCH_TEXT_LINE_PARTIAL 3;
set CLIPBOARD_PATCH_TEXT_LINE_END_APPENDIX 4;
set MAX_CHUNK_SIZE 960;
set clipboard_data [clipboard get]
eeropic marked this conversation as resolved.
Show resolved Hide resolved
if {[string length $clipboard_data] == 0} {
::pdwindow::post "Clipboard is empty.\n"
return
}
# TODO: better validation of PD patch clipboard data
if {[string index $clipboard_data 0] != "#"} {
::pdwindow::post "Warning: Clipboard content does not seem to be valid PD patch: \n"
::pdwindow::post $clipboard_data
return
}
pdsend "[winfo toplevel $tkcanvas] got-clipboard-contents $CLIPBOARD_PATCH_TEXT_START"
foreach line [split $clipboard_data \n] {
if {[string length $line] > 0} {
set escaped_line [string range $line 0 end-1]
set escaped_line [string map {" " {\ } ";" {\\;} "," {\\,} "\$" {\\$}} $escaped_line]
# split each line into chunks, and make sure atoms don't get split
while {[string length $escaped_line] > $MAX_CHUNK_SIZE} {
set boundary [string last { } [string range $escaped_line 0 [expr $MAX_CHUNK_SIZE - 1]]]
set chunk [string range $escaped_line 0 $boundary]
pdsend "[winfo toplevel $tkcanvas] got-clipboard-contents $CLIPBOARD_PATCH_TEXT_LINE_PARTIAL $chunk"
set escaped_line [string range $escaped_line [expr $boundary + 1] end]
}
set appendix_part ""
# find appendix part, i.e. ", f 12" denoting object width in chars, extract and remove
if {[regexp {\,\\\s(f\\\s\d+)$} $escaped_line -> appendix_part]} {
set escaped_line [regsub {\,\\\sf\\\s\d+$} $escaped_line ""]
# remove trailing slash
set escaped_line [string range $escaped_line 0 end-2]
pdsend "[winfo toplevel $tkcanvas] got-clipboard-contents $CLIPBOARD_PATCH_TEXT_LINE_PARTIAL $escaped_line"
pdsend "[winfo toplevel $tkcanvas] got-clipboard-contents $CLIPBOARD_PATCH_TEXT_LINE_END_APPENDIX $appendix_part"
} else {
pdsend "[winfo toplevel $tkcanvas] got-clipboard-contents $CLIPBOARD_PATCH_TEXT_LINE_END $escaped_line"
}
}
}
pdsend "[winfo toplevel $tkcanvas] got-clipboard-contents $CLIPBOARD_PATCH_TEXT_END"
}

proc pdtk_copy_to_clipboard_as_text {tkcanvas args} {
clipboard clear
set clipboard_content ""
set atom_line ""
set obj_type ""
for {set i 0} {$i < [llength $args]} {incr i} {
set prev_atom [lindex $args [expr $i - 1]]
set atom [lindex $args $i]
set next_atom [lindex $args [expr $i + 1]]
set next_next_atom [lindex $args [expr $i + 2]]
# Check for beginning of new line (#) but discard hex colors
if {[string first "#" $atom] == 0 && ![regexp {^#[0-9a-fA-F]{6}$} $atom]} {
append clipboard_content [string trim $atom_line]
append clipboard_content "\n"
set atom_line "$atom "
set obj_type $next_atom
} else {
if {$atom == ";" && ([string first "#" $next_atom] == 0 || $next_atom == "")} {
set atom_line [string trimright $atom_line]
append atom_line ";"
} elseif {$atom == ";"} {
append atom_line "\\; "
} elseif {[string first "\$" $atom] == 0} {
append atom_line "\\$" [string range $atom 1 end] " "
} elseif {$atom == ","} {
# text items can have unescaped comma delimiting the width attribute
if {$obj_type == "text"} {
append atom_line [expr {$next_atom != "f" ? "\\, " : ", "}]
} else {
append atom_line "\\, "
}
} else {
set delimiter [expr {$obj_type == "text" && $next_next_atom == "f" ? "" : " "}]
append atom_line $atom $delimiter
}
}
}
append clipboard_content "$atom_line\n"
set processed_content $clipboard_content
clipboard append [string trimleft $processed_content]
}


#------------------------------------------------------------------------------#
# canvas popup menu

Expand Down