1# Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org>
2# Copyright (C) 2009-2010, 2013 Kai Willadsen <kai.willadsen@gmail.com>
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 2 of the License, or (at
7# your option) any later version.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12# General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17import difflib
18import os
19
20from gi.repository import Gdk
21from gi.repository import Gio
22from gi.repository import GLib
23from gi.repository import Gtk
24from gi.repository import GtkSource
25
26from meld.conf import _
27from meld.iohelpers import prompt_save_filename
28from meld.misc import error_dialog
29from meld.settings import meldsettings
30from meld.sourceview import LanguageManager
31from meld.ui.gnomeglade import Component
32
33
34class PatchDialog(Component):
35
36    def __init__(self, filediff):
37        super().__init__("patch-dialog.ui", "patchdialog")
38
39        self.widget.set_transient_for(filediff.widget.get_toplevel())
40        self.filediff = filediff
41
42        buf = GtkSource.Buffer()
43        self.textview.set_buffer(buf)
44        lang = LanguageManager.get_language_from_mime_type("text/x-diff")
45        buf.set_language(lang)
46        buf.set_highlight_syntax(True)
47
48        self.textview.modify_font(meldsettings.font)
49        self.textview.set_editable(False)
50
51        self.index_map = {self.left_radiobutton: (0, 1),
52                          self.right_radiobutton: (1, 2)}
53        self.left_patch = True
54        self.reverse_patch = self.reverse_checkbutton.get_active()
55
56        if self.filediff.num_panes < 3:
57            self.side_selection_label.hide()
58            self.side_selection_box.hide()
59
60        meldsettings.connect('changed', self.on_setting_changed)
61
62    def on_setting_changed(self, setting, key):
63        if key == "font":
64            self.textview.modify_font(meldsettings.font)
65
66    def on_buffer_selection_changed(self, radiobutton):
67        if not radiobutton.get_active():
68            return
69        self.left_patch = radiobutton == self.left_radiobutton
70        self.update_patch()
71
72    def on_reverse_checkbutton_toggled(self, checkbutton):
73        self.reverse_patch = checkbutton.get_active()
74        self.update_patch()
75
76    def update_patch(self):
77        indices = (0, 1)
78        if not self.left_patch:
79            indices = (1, 2)
80        if self.reverse_patch:
81            indices = (indices[1], indices[0])
82
83        texts = []
84        for b in self.filediff.textbuffer:
85            start, end = b.get_bounds()
86            text = b.get_text(start, end, False)
87            lines = text.splitlines(True)
88
89            # Ensure that the last line ends in a newline
90            barelines = text.splitlines(False)
91            if barelines and lines and barelines[-1] == lines[-1]:
92                # Final line lacks a line-break; add in a best guess
93                if len(lines) > 1:
94                    previous_linebreak = lines[-2][len(barelines[-2]):]
95                else:
96                    previous_linebreak = "\n"
97                lines[-1] += previous_linebreak
98
99            texts.append(lines)
100
101        names = [self.filediff.textbuffer[i].data.label for i in range(3)]
102        prefix = os.path.commonprefix(names)
103        names = [n[prefix.rfind("/") + 1:] for n in names]
104
105        buf = self.textview.get_buffer()
106        text0, text1 = texts[indices[0]], texts[indices[1]]
107        name0, name1 = names[indices[0]], names[indices[1]]
108
109        diff = difflib.unified_diff(text0, text1, name0, name1)
110        diff_text = "".join(d for d in diff)
111        buf.set_text(diff_text)
112
113    def save_patch(self, targetfile: Gio.File):
114        buf = self.textview.get_buffer()
115        sourcefile = GtkSource.File.new()
116        saver = GtkSource.FileSaver.new_with_target(
117            buf, sourcefile, targetfile)
118        saver.save_async(
119            GLib.PRIORITY_HIGH,
120            callback=self.file_saved_cb,
121        )
122
123    def file_saved_cb(self, saver, result, *args):
124        gfile = saver.get_location()
125        try:
126            saver.save_finish(result)
127        except GLib.Error as err:
128            filename = GLib.markup_escape_text(gfile.get_parse_name())
129            error_dialog(
130                primary=_("Could not save file %s.") % filename,
131                secondary=_("Couldn’t save file due to:\n%s") % (
132                    GLib.markup_escape_text(str(err))),
133            )
134
135    def run(self):
136        self.update_patch()
137
138        result = self.widget.run()
139        if result < 0:
140            self.widget.hide()
141            return
142
143        # Copy patch to clipboard
144        if result == 1:
145            buf = self.textview.get_buffer()
146            start, end = buf.get_bounds()
147            clip = Gtk.Clipboard.get_default(Gdk.Display.get_default())
148            clip.set_text(buf.get_text(start, end, False), -1)
149            clip.store()
150        # Save patch as a file
151        else:
152            gfile = prompt_save_filename(_("Save Patch"))
153            if gfile:
154                self.save_patch(gfile)
155
156        self.widget.hide()
157