# jfindpanel.tcl - `find' panel for text widgets
#
###############################################################################
#  Copyright 1992-2001 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.                                              #
###############################################################################

package provide aq 0.1

# following has side-effect of making sure ::aq namespace is defined
namespace eval ::aq {
  namespace export {[a-z]*}
  catch {
    package require jldb
    # following likely to fail, because done by other libraries too
    namespace import ::jldb::shortcuts::*
  }
}

######################################################################
# ::aq::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 ::aq::find {args} {
  ::aq::parse_args {
    {replace 1}
    {findhistory j_find}
    {replacehistory j_repl}
  }
  
  set old_focus [focus]			;# so we can restore original focus
  if {[llength $args]} {
    ::aq::active_window -class Text [lindex $args 0]
  }
  global j_find
  append j_find(searchfor) {}		;# make sure it's defined
  append j_find(replacewith) {}		;# make sure it's defined
  
  set j_find(findhistory) $findhistory
  set j_find(replacehistory) $replacehistory

  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 {! [info exists j_find(eol)]} {
    set j_find(eol) 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 withdraw .find
  
  wm title .find [= title:find_panel "Find Panel"]
  frame .find.t
  ::aq::variable_entry .find.t.search \
    -label [::jldb::short_text find:searchfor "Search for:"] \
    -history $findhistory \
    -variable j_find(searchfor)
  ::aq::variable_entry .find.t.replace \
    -label [::jldb::short_text find:replacewith "Replace with:"] \
    -history $replacehistory \
    -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 [::jldb::short_text find:backwards {Backwards}] \
    -variable j_find(backwards)
  checkbutton .find.t.options.case -anchor w \
    -text [::jldb::short_text find:case "Case\255sensitive"] \
    -variable j_find(case)
  checkbutton .find.t.options.regex -anchor w \
    -text [::jldb::short_text find:regex "Regex"] \
    -variable j_find(regex)
  checkbutton .find.t.options.eol -anchor w \
    -text [::jldb::short_text find:eol "EOL"] \
    -variable j_find(eol)
  set buttons(search) {
    search Search
      {
        if {![string match "" $j_find(findhistory)]} {
          ::aq::history_append $j_find(findhistory) $j_find(searchfor)
        }
        if {[::aq::find_pattern $j_find(searchfor)] == 0} {
          ::aq::alert -text [= {Not found.}]
        }
      }
    }
  set buttons(replace) {
    replace Replace
      {
        if {![string match "" $j_find(replacehistory)]} {
          ::aq::history_append \
            $j_find(replacehistory) $j_find(replacewith)
        }
        ::aq::find_replace $j_find(searchfor) $j_find(replacewith)
        ::aq::find_pattern $j_find(searchfor)
      }
    }
  set buttons(replace_all) {
    replace_all {Replace All}
      {
        if {![string match "" $j_find(findhistory)]} {
          ::aq::history_append \
            $j_find(findhistory) $j_find(searchfor)
        }
        if {![string match "" $j_find(replacehistory)]} {
          ::aq::history_append \
            $j_find(replacehistory) $j_find(replacewith)
        }
        ::aq::find_replace_all $j_find(searchfor) $j_find(replacewith)
      }
    }
  set buttons(cancel) {cancel Cancel {destroy .find}}

  if {$replace} {
    ::aq::buttonbar .find.b -default search -buttons [list \
      $buttons(search) \
      $buttons(replace) \
      $buttons(replace_all) \
      $buttons(cancel) \
    ]
  } else {
    ::aq::buttonbar .find.b -default search -buttons [list \
      $buttons(search) \
      $buttons(cancel) \
    ]
  }
  
  pack .find.t.options.filler -side left -fill both
  pack \
    .find.t.options.backwards \
    [::aq::filler .find.t.options] \
    .find.t.options.case \
    [::aq::filler .find.t.options] \
    .find.t.options.regex \
    [::aq::filler .find.t.options] \
    .find.t.options.eol \
    -side left -fill y
  
  pack [::aq::filler .find.t] -side top
  pack .find.t.search -side top -expand yes -fill x
  if {$replace} {
    pack [::aq::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 [::aq::rule .find] -side top -fill x
  pack .find.b -side bottom -fill x
  
  # Control-j in either field inserts a newline
  # Control-i in either field inserts a tab
  # Meta-g (or Return, below) in either field searches:
  # Meta-r in either field replaces:
  foreach entry {.find.t.search.e .find.t.replace.e} {
    bind $entry <Control-Key-i> {%W insert insert "\t"; break}
    bind $entry <Control-Key-j> {%W insert insert "\n"; break}
    bind $entry <Meta-g> {.find.b.search invoke; break}
    bind $entry <Meta-r> {.find.b.replace invoke; break}
  }
  
  ::aq::default_button .find.b.search .find.t.search.e .find.t.replace.e
  ::aq::cancel_button .find.b.cancel .find.t.search.e .find.t.replace.e

  focus .find.t.search.e
  
  update idletasks
  wm deiconify .find
  tkwait window .find
  catch {focus -force $old_focus}	;# can't figure out a better way...
}

######################################################################
# ::aq::find_again t - search again for same string
######################################################################

proc ::aq::find_again { {t any} } {
  global j_find
  append j_find(searchfor) {}			;# make sure it's defined
  if {[string match any $t]} {
    set t [::aq::active_window -class Text]
  }
  if {$j_find(searchfor) == {}} {
    ::aq::find $t
  } else {
    if {[::aq::find_pattern $j_find(searchfor) $t] == 0} {
      ::aq::alert -text [= {Not found.}]
    }
  }
}

######################################################################
# ::aq::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 ::aq::find_pattern { string {t any} } {
  global j_find				;# text widget to search in
  global tk_version
  if {[string match any $t]} {
    set t [::aq::active_window -class Text]
  }
  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 {! [info exists j_find(eol)]} {
    set j_find(eol) 0
  }

  # don't bother looking for the null string:
  if {$string == {}} {
    return 0				;# return 0 if null string
  }
  
  set found [j:text:find $t \
    -pattern $string \
    -case $j_find(case) \
    -regexp $j_find(regex) \
    -backwards $j_find(backwards)]
  
  # if j_find(eol), select trailing newline if any
  # This depends crucially on j:text:find leaving the insert at the end
  #   of the found string.
  if { $found && $j_find(eol) } {
    if {[string compare "\n" [$t get insert]] == 0} {
      $t tag add sel insert
    }
  }
  
  return $found
}

######################################################################
# ::aq::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 ::aq::find_replace { searchfor replacewith {t any} } {
  global j_find
  if {[::aq::no_selection]} {
    return 0
  }
  if {[string match any $t]} {
    set t [::aq::active_window -class Text]
  }
  if {! [info exists j_find(regex)]} {
    set j_find(regex) 0
  }
  if {$j_find(regex)} {
    return [::aq::find_replace_regexp $searchfor $replacewith $t]
  } else {
    j:text:replace $t sel.first sel.last $replacewith
    return 1
  }
}

######################################################################
# ::aq::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 ::aq::find_replace_regexp { searchfor replacewith {t any} } {
  if {[::aq::no_selection]} {
    return 0
  }
  if {[string match any $t]} {
    set t [::aq::active_window -class Text]
  }
  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
  }
}

######################################################################
# ::aq::find_replace_all from to t - change all from to to in widget t
######################################################################

proc ::aq::find_replace_all {from to {t any} } {
  global j_find
  if {[string match any $t]} {
    set t [::aq::active_window -class Text]
  }
  j:text:move $t 0.0
  while {[j:text:find $t \
    -pattern $from \
    -wrap 0 \
    -backwards 0 \
    -tag sel \
    -case $j_find(case) \
    -regexp $j_find(regex)]} {
    j:text:replace $t sel.first sel.last $to
  }
}
