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