On 1/29/07, Michael Haubenwallner <[EMAIL PROTECTED]> wrote:


Cool, this is an great enrichment. Installation and usage is without any
problems (working from win2k, python2.4.4 and python2.5.0).

Two suggestions come to my mind:
- can we have the window height stay the same ( if the window is not
   fullscreen a new row is added atm)
- i am used to the close button in the firefox searchbar, removing the
   searchbar is not easy atm, selecting "Find..." (CTRL-F) again throws
   a dialog "Error: Empty regular expression"

Michael

--
http://zope.org/Members/d2m
http://planetzope.org



Thanks for the suggestions! Updated files attached.

There's no need for a close button - to stop searching hit Escape or click
anywhere away from the search bar. (Sorry, I should have mentioned this in
the usage.)

When you hit Ctrl+f with the search bar active, it searches for whatever
you've typed. The message box was popping up because the search expression
was empty. I've changed this, now it just beeps.

I've fixed the window height issue, including window maximization issues and
making sure the current text is not hidden by the search bar popping up.
Works smoothly on Windows, need testers on other platforms to check this
out.

- Tal
"""SearchBar.py - An IDLE extension for searching for text in windows.

The interface is a small bar which appears on the bottom of the window,
and dissapears when the user stops searching.

This extension implements the usual search options, as well as regular
expressions.

Another nice feature is that while searching all matches are highlighted.

"""

import time
import string
import re

import Tkinter
from Tkconstants import TOP, BOTTOM, LEFT, RIGHT, X, NONE

from configHandler import idleConf

import SearchEngine
import WindowSearchEngine

##class FindBar:
####    menudefs = [('options', [('!Findbar', '<<toggle-findbar>>')])]
##
##    def __init__(self, editwin):
##        self.editwin = editwin
##        self.text = editwin.text
##
##        # Initialize the search manager
##        mark_fg = idleConf.GetOption("extensions", "FindBar",
##                                     "mark_fg", type="str", default="red")
##        self.window_engine = FindBarSearchManager(self.text,
##                                                  mark_fg=mark_fg)
##       
##        self.findbar = FindBarWidget(self.editwin, self.window_engine)
##        self.replacebar = FindBarWidget(self.editwin, self.window_engine,
##                                        is_replace=True)
##
##        self.enabled = False
##        self.toggle_findbar_event()
##
##    def toggle_findbar_event(self, event=None):
##        if not self.enabled:
##            self._enable()
##        else:
##            self._disable()
##
##    def _enable(self):
##        self.showid = self.text.bind("<<find>>",
##                                     self.findbar.show_findbar_event)
####            self.againid = self.text.bind("<<find-again>>",
####                                         self.findbar.search_again_event)
##        self.selectid = self.text.bind("<<find-selection>>",
##                                       self.findbar.show_findbar_event)
####        self.replaceid = self.text.bind("<<replace>>",
####                                        self.replacebar.show_findbar_event)
##        self.text.tag_configure("findsel",
##            background=self.text.tag_cget("sel","background"),
##            foreground=self.text.tag_cget("sel","foreground"))
##
##    def _disable(self):
##        self.text.unbind("<<find>>", self.showid)
####            self.text.unbind("<<find-again>>", self.againid)
##        self.text.unbind("<<find-selection>>", self.selectid)
####        self.text.unbind("<<replace>>", self.replaceid)
##        self.text.tag_delete("findsel")

class SearchBar:
    menudefs = []
    def __init__(self, editwin):
        self.fb = find_bar = FindBar(editwin, editwin.status_bar)
        self.rb = replace_bar = ReplaceBar(editwin, editwin.status_bar)

        editwin.text.bind("<<find>>",
                          find_bar.show_findbar_event)
        editwin.text.bind("<<find-again>>",
                          find_bar.search_again_event)
        editwin.text.bind("<<find-selection>>",
                          find_bar.search_selection_event)
        editwin.text.bind("<<replace>>",
                          replace_bar.show_findbar_event)


def FindBar(editwin, pack_after):
    return SearchBarWidget(editwin, pack_after, is_replace=False)
def ReplaceBar(editwin, pack_after):
    return SearchBarWidget(editwin, pack_after, is_replace=True)

class SearchBarWidget:
    def __init__(self, editwin, pack_after, is_replace=False):
        self.editwin = editwin
        self.text = editwin.text
        self.root = self.text._root()
        self.engine = SearchEngine.get(self.root)
        self.window_engine = WindowSearchEngine.get(editwin)
        self.is_replace = is_replace

        self.top = editwin.top
        self.pack_after = pack_after

        self.widgets_built = False
        self.shown = False

        self.find_var = Tkinter.StringVar(self.root)

        # The text widget's selection isn't shown when it doesn't have the
        # focus. Let's replicate it so it will be seen while searching as well.
        self.text.tag_configure("findsel",
            background=self.text.tag_cget("sel","background"),
            foreground=self.text.tag_cget("sel","foreground"))

        self._hide()

    def _show(self):
        if not self.widgets_built:
            self._build_widgets()
            
        if not self.shown:
            toplevel = self.editwin.top
            geom = toplevel.wm_geometry()
            self.bar_frame.pack(side=BOTTOM, fill=X, expand=0, pady=1,
                                after=self.pack_after)
            # Reset the window's size only if it is in 'normal' state.
            # On Windows, if this is done when the window is maximized
            # ('zoomed' state), then the window will not return to its
            # original size when it is unmaximized.
            if toplevel.wm_state() == 'normal':
                toplevel.wm_geometry(geom)
                # Ensure that the insertion point is still visible
                toplevel.update()
                self.text.see("insert")

            self.window_engine.show_find_marks()
            self.shown = True # must be _before_ reset_selection()!
            # Add the "findsel" tag, which looks like the selection
            self._reset_selection()            

        self._is_incremental = self.is_incremental()

    def _hide(self):
        if self.widgets_built and self.shown:
            self.bar_frame.pack_forget()
            self.window_engine.reset()
            self.window_engine.hide_find_marks()

            sel = self._get_selection()
            self.shown = False # must be _after_ get_selection()!
            if sel:
                self._set_selection(sel[0], sel[1])
                self.text.mark_set("insert", sel[0])
            else:
                self._reset_selection()
            self.text.see("insert")

        self.text.tag_remove("findsel","1.0","end")
        self._is_incremental = None

    def is_incremental(self):
        if self._is_incremental is None:
            return idleConf.GetOption("extensions", "SearchBar",
                                      "is_incremental", type="bool",
                                      default=False)
        else:
            return self._is_incremental

    def _incremental_callback(self, *args):
        if self.shown and self.is_incremental():
            if self.find_var.get():
                self._safe_search(start=self.text.index("insert"))
            else:
                self.window_engine.reset()
                self._clear_selection()
                self.text.see("insert")

    def _build_widgets(self):
        if not self.widgets_built:
            def _make_entry(parent, label, var):
                l = Tkinter.Label(parent, text=label)
                l.pack(side=LEFT, fill=NONE, expand=0)
                e = Tkinter.Entry(parent, textvariable=var, exportselection=0,
                                  width=30, border=1)
                e.pack(side=LEFT, fill=NONE, expand=0)
                e.bind("<Escape>", self.hide_findbar_event)
                return e

            def _make_checkbutton(parent, label, var):
                btn = Tkinter.Checkbutton(parent, anchor="w",
                                          text=label, variable=var)
                btn.pack(side=LEFT, fill=NONE, expand=0)
                btn.bind("<Escape>", self.hide_findbar_event)
                return btn

            def _make_button(parent, label, command):
                btn = Tkinter.Button(parent, text=label, command=command)
                btn.pack(side=LEFT, fill=NONE, expand=0)
                btn.bind("<Escape>", self.hide_findbar_event)
                return btn

            # Frame for the entire bar
            self.bar_frame = Tkinter.Frame(self.top, border=1, relief="flat")

            # Frame for the 'Find:' / 'Replace:' entry + search options
            self.find_frame = Tkinter.Frame(self.bar_frame, border=0)

            # 'Find:' / 'Replace:' entry
            if not self.is_replace: tmp = "Find:"
            else: tmp = "Replace:"
            self.find_ent = _make_entry(self.find_frame,
                                        tmp, self.find_var)

            # Regular expression checkbutton
            btn = _make_checkbutton(self.find_frame,
                                    "Reg-Exp", self.engine.revar)
            if self.engine.isre():
                btn.select()
            self.reg_btn = btn

            # Match case checkbutton
            btn = _make_checkbutton(self.find_frame,
                                    "Match case", self.engine.casevar)
            if self.engine.iscase():
                btn.select()
            self.case_btn = btn

            # Whole word checkbutton
            btn = _make_checkbutton(self.find_frame,
                                    "Whole word", self.engine.wordvar)
            if self.engine.isword():
                btn.select()
            self.word_btn = btn

            # Wrap checkbutton
            btn = _make_checkbutton(self.find_frame,
                                    "Wrap around", self.engine.wrapvar)
            if self.engine.iswrap():
                btn.select()
            self.wrap_btn = btn

            # Direction checkbutton
            self.direction_txt_var = Tkinter.StringVar(self.root)
            btn = Tkinter.Checkbutton(self.find_frame,
                                      textvariable=self.direction_txt_var,
                                      variable=self.engine.backvar,
                                      command=self._update_direction_button,
                                      indicatoron=0,
                                      width=5,
                                      )
            btn.config(selectcolor=btn.cget("bg"))
            btn.pack(side=RIGHT, fill=NONE, expand=0)
            Tkinter.Label(self.find_frame, text="Direction:").pack(side=RIGHT,
                                                                   fill=NONE,
                                                                   expand=0)
            if self.engine.isback():
                btn.select()
                self.direction_txt_var.set("Up")
            else:
                btn.deselect()
                self.direction_txt_var.set("Down")
            btn.bind("<Escape>",self.hide_findbar_event)
            self.direction_btn = btn

            self.find_frame.pack(side=TOP, fill=X, expand=0)

            if self.is_replace:
                # Frame for the 'With:' entry + replace options
                self.replace_frame = Tkinter.Frame(self.bar_frame, border=0)

                self.replace_with_var = Tkinter.StringVar(self.root)
                self.replace_ent = _make_entry(self.replace_frame,"With:",
                                               self.replace_with_var)

                _make_button(self.replace_frame, "Find",
                             self._search)
                _make_button(self.replace_frame, "Replace",
                             self._replace_event)
                _make_button(self.replace_frame, "Replace All",
                             self._replace_all_event)

                self.replace_frame.pack(side=TOP, fill=X, expand=0)

            self.widgets_built = True

            # Key bindings for the 'Find:' / 'Replace:' Entry widget
            self.find_ent.bind("<Control-Key-f>", self._safe_search)
            self.find_ent.bind("<Control-Key-g>", self._safe_search)
            self.find_ent.bind("<Control-Key-R>", self._toggle_reg_event)
            self.find_ent.bind("<Control-Key-C>", self._toggle_case_event)
            self.find_ent.bind("<Control-Key-W>", self._toggle_wrap_event)
            self.find_ent.bind("<Control-Key-D>", self._toggle_direction_event)
            self.find_ent_expander = EntryExpander(self.find_ent, self.text)
            self.find_ent_expander.bind("<Alt-Key-slash>")

            callback = self.find_ent._register(self._incremental_callback)
            self.find_ent.tk.call("trace", "variable", self.find_var, "w",
                                  callback)

            if not self.is_replace:
                # Key bindings for the 'Find:' Entry widget
                self.find_ent.bind("<Return>", self._safe_search)
            else:
                # Key bindings for the 'Replace:' Entry widget
                self.find_ent.bind("<Return>", self._replace_bar_find_entry_return_event)
                
                # Key bindings for the 'With:' Entry widget
                self.replace_ent.bind("<Return>", self._replace_event)
                self.replace_ent.bind("<Shift-Return>", self._safe_search)
                self.replace_ent.bind("<Control-Key-f>", self._safe_search)
                self.replace_ent.bind("<Control-Key-g>", self._safe_search)
                self.replace_ent.bind("<Control-Key-R>", self._toggle_reg_event)
                self.replace_ent.bind("<Control-Key-C>", self._toggle_case_event)
                self.replace_ent.bind("<Control-Key-W>", self._toggle_wrap_event)
                self.replace_ent.bind("<Control-Key-D>", self._toggle_direction_event)
                self.replace_ent_expander = EntryExpander(self.replace_ent,
                                                          self.text)
                self.replace_ent_expander.bind("<Alt-Key-slash>")

    def _destroy_widgets(self):
        if self.widgets_built:
            self.bar_frame.destroy()

    def show_findbar_event(self, event):
        # Get the current selection
        sel = self._get_selection()
        if sel:
            # Put the current selection in the "Find:" entry
            self.find_var.set(self.text.get(sel[0],sel[1]))

        # Now show the FindBar in all it's glory!
        self._show()

        # Set the focus to the "Find:"/"Replace:" entry
        self.find_ent.focus()

        # Select all of the text in the "Find:"/"Replace:" entry
        self.find_ent.selection_range(0,"end")

        # Hide the findbar if the focus is lost
        self.bar_frame.bind("<FocusOut>", self.hide_findbar_event)

        # Focus traversal (Tab or Shift-Tab) shouldn't return focus to
        # the text widget
        self.prev_text_takefocus_value = self.text.cget("takefocus")
        self.text.config(takefocus=0)
        return "break"

    def hide_findbar_event(self, event=None):
        self._hide()
        self.text.config(takefocus=self.prev_text_takefocus_value)
        self.text.focus()
        return "break"

    def search_again_event(self, event):
        if self.engine.getpat():
            return self._search(event)
        else:
            return self.show_findbar_event(event)

    def search_selection_event(self, event):
        # Get the current selection
        sel = self._get_selection()
        if not sel:
            # No selection - beep and leave
            self.text.bell()
            return "break"

        # Set the window's search engine's pattern to the current selection 
        self.find_var.set(self.text.get(sel[0],sel[1]))

        return self._search(event)

    def _toggle_reg_event(self, event):
        self.reg_btn.invoke()
        return "break"

    def _toggle_case_event(self, event):
        self.case_btn.invoke()
        return "break"

    def _toggle_wrap_event(self, event):
        self.wrap_btn.invoke()
        return "break"

    def _toggle_direction_event(self, event):
        self.direction_btn.invoke()
        return "break"

    def _update_direction_button(self):
        if self.engine.backvar.get():
            self.direction_txt_var.set("Up")
        else:
            self.direction_txt_var.set("Down")

    def _replace_bar_find_entry_return_event(self, event=None):
        # Set the focus to the "With:" entry
        self.replace_ent.focus()
        # Select all of the text in the "With:" entry
        self.replace_ent.selection_range(0,"end")
        return "break"

    def _search_text(self, start, is_safe):
        regexp = self._set_regexp()
        if not regexp:
            return None

        direction = not self.engine.isback()
        wrap = self.engine.iswrap()
        sel = self._get_selection()

        if start is None:
            if sel:
                start = sel[0]
            else:
                start = self.text.index("insert")
        if ( direction and sel and start == sel[0] and
             regexp.match(self.text.get(sel[0],sel[1])) ):
            _start = start + "+1c"
        else:
            _start = start
        res = self.window_engine.findnext(regexp,
                                          _start, direction, wrap, is_safe)

        # ring the bell if the selection was found again
        if sel and start == sel[0] and res == sel:
            self.text.bell()

        return res

    def _search(self, event=None, start=None, is_safe=False):
        t = time.time()
        res = self._search_text(start, is_safe)
        if res:
            first, last = res
            self._set_selection(first, last)
            self.text.see(first)
            if not self.shown:
                self.text.mark_set("insert", first)
        else:
            self._clear_selection()
            self.text.bell()
        return "break"

    def _safe_search(self, event=None, start=None):
        return self._search(event=event, start=start, is_safe=True)

    def _replace_event(self, event=None):
        regexp = self._set_regexp()
        if not regexp:
            return "break"

        # Replace if appropriate
        sel = self._get_selection()
        if sel and regexp.match(self.text.get(sel[0], sel[1])):
            replace_with = self.replace_with_var.get()
            if Tkinter.tkinter.TK_VERSION >= '8.5':
                # Requires at least Tk 8.5!
                # This is better since the undo mechanism counts the
                # replacement as one action
                self.text.replace(sel[0],
                                  sel[1],
                                  replace_with)
            else: # TK_VERSION < 8.5 - no replace method
                if sel[0] != sel[1]:
                    self.text.delete(sel[0], sel[1])
                if replace_with:
                    self.text.insert(sel[0], replace_with)
            self.text.mark_set("insert", sel[0] + '%dc' % len(replace_with))

        # Now search for the next appearance
        return self._search(event, is_safe=False)

    def _replace_all_event(self, event=None):
        regexp = self._set_regexp()
        if not regexp:
            return "break"

        direction = not self.engine.isback()
        wrap = self.engine.iswrap()
        self.window_engine.replace_all(regexp, self.replace_with_var.get(),
                                       direction, wrap)
        return "break"

    def _set_regexp(self):
        search_expression = self.find_var.get()
        # If the search expression is empty, bail out.
        # (otherwise SearchEngine pops up an annoying message box)
        if not search_expression:
            return None

        self.engine.patvar.set(search_expression)
        regexp = self.engine.getprog()
        return regexp


    ### Selection related methods    
    def _clear_selection(self):
        tagname = self.shown and "findsel" or "sel"
        self.text.tag_remove(tagname, "1.0", "end")

    def _set_selection(self, start, end):
        self._clear_selection()
        tagname = self.shown and "findsel" or "sel"
        self.text.tag_add(tagname, start, end)

    def _get_selection(self):
        tagname = self.shown and "findsel" or "sel"
        return self.text.tag_nextrange(tagname, '1.0', 'end')

    def _reset_selection(self):
        if self.shown:
            sel = self.text.tag_nextrange("sel", '1.0', 'end')
            if sel:
                self._set_selection(sel[0], sel[1])
            else:
                self._clear_selection()


class EntryExpander(object):
    """Expand words in an entry, taking possible words from a text widget."""
    def __init__(self, entry, text):
        self.text = text
        self.entry = entry
        self.reset()

        self.entry.bind('<Map>', self.reset)

    def reset(self, event=None):
        self._state = None

    def bind(self, event_string):
        self.entry.bind(event_string, self._expand_word_event)

    def _expand_word_event(self, event=None):
        curinsert = self.entry.index("insert")
        curline = self.entry.get()
        if not self._state:
            words = self._get_expand_words()
            index = 0
        else:
            words, index, insert, line = self._state
            if insert != curinsert or line != curline:
                words = self._get_expand_words()
                index = 0
        if not words:
            self.text.bell()
            return "break"

        curword = self._get_curr_word()
        newword = words[index]
        index = (index + 1) % len(words)
        if index == 0:
            self.text.bell() # Warn the user that we cycled around

        idx = int(self.entry.index("insert"))
        self.entry.delete(str(idx - len(curword)), str(idx))       
        self.entry.insert("insert", newword)

        curinsert = self.entry.index("insert")
        curline = self.entry.get()        
        self._state = words, index, curinsert, curline
        return "break"
        
    def _get_expand_words(self):
        curword = self._get_curr_word()
        if not curword:
            return []

        regexp = re.compile(r"\b" + curword + r"\w+\b")
        # Start at 'insert wordend' so current word is first
        beforewords = regexp.findall(self.text.get("1.0", "insert wordend"))
        beforewords.reverse()
        afterwords = regexp.findall(self.text.get("insert wordend", "end"))
        # Interleave the lists of words
        # (This is the next best thing to sorting by distance)
        allwords = []
        for a,b in zip(beforewords, afterwords):
            allwords += [a,b]
        minlen = len(allwords)/2
        allwords += beforewords[minlen:] + afterwords[minlen:]

        words_list = []
        words_dict = {}
        for w in allwords:
            if w not in words_dict:
                words_dict[w] = w
                words_list.append(w)
        words_list.append(curword)
        return words_list

    _wordchars = string.ascii_letters + string.digits + "_"
    def _get_curr_word(self):
        line = self.entry.get()
        i = j = self.entry.index("insert")
        while i > 0 and line[i-1] in self._wordchars:
            i = i-1
        return line[i:j]
    
import re
from configHandler import idleConf

import time

def get(editwin):
    if not hasattr(editwin, "_window_search_engine"):
        editwin._window_search_engine = WindowSearchEngine(editwin.text)
    return editwin._window_search_engine

class WindowSearchEngine:
    def __init__(self, text):
        self.text = text

        # Initialize 'findmark' tag
        self.hide_find_marks()
        
        self.reset()

    def __del__(self):
        self.text.tag_delete("findmark")

    def show_find_marks(self):
        # Get the highlight colors for 'hit'
        # Do this here (and not in __init__) for color config changes to take
        # effect immediately
        currentTheme = idleConf.CurrentTheme()
        mark_fg = idleConf.GetHighlight(currentTheme, 'hit', fgBg='fg')
        mark_bg = idleConf.GetHighlight(currentTheme, 'hit', fgBg='bg')

        self.text.tag_configure("findmark",
                                foreground=mark_fg,
                                background=mark_bg)

    def hide_find_marks(self):
        self.text.tag_configure("findmark",
                                foreground='',
                                background='')

    def reset(self):
        self.text.tag_remove("findmark", "1.0", "end")
        self.regexp = None

    def _pos2idx(self, pos):
        "Convert a position in the text string to a Text widget index"
        return self.text.index("1.0+%dc"%pos)

    def _set_regexp(self, regexp):
        "Set the current regexp; search for and mark all matches in the text"
        ## When searching for an extension of the previous search,
        ## i.e. regexp.startswith(self.regexp), update hits instead of starting from
        ## scratch
        self.reset()
        self.regexp = regexp

        txt = self.text.get("1.0", "end-1c")
        prev = 0
        line = 1
        rfind = txt.rfind
        tag_add = self.text.tag_add
        for res in regexp.finditer(txt):
            start, end = res.span()
            line += txt[prev:start].count('\n')
            prev = start
            start_idx = "%d.%d" % (line,
                                   start - (rfind('\n', 0, start) + 1))
            end_idx = start_idx + '+%dc'%(end-start)
            tag_add("findmark", start_idx, end_idx)

    def findnext(self, regexp, start, direction=1, wrap=True, is_safe=False):
        """Find the next text sequence which matches the given regexp.

        The 'next' sequence is the one after the selection or the insert
        cursor, or before if the direction is up instead of down.

        The 'is_safe' argument tells whether it is safe to assume that the text
        being searched has not been changed since the previous search; if the
        text hasn't been changed then the search is almost trivial (due to
        pre-processing).
        
        """
        if regexp != self.regexp or not is_safe:
            self._set_regexp(regexp)

        # Search!
        if direction:
            next = self.text.tag_nextrange("findmark", start)
            if not next and wrap:
                next = self.text.tag_nextrange("findmark", '1.0', start)
        else:
            next = self.text.tag_prevrange("findmark", start)
            if not next and wrap:
                next = self.text.tag_prevrange("findmark", 'end', start)
    
        return next
        
    def replace_all(self, regexp, replace_with):
        hit = self.findnext(regexp, '1.0',
                            direction=1, wrap=False, is_safe=False)
        while hit:
            first, last = hit
            if Tkinter.tkinter.TK_VERSION >= '8.5':
                # Requires at least Tk 8.5!
                # This is better since the undo mechanism counts the
                # replacement as one action
                self.text.replace(first,
                                  last,
                                  replace_with)
            else: # TK_VERSION < 8.5 - no replace method
                if first != last:
                    self.text.delete(first, last)
                if replace_with:
                    self.text.insert(first, replace_with)
            hit = self.findnext(regexp, first + '%dc' % len(replace_with),
                                direction=1, wrap=False, is_safe=True)

def get_selection(text):
    "Get the selection range in a text widget"
    tmp = text.tag_nextrange("sel","1.0","end")
    if tmp:
        first, last = tmp
    else:
        first = last = text.index("insert")
    return first, last

##def idx2ints(idx):
##    "Convert a Text widget index to a (line, col) pair"
##    line, col = map(int,idx.split(".")) # Fails on invalid index
##    return line, col

##def ints2idx(ints):
##    "Convert a (line, col) pair to Tk's Text widget's format."
##    return "%d.%d" % ints # Fails on invalid index
_______________________________________________
IDLE-dev mailing list
[email protected]
http://mail.python.org/mailman/listinfo/idle-dev

Reply via email to