meld-fourdiff 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. meld/__init__.py +0 -0
  2. meld/accelerators.py +67 -0
  3. meld/actiongutter.py +420 -0
  4. meld/chunkmap.py +467 -0
  5. meld/conf.py +88 -0
  6. meld/const.py +43 -0
  7. meld/diffgrid.py +349 -0
  8. meld/dirdiff.py +2022 -0
  9. meld/externalhelpers.py +141 -0
  10. meld/filediff.py +2851 -0
  11. meld/filters.py +150 -0
  12. meld/fourdiff.py +536 -0
  13. meld/gutterrendererchunk.py +214 -0
  14. meld/imagediff.py +322 -0
  15. meld/iohelpers.py +273 -0
  16. meld/linkmap.py +140 -0
  17. meld/matchers/__init__.py +0 -0
  18. meld/matchers/diffutil.py +532 -0
  19. meld/matchers/helpers.py +117 -0
  20. meld/matchers/merge.py +302 -0
  21. meld/matchers/myers.py +445 -0
  22. meld/meldapp.py +422 -0
  23. meld/meldbuffer.py +358 -0
  24. meld/melddoc.py +168 -0
  25. meld/meldwindow.py +523 -0
  26. meld/menuhelpers.py +23 -0
  27. meld/misc.py +466 -0
  28. meld/newdifftab.py +193 -0
  29. meld/patchdialog.py +163 -0
  30. meld/preferences.py +380 -0
  31. meld/recent.py +247 -0
  32. meld/settings.py +149 -0
  33. meld/share/applications/org.gnome.Meld.desktop +171 -0
  34. meld/share/icons/hicolor/scalable/apps/org.gnome.Meld.svg +182 -0
  35. meld/share/icons/hicolor/symbolic/apps/org.gnome.Meld-symbolic.svg +3 -0
  36. meld/share/locale/ar/LC_MESSAGES/meld.mo +0 -0
  37. meld/share/locale/bg/LC_MESSAGES/meld.mo +0 -0
  38. meld/share/locale/bs/LC_MESSAGES/meld.mo +0 -0
  39. meld/share/locale/ca/LC_MESSAGES/meld.mo +0 -0
  40. meld/share/locale/ca@valencia/LC_MESSAGES/meld.mo +0 -0
  41. meld/share/locale/cs/LC_MESSAGES/meld.mo +0 -0
  42. meld/share/locale/da/LC_MESSAGES/meld.mo +0 -0
  43. meld/share/locale/de/LC_MESSAGES/meld.mo +0 -0
  44. meld/share/locale/dz/LC_MESSAGES/meld.mo +0 -0
  45. meld/share/locale/el/LC_MESSAGES/meld.mo +0 -0
  46. meld/share/locale/en_CA/LC_MESSAGES/meld.mo +0 -0
  47. meld/share/locale/en_GB/LC_MESSAGES/meld.mo +0 -0
  48. meld/share/locale/eo/LC_MESSAGES/meld.mo +0 -0
  49. meld/share/locale/es/LC_MESSAGES/meld.mo +0 -0
  50. meld/share/locale/eu/LC_MESSAGES/meld.mo +0 -0
  51. meld/share/locale/fa/LC_MESSAGES/meld.mo +0 -0
  52. meld/share/locale/fi/LC_MESSAGES/meld.mo +0 -0
  53. meld/share/locale/fr/LC_MESSAGES/meld.mo +0 -0
  54. meld/share/locale/gl/LC_MESSAGES/meld.mo +0 -0
  55. meld/share/locale/he/LC_MESSAGES/meld.mo +0 -0
  56. meld/share/locale/hi/LC_MESSAGES/meld.mo +0 -0
  57. meld/share/locale/hu/LC_MESSAGES/meld.mo +0 -0
  58. meld/share/locale/id/LC_MESSAGES/meld.mo +0 -0
  59. meld/share/locale/it/LC_MESSAGES/meld.mo +0 -0
  60. meld/share/locale/ja/LC_MESSAGES/meld.mo +0 -0
  61. meld/share/locale/ka/LC_MESSAGES/meld.mo +0 -0
  62. meld/share/locale/ko/LC_MESSAGES/meld.mo +0 -0
  63. meld/share/locale/nb/LC_MESSAGES/meld.mo +0 -0
  64. meld/share/locale/ne/LC_MESSAGES/meld.mo +0 -0
  65. meld/share/locale/nl/LC_MESSAGES/meld.mo +0 -0
  66. meld/share/locale/oc/LC_MESSAGES/meld.mo +0 -0
  67. meld/share/locale/pa/LC_MESSAGES/meld.mo +0 -0
  68. meld/share/locale/pl/LC_MESSAGES/meld.mo +0 -0
  69. meld/share/locale/pt/LC_MESSAGES/meld.mo +0 -0
  70. meld/share/locale/pt_BR/LC_MESSAGES/meld.mo +0 -0
  71. meld/share/locale/ro/LC_MESSAGES/meld.mo +0 -0
  72. meld/share/locale/ru/LC_MESSAGES/meld.mo +0 -0
  73. meld/share/locale/rw/LC_MESSAGES/meld.mo +0 -0
  74. meld/share/locale/sk/LC_MESSAGES/meld.mo +0 -0
  75. meld/share/locale/sl/LC_MESSAGES/meld.mo +0 -0
  76. meld/share/locale/sq/LC_MESSAGES/meld.mo +0 -0
  77. meld/share/locale/sr/LC_MESSAGES/meld.mo +0 -0
  78. meld/share/locale/sr@latin/LC_MESSAGES/meld.mo +0 -0
  79. meld/share/locale/sv/LC_MESSAGES/meld.mo +0 -0
  80. meld/share/locale/tr/LC_MESSAGES/meld.mo +0 -0
  81. meld/share/locale/uk/LC_MESSAGES/meld.mo +0 -0
  82. meld/share/locale/vi/LC_MESSAGES/meld.mo +0 -0
  83. meld/share/locale/zh_CN/LC_MESSAGES/meld.mo +0 -0
  84. meld/share/locale/zh_TW/LC_MESSAGES/meld.mo +0 -0
  85. meld/share/meld/COPYING +339 -0
  86. meld/share/meld/gschemas.compiled +0 -0
  87. meld/share/meld/org.gnome.Meld.gresource +0 -0
  88. meld/share/meld/styles/meld-base.style-scheme.xml +50 -0
  89. meld/share/meld/styles/meld-dark.style-scheme.xml +51 -0
  90. meld/share/metainfo/org.gnome.Meld.metainfo.xml +359 -0
  91. meld/share/mime/packages/org.gnome.Meld.xml +43 -0
  92. meld/sourceview.py +521 -0
  93. meld/style.py +129 -0
  94. meld/task.py +168 -0
  95. meld/tree.py +301 -0
  96. meld/treehelpers.py +132 -0
  97. meld/ui/__init__.py +0 -0
  98. meld/ui/bufferselectors.py +149 -0
  99. meld/ui/cellrenderers.py +128 -0
  100. meld/ui/emblemcellrenderer.py +127 -0
  101. meld/ui/filebutton.py +88 -0
  102. meld/ui/findbar.py +197 -0
  103. meld/ui/gladesupport.py +14 -0
  104. meld/ui/gtkcompat.py +145 -0
  105. meld/ui/gtkutil.py +25 -0
  106. meld/ui/historyentry.py +153 -0
  107. meld/ui/listwidget.py +69 -0
  108. meld/ui/msgarea.py +143 -0
  109. meld/ui/notebook.py +167 -0
  110. meld/ui/notebooklabel.py +59 -0
  111. meld/ui/pathlabel.py +247 -0
  112. meld/ui/recentselector.py +94 -0
  113. meld/ui/statusbar.py +297 -0
  114. meld/ui/util.py +97 -0
  115. meld/ui/vcdialogs.py +127 -0
  116. meld/undo.py +289 -0
  117. meld/vc/COPYING +21 -0
  118. meld/vc/README +4 -0
  119. meld/vc/__init__.py +50 -0
  120. meld/vc/_null.py +50 -0
  121. meld/vc/_vc.py +482 -0
  122. meld/vc/bzr.py +239 -0
  123. meld/vc/cvs.py +166 -0
  124. meld/vc/darcs.py +174 -0
  125. meld/vc/git.py +380 -0
  126. meld/vc/mercurial.py +116 -0
  127. meld/vc/svn.py +205 -0
  128. meld/vcview.py +972 -0
  129. meld/windowstate.py +76 -0
  130. meld_fourdiff-0.0.1.data/scripts/meld-fourdiff +467 -0
  131. meld_fourdiff-0.0.1.dist-info/COPYING +339 -0
  132. meld_fourdiff-0.0.1.dist-info/METADATA +451 -0
  133. meld_fourdiff-0.0.1.dist-info/RECORD +134 -0
  134. meld_fourdiff-0.0.1.dist-info/WHEEL +4 -0
meld/__init__.py ADDED
File without changes
meld/accelerators.py ADDED
@@ -0,0 +1,67 @@
1
+
2
+ from typing import Dict, Sequence, Union
3
+
4
+ from gi.repository import Gtk
5
+
6
+ VIEW_ACCELERATORS: Dict[str, Union[str, Sequence[str]]] = {
7
+ 'app.quit': '<Primary>Q',
8
+ 'app.help': 'F1',
9
+ 'app.preferences': '<Primary>comma',
10
+ 'view.find': '<Primary>F',
11
+ 'view.find-next': ('<Primary>G', 'F3'),
12
+ 'view.find-previous': ('<Primary><Shift>G', '<Shift>F3'),
13
+ 'view.find-replace': '<Primary>H',
14
+ 'view.go-to-line': '<Primary>I',
15
+ # Overridden in CSS
16
+ 'view.next-change': ('<Alt>Down', '<Alt>KP_Down', '<Primary>D'),
17
+ 'view.next-pane': '<Alt>Page_Down',
18
+ 'view.open-external': '<Primary><Shift>O',
19
+ # Overridden in CSS
20
+ 'view.previous-change': ('<Alt>Up', '<Alt>KP_Up', '<Primary>E'),
21
+ 'view.previous-pane': '<Alt>Page_Up',
22
+ 'view.redo': '<Primary><Shift>Z',
23
+ 'view.refresh': ('<control>R', 'F5'),
24
+ 'view.save': '<Primary>S',
25
+ 'view.save-all': '<Primary><Shift>L',
26
+ 'view.save-as': '<Primary><Shift>S',
27
+ 'view.undo': '<Primary>Z',
28
+ 'win.close': '<Primary>W',
29
+ 'win.gear-menu': 'F10',
30
+ 'win.fullscreen': 'F11',
31
+ 'win.new-tab': '<Primary>N',
32
+ 'win.stop': 'Escape',
33
+ # Shared bindings for per-view filter menu buttons
34
+ 'view.vc-filter': 'F8',
35
+ 'view.folder-filter': 'F8',
36
+ 'view.text-filter': 'F8',
37
+ # File comparison actions
38
+ 'view.file-previous-conflict': '<Primary>J',
39
+ 'view.file-next-conflict': '<Primary>K',
40
+ 'view.file-push-left': '<Alt>Left',
41
+ 'view.file-push-right': '<Alt>Right',
42
+ 'view.file-pull-left': '<Alt><shift>Right',
43
+ 'view.file-pull-right': '<Alt><shift>Left',
44
+ 'view.file-copy-left-up': '<Alt>bracketleft',
45
+ 'view.file-copy-right-up': '<Alt>bracketright',
46
+ 'view.file-copy-left-down': '<Alt>semicolon',
47
+ 'view.file-copy-right-down': '<Alt>quoteright',
48
+ 'view.file-delete': ('<Alt>Delete', '<Alt>KP_Delete'),
49
+ 'view.show-overview-map': 'F9',
50
+ # Folder comparison actions
51
+ 'view.folder-compare': 'Return',
52
+ 'view.folder-copy-left': '<Alt>Left',
53
+ 'view.folder-copy-right': '<Alt>Right',
54
+ 'view.folder-delete': 'Delete',
55
+ # Version control actions
56
+ 'view.vc-commit': '<Primary>M',
57
+ 'view.vc-console-visible': 'F9',
58
+ # Swap the two panes
59
+ 'view.swap-2-panes': '<Alt>backslash',
60
+ 'view.toggle-fourdiff-view': '<Primary>T',
61
+ }
62
+
63
+
64
+ def register_accels(app: Gtk.Application):
65
+ for name, accel in VIEW_ACCELERATORS.items():
66
+ accel = accel if isinstance(accel, tuple) else (accel,)
67
+ app.set_accels_for_action(name, accel)
meld/actiongutter.py ADDED
@@ -0,0 +1,420 @@
1
+ # Copyright (C) 2019 Kai Willadsen <kai.willadsen@gmail.com>
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 2 of the License, or (at
6
+ # your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+ import bisect
17
+ from typing import Dict, Optional
18
+
19
+ from gi.repository import Gdk, GdkPixbuf, GObject, Gtk
20
+
21
+ from meld.conf import _
22
+ from meld.const import ActionMode, ChunkAction
23
+ from meld.settings import get_meld_settings
24
+ from meld.style import get_common_theme
25
+ from meld.ui.gtkcompat import get_style
26
+ from meld.ui.gtkutil import make_gdk_rgba
27
+
28
+
29
+ class ActionIcons:
30
+
31
+ #: Fixed size of the renderer. Ideally this would be font-dependent and
32
+ #: would adjust to other textview attributes, but that's both quite
33
+ #: difficult and not necessarily desirable.
34
+ pixbuf_height = 16
35
+ icon_cache: Dict[str, GdkPixbuf.Pixbuf] = {}
36
+ icon_name_prefix = 'meld-change'
37
+
38
+ @classmethod
39
+ def load(cls, icon_name: str):
40
+ icon = cls.icon_cache.get(icon_name)
41
+
42
+ if not icon:
43
+ icon_theme = Gtk.IconTheme.get_default()
44
+ icon = icon_theme.load_icon(
45
+ f'{cls.icon_name_prefix}-{icon_name}', cls.pixbuf_height, 0)
46
+ cls.icon_cache[icon_name] = icon
47
+
48
+ return icon
49
+
50
+
51
+ class ActionGutter(Gtk.DrawingArea):
52
+
53
+ __gtype_name__ = 'ActionGutter'
54
+
55
+ action_mode = GObject.Property(
56
+ type=int,
57
+ nick='Action mode for chunk change actions',
58
+ default=ActionMode.Replace,
59
+ )
60
+
61
+ @GObject.Property(
62
+ type=object,
63
+ nick='List of diff chunks for display',
64
+ )
65
+ def chunks(self):
66
+ return self._chunks
67
+
68
+ @chunks.setter
69
+ def chunks_set(self, chunks):
70
+ self._chunks = chunks
71
+ self.chunk_starts = [c.start_a for c in chunks]
72
+ self.pointer_chunk = None
73
+
74
+ @GObject.Property(
75
+ type=Gtk.TextDirection,
76
+ nick='Which direction should directional changes appear to go',
77
+ flags=(
78
+ GObject.ParamFlags.READABLE |
79
+ GObject.ParamFlags.WRITABLE |
80
+ GObject.ParamFlags.CONSTRUCT_ONLY
81
+ ),
82
+ default=Gtk.TextDirection.LTR,
83
+ )
84
+ def icon_direction(self):
85
+ return self._icon_direction
86
+
87
+ @icon_direction.setter
88
+ def icon_direction_set(self, direction: Gtk.TextDirection):
89
+ if direction not in (Gtk.TextDirection.LTR, Gtk.TextDirection.RTL):
90
+ raise ValueError('Invalid icon direction {}'.format(direction))
91
+
92
+ replace_icons = {
93
+ Gtk.TextDirection.LTR: 'apply-right',
94
+ Gtk.TextDirection.RTL: 'apply-left',
95
+ }
96
+ self.action_map = {
97
+ ActionMode.Replace: ActionIcons.load(replace_icons[direction]),
98
+ ActionMode.Delete: ActionIcons.load('delete'),
99
+ ActionMode.Insert: ActionIcons.load('copy'),
100
+ }
101
+ self._icon_direction = direction
102
+
103
+ _source_view: Gtk.TextView
104
+ _source_editable_connect_id: int = 0
105
+
106
+ @GObject.Property(
107
+ type=Gtk.TextView,
108
+ nick='Text view for which action are displayed',
109
+ default=None,
110
+ )
111
+ def source_view(self):
112
+ return self._source_view
113
+
114
+ @source_view.setter
115
+ def source_view_setter(self, view: Gtk.TextView):
116
+ if self._source_editable_connect_id:
117
+ self._source_view.disconnect(self._source_editable_connect_id)
118
+
119
+ self._source_editable_connect_id = view.connect(
120
+ 'notify::editable', lambda *args: self.queue_draw())
121
+ self._source_view = view
122
+ self.queue_draw()
123
+
124
+ _target_view: Gtk.TextView
125
+ _target_editable_connect_id: int = 0
126
+
127
+ @GObject.Property(
128
+ type=Gtk.TextView,
129
+ nick='Text view to which actions are directed',
130
+ default=None,
131
+ )
132
+ def target_view(self):
133
+ return self._target_view
134
+
135
+ @target_view.setter
136
+ def target_view_setter(self, view: Gtk.TextView):
137
+ if self._target_editable_connect_id:
138
+ self._target_view.disconnect(self._target_editable_connect_id)
139
+
140
+ self._target_editable_connect_id = view.connect(
141
+ 'notify::editable', lambda *args: self.queue_draw())
142
+ self._target_view = view
143
+ self.queue_draw()
144
+
145
+ @GObject.Signal
146
+ def chunk_action_activated(
147
+ self,
148
+ action: str, # String-ified ChunkAction
149
+ from_view: Gtk.TextView,
150
+ to_view: Gtk.TextView,
151
+ chunk: object,
152
+ ) -> None:
153
+ ...
154
+
155
+ def __init__(self):
156
+ super().__init__()
157
+
158
+ # Object-type defaults
159
+ self.chunks = []
160
+ self.action_map = {}
161
+
162
+ # State for "button" implementation
163
+ self.buttons = []
164
+ self.pointer_chunk = None
165
+ self.pressed_chunk = None
166
+
167
+ self.motion_controller = Gtk.EventControllerMotion(widget=self)
168
+ self.motion_controller.set_propagation_phase(Gtk.PropagationPhase.TARGET)
169
+ self.motion_controller.connect("enter", self.motion_event)
170
+ self.motion_controller.connect("leave", self.motion_event)
171
+ self.motion_controller.connect("motion", self.motion_event)
172
+
173
+ def on_setting_changed(self, settings, key):
174
+ if key == 'style-scheme':
175
+ self.fill_colors, self.line_colors = get_common_theme()
176
+ alpha = self.fill_colors['current-chunk-highlight'].alpha
177
+ self.chunk_highlights = {
178
+ state: make_gdk_rgba(*[alpha + c * (1.0 - alpha) for c in colour])
179
+ for state, colour in self.fill_colors.items()
180
+ }
181
+
182
+ def do_realize(self):
183
+ self.set_events(
184
+ Gdk.EventMask.ENTER_NOTIFY_MASK |
185
+ Gdk.EventMask.LEAVE_NOTIFY_MASK |
186
+ Gdk.EventMask.POINTER_MOTION_MASK |
187
+ Gdk.EventMask.BUTTON_PRESS_MASK |
188
+ Gdk.EventMask.BUTTON_RELEASE_MASK |
189
+ Gdk.EventMask.SCROLL_MASK
190
+ )
191
+ self.connect('notify::action-mode', lambda *args: self.queue_draw())
192
+
193
+ meld_settings = get_meld_settings()
194
+ meld_settings.connect('changed', self.on_setting_changed)
195
+ self.on_setting_changed(meld_settings, 'style-scheme')
196
+
197
+ return Gtk.DrawingArea.do_realize(self)
198
+
199
+ def update_pointer_chunk(self, x, y):
200
+ # This is the simplest button/intersection implementation in
201
+ # the world, but it basically works for our purposes.
202
+ for button in self.buttons:
203
+ x1, y1, x2, y2, chunk = button
204
+
205
+ # Check y first; it's more likely to be out of range
206
+ if y1 <= y <= y2 and x1 <= x <= x2:
207
+ new_pointer_chunk = chunk
208
+ break
209
+ else:
210
+ new_pointer_chunk = None
211
+
212
+ if new_pointer_chunk != self.pointer_chunk:
213
+ self.pointer_chunk = new_pointer_chunk
214
+ self.queue_draw()
215
+
216
+ def motion_event(
217
+ self,
218
+ controller: Gtk.EventControllerMotion,
219
+ x: float | None = None,
220
+ y: float | None = None,
221
+ ):
222
+ if x is None or y is None:
223
+ # Missing coordinates are leave events
224
+ if self.pointer_chunk:
225
+ self.pointer_chunk = None
226
+ self.queue_draw()
227
+ else:
228
+ # This is either an enter or motion event; we treat them the same
229
+ self.update_pointer_chunk(x, y)
230
+
231
+ def do_button_press_event(self, event):
232
+ if self.pointer_chunk:
233
+ self.pressed_chunk = self.pointer_chunk
234
+
235
+ return Gtk.DrawingArea.do_button_press_event(self, event)
236
+
237
+ def do_button_release_event(self, event):
238
+ if self.pointer_chunk and self.pointer_chunk == self.pressed_chunk:
239
+ self.activate(self.pressed_chunk)
240
+ self.pressed_chunk = None
241
+
242
+ return Gtk.DrawingArea.do_button_press_event(self, event)
243
+
244
+ def _action_on_chunk(self, action: ChunkAction, chunk):
245
+ self.chunk_action_activated.emit(
246
+ action.value, self.source_view, self.target_view, chunk)
247
+
248
+ def activate(self, chunk):
249
+
250
+ action = self._classify_change_actions(chunk)
251
+
252
+ # FIXME: When fully transitioned to GAction, we should see
253
+ # whether we can do this by getting the container's action
254
+ # group and activating the actions directly instead.
255
+
256
+ if action == ActionMode.Replace:
257
+ self._action_on_chunk(ChunkAction.replace, chunk)
258
+ elif action == ActionMode.Delete:
259
+ self._action_on_chunk(ChunkAction.delete, chunk)
260
+ elif action == ActionMode.Insert:
261
+ copy_menu = self._make_copy_menu(chunk)
262
+ copy_menu.popup_at_pointer(None)
263
+
264
+ def _make_copy_menu(self, chunk):
265
+ copy_menu = Gtk.Menu()
266
+ copy_up = Gtk.MenuItem.new_with_mnemonic(_('Copy _up'))
267
+ copy_down = Gtk.MenuItem.new_with_mnemonic(_('Copy _down'))
268
+ copy_menu.append(copy_up)
269
+ copy_menu.append(copy_down)
270
+ copy_menu.show_all()
271
+
272
+ def copy_chunk(widget, action):
273
+ self._action_on_chunk(action, chunk)
274
+
275
+ copy_up.connect('activate', copy_chunk, ChunkAction.copy_up)
276
+ copy_down.connect('activate', copy_chunk, ChunkAction.copy_down)
277
+ return copy_menu
278
+
279
+ def get_chunk_range(self, start_y, end_y):
280
+ start_line = self.source_view.get_line_num_for_y(start_y)
281
+ end_line = self.source_view.get_line_num_for_y(end_y)
282
+
283
+ start_idx = bisect.bisect(self.chunk_starts, start_line)
284
+ end_idx = bisect.bisect(self.chunk_starts, end_line)
285
+
286
+ if start_idx > 0 and start_line <= self.chunks[start_idx - 1].end_a:
287
+ start_idx -= 1
288
+
289
+ return self.chunks[start_idx:end_idx]
290
+
291
+ def do_draw(self, context):
292
+ view = self.source_view
293
+ if not view or not view.get_realized():
294
+ return
295
+
296
+ self.buttons = []
297
+
298
+ width = self.get_allocated_width()
299
+ height = self.get_allocated_height()
300
+
301
+ style_context = self.get_style_context()
302
+ Gtk.render_background(style_context, context, 0, 0, width, height)
303
+
304
+ buf = view.get_buffer()
305
+
306
+ context.save()
307
+ context.set_line_width(1.0)
308
+
309
+ # Get our linked view's visible offset, get our vertical offset
310
+ # against our view (e.g., for info bars at the top of the view)
311
+ # and translate our context to match.
312
+ view_y_start = view.get_visible_rect().y
313
+ view_y_offset = view.translate_coordinates(self, 0, 0)[1]
314
+ gutter_y_translate = view_y_offset - view_y_start
315
+ context.translate(0, gutter_y_translate)
316
+
317
+ button_x = 1
318
+ button_width = width - 2
319
+
320
+ for chunk in self.get_chunk_range(view_y_start, view_y_start + height):
321
+
322
+ change_type, start_line, end_line, *_unused = chunk
323
+
324
+ rect_y = view.get_y_for_line_num(start_line)
325
+ rect_height = max(
326
+ 0, view.get_y_for_line_num(end_line) - rect_y - 1)
327
+
328
+ # Draw our rectangle outside x bounds, so we don't get
329
+ # vertical lines. Fill first, over-fill with a highlight
330
+ # if in the focused chunk, and then stroke the border.
331
+ context.rectangle(-0.5, rect_y + 0.5, width + 1, rect_height)
332
+ if start_line != end_line:
333
+ context.set_source_rgba(*self.fill_colors[change_type])
334
+ context.fill_preserve()
335
+ if view.current_chunk_check(chunk):
336
+ highlight = self.fill_colors['current-chunk-highlight']
337
+ context.set_source_rgba(*highlight)
338
+ context.fill_preserve()
339
+ context.set_source_rgba(*self.line_colors[change_type])
340
+ context.stroke()
341
+
342
+ # Button rendering and tracking
343
+ action = self._classify_change_actions(chunk)
344
+ if action is None:
345
+ continue
346
+
347
+ it = buf.get_iter_at_line(start_line)
348
+ button_y, button_height = view.get_line_yrange(it)
349
+ button_y += 1
350
+ button_height -= 2
351
+
352
+ button_style_context = get_style(None, 'button.flat.image-button')
353
+ if chunk == self.pointer_chunk:
354
+ button_style_context.set_state(Gtk.StateFlags.PRELIGHT)
355
+
356
+ Gtk.render_background(
357
+ button_style_context, context, button_x, button_y,
358
+ button_width, button_height)
359
+ Gtk.render_frame(
360
+ button_style_context, context, button_x, button_y,
361
+ button_width, button_height)
362
+
363
+ # TODO: Ideally we'd do this in a pre-render step of some
364
+ # kind, but I'm having trouble figuring out what that would
365
+ # look like.
366
+ self.buttons.append(
367
+ (
368
+ button_x,
369
+ button_y + gutter_y_translate,
370
+ button_x + button_width,
371
+ button_y + gutter_y_translate + button_height,
372
+ chunk,
373
+ )
374
+ )
375
+
376
+ pixbuf = self.action_map.get(action)
377
+ icon_x = button_x + (button_width - pixbuf.props.width) // 2
378
+ icon_y = button_y + (button_height - pixbuf.props.height) // 2
379
+ Gtk.render_icon(
380
+ button_style_context, context, pixbuf, icon_x, icon_y)
381
+
382
+ context.restore()
383
+
384
+ def _classify_change_actions(self, change) -> Optional[ActionMode]:
385
+ """Classify possible actions for the given change
386
+
387
+ Returns the action that can be performed given the content and
388
+ context of the change.
389
+ """
390
+ source_editable = self.source_view.get_editable()
391
+ target_editable = self.target_view.get_editable()
392
+
393
+ if not source_editable and not target_editable:
394
+ return None
395
+
396
+ # Reclassify conflict changes, since we treat them the same as a
397
+ # normal two-way change as far as actions are concerned
398
+ change_type = change[0]
399
+ if change_type == 'conflict':
400
+ if change[1] == change[2]:
401
+ change_type = 'insert'
402
+ elif change[3] == change[4]:
403
+ change_type = 'delete'
404
+ else:
405
+ change_type = 'replace'
406
+
407
+ if change_type == 'insert':
408
+ return None
409
+
410
+ action = self.action_mode
411
+ if action == ActionMode.Delete and not source_editable:
412
+ action = None
413
+ elif action == ActionMode.Insert and change_type == 'delete':
414
+ action = ActionMode.Replace
415
+ if not target_editable:
416
+ action = ActionMode.Delete
417
+ return action
418
+
419
+
420
+ ActionGutter.set_css_name('action-gutter')