unified形式のdiffを取得してGTK+のテキストビューに表示する(diffファイル保存可)
「Pythonのdifflibモジュールを用いて複数行テキストどうしの差分を取得する」の例と同じようにunified形式の差分を取得するプログラムをPyGTKで作成した。
上部の2つのテキスト入力欄にそれぞれのテキストファイルの場所を入力後に下部の「Get diff」ボタンを押すことでGTK+のテキストビューにunified形式の差分を色付けして表示し、これをファイル保存ダイアログから保存することもできる。
色付け処理やフォントの変更は「PyGTKでテキストビュー内の文字列にスタイルを適用する」や「PyGTKでフォント選択ボタンのフォントをテキストビューに適用」の要領。行の最初の文字列を見るだけでどのスタイルで修飾するべきかを判別できるため、色付けは簡単にできた。
(2010/9/20)一部の処理を整理して見やすくしたものとその手法についてを「PyGTKでテキストビューを用いる際にテキストタグを多く作成・設定する場合の処理の整理について」で扱っている。
[任意]ファイル名: getdifftest.py
#! /usr/bin/python # -*- coding: utf-8 -*- import difflib import urllib import time import sys import os try: import pygtk pygtk.require('2.0') except: pass try: import pango import gtk except: print >> sys.stderr, 'Error: PyGTK is not installed' sys.exit(1) def run_errordialog(title, format1, format2=None): """ エラーダイアログを表示 """ errdlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_CLOSE, message_format=format1) if format2: errdlg.format_secondary_text(format2) errdlg.props.title = title errdlg.run() errdlg.destroy() def save_difffile(outfile, text): """ 差分ファイルを保存 """ try: f_out = open(outfile, 'w') except IOError, (errno, msg): run_errordialog('Error', 'cannot open output file "%s": %s', (outfile, msg)) return False try: try: f_out.write(text) except IOError, (errno, msg): run_errordialog('Error', 'cannot write output file "%s": %s', (outfile, msg)) return False finally: f_out.close() return True class LabelWithMnemonic(gtk.Label): """ ニーモニック・ウィジェットを初期化時に指定するラベル """ def __init__(self, str, widget): gtk.Label.__init__(self, str) self.props.use_underline = True self.props.mnemonic_widget = widget class BrowseButton(gtk.Button): """ 参照ボタン """ def __init__(self, entry): gtk.Button.__init__(self) self.__entry = entry self.props.label = '...' self.__filename = None self.connect('clicked', self.__on_self_clicked) def set_filename(self, filename): self.__filename = filename def __on_self_clicked(self, widget): opendlg = gtk.FileChooserDialog(title='Select file', buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT)) opendlg.props.local_only = True if self.__filename: opendlg.set_filename(self.__filename) if opendlg.run() == gtk.RESPONSE_ACCEPT: self.__filename = opendlg.get_filename() self.__entry.props.text = self.__filename opendlg.destroy() class PathEntry(gtk.Entry): """ パス指定用テキスト入力欄 """ __TARGET_TYPE_TEXT_URI_LIST = 12345 __dnd_list = [('text/uri-list', 0, __TARGET_TYPE_TEXT_URI_LIST),] def __init__(self): gtk.Entry.__init__(self) self.connect('drag_data_received', self.__on_drag_data_received) self.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP, self.__dnd_list, gtk.gdk.ACTION_COPY) def __on_drag_data_received(self, widget, context, x, y, selection, info, time): """ テキストを受け取ったパスに指定 """ for item in selection.data.rstrip('\x00').splitlines(): if item.startswith('file:'): path = os.path.normpath(urllib.url2pathname(item[5:])) if os.access(path, os.R_OK): self.props.text = path class MainWindow(gtk.Window): """ メインウィンドウ """ def __init__(self, *args, **kwargs): gtk.Window.__init__(self, *args, **kwargs) # ショートカットキー(アクセラレータ) self.__accelgroup = gtk.AccelGroup() self.add_accel_group(self.__accelgroup) # メニュー項目 self.__item_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.__accelgroup) self.__item_save = gtk.ImageMenuItem(gtk.STOCK_SAVE, self.__accelgroup) self.__item_saveas = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS, self.__accelgroup) self.__menu_file = gtk.Menu() self.__menu_file.prepend(self.__item_quit) self.__menu_file.prepend(gtk.SeparatorMenuItem()) self.__menu_file.prepend(self.__item_saveas) self.__menu_file.prepend(self.__item_save) self.__item_file = gtk.MenuItem('_File') self.__item_file.props.submenu = self.__menu_file self.__menubar = gtk.MenuBar() self.__menubar.append(self.__item_file) # 入力ファイル self.__entry_file_a = PathEntry() self.__entry_file_b = PathEntry() self.__btn_browse_file_a = BrowseButton(self.__entry_file_a) self.__label_file_a = LabelWithMnemonic('_A:', self.__entry_file_a) self.__label_file_b = LabelWithMnemonic('_B:', self.__entry_file_b) self.__btn_browse_file_b = BrowseButton(self.__entry_file_b) # テキストタグ self.__texttag_plus3 = gtk.TextTag('plus3') self.__texttag_plus3.props.foreground = '#e71585' self.__texttag_plus3.props.background = '#ffe4e1' self.__texttag_plus3.props.weight = pango.WEIGHT_BOLD self.__texttag_plus = gtk.TextTag('plus') self.__texttag_plus.props.family = 'Monospace' self.__texttag_plus.props.foreground = '#e71585' self.__texttag_plus.props.weight = pango.WEIGHT_BOLD self.__texttag_minus3 = gtk.TextTag('minus3') self.__texttag_minus3.props.foreground = '#4169e1' self.__texttag_minus3.props.background = '#e6e6fa' self.__texttag_minus3.props.weight = pango.WEIGHT_BOLD self.__texttag_minus = gtk.TextTag('minus') self.__texttag_minus.props.foreground = '#4169e1' self.__texttag_minus.props.weight = pango.WEIGHT_BOLD self.__texttag_hunk = gtk.TextTag('hunk') self.__texttag_hunk.props.foreground = '#2e8b57' self.__texttag_hunk.props.background = '#f0fff0' self.__texttag_normal = gtk.TextTag('normal') self.__texttag_normal.props.family = 'Monospace' self.__texttag_error = gtk.TextTag('error') self.__texttag_error.props.foreground = '#c00' # テキストビュー self.__texttagtable = gtk.TextTagTable() self.__texttagtable.add(self.__texttag_plus3) self.__texttagtable.add(self.__texttag_plus) self.__texttagtable.add(self.__texttag_minus3) self.__texttagtable.add(self.__texttag_minus) self.__texttagtable.add(self.__texttag_hunk) self.__texttagtable.add(self.__texttag_normal) self.__texttagtable.add(self.__texttag_error) self.__textbuf = gtk.TextBuffer(self.__texttagtable) self.__textview = gtk.TextView(self.__textbuf) self.__textview.props.wrap_mode = gtk.WRAP_CHAR self.__textview.props.editable = False self.__textview.modify_font(pango.FontDescription('Monospace 8')) self.__sw = gtk.ScrolledWindow() self.__sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) self.__sw.add(self.__textview) # ボタン self.__fontbtn = gtk.FontButton('Monospace 8') self.__button_comp = gtk.Button('Get _diff') # レイアウト用コンテナ self.__hbox_files = gtk.HBox() self.__hbox_files.pack_start(self.__label_file_a, expand=False, fill=False) self.__hbox_files.pack_start(self.__entry_file_a) self.__hbox_files.pack_start(self.__btn_browse_file_a, expand=False, fill=False) self.__hbox_files.pack_start(self.__label_file_b, expand=False, fill=False) self.__hbox_files.pack_start(self.__entry_file_b) self.__hbox_files.pack_start(self.__btn_browse_file_b, expand=False, fill=False) self.__hbox_btns = gtk.HBox() self.__hbox_btns.pack_start(self.__fontbtn) self.__hbox_btns.pack_start(self.__button_comp) self.__vbox = gtk.VBox() self.__vbox.pack_start(self.__menubar, expand=False, fill=False) self.__vbox.pack_start(self.__hbox_files, expand=False, fill=False) self.__vbox.pack_start(self.__sw) self.__vbox.pack_start(self.__hbox_btns, expand=False, fill=False) # シグナル self.connect('delete_event', gtk.main_quit) self.__item_quit.connect('activate', gtk.main_quit) self.__item_save.connect('activate', self.__on_item_save_activate) self.__item_saveas.connect('activate', self.__on_item_saveas_activate) self.__fontbtn.connect('font-set', lambda widget: self.__textview.modify_font(pango.FontDescription(widget.props.font_name))) self.__button_comp.connect('clicked', self.__on_button_comp_clicked) # ウィンドウ self.add(self.__vbox) self.set_size_request(400, 300) self.props.title = '(no input) - getdifftest' # 出力ファイル self.__outfile = None def __on_item_save_activate(self, widget): """ 差分を上書き保存 """ if self.__outfile: save_difffile(self.__outfile, self.__textbuf.props.text) else: self.__item_saveas.activate() def __on_item_saveas_activate(self, widget): """ 差分を別名で保存 """ savedlg = gtk.FileChooserDialog(title='Save diff file', action=gtk.FILE_CHOOSER_ACTION_SAVE, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT)) savedlg.props.do_overwrite_confirmation = True savedlg.props.local_only = True if self.__outfile: savedlg.set_filename(self.__outfile) res = savedlg.run() if res == gtk.RESPONSE_ACCEPT: self.__outfile = savedlg.get_filename() savedlg.destroy() if res == gtk.RESPONSE_ACCEPT: save_difffile(self.__outfile, self.__textbuf.props.text) self.props.title = '%s - getdifftest' % self.__outfile def __append_output(self, text, tagname): """ TextBufferにテキストを追加 """ iter = self.__textbuf.get_end_iter() self.__textbuf.place_cursor(iter) self.__textbuf.insert_with_tags_by_name(iter, text, tagname) def __on_button_comp_clicked(self, widget): """ 比較ボタンが押されたら差分をテキストビューに表示 """ self.__textbuf.props.text = '' # 各ファイルのデータをリストに読み込む infile_a = self.__entry_file_a.props.text try: f_in_a = open(infile_a, 'r') except IOError, (errno, msg): self.__append_output('Error: cannot open file "%s": %s' % (infile_a, msg), 'error') return infile_b = self.__entry_file_b.props.text try: f_in_b = open(infile_b, 'r') except IOError, (errno, msg): f_in_a.close() self.__append_output('Error: cannot open file "%s": %s' % (infile_b, msg), 'error') return try: try: text_a = f_in_a.readlines() except IOError, (errno, msg): self.__append_output('Error: cannot read file "%s": %s' % (infile_a, msg), 'error') f_in_b.close() return finally: f_in_a.close() try: try: text_b = f_in_b.readlines() except IOError, (errno, msg): self.__append_output('Error: cannot read file "%s": %s' % (infile_b, msg), 'error') return finally: f_in_b.close() # 比較 g = difflib.unified_diff(text_a, text_b, infile_a, infile_b, time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.path.getmtime(infile_a))), time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.path.getmtime(infile_b))), 3, '\n') # 結果をテキストビューに表示 for l in g: if l.startswith('+++'): self.__append_output(l, 'plus3') elif l.startswith('---'): self.__append_output(l, 'minus3') elif l.startswith('+'): self.__append_output(l, 'plus') elif l.startswith('-'): self.__append_output(l, 'minus') elif l.startswith('@@'): self.__append_output(l, 'hunk') else: self.__append_output(l, 'normal') self.props.title = '*new diff* - getdifftest' self.__outfile = None class GetDiffTest: """ unified形式のdiffを取得してテキストビューに表示 """ def main(self): """ アプリケーションのメイン処理 """ win = MainWindow() win.show_all() gtk.main() if __name__ == '__main__': app = GetDiffTest() app.main()
(2010/4/4)フォント変更の処理を改善
メニューの「(上書き)保存」と「別名で保存」が分かれているが、編集はできないので上書きの保存ができてもあまり意味はないかもしれない。
ファイルの場所を入力する2つの欄は、密かにドラッグ・アンド・ドロップでそのファイルの場所が入力されるようになっている。
使用したバージョン:
- Python 2.6.4
- PyGTK 2.16.0