#!/usr/bin/env python r''' faulkner's scriptable text editor. imports configuration variables from config.flauxtext when imported. also tries to import psyco, a python JIT compiler: http://psyco.sf.net flauxtext is released under the GNU General Public License Version 2. if you should need my full legal name [which does not contain anything like 'faulkner'], you may contact me at faulkner@users.sf.net usage: flauxtext [-] [--help|-h] [--test|-t] [--version|-v] [--profile [file_to_write]] \ [--noalpha] [--text|-T|-x] [file_to_open [file_to_open [...]]] \ [dirname [dirname [...]]] [--regexp|-r "regexp" dirname [dirname [...]]] - read stdin until EOF, put it into the first buffer --help, -h print this help message and exit --test run doctest on this module [ignores any other commandline opts] then exit --profile [file_to_write] run profile on this module [if file_to_write does not yet exist, i will write the profile output to it] when this opens flauxtext, play around with a few tools and functions and whatnot --version, -v print my version number and exit --noalpha ignore transparency setting in config.flauxtext, and be completely opaque transparency only works on winXP with the win32all package anyway to get the same effect, you could set "transparency" in config.flauxtext to False --regexp, -r open all files matching regexp in all dirnames following -r|--regexp; you don't need to quote regexp this must be the last option handiest values for regexp: "(?i).*\.pyw?$" # this matches all files ending in .py, .pyw, .PY, .PYW "(?i).*\.(c|cpp|h)$" # open all files ending in .c, .cpp, .h, .C, .CPP, .H, .Cpp, .cPp, .cpP ".*~$" # open all files ending in ~ [tilde] note: you must quote the pattern. --text, -T, -x open only text files of those specified on the commandline; do not try to open binary files --notrack, -n ignore the instancetracking configuration variable, do not alert previous instances, do open a new instance fair warning: i may hang for a while if you pass me too many dirnames, or dirname is an exceptionally large dir. command-line examples: flauxtext . opens all files in current directory flauxtext -x / opens all text files in root directory on POSIX systems [for windows, reverse the slash ;-] flauxtext --profile %home%\f%date%.pro writes a log of my profile to a file in your %HOME% directory, whose name contains the current date flauxtext -t > /tmp/f.test writes a log of my doctest to that temp file [the pipe and slashes indicate a POSIX compliant OS] flauxtext -nxr ".*~$" . DOES NOT WORK flauxtext's handling of options is primitive, so you must expand "-nxr" out to "-n -x -r" python command-line examples: >>> ft = flauxtext.flauxtext(['C:\\autoexec.bat','c:\\program files\\flauxtext\\config.flauxtext']) >>> ft.open('C:\\wepkey.txt') >>> assert ft.start == ft.__call__ >>> ft() ''' try: import psyco # a JIT compiler psyco.profile() except: pass import gtk, pango, gobject, os, sys, re, string, fnmatch, copy, threading, time, xml.parsers.expat try: from subprocess import Popen as subprocess_Popen Popen = lambda cmd: subprocess_Popen(cmd, shell=True) except: import threading Popen = lambda cmd: threading.Thread(target=os.popen, args=(cmd,)).start() if gtk.pygtk_version < (2,4): print "i'm sorry, but i need PyGTK 2.4 or later.\nyou can download the latest version of pygtk here:\nhttp://www.pygtk.org/" sys.exit(1) PATH = [os.path.dirname(os.path.abspath(sys.argv[0])), os.path.abspath('.'), os.path.abspath('..')] DND_TARGETS = [('application/octet-stream', 0, 8), # octet=8 ('TEXT', 0, 23), # string.lowercase.index('x')=23 ('STRING', 0, 6), # len('string')=6 ('text/uri', 0, 43), # len('text')=4,len('uri')=3 ] SEARCH_ENGINES = { 'Google':'"http://www.google.com/search?q', 'GoogleImgs':'"http://images.google.com/images?q', 'Freshmeat':'"http://freshmeat.net/search/?q', 'Wikipedia':'"http://en.wikipedia.org/w/wiki.phtml?search', 'OED':'"http://dictionary.oed.com/cgi/findword?query_type=word&queryword', 'MWThes':'"http://www.m-w.com/cgi-bin/thesaurus?va', 'MW':'"http://www.m-w.com/cgi-bin/dictionary?va', 'SF':'"http://sourceforge.net/search/?words', } WIN32_PRINTERS_LEVEL = 5 ESC_KEYVAL = 65307 TAB_KEYVAL = 65289 SHIFT_TAB_KEYVAL = 65056 CAPS_LOCK_KEYVAL = 65509 UNTITLED_FILENAME = 'untitled' gtk.STOCK_NONE = None FIND_FIND = 1 FIND_FIND_ALL = 2 FIND_REPLACE = 3 FIND_REPLACE_ALL = 4 FIND_HERE_FORWARD = 0 FIND_AFTER_START = 1 FIND_HERE_BACKWARD = 2 FIND_BEFORE_END = 3 BINARY_TEXT_FILTER_RATIO = 0.3 txtchars = ''.join(map(chr, range(32, 127))) + '\n\r\t\b' def istextfile(path, blocksize=512): ''' returns True if path is a text file, False otherwise, based on the first blocksize chars in path. ''' if not os.path.isfile(path): return False s = open(path).read(blocksize) if "\0" in s: return False if not s: return True # empty files are text if float(len(s.translate(string.maketrans('', ''), txtchars)))/float(len(s)) > BINARY_TEXT_FILTER_RATIO: return False return True xml_encode = lambda s: ''.join([('&#%d;' % ord(x)) for x in s]) html_color_to_gdk = lambda s: gtk.gdk.Color(*tuple([256 * int(s[i:i+2], 16) for i in range(0, 5, 2)])) def imp0rt(*nyms): ''' convenience function for safely importing modules into the global namespace, doesn't catch ImportErrors ''' for nym in nyms: try: eval(nym) except: exec 'global %s; import %s' % (nym.split('.')[0], nym) def getmyfile(fnym, hugebool=False, readlen=1023, mk=True): ''' takes a file basename, looks in my global PATH list, returns a full pathname. if fnym is not in PATH and hugebool evaluates to True, uses 'cd \\ && dir /s /b ' on nt, or 'locate ' on posix to get a list of candidates. if 'flauxtext' is in the first readlen bytes of a file in this list, that candidate's pathname is returned. if all that fails and mk evaluates to True, make the file in os.path.dirname(__file__), and return the new path. ''' for pth in PATH: if os.path.isfile(os.path.join(pth, fnym)): return os.path.join(pth, fnym) if hugebool: if os.name=='nt': lookcmd='cd \\ && dir /s /b ' else: lookcmd='locate ' for lin in os.popen(lookcmd+fnym): if os.path.isfile(lin) and 'flauxtext' in open(lin).read(readlen): return lin if mk: newfnym=os.path.join(os.path.dirname(__file__),fnym) open(newfnym,'w').close() # create an empty file return newfnym configfn = getmyfile('config.flauxtext') contactsfn = getmyfile('contacts.flauxtext') mrufn = getmyfile('history.flauxtext') specialsfn = getmyfile('specials.flauxtext') mruxmlfn = getmyfile('mru.xml') uixmlfn = getmyfile('ui.xml') # User Interface eXtended Markup Language FileName--yay, acronyms! specialsxmlfn = getmyfile('specials.xml') uixmlf = open(uixmlfn) uixmldata = uixmlf.read() uixmlf.close() mruxmlf = open(mruxmlfn) mruxmldata = mruxmlf.read() mruxmlf.close() specialsxmlf = open(specialsxmlfn) specialsxmldata = specialsxmlf.read() specialsxmlf.close() del uixmlf, mruxmlf, specialsxmlf execfile(configfn, globals()) class config: execfile(configfn, globals(), locals()) instancetrackingfn = os.path.join(tempdir, 'instancetracking.flauxtext') def imp0rt_plugin(plg): ''' first tries to evaluate the name in plg if that fails, it __import__s plg into the global namespace after changing sys.path to [plugindir] ''' if plg.startswith('plugin'): plg = plg[6:] try: eval('plugin' + plg) except: try: real_path = sys.path sys.path = [plugindir] + sys.path globals()['plugin' + plg] = __import__(plg) eval('plugin' + plg).flauxtext = Namespace() sys.path = real_path except Exception, e: print '\nerror loading plugin:', plg, '...', e def __imp0rt_plugin__(plg): ''' equivalent of __import__ for plugins. ''' imp0rt_plugin(plg) if not plg.startswith('plugin'): plg = 'plugin' + plg return eval(plg) if proxyserver != '': imp0rt('urllib2') authinfo = urllib2.HTTPBasicAuthHandler() authinfo.add_password(None, proxyserver, proxyname, proxypass) urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler({'http':proxyserver}), authinfo, urllib2.CacheFTPHandler)) def respond_dialog_on_escape(d, r=0): ''' send response r to dialog d when you hit the escape key inside it. ''' def callback(wid, evt): if evt.keyval == ESC_KEYVAL: wid.response(r) d.connect('key-press-event', callback) class Namespace: ''' allows you to treat this global namespace as an object with fields as opposed to a dictionary. passed to plugins so they can treat the application as if it were a module like any other. ''' def __init__(self): self.__dict__ = globals() class UndoStackEntry: def __init__(self, *a, **kw): self.was_inserted = kw.get('was_inserted', True) self.data = kw.get('data', '') self.offset = kw.get('offset', 0) def __repr__(self): return '<%s %d>%s' % (['deleted', 'inserted'][self.was_inserted], self.offset, self.data) def adjust_offset(self, d_offset): self.offset += d_offset def append_data(self, new_data): self.data += new_data class UndoStack: def __init__(self): self.stack = [] self.pointer = -1 def previous(self): if self.pointer > 0: return self.stack[pointer - 1] def current(self): try: return self.stack[self.pointer] except: return None def next(self): if self.pointer < len(self.stack): return self.stack[self.pointer + 1] def push(self, entry): self.stack.append(entry) self.pointer += 1 def __len__(self): return len(self.stack) def __getitem__(self, index): return self.stack[index] def __setitem__(self, index, value): self.stack[index] = value def __eq__(self, o): return self.__dict__ == o.__dict__ class FlauxText: ''' self.vbox contains self.notebk [a gtk.Notebook], self.menubar, optional self.toolbar, 4 optional status bars in self.statbarhbox. Example: >>> from flauxtext import * # import all the modules i import, along with this class >>> f = FlauxText() >>> f.w = gtk.Window(); f.w.add(f.vbox) >>> f.w.show_all(); gtk.main() if, in your own pygtk app, you want my hotkeys to work, you need to connect "f.uiman" to your window. open my source and find "def __call__" to see how to do that. if you want FlauxText to manage your window's title and icon [and parent the modal dialogs, etc.], set your toplevel window as the "w" attribute of FlauxText, as i did above. the muggle [non-magical] methods are alphabetized in my source. if you want to disconnect events in textbuffers or textviews from their handlers, use the_widget.disconnect(self.handler_ids[self.notebk.get_current_page()]['event-name']) methods beginning with 'callback_' correspond to actions in ui.xml. to learn about what i do in __init__ without grokking the source: >>> import flauxtext, pprint >>> ft=flauxtext.FlauxText() >>> pprint.pprint(ft.__dict__) ''' # __init__ at end; start = __call__ in magic methods # find "# XXX" to find sad/angry callbacks clipboard = gtk.clipboard_get() # MAGIC METHODS! # not alphabetized like all other methods; grouped logically def __call__(self, filenames=()): ''' makes an otherwise empty window, adorns it, and puts an instance of FlauxText in it. oh, and it opens files passed from the command line with open(). besides a list of strings, "filenames" can also be a single string representing a pathname. ''' def house_keeping(): ''' called every half second or so, keeps house: open files in instancetracking.flauxtext if necessary update self.statbar_cursor with the current cursor position ''' # open files in instancetracking if self.tempfilemtime < os.path.getmtime(instancetrackingfn): insff = open(instancetrackingfn) filestoopen = re.split('\r*\n*', insff.read()) insff.close() for i in filestoopen: if os.path.isfile(i): self.open(i) self.tempfilemtime = os.path.getmtime(instancetrackingfn) self.w.present() # update line, column numbers and insert/overwrite tv = self.current_textview() buf = tv.get_buffer() ti = buf.get_iter_at_mark(buf.get_mark("insert")) self.statbar_cursor.pop(1) self.statbar_cursor.push(1, statbarcursorformat % (ti.get_line()+1,ti.get_visible_line_offset())) self.statbar_insovr.pop(1) self.statbar_insovr.push(1, (tv.get_overwrite() and "ovr" or "ins")) return True # tell gobject.timeout_add to keep calling this method self.w = gtk.Window(gtk.WINDOW_TOPLEVEL) self.w.connect('destroy-event', self.callback_quit) self.w.connect('delete-event', self.callback_quit) if usectrltabs: self.w.connect('key-press-event', self.cycle_tabs__) self.w.set_title(windowtitle % self.current_filename()) self.w.set_icon(gtk.gdk.pixbuf_new_from_file(getmyfile('icon.xpm'))) self.w.add(self.vbox) gobject.timeout_add(updatetitlemilliseconds, house_keeping) self.w.add_accel_group(self.accelgroup) if winderpos and isinstance(winderpos, (list, tuple)) and len(winderpos) == 2: self.w.move(*winderpos) if windersize and isinstance(windersize, (list, tuple)) and len(windersize) == 2: self.w.resize(*windersize) if maxwindowatstart: self.w.maximize() self.w.show() execfile(os.path.join(plugindir, '__call__.py'), globals(), locals()) # open files requested on the cli if isinstance(filenames, (str, unicode)): filenames = [filenames] elif not isinstance(filenames, (list, tuple)): print "the only argument to my constructor has to be either a string or an iterable series of strings. [unicodes are strings.]", print "i don't understand", filenames filenames = [] if '-' in filenames: def pipe(): txt = sys.stdin.readline() textbuffer = self.current_buffer() textbuffer.filename = 'sys.stdin' while txt != '': textbuffer.insert(textbuffer.get_end_iter(), txt) txt = sys.stdin.readline() imp0rt('threading') threading.Thread(target=pipe).start() filenames.remove('-') for i in filenames: if i in os.listdir('.'): i = os.path.abspath(i) if os.path.isfile(i): self.open(i) elif os.path.isdir(i): map(self.open, [os.path.join(i, j) for j in os.listdir(i) if istextfile(os.path.join(i, j))]) self.current_textview().grab_focus() gtk.main() return 0 def __contains__(self, i=UNTITLED_FILENAME): for b in self: if (b.filename == i) or (i == os.path.basename(b.filename)): return True return False def __str__(self, which=None): ''' return the text in which buffer. if which==None:return text in current buffer. ''' if which == None: which = self.notebk.get_current_page() return self[which].get_all_text() def __list__(self): ''' return a list of my buffers. ''' return [b for b in self] def __iter__(self): ''' yields self[i] for i in range(len(self)) ''' for i in xrange(len(self)): yield self[i] def __getitem__(self, whch=Ellipsis): ''' allows you to treat an instance of this class as a list, dict, or something else entirely: >>> flauxtext.FlauxText("C:\\autoexec.bat")[0] >>> flauxtext.FlauxText("C:\\autoexec.bat")['C:\\autoexec.bat'] returns a gtk.TextBuffer containing the contents of c:\\autoexec.bat >>> flauxtext.FlauxText(glob.glob('*.txt'))[...] returns a list of all my gtk.TextBuffer()s >>> flauxtext.FlauxText(glob.glob('*.txt'))[[1,3]] >>> flauxtext.FlauxText(glob.glob('*.txt'))[(1,3)] both return lists of my first two TextBuffers ''' if whch == Ellipsis: return list(self) elif whch == None: return self.current_buffer() elif isinstance(whch, slice): return [self[i] for i in range(whch.start, whch.stop, isinstance(whch.step, int) and whch.step or 1)] elif isinstance(whch, (int, long)) and (0 <= whch < len(self)): return self.notebk.get_nth_page(whch).get_child().get_buffer() elif isinstance(whch, str): for i, b in enumerate(self): if whch in [b.filename, os.path.basename(b.filename)]: return self[i] elif isinstance(whch, (tuple, list)): return [self[i] for i in whch] def __getslice__(self, *args): ''' args are apply()d to range ''' return [self[i] for i in range(*args)] def __len__(self): return self.notebk.get_n_pages() def __le__(self, other): return (type(self) == type(other)) and (False not in [(i in other.all_files()) for i in self.all_files()]) def __ge__(self, other): return (type(self) == type(other)) and other.__le__(self) def __eq__(self, other): return (type(self) == type(other)) and (self >= other) and (self <= other) def __lt__(self, other): return (type(self) == type(other)) and not (self >= other) def __gt__(self, other): if type(self) != type(other): return False else: return not self <= other def __ne__(self, other): if type(self) != type(other): return False else: return not self == other def __add__(self, otherself, v=True, expand=True, fill=True, padding=0): ''' usage: newselfvbox = ft + flauxtext.FlauxText() takes 2 instances of FlauxText, packs both their main vboxes into a new VBox [if v; else: HBox], and returns that new box. ''' if v: self.vbox.pack_start(otherself.vbox,expand,fill,padding) return otherself.vbox else: h=gtk.HBox() h.show() h.pack_start(self.vbox()) h.pack_start(otherself.vbox) return h def __iadd__(self, otherself, *a): ''' ft += flauxtext.FlauxText() is the in-place version of ft.vbox = ft + flauxtext.FlauxText() ''' self.vbox = self.__add__(otherself, *a) def __radd__(self,otherself,v=True,expand=True,fill=True,padding=0): ''' proxy for __add__; does exactly the same thing, only Reversed ''' if v: otherself.vbox.pack_start(self.vbox,expand,fill,padding) return otherself.vbox else: h=gtk.HBox() h.show() h.pack_start(otherself.vbox()) h.pack_start(self.vbox) return h def __mul__(self, many, v=True): ''' usage: newselfvbox=self*many if you want newselfvbox to hold only an HBox which holds many selves, use: newselfvbox=self.__rmul__(many,False) ''' if many < 1: many = 1 if v: b = gtk.VBox() else: b = gtk.HBox() imp0rt('copy') for i in range(many - 1): b.pack_start(copy.deepcopy(self.vbox)) return self.vbox def __imul__(self, many, v=True): ''' self *= many ''' self.vbox = self.__mul__(many, v) def __rmul__(self, many, v=True): ''' usage: newselfvbox=many*self if you want newselfvbox to hold only an HBox which holds many selves, use: newselfvbox=self.__rmul__(many,False) this method is a proxy for __mul__ ''' return self.__mul__(many,v) def __pow__(self, side): ''' makes a square grid of side ** 2 me's ''' v = gtk.VBox() v.show() h = gtk.HBox() h.show() h.pack_start(self.vbox) imp0rt('copy') for i in range(side-1): h = gtk.HBox() h.show() v.pack_start(h) for i in range(side): h.pack_start(copy.deepcopy(self.vbox)) return v def __ipow__(self, side): ''' ''' self.vbox = self ** side def __rpow__(self,side): ''' nonsensical, but usage: gridselfvbox=side**self this method is a proxy for __pow__, for consistency ''' return self.__pow__(side) # end magic methods def all_files(self): return [b.filename for b in self] def autoindent(self, txtb): ''' called whenever you change the text in txtb [the current gtk.TextBuffer] can't be put in registerwithundo for strange scheduling reasons. :-P ''' curundoentry = self.current_undo_entry() curfn = self.current_filename() insitr = txtb.get_iter_at_mark(txtb.get_insert()) ins_str = '' if self.autoident and (curundoentry != None) and (curundoentry.was_inserted) and (curundoentry.data[-1] == '\n'): prevline = txtb.get_text(txtb.get_iter_at_line(insitr.get_line()-1), txtb.get_iter_at_mark(txtb.get_insert()))[:-1] ins_str = re.split('[^ \t]*', prevline)[0] for key, val in commentchars.items(): if fnmatch.fnmatch(curfn, key) and isinstance(val, str): prevline = prevline.split(val)[0].rstrip() break if (len(prevline) > 0) and (prevline[-1] in ':{(['): ins_str += spaceselectedinsertstring if rightedge and (insitr.get_line_offset() >= rightedgecol) and (len(ins_str) < rightedgecol-1): ins_str = '\n' + ins_str txtb.insert_at_cursor(ins_str) def callback_add_remove_special(self, action): ''' either adds or removes the current file from the specials list. ''' fn = self.current_filename() f = open(specialsfn) ls = re.split('\r?\n?', f.read()) f.close() if fn in ls: ls.remove(fn) self.specialfilelist = ls f = open(specialsfn,'w') f.write('\n'.join(ls)) else: self.specialfilelist.append(fn) f = open(specialsfn,'a') # append filename f.write('\n'+fn) f.close() self.uiman.remove_ui(self.uiman.merge_ids['specials']) specials_xml_data = '' for i in xrange(len(self.specialfilelist)): if os.path.isfile(self.specialfilelist[i]): specials_xml_data += '' % ( i, (i + 1), xml_encode(os.path.basename(self.specialfilelist[i].replace('_', '__'))), xml_encode(self.specialfilelist[i]) ) self.xml_to_ui(specialsxmldata.replace('', specials_xml_data), 'specials') def callback_chtheme(self, action=None): ''' Popen(gtkchthemecmd) ''' Popen(gtkchthemecmd) def callback_close_all_but_current(self, action=None): ''' loops backwards through my pages: closes the page if it isn't current ''' cur = self.notebk.get_current_page() for i in range(self.notebk.get_n_pages() - 1, -1, -1): if i != cur: self.close_buffer(None, i) def callback_close_current(self, *a): ''' alias of close_buffer. ''' self.close_buffer(*a) def callback_colors(self, action=None): ''' opens a dialog containing a gtk.ColorSelection widget and some buttons, lets you change the text and background colors, and insert rgb values in various formats into the current buffer: #rrggbb, (rr,gg,bb,aa), (rr,gg,bb), <-- hex #rrrgggbbb, (rrr,ggg,bbb,aaa), (rrr,ggg,bbb), <-- decimal ''' curbuf = self.current_buffer() d = self.mk_dialog("what is your favorite color?", ('done', 0)) c = gtk.ColorSelection() d.vbox.pack_start(c) c.set_has_palette(True) c.set_has_opacity_control(True) c.set_previous_alpha(65535) c.set_current_alpha(65535) try: c.set_current_color(html_color_to_gdk(self.current_selection_bounds_or_all(True).strip('#'))) except: pass def set_all_text_color(btn): for txtb in list(self): t = gtk.TextTag('becky') t.set_property('foreground-gdk', c.get_current_color()) try: txtb.get_property('tag-table').remove(curbuf.get_property('tag-table').lookup('becky')) except: pass txtb.get_tag_table().add(t) curbuf.apply_tag_by_name('becky', curbuf.get_start_iter(), curbuf.get_end_iter()) def set_text_color(btn): t = gtk.TextTag('becky') t.set_property('foreground-gdk', c.get_current_color()) try: curbuf.get_property('tag-table').remove(curbuf.get_property('tag-table').lookup('becky')) except: pass curbuf.get_tag_table().add(t) curbuf.apply_tag_by_name('becky', curbuf.get_start_iter(),curbuf.get_end_iter()) def set_all_background_color(btn): for txtb in list(self): t=gtk.TextTag('bg-gdk') t.set_property('foreground-gdk',c.get_current_color()) try: txtb.get_property('tag-table').remove(curbuf.get_property('tag-table').lookup('bg-gdk')) except: pass txtb.get_tag_table().add(t) curbuf.apply_tag_by_name('becky',curbuf.get_start_iter(),curbuf.get_end_iter()) def set_background_color(btn): t=gtk.TextTag('bg-gdk') t.set_property('background-gdk',c.get_current_color()) try: curbuf.get_property('tag-table').remove(curbuf.get_property('tag-table').lookup('bg-gdk')) except: pass curbuf.get_tag_table().add(t) curbuf.apply_tag_by_name('bg-gdk',curbuf.get_start_iter(),curbuf.get_end_iter()) def insert(btn): if 'a' in btn.get_label(): alpha = c.get_current_alpha()/256 else: alpha = False tuple_bool = ('(' in btn.get_label() and ',' in btn.get_label()) # :-) hex_bool = (not 'rrr' in btn.get_label() and not 'bbb' in btn.get_label()) gdk_color = c.get_current_color() r = gdk_color.red / 256 g = gdk_color.green / 256 b = gdk_color.blue / 256 if alpha is False: a = '' else: a = alpha if hex_bool: r, g, b = map(lambda x: hex(x)[2:], [r, g, b]) rj = 2 try: a = hex(a)[2:] except: pass else: r, g, b, a = map(str, [r, g, b, a]) rj = 3 r, g, b = map(lambda x: x.rjust(rj).replace(' ', '0'), [r, g, b]) if len(a) > 0: a = a.rjust(rj).replace(' ', '0') if tuple_bool: s = '(' + ','.join((r, g, b, a)) + ')' else: s = '#' + r + g + b + a curbuf.insert_at_cursor(s.replace(',)', ')')) for button_set in [ [('set text color', set_text_color), ('set all text color', set_all_text_color)], [('set background color', set_background_color), ('set all background color', set_all_background_color)], [('insert #rrggbb', insert), ('insert #rrrgggbbb', insert)], [('insert (rr,gg,bb)', insert), ('insert (rrr,ggg,bbb)', insert)], [('insert (rr,gg,bb,aa)', insert), ('insert (rrr,ggg,bbb,aaa)', insert)], ]: hbox = gtk.HBox() d.vbox.pack_start(hbox) for button_text, callback in button_set: button = gtk.Button(button_text) button.connect('clicked', callback) hbox.pack_start(button) d.run() def callback_copy_current_path_to_clipboard(self, action=None): ''' copy the current file's full pathname to your clipboard. ''' self.clipboard.set_text(self.current_filename()) def callback_cvs_commit(self, action=None): ''' commit current file to your CVS repository ''' if self.current_filename() == UNTITLED_FILENAME: self.flash_info("YOU MUST SAVE/OPEN A FILE BEFORE YOU CAN COMMIT IT TO CVS.", 4e3, 'cvs_commit_bad_filename') return d = self.mk_dialog('cvs options?', (gtk.STOCK_OK, 1, gtk.STOCK_CANCEL, 0)) d.vbox.pack_start(gtk.Label("change current working directory to")) c = gtk.Entry() d.vbox.pack_start(c) c.set_text(os.path.dirname(self.current_filename())) d.vbox.pack_start(gtk.Label("then run\n'" + cvscmd + "' +")) e = gtk.Entry() d.vbox.pack_start(e) e.set_text(self.cvsops) e.connect('activate', lambda x=None,y=None: d.response(1)) d.vbox.pack_start(gtk.Label('CVSROOT = ' + os.environ.get('CVSROOT','[undefined]'))) d.vbox.pack_start(gtk.Label('CVS_RSH = ' + os.environ.get('CVS_RSH','[undefined]'))) self.cvsops = e.get_text() if d.run(): os.chdir(c.get_text()) Popen(termcmd + cvscmd + self.cvsops) def callback_delete(self, action=None): ''' permanently deletes the file in current buffer, then closes it. ''' fn = self.current_filename() d = self.mk_dialog('Are you sure?', (gtk.STOCK_NO,1,gtk.STOCK_YES,0)) d.vbox.pack_start(gtk.Label('do you want to permanently delete this file?\n'+fn)) if d.run(): return rmcmd = ['rm ', 'del '][os.name == 'nt'] if 0 == os.system(rmcmd + fn): self.flash_info(fn + ' SUCCESSFULLY DELETED', 4000, 'deleted') else: self.flash_info("i couldn't delete " + fn, 4000, 'cannot_delete') self.manage_mru(fn) self.current_buffer().set_text('') self.notebk.set_tab_label_text(self.notebk.get_nth_page(self.notebk.get_current_page()), UNTITLED_FILENAME) if len(self) == 1: self.new_buffer() self.close_buffer() def callback_diff(self, action=None): ''' generate a diff file, open it in a new buffer, and offer to save it ''' oldfn = self.current_filename() d = gtk.FileChooserDialog("diff current_file with what file?",None,gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL,0,gtk.STOCK_OPEN,1)) d.set_property("show-hidden", openshowhidden) d.set_default_response(1) d.set_current_folder(os.path.dirname(oldfn)) r = d.run() fn = d.get_filename() d.destroy() if r: imp0rt('difflib') self.new_buffer().set_text('\n'.join([ i for i in difflib.context_diff( re.split('\r?\n?', self.current_text()), re.split('\r?\n?', open(fn).read()) ) ] )) def callback_edit_my(self, what, *a): ''' depending on action.get_name(), opens one of my text files [source, config file, ui.xml, etc.]. ''' d = { 'config': 'configfn', 'contacts': 'contactsfn', 'history': 'mrufn', 'readme': 'getmyfile("readme.txt")', 'regexp': 'getmyfile("regexp.txt")', 'source': '__file__', 'specials': 'specialsfn', 'uixml': 'uixmlfn', } if what in d: if what == 'source': open(os.path.join(os.path.dirname(__file__),'backupsrc.pyw'),'w').write(open(__file__).read()) self.open(eval(d[what])) else: self.flash_info('sorry, but i forgot what that file was', 1e4, 'edit_my_what') def callback_email_faulkner(self, action): ''' opens a Dialog giving you my email address and instructions on how to use FlauxText to email me. ''' d = self.mk_dialog('help!', (gtk.STOCK_OK,1)) d.vbox.pack_start(gtk.Label("copy this address to your clipboard," + " then open a new document and compose an email to the author of FlauxText." + "\nthis email should include a complete description of your trouble with his software, " + "\nany pertinent output to the terminal from which you started FlauxText," + "\nversion numbers of python, GTK, PyGTK, and your operating system OR lavish praise and thanksgiving." + "\n\nwhen you've written this email, hit ^m [Control+m], or WebTools > Email." + " paste the address below into the 'to' field." )) e = gtk.Entry() e.set_text("faulkner@users.sf.net") d.vbox.pack_start(e) d.run() def callback_exec(self, action=None): ''' exec's the current buffer ''' try: execfile(execrc) except: pass try: StringIO except: from StringIO import StringIO imp0rt('traceback') _stdout_ = sys.stdout sys.stdout = sys.__stdout__ = StringIO() try: exec self.current_text() in globals(), locals() except: self.new_buffer().set_text('\n'.join(traceback.format_exception(*sys.exc_info()))) ss = sys.stdout sys.stdout = sys.__stdout__ = _stdout_ oput = ss.getvalue() ss.close() if oput != '': self.new_buffer().set_text(re.sub('[^%s]' % txtchars, '', oput)) def callback_filemanager(self, action=None): ''' changes working directory to that of the current file, and calls either termcmd or filemanagercmd, depending on action.get_name(). set termcmd and filemanagercmd in either the [nt] or [posix] section of config.flauxtext, depending on your OS. ''' if self.current_filename() == UNTITLED_FILENAME: dirnym = os.sep # open a terminal at the root else: dirnym = os.path.dirname(self.current_filename()) try: os.chdir(dirnym) except OSError: pass Popen(filemanagercmd % tuple([dirnym]*(len(filemanagercmd.split('%s'))-1))) def callback_find(self, action=None): ''' show the tip of the iceberg of the find function. your input will be passed to self.find, which will sort all the data out and call one of 4 other methods, all of whose names begin with the word "find". you can change the layout of the dialog by moving around lines in FlauxText.find() [in the source] ''' completion_list = gtk.ListStore(str) for string in self.find_history: completion_list.append([string]) completion = gtk.EntryCompletion() completion.set_model(completion_list) completion.set_text_column(0) # set up the widgets. scroll down to see how the format is created d = gtk.Dialog('we seek the holy grail!', self.w, gtk.DIALOG_NO_SEPARATOR | gtk.DIALOG_DESTROY_WITH_PARENT, ('_find next', FIND_FIND, '_hilite all', FIND_FIND_ALL, '_replace', FIND_REPLACE, 'replace _all', FIND_REPLACE_ALL)) d.connect('key-press-event', lambda wid,evt:[wid.destroy() for i in [0] if evt.keyval==ESC_KEYVAL]) findl = gtk.Label("find") finde = gtk.Entry() finde.set_completion(completion) finde.connect('activate',lambda x:d.response(1)) findre = gtk.CheckButton("re_gexp") repl = gtk.Label("replace with") completion = gtk.EntryCompletion() completion.set_model(completion_list) completion.set_text_column(0) repe = gtk.Entry() repe.set_completion(completion) repe.connect('activate',lambda x:d.response(3)) repre = gtk.CheckButton("rege_xp") if findcombobox: # midnight hack; apologies cmbodirbx = gtk.combo_box_new_text() cmbodirbx.append_text('start here, go forward') cmbodirbx.append_text('start here, go backward') cmbodirbx.append_text('start at the beginning') cmbodirbx.append_text('start at the end') cmbodirbx.set_active(0) else: directio = gtk.CheckButton("_backwards") findcase = gtk.CheckButton("re._I") findcase.set_active(True) # ignore case findmlin = gtk.CheckButton("re._M") findmlin.set_active(True) # multilin--'^' matches starts of lines finddtal = gtk.CheckButton("re._S") finddtal.set_active(True) # dotall--'.' also matches newline findloca = gtk.CheckButton("re._L") findloca.set_active(True) # locale sensitive onesht = gtk.CheckButton("_1") onesht.set_active(findoneshot) onesht.connect("toggled", lambda *a: globals().__setitem__('findoneshot', not findoneshot)) fst_cb = gtk.CheckButton("fs_t") fst_cb.set_active(fstinit) fst_tv = gtk.TextView() fst_tb = fst_tv.get_buffer() fst_tb.set_text(fstinittext) fst_tv.set_property('no-show-all', not fstinit) fst_cb.connect('toggled', lambda fst_cb_=fst_cb: [fst_tv.set_property('visible', fst_cb.get_active()), d.resize(1, 1)]) # FORMAT OF TEH FIND DIALOG mhbox = gtk.HBox() d.vbox.pack_start(mhbox) # the biggest box; only one in d.vbox lvbox = gtk.VBox() mhbox.pack_start(lvbox) # left vertical box in the biggest horizontal box rvbx = gtk.VBox() mhbox.pack_start(rvbx) # right vbox in the biggest hbox fhbx = gtk.HBox() lvbox.pack_start(fhbx) # hbox holding the Find label, text entry, and regexp checkbutton rhbx = gtk.HBox() lvbox.pack_start(rhbx) # hbox holding the Replace label, text entry, and regexp checkbutton tinyhbx = gtk.HBox() # small hbox in the top of the right vbox holding 4 checkbuttons ittyhbx = gtk.HBox() fhbx.pack_start(findl) fhbx.pack_start(finde) fhbx.pack_start(findre) # put the find text entry and find by regexp checkbutton in fhbx rhbx.pack_start(repl) rhbx.pack_start(repe) rhbx.pack_start(repre) # put replace '''" in rhbx rvbx.pack_start(tinyhbx) # put the tinyhbx in the right vbox rvbx.pack_start(ittyhbx) ittyhbx.pack_start(onesht) if findcombobox: ittyhbx.pack_start(cmbodirbx) # put combobox in the right vbox else: ittyhbx.pack_start(directio) ittyhbx.pack_start(fst_cb) tinyhbx.pack_start(findcase) # put the re flags checkbuttons in the tinyhbx tinyhbx.pack_start(findmlin) tinyhbx.pack_start(finddtal) tinyhbx.pack_start(findloca) d.vbox.pack_start(fst_tv) d.add_button('_done', 0).connect("clicked",lambda b: d.destroy()) def call_find(dialog, response_id): if fst_cb.get_active(): fst_code = fst_tb.get_text(fst_tb.get_start_iter(), fst_tb.get_end_iter()) else: fst_code = None if findcombobox: start_direction = cmbodirbx.get_active() else: start_direction = directio.get_active() if response_id != 0: self.find( find_str=finde.get_text(), replace_str=repe.get_text(), search_as_regexp=findre.get_active(), replace_as_regexp=repre.get_active(), directive=response_id, fst_code=fst_code, start_direction=start_direction, re_flags={ re.I : findcase.get_active(), re.M : findmlin.get_active(), re.S : finddtal.get_active(), re.L : findloca.get_active(), }, ) d.connect('response', call_find) d.show_all() d.run() if eval('findoneshot'): d.destroy() def callback_find_again(self, action=None): ''' ''' d = self.findagaininfo d['start_direction'] = FIND_HERE_FORWARD return self.find(**d) def callback_find_previous(self, action=None): ''' ''' d = self.findagaininfo d['start_direction'] = FIND_HERE_BACKWARD return self.find(**d) def callback_font(self, action=None): ''' opens a font dialog, allowing you to set the font of the current or all buffers. ''' d = self.mk_dialog('pick a font, any font', ('Apply', 1, 'Apply to All', 2, 'Cancel', 0)) f = gtk.FontSelection() d.vbox.pack_start(f) f.set_font_name(font) r = d.run() if r: newfont = f.get_font_name() if r is 1: self.current_textview().modify_font(pango.FontDescription(newfont)) elif r is 2: for i in range(self.notebk.get_n_pages()): self.notebk.get_nth_page(i).get_child().modify_font(pango.FontDescription(newfont)) def callback_font_resize(self, *a): ''' adds ((the first argument that can be coerced to an int) or 120) to the current font's size ''' a = list(a) + [120] for i in a: try: dsize = int(a[0]) break except: pass v = self.current_textview() f = v.get_property('style').font_desc f.set_size(f.get_size() + dsize) v.modify_font(f) def callback_get_output(self, action=None): ''' open a tiny dialog asking for a command to run, open a new buffer containing the output from that command ''' d = self.mk_dialog('command me', (gtk.STOCK_OK,1,gtk.STOCK_CANCEL,0)) respond_dialog_on_escape(d) e = gtk.Entry() e.connect('activate',lambda x:d.response(1)) d.vbox.pack_start(e) d.vbox.pack_start(gtk.Label("whatever you enter here will be run\nas if you'd run it from the command line.")) if d.run(): self.new_buffer().set_text(os.popen(e.get_text()).read()) def callback_goto_line(self, action=None): ''' opens a Dialog asking you for an int or "end", then calls getlinewcb. ''' d = self.mk_dialog('line', (gtk.STOCK_OK,1,gtk.STOCK_CANCEL,0)) e = gtk.SpinButton(gtk.Adjustment(1, 0, self.current_buffer().get_line_count(), 1, 10), 1) e.connect('activate', lambda x=None: d.response(1)) d.vbox.pack_start(e) if d.run(): lnn = e.get_value_as_int() tv = self.current_textview() bfr = tv.get_buffer() bfr.place_cursor(bfr.get_iter_at_line(lnn-1)) tv.scroll_to_iter(bfr.get_iter_at_line(lnn-1), 0.0, True, 0.0, 1.0) def callback_goto_paren(self, action=None): ''' ''' tv = self.current_textview() b = tv.get_buffer() i = b.get_iter_at_offset(self.find_matching_paren(b.get_iter_at_mark(b.get_insert()).get_offset())) b.place_cursor(i) tv.scroll_to_iter(i) def callback_homepage(self, action): ''' opens __url__ in your webbrowser. ''' Popen('%s"%s"' % (webbrowsercmd, __url__)) def callback_iconify(self, action=None): ''' ''' self.w.iconify() def callback_insert_file(self, action=None, *a): ''' opens a FileChooserDialog, and inserts at cursor the file you pick. ''' txtbuf = self.current_buffer() if isinstance(action, str) and os.path.exists(action): txtbuf.insert_at_cursor(open(action, 'r').read()) self.flash_info("inserted " + filename, 4e3, 'inserted_file') else: dialog = gtk.FileChooserDialog("Open..",None,gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) response = dialog.run() filename = dialog.get_filename() dialog.destroy() if response == gtk.RESPONSE_OK: txtbuf.insert_at_cursor(open(filename, 'r').read()) self.flash_info(None, "inserted " + filename) def callback_insert_time_date(self, action=None): ''' inserts into the current buffer the current time and date using the format string "timeformat" in the [DEFAULT] section of config.flauxtext. ''' self.current_buffer().insert_at_cursor( time.strftime(timeformat, time.localtime()) .replace('\3p0k', str(time.time())) .replace('\3P0K', str(time.time() - time.timezone)) ) def callback_insert_unicode(self, *a): ''' open a new buffer, disconnect its signals, set it to wrap_char, and try to insert all unicode16 chars into it. ''' tb = self.new_buffer() tb.disconnect(tb.handler_ids['insert-text']) tb.disconnect(tb.handler_ids['delete-range']) tb.disconnect(tb.handler_ids['changed']) self.current_textview().set_wrap_mode(gtk.WRAP_CHAR) for i in xrange(1, 256 * 256): try: tb.insert_at_cursor(unichr(i)) except: pass def callback_macro(self, *args): ''' for all strings in args, [in order,] call the corresponding callback_ method [where the argument separator is ',' instead of '__']. ''' namespace = locals() for callback_name in args: if isinstance(callback_name, (str, unicode)): try: parts = callback_name.split(',') eval('self.callback_' + parts[0], namespace)(*(parts[1:] + [None])) except Exception, e: print e def callback_make(self, name, action): ''' make a new buffer containing the value of action.get_name()[6:]+'_stub' ''' self.new_buffer().set_text(eval(name + '_stub')) def callback_man_page(self, action=None): ''' opens a Dialog asking you for the name of a man page, then opens it in either a new buffer or a terminal. ''' d = self.mk_dialog('man page', (gtk.STOCK_OK,1,gtk.STOCK_CANCEL,0)) e = gtk.Entry() d.vbox.pack_start(e) e.connect('activate',lambda x: d.response(1)) e.set_text(self.lastmanpage) c = gtk.CheckButton('open this man page in a terminal') d.vbox.pack_start(c) if d.run(): self.lastmanpage = e.get_text() if c.get_active(): os.popen3(termcmd + "man " + self.lastmanpage) else: s = os.popen( 'man -P "' + pycmd + "-c 'from sys import stdin;print stdin.read()'\" " + self.lastmanpage ).read() # use transducers to remove "bold" and "underlined" text, and non-ascii characters common in man pages s = re.sub('\_\x08(?P.)', lambda m: m.groups()[0][0], s) s = re.sub('(?P.)\x08(?P=a)', lambda m: m.groups()[0][0], s) self.new_buffer().set_text(s.replace('\x08','').replace('\xb7','')) def callback_maximize(self, action=None): ''' if self.w is maximized, calls self.w.unmaximize(); else vice-versa. ''' if self.w.maximize_initially: self.w.unmaximize() else: self.w.maximize() def callback_mru(self, number, *a): ''' opens selected previous file ''' self.open(self.histry[int(number)]) def callback_multiple_copy(self, action=None): ''' writes selected text to the first empty multiplecopypaste file. ''' for i in range(10): fname = os.path.join(tempdir, 'multiplecopypaste%d.flauxtext' % i) if not (os.path.exists(fname) and len(open(fname).read())>0): break bufr = self.current_buffer() try: start, end = bufr.get_selection_bounds() except: start, end = bufr.get_start_iter(), bufr.get_end_iter() open(fname, 'w').write(bufr.get_text(start, end)) self.flash_info('wrote current/all text to '+fname, 4e3, 'copyn') def callback_multiple_paste(self, num, action): ''' inserts at cursor the contents of the multiplecopypaste file associated with action.get_name(). ''' filename = os.path.join(tempdir, 'multiplecopypaste%s.flauxtext' % num) if istextfile(filename): self.current_buffer().insert_at_cursor(open(filename).read()) def callback_new(self, action=None): ''' alias of new_buffer ''' self.new_buffer(action) self.w.set_title(windowtitle % self.current_filename()) def callback_next_n_tab(self, action=None): ''' ''' if ntabn > self.notebk.get_n_pages() - self.notebk.get_current_page(): self.notebk.set_current_page(ntabn % self.notebk.get_n_pages()) else: self.notebk.set_current_page(self.notebk.get_current_page() + ntabn) try: self.actiongroup.get_action('toggle_editable').set_active(self.current_textview().get_editable()) # update the "Editable" toggleaction except: pass self.update_window_title() def callback_next_tab(self, action=None): ''' ''' if self.notebk.get_current_page() end.get_offset(): st, end = end, st fn = bufr.get_text(st, end) if os.path.isfile(fn): self.open(fn) else: self.flash_info('please select a path', 3e3, 'bad_path_selected') def callback_open_url(self, action): ''' passes the selected url to your webbrowser. ''' bufr = self.current_buffer() start, end = bufr.get_selection_bounds() if start.get_offset() < end.get_offset(): Popen(webbrowsercmd + bufr.get_text(start, end)) elif start.get_offset() > end.get_offset(): Popen(webbrowsercmd + bufr.get_text(end, start)) else: self.flash_info('please select a url and try again.', 1e4, 'no_url_selected') def callback_open_url_source(self, action): ''' callback for Tools > Open Selected URL Source, calls openurlsrc. ''' imp0rt('urllib2') bufr = self.current_buffer() start, end = bufr.get_selection_bounds() if start.get_offset() < end.get_offset(): self.new_buffer().set_text(urllib2.urlopen(bufr.get_text(start, end)).read()) elif start.get_offset() > end.get_offset(): self.new_buffer().set_text(urllib2.urlopen(bufr.get_text(end, start)).read()) else: self.flash_info('please select a url and try again.', 1e4, 'no_url_selected') def callback_permissions(self, action): ''' opens a dialog asking for the permissions you want to set to the current file. ''' d = self.mk_dialog('permissions', (gtk.STOCK_OK, 1, gtk.STOCK_CANCEL, 0)) e = gtk.Entry() d.vbox.pack_start(e) d.vbox.pack_start(gtk.Label('when you click "OK", this will run\n' +'os.popen3("chmod "+text you enter above+" "+current filename).')) s = gtk.CheckButton("i don't own this file, and you need my root password in order to chmod it.") d.vbox.pack_start(s) if d.run(): if s.get_active(): runthis=termcmd+'sudo '+term+'chmod '+e.get_text()+' '+self.current_filename() else: runthis=termcmd+'chmod '+e.get_text()+' '+self.current_filename() Popen(runthis) def callback_preview_mcp(self, action=None): ''' opens a dialog containing textbuffers containing the contents of my multiplecopypaste files. ''' d = self.mk_dialog('what are YOU doing?', (gtk.STOCK_OK, 1), response_on_esc=1) d.connect('key-press-event', lambda x=None, y=None, z=None: d.resize(10,10)) saveme = gtk.ToggleButton('Save each buffer when changed') saveme.set_active(True) d.action_area.pack_start(saveme) t = gtk.Table(10,3) d.vbox.pack_start(t) t.set_col_spacings(3);t.set_row_spacings(3) vs = gtk.VSeparator() t.attach(vs,1,2,0,10) def savethisnow(b, fn, s): if s.get_active(): f = open(fn, 'w') f.write(b.get_text(b.get_start_iter(), b.get_end_iter())) f.close() for i in range(10): fname = os.path.join(tempdir,'multiplecopypaste%d.flauxtext'%i) t.attach(gtk.Label(fname), 0, 1, i, i+1) if os.path.exists(fname): a = open(fname).read() else: a = '[file does not yet exist.]' tv = gtk.TextView() t.attach(tv, 2, 3, i, i + 1) tv.get_buffer().set_text(a) tv.get_buffer().connect('changed', savethisnow, fname, saveme) d.run() def callback_previous_n_tab(self, action=None): ''' ''' if ntabn > self.notebk.get_current_page(): self.notebk.set_current_page(self.notebk.get_n_pages() - ntabn + self.notebk.get_current_page()) else: self.notebk.set_current_page(self.notebk.get_current_page() - ntabn) try: self.actiongroup.get_action('toggle_editable').set_active(self.current_textview().get_editable()) # update the "Editable" toggleaction except: pass self.update_window_title() def callback_previous_tab(self, action=None): ''' ''' self.notebk.set_current_page(self.notebk.get_current_page()-1) try: self.actiongroup.get_action('toggle_editable').set_active(self.current_textview().get_editable()) # update the "Editable" toggleaction except: pass self.update_window_title() def callback_print(self, action=None): ''' print to a hardcopy on NT, this is basically open(printer_PATH, 'w').write(self.current_text()) on POSIX, this is os.popen('lpr','w').write(self.current_text()) ''' d = self.mk_dialog('print to where?', (gtk.STOCK_OK, 1, gtk.STOCK_CANCEL, 0)) d.vbox.pack_start(gtk.Label((os.name=='nt') and 'print to what printer?\n[start typing an entry in printer in config.flauxtext]' or 'pipe to what command?')) e = gtk.Entry() d.vbox.pack_start(e) if isinstance(printer, list): ec = gtk.EntryCompletion() ec.set_text_column(0) ecl = gtk.ListStore(str) for i in printer: ecl.append([i]) try: imp0rt('win32print') for i in win32print.EnumPrinters(WIN32_PRINTERS_LEVEL): ecl.append([i[2]]) except: pass ec.set_model(ecl) e.set_completion(ec) elif isinstance(printer, str): e.set_text(printer) e.connect('activate', lambda x=None,y=None: d.response(1)) if d.run(): try: if os.name=='nt': open(e.get_text(), 'w').write(self.current_text()) else: os.popen(e.get_text(), 'w').write(self.current_text()) self.flash_info('go check the printer ;-)', 3e3, 'done_printing') except Exception, e: self.flash_info('ERROR PRINTING: '+reduce(lambda x,y: str(x)+str(y), e.args), 5e3, 'cannot_print') def callback_plugin(self, *a): ''' alias for self.run_plugin. ''' return self.run_plugin(*a) def callback_pygtkref(self, action): ''' opens the pygtk reference in your webbrowser ''' Popen(webbrowsercmd + pygtkrefurl) def callback_python_documentation(self, action): ''' opens a Dialog asking you for the name of a python module, then opens that module's help documentation in either a new buffer or a terminal. ''' d = self.mk_dialog('help(me)', (gtk.STOCK_OK,1,gtk.STOCK_CANCEL,0)) e = gtk.Entry() d.vbox.pack_start(e) e.connect('activate',lambda x: d.response(1)) e.set_text(self.lasthelppy) c=gtk.CheckButton('open this documentation in a terminal') d.vbox.pack_start(c) if d.run(): self.lasthelppy=e.get_text() if c.get_active(): runthis=termcmd+pycmd+' -c "exec \\"import %s\\nhelp(%s)\\""'%tuple([self.lasthelppy]*2) Popen(runthis) else: t=os.popen(pycmd + ' -c "help(\'%s\')"' % self.lasthelppy).read() if "Help on" in t:t=t[t.index("Help on"):] buf=self.new_buffer() buf.set_text(t) self.current_textview().scroll_to_iter(buf.get_start_iter(),0.0) buf.place_cursor(buf.get_start_iter()) self.notebk.set_tab_label_text(self.notebk.get_nth_page(self.notebk.get_current_page()), self.lasthelppy) def callback_quit(self, action=None): ''' when you hit ^q, if your config allows, i will ask you if you want to save any modified open files. ''' if checkforunsavedbeforequitting: numlen = self.notebk.get_n_pages() for i in range(self.notebk.get_n_pages()): j = numlen - 1 - i # work backwards from the end jthbfr = self[j] jthtabtxt = self.notebk.get_tab_label_text(self.notebk.get_nth_page(j)) if ((self[j].filename == UNTITLED_FILENAME) and len(jthbfr.get_all_text()) > 0 ) or (jthtabtxt[0] == jthtabtxt[-1] == '*'): d = self.mk_dialog("save buffer before closing?", (gtk.STOCK_YES,1,gtk.STOCK_NO,2,gtk.STOCK_CANCEL,3)) response = d.run() if response == 3: return False else: if response == 1: self.notebk.set_current_page(i) self.save() curpagen = self.notebk.get_current_page() self.notebk.remove_page(curpagen) else: curpagen = self.notebk.get_current_page() self.notebk.remove_page(curpagen) if self.notebk.get_n_pages() < 1: gtk.main_quit() else: gtk.main_quit() def callback_redo(self, action=None): ''' ''' buffer = self.current_buffer() pagen = self.notebk.get_current_page() curundostack = buffer.undostack curundoentry = curundostack.next() if len(curundostack) >= curundostack.pointer and isinstance(curundoentry, UndoStackEntry): buffer.disconnect(buffer.handler_ids['insert-text']) buffer.disconnect(buffer.handler_ids['delete-range']) buffer.disconnect(buffer.handler_ids['changed']) if curundoentry.was_inserted: buffer.insert(buffer.get_iter_at_offset(curundoentry.offset), curundoentry.data) else: buffer.delete(buffer.get_iter_at_offset(curundoentry.offset), buffer.get_iter_at_offset(curundoentry.offset + len(curundoentry.data))) buffer.handler_ids['insert-text'] = buffer.connect('insert-text', self.registerwithundo) buffer.handler_ids['delete-range'] = buffer.connect('delete-range', self.registerwithundo) buffer.handler_ids['changed'] = buffer.connect('changed', self.autoindent) curundostack.pointer += 1 buffer.place_cursor(buffer.get_iter_at_offset(curundoentry.offset)) chil = self.notebk.get_nth_page(pagen) tabtxt = self.notebk.get_tab_label_text(chil) if (tabtxt[0] != '*') and (tabtxt[-1] != '*') and (self.current_filename() != UNTITLED_FILENAME): # update the tabtext if self.autosave: self.save() else: self.notebk.set_tab_label_text(chil, '** ' + tabtxt + ' **') self.w.set_title((windowtitle % self.current_filename()) + windowtitlemodified) self.statbar_info.set('') else: self.flash_info('nothing to redo O_o', 5e3, 'cannot_redo') def callback_reload(self, action=None): ''' closes current buffer, reopens it. ''' n = self.notebk.get_current_page() fn = self[n].filename if self.notebk.get_n_pages() == 1: self.new_buffer() self.close_buffer(None, n) self.open(fn) def callback_reposition(self, action=None): ''' ''' self.w.move(*winderpos[:2]) def callback_resize(self, action=None): ''' ''' self.w.resize(*windersize[:2]) def callback_run(self, action=None): ''' handles all F5 menuitems fnmatch()es current filename versus keys in runcmds [in config.flauxtext], runs the associated value in runcmds. opens a dialog for arguments if "wargs" in action.get_name(). opens output in a new buffer if "GetOutput" in action.get_name() if the appropriate runcmds entry is iterable, all but the last are executed sequentially, then the last is passed through the 'wargs' and 'GetOutput' filters [and/or passed to Popen] ''' runthis = self.run_what(self.current_filename()) if runthis: self.flash_info(runthis, 2e3, 'running') Popen(runthis) def callback_run_args(self, action=None): ''' ''' runthis = self.run_what(self.current_filename()) if runthis: d = self.mk_dialog('args', (gtk.STOCK_OK, 1, gtk.STOCK_CANCEL, 0)) d.vbox.pack_start(gtk.Label("i'll run\n'"+runthis+"' what you enter here")) e = gtk.Entry() e.connect('activate',lambda x:d.response(1)) d.vbox.pack_start(e) if d.run(): runthis += ' ' + e.get_text() else: return self.flash_info(runthis, 2e3, 'running') Popen(runthis) def callback_run_get_output(self, action=None): ''' ''' runthis = self.run_what(self.current_filename()) if runthis: self.flash_info(runthis, 2e3, 'running') fs = os.popen3(runthis) put = '' out = fs[1].read() err = fs[2].read() if err != '': put += 'STDERR:\n' + err if out != '': put += '\n\nSTDOUT:\n' + out self.new_buffer().set_text(put) def callback_run_args_get_output(self, action=None): ''' ''' runthis = self.run_what(self.current_filename()) if runthis: d = self.mk_dialog('args', (gtk.STOCK_OK, 1, gtk.STOCK_CANCEL, 0)) d.vbox.pack_start(gtk.Label("i'll run\n'"+runthis+"' what you enter here")) e = gtk.Entry() e.connect('activate',lambda x:d.response(1)) d.vbox.pack_start(e) if d.run(): runthis += ' ' + e.get_text() else: return self.flash_info(runthis, 2e3, 'running') fs = os.popen3(runthis) put = '' out = fs[1].read() err = fs[2].read() if err != '': put += 'STDERR:\n' + err if out != '': put += '\n\nSTDOUT:\n' + out self.new_buffer().set_text(put) def callback_save(self, action=None): ''' alias of save ''' return self.save() def callback_saveas(self, action=None): ''' prompt for a filename for the current buffer ''' fn = self.current_filename() d = gtk.FileSelection("Save As...") respond_dialog_on_escape(d) d.set_filename([fn, os.path.abspath(defaultsavepath)][fn == UNTITLED_FILENAME]) d.ok_button.connect('clicked', lambda w: ( not os.path.isdir(d.get_filename()) and (d.response(1) or True) or self.flash_info('you must choose a filename', 3e3, 'bad_save_filename'))) d.cancel_button.connect('clicked', lambda w: d.response(0)) if d.run(): fn = d.get_filename() self.w.set_title(windowtitle % fn) bfr = self.current_buffer() bfr.filename = fn self.save_file(fn, bfr.get_all_text()) try: self.manage_mru(fn, True) except IndexError: pass d.destroy() def callback_search(self, action='Google'): ''' searches for selected text in the chosen search engine [whose name is either action or action.get_name()] using the webbrowsercmd set in [os.name] in my configfile. ''' txtb = self.current_buffer() selectedtext = '' try: starts, ends = txtb.get_selection_bounds() anym = (isinstance(action, str) and action != '') and action or action.get_name() selectedtext = txtb.get_text(starts, ends).replace(' ','+') if anym not in SEARCH_ENGINES.keys(): self.flash_info('i forgot how to use ' + anym + ', so i just used google. :-/', 3e3, 'bad_search_engine') anym = 'Google' runthis = webbrowsercmd + SEARCH_ENGINES[anym] + '=' + selectedtext + '"' self.flash_info(runthis, 2e3, 'running') Popen(runthis) except ValueError: self.flash_info('select something first.', 3e3, 'bad_search_terms') def callback_special_file(self, number, evt): ''' opens the special file corresponding to number ''' self.open(self.specialfilelist[int(number)]) def callback_split_horizontally(self, action=None): ''' ''' self.split(False).add2(FlauxText(self.all_files(), w=self.w).vbox) def callback_split_vertically(self, action=None): ''' ''' self.split().add2(FlauxText(self.all_files(), w=self.w).vbox) def callback_statistics(self, action=None): ''' open a dialog containing only a gtk.Table which in turn contains statistics such as the number of chars, lines, and words in the current buffer, and the numbers of times the most common words and chars appear. set the number of common words and chars i show you in my configfile at the variable "statlen" under "DEFAULT". ''' buff = self.current_buffer() if len(buff.get_selection_bounds()) > 1: # if user has selected something, look only at that (start,end)=buff.get_selection_bounds() text=buff.get_text(start,end) else: # otherwise, look at everything text = buff.get_all_text() char_dicto = {} char_count = len(text) for i in string.printable: char_dicto[i] = len(text.split(i)) - 1 word_dicto = {} words = re.split('\W*',text) for i in range(words.count('')): words.remove('') word_count = len(words) for word in words: #word_dicto[word] = word_dicto.get(word, 0) + 1 # actually slower than try...except try: word_dicto[word] += 1 except: word_dicto[word] = 1 statable = gtk.Table(7, 17) statable.set_col_spacings(3) #0-----------1----2------3----4----5---6----7---8----9----0------1---2----3----4----5--------6----7---8 #| lines | vs | n | vs | vs | ( | vs | n | vs | vs | word | vs | n | vs | vs | letter | vs | n | #1-----------|----|------|----|----|---|----|---|----|----|------|----|---|----|----|--------|----|---| #| chars | vs | n | vs | vs | ) | vs | n | vs | vs | word | vs | n | vs | vs | letter | vs | n | #2-----------|----|------|----|----|---|----|---|----|----|------|----|---|----|----|--------|----|---| #| words | vs | n | vs | vs | { | vs | n | vs | vs | word | vs | n | vs | vs | letter | vs | n | #3-----------|----|------|----|----|---|----|---|----|----|------|----|---|----|----|--------|----|---| #| ' | vs | n | vs | vs | } | vs | n | vs | vs | word | vs | n | vs | vs | letter | vs | n | #4-----------|----|------|----|----|---|----|---|----|----|------|----|---|----|----|--------|----|---| #| " | vs | n | vs | vs | [ | vs | n | vs | vs | word | vs | n | vs | vs | letter | vs | n | #5-----------|----|------|----|----|---|----|---|----|----|------|----|---|----|----|--------|----|---| #| <\w*> | vs | n | vs | vs | ] | vs | n | vs | vs | word | vs | n | vs | vs | letter | vs | n | #6-----------|----|------|----|----|---|----|---|----|----|------|----|---|----|----|--------|----|---| #| | vs | n | vs | vs | < | vs | n | vs | vs | word | vs | n | vs | vs | letter | vs | n | #7-----------|----|------|----|----|---|----|---|----|----|------|----|---|----|----|--------|----|---| #| = | vs | n | vs | vs | > | vs | n | vs | vs | word | vs | n | vs | vs | letter | vs | n | #8-----------|----|------|----|----|---|----|---|----|----|------|----|---|----|----|--------|----|---| #statable.attach(widget, left, right, top, bottom, xoptions=gtk.EXPAND|gtk.FILL, # yoptions=gtk.EXPAND|gtk.FILL, xpadding=0, ypadding=0) lrs = [(1, 2), (3, 4), (4, 5), (6, 7), (8, 9), (9, 10), (11, 12), (13, 14), (14, 15), (16, 17)] for i in lrs: vs = gtk.VSeparator() statable.attach(vs, i[0], i[1], 0, 8) html = '[\w=\?/:"\'\. ]+' ls = ['chars','words','\\r,\\n',"'",'"','<'+html+'>','','='] for i in range(len(ls)): statable.attach(gtk.Label(ls[i]), 0, 1, i, i + 1) ls = [str(char_count), str(word_count), str(len(text.split('\r'))-1)+","+str(len(text.split('\n'))-1), str(len(text.split("'"))-1), str(len(text.split('"'))-1), str(len(re.compile('<'+html+'>').findall(text))), str(len(re.compile('').findall(text))), str(len(re.compile('=').findall(text)))] for i in range(len(ls)): statable.attach(gtk.Label(ls[i]), 2, 3, i, i + 1) ls = list('(){}[]<>') for i in range(len(ls)): statable.attach(gtk.Label(ls[i]), 5, 6, i, i + 1) statable.attach(gtk.Label(str(len(text.split(ls[i]))-1)), 7, 8, i, i + 1) wdi = word_dicto.items() wdi.sort(key=lambda x: x[1]) wdi.reverse() for i in range(len(wdi)): if i > statlen: break else: statable.attach(gtk.Label(wdi[i][0]), 10, 11, i, i + 1) statable.attach(gtk.Label(wdi[i][1]), 12, 13, i, i + 1) cdi = char_dicto.items() cdi.sort(key=lambda x: x[1]) cdi.reverse() for i in range(len(cdi)): if i > statlen: break else: statable.attach(gtk.Label(cdi[i][0].replace(' ','\\s').replace('\r','\\r').replace('\n','\\n')), 15, 16, i, i + 1) statable.attach(gtk.Label(cdi[i][1]), 17, 18, i, i + 1) def statsecb(entry, n, r, txt): s = entry.get_text() try: n.set_text(str(len(re.split(r.get_active() and s or re.escape(s), txt))-1)) except: pass d = self.mk_dialog(self.current_filename(), ()) d.vbox.pack_start(statable) e = gtk.Entry() n = gtk.Entry() rex = gtk.CheckButton('re') n.set_property('editable',False) n.set_property("width-request", 20) e.set_property("width-request", 20) rex.set_property("width-request", 10) e.connect("changed", statsecb, n, rex,text) d.action_area.set_property('homogeneous',False) d.action_area.pack_start(rex, False, False) d.action_area.pack_start(e, False, False) d.action_area.pack_start(n, False, False) dun = gtk.Button('_Done', gtk.STOCK_OK) dun.connect('clicked', lambda x: d.response(0)) d.action_area.pack_start(dun, False, False) e.grab_focus() d.run() def callback_swap_crlf_lf(self, action=None): ''' if there is at least one '\\r' [carraige-return] in the current file, this eliminates all carriage-returns. if not, this exchanges all '\\n' [newlines] with '\\r\\n' [carriage-return line-feed]. ''' bfr = self.current_buffer() txt = bfr.get_all_text() reptxt = '' if len(txt.split('\r\n'))>1: reptxt = '\n'.join(txt.split('\r\n')) self.flash_info("removed all \\r\\n from current buffer", 2e3, 'fixed_crlf') else: reptxt = '\r\n'.join(txt.split('\n')) self.flash_info("replaced all \\n in current buffer with \\r\\n", 2e3, 'fixed_crlf') bfr.set_text(reptxt) def callback_terminal(self, action=None): ''' changes working directory to that of the current file, and calls either termcmd or filemanagercmd, depending on action.get_name(). set termcmd and filemanagercmd in either the [nt] or [posix] section of config.flauxtext, depending on your OS. ''' if self.current_filename() == UNTITLED_FILENAME: dirnym = os.sep # open a terminal at the root else: dirnym = os.path.dirname(self.current_filename()) try: os.chdir(dirnym) except OSError: pass Popen(' '.join(termcmd.split(' ')[:-2])) def callback_toggle_autoindentation(self, action): self.autoident = not self.autoident def callback_toggle_autosave(self, action=None): self.autosave = not self.autosave def callback_toggle_editable(self, tglaction=None): ''' toggle the "editable" status of the current textview. ''' txtvw = self.current_textview() if tglaction is None: bul = not txtvw.get_editable() try: self.actiongroup.get_action('toggle_editable').set_active(bul) except AttributeError: pass else: bul = tglaction.get_active() txtvw.set_editable(bul) def callback_toggle_highlight_parens(self, action=None): self.findparens = not self.findparens def callback_toggle_keep_above(self, action=None): self.w.set_keep_above(action.get_active()) def callback_toggle_linenums(self, action=None): ''' toggles line-number border-window size from 0 to 30 pixels. ''' txtview = self.current_textview() if linenumwindowsize in map(txtview.get_border_window_size, [gtk.TEXT_WINDOW_LEFT, gtk.TEXT_WINDOW_RIGHT]): for parameter in [gtk.TEXT_WINDOW_LEFT, gtk.TEXT_WINDOW_RIGHT]: if txtview.get_border_window_size(param) == linenumwindowsize: txtview.set_border_window_size(param, 0) else: if linenums in ['Left', 'Both']: txtview.set_border_window_size(gtk.TEXT_WINDOW_LEFT, linenumwindowsize) if linenums in ['Right', 'Both']: txtview.set_border_window_size(gtk.TEXT_WINDOW_RIGHT, linenumwindowsize) def callback_toggle_right_edge(self, action=None): global rightedge rightedge = not rightedge def callback_toggle_statusbar(self, action=None): ''' toggle visible property of self.statbarhbox. ''' self.statbarhbox.set_property("visible", not self.statbarhbox.get_property("visible")) def callback_toggle_tabbar(self,action): ''' toggles visible status of the tabs on self.notebk. ''' self.notebk.set_show_tabs(not self.notebk.get_show_tabs()) def callback_toggle_tearoff_menus(self, action=None): ''' turn on/off menu tearing off ''' self.uiman.set_add_tearoffs(not self.uiman.get_add_tearoffs()) def callback_undo(self, action=None): ''' ''' buffer = self.current_buffer() pagen = self.notebk.get_current_page() curundostack = buffer.undostack curundoentry = curundostack.current() if len(curundostack) >= curundostack.pointer and isinstance(curundoentry, UndoStackEntry): buffer.disconnect(buffer.handler_ids['insert-text']) buffer.disconnect(buffer.handler_ids['delete-range']) buffer.disconnect(buffer.handler_ids['changed']) if curundoentry.was_inserted: buffer.delete(buffer.get_iter_at_offset(curundoentry.offset), buffer.get_iter_at_offset(curundoentry.offset + len(curundoentry.data))) else: buffer.insert(buffer.get_iter_at_offset(curundoentry.offset), curundoentry.data) buffer.handler_ids['insert-text'] = buffer.connect('insert-text', self.registerwithundo) buffer.handler_ids['delete-range'] = buffer.connect('delete-range', self.registerwithundo) buffer.handler_ids['changed'] = buffer.connect('changed', self.autoindent) curundostack.pointer -= 1 buffer.place_cursor(buffer.get_iter_at_offset(curundoentry.offset)) chil = self.notebk.get_nth_page(pagen) tabtxt = self.notebk.get_tab_label_text(chil) if (tabtxt[0] != '*') and (tabtxt[-1] != '*') and (self.current_filename() != UNTITLED_FILENAME): # update the tabtext if self.autosave: self.save() else: self.notebk.set_tab_label_text(chil, '** ' + tabtxt + ' **') self.w.set_title((windowtitle % self.current_filename()) + windowtitlemodified) self.statbar_info.set('') else: self.flash_info('nothing to undo O_o', 5e3, 'cannot_undo') def callback_view_undo_stack(self, action=None): ''' opens a dialog showing you the contents of my undostack. ''' curundostack = self.current_undostack() d = self.mk_dialog("undostack", (gtk.STOCK_OK, 1)) d.vbox.set_property('homogeneous', False) d.vbox.pack_start(gtk.Label(self.current_filename() + '\n'), expand=False, fill=False) sw = gtk.ScrolledWindow() d.vbox.pack_start(sw) # expand with the window sw.set_size_request(400, 200) t = gtk.Table(len(curundostack), 6) t.set_col_spacings(3) sw.add_with_viewport(t) for i in [1,3,5]: t.attach(gtk.VSeparator(), i, i+1, 0, len(curundostack)+1) t.attach(gtk.Label("char_offset"), 0, 1, 0, 1, xoptions=False, yoptions=False) t.attach(gtk.Label("inserted?"), 2, 3, 0, 1, xoptions=False, yoptions=False) t.attach(gtk.Label("text"), 4, 5, 0, 1, xoptions=False, yoptions=False) t.attach(gtk.Label("index in stack"), 6, 7, 0, 1, xoptions=False, yoptions=False) for i in range(1, len(curundostack)): for j, x in enumerate([curundostack[i].offset, curundostack[i].was_inserted, curundostack[i].data]): t.attach(gtk.Label(repr(x)), 2 * j, 2 * j + 1, 1 + i, 2 + i) t.attach(gtk.Label(str(i)), 6, 7, 1 + i, 2 + i) d.vbox.pack_start(gtk.Label('current index: ' + str(curundostack.pointer)), expand=False, fill=False) sw.grab_focus() d.run() def callback_word_wrap(self, action=None): ''' rotates word wrap according to the list of constants in wordwraps set in config.flauxtext ''' txtview = self.current_textview() txtview.set_wrap_mode(wordwraps[(wordwraps.index(txtview.get_wrap_mode()) + 1) % len(wordwraps)]) def capslock_statbar_switch(self, widgt, event): ''' called whenever you press a key in one of my textview widgets, updates self.statbar_capslock ''' value = ['a', 'A'][bool(event.state & gtk.gdk.LOCK_MASK)] if event.keyval == CAPS_LOCK_KEYVAL: value = ['a', 'A'][self.statbar_capslock.value == 'a'] if value != self.statbar_capslock.value: self.statbar_capslock.pop(1) self.statbar_capslock.push(1, value) self.statbar_capslock.value = value def close_buffer(self, action=None, which=None): ''' if which file is unsaved, i'll offer to save it then close it; otherwise, i'll just close it. if which is None, the current buffer. ''' if which is None: which = self.notebk.get_current_page() # ask to save the buffer if it has been modified since last save, or if it is untitled and contains text. bfr = self[which] tabtext = self.notebk.get_tab_label_text(self.notebk.get_nth_page(which)) if ((self[which].filename == UNTITLED_FILENAME) and (len(bfr.get_all_text()) > 0) ) or (tabtext[0] == tabtext[-1]=='*'): d = self.mk_dialog("save before closing?", (gtk.STOCK_YES, 1, gtk.STOCK_NO, 2, gtk.STOCK_CANCEL, 3)) # 1=yes, save buffer # 2=no, don't save # 3=don't close the buffer response = d.run() if response != 3: if response == 1: self.save() self.notebk.remove_page(which) else: # current buffer has been saved self.notebk.remove_page(which) if self.notebk.get_n_pages() == 0: if newbufferoncloselast: self.new_buffer() else: gtk.main_quit() elif tabshideifone and (self.notebk.get_n_pages() == 1): self.notebk.set_show_tabs(False) try: self.w.set_title(windowtitle % self.current_filename()) except: # we might not have a current_filename pass return 0 def connect_menuitem_selections_statbar_info_recurse(self, p='', d={}): ''' a recursive method which takes a parent [str] and a dict describing children. the parent is interpreted as a UIManager path, and the dict maps path elements to child path elements. example usage: self.connect_menuitem_selections_statbar_info_recurse('/MenuBar', {'/File':{'/New':''}}) of course, the dict used by FlauxText is dynamically generated by an XML parser, and is a good deal more involved. ''' make_info_setter = lambda s: lambda *a: self.statbar_info.set(s) # closures are so metal. for k in d.keys(): if isinstance(d[k], dict): self.connect_menuitem_selections_statbar_info_recurse(p + k, d[k]) else: #elif d[k] == '': a = self.uiman.get_action(p + k) if a == None: continue ttt = a.get_property("tooltip") if not ttt: ttt = " " for w in a.get_proxies(): try: w.connect("select", make_info_setter(ttt)) w.connect("deselect", make_info_setter('')) except TypeError: # toolbar items don't have that signal, and don't need to be connected to it pass def current_buffer(self): ''' a one-liner used to simplify nearly every one of my other callbacks. return the current buffer. ''' return self.current_textview().get_buffer() def current_filename(self): ''' a one-liner used to simplify most of my other callbacks. return the current filename. ''' return self[self.notebk.get_current_page()].filename def current_scrolledwindow(self): ''' a one-liner used to simplify most of my callbacks. return the current textview. ''' return self.notebk.get_nth_page(self.notebk.get_current_page()) def current_selection_bounds_or_all(self, text=False, raise_if_no_selection=False): ''' find the pair of TextIters specifying the currently selected text. if not raise_if_no_selection: default those iters to b.get_start_iter() and b.get_end_iter() it text: return that text else: return those iters ''' b = self.current_buffer() if raise_if_no_selection: start, end = b.get_selection_bounds() if start.get_offset() > end.get_offset(): start, end = end, start else: try: start, end = b.get_selection_bounds() if start.get_offset() > end.get_offset(): start, end = end, start except ValueError: start = b.get_start_iter() end = b.get_end_iter() if text: return b.get_text(start, end) else: return start, end def current_text(self): ''' return the contents of the current buffer ''' return self.current_buffer().get_all_text() def current_textview(self): ''' a one-liner used to simplify most of my other callbacks. return the current textview. ''' return self.current_scrolledwindow().get_child() def current_undo_entry(self): ''' returns the current 3list in the undostack; used in some of my undo methods. ''' return self.current_undostack().current() def current_undostack(self): ''' returns the int-and-tuples list associated with the current page in the notebk ''' return self.current_buffer().undostack def cycle_tabs__(self, wid, evt): ''' optional feature [usectrltabs] allows you to use ctrl+tab to switch forwards through tabs, ctrl+shift+tab to switch backwards. [UIManager can't override gtk's silly default "ctrl+tab rotates through each individual widget", so i have to do it by hand] ''' if evt.state & gtk.gdk.CONTROL_MASK: if evt.keyval == TAB_KEYVAL: self.callback_next_tab() elif evt.keyval == SHIFT_TAB_KEYVAL: self.callback_previous_tab() elif evt.keyval == 99: # horrible hack to emulate copy # 99 == ord('c') tb = self.current_buffer() start, end = tb.get_selection_bounds() if start.get_offset() > end.get_offset(): start, end = end, start text = tb.get_text(start, end) self.clipboard.set_text(text) def dnd_get_data(self, txtv, context, selection, info, timestamp): ''' do nothing ''' pass def dnd_received_data(self, txtv, context, x, y, selection, info, timestamp): ''' connected to the 'drag-data-received' signal of each textview txtv is one of my gtk.TextView()s--the one on which you dropped dragged data context is a gtk.gdk.DragContext; its targets attribute is a list of URIs selection is a gtk.SelectionData describing what you dropped if you dragged files on me from a file manager like explorer.exe, its data attribute is a '\r\n' separated list of URL-encoded filenames info is almost always 0 [zero] if you drag multiple files or a directory onto me, dragdropopentextonly in config.flauxtext determines whether or not i try to open binary files ''' imp0rt('urllib.unquote') if context.targets[0] == 'text/uri-list': L = [urllib.unquote(fn).replace('file:///','') for fn in selection.data.split(os.linesep)] if '\x00' in L: L.remove('\x00') for fn in L: if (dragdropopentextonly and istextfile(fn)) or (not dragdropopentextonly and os.path.isfile(fn)): self.open(fn) elif os.path.isdir(fn): for f in os.listdir(fn): f = os.path.join(fn, f) if (dragdropopentextonly and istextfile(f)) or (not dragdropopentextonly and os.path.isfile(f)): self.open(f) def find(self, find_str='', replace_str='', search_as_regexp=False, replace_as_regexp=False, directive=FIND_FIND, re_flags={}, start_direction=FIND_HERE_FORWARD, fst_code=None, which_buffer=None ): ''' ''' buffer = self[which_buffer] if isinstance(re_flags, int): re_flags_int = re_flags else: re_flags_int = 0 for flag, flag_mask in re_flags.iteritems(): if flag_mask: re_flags_int |= flag self.findagaininfo = {'find_str': find_str, 'replace_str': replace_str, 'directive': directive, 're_flags': re_flags_int, 'which_buffer': which_buffer, 'start_direction': start_direction, 'search_as_regexp': search_as_regexp, 'replace_as_regexp': replace_as_regexp, 'fst_code': fst_code } if fst_code is None: self.fst = None elif (not hasattr(self.fst, 'code')) or (self.fst.code != fst_code): # you enabled fst and your function is different from the current self.fst exec fst_code in locals() self.fst = fst self.fst.code = fst_code if find_str not in self.find_history: self.find_history.append(find_str) if replace_str not in self.find_history: self.find_history.append(replace_str) if start_direction == FIND_AFTER_START: buffer.move_mark_by_name('insert', buffer.get_start_iter()) elif start_direction == FIND_BEFORE_END: buffer.place_cursor(buffer.get_end_iter()) if not search_as_regexp: find_str = re.escape(find_str) if replace_as_regexp: replace_str = replace_str.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r').replace('\\f', '\f') pattern = re.compile(find_str, re_flags_int) if directive == FIND_FIND: match = None if start_direction in [FIND_HERE_FORWARD, FIND_AFTER_START]: insitr = buffer.get_iter_at_mark(buffer.get_insert()) curoffs = insitr.get_offset() # offset of current TextIter match = pattern.search(buffer.get_text(insitr, buffer.get_end_iter())) if (match != None) and (match.start() == 0): # if i just re-found the text at the cursor, step forward curoffs += 1 match = pattern.search(buffer.get_text(buffer.get_iter_at_offset(curoffs), buffer.get_end_iter())) if match == None: curoffs = 0 if findasktowrap: d = self.mk_dialog('wrap?', (gtk.STOCK_OK, 1, gtk.STOCK_CANCEL, 0)) d.vbox.pack_start(gtk.Label("click OK to start again from the beginning.\n" + "click Cancel to stop searching.\n" + "if you want to automatically wrap, set findasktowrap in config.flauxtext to True.")) if d.run(): match = pattern.search(buffer.get_all_text()) else: match = pattern.search(buffer.get_all_text()) if match != None: buffer.select_range( buffer.get_iter_at_offset(curoffs + match.start()), buffer.get_iter_at_offset(curoffs+match.end()) ) else: self.flash_info(find_str + ' not found', 2e3, 'not_found') else: # find backwards match = None for match in pattern.finditer(buffer.get_text(buffer.get_start_iter(), buffer.get_iter_at_mark(buffer.get_insert()))): pass # run through all the matches, keep only the last in the current namespace if match == None: if findasktowrap: d = self.mk_dialog('wrap?', (gtk.STOCK_OK, 1, gtk.STOCK_CANCEL, 0)) d.vbox.pack_start(gtk.Label("click OK to start again from the end.\n" + "click Cancel to stop searching.\n" + "if you want to automatically wrap, set findasktowrap in config.flauxtext to True.")) if d.run(): for match in pattern.finditer(buffer.get_all_text()): # keep the last match in the namespace pass else: for match in pattern.finditer(buffer.get_all_text()): # keep the last match in the namespace pass if match != None: buffer.select_range(buffer.get_iter_at_offset(match.start()), buffer.get_iter_at_offset(match.end())) else: self.flash_info(find_str + ' not found', 2e3, 'not_found') self.current_textview().scroll_to_mark(buffer.get_insert(),0.0,True) elif directive == FIND_FIND_ALL: try: start, end = buffer.get_selection_bounds() except ValueError: start, end = buffer.get_start_iter(), buffer.get_end_iter() for itr in pattern.finditer(buffer.get_text(start, end)): buffer.apply_tag_by_name('becky', buffer.get_iter_at_offset(itr.start() + start.get_offset()), buffer.get_iter_at_offset(itr.end() + start.get_offset()) ) elif directive == FIND_REPLACE: if start_direction in [FIND_HERE_FORWARD, FIND_AFTER_START]: insert = buffer.get_iter_at_mark(buffer.get_insert()) match = pattern.search(buffer.get_text(insert, buffer.get_end_iter())) curoffs = insert.get_offset() # offset of current TextIter if (match is not None) and (match.start() == 0): curoffs += 1 buffer.move_mark(buffer.get_insert(), buffer.get_iter_at_offset(curoffs)) match = pattern.search(buffer.get_text(buffer.get_iter_at_mark(buffer.get_insert()), buffer.get_end_iter())) else: # replace backward match = None for match in pattern.finditer(buffer.get_text(buffer.get_start_iter(), buffer.get_iter_at_mark(buffer.get_insert()))): pass # run through all the matches, keep only the last in the current namespace curoffs = 0 if match is None: self.flash_info(find_str + ' not found', 2e3, 'not_found') return else: inso = curoffs + match.start() endo = curoffs + match.end() if self.fst: replace_str = self.fst(match) buffer.delete(buffer.get_iter_at_offset(inso), buffer.get_iter_at_offset(endo)) buffer.insert(buffer.get_iter_at_offset(inso), replace_str) self.current_textview().scroll_to_mark(buffer.get_insert(), 0.0, True) elif directive == FIND_REPLACE_ALL: try: start, end = buffer.get_selection_bounds() except ValueError: start, end = buffer.get_start_iter(), buffer.get_end_iter() text = buffer.get_text(start, end) if self.fst: try: reptext = re.sub(pattern, self.fst, text) except Exception, e: self.flash_info('YOUR FST CODE IS BUGGY: ' + str(e), 5e4, 'bad_fst_code') return else: reptext = replace_str.join(pattern.split(text)) buffer.select_range(start, end) buffer.delete_selection(False, True) # False=deletion is NOT caused by user interaction; True=buffer IS editable by default buffer.insert_at_cursor(reptext) self.current_textview().scroll_to_mark(buffer.get_insert(), 0.0, True) # textview may not correspond with buffer else: self.flash_info('invalid directive: %r' % directive, 'invalid_directive') def find_matching_paren(self, idx): ''' return another idx [textiter.offset] pointing to the brother of the paren at the given idx. [all in the current buffer] relies on calling method to hilight the brother supported 'paren's: (){}[]<> basic strategy: while nest_level > 0: inc/decrement idx according to the type of the paren if we hit the start/end of bfr, return None inc/decrement nest_level if appropriate return idx ''' nest_level = 1 iter_time = 0 bfr = self.current_buffer() paren = bfr.get_text(bfr.get_iter_at_offset(idx-1), bfr.get_iter_at_offset(idx)) if paren in '([{<': # search forward for the brother direction = 1 elif paren in ')}]>': # search backward direction = -1 else: # "paren" is not a paren return None if paren in '()': prn_type = '()' elif paren in '[]': prn_type = '[]' elif paren in '{}': prn_type = '{}' elif paren in '<>': prn_type = '<>' while nest_level > 0: idx += direction iter_time += 1 titr = bfr.get_iter_at_offset(idx) if (direction == 1 and titr.is_end()) or (direction == -1 and titr.is_start()) or ( isinstance(maxparenlen, int) and (1 < maxparenlen < iter_time)): # search failed: we haven't found a brother and we're still in loop return None cur_char = bfr.get_text(bfr.get_iter_at_offset(idx-1), titr) if cur_char in prn_type: if cur_char == paren: nest_level += 1 else: nest_level -= 1 return idx def flash_info(self, msg='', wait=1000, lbl=None, n=1): ''' flashes msg in self.statbar_info n times, with a flash period of wait milliseconds. if n < 0: FLASH FOREVER!!11ONEONE ''' if (lbl in infodialogwhen) or ('__ALL__' in infodialogwhen): d = self.mk_dialog('alert!', (gtk.STOCK_OK, 0)) d.vbox.pack_start(gtk.Label(msg)) d.vbox.pack_start(gtk.Label("if you'd rather this flash on the status bar, remove\n" + lbl + "\nfrom 'infodialogwhen' in config.flauxtext.")) d.run() else: self.statbar_info.set(msg) gobject.timeout_add(int(wait), lambda *a: self.statbar_info.set('')) if n > 1: gobject.timeout_add(wait * 2, self.flash_info, msg, wait, n - 1) elif n < 0: gobject.timeout_add(wait * 2, self.flash_info, msg, wait, n) def hiliteparens(self, buf, ti, txtmark): ''' called whenever you move a TextMark txtmark [including the cursor] if char before ti [TextIter at txtmark] is one of '[]{}()<>', hilights it and its brother for 1 second, if appropriate. ''' if (txtmark.get_name() != None) and (txtmark.get_name != 'insert'): # ignore any textmarks but the cursor return itrb4ti = buf.get_iter_at_offset(ti.get_offset()-1) curchar = buf.get_text(itrb4ti, ti) if self.findparens and (curchar != '') and (curchar in '[]{}()<>'): brotheridx = self.find_matching_paren(ti.get_offset()) if brotheridx==None: tagnym = 'None' buf.apply_tag_by_name(tagnym, itrb4ti, ti) gobject.timeout_add(parenhilitems, self.unhiliteparens, buf, tagnym, itrb4ti.get_offset(), ti.get_offset()) else: tagnym = curchar brotheritr = buf.get_iter_at_offset(brotheridx) itrb4brother = buf.get_iter_at_offset(brotheridx-1) brother = buf.get_text(itrb4brother, brotheritr) buf.apply_tag_by_name(brother, itrb4brother, brotheritr) gobject.timeout_add(parenhilitems, self.unhiliteparens, buf, brother, brotheridx-1, brotheridx) def line_numbers_expose(self, textview, event, user_data=None): ''' makes the line-numbers. ''' left_win = textview.get_window(gtk.TEXT_WINDOW_LEFT) right_win = textview.get_window(gtk.TEXT_WINDOW_RIGHT) if event.window == left_win: type = gtk.TEXT_WINDOW_LEFT target = left_win elif event.window == right_win: type = gtk.TEXT_WINDOW_RIGHT target = right_win else: return False first_y = event.area.y last_y = first_y + event.area.height x, first_y = textview.window_to_buffer_coords(type, 0, first_y) x, last_y = textview.window_to_buffer_coords(type, 0, last_y) numbers = [] pixels = [] iter, top = textview.get_line_at_y(first_y) # For each iter, get its location and add it to the arrays. # Stop when we pass last_y count = 0 size = 0 while not iter.is_end(): y, height = textview.get_line_yrange(iter) pixels.append(y) numbers.append(iter.get_line()) count += 1 if (y + height) >= last_y: break iter.forward_line() layout = self.w.create_pango_layout("") for i in range(count): x, pos = textview.buffer_to_window_coords(type, 0, pixels[i]) strg = "%d" % (numbers[i]+1) layout.set_text(strg) self.w.style.paint_layout(target, self.w.state, False, None, self.w, None, 2, pos + 2, layout) return False def manage_mru(self, fn, add_bool=False): ''' removes fn from mrufn if add_bool: put it back in at the top ''' try: self.histry.remove(fn) except: pass f = open(mrufn) histrytowrite = f.read() f.close() histrytowrite = re.split('\r?\n?', histrytowrite) if fn in histrytowrite: histrytowrite.remove(fn) if add_bool: histrytowrite = [fn] + histrytowrite self.histry = [fn] + self.histry histrytowrite = histrytowrite[:2*lenmru] for i in range(len(histrytowrite)): histrytowrite[i] = histrytowrite[i].strip() # pesky '\r' for i in histrytowrite: if len(i) < 2: histrytowrite.remove(i) histryfile = open(mrufn,'w') histryfile.write('\n'.join(histrytowrite)) histryfile.close() self.uiman.remove_ui(self.uiman.merge_ids['mru']) mru_xml_data = '' if mrunumbers: mru_label = lambda i: '' % ( i, i, xml_encode(os.path.basename(self.histry[i].replace('_', '__'))), xml_encode(self.histry[i]) ) else: mru_label = lambda i: '' % ( i, xml_encode(os.path.basename(self.histry[i].replace('_', '__'))), xml_encode(self.histry[i]) ) for i in xrange(len(self.histry)): if os.path.isfile(self.histry[i]): mru_xml_data += mru_label(i) self.xml_to_ui(mruxmldata.replace('', mru_xml_data), 'mru') def mk_dialog(self, title, buttons, kill_after=True, response_on_esc=0, modal=True): d = gtk.Dialog(title, self.w, (gtk.DIALOG_NO_SEPARATOR | (modal and gtk.DIALOG_MODAL or 0)), buttons) respond_dialog_on_escape(d, response_on_esc) def newrun(d=d): d.show_all() r = gtk.Dialog.run(d) if kill_after: d.destroy() return r d.run = newrun return d def new_buffer(self, action=None): ''' adds an empty page to self.notebk, adorns and focuses it. returns the new textbuffer, so you can say ft.new_buffer().set_text(open(fn).read()) also appends a dict to self.buffer_handler_ids mapping event names to handler_ids which can be passed to the "disconnect" method of the indicated widget textview # callback "key-press-event" # capslock_statbar_switch "populate-popup" # populatecontextmenu "expose_event" # line_numbers_expose "drag_data_received" # dnd_received_data textbuffer # callback "insert-text" # registerinsertionwithundo "delete-range" # registerdeletionwithundo "mark-set" # hiliteparens where textview can be gotten with current_textview() or self.notebk.get_nth_page(n).get_child() and textbuffer can be gotten with current_buffer() or textview.get_buffer() ''' pagestokeepunmodified = [] for i in range(self.notebk.get_n_pages()): tabtxt = self.notebk.get_tab_label_text(self.notebk.get_nth_page(i)) if (tabtxt[0] is not '*') and (tabtxt[-1] is not '*'): pagestokeepunmodified.append(i) # done cacheing state of current buffers, start making a new one handler_ids = {} sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) textview = gtk.TextView() textview.set_wrap_mode(wordwrappref) textbuffer = textview.get_buffer() textbuffer.get_all_text = lambda:textbuffer.get_text(textbuffer.get_start_iter(), textbuffer.get_end_iter()) handler_ids["expose_event"] = textview.connect("expose_event", self.line_numbers_expose) handler_ids["mark-set"] = textbuffer.connect("mark-set", self.hiliteparens) handler_ids["key-press-event"] = textview.connect("key-press-event", self.capslock_statbar_switch) handler_ids["populate-popup"] = textview.connect("populate-popup", self.populatecontextmenu) handler_ids["changed"] = textbuffer.connect("changed", self.autoindent) handler_ids["insert-text"] = textbuffer.connect("insert-text", self.registerwithundo) handler_ids["delete-range"] = textbuffer.connect("delete-range", self.registerwithundo) try: # allow drag-n-dropping stuff on me--like files from explorer.exe----REQUIRES PYGTK 2.6 handler_ids["drag_data_received"] = textview.connect("drag_data_received", self.dnd_received_data) textview.drag_dest_add_uri_targets() except: pass textview.modify_font(pango.FontDescription(font)) found_tag = gtk.TextTag('becky') # found becky! :-) for kee,val in foundtexttagstyle.items(): try: found_tag.set_property(kee, val) except: print kee, 'is an invalid gtk.TextTag property;', print 'please change "foundtexttagstyle" in config.flauxtext accordingly' textbuffer.tag_table.add(found_tag) for tagnym, styledict in findparenstyle.items(): temptag = gtk.TextTag(tagnym) for kee, val in styledict.items(): try: temptag.set_property(kee, val) except: print kee, 'is an invalid gtk.TextTag property; please change "findparenstyle" in config.flauxtext accordingly' textbuffer.tag_table.add(temptag) if not self.notebk.get_show_tabs(): self.notebk.set_show_tabs(True) if linenums in ['Right', 'Both']: textview.set_border_window_size(gtk.TEXT_WINDOW_RIGHT, linenumwindowsize) if linenums in ['Left', 'Both']: textview.set_border_window_size(gtk.TEXT_WINDOW_LEFT, linenumwindowsize) sw.add(textview) sw.show() textview.show() self.notebk.append_page(sw, gtk.Label(UNTITLED_FILENAME)) textbuffer.undostack = UndoStack() textbuffer.filename = UNTITLED_FILENAME textbuffer.handler_ids = handler_ids self.notebk.set_current_page(self.notebk.page_num(sw)) try: self.actiongroup.get_action('toggle_editable').set_active(True) except: pass # done making a new buffer, change the state of all pages to reflect cache [pagestokeepunmodified] zothbfr = self.notebk.get_nth_page(0).get_child().get_buffer() if ((self.notebk.get_n_pages() == 2) and (self.notebk.get_tab_label_text(self.notebk.get_nth_page(0)) == self[0].filename == UNTITLED_FILENAME) and (zothbfr.get_all_text() == '')): # if you just opened FlauxText and haven't edited the initial textbuffer, i'll remove it self.notebk.remove_page(0) if self.notebk.get_n_pages()>1: for i in pagestokeepunmodified: pg_i = self.notebk.get_nth_page(i) tab_txt = self.notebk.get_tab_label_text(pg_i) if tab_txt[0] == tab_txt[-1] == '*': self.notebk.set_tab_label_text(pg_i, tab_txt[3:-3]) if tabshideifone and self.notebk.get_n_pages()==1: self.notebk.set_show_tabs(False) if self.notebk.get_n_pages()>1 and not self.notebk.get_show_tabs(): self.notebk.set_show_tabs(True) return textbuffer def notebk_page_switched(self, notebk=None, page=None, page_num=None): ''' called whenever self.notebk's page number changes; updates certain things like View > Toggles > Editable. ''' try: self.actiongroup.get_action('toggle_editable').set_active(self.current_textview().get_editable()) except: pass def open(self, filename): ''' adds filename to self.notebk using new_buffer. also manages my MRU, and does the createifdne logic. ''' if neveropenbinary and not istextfile(filename): self.flash_info(filename + ' is binary and neveropenbinary in my configfile is True', 4e4, 'binary_file') return fl = None try: fl = open(filename) except IOError: # file DNE if createifdne: fl = open(filename,'w') fl.close() fl = open(filename) else: print "could not find", filename, "... removing it from the history file." try: self.manage_mru(filename) return False except Exception, e: print e try: text = fl.read() fl.close() textbuffer = self.new_buffer() textbuffer.set_text(text) curpagen = self.notebk.get_current_page() self.notebk.set_tab_label_text(self.notebk.get_nth_page(curpagen), os.path.basename(filename)) self.w.set_title(windowtitle % filename) textbuffer.place_cursor(textbuffer.get_start_iter()) self.current_textview().scroll_to_iter(textbuffer.get_start_iter(),0.0) textbuffer.filename = filename textbuffer.undostack = UndoStack() try: self.manage_mru(filename, True) except IndexError: print i # shouldn't ever happen except IOError: self.flash_info("could not open %s for writing. you won't be able to open from the File > Previous menu." % (mrufn, filename), 3000, 'badmru') except AttributeError: pass def populatecontextmenu(self, textview, menu): ''' adds the menuitems in the Edit > Selected submenu to the context menu when you right-click on a textview. ''' submi = gtk.MenuItem('_Edit Selected') subm = gtk.Menu() submi.set_submenu(subm) submi.connect('activate-item',lambda a:subm.popup(None,submi,None,-1,-1)) callback = lambda action_name: lambda menuitem: self.run_plugin('editselected', action_name) for action_name in __imp0rt_plugin__('editselected').actions: action = self.actiongroup.get_action('plugin__editselected__' + action_name) if action is not None: mi = gtk.MenuItem(action.get_property('label')) mi.set_property('name', action_name) mi.show() mi.connect('activate', callback(action_name)) subm.append(mi) del mi submi.show() subm.show() menu.attach(submi, 0, 1, 0, 1) def registerwithundo(self, tb, titr, txt, ln=None): ''' called when you insert/delete txt into/from tb [the current textbuffer] at the textiter at the cursor [titr]. i use ln as a flag; the callback for 'insert-text' passes it as the length of the text entered, but that's accessible as len(txt) the callback for 'delete-range' doesn't provide it, so (ln != None) is True if text was inserted appends [tb.get_char_offset(),insbool,txt] to the undostack for the current buffer if any of: len(txt) > 1 # if you insert/delete more than a single char at a time ln == None # if you deleted text, i have no good way of adding it to previously deleted text self.current_undo_list() == None # if that list would be the first for its buffer (re.match('\W',txt) and re.search('\w', curundoentry.data)) # undo by words # as a consequence of the way this bit of logic is phrased, the following statement would always pass # assert re.match('\W*\w+', self.current_undo_list()[2]) ((ln != None) != curundoentry.was_inserted) # if you're inserting/deleting now, and you'd just deleted/inserted titr.get_offset() != self.current_undo_list()[1] + len(self.current_undo_list()[2]) # if you moved the cursor if none of those conditions are met, txt is appended to the current_undo_list()'s string. ''' curfn = tb.filename curundostack = tb.undostack curundoentry = curundostack.current() offset = titr.get_offset() stitr = tb.get_start_iter() enditr = tb.get_end_iter() if ln == None: txt = tb.get_text(titr, txt) if curundostack.pointer < (len(curundostack) - 1): # if you undid, then edited the buffer: forget what you'd done before curundostack.stack = curundostack.stack[:curundostack.pointer + 1] for i in findparenstyle.keys(): tb.remove_tag_by_name(i, stitr, enditr) current_scrolled_window = self.current_scrolledwindow() tabtxt = self.notebk.get_tab_label_text(current_scrolled_window) if (tabtxt[0] != '*') and (tabtxt[-1] != '*') and (curfn != UNTITLED_FILENAME): if self.autosave: self.save() else: self.notebk.set_tab_label_text(current_scrolled_window, '** ' + tabtxt + ' **') self.w.set_title((windowtitle % curfn) + windowtitlemodified) self.statbar_info.set('') if ((len(txt) > 1) or (ln == None) or (curundoentry == None) or (re.match('\W', txt) and re.search('\w', curundoentry.data)) or ((ln != None) != curundoentry.was_inserted) or (offset != (curundoentry.offset + len(curundoentry.data))) ): # make a new UndoStackEntry, increment the pointer curundostack.push(UndoStackEntry(offset=offset, was_inserted=(ln != None), data=txt)) else: # you're typing regularly curundoentry.data += txt def run_plugin(self, plg, *a, **kw): ''' if plg is an instance of gtk.Action: plg is prepended to a; plg = plg.get_name() calls the function named the value of plg in the plugin named plg, importing it as necessary. self is dynamically injected into the plugin's global namespace. flauxtext's global namespace is therefore accessible as 'self.globals'. *a and **kw are passed on to it. ''' if isinstance(plg, gtk.Action): a = [plg] + list(a) plg = plg.get_name() imp0rt_plugin(plg) if not plg.startswith('plugin'): plg = 'plugin' + plg eval(plg).self = self try: eval('%s.%s' % (plg, plg[6:]))(*a, **kw) except Exception, e: self.flash_info('malformed plugin: ' + plg[6:] + ' '*4 + str(e), 6e3, 'bad_plugin') def run_what(self, filename): ''' convenience function used by the 'run...' menuitems. fetches a command based on the filename and runcmds ''' if filename == UNTITLED_FILENAME: self.flash_info('you must save before you can run.', 3e3, 'unable_to_f5') return filenameopts = {'full': filename, 'dir': os.path.dirname(filename), 'ext': os.path.splitext(filename)[-1], 'base':os.path.splitext(os.path.basename(filename))[0] } runthis = '' for kee in runcmds: # runcmds is a dict in the os portion of config.flauxtext mapping fnmatch patterns to commands if fnmatch.fnmatch(filename,kee): if isinstance(runcmds[kee], str): # runthis=runcmds[kee]%tuple([filename]*(len(runcmds[kee].split('%s'))-1)) runthis = runcmds[kee] % filenameopts elif isinstance(runcmds[kee], (tuple, list)): for i in range(len(runcmds[kee])-1): os.popen(runcmds[kee][i] % filenameopts) runthis = runcmds[kee][-1] % filenameopts if runthis == '': runthis = termcmd + '"' + filename + '"' return runthis def save(self, which=None): ''' calls save_file if self.current_filename() != UNTITLED_FILENAME else calls callback_saveas ''' if which == None: bfr = self.current_buffer() fn = self.current_filename() else: bfr = self[which] if isinstance(which, (str, unicode)): fn = which else: fn = self[which].filename if fn == UNTITLED_FILENAME: self.callback_saveas(None) else: self.save_file(fn, bfr.get_all_text()) def save_file(self, filename, data): ''' opens filename for writing, writes data to filename, and relabels the current tab, or opens an error dialog. ''' try: open(filename, "w").write(data) self.notebk.set_tab_label_text(self.notebk.get_nth_page(self.notebk.get_current_page()),os.path.basename(filename)) except: d = self.mk_dialog('error! OMGOMG!', (gtk.STOCK_OK, 0)) d.vbox.pack_start(gtk.Label('unable to open '+filename+' for writing.')) d.run() def split(self, vertical=True, add2=False): ''' splits the main area into a (vertical and gtk.VPaned or gtk.HPaned), reparents self.vbox into the (add2 and 'second' or 'first') half of the new Paned appends the new Paned to self.panes, and returns it if you don't particularly want resize-ability and horizontal split, just use self.vbox.add(your_widget) ''' new_pane = (vertical and gtk.VPaned or gtk.HPaned)() new_pane.show() self.panes.append(new_pane) new_panes_parent = self.vbox.get_parent() new_panes_parent.remove(self.vbox) new_panes_parent.add(new_pane) if add2: new_pane.add2(self.vbox) else: new_pane.add1(self.vbox) new_panes_parent.show_all() return new_pane def split_from(self, direction, raise_if_sad=False): ''' call split in such a way as to return a pane on the direction side of the window, where direction is in ['left', 'right', 'top', 'bottom']. if direction is invalid and not raise_if_sad: direction = 'bottom' ''' if direction not in ['left', 'right', 'top', 'bottom']: if raise_if_sad: raise TypeError("direction not in ['left', 'right', 'top', 'bottom']") else: direction = 'bottom' return self.split(where in ['top', 'bottom'], where in ['left', 'top']) start = __call__ def unhiliteparens(self, bfr, tagnym, stidx, enidx): ''' removes the tag "tagnym" from the given buffer between the given indices used to suppress GtkWarnings while unhiliting pairs of parentheses ''' bfr.remove_tag_by_name(tagnym, bfr.get_iter_at_offset(stidx), bfr.get_iter_at_offset(enidx)) def update_window_title(self): ''' convenience method to change self.w's title ''' wintitle = windowtitle % self.current_filename() tabtext = self.notebk.get_tab_label_text(self.notebk.get_nth_page(self.notebk.get_current_page())) if tabtext[0] == tabtext[-1] == '*': wintitle = wintitle + windowtitlemodified if self.w.get_title() != wintitle: self.w.set_title(wintitle) def xml_to_ui(self, xml_str='', merge_id=None): ''' parses xml_str with xml.parsers.expat, extracts action tuples and gtk's version of uixml, passing both to self.uiman. also does the menustatbar logic. ''' action_tuples = [] toggle_action_tuples = [] new_xml = [] ignore_next_end_tag = [False] ignore_until_end_tag = [] def start_tag(tagtype, attrs={}): tagtype = tagtype.lower() if tagtype in ['separator', 'ui']: new_xml.append('<%s>' % tagtype) elif tagtype in ['menubar', 'toolbar']: new_xml.append('<%s name="%s">' % (tagtype, attrs.get('name', ''))) elif tagtype in ['menu', 'menuitem', 'toolitem']: if (len(ignore_until_end_tag) > 0) or (('if' in attrs) and not eval(attrs['if'], globals(), namespace)): ignore_until_end_tag.append(tagtype) else: action_name = attrs.get('action', '') #print action_name # XXX if 'hotkey' in attrs: hotkey = attrs['hotkey'] for modifier in ['control', 'shift', 'alt', 'meta', 'super', 'hyper']: hotkey = re.sub('(?i)' + re.escape(modifier + '+'), '<%s>' % modifier, hotkey) else: hotkey = None if tagtype == 'menu': callback = None else: try: if '__' in action_name: action_name_parts = action_name.split('__') callback_ = eval('self.callback_' + action_name_parts[0], namespace) callback = lambda evt: callback_(*(action_name_parts[1:] + [evt])) else: callback = eval('self.callback_' + action_name, namespace) except Exception, e: print e callback = None action_tuples.append(( action_name, eval('gtk.STOCK_' + attrs.get('icon', 'NONE').upper()), attrs.get('label', action_name), hotkey, attrs.get('tooltip', None), callback )) if 'toggle_init' in attrs: toggle_action_tuples.append(action_tuples.pop() + (eval(attrs['toggle_init']),)) new_xml.append('<%s action="%s">' % (tagtype, action_name)) else: print 'error parsing uixml:', tagtype, attrs new_xml.append('<%s %s>' % ((tagtype, ''.join([(k + '="%s"' % v) for k, v in attrs.items()])))) def end_tag(tagtype): if ignore_until_end_tag and (tagtype == ignore_until_end_tag[-1]): ignore_until_end_tag.pop() else: new_xml.append('' % tagtype) if menustatbar: old_start_tag, old_end_tag = start_tag, end_tag d = {'/MenuBar':{}} lvl = ['MenuBar'] def start_tag(tagtype, attrs={}): old_start_tag(tagtype, attrs) if 'action' in attrs: if tagtype == 'menuitem': statement = ('d["/%s"] = ""' % '"]["/'.join(lvl + [attrs['action']])) exec statement in namespace elif tagtype in ['menu', 'menubar']: lvl.append(attrs['action']) statement = ('d["/%s"] = {}' % '"]["/'.join(lvl)) exec statement in namespace def end_tag(tagtype=''): old_end_tag(tagtype) if tagtype in ['menu', 'menubar']: lvl.pop() namespace = locals() p = xml.parsers.expat.ParserCreate() p.StartElementHandler = start_tag p.EndElementHandler = end_tag try: p.Parse(xml_str, 1) except Exception, e: print 'could not parse xml:', e return self.actiongroup.add_actions(action_tuples) self.actiongroup.add_toggle_actions(toggle_action_tuples) if self.actiongroup in self.uiman.get_action_groups(): # if we find multiple xml files to merge, uiman freaks out if we add the same but different actiongroup again self.uiman.remove_action_group(self.actiongroup) self.uiman.insert_action_group(self.actiongroup, 0) try: if merge_id != None: self.uiman.merge_ids[merge_id] = self.uiman.add_ui_from_string(''.join(new_xml)) else: self.uiman.add_ui_from_string(''.join(new_xml)) except Exception, e: # shouldn't ever happen unless you borked some xml print 'badness10000:', ''.join(new_xml) print e self.uiman.ensure_update() if menustatbar: self.connect_menuitem_selections_statbar_info_recurse('', d) ########################################################################################################## ## INIT INIT INIT INIT INIT INIT INIT INIT INIT INIT INIT INIT INIT ########################################################################################################## def __init__(self, **kwargs): ''' FlauxText constructor. parses rc, loads MRU, loads ui.xml, makes a big honkin' VBox and packs a menubar [and toolbar] into it from the UIManager, packs a Notebook into the VBox, connects menubar menuitems to Actions to callbacks, dynamically creates the MRU and Special files [and os-specific] menuitems and connects those, puts initial page into the Notebook, and finally creates 4 statusbars and packs those into an HBox at the bottom of the VBox. ''' global uixmldata gtk.rc_parse(getmyfile('rc.flauxtext')) self.histry = map(str.strip, filter(os.path.isfile, re.split('\r?\n?', open(mrufn).read())))[:lenmru] self.specialfilelist = map(str.strip, filter(os.path.isfile, re.split('\r?\n?', open(specialsfn).read()))) if not '' in uixmldata: # ui.xml was created in the getmyfile call print "it needs to be named config.flauxtext, be INI-style, be in the same directory as 'flauxtext.pyw' [", print os.path.dirname(__file__), "], and define all of my configuration variables." print "if you accidentally deleted it, please download it again from" print "http://sf.net/projects/fauxlkner" sys.exit(2) if not os.path.exists(instancetrackingfn): # manage my poorly-named "instancetracking" file open(instancetrackingfn, 'w').write('') self.tempfilemtime = os.path.getmtime(instancetrackingfn) # manage my pid file, which is more responsible for instance tracking f = open(os.path.join(tempdir, 'pid.flauxtext'), 'w') try: f.write(str(os.getpgid(os.getpid()))) except: f.write(str(os.getpid())) f.close() self.lasthelppy = 'flauxtext' self.lastmanpage = 'bash' self.findagaininfo = {} self.autoident = autoindent self.autosave = autosaveinit self.cvsops = '' self.transparency = transparency self.findparens = findparens self.find_history = [] self.smtpcontactslist = None # load those in the smtp plugin, which might never be loaded self.panes = [] # list of gtk.Pane()s containing each other. the last Pane in this list holds self.vbox; see self.split self.w = None # save creating the top-level window for either start() or scripter's own methods # set it to None now so modal dialogs work somewhat if i'm imported [as opposed to not at all] self.vbox = gtk.VBox() self.vbox.show() self.uiman = gtk.UIManager() self.uiman.merge_ids = {} self.accelgroup = self.uiman.get_accel_group() self.actiongroup = gtk.ActionGroup('UIManagr') if tearoffmenus: self.uiman.set_add_tearoffs(True) self.notebk = gtk.Notebook() self.notebk.show() self.notebk.popup_enable() self.notebk.homogeneous=True self.notebk.set_scrollable(True) self.notebk.set_show_border(False) self.notebk.set_tab_pos(tabbarlocation) # populate the first tab textbuffer = self.new_buffer() specials_xml_data = '' bad_specials = [] for i in xrange(len(self.specialfilelist)): if os.path.isfile(self.specialfilelist[i]): specials_xml_data += '' % ( i, i + 1, xml_encode(os.path.basename(self.specialfilelist[i])), xml_encode(self.specialfilelist[i]) ) if openspecialsatstart: self.open(self.specialfilelist[i]) else: bad_specials.append(i) self.specialfilelist = [x for i, x in enumerate(self.specialfilelist) if i not in bad_specials] mru_xml_data = '' bad_mru = [] if mrunumbers: mru_label = lambda i: '' % ( i, i, xml_encode(os.path.basename(self.histry[i].replace('_', '__'))), xml_encode(self.histry[i]) ) else: mru_label = lambda i: '' % ( i, xml_encode(os.path.basename(self.histry[i].replace('_', '__'))), xml_encode(self.histry[i]) ) for i in xrange(len(self.histry)): if os.path.isfile(self.histry[i]): mru_xml_data += mru_label(i) else: bad_mru.append(i) self.specialfilelist = [x for i, x in enumerate(self.specialfilelist) if i not in bad_mru] self.xml_to_ui(uixmldata, 'main') self.xml_to_ui(mruxmldata.replace('', mru_xml_data), 'mru') self.xml_to_ui(specialsxmldata.replace('', specials_xml_data), 'specials') for fn in os.listdir(plugindir): if os.path.splitext(fn)[1] == '.xml': self.xml_to_ui(open(os.path.join(plugindir, fn)).read(), fn) self.menubar = self.uiman.get_widget('/MenuBar') self.toolbar = self.uiman.get_widget('/ToolBar') if self.menubar == None: print "error in ui.xml: can't find /MenuBar" sys.exit(2) else: self.vbox.pack_start(self.menubar, False) self.menubar.show() if self.toolbar: self.toolbar.set_icon_size(toolbariconsize) self.toolbar.set_style(toolbarstyle) self.toolbar.show() if toolbarvert in ['Left', 'Right']: self.tbarhbox = gtk.HBox(False) self.tbarhbox.show() self.vbox.pack_start(self.tbarhbox) self.toolbar.set_property('orientation', gtk.ORIENTATION_VERTICAL) self.tbarhbox.set_property('homogeneous', False) if toolbarvert == 'Left': self.tbarhbox.pack_start(self.toolbar, False, False) self.tbarhbox.pack_start(self.notebk) else: self.tbarhbox.pack_start(self.notebk) self.tbarhbox.pack_start(self.toolbar, False, False) else: self.vbox.pack_start(self.toolbar, False) self.vbox.pack_start(self.notebk) self.notebk_switch_page_handler_id = self.notebk.connect("switch-page", self.notebk_page_switched) self.statbarhbox = gtk.HBox() self.vbox.pack_start(self.statbarhbox, False) if showstatbar: self.statbarhbox.show() # make the status bar objects and pack them into the statbarhbox for nym in ['info', 'insovr', 'capslock', 'cursor']: real_nym = 'self.statbar_' + nym exec real_nym + ' = gtk.Statusbar()' in locals(), globals() sb = eval(real_nym) sb.show() sb.set_property('has-resize-grip', nym in statbarresizegrip) exec ('def set_this(s):\n try:self.statbar_%s.pop(1),self.statbar_%s.push(1, s)\n except:print s' % (nym, nym)) in locals() # i'm sorry, but the TSQ, spaced-out version of the above def statement wouldn't compile sb.set = set_this # pack the statbars according to the order you set in config for i in statbarorder: s = "self.statbarhbox.pack_start(self.statbar_%s, False, False)" % i s = s.replace("info, False, False", "info") # info is the only bar that should expand and receive extra space eval(s) # set sizes of 3 of the 4 statbars: w,h # these sizes will never change because these widgets were packed into their HBox with fill=expand=False # statbar_info will expand to fill the width of my toplevel window self.statbar_capslock.set_size_request(12, 15) self.statbar_insovr.set_size_request(30, 15) self.statbar_cursor.set_size_request(120, 15) # push starting strings to the statbars self.statbar_info.push(1, '') self.statbar_insovr.push(1, 'ins') self.statbar_capslock.push(1, 'a') self.statbar_capslock.value = 'a' self.statbar_cursor.push(1, statbarcursorformat % (1, 0)) self.__dict__.update(kwargs) # end __init__ # end class FlauxText def alreadyrunning(): ''' returns True if the contents of pid.flauxtext is in a tasklist [ps], False otherwise. ''' if os.name == 'nt': ps = os.popen('tasklist').read().lower() else: ps = os.popen('ps -A -o pgid').read().lower() try: f = open(os.path.join(tempdir, 'pid.flauxtext')) myotherpid = f.read() f.close() except IOError: # couldn't open pid.flauxtext return False return myotherpid in ps def test(): ''' runs doctest.testmod() ''' import doctest doctest.testmod() def parseargs(arg): ''' arg is a list of strings containing any number of the following mixed in with file/dirnames: -r|--regexp, -x|-T|--text, --noalpha return a list of filenames, and/or adjust my transparency global ''' if "--noalpha" in arg: transparency = False arg.remove("--noalpha") if "--regexp" in arg or "-r" in arg: if "-r" in arg: flag = '-r' else: flag = '--regexp' rxp = arg[arg.index(flag) + 1] for dirnym in arg[arg.index(flag) + 2:]: arg.remove(dirnym) for f in os.listdir(dirnym): if os.path.isfile(f) and re.match(rxp,f): arg.append(os.path.join(dirnym, f)) arg.remove(rxp) arg.remove(flag) if "-T" in arg or "-x" in arg or "--text" in arg: for i in arg: if os.path.isdir(i): arg.remove(i) arg.append([t for t in os.listdir(i) if istextfile(t)]) elif not istextfile(i): arg.remove(i) return [os.path.abspath(arg[i]) for i in range(len(arg)) if (os.path.isfile(arg[i]) or os.path.isdir(arg[i]) or (arg[i] == '-')) ] __version__ = 112 # int version here; major releases on sf __email__ = 'faulkner@users.sf.net' __author__ = 'faulkner <' + __email__ + '>' # email me if you should need my full legal name __license__ = 'GNU GPL 2' __url__ = 'http://fauxlkner.sf.net' __todo__ = [ 'fix the saveas folder bug', 'fix plugins: collab, hexed, ubbpost, macro', 'make all names more consistent with the pygtk naming scheme', 'update all docstrings', 'pygtksourceview on windows', 'rename/move/eliminate: unhiliteparens, hiliteparens, dnd_get_data, dnd_received_data', ] if __name__ == '__main__': if "-h" in sys.argv or "--help" in sys.argv: print __doc__ elif "--profile" in sys.argv: import profile arg = sys.argv[1:] arg.remove("--profile") profile.run('FlauxText()(parseargs(arg))') elif "--test" in sys.argv: test() elif "--version" in sys.argv or "-v" in sys.argv: print "release 2, revision", str(__version__) else: if (checkforalreadyrunning and not (("--notrack" in sys.argv) or ("-n" in sys.argv)) and (len(sys.argv) > 1) and alreadyrunning() ): open(os.path.join(tempdir, 'instancetracking.flauxtext'),'w').write(os.linesep.join(parseargs(sys.argv[1:]))) else: FlauxText()(parseargs(sys.argv[1:]))