1/*
2 * This file is part of gitg
3 *
4 * Copyright (C) 2015 - Jesse van den Kieboom
5 *
6 * gitg is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * gitg is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with gitg. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20enum Gitg.DiffSelectionMode {
21	NONE,
22	SELECTING,
23	DESELECTING
24}
25
26class Gitg.DiffViewFileSelectable : Object
27{
28	private string d_selection_category = "selection";
29	private Gtk.TextTag d_selection_tag;
30	private DiffSelectionMode d_selection_mode;
31	private Gtk.TextMark d_start_selection_mark;
32	private Gtk.TextMark d_end_selection_mark;
33	private Gee.HashMap<int, bool> d_originally_selected;
34	private Gdk.Cursor d_cursor_ptr;
35	private Gdk.Cursor d_cursor_hand;
36	private bool d_is_rubber_band;
37
38	public Gtk.SourceView source_view
39	{
40		get; construct set;
41	}
42
43	public bool has_selection
44	{
45		get; private set;
46	}
47
48	public int[] get_selected_lines()
49	{
50		var ret = new int[0];
51		Gtk.TextIter iter;
52
53		var buffer = source_view.buffer as Gtk.SourceBuffer;
54
55		buffer.get_start_iter(out iter);
56
57		while (buffer.forward_iter_to_source_mark(ref iter, d_selection_category))
58		{
59			ret += iter.get_line();
60		}
61
62		return ret;
63	}
64
65	public DiffViewFileSelectable(Gtk.SourceView source_view)
66	{
67		Object(source_view: source_view);
68	}
69
70	construct
71	{
72		source_view.button_press_event.connect(button_press_event_on_view);
73		source_view.motion_notify_event.connect(motion_notify_event_on_view);
74		source_view.leave_notify_event.connect(leave_notify_event_on_view);
75		source_view.enter_notify_event.connect(enter_notify_event_on_view);
76		source_view.button_release_event.connect(button_release_event_on_view);
77
78		source_view.realize.connect(() => {
79			update_cursor(cursor_ptr);
80		});
81
82		source_view.notify["state-flags"].connect(() => {
83			update_cursor(cursor_ptr);
84		});
85
86		d_selection_tag = source_view.buffer.create_tag("selection");
87
88		source_view.style_updated.connect(update_theme);
89		update_theme();
90
91		d_originally_selected = new Gee.HashMap<int, bool>();
92
93		Gtk.TextIter start;
94		source_view.buffer.get_start_iter(out start);
95
96		d_start_selection_mark = source_view.buffer.create_mark(null, start, false);
97		d_end_selection_mark = source_view.buffer.create_mark(null, start, false);
98	}
99
100	private Gdk.Cursor cursor_ptr
101	{
102		owned get
103		{
104			if (d_cursor_ptr == null)
105			{
106				d_cursor_ptr = new Gdk.Cursor.for_display(source_view.get_display(), Gdk.CursorType.LEFT_PTR);
107			}
108
109			return d_cursor_ptr;
110		}
111	}
112
113	private Gdk.Cursor cursor_hand
114	{
115		owned get
116		{
117			if (d_cursor_hand == null)
118			{
119				d_cursor_hand = new Gdk.Cursor.for_display(source_view.get_display(), Gdk.CursorType.HAND1);
120			}
121
122			return d_cursor_hand;
123		}
124	}
125
126	private void update_cursor(Gdk.Cursor cursor)
127	{
128		var window = source_view.get_window(Gtk.TextWindowType.TEXT);
129
130		if (window == null)
131		{
132			return;
133		}
134
135		window.set_cursor(cursor);
136	}
137
138	private void update_theme()
139	{
140		var selection_attributes = new Gtk.SourceMarkAttributes();
141		var context = source_view.get_style_context();
142
143		Gdk.RGBA theme_selected_bg_color, theme_selected_fg_color;
144
145		if (context.lookup_color("theme_selected_bg_color", out theme_selected_bg_color))
146		{
147			selection_attributes.background = theme_selected_bg_color;
148		}
149
150		if (context.lookup_color("theme_selected_fg_color", out theme_selected_fg_color))
151		{
152			d_selection_tag.foreground_rgba = theme_selected_fg_color;
153		}
154
155		source_view.set_mark_attributes(d_selection_category, selection_attributes, 0);
156	}
157
158	private bool get_line_selected(Gtk.TextIter iter)
159	{
160		Gtk.TextIter start = iter;
161
162		start.set_line_offset(0);
163
164		var buffer = source_view.buffer as Gtk.SourceBuffer;
165
166		return buffer.get_source_marks_at_iter(start, d_selection_category) != null;
167	}
168
169	private bool get_line_is_diff(Gtk.TextIter iter)
170	{
171		Gtk.TextIter start = iter;
172
173		start.set_line_offset(0);
174
175		var buffer = source_view.buffer as Gtk.SourceBuffer;
176
177		return (buffer.get_source_marks_at_iter(start, "added") != null) ||
178		       (buffer.get_source_marks_at_iter(start, "removed") != null);
179	}
180
181	private bool get_line_is_hunk(Gtk.TextIter iter)
182	{
183		Gtk.TextIter start = iter;
184
185		start.set_line_offset(0);
186
187		var buffer = source_view.buffer as Gtk.SourceBuffer;
188
189		return buffer.get_source_marks_at_iter(start, "header") != null;
190	}
191
192	private bool get_iter_from_pointer_position(out Gtk.TextIter iter)
193	{
194		var win = source_view.get_window(Gtk.TextWindowType.TEXT);
195
196		int x, y, width, height;
197
198		// To silence unassigned iter warning
199		var dummy_iter = Gtk.TextIter();
200		iter = dummy_iter;
201
202		width = win.get_width();
203		height = win.get_height();
204
205		var pointer = Gdk.Display.get_default().get_default_seat().get_pointer();
206		win.get_device_position(pointer, out x, out y, null);
207
208		if (x < 0 || y < 0 || x > width || y > height)
209		{
210			return false;
211		}
212
213		return get_iter_from_event_position(out iter, x, y);
214	}
215
216	private bool get_iter_from_event_position(out Gtk.TextIter iter, int x, int y)
217	{
218		int win_x, win_y;
219
220		source_view.window_to_buffer_coords(Gtk.TextWindowType.TEXT, x, y, out win_x, out win_y);
221		source_view.get_line_at_y(out iter, win_y, null);
222
223		return true;
224	}
225
226	private void update_selection_range(Gtk.TextIter start, Gtk.TextIter end, bool select)
227	{
228		var buffer = source_view.buffer as Gtk.SourceBuffer;
229
230		Gtk.TextIter real_start, real_end;
231
232		real_start = start;
233		real_end = end;
234
235		if (real_start.compare(real_end) > 0)
236		{
237			var tmp = real_end;
238
239			real_end = real_start;
240			real_start = tmp;
241		}
242
243		real_start.set_line_offset(0);
244
245		if (!real_end.ends_line())
246		{
247			real_end.forward_to_line_end();
248		}
249
250		var start_line = real_start.get_line();
251		var end_line = real_end.get_line();
252
253		var current = real_start;
254
255		while (start_line <= end_line)
256		{
257			if (get_line_is_diff(current))
258			{
259				if (!d_originally_selected.has_key(start_line))
260				{
261					d_originally_selected[start_line] = get_line_selected(current);
262				}
263
264				if (select)
265				{
266					buffer.create_source_mark(null, d_selection_category, current);
267
268					var line_end = current;
269
270					if (!line_end.ends_line())
271					{
272						line_end.forward_to_line_end();
273					}
274
275					buffer.apply_tag(d_selection_tag, current, line_end);
276				}
277			}
278
279			if (!current.forward_line())
280			{
281				break;
282			}
283
284			start_line++;
285		}
286
287		if (!select)
288		{
289			buffer.remove_source_marks(real_start, real_end, d_selection_category);
290			buffer.remove_tag(d_selection_tag, real_start, real_end);
291		}
292	}
293
294	private void clear_original_selection(Gtk.TextIter start, Gtk.TextIter end, bool include_end)
295	{
296		var current = start;
297		current.set_line_offset(0);
298
299		var end_line = end.get_line();
300		var current_line = current.get_line();
301
302		if (include_end)
303		{
304			end_line++;
305		}
306
307		while (current_line < end_line)
308		{
309			var originally_selected = d_originally_selected[current_line];
310
311			update_selection_range(current, current, originally_selected);
312
313			current.forward_line();
314			current_line++;
315		}
316	}
317
318	private void forward_to_hunk_end(ref Gtk.TextIter iter)
319	{
320		iter.forward_line();
321
322		var buffer = source_view.buffer as Gtk.SourceBuffer;
323
324		if (!buffer.forward_iter_to_source_mark(ref iter, "header"))
325		{
326			iter.forward_to_end();
327		}
328	}
329
330	private bool hunk_is_all_selected(Gtk.TextIter iter)
331	{
332		var start = iter;
333		start.forward_line();
334
335		var end = iter;
336		forward_to_hunk_end(ref end);
337
338		while (start.compare(end) <= 0)
339		{
340			if (get_line_is_diff(start) && !get_line_selected(start))
341			{
342				return false;
343			}
344
345			if (!start.forward_line())
346			{
347				break;
348			}
349		}
350
351		return true;
352	}
353
354	private void update_selection_hunk(Gtk.TextIter iter, bool select)
355	{
356		var end = iter;
357		forward_to_hunk_end(ref end);
358
359		update_selection_range(iter, end, select);
360	}
361
362	private bool button_press_event_on_view(Gdk.EventButton event)
363	{
364		if (event.button != 1)
365		{
366			return false;
367		}
368
369		Gtk.TextIter iter;
370
371		if (!get_iter_from_pointer_position(out iter))
372		{
373			return false;
374		}
375
376		var buffer = source_view.buffer;
377
378		if ((event.state & Gdk.ModifierType.SHIFT_MASK) != 0)
379		{
380			update_selection(iter);
381			return true;
382		}
383
384		if (get_line_is_hunk(iter))
385		{
386			update_selection_hunk(iter, !hunk_is_all_selected(iter));
387			return true;
388		}
389
390		d_is_rubber_band = true;
391
392		var select = !get_line_selected(iter);
393
394		if (select)
395		{
396			d_selection_mode = DiffSelectionMode.SELECTING;
397		}
398		else
399		{
400			d_selection_mode = DiffSelectionMode.DESELECTING;
401		}
402
403		d_originally_selected.clear();
404
405		buffer.move_mark(d_start_selection_mark, iter);
406		buffer.move_mark(d_end_selection_mark, iter);
407
408		update_selection(iter);
409
410		return true;
411	}
412
413	private void update_selection(Gtk.TextIter cursor)
414	{
415		var buffer = source_view.buffer;
416
417		Gtk.TextIter start, end;
418
419		buffer.get_iter_at_mark(out start, d_start_selection_mark);
420		buffer.get_iter_at_mark(out end, d_end_selection_mark);
421
422		// Clear to original selection
423		if (start.get_line() < end.get_line())
424		{
425			var next = start.get_line() < cursor.get_line() ? cursor : start;
426			next.forward_line();
427
428			clear_original_selection(next, end, true);
429		}
430		else
431		{
432			clear_original_selection(end, cursor, false);
433		}
434
435		update_selection_range(start, cursor, d_selection_mode == DiffSelectionMode.SELECTING);
436		buffer.move_mark(d_end_selection_mark, cursor);
437	}
438
439	private bool update_selection_event(Gdk.ModifierType state, int x, int y)
440	{
441		Gtk.TextIter iter;
442
443		if (!get_iter_from_event_position(out iter, x, y))
444		{
445			return false;
446		}
447
448		if (d_is_rubber_band || (get_line_is_diff(iter) || get_line_is_hunk(iter)))
449		{
450			update_cursor(cursor_hand);
451		}
452		else
453		{
454			update_cursor(cursor_ptr);
455		}
456
457		if (!d_is_rubber_band)
458		{
459			return false;
460		}
461
462		update_selection(iter);
463		return true;
464	}
465
466	private bool motion_notify_event_on_view(Gdk.EventMotion event)
467	{
468		return update_selection_event(event.state, (int)event.x, (int)event.y);
469	}
470
471	private bool leave_notify_event_on_view(Gdk.EventCrossing event)
472	{
473		return update_selection_event(event.state, (int)event.x, (int)event.y);
474	}
475
476	private bool enter_notify_event_on_view(Gdk.EventCrossing event)
477	{
478		return update_selection_event(event.state, (int)event.x, (int)event.y);
479	}
480
481	private void update_has_selection()
482	{
483		var buffer = source_view.buffer;
484
485		Gtk.TextIter iter;
486		buffer.get_start_iter(out iter);
487
488		bool something_selected = false;
489
490		if (get_line_selected(iter))
491		{
492			something_selected = true;
493		}
494		else
495		{
496			something_selected = (buffer as Gtk.SourceBuffer).forward_iter_to_source_mark(ref iter, d_selection_category);
497		}
498
499		if (something_selected != has_selection)
500		{
501			has_selection = something_selected;
502		}
503	}
504
505	private bool button_release_event_on_view(Gdk.EventButton event)
506	{
507		d_is_rubber_band = false;
508
509		if (event.button != 1)
510		{
511			return false;
512		}
513
514		update_has_selection();
515
516		return true;
517	}
518}
519
520// ex:ts=4 noet
521