# jfindpanel.tcl - `find' panel for text widgets
#
######################################################################
# Copyright 1992-1995 by Jay Sekora.  This file may be freely        #
# distributed, modified or unmodified, for any purpose, provided     #
# that this copyright notice is retained verbatim in all copies and  #
# no attempt is made to obscure the authorship of this file.  If you #
# distribute any modified versions, I ask, but do not require, that  #
# you clearly mark any changes you make as such and that you provide #
# your users with instructions for getting the original sources.     #
######################################################################

### TO DO
###   have find wrap around (if last time didn't match)
###   regex search/replace
###   `find' tags instead of selection (set of buttons)
###   rewrite find routines to pass options instead of using globals
###     e.g. j:find:find_pattern -case 0 -regex 1 \
###            -backwards 0 -tag found {foo$} .main.t

######################################################################
# j:find ?options? t - search-and-replace panel for text widget t
# option is:
#   -replace (default 1)
# if $replace, the replace box and buttons will be drawn, otherwise
#   it'll only be a search panel
######################################################################

proc j:find {args} {
  j:parse_args {{replace 1}}
  
  set old_focus [focus]			;# so we can restore original focus
  set t $args				;# text widget to search in
  global j_find
  set j_find(widget) $t
  append j_find(searchfor) {}		;# make sure it's defined
  append j_find(replacewith) {}		;# make sure it's defined

  if {! [info exists j_find(backwards)]} {
    set j_find(backwards) 0
  }
  if {! [info exists j_find(case)]} {
    set j_find(case) 0
  }
  if {! [info exists j_find(regex)]} {
    set j_find(regex) 0
  }

  if [winfo exists .find] {
    wm withdraw .find
    wm deiconify .find			;# just try to make it visible
    focus .find.t.search.e		;# and focus on the search field
    return 0
  }

  toplevel .find
  wm title .find [j:ldb title:find_panel "Find Panel"]
  frame .find.t
  j:variable_entry .find.t.search \
    -label [j:ldb find:searchfor "Search for:"] \
    -variable j_find(searchfor)
  j:variable_entry .find.t.replace \
    -label [j:ldb find:replacewith "Replace with:"] \
    -variable j_find(replacewith)
  frame .find.t.options
  label .find.t.options.filler -text {} -width 16 -anchor e
  checkbutton .find.t.options.backwards -anchor w \
    -text [j:ldb find:backwards {Backwards}] \
    -variable j_find(backwards)
  checkbutton .find.t.options.case -anchor w \
    -text [j:ldb find:case "Case\255sensitive"] \
    -variable j_find(case)
  checkbutton .find.t.options.regex -anchor w \
    -text [j:ldb find:regex "Regex"] \
    -variable j_find(regex)
  set buttons(search) {
    search Search
      {
        if {[j:find:find_pattern $j_find(searchfor) $j_find(widget)] == 0} {
          j:alert -text "Not found."
        }
      }
    }
  set buttons(replace) {
    replace Replace
      {
        j:find:replace $j_find(searchfor) $j_find(replacewith) $j_find(widget)
        j:find:find_pattern $j_find(searchfor) $j_find(widget)
      }
    }
  set buttons(replace_all) {
    replace_all {Replace All}
      {
        j:find:replace_all $j_find(searchfor) $j_find(replacewith) \
          $j_find(widget)
      }
    }
  set buttons(cancel) {cancel Cancel {destroy .find}}

  if $replace {
    j:buttonbar .find.b -default search -buttons [list \
      $buttons(search) \
      $buttons(replace) \
      $buttons(replace_all) \
      $buttons(cancel) \
    ]
  } else {
    j:buttonbar .find.b -default search -buttons [list \
      $buttons(search) \
      $buttons(cancel) \
    ]
  }
  
  j:tab_ring .find.t.search.e .find.t.replace.e
  
  pack .find.t.options.filler -side left -fill both
  pack \
    .find.t.options.backwards \
    [j:filler .find.t.options] \
    .find.t.options.case \
    [j:filler .find.t.options] \
    .find.t.options.regex \
    -side left -fill y
  
  pack [j:filler .find.t] -side top
  pack .find.t.search -side top -expand yes -fill x
  if $replace {
    pack [j:filler .find.t] -side top
    pack .find.t.replace -side top -expand yes -fill x
  }
  pack .find.t.options -side top -expand yes -fill both
  pack .find.t -side top -fill both -padx 10 -pady 5
  pack [j:rule .find] -side top -fill x
  pack .find.b -side bottom -fill x

  # Meta-g (or Return, below) in either field searches:
  bind .find.t.search.e <Meta-g> \
    {.find.b.search invoke}
  bind .find.t.replace.e <Meta-g> \
    {.find.b.search invoke}

  j:default_button .find.b.search .find.t.search.e .find.t.replace.e
  j:cancel_button .find.b.cancel .find.t.search.e .find.t.replace.e

  focus .find.t.search.e
  
  tkwait window .find
  j:tk3 {focus $old_focus}
  j:tk4 {
    catch {focus -force $old_focus}	;# can't figure out a better way...
  }
}

######################################################################
# j:find:again t - search again for same string
######################################################################

proc j:find:again {t} {
  global j_find
  append j_find(searchfor) {}			;# make sure it's defined
  set j_find(widget) $t

  if {$j_find(searchfor) == {}} {
    j:find $t
  } else {
    if {[j:find:find_pattern $j_find(searchfor) $t] == 0} {
      j:alert -text "Not found."
    }
  }
}

######################################################################
# j:find:find_pattern string t - find and select string in text widget t
######################################################################
# WARNING: since this takes a copy of the file, it could use a LOT
# of memory!
# should be rewritten to use a different mark than insert.

proc j:find:find_pattern { string t } {
  global j_find				;# text widget to search in
  set j_find(widget) $t
  append j_find(searchfor) {}		;# make sure it's defined
  append j_find(replacewith) {}		;# make sure it's defined

  if {! [info exists j_find(backwards)]} {
    set j_find(backwards) 0
  }
  if {! [info exists j_find(case)]} {
    set j_find(case) 0
  }
  if {! [info exists j_find(regex)]} {
    set j_find(regex) 0
  }

  # don't bother looking for the null string:
  if {$string == {}} {
    return 0				;# return 0 if null string
  }
  
  if $j_find(regex) {
    if $j_find(backwards) {
      j:alert -text "Backwards regex searches not yet implemented."
      return 0
    } else {				;# forwards:
      set text [$t get insert end]
      if $j_find(case) {		;# case-sensitive:
        set countfrom insert
        if [regexp -indices -- $string $text range] {
          set foundpos [lindex $range 0]
          set lastpos [expr [lindex $range 1] + 1]
        } else {
          set foundpos -1
        }
      } else {				;# not case-sensitive:
        set countfrom insert
        if [regexp -nocase -indices -- $string $text range] {
          set foundpos [lindex $range 0]
          set lastpos [expr [lindex $range 1] + 1]
        } else {
          set foundpos -1
        }
      }
    }
  } else {
    if $j_find(backwards) {
      set lastfirst last
      set textpart [$t get 0.0 {insert -1char}]
      set countfrom 0.0
    } else {
      set lastfirst first
      set textpart [$t get insert end]
      set countfrom insert
    }
  
    if {!$j_find(case)} {
      set string [string tolower $string]
      set textpart [string tolower $textpart]
    }
    set foundpos [string $lastfirst $string $textpart]
    # find length of selection:
    set lastpos [expr {$foundpos + [string length $string]}]
  }
  if {$foundpos == -1} then {
    return 0				;# return 0 if not found
  }
  # deselect any already-selected text:
  catch {$t tag remove sel sel.first sel.last}
  $t tag add sel \
    "$countfrom + $foundpos chars" "$countfrom + $lastpos chars"
  # move insert just after the match (so we can continue from there)
  j:text:move $t "$countfrom + $lastpos chars"
  return 1				;# return 1 if found
}

######################################################################
# j:find:replace from to t - replace selection in t with string
######################################################################
# SHOULD CONFIRM THAT THE SELECTION IS IN t!
# SHOULD CHECK THAT THE SELECTION MATCHES searchfor!

proc j:find:replace { searchfor replacewith t } {
  global j_find
  if [j:no_selection] {
    return 0
  }
  if {! [info exists j_find(regex)]} {
    set j_find(regex) 0
  }
  if $j_find(regex) {
    return [j:find:replace_regexp $searchfor $replacewith $t]
  } else {
    j:text:replace $t sel.first sel.last $replacewith
    return 1
  }
}

######################################################################
# j:find:replace_regexp from to t - regexp (search-and-)replace 
#   selection in t with $to
######################################################################
# SHOULD CONFIRM THAT THE SELECTION IS IN T!
# SHOULD CHECK THAT THE SELECTION MATCHES searchfor!

proc j:find:replace_regexp { searchfor replacewith t } {
  if [j:no_selection] {
    return 0
  }
  if {! [info exists j_find(case)]} {
    set j_find(case) 0
  }
  set searchstring [$t get sel.first sel.last]
  if $j_find(case) {
    set regsub_result \
      [regsub -- $searchfor $searchstring $replacewith newstring]
  } else {
    set regsub_result \
      [regsub -nocase -- $searchfor $searchstring $replacewith newstring]
  }
  if $regsub_result {
    j:text:replace $t sel.first sel.last $replacewith
    return 1
  } else {
    return 0
  }
}

######################################################################
# j:find:replace_all from to t - change all from to to in widget t
### BUG: this and the other routines need to be rewritten not to use
### the "insert" mark
######################################################################

proc j:find:replace_all {from to t} {
  j:text:move $t 0.0
  while {[j:find:find_pattern $from $t]} {
    j:find:replace $from $to $t
  }
}


