""" Regular expression editor/debugger. REdemo improved by leonardo maffi, V.18, Dec 22 2006. Other possible improvements: - Add Undo. - Support reverb2 syntax instead of RE syntax (and in the meantime use gerarchic REs) - Add a scrollbox of past REs? - Add a list of commnon/useful REs? - Add syntax colouring of the RE. - Add balloons (or something similar) to the RE to denote Group number/name. - Improve groups visualization. - live evaluation of the RE can be disabled (so you need to click a button to do it). - hoveging over a match to see its details in a popup - Clicking on the match jumps to its respective result view. - Clicking on a match's details in the result view will highlight - support find/match/split - Add tooltips in other places too? - Unicode support? - Add scrollbars to the last pane? From Scale: - potresti rendere ridimensionabili le aree del testo di prova e dei gruppi, dato che spesso e' utile usare un testo lungo ma i gruppi tendono ad essere pochi. Anche il campo per la RE puo' essere ridotto un po' in altezza. - possibile (piccolo) bug: nel testo di prova non si puo' scorrere con le frecce direzionali o con page up/down se c'e' un match evidenziato e la sua altezza e' superiore a quella della casella di testo. Lo puoi vedere inserendo un testo di parecchie righe e lasciando vuota la casella della RE. - invece dell'opzione per cercare solo il primo match, o in aggiunta ad essa, potrebbe esserci un pulsante per iterare i vari match successivi. - si potrebbe aggiungere l'apertura di un file per leggervi il testo di prova. """ import re, Tkinter as tk class ReDemo: def __init__(self, master): self.master = master self.master.title('REdemo') self.tooltips = [] # For the help self.promptdisplay = tk.Label(self.master, anchor="w", text="Enter a Regular Expression:") self.promptdisplay.pack(side="top", fill="x") self.regexdisplay = self.ScrollText(font="Courier 8") self.regexdisplay.focus_set() self.regexdisplay.bind('', self.recompile) self.add_tags() self.add_options() self.statusdisplay = tk.Label(self.master, text="", anchor="w") self.statusdisplay.pack(side="top", fill="x") self.labeldisplay = tk.Label(self.master, anchor="w", text="Insert text to search into:") self.labeldisplay.pack(fill="x") self.showframe = tk.Frame(master) self.showframe.pack(fill="x", anchor="w") self.showvar = tk.StringVar(master) self.showvar.set("all") self.showfirstradio = tk.Radiobutton(self.showframe, text="Highlight first match", variable=self.showvar, value="first", command=self.recompile) self.showfirstradio.pack(side="left") self.showallradio = tk.Radiobutton(self.showframe, text="Highlight all matches", variable=self.showvar, value="all", command=self.recompile) self.showallradio.pack(side="left") self.stringdisplay = self.ScrollText() self.stringdisplay.tag_configure("hit1", background="yellow") self.stringdisplay.bind('', self.reevaluate) self.grouplabel = tk.Label(self.master, text="Groups:", anchor="w") self.grouplabel.pack(fill="x") self.grouplist = tk.Listbox(self.master) self.grouplist.pack(expand=1, fill="both") self.compiled = None self.recompile() btags = self.regexdisplay.bindtags() self.regexdisplay.bindtags(btags[1:] + btags[:1]) btags = self.stringdisplay.bindtags() self.stringdisplay.bindtags(btags[1:] + btags[:1]) def ScrollText(self, font=None, height=6, wrap="word"): frame = tk.Frame(self.master, bd=2, relief="sunken") frame.grid_rowconfigure(0, weight=1) frame.grid_columnconfigure(0, weight=1) yscrollbar = tk.Scrollbar(frame) yscrollbar.grid(row=0, column=1, sticky="ns") text = tk.Text(frame, wrap=wrap, bd=0, yscrollcommand=yscrollbar.set, height=height) if font: text["font"] = font text.grid(row=0, column=0, sticky="nsew") yscrollbar["command"] = text.yview frame.pack(fill="both", expand=True) return text def add_tags(self): # This method can be disabled, if necessary. import tkFont from textwrap import fill, dedent tags_help = r""" . (dot) In the default mode, this matches any character except a newline. If the DOTALL flag has been specified, this matches any character including a newline. ^ (Caret.) Matches at the beginning of lines. Unless the MULTILINE flag has been set, this will only match at the beginning of the string. In MULTILINE mode, this also matches immediately after each newline within the string. You can match the characters not within a range by complementing the set. This is indicated by including a "^" as the first character of the set; "^" elsewhere will simply match the "^" character. For example, [^5] will match any character except "5", and [^^] will match any character except "^". $ Matches at the end of a line, which is defined as either the end of the string, or in MULTILINE mode as any location followed by a newline character. * Causes the resulting RE to match 0 or more repetitions of the preceding RE, as many repetitions as are possible. ab* will match 'a', 'ab', or 'a' followed by any number of 'b's. + Causes the resulting RE to match 1 or more repetitions of the preceding RE. ab+ will match 'a' followed by any non-zero number of 'b's; it will not match just 'a'. ? Causes the resulting RE to match 0 or 1 repetitions of the preceding RE. ab? will match either 'a' or 'ab'. *? Adding "?" after the qualifier makes it perform the match in non-greedy or minimal fashion; as few characters as possible will be matched. +? Adding "?" after the qualifier makes it perform the match in non-greedy or minimal fashion; as few characters as possible will be matched. ?? Adding "?" after the qualifier makes it perform the match in non-greedy or minimal fashion; as few characters as possible will be matched. {m} Specifies that exactly m copies of the previous RE should be matched; fewer matches cause the entire RE not to match. {m,n} Causes the resulting RE to match from m to n repetitions of the preceding RE, attempting to match as many repetitions as possible. {m,n}? Causes the resulting RE to match from m to n repetitions of the preceding RE, attempting to match as few repetitions as possible. \ Either escapes special characters (permitting you to match characters like "*", "?", and so forth), or signals a special sequence. [] To indicate a set of characters. Characters can be listed individually, or a range of characters can be indicated. Ex: [a-zA-Z0-9] matches any letter or digit. All numeric escapes are treated as characters. Inside \b represents the backspace character. You can match the characters not within a range by complementing the set with ^ as the first character of the set. | A|B, where A and B can be arbitrary REs, creates a regular expression that will match either A or B. An arbitrary number of REs can be separated by the "|" in this way. This operator is never greedy. \A Matches only at the start of the string. When not in MULTILINE mode, \A and ^ are effectively the same. In MULTILINE mode, however, they're different; \A still matches only at the beginning of the string, but ^ may match at any location inside the string that follows a newline character. \b Word boundary. This is a zero-width assertion that matches at the position between a word character and a non-word character, or at the start and/or end of the string if the first and/or last characters in the string are word characters (it matches the empty string). Note: inside a character class, where there's no use for this assertion, \b represents the backspace character. \B Matches at the position between two word characters or at the position between two non-word characters (it matches the empty string). This is just the opposite of \b. \d When the UNICODE flag is not specified, matches any decimal digit. With UNICODE, it matches anything marked as digit. \D When the UNICODE flag is not specified, matches any non-digit character. With UNICODE, it matches anything other than character marked as digits. \s When the LOCALE and UNICODE flags are not specified, matches any whitespace character; equivalent to [ \t\n\r\f\v]. With LOCALE, it will match this set plus whatever characters are defined as space for the current locale. \S When the LOCALE and UNICODE flags are not specified, matches any non-whitespace character; this is equivalent to the set [^ \t\n\r\f\v] With LOCALE, it will match any character not in this set, and not defined as space in the current locale. \w When the LOCALE and UNICODE flags are not specified, matches any alphanumeric character and the underscore; this is equivalent to the set [a-zA-Z0-9_]. With LOCALE, it will match the set [0-9_] plus whatever characters are defined as alphanumeric for the current locale. \W When the LOCALE and UNICODE flags are not specified, matches any non-alphanumeric character; this is equivalent to the set [^a-zA-Z0-9_]. With LOCALE, it will match any character not in the set [0-9_], and not defined as alphanumeric for the current locale. \Z Matches only at the end of the string. \number Matches the contents of the group of the same number. It can only be used to match one of the first 99 groups. If the first digit of number is 0, it will be as the character with octal value number. User-defined groups start from 1. -- (...) Group. Matches whatever regular expression is inside the parentheses. The contents can be matched later with the \number special sequence. (?iLmsux) (One or more letters from the set "i", "L", "m", "s", "u", "x".) The group matches the empty string; the letters set the corresponding flags (re.I, re.L, re.M, re.S, re.U, re.X) for the entire regular expression. (?:...) A non-grouping version of regular parentheses. Matches whatever regular expression is inside the parentheses, but the substring matched by the group cannot be retrieved after performing a match or referenced later in the pattern. (?P...) Similar to regular parentheses, but the substring matched by the group is accessible via the symbolic group name name. A symbolic group is also a numbered group. (?P=name) Matches whatever text was matched by the earlier group named name. (?#...) A comment; the contents of the parentheses are simply ignored. -- (?=...) Lookahead assertion. Matches if ... matches next, but doesn't consume any of the string. (?!...) Negative lookahead assertion. Matches if ... doesn't match next. The contained pattern must only match strings of some fixed length. (?<=...) Positive lookbehind assertion. Matches if the current position in the string is preceded by a match for ... that ends at the current position. The contained pattern must only match strings of some fixed length. (?", add_txt) self.tooltips.append(ToolTip(tag_lab, text=helpt, wraplength=0, delay=500)) def add_options(self): from textwrap import fill, dedent options_help1 = r""" Perform case-insensitive matching; expressions like [A-Z] will match lowercase letters, too. This is not affected by the current locale. Make \w, \W, \b, \B, \s and \S dependent on the current locale. When specified, the pattern character "^" matches at the beginning of the string and at the beginning of each line (immediately following each newline); and the pattern character "$" matches at the end of the string and at the end of each line (immediately preceding each newline). By default, "^" matches only at the beginning of the string, and "$" only at the end of the string and immediately before the newline (if any) at the end of the string. Make the "." special character match any character at all, including a newline; without this flag, "." will match anything except a newline. Whitespace within the pattern is ignored, except when in a character class or preceded by an unescaped backslash, and, when a line contains a "#" neither in a character class or preceded by an unescaped backslash, all characters from the leftmost such "#" through the end of the line are ignored. """ options_help2 = [fill(par.strip(), 70) for par in dedent(options_help1).split("\n\n")] frame = tk.Frame(self.master) frame.pack(fill="x") #options = "IGNORECASE LOCALE MULTILINE DOTALL VERBOSE UNICODE".split() options = "IGNORECASE LOCALE MULTILINE DOTALL VERBOSE".split() letters = "ILMSX" self.vars = [tk.IntVar() for _ in options] self.boxes = [] for var, name, sy, help_txt in zip(self.vars, options, letters, options_help2): val = getattr(re, name) box = tk.Checkbutton(frame, variable=var, text="%s (%s) "%(name,sy), offvalue=0, onvalue=val, command=self.recompile) box.pack(side="left") self.tooltips.append(ToolTip(box, text=help_txt, wraplength=0, delay=500)) self.boxes.append(box) def getflags(self): return sum(var.get() for var in self.vars) def recompile(self, event=None): try: self.compiled = re.compile(self.regexdisplay.get("0.0", "end")[:-1], self.getflags()) bg = self.promptdisplay['background'] self.statusdisplay.config(text="", background=bg) except re.error, msg: self.compiled = None self.statusdisplay.config(text="re.error: %s" % str(msg), background="#ff6565") self.reevaluate() def reevaluate(self, event=None): try: self.stringdisplay.tag_remove("hit0", "1.0", tk.END) except tk.TclError: pass try: self.stringdisplay.tag_remove("hit1", "1.0", tk.END) except tk.TclError: pass try: self.stringdisplay.tag_remove("hit2", "1.0", tk.END) except tk.TclError: pass self.grouplist.delete(0, tk.END) if not self.compiled: return self.stringdisplay.tag_configure("hit0", background="orange") self.stringdisplay.tag_configure("hit1", background="yellow") self.stringdisplay.tag_configure("hit2", background="#ffe800") text = self.stringdisplay.get("1.0", tk.END) ntag = 0 last = 0 nmatches = 0 while last <= len(text): m = self.compiled.search(text, last) if m is None: break first, last = m.span() if last == first: last = first + 1 tag = "hit0" else: tag = ["hit1", "hit2"][ntag % 2] pfirst = "1.0 + %d chars" % first plast = "1.0 + %d chars" % last self.stringdisplay.tag_add(tag, pfirst, plast) if nmatches == 0: self.stringdisplay.yview_pickplace(pfirst) groups = list(m.groups()) groups.insert(0, m.group()) for i in xrange(len(groups)): g = "%2d: %r" % (i, groups[i]) self.grouplist.insert(tk.END, g) nmatches += 1 if self.showvar.get() == "first": break ntag += 1 if nmatches == 0: self.statusdisplay.config(text="(no match)", background="yellow") else: self.statusdisplay.config(text="") #=========================================================================================== ''' Michael Lange The ToolTip class provides a flexible tooltip widget for Tkinter; it is based on IDLE's ToolTip module which unfortunately seems to be broken (at least the version I saw). INITIALIZATION OPTIONS: anchor: where the text should be positioned inside the widget, must be on of "n", "s", "e", "w", "nw" and so on; default is "center" bd: borderwidth of the widget; default is 1 (NOTE: don't use "borderwidth" here) bg: background color to use for the widget; default is "lightyellow" (NOTE: don't use "background") delay: time in ms that it takes for the widget to appear on the screen when the mouse pointer has entered the parent widget; default is 1500 fg: foreground (i.e. text) color to use; default is "black" (NOTE: don't use "foreground") follow_mouse: if set to 1 the tooltip will follow the mouse pointer instead of being displayed outside of the parent widget; this may be useful if you want to use tooltips for large widgets like listboxes or canvases; default is 0 font: font to use for the widget; default is system specific justify: how multiple lines of text will be aligned, must be "left", "right" or "center"; default is "left" padx: extra space added to the left and right within the widget; default is 4 pady: extra space above and below the text; default is 2 relief: one of "flat", "ridge", "groove", "raised", "sunken" or "solid"; default is "solid" state: must be "normal" or "disabled"; if set to "disabled" the tooltip will not appear; default is "normal" text: the text that is displayed inside the widget textvariable: if set to an instance of Tkinter.StringVar() the variable's value will be used as text for the widget width: width of the widget; the default is 0, which means that "wraplength" will be used to limit the widgets width wraplength: limits the number of characters in each line; wraplength=0 means the text is wrapped according to its newlines. default is 150. WIDGET METHODS: configure(**opts) : change one or more of the widget's options as described above; the changes will take effect the next time the tooltip shows up; NOTE: follow_mouse cannot be changed after widget initialization Other widget methods that might be useful if you want to subclass ToolTip: enter() : callback when the mouse pointer enters the parent widget leave() : called when the mouse pointer leaves the parent widget motion() : is called when the mouse pointer moves inside the parent widget if follow_mouse is set to 1 and the tooltip has shown up to continually update the coordinates of the tooltip window coords() : calculates the screen coordinates of the tooltip window create_contents() : creates the contents of the tooltip window (by default a Tkinter.Label) ''' # Ideas gleaned from PySol # Source: http://tkinter.unpythonic.net/wiki/ToolTip class ToolTip: def __init__(self, master, text='Your text here', delay=1500, **opts): self.master = master self._opts = {'anchor':'center', 'bd':1, 'bg':'lightyellow', 'delay':delay, 'fg':'black',\ 'follow_mouse':0, 'font':None, 'justify':'left', 'padx':4, 'pady':2,\ 'relief':'solid', 'state':'normal', 'text':text, 'textvariable':None,\ 'width':0, 'wraplength':150} self.configure(**opts) self._tipwindow = None self._id = None self._id1 = self.master.bind("", self.enter, '+') self._id2 = self.master.bind("", self.leave, '+') self._id3 = self.master.bind("", self.leave, '+') self._follow_mouse = 0 if self._opts['follow_mouse']: self._id4 = self.master.bind("", self.motion, '+') self._follow_mouse = 1 def configure(self, **opts): for key in opts: if self._opts.has_key(key): self._opts[key] = opts[key] else: KeyError = 'KeyError: Unknown option: "%s"' %key raise KeyError ##----these methods handle the callbacks on "", "" and ""---------------## ##----events on the parent widget; override them if you want to change the widget's behavior--## def enter(self, event=None): self._schedule() def leave(self, event=None): self._unschedule() self._hide() def motion(self, event=None): if self._tipwindow and self._follow_mouse: x, y = self.coords() self._tipwindow.wm_geometry("+%d+%d" % (x, y)) ##------the methods that do the work:---------------------------------------------------------## def _schedule(self): self._unschedule() if self._opts['state'] == 'disabled': return self._id = self.master.after(self._opts['delay'], self._show) def _unschedule(self): id = self._id self._id = None if id: self.master.after_cancel(id) def _show(self): if self._opts['state'] == 'disabled': self._unschedule() return if not self._tipwindow: self._tipwindow = tw = tk.Toplevel(self.master) # hide the window until we know the geometry tw.withdraw() tw.wm_overrideredirect(1) if tw.tk.call("tk", "windowingsystem") == 'aqua': tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, "help", "none") self.create_contents() tw.update_idletasks() x, y = self.coords() tw.wm_geometry("+%d+%d" % (x, y)) tw.deiconify() def _hide(self): tw = self._tipwindow self._tipwindow = None if tw: tw.destroy() ##----these methods might be overridden in derived classes:----------------------------------## def coords(self): # The tip window must be completely outside the master widget; # otherwise when the mouse enters the tip window we get # a leave event and it disappears, and then we get an enter # event and it reappears, and so on forever :-( # or we take care that the mouse pointer is always outside the tipwindow :-) tw = self._tipwindow twx, twy = tw.winfo_reqwidth(), tw.winfo_reqheight() w, h = tw.winfo_screenwidth(), tw.winfo_screenheight() # calculate the y coordinate: if self._follow_mouse: y = tw.winfo_pointery() + 20 # make sure the tipwindow is never outside the screen: if y + twy > h: y = y - twy - 30 else: y = self.master.winfo_rooty() + self.master.winfo_height() + 3 if y + twy > h: y = self.master.winfo_rooty() - twy - 3 # we can use the same x coord in both cases: x = tw.winfo_pointerx() - twx / 2 if x < 0: x = 0 elif x + twx > w: x = w - twx return x, y def create_contents(self): opts = self._opts.copy() for opt in ('delay', 'follow_mouse', 'state'): del opts[opt] label = tk.Label(self._tipwindow, **opts) label.pack() root = tk.Tk() demo = ReDemo(root) root.mainloop()