Search plugin revision:
*added status bar shows link locations and search text (if searching for a 
keyword tag)

* quoted text works, e.g., "it's a secret to everybody"
* regexes seem to work, e.g., "outlet.*symbol" will match all objects that 
output a symbol
* pd-style word boundaries, e.g., "clip~" works when "whole words" option is 
checked
* font +/- with ctrl-plus/ctrl-minus keys

-Jonathan
# plugin to allow searching all the documentation using a regexp
# check the Help menu for the Search item to use it

# Bugs:
# tiny text in combobox dropdown menu on Windows
# can't interrupt long searches on Windows (never get them in Fedora 15)
# Todo:
# try to clean up user input prevent regex error messages

package require Tk 8.5
package require pd_bindings
package require pd_menucommands

namespace eval ::dialog_search:: {
    variable searchtext {}
    variable search_history {}
    variable count {}
    variable genres [list [_ "All documents"] \
			[_ "Object Help Patches"] \
			[_ "All About Pd"] \
			[_ "Tutorials"] \
			[_ "Manual"] \
			[_ "Uncategorized"]
		]
}

# TODO check line formatting options

# find_doc_files
# basedir - the directory to start looking in
proc ::dialog_search::find_doc_files { basedir } {
    # Fix the directory name, this ensures the directory name is in the
    # native format for the platform and contains a final directory seperator
    set basedir [string trimright [file join $basedir { }]]
    set fileList {}

    # Look in the current directory for matching files, -type {f r}
    # means ony readable normal files are looked at, -nocomplain stops
    # an error being thrown if the returned list is empty
    foreach fileName [glob -nocomplain -type {f r} -path $basedir $helpbrowser::doctypes] {
        lappend fileList $fileName
    }

    # Now look for any sub direcories in the current directory
    foreach dirName [glob -nocomplain -type {d  r} -path $basedir *] {
        # Recusively call the routine on the sub directory
	# (if it's not already in Pd's search path) and
	# append any new files to the results
	set nomatch [lsearch [concat [file join $::sys_libdir doc] $::sys_searchpath $::sys_staticpath] $dirName]
	if { $nomatch eq "-1" } {
            set subDirList [find_doc_files $dirName]
            if { [llength $subDirList] > 0 } {
                foreach subDirFile $subDirList {
                    lappend fileList $subDirFile
                }
            }
	}
    }
    return $fileList
}

# TODO: break up into: l
proc ::dialog_search::open_file { xpos ypos mytoplevel clicked } {
    set textwidget "$mytoplevel.resultstext"
    set i [$textwidget index @$xpos,$ypos]
    set range [$textwidget tag nextrange filename $i]
    set filename [eval $textwidget get $range]
    set range [$textwidget tag nextrange basedir $i]
    set basedir [eval $textwidget get $range]
    append basedir "/"
    if {$clicked eq "1"} {
        if {$filename ne ""} {
    	    menu_doc_open $basedir $filename
        }
    } else {
        $mytoplevel.statusbar configure -text "Open $basedir$filename"
    }
}

# only does keywords for now-- maybe expand this to handle any meta tags
proc ::dialog_search::grab_metavalue { xpos ypos mytoplevel clicked } {
    set textwidget "$mytoplevel.resultstext"
#    set xpos_offset 20 
#    set xpos [expr {$xpos + $xpos_offset}]
    set i [$textwidget index @$xpos,$ypos]
    set range [$textwidget tag nextrange metavalue_h $i]
    set value [eval $textwidget get $range]
    set text {keywords.*}
    append text $value
    if {$clicked eq "1"} {
        set ::dialog_search::searchtext ""
        set ::dialog_search::searchtext $text
        ::dialog_search::search
    } else {
        $mytoplevel.statusbar configure -text $text
    }
}

# show/hide results based on genre
proc ::dialog_search::filter_results { combobox text } {
    variable genres
    set elide {}
    if { [$combobox current] eq "0" } {
    	foreach genre $genres {
    	    $text tag configure [join $genre "_"] -elide off
    	    set tag [join $genre "_"]
    	    append tag "_count"
    	    $text tag configure $tag -elide on
	}
	set tag [join [lindex $genres 0] "_"]
	append tag "_count"
	$text tag configure $tag -elide off
    } else {
    	foreach genre $genres {
    	    if { [$combobox get] ne $genre } {
    		$text tag configure [join $genre "_"] -elide on
    		set tag [join $genre "_"]
    		append tag "_count"
    		$text tag configure $tag -elide on
    	    } else {
    		$text tag configure [join $genre "_"] -elide off
    		set tag [join $genre "_"]
    		append tag "_count"
    		$text tag configure $tag -elide off
    	    }
    	}
    }
    $combobox selection clear
    focus $text
}

proc ::dialog_search::readfile {filename} {
    set fp [open $filename]
    set file_contents [split [read $fp] \n]
    close $fp
    return $file_contents
}

proc ::dialog_search::search { } {
    variable searchtext
    variable search_history
    if {$searchtext eq ""} return
    if { [lsearch $search_history $searchtext] eq "-1" } {
    	lappend search_history $searchtext
    	.search.searchtextentry configure -values $search_history
    }
    .search.searchtextentry selection clear
    .search.searchtextentry configure -foreground gray -background gray90
    .search.resultstext configure -state normal
    .search.resultstext delete 0.0 end
    update idletasks
    do_search
    # BUG? the above might cause weird bugs, so consider http://wiki.tcl.tk/1255
    # and http://wiki.tcl.tk/1526
    # after idle [list after 10 ::dialog_search::do_search]
}

proc ::dialog_search::do_search { } {
    variable searchtext
    variable count
    variable genres
    set count {}
    foreach genre $genres {
    	lappend count "0"
    }
    set widget .search.resultstext
    # Get rid of pesky leading/trailing spaces...
    set formatted_searchtext [string trim $searchtext]
    # ...and double spaces between terms
    regsub -all {\s+} $formatted_searchtext { } formatted_searchtext
    foreach basedir [concat [file join $::sys_libdir doc] $::sys_searchpath $::sys_staticpath] {
        # Fix the directory name, this ensures the directory name is in the
        # native format for the platform and contains a final directory seperator
        set basedir [file normalize $basedir]
        foreach docfile [find_doc_files $basedir] {
            searchfile $formatted_searchtext [readfile $docfile] $widget \
                [string replace $docfile 0 [string length $basedir]] $basedir
        }
    }
    .search.searchtextentry configure -foreground black -background white
    $widget insert 0.0 " matching docs.\n"
    set i 0
    foreach genre $genres {
    		set tag [join $genre "_"]
    		append tag "_count"
    		$widget insert 0.0 [lindex $count $i] $tag
    		incr i
    }
    $widget insert 0.0 "       Found "
    $widget insert 0.0 "Home" "link intro"
    $widget configure -state disabled
}

proc ::dialog_search::searchfile {searchtext file_contents widget filename basedir} {
    variable count
    variable genres
    set match 0
    set description ""
    set keywords ""
    set genre ""
    set metadata ""
    set title ""
    set terms_to_match 1
    if { $::dialog_search::matchall == 1 } {
	set terms_to_match [llength $searchtext]
    }
    if {[regexp -nocase -- ".*-help\.pd" $filename]} {
    	# object help
    	set genre 1
    	regsub -nocase -- "(^.*)(5.reference/)(.*)-help.pd" $filename {\1\3} title
    } elseif {[regexp -nocase -- "all_about_.*\.pd" $filename]} {
	regsub -nocase -- ".*all_about_(.)(.*)\.pd" $filename {\1\2} title
	regsub -all -- "_" $title " " title
	# all about pd
	set genre 2
    } elseif {[regexp -nocase -- "\.html?" $filename]} {
    	set title $filename
    	# Pd Manual (or some html page in the docs)
    	set genre 4
    } else {
    	set title $filename
    }
    set wb {[\s,\";\.-]}
    foreach term $searchtext {
	set parsefile 1
	if { $::dialog_search::matchwords == 1 } {
	    if {[regexp -nocase -- \
		[join [list $wb $term $wb] ""] $filename] ||
		[regexp -nocase -- $term \
		    [regsub {.*[\/\\](.*)$} $filename {\1}]]} {
		incr match
		set parsefile 0
	    }
	} else {
	    if {[regexp -nocase -- "$term" $filename]} {
		incr match
		set parsefile 0
	    }
	}
	if { $parsefile } {
	    foreach line $file_contents {
		if { $::dialog_search::matchwords == 1 } {
    		    if {[regexp -nocase -- [join [list $wb $term $wb] ""] $line]} {
    			incr match
    			break
    		    }
		} else {
    		    if {[regexp -nocase -- "$term" $line]} {
    			incr match
    			break
    		    }
		}
	    }
	}
    }
    if { $match >= $terms_to_match } {
        set len [llength [split $searchtext]]
    	regexp -nocase -- "#X text \[0-9\]+ \[0-9\]+ description\[:\]? (.*?);.*" $file_contents -> description
    	regsub -all {[{}\\]} $description {} description
    	regexp -nocase -- "#X text \[0-9\]+ \[0-9\]+ keywords\[:\]? (.*?);.*" $file_contents -> keywords
    	regsub -all {[{}]} $keywords {} keywords
    	if {[regexp -nocase -- "#X text \[0-9\]+ \[0-9\]+ genre tutorial" $file_contents]} {
    	    set genre 3
    	}
    	if { $genre eq "" } {
    	    set genre 5
    	}
    	lset count $genre [expr [lindex $count $genre] + 1]
    	set genre [join [lindex $genres $genre] "_"]
    	lset count 0 [expr [lindex $count 0] + 1]    		
    	# print out the match results
    	$widget insert end "$title" "title link $genre"
    	$widget insert end "$filename" filename
    	$widget insert end "$basedir" basedir
    	if { $description eq "" } {
    	    set description "No DESCRIPTION tag."
    	}
    	$widget insert end "\n$description\n" "description $genre"
    	if { $keywords ne "" } {
    	    $widget insert end "Keywords:" "keywords $genre"
	    set i 0
    	    foreach value $keywords {
		set metavalue "metavalue$i"
		incr i
    		$widget insert end " " link
    		$widget insert end $value "$metavalue keywords link $genre"
                # have to make an elided copy for use with "nextrange"
                # since you can't just get the tag's index underneath
                # the damn cursor!!!
                $widget insert end $value metavalue_h
    	    }
    	$widget insert end "\n" $genre
    	}
    }
}

proc ::dialog_search::ok {mytoplevel} {
    # this is a placeholder for the standard dialog bindings
}

proc ::dialog_search::cancel {mytoplevel} {
    wm withdraw .search
}

proc ::dialog_search::open_search_dialog {mytoplevel} {
    if {[winfo exists $mytoplevel]} {
        wm deiconify $mytoplevel
        raise $mytoplevel
    } else {
        create_dialog $mytoplevel
    }
}

# hack to select all because tk's default bindings apparently
# assume the user is going to want emacs shortcuts to write a
# 1980s program inside a ttk::entry widget !?!?!?!?
proc ::dialog_search::sa { widget } {
    $widget selection range 0 end
    break
}

proc ::dialog_search::intro { t } {
    $t configure -state normal
    $t delete 0.0 end
    $t insert end "Pure Data Documentation Search\n\n" homepage_title
    $t insert end "Enter search terms above and " homepage_description
    $t insert end "use the dropdown menu to choose a category. " homepage_description
    $t insert end "You can also click on one of the tags below " homepage_description
    $t insert end "to start your search.\n\n" homepage_description
 
    $t insert end "Keywords\n" homepage_title
 
    $t insert end "abstraction" "metavalue1 link keywords"
    $t insert end "abstraction" metavalue_h
    $t insert end " object itself is written in Pure Data\n" def

    $t insert end "abstraction_op" "metavalue2 link keywords"
    $t insert end "abstraction_op" metavalue_h
    $t insert end " object that only makes sense in terms of" def
    $t insert end " abstractions\n" def

    $t insert end "analysis" "metavalue3 link keywords"
    $t insert end "analysis" metavalue_h
    $t insert end " object that does analysis\n" def

    $t insert end "anything_op" "metavalue4 link keywords"
    $t insert end "anything_op" metavalue_h
    $t insert end " store or manipulate an anything\n" def

    $t insert end "array" "metavalue5 link keywords"
    $t insert end "array" metavalue_h
    $t insert end " objects for creating and editing arrays\n" def

    $t insert end "bandlimited" "metavalue6 link keywords"
    $t insert end "bandlimited" metavalue_h
    $t insert end " objects that describe themselves as being" def
    $t insert end "bandlimited\n" def

    $t insert end "block_oriented" "metavalue7 link keywords"
    $t insert end "block_oriented" metavalue_h
    $t insert end " see Matju's definition\n" def

    $t insert end "canvas_op" "metavalue8 link keywords"
    $t insert end "canvas_op" metavalue_h
    $t insert end " object whose behavior only makes sense in terms " def
    $t insert end "of a canvas\n" def

    $t insert end "control" "metavalue9 link keywords"
    $t insert end "control" metavalue_h
    $t insert end " control rate objects\n" def

    $t insert end "conversion" "metavalue1 link keywords"
    $t insert end "conversion" metavalue_h
    $t insert end " convert from one set of units to another\n" def

    $t insert end "data_structure" "metavalue2 link keywords"
    $t insert end "data_structure" metavalue_h
    $t insert end " objects for creating and managing data structures\n" def

    $t insert end "filter" "metavalue3 link keywords"
    $t insert end "filter" metavalue_h
    $t insert end " object that filters the data\n" def

    $t insert end "GUI" "metavalue4 link keywords"
    $t insert end "GUI" metavalue_h
    $t insert end " objects that provide a graphical user interface\n" def

    $t insert end "list_op" "metavalue1 link keywords"
    $t insert end "list_op" metavalue_h
    $t insert end " object that manipulates or stores a list\n" def

    $t insert end "MIDI" "metavalue2 link keywords"
    $t insert end "MIDI" metavalue_h
    $t insert end " objects that provide MIDI functionality\n" def

    $t insert end "needs_work" "metavalue3 link keywords"
    $t insert end "needs_work" metavalue_h
    $t insert end " help patches under construction\n" def

    $t insert end "network" "metavalue4 link keywords"
    $t insert end "network" metavalue_h
    $t insert end " object that provides access to or sends/receives " def
    $t insert end "data over a network connection.\n" def

    $t insert end "nonlocal" "metavalue1 link keywords"
    $t insert end "nonlocal" metavalue_h
    $t insert end " objects that can make nonlocal connections to " def
    $t insert end "other objects (i.e., communicate with other objects " def
    $t insert end "without wires\n" def

    $t insert end "orphan" "metavalue2 link keywords"
    $t insert end "orphan" metavalue_h
    $t insert end " help patches that can't get accessed by right-clicking" def
    $t insert end {on the corresponding object (like [drawsymbol])} def
    $t insert end "\n" def

    $t insert end "patchfile_op" "metavalue3 link keywords"
    $t insert end "patchfile_op" metavalue_h
    $t insert end " object whose behavior only makes sense in " def
    $t insert end "terms of a patchfile\n" def

    $t insert end "pd_op" "metavalue4 link keywords"
    $t insert end "pd_op" metavalue_h
    $t insert end " object that can report on or manipulate global Pd " def
    $t insert end "operation\n" def

    $t insert end "ramp" "metavalue1 link keywords"
    $t insert end "ramp" metavalue_h
    $t insert end " a ramp\n" def

    $t insert end "random" "metavalue2 link keywords"
    $t insert end "random" metavalue_h
    $t insert end " object outputs a random value, list, or signal\n" def

    $t insert end "signal" "metavalue3 link keywords"
    $t insert end "signal" metavalue_h
    $t insert end " audiorate objects\n" def

    $t insert end "soundfile" "metavalue4 link keywords"
    $t insert end "soundfile" metavalue_h
    $t insert end " object that can play, manipulate, and/or save a " def
    $t insert end "sound file (wav, ogg, mp3, etc.)\n" def

    $t insert end "storage" "metavalue1 link keywords"
    $t insert end "storage" metavalue_h
    $t insert end " objects whose main function is to store a value\n" def

    $t insert end "symbol_op" "metavalue2 link keywords"
    $t insert end "symbol_op" metavalue_h
    $t insert end " object that manipulates or stores a symbol\n" def

    $t insert end "time" "metavalue3 link keywords"
    $t insert end "time" metavalue_h
    $t insert end " objects that measure time or which the user can " def
    $t insert end "use to manipulate time\n" def

    $t insert end "trigonometry" "metavalue4 link keywords"
    $t insert end "trigonometry" metavalue_h
    $t insert end " objects that provide trigonometric functionality\n" def

    $t configure -state disabled
}

# hack to get <ctrl-backspace> to delete the word to the left of the cursor
proc ::dialog_search::ctrl_bksp {mytoplevel} {
    set last [$mytoplevel index insert]
    set first $last
    while { $first > 0 } {
	set char [string index [$mytoplevel get] $first-1]
	set prev [string index [$mytoplevel get] $first]
	if { $char eq " " && $first < $last && $prev ne " " || [$mytoplevel selection present] } { break }
	incr first -1
    }
    incr first
    $mytoplevel delete $first $last
}

proc ::dialog_search::font_size {text dir} {
    set offset {}
    if {$dir == 1} {
	set offset 2
    } else {
	set offset -2
    }
    foreach tag [$text tag names] {
	set val [$text tag cget $tag -font]
	if {[string is digit -strict [lindex $val 1]]} {
	   $text tag configure $tag -font "Helvetica [expr {max([lindex $val 1]+$offset,8)}]"
	}
    }
}

proc ::dialog_search::create_dialog {mytoplevel} {
    variable selected_file
    variable genres
    toplevel $mytoplevel -class DialogWindow
    wm title $mytoplevel [_ "Search"]

    # style tweak: get rid of arrow so the combobox looks like a simple entry widget
    ttk::style configure Entry.TCombobox -arrowsize 0
    ttk::style configure Genre.TCombobox

    # widgets
    ttk::combobox $mytoplevel.searchtextentry -textvar ::dialog_search::searchtext \
	-font "Helvetica 12" -style "Entry.TCombobox" -cursor "xterm"
    ttk::button $mytoplevel.searchbutton -text [_ "Search"] -takefocus 0 \
	-command ::dialog_search::search
    ttk::frame $mytoplevel.f -padding 2
    ttk::combobox $mytoplevel.f.genrebox -values $genres -state readonly -style "Genre.TCombobox"
    $mytoplevel.f.genrebox current 0
    ttk::checkbutton $mytoplevel.f.matchall_check -text [_ "Match all terms"] \
	-variable ::dialog_search::matchall
    ttk::checkbutton $mytoplevel.f.matchwords_check -text [_ "Match whole words only"] \
	-variable ::dialog_search::matchwords
    ttk::label $mytoplevel.advancedlabel -text [_ "Advanced"] -foreground "#0000ff"
    text $mytoplevel.resultstext -yscrollcommand "$mytoplevel.yscrollbar set" \
        -bg white -highlightcolor blue -height 30 -wrap word -state disabled \
	-padx 8 -pady 4 
    ttk::scrollbar $mytoplevel.yscrollbar -command "$mytoplevel.resultstext yview" \
        -takefocus 0
    ttk::label $mytoplevel.statusbar -text "Pure Data Search" -justify left \
        -padding {4 4 4 4}

    grid $mytoplevel.f.genrebox $mytoplevel.f.matchall_check \
	$mytoplevel.f.matchwords_check -padx 3
    grid $mytoplevel.searchtextentry -column 0 -columnspan 3 -row 0 -padx 3 -pady 2 -sticky ew
    grid $mytoplevel.searchbutton -column 3 -columnspan 2 -row 0 -padx 3 -sticky ew
    grid $mytoplevel.f -column 0 -columnspan 3 -row 1 -sticky ew
    grid $mytoplevel.advancedlabel -column 3 -columnspan 2 -row 1 -sticky w
    grid $mytoplevel.resultstext -column 0 -columnspan 4 -row 2 -sticky nsew
    grid $mytoplevel.yscrollbar -column 4 -row 2 -sticky ns
    grid $mytoplevel.statusbar -column 0 -columnspan 4 -row 3 -sticky nsew
    grid columnconfigure $mytoplevel 0 -weight 1
    grid columnconfigure $mytoplevel 4 -weight 0
    grid rowconfigure    $mytoplevel 2 -weight 1

    # tags
    $mytoplevel.resultstext tag configure title -foreground "#0000ff" -underline on \
	-font "helvetica 14" -spacing1 5
    $mytoplevel.resultstext tag configure filename -elide on
    $mytoplevel.resultstext tag configure metavalue -foreground "#0000ff"
    $mytoplevel.resultstext tag configure metavalue_h -elide on
    $mytoplevel.resultstext tag configure basedir -elide on
    $mytoplevel.resultstext tag configure description -font "helvetica 12"
    $mytoplevel.resultstext tag configure homepage_title -font "helvetica 12" -justify center
    $mytoplevel.resultstext tag configure homepage_description -font "helvetica 12"
    $mytoplevel.resultstext tag configure keywords \
	-font "helvetica 12"
    $mytoplevel.resultstext tag configure def -font "helvetica 10"
    # create a bunch of link bindings so that you get <Enter> and <Leave>
    # events when two links are right next to each other
    $mytoplevel.resultstext tag configure link -foreground "#0000ff"
    $mytoplevel.resultstext tag bind link <Enter> "$mytoplevel.resultstext configure \
        -cursor hand2"
    $mytoplevel.resultstext tag bind link <Leave> "$mytoplevel.resultstext configure \
        -cursor xterm; $mytoplevel.statusbar configure -text \"\""
    $mytoplevel.resultstext tag bind intro <Button-1> " ::dialog_search::intro \
	$mytoplevel.resultstext "
    # hack to force new <Enter> events for tags next to each other
    for {set i 0} {$i<30} {incr i} {
	$mytoplevel.resultstext tag bind "metavalue$i" <Button-1> \
	    "::dialog_search::grab_metavalue %x %y $mytoplevel 1"
        $mytoplevel.resultstext tag bind "metavalue$i" <Enter> \
	    "::dialog_search::grab_metavalue %x %y $mytoplevel 0"
    }
    $mytoplevel.resultstext tag bind title <Button-1> " ::dialog_search::open_file %x %y \
	$mytoplevel 1"
    $mytoplevel.resultstext tag bind title <Enter> "::dialog_search::open_file %x %y \
        $mytoplevel 0"

    # search window widget bindings
    bind $mytoplevel <$::modifier-equal> "::dialog_search::font_size $mytoplevel.resultstext 1"
    bind $mytoplevel <$::modifier-minus> "::dialog_search::font_size $mytoplevel.resultstext 0"
    bind $mytoplevel.searchtextentry <Return> "$mytoplevel.searchbutton invoke"
    bind $mytoplevel.searchtextentry <$::modifier-Key-BackSpace> \
	"::dialog_search::ctrl_bksp $mytoplevel.searchtextentry"
    bind $mytoplevel.searchtextentry <$::modifier-Key-a> \
        "$mytoplevel.searchtextentry selection range 0 end; break"
    bind $mytoplevel.f.genrebox <<ComboboxSelected>> "::dialog_search::filter_results \
	$mytoplevel.f.genrebox $mytoplevel.resultstext"
    bind $mytoplevel.f.matchall_check <Return> "$mytoplevel.searchbutton invoke"
    bind $mytoplevel.f.matchwords_check <Return> "$mytoplevel.searchbutton invoke"
    bind $mytoplevel.advancedlabel <Enter> "$mytoplevel.advancedlabel configure \
	-cursor hand2 "
    bind $mytoplevel.advancedlabel <Leave> "$mytoplevel.advancedlabel configure \
	-cursor xterm "
    bind $mytoplevel.advancedlabel <Button-1> \
	{menu_doc_open doc/5.reference all_about_finding_objects.pd}
    ::pd_bindings::dialog_bindings $mytoplevel "search"

    # Add state and set focus
    $mytoplevel.searchtextentry insert 0 "Enter search terms"
    $mytoplevel.searchtextentry selection range 0 end
    # go ahead and set tags for the default genre
    filter_results $mytoplevel.f.genrebox $mytoplevel.resultstext
    focus $mytoplevel.searchtextentry
    ::dialog_search::intro $mytoplevel.resultstext
    set ::dialog_search::matchall 1
    set ::dialog_search::matchwords 1
}

# create the menu item on load
set mymenu .menubar.help
set inserthere [$mymenu index [_ "Report a bug"]]
$mymenu insert $inserthere separator
$mymenu insert $inserthere command -label [_ "Search"] \
    -command {::dialog_search::open_search_dialog .search}
_______________________________________________
[email protected] mailing list
UNSUBSCRIBE and account-management -> 
http://lists.puredata.info/listinfo/pd-list

Reply via email to