package provide pdtk_canvas 0.1 package require pd_bindings namespace eval ::pdtk_canvas:: { # the untitled name prefix pd checks for using a macro in g_canvas.h, # a saveas panel is shown when saving a file with this name variable untitled_name "PDUNTITLED" variable untitled_len 10 variable enable_cords_to_foreground 0 namespace export pdtk_canvas_popup namespace export pdtk_canvas_editmode namespace export pdtk_canvas_getscroll namespace export pdtk_canvas_setparents namespace export pdtk_canvas_reflecttitle namespace export pdtk_canvas_menuclose } # store the filename associated with this window, # so we can use it during menuclose array set ::pdtk_canvas::::window_fullname {} array set ::pdtk_canvas::geometry_needs_init {} # One thing that is tricky to understand is the difference between a Tk # 'canvas' and a 'canvas' in terms of Pd's implementation. They are similar, # but not the same thing. In Pd code, a 'canvas' is basically a patch, while # the Tk 'canvas' is the backdrop for drawing everything that is in a patch. # The Tk 'canvas' is contained in a 'toplevel' window. That window has a Tk # class of 'PatchWindow'. # TODO figure out weird frameless window when you open a graph #TODO: http://wiki.tcl.tk/11502 # MS Windows #wm geometry . returns contentswidthxcontentsheight+decorationTop+decorationLeftEdge. #and #winfo rooty . returns contentsTop #winfo rootx . returns contentsLeftEdge if {[tk windowingsystem] eq "win32" || \ $::tcl_version < 8.5 || \ ($::tcl_version == 8.5 && \ [tk windowingsystem] eq "aqua" && \ [lindex [split [info patchlevel] "."] 2] < 13) } { # fit the geometry onto screen for Tk 8.4 or win32, # also check for Tk Cocoa backend on macOS which is only stable in 8.5.13+; # newer versions of Tk can handle multiple monitors so allow negative pos proc pdtk_canvas_wrap_window {x y w h} { foreach {width height} [wm maxsize .] break # get virtual root coordinates for minimum position set xmin [winfo vrootx .] set ymin [winfo vrooty .] # clip window size to screen size set w [expr {min($w, $width)}] set h [expr {min($h, $height - $::menubarsize)}] # get max position set xmax [expr {$xmin + $width - $w}] set ymax [expr {$ymin + $height - $h}] # clip given position set x [expr {max(min($x, $xmax), $xmin)}] set y [expr {max(min($y, $ymax), $ymin + $::menubarsize)}] return [list $x $y $w $h] } } { proc pdtk_canvas_wrap_window {x y w h} { return [list ${x} ${y} ${w} ${h}] } } # this proc is split out on its own to make it easy to override. This makes it # easy for people to customize these calculations based on their Window # Manager, desires, etc. proc pdtk_canvas_place_window {width height geometry} { # read back the current geometry +posx+posy into variables set w $width set h $height set xypos "" if { "" != ${geometry} } { scan $geometry {%[+]%d%[+]%d} - x - y foreach {x y w h} [pdtk_canvas_wrap_window $x $y $width $height] {break} set xypos +${x}+${y} } return [list ${w} ${h} ${w}x${h}${xypos}] } #------------------------------------------------------------------------------# # canvas new/saveas proc pdtk_canvas_new {mytoplevel width height geometry editable} { if { "" eq $geometry } { # no position set: this is a new window (rather than one loaded from file) # we set a flag here, so we can query (and report) the actual geometry, # once the window is fully created set ::pdtk_canvas::geometry_needs_init($mytoplevel) 1 } foreach {width height geometry} [pdtk_canvas_place_window $width $height $geometry] {break;} set ::undo_actions($mytoplevel) no set ::redo_actions($mytoplevel) no # release the window grab here so that the new window will # properly get the Map and FocusIn events when its created ::pdwindow::busyrelease # set the loaded array for this new window so things can track state set ::loaded($mytoplevel) 0 toplevel $mytoplevel -width $width -height $height -class PatchWindow wm group $mytoplevel . $mytoplevel configure -menu $::patch_menubar # we have to wait until $mytoplevel exists before we can generate # a <> event for it, that's why this is here and not in the # started_loading_file proc. Perhaps this doesn't make sense tho event generate $mytoplevel <> if { "" != ${geometry} } { wm geometry $mytoplevel $geometry } wm minsize $mytoplevel $::canvas_minwidth $::canvas_minheight set tkcanvas [tkcanvas_name $mytoplevel] canvas $tkcanvas -width $width -height $height \ -highlightthickness 0 -scrollregion [list 0 0 $width $height] \ -xscrollcommand "$mytoplevel.xscroll set" \ -yscrollcommand "$mytoplevel.yscroll set" \ -background white scrollbar $mytoplevel.xscroll -orient horizontal -command "$tkcanvas xview" scrollbar $mytoplevel.yscroll -orient vertical -command "$tkcanvas yview" pack $tkcanvas -side left -expand 1 -fill both # for some crazy reason, win32 mousewheel scrolling is in units of # 120, and this forces Tk to interpret 120 to mean 1 scroll unit if {$::windowingsystem eq "win32"} { $tkcanvas configure -xscrollincrement 1 -yscrollincrement 1 } ::pd_bindings::patch_bindings $mytoplevel # give focus to the canvas so it gets the events rather than the window focus $tkcanvas # let the scrollbar logic determine if it should make things scrollable set ::xscrollable($tkcanvas) 0 set ::yscrollable($tkcanvas) 0 # init patch properties arrays set ::editingtext($mytoplevel) 0 set ::childwindows($mytoplevel) {} # this should be at the end so that the window and canvas are all ready # before this variable changes. set ::editmode($mytoplevel) $editable } # if the patch canvas window already exists, then make it come to the front proc pdtk_canvas_raise {mytoplevel} { wm deiconify $mytoplevel raise $mytoplevel set mycanvas $mytoplevel.c focus $mycanvas } proc pdtk_canvas_saveas {mytoplevel initialfile initialdir destroyflag} { if { ! [file isdirectory $initialdir]} {set initialdir $::filenewdir} set filename [tk_getSaveFile -initialdir $initialdir \ -initialfile [::pdtk_canvas::cleanname "$initialfile"] \ -defaultextension .pd -filetypes $::filetypes \ -parent $mytoplevel] if {$filename eq ""} return; # they clicked cancel set extension [file extension $filename] set oldfilename $filename set filename [regsub -- "$extension$" $filename [string tolower $extension]] if { ! [regexp -- "\.(pd|pat|mxt)$" $filename]} { # we need the file extension even on Mac OS X set filename $filename.pd } # test again after downcasing and maybe adding a ".pd" on the end if {$filename ne $oldfilename && [file exists $filename]} { set answer [tk_messageBox -type okcancel -icon question -default cancel\ -message [_ "\"$filename\" already exists. Do you want to replace it?"]] if {$answer eq "cancel"} return; # they clicked cancel } set dirname [file dirname $filename] set basename [file tail $filename] pdsend "$mytoplevel savetofile [enquote_path $basename] [enquote_path \ $dirname] $destroyflag" set ::filenewdir $dirname # add to recentfiles ::pd_guiprefs::update_recentfiles $filename } ##### ask user Save? Discard? Cancel?, and if so, send a message on to Pd ###### proc ::pdtk_canvas::pdtk_canvas_menuclose {mytoplevel reply_to_pd} { raise $mytoplevel set filename [lindex [array get ::pdtk_canvas::::window_fullname $mytoplevel] 1] set message [_ "Do you want to save the changes you made in '%s'?" $filename] set answer [tk_messageBox -message $message -type yesnocancel -default "yes" \ -parent $mytoplevel -icon question] switch -- $answer { yes {pdsend "$mytoplevel menusave 1"} no {pdsend $reply_to_pd} cancel {} } } #------------------------------------------------------------------------------# # mouse usage # TODO put these procs into the pdtk_canvas namespace proc pdtk_canvas_motion {tkcanvas x y mods} { set mytoplevel [winfo toplevel $tkcanvas] pdsend "$mytoplevel motion [$tkcanvas canvasx $x] [$tkcanvas canvasy $y] $mods" } proc pdtk_canvas_mouse {tkcanvas x y b f} { set mytoplevel [winfo toplevel $tkcanvas] pdsend "$mytoplevel mouse [$tkcanvas canvasx $x] [$tkcanvas canvasy $y] $b $f" } proc pdtk_canvas_mouseup {tkcanvas x y b {f 0}} { set mytoplevel [winfo toplevel $tkcanvas] pdsend "$mytoplevel mouseup [$tkcanvas canvasx $x] [$tkcanvas canvasy $y] $b $f" } proc pdtk_canvas_rightclick {tkcanvas x y b} { set mytoplevel [winfo toplevel $tkcanvas] pdsend "$mytoplevel mouse [$tkcanvas canvasx $x] [$tkcanvas canvasy $y] $b 8" } # on X11, button 2 pastes from X11 clipboard, so simulate normal paste actions proc pdtk_canvas_clickpaste {tkcanvas x y b} { pdtk_canvas_mouse $tkcanvas $x $y $b 0 pdtk_canvas_mouseup $tkcanvas $x $y $b 0 if { [catch {set pdtk_pastebuffer [selection get]}] } { # no selection... do nothing } else { for {set i 0} {$i < [string length $pdtk_pastebuffer]} {incr i 1} { set cha [string index $pdtk_pastebuffer $i] scan $cha %c keynum pdsend "[winfo toplevel $tkcanvas] key 1 $keynum 0" } } } #------------------------------------------------------------------------------# # canvas popup menu # since there is one popup that is used for all canvas windows, the menu # -commands use {} quotes so that $::focused_window is interpreted when the # menu item is called, not when the command is mapped to the menu item. This # is the same as the menubar in pd_menus.tcl but the opposite of the 'bind' # commands in pd_bindings.tcl proc ::pdtk_canvas::create_popup {popupid actionwindow x y} { if { ! [winfo exists $popupid]} { # the popup menu for the canvas menu $popupid -tearoff false $popupid add command -label [_ "Properties"] \ -command "::pdtk_canvas::done_popup $actionwindow 0 $x $y" $popupid add command -label [_ "Open"] \ -command "::pdtk_canvas::done_popup $actionwindow 1 $x $y" $popupid add command -label [_ "Help"] \ -command "::pdtk_canvas::done_popup $actionwindow 2 $x $y" } } proc ::pdtk_canvas::done_popup {mytoplevel action x y} { pdsend "$mytoplevel done-popup $action $x $y" } proc ::pdtk_canvas::pdtk_canvas_popup {mytoplevel xcanvas ycanvas hasproperties hasopen} { set toplevel [winfo toplevel $mytoplevel] set tkcanvas [tkcanvas_name $toplevel] set popup ${toplevel}.popup destroy $popup ::pdtk_canvas::create_popup ${popup} ${toplevel} ${xcanvas} ${ycanvas} if {$hasproperties} { ${popup} entryconfigure [_ "Properties"] -state normal } else { ${popup} entryconfigure [_ "Properties"] -state disabled } if {$hasopen} { ${popup} entryconfigure [_ "Open"] -state normal } else { ${popup} entryconfigure [_ "Open"] -state disabled } set scrollregion [$tkcanvas cget -scrollregion] # get the canvas location that is currently the top left corner in the window set left_xview_pix [expr [lindex [$tkcanvas xview] 0] * [lindex $scrollregion 2]] set top_yview_pix [expr [lindex [$tkcanvas yview] 0] * [lindex $scrollregion 3]] # take the mouse clicks in canvas coords, add the root of the canvas # window, and subtract the area that is obscured by scrolling set xpopup [expr int($xcanvas + [winfo rootx $tkcanvas] - $left_xview_pix)] set ypopup [expr int($ycanvas + [winfo rooty $tkcanvas] - $top_yview_pix)] tk_popup ${popup} ${xpopup} ${ypopup} 0 } if {[tk windowingsystem] eq "aqua" } { # I don't know how to move the mouse on OSX, so skip it proc ::pdtk_canvas::setmouse {tkcanvas x y} { } } else { proc ::pdtk_canvas::setmouse {tkcanvas x y} { # set the mouse to the given position # (same coordinate system as reported by pdtk_canvas_motion) event generate $tkcanvas -warp 1 -x $x -y $y } } #------------------------------------------------------------------------------# # procs for when file loading starts/finishes proc ::pdtk_canvas::started_loading_file {patchname} { ::pdwindow::busygrab } # things to run when a patch is finished loading. This is called when # the OS sends the "Map" event for this window. proc ::pdtk_canvas::finished_loading_file {mytoplevel} { # ::pdwindow::busyrelease is in pdtk_canvas_new so that the grab # is released before the new toplevel window gets created. # Otherwise the grab blocks the new window from getting the # FocusIn event on creation. # set editmode to make sure the menu item is in the right state pdtk_canvas_editmode $mytoplevel $::editmode($mytoplevel) set ::loaded($mytoplevel) 1 # send the virtual events now that everything is loaded event generate $mytoplevel <> # if the window was created without a position (that is: a new window), # we have the opportunity to query the actual position now if { "" ne [array names ::pdtk_canvas::geometry_needs_init $mytoplevel ] } { array unset ::pdtk_canvas::geometry_needs_init $mytoplevel scan [wm geometry $mytoplevel] {%dx%d%[+]%d%[+]%d} width height - x - y # on X11, 'wm geometry' won't report a useful position until the window was moved # but 'winfo geometry' does (though slightly off, but we ignore this offset # for newly created, never moved windows) # other windowingsystems will already report a useful position, and luckily # they report the same for 'wm geometry' and 'winfo geometry' if { "+$x+$y" eq "+0+0" } { scan [winfo geometry $mytoplevel] {%dx%d%[+]%d%[+]%d} width height - x - y pdsend "$mytoplevel setbounds $x $y [expr $x + $width] [expr $y + $height]" } } } #------------------------------------------------------------------------------# # procs for canvas events # check or uncheck the "edit" menu item proc ::pdtk_canvas::pdtk_canvas_editmode {mytoplevel state} { set ::editmode_button $state set ::editmode($mytoplevel) $state event generate $mytoplevel <> } # message from Pd to update the currently available undo/redo action proc pdtk_undomenu {mytoplevel undoaction redoaction} { set ::undo_actions($mytoplevel) $undoaction set ::redo_actions($mytoplevel) $redoaction if {$mytoplevel ne "nobody"} { ::pd_menus::update_undo_on_menu $mytoplevel $undoaction $redoaction } } # This proc configures the scrollbars whenever anything relevant has # been updated. It should always receive a tkcanvas, which is then # used to generate the mytoplevel, needed to address the scrollbars. proc ::pdtk_canvas::pdtk_canvas_getscroll {tkcanvas} { # delay until we are ready after idle [list ::pdtk_canvas::do_getscroll $tkcanvas] } proc ::pdtk_canvas::do_getscroll {tkcanvas} { if {! [winfo exists $tkcanvas]} { return } set mytoplevel [winfo toplevel $tkcanvas] set height [winfo height $tkcanvas] set width [winfo width $tkcanvas] set bbox [$tkcanvas bbox all] if {$bbox eq "" || [llength $bbox] != 4} {return} set xupperleft [lindex $bbox 0] set yupperleft [lindex $bbox 1] if {$xupperleft > 0} {set xupperleft 0} if {$yupperleft > 0} {set yupperleft 0} set xlowerright [lindex $bbox 2] set ylowerright [lindex $bbox 3] if {$xlowerright < $width} {set xlowerright $width} if {$ylowerright < $height} {set ylowerright $height} set scrollregion [concat $xupperleft $yupperleft $xlowerright $ylowerright] $tkcanvas configure -scrollregion $scrollregion # X scrollbar if {[lindex [$tkcanvas xview] 0] == 0.0 && [lindex [$tkcanvas xview] 1] == 1.0} { set ::xscrollable($tkcanvas) 0 pack forget $mytoplevel.xscroll } else { set ::xscrollable($tkcanvas) 1 pack $mytoplevel.xscroll -side bottom -fill x -before $tkcanvas } # Y scrollbar, it gets touchy at the limit, so say > 0.995 if {[lindex [$tkcanvas yview] 0] == 0.0 && [lindex [$tkcanvas yview] 1] > 0.995} { set ::yscrollable($tkcanvas) 0 pack forget $mytoplevel.yscroll } else { set ::yscrollable($tkcanvas) 1 pack $mytoplevel.yscroll -side right -fill y -before $tkcanvas } } proc ::pdtk_canvas::scroll {tkcanvas axis amount} { if {$axis eq "x" && $::xscrollable($tkcanvas) == 1} { $tkcanvas xview scroll [expr {- ($amount)}] units } if {$axis eq "y" && $::yscrollable($tkcanvas) == 1} { $tkcanvas yview scroll [expr {- ($amount)}] units } } #------------------------------------------------------------------------------# # get patch window child/parent relationships # add a child window ID to the list of children, if it isn't already there proc ::pdtk_canvas::addchild {mytoplevel child} { # if either ::childwindows($mytoplevel) does not exist, or $child does not # exist inside of the ::childwindows($mytoplevel list if { [lsearch -exact [array names ::childwindows $mytoplevel]] == -1 \ || [lsearch -exact $::childwindows($mytoplevel) $child] == -1} { set ::childwindows($mytoplevel) [lappend ::childwindows($mytoplevel) $child] } } # receive a list of all my parent windows from 'pd' proc ::pdtk_canvas::pdtk_canvas_setparents {mytoplevel args} { # check if the user passed a list (instead of multiple arguments) if { [llength $args] == 1 } {set args [lindex $args 0]} set parents {} foreach parent $args { if { [catch {set parent [winfo toplevel $parent]}] } { if { [file extension $parent] eq ".c" } {set parent [file rootname $parent]} } lappend parents $parent addchild $parent $mytoplevel } set ::parentwindows($mytoplevel) $parents } # receive information for setting the info in the title bar of the window proc ::pdtk_canvas::pdtk_canvas_reflecttitle {mytoplevel \ path name arguments dirty} { set path [::pdtk_text::unescape $path] set name [::pdtk_text::unescape $name] set arguments [::pdtk_text::unescape $arguments] set name [::pdtk_canvas::cleanname "$name"] set ::windowname($mytoplevel) $name set ::pdtk_canvas::::window_fullname($mytoplevel) "$path/$name" if {$::windowingsystem eq "aqua"} { wm attributes $mytoplevel -modified $dirty if {[file exists "$path/$name"]} { # for some reason -titlepath can still fail so just catch it if [catch {wm attributes $mytoplevel -titlepath "$path/$name"}] { wm title $mytoplevel "$path/$name" } } wm title $mytoplevel "$name$arguments" } else { if {$dirty} {set dirtychar "*"} else {set dirtychar " "} wm title $mytoplevel "$name$dirtychar$arguments - $path" } } #------------------------------------------------------------------------------# # utils # provide a clean filename to avoid saving files with the untitled name prefix proc ::pdtk_canvas::cleanname {name} { variable untitled_name variable untitled_len if {[string compare -length $untitled_len "$name" "$untitled_name"] == 0} { # replace untitled prefix with a display name # TODO localize "Untitled" & make sure translations do not contain spaces return [string replace "$name" 0 [expr $untitled_len - 1] "Untitled"] } return $name } proc ::pdtk_canvas::cords_to_foreground {mytoplevel {state 1}} { if {$::pdtk_canvas::enable_cords_to_foreground} { set col black if { $state == 0 } { set col lightgrey } foreach id [$mytoplevel find withtag {cord && !selected}] { # don't apply backgrouding on selected (blue) lines if { [lindex [$mytoplevel itemconfigure $id -fill] 4 ] ne "blue" } { $mytoplevel itemconfigure $id -fill $col } } } }