Tkgetfile - An enhanced FileOpen browser replacement for tk_getOpenFile
# # tkgetfile.tcl -- Enhanced file selector using tablelist widget (to provide # detail view and easy access to sorting and column sizing). # Intended as a replacement for tk_getOpenFile. # # Features a detailed view of directories with Name, Size, and Date Modified. # The user can click on any column heading to sort that column in ascending or # descending order. # Input a file name with wildcards (e.g. *.c) to see all files that match. # You can use the Tab keys, up-down arrows (depending on focus), Return, etc. # # Author: Walter B. Wulczak <[email protected]> # Date: Oct 10 2005 # # Requires the package "tablelist" ( http://www.nemethi.de ) # Tested with Tcl/Tk 8.1 and 8.4 under Unix. # # Usage: # set filename [tkgetfile ?-option value? ] # if {$filename != ""} { # # Open the file and do other stuff ... # } # # Options are: # [-initialdir dir] Specifies that the files in dir should be # displayed when the dialog pops up. Defaults # to the current working directory. # [-parent window] Display tkgetfile over parent window. # [-title string] Make string the label of dialog window. # # Layout: # # Open File: # +------------------+ +-----+ # | | +UpDir+ # +------------------+ +-----+ # # Name Size Date Modified # +------------------------------------+ # | file1 25 2005-01-24 19:11:55 |S # | file2 |c # | file3 |r # | |b # | |a # | filen |r # +------------------------------------+ # Directory Name: currrent-dir # # +------+ +--------+ # | OK | | cancel | # +------+ +--------+ # ############################################################### # # Thanks to: # Csaba Nemethi, author of tablelist, for helpful suggestions. # # 10-04-05 wbw: Replaced listbox with tablelist. Dir folder image added. # Corrected key and button binding definitions. # 10-05-05 wbw: Recoded 3 sections doing the same thing into tkgetfileshowdir. # Updir button added. # 10-06-05 wbw: Work around glob quirk. The following returns no files: # set selected "*.c *.tcl" # set globlist [glob $selected] # or # set globlist [glob [list $selected]] # This works (returns files matching either pattern): # set selected "*.c *.tcl" # set globlist [eval glob $selected] # 10-06-05 wbw: Every time tkgetfileshowdir is called, sort the list per the # last known sorting order. # 10-07-05 wbw: File names with spaces now handled (removed "glob" call). # Catch exceptions when user clicks beyond the table. # 10-08-05 wbw: Added support for "-initialdir" and "-title". # 10-09-05 wbw: Switched from "ls -a" to "glob". Bindings corrected. # 10-10-05 wbw: Cleanup and comment out "puts" debugging statements. # # # # # # Copyright 2005 Walter B. Wulczak # Permission to use, copy, modify, and distribute this # software and its documentation for any purpose and without # fee is hereby granted, provided that this copyright # notice appears in all copies. Walter B. Wulczak # makes no representations about the suitability of this # software for any purpose. It is provided "as is" without # express or implied warranty. # # Based in part on fileselect.tcl by: # Mario Jorge Silva [email protected] # University of California Berkeley # # Parts Copyright 1993 Regents of the University of California # Permission to use, copy, modify, and distribute this # software and its documentation for any purpose and without # fee is hereby granted, provided that this copyright # notice appears in all copies. The University of California # makes no representations about the suitability of this # software for any purpose. It is provided "as is" without # express or implied warranty. # package require tablelist proc tkgetfile {args} { global tkgetfile_selected set tkgetfile_selected "-Cancelled-" global tkgetfilemsg set tkgetfilemsg(title) "Select File" set tkgetfilemsg(parent) "" # # arguments # set index 0 set max [llength $args] while { $index < $max } { switch -exact -- [lindex $args $index] { "-initialdir" { incr index cd [lindex $args $index] incr index } "-parent" { incr index set tkgetfilemsg(parent) [lindex $args $index] incr index } "-title" { incr index set tkgetfilemsg(title) [lindex $args $index] incr index } default { puts stderr "Unsupported option [lindex $args $index]" } } } proc setfilename {f} { global tkgetfile_selected set tkgetfile_selected $f # puts stderr "tkgetfile.tcl result is: $f" } tkgetfileINT setfilename "Open File" .openFile # pick one of these 2 lines; you only need the one you like most here. # tkwait variable tkgetfile_selected tkwait window .openFile if { [string compare $tkgetfile_selected "-Cancelled-"] == 0 } then { # puts stderr "Selection cancelled" return "" } # Return full path name if {[regexp "/" $tkgetfile_selected] != 0} { return $tkgetfile_selected} return [pwd]/$tkgetfile_selected } # Names starting with "tkgetfile" are reserved by this module # this is the default proc called when "OK" is pressed # to indicate yours, give it as the first arg to "tkgetfileINT" proc tkgetfile.default.cmd {f} { puts stderr "Selected file $f" } image create photo b_up -data { R0lGODlhFgATAMIAAHt7e9/fX////gAAAK6uSv///////////yH+Dk1hZGUgd2l0aCBHSU1QACH5 BAEAAAcALAAAAAAWABMAAANVeArcoDBKEKoNT2p6b9ZLJzrkAQhoqq4qMJxi3LnwRcjeK9jDjWM6 C2FA9Mlou8CQWMQhO4Nf5XmJSqkW6w9bYXqZFq40HBzPymYyac1uDA7fuJyZAAA7 } image create photo b_dir -data { R0lGODlhEAAQAMIAAHB/cN/fX////gAAAP///////////////yH+Dk1hZGUgd2l0aCBHSU1QACH5 BAEAAAQALAAAAAAQABAAAAM2SLrc/jA2QKkEIWcAsdZVpQBCaZ4lMBDk525r+34qK8x0fOOwzfcy Xi2IG4aOoRVhwGw6nYQEADs= } # this is the proc that creates the file selector box proc tkgetfileINT { {cmd tkgetfile.default.cmd} {purpose "Open file:"} {w .tkgetfileWindow} } { global tkgetfilemsg catch {destroy $w} toplevel $w grab $w # wm title $w "Select File" wm title $w $tkgetfilemsg(title) if {$tkgetfilemsg(parent) != ""} { set par $tkgetfilemsg(parent) set xOrgWin [expr [winfo rootx $par] + [winfo width $par] / 2 -200] set yOrgWin [expr [winfo rooty $par] + [winfo height $par] / 2 -200] wm geometry $w +$xOrgWin+$yOrgWin wm transient $w $tkgetfilemsg(parent) } # path independent names for the widgets global tkgetfile set tkgetfile(entry) $w.file.eframe.entry set tkgetfile(list) $w.file.sframe.list set tkgetfile(scroll) $w.file.sframe.scroll set tkgetfile(ok) $w.bframe.okframe.ok set tkgetfile(cancel) $w.bframe.cancel set tkgetfile(dirlabel) $w.file.dirlabel # widgets frame $w.file -bd 5 frame $w.bframe -bd 2 pack append $w \ $w.file {top expand filly} \ $w.bframe {top frame n} # $w.bframe {left expand frame n} frame $w.file.eframe frame $w.file.sframe # label $w.file.dirlabel -anchor w -width 40 -text "Directory Name: [pwd]" label $w.file.dirlabel -anchor w -text "Directory Name: [pwd]" pack append $w.file \ $w.file.eframe {top frame w} \ $w.file.sframe {top expand fillx filly} \ $w.file.dirlabel {top frame w} label $w.file.eframe.label -anchor w -width 40 -text $purpose entry $w.file.eframe.entry -relief sunken -background white button $w.file.eframe.up -image b_up -command "tkgetfileshowdir .." pack append $w.file.eframe \ $w.file.eframe.label {top expand frame w} \ $w.file.eframe.up {right frame e} \ $w.file.eframe.entry {top fillx frame w} scrollbar $w.file.sframe.yscroll -relief sunken \ -command "$w.file.sframe.list yview" # listbox $w.file.sframe.list -relief sunken \ # -yscroll "$w.file.sframe.yscroll set" -selectmode single -width 40 \ # -background white tablelist::tablelist $w.file.sframe.list -columns {0 "Name" 0 "Size" right 0 "Date Modified" } \ -stretch all -background white -width 0 \ -yscrollcommand [list $w.file.sframe.yscroll set] \ -stripebackground #f0f0f0 \ -labelcommand tablelist::sortByColumn \ -font "-*-helvetica-medium-r-normal-*-12-*-*-*-p-*-iso8859-1" \ -activestyle frame $w.file.sframe.list columnconfigure 1 -name fileSize -sortmode integer $w.file.sframe.list columnconfigure 0 -editable 0 $w.file.sframe.list columnconfigure 1 -editable 0 $w.file.sframe.list columnconfigure 2 -editable 0 pack append $w.file.sframe \ $w.file.sframe.yscroll {right filly} \ $w.file.sframe.list {left expand fill} # buttons # frame $w.bframe.okframe -borderwidth 2 -relief sunken frame $w.bframe.okframe -borderwidth 2 -relief flat button $w.bframe.okframe.ok -text OK -relief raised -padx 20 \ -command "tkgetfile.ok.cmd $w $cmd" button $w.bframe.cancel -text cancel -relief raised -padx 10 \ -command "tkgetfile.cancel.cmd $w" pack append $w.bframe.okframe $w.bframe.okframe.ok {padx 1 pady 1} pack append $w.bframe $w.bframe.okframe {left expand padx 2 pady 2}\ $w.bframe.cancel {left} # Fill the listbox with a list of the files in the directory tkgetfileshowdir [pwd] #--------------------------------------- # Set up bindings for the browser. bind $tkgetfile(entry) <Return> {eval $tkgetfile(ok) invoke} bind $tkgetfile(ok) <Return> {eval $tkgetfile(ok) invoke} bind $tkgetfile(entry) <Control-c> {eval $tkgetfile(cancel) invoke} bind $w <Control-c> {eval $tkgetfile(cancel) invoke} # 10-05-05 wbw: Don't see a good reason for the next line as it causes # a doubling up of the <Return> invokation above. # bind $w <Return> {eval $tkgetfile(ok) invoke} # tk_listboxSingleSelect $tkgetfile(list) set bodyTag [$tkgetfile(list) bodytag] bind $bodyTag <Button-1> { # puts stderr "button 1 release" foreach {tablelist::W tablelist::x tablelist::y} \ [tablelist::convEventFields %W %x %y] {} set clickcell [$tkgetfile(list) nearest $tablelist::y] # puts stderr "Nearest clicked on cell $clickcell" # A button click in an invalid area could exceed the tablelist if { [$tkgetfile(list) index end] > $clickcell } { $tkgetfile(entry) delete 0 end # Get the cell at 0,y (beginning cell of the selected line) $tkgetfile(entry) insert 0 [$tkgetfile(list) getcells [$tkgetfile(list) nearest $tablelist::y],0] } focus $tkgetfile(list) } # 10-05-05 wbw: Binding must be KeyRelease as tablelist widget first does # selection setup via its own Key binding. bind $bodyTag <KeyRelease> { foreach {tablelist::W tablelist::x tablelist::y} \ [tablelist::convEventFields %W %x %y] {} $tkgetfile(entry) delete 0 end set currow [$tkgetfile(list) curselection] # puts stderr "currow $currow [$tkgetfile(list) getcells active]" $tkgetfile(entry) insert 0 [$tkgetfile(list) getcells active] } bind $bodyTag <Double-ButtonPress-1> { # puts stderr "double button 1" foreach {tablelist::W tablelist::x tablelist::y} \ [tablelist::convEventFields %W %x %y] {} # set clickcell [$tkgetfile(list) getscells [$tkgetfile(list) containingcell 0 $tablelist::y]] set clickcell [$tkgetfile(list) nearest $tablelist::y] # puts stderr "Converted double click on cell $clickcell" if { [$tkgetfile(list) index end] > $clickcell } { $tkgetfile(entry) delete 0 end # Get the cell at 0,y (beginning cell of the selected line) $tkgetfile(entry) insert 0 [$tkgetfile(list) getcells [$tkgetfile(list) nearest $tablelist::y],0] $tkgetfile(ok) invoke } } # This binding creates a conflict with tablelist::condEditActiveCell due # to the window being destroyed before condEditActiveCell is called! if 0 { bind $bodyTag <Return> { $tkgetfile(entry) delete 0 end $tkgetfile(entry) insert 0 [$tkgetfile(list) getcells active] $tkgetfile(ok) invoke } } # set kbd focus to list widget, not entry widget focus $tkgetfile(entry) # focus $tkgetfile(list) } # auxiliary button procedures proc tkgetfile.cancel.cmd {w} { # puts stderr "Cancel" destroy $w } proc tkgetfile.ok.cmd {w cmd} { global tkgetfile set selected [$tkgetfile(entry) get] # puts stderr "The tkgetfile.ok.cmd selection is: $selected" if [file isfile "$selected"] { # after 5 destroy $w destroy $w $cmd $selected return } if { [string compare $selected "" ] == 0} { # puts stderr "tkgetfile.ok.cmd received blank selection" return } # selection may be a directory. Expand it. if {[file isdirectory "$selected"] != 0} { tkgetfileshowdir $selected return } # some nasty file names may cause "file isdirectory" to return an error set sts [catch { file isdirectory $selected } errorMessage ] if { $sts != 0 } then { tk_dialog .oops "STS directory error" "Filename directory test return error: $errorMessage" error 0 OK return } # perform globbing on the selection. # If globing returns an error, return (leaving the file listbox empty) # If resulting list length > 1, put the list on the file listbox and return # If globing expands to a list of filenames in multiple directories, # the indicated regexp is invalid and the error handler is called instead. set sts [catch { set globlist [eval glob $selected] # puts stderr "globlist: $globlist" } errorMessage ] if { $sts != 0 } then { tk_dialog .oops "STS error" "Error: $errorMessage" error 0 OK return } # handle wildcard filenames (e.g. *.txt, *.c, etc.) # if {[llength $globlist] > 1} if {$globlist != $selected} { if {[regexp "/" $globlist] != 0} { tk_dialog .oops "regexp error" "Invalid regular expression (don't mix '/' with wildcards): $selected" error 0 OK return } tkgetfileshowdir $selected return } if [file isfile "$selected"] { destroy $w $cmd $selected } else { tk_dialog .oops "Invalid File Name" "You didn't choose anything" error 0 OK return } } proc tkgetfileshowdir {dirpath} { # Fill tablelist with a list of the files in the directory (with glob). global tkgetfile # puts stderr "tkgetfileshowdir $dirpath" if {[file isdirectory $dirpath] != 0} { cd $dirpath set dirpath [pwd] $tkgetfile(dirlabel) configure -text "Directory Name: $dirpath" # puts stderr "Expanding directory $dirpath" set dirpath ".* *" } # Clean the text entry and prepare the list $tkgetfile(entry) delete 0 end $tkgetfile(list) delete 0 end # set globlist [lsort [eval glob $dirpath]] # if {[llength $globlist] <= 1} # if {$globlist == $dirpath} { # set globlist [ exec /bin/ls -a $dirpath] # } # set globlist [ exec sh -c "/bin/ls -a $dirpath"] # puts stderr $globlist # foreach i [exec /bin/ls -a $dirpath] # foreach i [split $globlist \n] # foreach i [lsort [eval glob [file join $dirpath *]]] foreach i [lsort [eval glob -nocomplain $dirpath]] { if {[string compare $i "."] != 0 && \ [string compare $i ".."] != 0 } { set fileSize [file size $i] set dttm [clock format [file mtime $i] -format "%Y-%m-%d %H:%M:%S" ] $tkgetfile(list) insert end [list $i $fileSize $dttm] if {[file isdirectory $i] != 0} { $tkgetfile(list) cellconfigure end,0 -image b_dir } } } # Sort the list per the last user-specified sorting order. set sortcol [$tkgetfile(list) sortcolumn] if {$sortcol != -1 } { $tkgetfile(list) sortbycolumn $sortcol -[$tkgetfile(list) sortorder ] } }