📄 filediff.py
字号:
### Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org>### This program is free software; you can redistribute it and/or modify### it under the terms of the GNU General Public License as published by### the Free Software Foundation; either version 2 of the License, or### (at your option) any later version.### This program is distributed in the hope that it will be useful,### but WITHOUT ANY WARRANTY; without even the implied warranty of### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the### GNU General Public License for more details.### You should have received a copy of the GNU General Public License### along with this program; if not, write to the Free Software### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USAfrom __future__ import generatorsimport codecsimport mathimport osimport reimport difflibimport structimport pangoimport gobjectimport gtkimport gtk.keysymsimport diffutilimport gnomegladeimport miscimport melddocimport pathssourceview_available = 0for sourceview in "gtksourceview sourceview".split(): try: gsv = __import__(sourceview) sourceview_available = 1 break except ImportError: passif sourceview_available: def set_highlighting_enabled(buf, fname, enabled): # gnome.vfs.get_mime_type seems to be broken. fake it. extmap = { "xml":"text/xml", "glade":"text/xml", "cpp":"text/x-cpp", "cxx":"text/x-cpp", "cc":"text/x-cpp", "C":"text/x-cpp", "c":"text/x-c", "hpp":"text/x-cpp", "hxx":"text/x-cpp", "hh":"text/x-cpp", "H":"text/x-cpp", "h":"text/x-cpp", "inl":"text/x-cpp", "desktop": "application/x-desktop", "diff": "text/x-diff", "patch": "text/x-diff", "html": "text/html", "po": "text/x-po", "py": "text/x-python" } ext = fname.split(".")[-1] man = gsv.SourceLanguagesManager() gsl = man.get_language_from_mime_type( extmap.get(ext, "text/plain") ) if gsl: buf.set_language(gsl) buf.set_highlight(enabled)gdk = gtk.gdk################################################################################## FileDiff#################################################################################MASK_SHIFT, MASK_CTRL, MASK_ALT = 1, 2, 3class FileDiff(melddoc.MeldDoc, gnomeglade.Component): """Two or three way diff of text files. """ keylookup = {gtk.keysyms.Shift_L : MASK_SHIFT, gtk.keysyms.Control_L : MASK_CTRL, gtk.keysyms.Alt_L : MASK_ALT, gtk.keysyms.Shift_R : MASK_SHIFT, gtk.keysyms.Control_R : MASK_CTRL, gtk.keysyms.Alt_R : MASK_ALT } def __init__(self, prefs, num_panes): """Start up an filediff with num_panes empty contents. """ melddoc.MeldDoc.__init__(self, prefs) override = {} if sourceview_available: override["GtkTextView"] = gsv.SourceView override["GtkTextBuffer"] = gsv.SourceBuffer gnomeglade.Component.__init__(self, paths.share_dir("glade2/filediff.glade"), "filediff", override) self._map_widgets_into_lists( ["textview", "fileentry", "diffmap", "scrolledwindow", "linkmap", "statusimage"] ) self._update_regexes() self.warned_bad_comparison = False if sourceview_available: for v in self.textview: v.set_buffer( gsv.SourceBuffer() ) v.set_show_line_numbers(self.prefs.show_line_numbers) self.keymask = 0 self.load_font() self.deleted_lines_pending = -1 self.textview_overwrite = 0 self.textview_focussed = None self.textview_overwrite_handlers = [ t.connect("toggle-overwrite", self.on_textview_toggle_overwrite) for t in self.textview ] for i in range(3): w = self.scrolledwindow[i] w.get_vadjustment().connect("value-changed", self._sync_vscroll ) w.get_hadjustment().connect("value-changed", self._sync_hscroll ) self._connect_buffer_handlers() self.linediffer = diffutil.Differ() for l in self.linkmap: # glade bug workaround l.set_events(gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK ) l.set_double_buffered(0) # we call paint_begin ourselves self.bufferdata = [] for text in self.textview: text.set_wrap_mode( self.prefs.edit_wrap_lines ) buf = text.get_buffer() self.bufferdata.append( MeldBufferData() ) def add_tag(name, props): tag = buf.create_tag(name) for p,v in props.items(): tag.set_property(p,v) add_tag("edited line", {"background": self.prefs.color_edited_bg, "foreground": self.prefs.color_edited_fg} ) add_tag("delete line", {"background": self.prefs.color_delete_bg, "foreground": self.prefs.color_delete_fg} ) add_tag("replace line", {"background": self.prefs.color_replace_bg, "foreground": self.prefs.color_replace_fg} ) add_tag("conflict line", {"background": self.prefs.color_conflict_bg, "foreground": self.prefs.color_conflict_fg} ) add_tag("inline line", {"background": self.prefs.color_inline_bg, "foreground": self.prefs.color_inline_fg} ) class ContextMenu(gnomeglade.Component): def __init__(self, app): gladefile = paths.share_dir("glade2/filediff.glade") gnomeglade.Component.__init__(self, gladefile, "popup") self.parent = app self.pane = -1 def popup_in_pane( self, pane ): self.pane = pane self.copy_left.set_sensitive( pane > 0 ) self.copy_right.set_sensitive( pane+1 < self.parent.num_panes ) self.widget.popup( None, None, None, 3, gtk.get_current_event_time() ) def on_save_activate(self, menuitem): self.parent.save() def on_save_as_activate(self, menuitem): self.parent.save_file( self.pane, 1) def on_make_patch_activate(self, menuitem): self.parent.make_patch( self.pane ) def on_cut_activate(self, menuitem): self.parent.on_cut_activate() def on_copy_activate(self, menuitem): self.parent.on_copy_activate() def on_paste_activate(self, menuitem): self.parent.on_paste_activate() def on_copy_left_activate(self, menuitem): self.parent.copy_selected(-1) def on_copy_right_activate(self, menuitem): self.parent.copy_selected(1) def on_edit_activate(self, menuitem): if self.parent.bufferdata[self.pane].filename: self.parent._edit_files( [self.parent.bufferdata[self.pane].filename] ) self.popup_menu = ContextMenu(self) self.find_dialog = None self.last_search = None self.set_num_panes(num_panes) gtk.idle_add( lambda *args: self.load_font()) # hack around Bug 316730 def _update_regexes(self): self.regexes = [] for r in [ misc.ListItem(i) for i in self.prefs.regexes.split("\n") ]: if r.active: try: self.regexes.append( (re.compile(r.value+"(?m)"), r.value) ) except re.error: pass def _disconnect_buffer_handlers(self): for textview in self.textview: buf = textview.get_buffer() assert hasattr(buf,"handlers") textview.set_editable(0) for h in buf.handlers: buf.disconnect(h) def _connect_buffer_handlers(self): for textview in self.textview: buf = textview.get_buffer() textview.set_editable(1) id0 = buf.connect("insert-text", self.on_text_insert_text) id1 = buf.connect("delete-range", self.on_text_delete_range) id2 = buf.connect_after("insert-text", self.after_text_insert_text) id3 = buf.connect_after("delete-range", self.after_text_delete_range) buf.textview = textview buf.handlers = id0, id1, id2, id3 def _update_cursor_status(self, buf): def update(): it = buf.get_iter_at_mark( buf.get_insert() ) # Abbreviation for insert,overwrite so that it will fit in the status bar insert_overwrite = _("INS,OVR").split(",")[ self.textview_overwrite ] # Abbreviation for line, column so that it will fit in the status bar line_column = _("Ln %i, Col %i") % (it.get_line()+1, it.get_line_offset()+1) status = "%s : %s" % ( insert_overwrite, line_column ) self.emit("status-changed", status ) raise StopIteration; yield 0 self.scheduler.add_task( update().next ) def on_textbuffer_mark_set(self, buffer, it, mark): if mark.get_name() == "insert": self._update_cursor_status(buffer) def on_textview_focus_in_event(self, view, event): self.textview_focussed = view self._update_cursor_status(view.get_buffer()) def on_switch_event(self): if self.textview_focussed: self.scheduler.add_task( self.textview_focussed.grab_focus ) def _after_text_modified(self, buffer, startline, sizechange): if self.num_panes > 1: buffers = [t.get_buffer() for t in self.textview[:self.num_panes] ] pane = buffers.index(buffer) change_range = self.linediffer.change_sequence( pane, startline, sizechange, self._get_texts()) for it in self._update_highlighting( change_range[0], change_range[1] ): pass self.queue_draw() self._update_cursor_status(buffer) def _get_texts(self, raw=0): class FakeText(object): def __init__(self, buf, textfilter): self.buf, self.textfilter = buf, textfilter def __getslice__(self, lo, hi): b = self.buf txt = b.get_text(b.get_iter_at_line(lo), b.get_iter_at_line(hi), 0) txt = self.textfilter(txt) return txt.split("\n")[:-1] class FakeTextArray(object): def __init__(self, bufs, textfilter): self.texts = [FakeText(b, textfilter) for b in bufs] def __getitem__(self, i): return self.texts[i] return FakeTextArray( [t.get_buffer() for t in self.textview], [self._filter_text, lambda x:x][raw] ) def _filter_text(self, txt): def killit(m): assert m.group().count("\n") == 0 if len(m.groups()): s = m.group() for g in m.groups(): if g: s = s.replace(g,"") return s else: return "" try: for c,r in self.regexes: txt = c.sub(killit,txt) except AssertionError: if not self.warned_bad_comparison: misc.run_dialog(_("Regular expression '%s' changed the number of lines in the file. " \ "Comparison will be incorrect. See the user manual for more details.") % r) self.warned_bad_comparison = True return txt def after_text_insert_text(self, buffer, it, newtext, textlen): lines_added = newtext.count("\n") starting_at = it.get_line() - lines_added self._after_text_modified(buffer, starting_at, lines_added) def after_text_delete_range(self, buffer, it0, it1): starting_at = it0.get_line() assert self.deleted_lines_pending != -1 self._after_text_modified(buffer, starting_at, -self.deleted_lines_pending) self.deleted_lines_pending = -1 def load_font(self): fontdesc = pango.FontDescription(self.prefs.get_current_font()) context = self.textview0.get_pango_context() metrics = context.get_metrics( fontdesc, context.get_language() ) self.pixels_per_line = (metrics.get_ascent() + metrics.get_descent()) / 1024 self.pango_char_width = metrics.get_approximate_char_width() tabs = pango.TabArray(10, 0) tab_size = self.prefs.tab_size; for i in range(10): tabs.set_tab(i, pango.TAB_LEFT, i*tab_size*self.pango_char_width) for i in range(3): self.textview[i].modify_font(fontdesc) self.textview[i].set_tabs(tabs) for i in range(2): self.linkmap[i].queue_draw() load = lambda x: gnomeglade.load_pixbuf( paths.share_dir("glade2/pixmaps/"+x), self.pixels_per_line) self.pixbuf_apply0 = load("button_apply0.xpm") self.pixbuf_apply1 = load("button_apply1.xpm") self.pixbuf_delete = load("button_delete.xpm") self.pixbuf_copy0 = load("button_copy0.xpm") self.pixbuf_copy1 = load("button_copy1.xpm") def on_preference_changed(self, key, value): if key == "draw_style": for l in self.linkmap: l.queue_draw() elif key == "tab_size": tabs = pango.TabArray(10, 0) for i in range(10): tabs.set_tab(i, pango.TAB_LEFT, i*value*self.pango_char_width) for i in range(3): self.textview[i].set_tabs(tabs) elif key == "use_custom_font" or key == "custom_font": self.load_font() elif key == "show_line_numbers": if sourceview_available: for t in self.textview: t.set_show_line_numbers( value ) elif key == "use_syntax_highlighting": if sourceview_available: for i in range(self.num_panes): set_highlighting_enabled( self.textview[i].get_buffer(), self.bufferdata[i].filename, self.prefs.use_syntax_highlighting ) elif key == "regexes": self._update_regexes() elif key == "edit_wrap_lines": [t.set_wrap_mode( self.prefs.edit_wrap_lines ) for t in self.textview] def on_key_press_event(self, object, event): x = self.keylookup.get(event.keyval, 0) if self.keymask | x != self.keymask: self.keymask |= x for l in self.linkmap[:self.num_panes-1]: a = l.get_allocation() w = self.pixbuf_copy0.get_width() l.queue_draw_area(0, 0, w, a[3]) l.queue_draw_area(a[2]-w, 0, w, a[3]) def on_key_release_event(self, object, event):
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -