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.
- meld/__init__.py +0 -0
- meld/accelerators.py +67 -0
- meld/actiongutter.py +420 -0
- meld/chunkmap.py +467 -0
- meld/conf.py +88 -0
- meld/const.py +43 -0
- meld/diffgrid.py +349 -0
- meld/dirdiff.py +2022 -0
- meld/externalhelpers.py +141 -0
- meld/filediff.py +2851 -0
- meld/filters.py +150 -0
- meld/fourdiff.py +536 -0
- meld/gutterrendererchunk.py +214 -0
- meld/imagediff.py +322 -0
- meld/iohelpers.py +273 -0
- meld/linkmap.py +140 -0
- meld/matchers/__init__.py +0 -0
- meld/matchers/diffutil.py +532 -0
- meld/matchers/helpers.py +117 -0
- meld/matchers/merge.py +302 -0
- meld/matchers/myers.py +445 -0
- meld/meldapp.py +422 -0
- meld/meldbuffer.py +358 -0
- meld/melddoc.py +168 -0
- meld/meldwindow.py +523 -0
- meld/menuhelpers.py +23 -0
- meld/misc.py +466 -0
- meld/newdifftab.py +193 -0
- meld/patchdialog.py +163 -0
- meld/preferences.py +380 -0
- meld/recent.py +247 -0
- meld/settings.py +149 -0
- meld/share/applications/org.gnome.Meld.desktop +171 -0
- meld/share/icons/hicolor/scalable/apps/org.gnome.Meld.svg +182 -0
- meld/share/icons/hicolor/symbolic/apps/org.gnome.Meld-symbolic.svg +3 -0
- meld/share/locale/ar/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/bg/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/bs/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/ca/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/ca@valencia/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/cs/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/da/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/de/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/dz/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/el/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/en_CA/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/en_GB/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/eo/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/es/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/eu/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/fa/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/fi/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/fr/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/gl/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/he/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/hi/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/hu/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/id/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/it/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/ja/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/ka/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/ko/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/nb/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/ne/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/nl/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/oc/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/pa/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/pl/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/pt/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/pt_BR/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/ro/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/ru/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/rw/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/sk/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/sl/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/sq/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/sr/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/sr@latin/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/sv/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/tr/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/uk/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/vi/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/zh_CN/LC_MESSAGES/meld.mo +0 -0
- meld/share/locale/zh_TW/LC_MESSAGES/meld.mo +0 -0
- meld/share/meld/COPYING +339 -0
- meld/share/meld/gschemas.compiled +0 -0
- meld/share/meld/org.gnome.Meld.gresource +0 -0
- meld/share/meld/styles/meld-base.style-scheme.xml +50 -0
- meld/share/meld/styles/meld-dark.style-scheme.xml +51 -0
- meld/share/metainfo/org.gnome.Meld.metainfo.xml +359 -0
- meld/share/mime/packages/org.gnome.Meld.xml +43 -0
- meld/sourceview.py +521 -0
- meld/style.py +129 -0
- meld/task.py +168 -0
- meld/tree.py +301 -0
- meld/treehelpers.py +132 -0
- meld/ui/__init__.py +0 -0
- meld/ui/bufferselectors.py +149 -0
- meld/ui/cellrenderers.py +128 -0
- meld/ui/emblemcellrenderer.py +127 -0
- meld/ui/filebutton.py +88 -0
- meld/ui/findbar.py +197 -0
- meld/ui/gladesupport.py +14 -0
- meld/ui/gtkcompat.py +145 -0
- meld/ui/gtkutil.py +25 -0
- meld/ui/historyentry.py +153 -0
- meld/ui/listwidget.py +69 -0
- meld/ui/msgarea.py +143 -0
- meld/ui/notebook.py +167 -0
- meld/ui/notebooklabel.py +59 -0
- meld/ui/pathlabel.py +247 -0
- meld/ui/recentselector.py +94 -0
- meld/ui/statusbar.py +297 -0
- meld/ui/util.py +97 -0
- meld/ui/vcdialogs.py +127 -0
- meld/undo.py +289 -0
- meld/vc/COPYING +21 -0
- meld/vc/README +4 -0
- meld/vc/__init__.py +50 -0
- meld/vc/_null.py +50 -0
- meld/vc/_vc.py +482 -0
- meld/vc/bzr.py +239 -0
- meld/vc/cvs.py +166 -0
- meld/vc/darcs.py +174 -0
- meld/vc/git.py +380 -0
- meld/vc/mercurial.py +116 -0
- meld/vc/svn.py +205 -0
- meld/vcview.py +972 -0
- meld/windowstate.py +76 -0
- meld_fourdiff-0.0.1.data/scripts/meld-fourdiff +467 -0
- meld_fourdiff-0.0.1.dist-info/COPYING +339 -0
- meld_fourdiff-0.0.1.dist-info/METADATA +451 -0
- meld_fourdiff-0.0.1.dist-info/RECORD +134 -0
- 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')
|