package provide pd_bindings 0.1 package require pd_menucommands package require dialog_find package require pd_connect namespace eval ::pd_bindings:: { namespace export global_bindings namespace export dialog_bindings namespace export patch_bindings variable key2iso } set ::pd_bindings::key2iso "" # wrapper around bind(3tk)to deal with CapsLock # the actual bind-sequence is is build as '<${seq_prefix}-${seq_nocase}', # with the $seq_nocase part bing bound both as upper-case and lower-case proc ::pd_bindings::bind_capslock {tag seq_prefix seq_nocase script} { bind $tag <${seq_prefix}-[string tolower ${seq_nocase}]> "::pd_menucommands::scheduleAction $script" bind $tag <${seq_prefix}-[string toupper ${seq_nocase}]> "::pd_menucommands::scheduleAction $script" } # TODO rename pd_bindings to window_bindings after merge is done # Some commands are bound using "" quotations so that the $mytoplevel is # interpreted immediately. Since the command is being bound to $mytoplevel, # it makes sense to have value of $mytoplevel already in the command. This is # the opposite of most menu/bind commands here and in pd_menus.tcl, which use # {} to force execution of any variables (i.e. $::focused_window) until later # binding by class is not recursive, so its useful for window events proc ::pd_bindings::class_bindings {} { # and the Pd window is in a class to itself bind PdWindow "::pd_bindings::window_focusin %W" bind PdWindow "::pd_bindings::window_destroy %W" # bind to all the windows dedicated to patch canvases bind PatchWindow "::pd_bindings::window_focusin %W" bind PatchWindow "::pd_bindings::patch_map %W" bind PatchWindow "::pd_bindings::patch_unmap %W" bind PatchWindow "::pd_bindings::patch_configure %W %w %h %x %y" bind PatchWindow "::pd_bindings::window_destroy %W" # dialog panel windows bindings, which behave differently than PatchWindows bind DialogWindow "::pd_bindings::dialog_configure %W" bind DialogWindow "::pd_bindings::dialog_focusin %W" bind DialogWindow "::pd_bindings::window_destroy %W" # help browser bindings bind HelpBrowser "::pd_bindings::dialog_configure %W" bind HelpBrowser "::pd_bindings::dialog_focusin %W" bind HelpBrowser "::pd_bindings::window_destroy %W" } proc ::pd_bindings::global_bindings {} { # we use 'bind all' everywhere to get as much of Tk's automatic binding # behaviors as possible, things like not sending an event for 'O' when # 'Control-O' is pressed bind_capslock all $::modifier-Key a {menu_send %W selectall} bind_capslock all $::modifier-Key b {menu_helpbrowser} bind_capslock all $::modifier-Key c {menu_send %W copy} bind_capslock all $::modifier-Key d {menu_send %W duplicate} bind_capslock all $::modifier-Key e {menu_toggle_editmode} bind_capslock all $::modifier-Key f {menu_find_dialog} bind_capslock all $::modifier-Key g {menu_send %W findagain} bind_capslock all $::modifier-Key k {menu_send %W connect_selection} bind_capslock all $::modifier-Key n {menu_new} bind_capslock all $::modifier-Key o {menu_open} bind_capslock all $::modifier-Key p {menu_print $::focused_window} bind_capslock all $::modifier-Key r {menu_raise_pdwindow} bind_capslock all $::modifier-Key s {menu_send %W menusave} bind_capslock all $::modifier-Key t {menu_send %W triggerize} bind_capslock all $::modifier-Key v {menu_send %W paste} bind_capslock all $::modifier-Key w {::pd_bindings::window_close %W} bind_capslock all $::modifier-Key x {menu_send %W cut} bind_capslock all $::modifier-Key z {menu_undo} bind all <$::modifier-Key-1> {::pd_menucommands::scheduleAction menu_send_float %W obj 0} bind all <$::modifier-Key-2> {::pd_menucommands::scheduleAction menu_send_float %W msg 0} bind all <$::modifier-Key-3> {::pd_menucommands::scheduleAction menu_send_float %W floatatom 0} bind all <$::modifier-Key-4> {::pd_menucommands::scheduleAction menu_send_float %W listbox 0} bind all <$::modifier-Key-5> {::pd_menucommands::scheduleAction menu_send_float %W text 0} bind all <$::modifier-Key-slash> {::pd_menucommands::scheduleAction pdsend "pd dsp 1"} bind all <$::modifier-Key-period> {::pd_menucommands::scheduleAction pdsend "pd dsp 0"} # take the '=' key as a zoom-in accelerator, because '=' is the non-shifted # "+" key... this only makes sense on US keyboards but some users # expected it... go figure. bind all <$::modifier-Key-equal> {::pd_menucommands::scheduleAction menu_send_float %W zoom 2} bind all <$::modifier-Key-plus> {::pd_menucommands::scheduleAction menu_send_float %W zoom 2} bind all <$::modifier-Key-minus> {::pd_menucommands::scheduleAction menu_send_float %W zoom 1} bind all <$::modifier-Key-KP_Add> {::pd_menucommands::scheduleAction menu_send_float %W zoom 2} bind all <$::modifier-Key-KP_Subtract> {::pd_menucommands::scheduleAction menu_send_float %W zoom 1} # note: we avoid CMD-H & CMD+Shift-H as it hides Pd on macOS # annoying, but Tk's bind needs uppercase letter to get the Shift bind_capslock all $::modifier-Shift-Key A {menu_send %W menuarray} bind_capslock all $::modifier-Shift-Key B {menu_send %W bng} bind_capslock all $::modifier-Shift-Key C {menu_send %W mycnv} bind_capslock all $::modifier-Shift-Key D {menu_send %W vradio} bind_capslock all $::modifier-Shift-Key G {menu_send %W graph} bind_capslock all $::modifier-Shift-Key J {menu_send %W hslider} bind_capslock all $::modifier-Shift-Key I {menu_send %W hradio} bind_capslock all $::modifier-Shift-Key L {menu_clear_console} bind_capslock all $::modifier-Shift-Key M {menu_message_dialog} bind_capslock all $::modifier-Shift-Key N {menu_send %W numbox} bind_capslock all $::modifier-Shift-Key Q {pdsend "pd quit"} bind_capslock all $::modifier-Shift-Key R {menu_send %W tidy} bind_capslock all $::modifier-Shift-Key S {menu_send %W menusaveas} bind_capslock all $::modifier-Shift-Key T {menu_send %W toggle} bind_capslock all $::modifier-Shift-Key U {menu_send %W vumeter} bind_capslock all $::modifier-Shift-Key V {menu_send %W vslider} bind_capslock all $::modifier-Shift-Key W {::pd_bindings::window_close %W 1} bind_capslock all $::modifier-Shift-Key Z {menu_redo} bind all {menu_send %W deselectall; ::pd_bindings::sendkey %W 1 %K %A 1 %k} # OS-specific bindings if {$::windowingsystem eq "aqua"} { # TK 8.5+ Cocoa handles quit, minimize, & raise next window for us if {$::tcl_version < 8.5} { bind_capslock all $::modifier-Key q {::pd_menucommands::scheduleAction ::pd_connect::menu_quit} bind_capslock all $::modifier-Key m {::pd_menucommands::scheduleAction menu_minimize %W} bind all <$::modifier-quoteleft> {::pd_menucommands::scheduleAction menu_raisenextwindow} } # 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 {::pd_bindings::sendkey %W 1 %K "" 1 %k} bind all {::pd_bindings::sendkey %W 0 %K "" 1 %k} bind all {::pd_bindings::sendkey %W 1 %K "" 1 %k} bind all {::pd_bindings::sendkey %W 0 %K "" 1 %k} bind all {::pd_bindings::sendkey %W 1 %K "" 1 %k} bind all {::pd_bindings::sendkey %W 0 %K "" 1 %k} bind all {::pd_bindings::sendkey %W 1 %K "" 1 %k} bind all {::pd_bindings::sendkey %W 0 %K "" 1 %k} } else { bind_capslock all $::modifier-Key q {::pd_connect::menu_quit} bind_capslock all $::modifier-Key m {menu_minimize %W} 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 bind all <$::modifier-greater> {menu_raisenextwindow} bind all <$::modifier-less> {menu_raisepreviouswindow} } bind all {::pd_bindings::sendkey %W 1 %K %A 0 %k} bind all {::pd_bindings::sendkey %W 0 %K %A 0 %k} bind all {::pd_bindings::sendkey %W 1 %K %A 1 %k} bind all {::pd_bindings::sendkey %W 0 %K %A 1 %k} } # bindings for .pdwindow are found in ::pdwindow::pdwindow_bindings in pdwindow.tcl # this is for the dialogs: find, font, sendmessage, gatom properties, array # properties, iemgui properties, canvas properties, data structures # properties, Audio setup, and MIDI setup proc ::pd_bindings::dialog_bindings {mytoplevel dialogname} { variable modifier bind $mytoplevel "dialog_${dialogname}::cancel $mytoplevel; break" bind $mytoplevel "dialog_${dialogname}::ok $mytoplevel; break" bind_capslock $mytoplevel $::modifier-Key w "dialog_${dialogname}::cancel $mytoplevel; break" # these aren't supported in the dialog, so alert the user, then break so # that no other key bindings are run if {$mytoplevel ne ".find"} { bind_capslock $mytoplevel $::modifier-Key s {bell; break} bind_capslock $mytoplevel $::modifier-Shift-Key s {bell; break} bind_capslock $mytoplevel $::modifier-Key p {bell; break} } else { # find may may allow passthrough to it's target search patch ::dialog_find::update_bindings } bind_capslock $mytoplevel $::modifier-Key t {bell; break} wm protocol $mytoplevel WM_DELETE_WINDOW "dialog_${dialogname}::cancel $mytoplevel" } # this is for canvas windows proc ::pd_bindings::clear_compose_keys {window} { # this is an ugly hack! # on macOS we might need to clear interim compose characters # but only while editing. # otherwise, we might send the BackSpace only to a selected object # and delete that (https://github.com/pure-data/pure-data/issues/2224) set skip 0 catch { if { $::editingtext([winfo toplevel $window]) == 0 } { set skip 1 } } if { $skip } { return } ::pd_bindings::sendkey ${window} 1 BackSpace "" 0 ::pd_bindings::sendkey ${window} 0 BackSpace "" 0 } proc ::pd_bindings::patch_bindings {mytoplevel} { variable modifier set tkcanvas [tkcanvas_name $mytoplevel] # on Mac OS X/Aqua, the Alt/Option key is called Option in Tcl if {$::windowingsystem eq "aqua"} { set alt "Option" } else { set alt "Alt" } # TODO move mouse bindings to global and bind to 'all' # mouse bindings ----------------------------------------------------------- # these need to be bound to $tkcanvas because %W will return $mytoplevel for # events over the window frame and $tkcanvas for events over the canvas bind $tkcanvas "pdtk_canvas_motion %W %x %y 0" bind $tkcanvas "pdtk_canvas_motion %W %x %y 1" bind $tkcanvas <$::modifier-Motion> "pdtk_canvas_motion %W %x %y 2" bind $tkcanvas <$::modifier-Shift-Motion> "pdtk_canvas_motion %W %x %y 3" bind $tkcanvas <$alt-Motion> "pdtk_canvas_motion %W %x %y 4" bind $tkcanvas <$alt-Shift-Motion> "pdtk_canvas_motion %W %x %y 5" bind $tkcanvas <$::modifier-$alt-Motion> "pdtk_canvas_motion %W %x %y 6" bind $tkcanvas <$::modifier-$alt-Shift-Motion> "pdtk_canvas_motion %W %x %y 7" bind $tkcanvas \ "pdtk_canvas_mouse %W %x %y %b 0" bind $tkcanvas \ "pdtk_canvas_mouse %W %x %y %b 1" bind $tkcanvas <$::modifier-ButtonPress-1> \ "pdtk_canvas_mouse %W %x %y %b 2" bind $tkcanvas <$::modifier-Shift-ButtonPress-1> \ "pdtk_canvas_mouse %W %x %y %b 3" bind $tkcanvas <$alt-ButtonPress-1> \ "pdtk_canvas_mouse %W %x %y %b 4" bind $tkcanvas <$alt-Shift-ButtonPress-1> \ "pdtk_canvas_mouse %W %x %y %b 5" bind $tkcanvas <$::modifier-$alt-ButtonPress-1> \ "pdtk_canvas_mouse %W %x %y %b 6" bind $tkcanvas <$::modifier-$alt-Shift-ButtonPress-1> \ "pdtk_canvas_mouse %W %x %y %b 7" bind $tkcanvas \ "pdtk_canvas_mouseup %W %x %y %b 0" bind $tkcanvas \ "pdtk_canvas_mouseup %W %x %y %b 1" bind $tkcanvas <$::modifier-ButtonRelease-1> \ "pdtk_canvas_mouseup %W %x %y %b 2" bind $tkcanvas <$::modifier-Shift-ButtonRelease-1> \ "pdtk_canvas_mouseup %W %x %y %b 3" bind $tkcanvas <$alt-ButtonRelease-1> \ "pdtk_canvas_mouseup %W %x %y %b 4" bind $tkcanvas <$alt-Shift-ButtonRelease-1> \ "pdtk_canvas_mouseup %W %x %y %b 5" bind $tkcanvas <$::modifier-$alt-ButtonRelease-1> \ "pdtk_canvas_mouseup %W %x %y %b 6" bind $tkcanvas <$::modifier-$alt-Shift-ButtonRelease-1> \ "pdtk_canvas_mouseup %W %x %y %b 7" if {$::windowingsystem eq "x11"} { # from http://wiki.tcl.tk/3893 bind all \ {event generate [focus -displayof %W] -delta 1} bind all \ {event generate [focus -displayof %W] -delta -1} bind all \ {event generate [focus -displayof %W] -delta 1} bind all \ {event generate [focus -displayof %W] -delta -1} } bind $tkcanvas {::pdtk_canvas::scroll %W y %D} bind $tkcanvas {::pdtk_canvas::scroll %W x %D} # clear interim compose character by sending a virtual BackSpace, # these events are pulled from Tk library/entry.tcl # should we try to keep track of the "marked text" selection which may be # more than a single character? ie. start & length of marked text catch { # bind $tkcanvas <> { # ::pdwindow::post "%W start marked text\n" # } # bind $tkcanvas <> { # ::pdwindow::post "%W end marked text\n" # } bind $tkcanvas <> { # ::pdwindow::post "%W clear marked text\n" ::pd_bindings::clear_compose_keys %W } bind $tkcanvas <> { # ::pdwindow::post "%W accent backspace\n" ::pd_bindings::clear_compose_keys %W } } stderr # "right clicks" are defined differently on each platform switch -- $::windowingsystem { "aqua" { bind $tkcanvas "pdtk_canvas_rightclick %W %x %y %b" # on Mac OS X, make a rightclick with Ctrl-click for 1 button mice bind $tkcanvas "pdtk_canvas_rightclick %W %x %y %b" } "x11" { bind $tkcanvas "pdtk_canvas_rightclick %W %x %y %b" # on X11, button 2 "pastes" from the X windows clipboard bind $tkcanvas "pdtk_canvas_clickpaste %W %x %y %b" } "win32" { bind $tkcanvas "pdtk_canvas_rightclick %W %x %y %b" } } # key to cycle through selection bind $tkcanvas "::pd_bindings::canvas_cycle %W 1 %K %A 0 %k" bind $tkcanvas "::pd_bindings::canvas_cycle %W -1 %K %A 1 %k" # on X11, is a different key by the name 'ISO_Left_Tab'... # other systems (at least aqua) do not like this name, so we 'catch' any errors catch {bind $tkcanvas "::pd_bindings::canvas_cycle %W -1 %K %A 1 %k" } stderr # window protocol bindings wm protocol $mytoplevel WM_DELETE_WINDOW "pdsend \"$mytoplevel menuclose 0\"" bind $tkcanvas "::pd_bindings::patch_destroy %W" } #------------------------------------------------------------------------------# # event handlers # do tasks when changing focus (Window menu, scrollbars, etc.) proc ::pd_bindings::window_focusin {mytoplevel} { # focused_window is used throughout for sending bindings, menu commands, # etc. to the correct patch receiver symbol. MSP took out a line that # confusingly redirected the "find" window which might be in mid search set ::focused_window $mytoplevel ::pd_menucommands::set_filenewdir $mytoplevel ::dialog_font::update_font_dialog $mytoplevel if {$mytoplevel eq ".pdwindow"} { ::pd_menus::configure_for_pdwindow } else { ::pd_menus::configure_for_canvas $mytoplevel } if {[winfo exists .font]} {wm transient .font $mytoplevel} # if we regain focus from another app, make sure to editmode cursor is right if {$::editmode($mytoplevel)} { $mytoplevel configure -cursor hand2 } } # global window close event, patch windows are closed by pd # while other window types are closed via their own bindings # force argument: # 0: request to close, verifying whether clean or dirty # 1: request to close, no verification proc ::pd_bindings::window_close {mytoplevel {force 0}} { # catch any non-existent windows # ie. the helpbrowser after it's been # closed by it's own binding if {$force eq 0 && ![winfo exists $mytoplevel]} { return } menu_send_float $mytoplevel menuclose $force } # "map" event tells us when the canvas becomes visible, and "unmap", # invisible. Invisibility means the Window Manager has minimized us. We # don't get a final "unmap" event when we destroy the window. proc ::pd_bindings::patch_map {mytoplevel} { pdsend "$mytoplevel map 1" ::pdtk_canvas::finished_loading_file $mytoplevel } proc ::pd_bindings::patch_unmap {mytoplevel} { pdsend "$mytoplevel map 0" } proc ::pd_bindings::patch_configure {mytoplevel width height x y} { if {$width == 1 || $height == 1} { # make sure the window is fully created update idletasks } # the geometry we receive from the callback is really for the frame # however, we need position including the border decoration # as this is how we restore the position scan [wm geometry $mytoplevel] {%dx%d%[+]%d%[+]%d} width height - x - y pdtk_canvas_getscroll [tkcanvas_name $mytoplevel] # send the size/location of the window and canvas to 'pd' in the form of: # left top right bottom pdsend "$mytoplevel setbounds $x $y [expr $x + $width] [expr $y + $height]" } proc ::pd_bindings::patch_destroy {window} { set mytoplevel [winfo toplevel $window] unset ::editmode($mytoplevel) unset ::undo_actions($mytoplevel) unset ::redo_actions($mytoplevel) unset ::editingtext($mytoplevel) unset ::loaded($mytoplevel) # unset my entries all of the window data tracking arrays array unset ::windowname $mytoplevel array unset ::parentwindows $mytoplevel array unset ::childwindows $mytoplevel } proc ::pd_bindings::dialog_configure {mytoplevel} { } proc ::pd_bindings::dialog_focusin {mytoplevel} { set ::focused_window $mytoplevel ::pd_menus::configure_for_dialog $mytoplevel if {$mytoplevel eq ".find"} {::dialog_find::focus_find} } proc ::pd_bindings::window_destroy {winid} { # make sure that ::focused_window does not refer to a non-existing window if {$winid eq $::focused_window} { set ::focused_window [wm transient $winid] } if {![winfo exists $::focused_window]} { set ::focused_window [focus] if {$::focused_window ne ""} { set ::focused_window [winfo toplevel $::focused_window] } } } # (Shift-)Tab for cycling through selection proc ::pd_bindings::canvas_cycle {mytoplevel cycledir key iso shift {keycode ""}} { menu_send_float $mytoplevel cycleselect $cycledir ::pd_bindings::sendkey $mytoplevel 1 $key $iso $shift $keycode } #------------------------------------------------------------------------------# # key usage # canvas_key() expects to receive the patch's mytoplevel because key messages # are local to each patch. Therefore, key messages are not send for the # dialog panels, the Pd window, help browser, etc. so we need to filter those # events out. proc ::pd_bindings::sendkey {window state key iso shift {keycode ""} } { #::pdwindow::error "::pd_bindings::sendkey .${state}. .${key}. .${iso}. .${shift}. .${keycode}.\n" # state: 1=keypress, 0=keyrelease # key (%K): the keysym corresponding to the event, substituted as a textual string # iso (%A): substitutes the UNICODE character corresponding to the event, or the empty string if the event does not correspond to a UNICODE character (e.g. the shift key was pressed) # shift: 1=shift, 0=no-shift # keycode (%k): keyboard code if { "$keycode" eq "" } { # old fashioned code fails to add %k-parameter; substitute... set keycode $key } # iso: # - 1-character: use it! # - multi-character: can happen with dead-keys (where an accent turns out to not be an accent after all...; e.g. "^" + "1" -> "^1") # : turn it into multiple key-events (one per character)! # - 0-character: we need to calculate iso # : if there's one stored in key2iso, use it (in the case of KeyRelease) # : do some substitution based on $key # : else use $key set isosplit [split $iso {}] if { [llength $isosplit] == 0 && $state == 0 } { catch {set iso [dict get $::pd_bindings::key2iso $keycode] } } switch -- [llength $isosplit] { 0 { switch -- $key { "BackSpace" { set key 8 } "Tab" { set key 9 } "Return" { set key 10 } "Escape" { set key 27 } "Space" { set key 32 } "space" { set key 32 } "Delete" { set key 127 } "KP_Delete" { set key 127 } "KP_Enter" { set key 10 } default { } } } 1 { scan $iso %c key if { "$key" eq "13" } { set key 10 } catch { if { "" eq "${::pd_bindings::key2iso}" } { set ::pd_bindings::key2iso [dict create] } # store the key2iso mapping dict set ::pd_bindings::key2iso $keycode $iso } } default { # split a multi-char $iso in single chars foreach k $isosplit { ::pd_bindings::sendkey $window $state $key $k $shift $keycode } return } } # some pop-up panels also bind to keys like the enter, but then disappear, # so ignore their events. The inputbox in the Startup dialog does this. if {! [winfo exists $window]} {return} # $window might be a toplevel or canvas, [winfo toplevel] does the right thing set mytoplevel [winfo toplevel $window] if {[winfo class $mytoplevel] ne "PatchWindow"} { set mytoplevel pd } pdsend "$mytoplevel key $state $key $shift" }