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