#!/bin/sh # This line continues for Tcl, but is a single line for 'sh' \ exec wish "$0" -- ${1+"$@"} # For information on usage and redistribution, and for a DISCLAIMER OF ALL # WARRANTIES, see the file, "LICENSE.txt," in this distribution. # Copyright (c) 1997-2009 Miller Puckette. # "." automatically gets a window, we don't want it. Withdraw it before doing # anything else, so that we don't get the automatic window flashing for a # second while pd loads. wm withdraw . puts -------------------------------pd-gui.tcl----------------------------------- package require Tcl 8.3 package require Tk package require Tk if {[tk windowingsystem] ne "win32"} {package require msgcat} # TODO figure out msgcat issue on Windows # Pd's packages are stored in the same directory as the main script (pd-gui.tcl) set auto_path [linsert $auto_path 0 [file dirname [info script]]] package require pd_connect package require pd_menus package require pd_bindings package require pdwindow package require dialog_array package require dialog_audio package require dialog_canvas package require dialog_font package require dialog_gatom package require dialog_iemgui package require dialog_midi package require pdtk_canvas package require pdtk_text # TODO eliminate this kludge: package require wheredoesthisgo # import into the global namespace for backwards compatibility namespace import ::pd_connect::pdsend namespace import ::pdwindow::pdtk_post namespace import ::dialog_array::pdtk_array_dialog namespace import ::dialog_audio::pdtk_audio_dialog namespace import ::dialog_canvas::pdtk_canvas_dialog namespace import ::dialog_font::pdtk_canvas_dofont namespace import ::dialog_gatom::pdtk_gatom_dialog namespace import ::dialog_iemgui::pdtk_iemgui_dialog namespace import ::dialog_midi::pdtk_midi_dialog namespace import ::dialog_midi::pdtk_alsa_midi_dialog # hack - these should be better handled in the C code namespace import ::dialog_array::pdtk_array_listview_new namespace import ::dialog_array::pdtk_array_listview_fillpage namespace import ::dialog_array::pdtk_array_listview_setpage namespace import ::dialog_array::pdtk_array_listview_closeWindow #------------------------------------------------------------------------------# # global variables set PD_MAJOR_VERSION 0 set PD_MINOR_VERSION 0 set PD_BUGFIX_VERSION 0 set PD_TEST_VERSION "" set TCL_MAJOR_VERSION 0 set TCL_MINOR_VERSION 0 set TCL_BUGFIX_VERSION 0 # for testing which platform we are running on ("aqua", "win32", or "x11") set windowingsystem "" # variable for vwait so that 'pd-gui' will timeout if 'pd' never shows up set wait4pd "init" # canvas font, received from pd in pdtk_pd_startup, set in s_main.c set font_family "courier" set font_weight "normal" # sizes of chars for each of the Pd fixed font sizes: # fontsize width(pixels) height(pixels) set font_fixed_metrics { 8 5 10 9 6 11 10 6 13 12 7 15 14 8 17 16 10 20 18 11 22 24 14 30 30 18 37 36 22 45 } # root path to lib of Pd's files, see s_main.c for more info set sys_libdir {} # root path where the pd-gui.tcl GUI script is located set sys_guidir {} set audioapi_list {} set midiapi_list {} set pd_whichapi 0 set pd_whichmidiapi 0 # current state of the DSP set dsp 0 # the toplevel window that currently is on top and has focus set focused_window . # TODO figure out how to get all windows into the menu_windowlist # store list of parent windows for Window menu set menu_windowlist {} # store that last 10 files that were opened set recentfiles_list {} set total_recentfiles 10 # keep track of the location of popup menu for CanvasWindows set popup_xpix 0 set popup_ypix 0 ## per toplevel/patch data # store editmode for each open canvas, starting with a blank array array set editmode {} #------------------------------------------------------------------------------# # coding style # # these are preliminary ideas, we'll change them as we work things out: # - when possible use "" doublequotes to delimit messages # - use '$::myvar' instead of 'global myvar' # - for the sake of clarity, there should not be any inline code, everything # should be in a proc that is ultimately triggered from main() # - if a menu_* proc opens a dialog panel, that proc is called menu_*_dialog # - use "eq/ne" for string comparison, NOT "==/!=" (http://wiki.tcl.tk/15323) # ## Names for Common Variables #---------------------------- # # variables named after the Tk widgets they represent # $mytoplevel = a window id made by a 'toplevel' command # $mygfxstub = a window id made by a 'toplevel' command via gfxstub/x_gui.c # $menubar = the 'menu' attached to each 'toplevel' # $mymenu = 'menu' attached to the menubar # $menuitem = 'menu' item # $mycanvas = 'canvas' # $canvasitem = 'canvas' item # # ## Prefix Names for procs #---------------------------- # pdtk_ pd -> pd-gui API (i.e. called from 'pd') # pdsend pd-gui -> pd API (sends a message to 'pd' using pdsend) # ------------------------------------------------------------------------------ # init functions proc set_pd_version {versionstring} { regexp -- {.*([0-9])\.([0-9]+)[\.\-]([0-9]+)([^0-9]?.*)} $versionstring \ wholematch \ ::PD_MAJOR_VERSION ::PD_MINOR_VERSION ::PD_BUGFIX_VERSION ::PD_TEST_VERSION } proc set_tcl_version {} { regexp {([0-9])\.([0-9])\.([0-9]+)} [info patchlevel] \ wholematch \ ::TCL_MAJOR_VERSION ::TCL_MINOR_VERSION ::TCL_BUGFIX_VERSION } # root paths to find Pd's files where they are installed proc set_pd_paths {} { set ::sys_guidir [file normalize [file dirname [info script]]] set ::sys_libdir [file normalize [file join $::sys_guidir ".."]] } proc init_for_platform {} { # we are not using Tk scaling, so fix it to 1 on all platforms. This # guarantees that patches will be pixel-exact on every platform tk scaling 1 switch -- $::windowingsystem { "x11" { # add control to show/hide hidden files in the open panel (load # the tk_getOpenFile dialog once, otherwise it will not work) catch {tk_getOpenFile -with-invalid-argument} set ::tk::dialog::file::showHiddenBtn 1 set ::tk::dialog::file::showHiddenVar 0 # set file types that open/save recognize set ::filetypes \ [list \ [list [_ "Associated Files"] {.pd .pat .mxt} ] \ [list [_ "Pd Files"] {.pd} ] \ [list [_ "Max Patch Files"] {.pat} ] \ [list [_ "Max Text Files"] {.mxt} ] \ ] } "aqua" { # set file types that open/save recognize set ::filetypes \ [list \ [list [_ "Associated Files"] {.pd .pat .mxt} ] \ [list [_ "Pd Files"] {.pd} ] \ [list [_ "Max Patch Files (.pat)"] {.pat} ] \ [list [_ "Max Text Files (.mxt)"] {.mxt} ] \ ] } "win32" { font create menufont -family Tahoma -size -11 # set file types that open/save recognize set ::filetypes \ [list \ [list [_ "Associated Files"] {.pd .pat .mxt} ] \ [list [_ "Pd Files"] {.pd} ] \ [list [_ "Max Patch Files"] {.pat} ] \ [list [_ "Max Text Files"] {.mxt} ] \ ] } } } # ------------------------------------------------------------------------------ # locale handling # official GNU gettext msgcat shortcut if {[tk windowingsystem] ne "win32"} { proc _ {s} {return [::msgcat::mc $s]} } else { proc _ {s} {return $s} } proc load_locale {} { if {[tk windowingsystem] ne "win32"} { ::msgcat::mcload [file join [file dirname [info script]] .. po] } # for Windows #set locale "en" ;# Use whatever is right for your app #if {[catch {package require registry}]} { # tk_messageBox -icon error -message "Could not get locale from registry" #} else { # set locale [string tolower \ # [string range \ # [registry get {HKEY_CURRENT_USER\Control Panel\International} sLanguage] 0 1] ] #} ##--moo: force default system and stdio encoding to UTF-8 encoding system utf-8 fconfigure stderr -encoding utf-8 fconfigure stdout -encoding utf-8 ##--/moo } # ------------------------------------------------------------------------------ # font handling # this proc gets the internal font name associated with each size proc get_font_for_size {size} { return "::pd_font_${size}" } # searches for a font to use as the default. Tk automatically assigns a # monospace font to the name "Courier" (see Tk 'font' docs), but it doesn't # always do a good job of choosing in respect to Pd's needs. So this chooses # from a list of fonts that are known to work well with Pd. proc find_default_font {} { set testfonts {Inconsolata "Courier New" "Liberation Mono" FreeMono \ "DejaVu Sans Mono" "Bitstream Vera Sans Mono"} foreach family $testfonts { if {[lsearch -exact -nocase [font families] $family] > -1} { set ::font_family $family break } } puts "DEFAULT FONT: $::font_family" } proc set_base_font {family weight} { if {[lsearch -exact [font families] $family] > -1} { set ::font_family $family } else { pdtk_post [format \ [_ "WARNING: Font family '%s' not found, using default (%s)"] \ $family $::font_family] } if {[lsearch -exact {bold normal} $weight] > -1} { set ::font_weight $weight set using_defaults 0 } else { pdtk_post [format \ [_ "WARNING: Font weight '%s' not found, using default (%s)"] \ $weight $::font_weight] } } # creates all the base fonts (i.e. pd_font_8 thru pd_font_36) so that they fit # into the metrics given by $::font_fixed_metrics for any given font/weight proc fit_font_into_metrics {} { # TODO the fonts picked seem too small, probably on fixed width foreach {size width height} $::font_fixed_metrics { set myfont [get_font_for_size $size] font create $myfont -family $::font_family -weight $::font_weight \ -size [expr {-$height}] set height2 $height set giveup 0 while {[font measure $myfont M] > $width} { incr height2 -1 font configure $myfont -size [expr {-$height2}] if {$height2 * 2 <= $height} { set giveup 1 break } } if {$giveup} { pdtk_post [format \ [_ "ERROR: %s failed to find font size (%s) that fits into %sx%s!"]\ [lindex [info level 0] 0] $size $width $height] continue } } } # ------------------------------------------------------------------------------ # procs called directly by pd # this is only called when 'pd' starts 'pd-gui', not the other way around proc pdtk_pd_startup {versionstring audio_apis midi_apis sys_font sys_fontweight} { # pdtk_post "-------------- pdtk_pd_startup ----------------" # pdtk_post "version: $versionstring" # pdtk_post "audio_apis: $audio_apis" # pdtk_post "midi_apis: $midi_apis" # pdtk_post "sys_font: $sys_font" # pdtk_post "sys_fontweight: $sys_fontweight" set oldtclversion 0 pdsend "pd init [enquote_path [pwd]] $oldtclversion $::font_fixed_metrics" set_pd_version $versionstring set ::audioapi_list $audio_apis set ::midiapi_list $midi_apis if {$::tcl_version >= 8.5} {find_default_font} set_base_font $sys_font $sys_fontweight fit_font_into_metrics # TODO what else is needed from the original? set ::wait4pd "started" } ##### routine to ask user if OK and, if so, send a message on to Pd ###### # TODO add 'mytoplevel' once merged to 0.43, with -parent proc pdtk_check {message reply_to_pd default} { # TODO this should use -parent and -title, but the hard part is figuring # out how to get the values for those without changing g_editor.c set answer [tk_messageBox -type yesno -icon question -default $default \ -message [_ $message]] if {$answer eq "yes"} { pdsend $reply_to_pd } } proc pdtk_fixwindowmenu {} { # TODO canvas_updatewindowlist() sets up the menu_windowlist with all of # the parent CanvasWindows, we should then use [wm stackorder .] to get # the rest of the CanvasWindows to make sure that all CanvasWindows are in # the menu. This would probably be better handled on the C side of # things, since then, the menu_windowlist could be built with the proper # parent/child relationships. # pdtk_post "Running pdtk_fixwindowmenu" } # ------------------------------------------------------------------------------ # X11 procs for handling singleton state and getting args from other instances # first instance proc singleton {key} { if {![catch { selection get -selection $key }]} { return 0 } selection handle -selection $key . "singleton_request" selection own -command first_lost -selection $key . return 1 } proc singleton_request {offset maxbytes} { wm deiconify .pdwindow raise .pdwindow return [tk appname] } proc first_lost {} { receive_args [selection get -selection PUREDATA] selection own -command first_lost -selection PUREDATA . } # all other instances proc send_args {offset maxChars} { return [string range $::argv $offset [expr {$offset+$maxChars}]] } proc others_lost {} { set ::singleton_state "exit" destroy . exit } # ------------------------------------------------------------------------------ # various startup related procs proc check_for_running_instances {argc argv} { # pdtk_post "check_for_running_instances $argc $argv" switch -- $::windowingsystem { "aqua" { # handled by ::tk::mac::OpenDocument in apple_events.tcl } "x11" { # http://wiki.tcl.tk/1558 if {![singleton PUREDATA_MANAGER]} { # other instances called by wish/pd-gui (exempt 'pd' by 5400 arg) if {$argc == 1 && [string is int $argv] && $argv >= 5400} {return} selection handle -selection PUREDATA . "send_args" selection own -command others_lost -selection PUREDATA . after 5000 set ::singleton_state "timeout" vwait ::singleton_state exit } else { # first instance selection own -command first_lost -selection PUREDATA . } } "win32" { ## http://wiki.tcl.tk/1558 # TODO on Win: http://tcl.tk/man/tcl8.4/TclCmd/dde.htm } } } # this command will open files received from a 2nd instance of Pd proc receive_args args { # pdtk_post "receive_files $args" raise . foreach filename $args { open_file $filename } } proc load_startup {} { global errorInfo # TODO search all paths for startup.tcl set startupdir [file normalize "$::sys_libdir/startup"] # pdtk_post "load_startup $startupdir" puts stderr "load_startup $startupdir" if { ! [file isdirectory $startupdir]} { return } foreach filename [glob -directory $startupdir -nocomplain -types {f} -- *.tcl] { puts "Loading $filename" set tclfile [open $filename] set tclcode [read $tclfile] close $tclfile if {[catch {uplevel #0 $tclcode} errorname]} { puts stderr "------------------------------------------------------" puts stderr "UNHANDLED ERROR: $errorInfo" puts stderr "FAILED TO LOAD $filename" puts stderr "------------------------------------------------------" } } } # ------------------------------------------------------------------------------ # main proc main {argc argv} { # TODO Tcl/Tk 8.3 doesn't have [tk windowingsystem] set ::windowingsystem [tk windowingsystem] tk appname pd-gui load_locale check_for_running_instances $argc $argv set_pd_paths init_for_platform # post_tclinfo # set a timeout for how long 'pd-gui' should wait for 'pd' to start after 20000 set ::wait4pd "timeout" # TODO check args for -stderr and set pdtk_post accordingly if {$argc == 1 && [string is int $argv] && $argv >= 5400} { # 'pd' started first and launched us, so get the port to connect to ::pd_connect::to_pd [lindex $argv 0] } else { # the GUI is starting first, so create socket and exec 'pd' set portnumber [::pd_connect::create_socket] set pd_exec [file join [file dirname [info script]] ../bin/pd] exec -- $pd_exec -guiport $portnumber & } # wait for 'pd' to call pdtk_pd_startup, or exit on timeout vwait ::wait4pd if {$::wait4pd eq "timeout"} { puts stderr [_ "ERROR: 'pd' never showed up, 'pd-gui' quitting!"] exit 2 } ::pd_bindings::class_bindings ::pd_menus::create_menubar ::pdtk_canvas::create_popup ::pdwindow::create_window ::pd_menus::configure_for_pdwindow load_startup # pdtk_post "------------------ done with main ----------------------" } main $::argc $::argv