1/* 2 * This file is part of gitg 3 * 4 * Copyright (C) 2012-2016 - Ignacio Casal Quinteiro 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 20namespace Gitg 21{ 22 public enum SelectionMode 23 { 24 NORMAL, 25 SELECTION 26 } 27 28 public class RepositoryListBox : Gtk.ListBox 29 { 30 private string? d_filter_text; 31 32 public signal void repository_activated(Repository repository); 33 public signal void show_error(string primary_message, string secondary_message); 34 35 [GtkTemplate (ui = "/org/gnome/gitg/ui/gitg-repository-list-box-row.ui")] 36 public class Row : Gtk.ListBoxRow 37 { 38 private Repository? d_repository; 39 private DateTime d_time = new DateTime.now_local(); 40 private bool d_loading; 41 [GtkChild] 42 private ProgressBin d_progress_bin; 43 [GtkChild] 44 private Gtk.Label d_repository_label; 45 [GtkChild] 46 private Gtk.Label d_description_label; 47 [GtkChild] 48 private Gtk.Label d_branch_label; 49 [GtkChild] 50 private Gtk.Spinner d_spinner; 51 [GtkChild] 52 private Gtk.CheckButton d_remove_check_button; 53 [GtkChild] 54 private Gtk.Revealer d_remove_revealer; 55 [GtkChild] 56 private Gtk.Box d_languages_box; 57 58 public signal void request_remove(); 59 60 private SelectionMode d_mode; 61 private string? d_dirname; 62 private string? d_branch_name; 63 64 public SelectionMode mode 65 { 66 get { return d_mode; } 67 68 set 69 { 70 if (d_mode != value) 71 { 72 d_mode = value; 73 74 d_remove_revealer.reveal_child = (d_mode == SelectionMode.SELECTION); 75 76 d_remove_check_button.active = false; 77 } 78 } 79 } 80 81 public new bool selected 82 { 83 get; set; 84 } 85 86 construct 87 { 88 d_remove_check_button.bind_property("active", 89 this, 90 "selected", 91 BindingFlags.BIDIRECTIONAL | 92 BindingFlags.SYNC_CREATE); 93 } 94 95 public Repository? repository 96 { 97 get { return d_repository; } 98 set 99 { 100 d_repository = value; 101 update_repository_data(); 102 } 103 } 104 105 public bool can_remove 106 { 107 get { return d_remove_check_button.sensitive; } 108 set { d_remove_check_button.sensitive = value; } 109 } 110 111 public DateTime time 112 { 113 get { return d_time; } 114 set { d_time = value; } 115 } 116 117 public double fraction 118 { 119 set { d_progress_bin.fraction = value; } 120 } 121 122 public string? repository_name 123 { 124 get { return d_repository_label.get_text(); } 125 set { d_repository_label.label = value; } 126 } 127 128 public string? dirname 129 { 130 get { return d_dirname; } 131 set 132 { 133 d_dirname = value; 134 update_branch_label(); 135 } 136 } 137 138 public string? branch_name 139 { 140 get { return d_branch_name; } 141 set 142 { 143 d_branch_name = value; 144 update_branch_label(); 145 } 146 } 147 148 private void update_branch_label() 149 { 150 if (d_branch_name == null || d_branch_name == "") 151 { 152 // Translators: this is used to construct: "at <directory>", to indicate where the repository is at. 153 d_branch_label.label = _("at %s").printf(d_dirname); 154 } 155 else if (d_dirname == null || d_dirname == "") 156 { 157 d_branch_label.label = d_branch_name; 158 } 159 else 160 { 161 // Translators: this is used to construct: "<branch-name> at <directory>" 162 d_branch_label.label = _("%s at %s").printf(d_branch_name, d_dirname); 163 } 164 } 165 166 private void update_repository_data() 167 { 168 string head_name = ""; 169 string head_description = ""; 170 171 if (d_repository != null) 172 { 173 try 174 { 175 var head = d_repository.get_head(); 176 head_name = head.parsed_name.shortname; 177 178 var commit = (Ggit.Commit)head.lookup(); 179 var tree = commit.get_tree(); 180 181 Ggit.OId? entry_id = null; 182 183 for (var i = 0; i < tree.size(); i++) 184 { 185 var entry = tree.get(i); 186 var name = entry.get_name(); 187 188 if (name != null && name.has_suffix(".doap")) 189 { 190 entry_id = entry.get_id(); 191 break; 192 } 193 } 194 195 if (entry_id != null) 196 { 197 var blob = d_repository.lookup<Ggit.Blob>(entry_id); 198 199 unowned uint8[] content = blob.get_raw_content(); 200 var doap = new Ide.Doap(); 201 doap.load_from_data((string)content, -1); 202 203 head_description = doap.get_shortdesc(); 204 205 foreach (var lang in doap.get_languages()) 206 { 207 var frame = new Gtk.Frame(null); 208 frame.shadow_type = Gtk.ShadowType.NONE; 209 frame.get_style_context().add_class("language-frame"); 210 frame.show(); 211 212 var label = new Gtk.Label(lang); 213 var attr_list = new Pango.AttrList(); 214 attr_list.insert(Pango.attr_scale_new(Pango.Scale.SMALL)); 215 label.set_attributes(attr_list); 216 label.show(); 217 218 frame.add(label); 219 d_languages_box.add(frame); 220 } 221 } 222 } catch {} 223 } 224 225 repository_name = d_repository != null ? d_repository.name : ""; 226 227 d_description_label.label = head_description; 228 d_description_label.visible = head_description != ""; 229 230 branch_name = head_name; 231 } 232 233 public bool loading 234 { 235 get { return d_loading; } 236 set 237 { 238 d_loading = value; 239 240 if (!d_loading) 241 { 242 d_spinner.stop(); 243 d_spinner.hide(); 244 d_progress_bin.fraction = 0; 245 } 246 else 247 { 248 d_spinner.show(); 249 d_spinner.start(); 250 } 251 } 252 } 253 254 public Row(Repository? repository, string dirname) 255 { 256 Object(repository: repository, dirname: dirname); 257 } 258 } 259 260 public SelectionMode mode { get; set; } 261 262 public bool bookmarks_from_recent_files { get; set; default = true; } 263 264 private File? d_location; 265 private uint d_save_repository_bookmarks_id; 266 private BookmarkFile d_bookmark_file; 267 268 public File? location 269 { 270 get 271 { 272 return d_location; 273 } 274 275 set 276 { 277 if (d_save_repository_bookmarks_id != 0) 278 { 279 Source.remove(d_save_repository_bookmarks_id); 280 save_repository_bookmarks(); 281 } 282 283 d_location = value; 284 d_bookmark_file = new BookmarkFile(); 285 286 try 287 { 288 d_bookmark_file.load_from_file(value.get_path()); 289 } 290 catch (FileError e) 291 { 292 if (bookmarks_from_recent_files) 293 { 294 // First time create, copy over from recent file manager 295 copy_bookmarks_from_recent_files(); 296 } 297 } 298 catch (Error e) 299 { 300 stderr.printf(@"Failed to read repository bookmarks: $(e.message)\n"); 301 } 302 } 303 } 304 305 private void copy_bookmarks_from_recent_files() 306 { 307 var manager = Gtk.RecentManager.get_default(); 308 var items = manager.get_items(); 309 310 foreach (var item in items) 311 { 312 if (!item.has_group("gitg")) 313 { 314 continue; 315 } 316 317 var uri = item.get_uri(); 318 319 d_bookmark_file.set_mime_type(uri, item.get_mime_type()); 320 d_bookmark_file.set_groups(uri, item.get_groups()); 321 d_bookmark_file.set_visited(uri, (time_t)item.get_modified()); 322 323 var app_name = Environment.get_application_name(); 324 var app_exec = string.join(" ", Environment.get_prgname(), "%f"); 325 326 try { d_bookmark_file.set_app_info(uri, app_name, app_exec, 1, -1); } catch {} 327 } 328 329 save_repository_bookmarks_timeout(); 330 } 331 332 protected override bool button_press_event(Gdk.EventButton event) 333 { 334 Gdk.Event *ev = (Gdk.Event *)event; 335 336 if (ev->triggers_context_menu() && mode == SelectionMode.NORMAL) 337 { 338 mode = SelectionMode.SELECTION; 339 340 var row = get_row_at_y((int)event.y) as Row; 341 342 if (row != null) 343 { 344 row.selected = true; 345 } 346 347 return true; 348 } 349 350 return base.button_press_event(event); 351 } 352 353 protected override void row_activated(Gtk.ListBoxRow row) 354 { 355 var r = (Row)row; 356 357 if (mode == SelectionMode.SELECTION) 358 { 359 r.selected = !r.selected; 360 return; 361 } 362 363 if (r.repository != null) 364 { 365 repository_activated(r.repository); 366 } 367 } 368 369 construct 370 { 371 set_header_func(update_header); 372 set_filter_func(filter); 373 set_sort_func(compare_widgets); 374 show(); 375 376 set_selection_mode(Gtk.SelectionMode.NONE); 377 378 d_bookmark_file = new BookmarkFile(); 379 } 380 381 ~RepositoryListBox() 382 { 383 if (d_save_repository_bookmarks_id != 0) 384 { 385 Source.remove(d_save_repository_bookmarks_id); 386 save_repository_bookmarks(); 387 } 388 } 389 390 private void update_header(Gtk.ListBoxRow row, Gtk.ListBoxRow? before) 391 { 392 row.set_header(before != null ? new Gtk.Separator(Gtk.Orientation.HORIZONTAL) : null); 393 } 394 395 private string normalize(string s) 396 { 397 return s.normalize(-1, NormalizeMode.ALL).casefold(); 398 } 399 400 private bool filter(Gtk.ListBoxRow row) 401 { 402 return d_filter_text != null ? normalize(((Row)row).repository_name).contains(normalize(d_filter_text)) : true; 403 } 404 405 private int compare_widgets(Gtk.ListBoxRow a, Gtk.ListBoxRow b) 406 { 407 return ((Row)b).time.compare(((Row)a).time); 408 } 409 410 public void populate_bookmarks() 411 { 412 var uris = d_bookmark_file.get_uris(); 413 414 foreach (var uri in uris) 415 { 416 try { 417 if (!d_bookmark_file.has_group(uri, "gitg")) 418 { 419 continue; 420 } 421 } catch { continue; } 422 423 File repo_file = File.new_for_uri(uri); 424 Repository repo; 425 426 try 427 { 428 repo = new Repository(repo_file, null); 429 } 430 catch 431 { 432 try 433 { 434 d_bookmark_file.remove_item(uri); 435 } catch {} 436 437 continue; 438 } 439 440 DateTime? visited = null; 441 442 try 443 { 444 visited = new DateTime.from_unix_utc(d_bookmark_file.get_visited(uri)); 445 } catch {}; 446 447 add_repository(repo, visited); 448 } 449 } 450 451 private Row get_row_for_repository(Repository repository) 452 { 453 Row? row = null; 454 455 foreach (var child in get_children()) 456 { 457 var d = (Row)child; 458 459 if (d.repository.get_location().equal(repository.get_location())) 460 { 461 row = d; 462 break; 463 } 464 } 465 466 return row; 467 } 468 469 private bool save_repository_bookmarks() 470 { 471 d_save_repository_bookmarks_id = 0; 472 473 if (location == null) 474 { 475 return false; 476 } 477 478 try 479 { 480 var dir = location.get_parent(); 481 dir.make_directory_with_parents(null); 482 } catch {} 483 484 try 485 { 486 d_bookmark_file.to_file(location.get_path()); 487 } 488 catch (Error e) 489 { 490 stderr.printf(@"Failed to save repository bookmarks: $(e.message)\n"); 491 } 492 493 return false; 494 } 495 496 private void add_repository_to_bookmarks(string uri, DateTime? visited = null) 497 { 498 d_bookmark_file.set_mime_type(uri, "inode/directory"); 499 d_bookmark_file.set_groups(uri, new string[] { "gitg" }); 500 d_bookmark_file.set_visited(uri, visited == null ? -1 : (time_t)visited.to_unix()); 501 502 var app_name = Environment.get_application_name(); 503 var app_exec = string.join(" ", Environment.get_prgname(), "%f"); 504 505 try { d_bookmark_file.set_app_info(uri, app_name, app_exec, 1, -1); } catch {} 506 507 save_repository_bookmarks_timeout(); 508 } 509 510 private void save_repository_bookmarks_timeout() 511 { 512 if (d_save_repository_bookmarks_id != 0) 513 { 514 return; 515 } 516 517 d_save_repository_bookmarks_id = Timeout.add(300, save_repository_bookmarks); 518 } 519 520 public void end_cloning(Row row, Repository? repository) 521 { 522 if (repository != null) 523 { 524 File? workdir = repository.get_workdir(); 525 File? repo_file = repository.get_location(); 526 527 var uri = (workdir != null) ? workdir.get_uri() : repo_file.get_uri(); 528 add_repository_to_bookmarks(uri); 529 530 row.repository = repository; 531 row.loading = false; 532 533 connect_repository_row(row); 534 } 535 else 536 { 537 remove(row); 538 } 539 } 540 541 public Row? begin_cloning(File location) 542 { 543 var row = new Row(null, Utils.replace_home_dir_with_tilde(location.get_parent())); 544 row.repository_name = location.get_basename(); 545 row.branch_name = _("Cloning…"); 546 547 row.loading = true; 548 row.show(); 549 550 add(row); 551 return row; 552 } 553 554 private void connect_repository_row(Row row) 555 { 556 var repository = row.repository; 557 var workdir = repository.workdir != null ? repository.workdir : repository.location; 558 559 if (workdir != null) 560 { 561 bind_property("mode", row, "mode"); 562 563 row.notify["selected"].connect(() => { 564 notify_property("has-selection"); 565 }); 566 567 row.request_remove.connect(() => { 568 try 569 { 570 d_bookmark_file.remove_item(workdir.get_uri()); 571 } catch {} 572 573 remove(row); 574 }); 575 576 row.can_remove = true; 577 } 578 else 579 { 580 row.can_remove = false; 581 } 582 583 } 584 585 public Row? add_repository(Repository repository, DateTime? visited = null) 586 { 587 Row? row = get_row_for_repository(repository); 588 589 var f = repository.workdir != null ? repository.workdir : repository.location; 590 591 if (row == null) 592 { 593 var dirname = Utils.replace_home_dir_with_tilde((repository.workdir != null ? repository.workdir : repository.location).get_parent()); 594 row = new Row(repository, dirname); 595 row.show(); 596 597 connect_repository_row(row); 598 599 add(row); 600 } 601 602 row.time = visited != null ? visited : new DateTime.now_local(); 603 invalidate_sort(); 604 605 if (f != null) 606 { 607 add_repository_to_bookmarks(f.get_uri(), visited); 608 } 609 610 return row; 611 } 612 613 public Row[] get_selection() 614 { 615 var ret = new Row[0]; 616 617 foreach (var row in get_children()) 618 { 619 var r = (Row)row; 620 621 if (r.selected) 622 { 623 ret += r; 624 } 625 } 626 627 return ret; 628 } 629 630 public bool has_selection 631 { 632 get 633 { 634 foreach (var row in get_children()) 635 { 636 var r = (Row)row; 637 638 if (r.selected) 639 { 640 return true; 641 } 642 } 643 644 return false; 645 } 646 } 647 648 public void filter_text(string? text) 649 { 650 d_filter_text = text; 651 652 invalidate_filter(); 653 } 654 } 655} 656 657// ex:ts=4 noet 658