1 module dcc.gtkd.main;
2 
3 private import std.random;
4 
5 private import std.stdio;
6 private import std.string;
7 private import std.conv;
8 private import std.algorithm : filter, sort, find, uniq, map;
9 private import std.file : exists, copy;
10 private import std.process : environment;
11 private import std.array : array;
12 private import std.signals;
13 
14 private import core.memory : GC;
15 private import core.thread;
16 
17 private import gtk.Main;
18 private import gtk.Label;
19 private import gtk.MainWindow;
20 private import gtk.Menu;
21 private import gtk.MenuBar;
22 private import gtk.MenuItem;
23 private import gtk.AccelGroup;
24 private import gtk.Paned;
25 private import gtk.ScrolledWindow;
26 private import gtk.MessageDialog;
27 private import gtk.Box;
28 private import gtk.TextView;
29 private import gtk.TextIter;
30 private import gtk.TreeView;
31 private import gtk.TreeViewColumn;
32 private import gtk.TreeIter;
33 private import gtk.TreeSelection;
34 private import gtk.TreeModelIF;
35 private import gtk.TreePath;
36 private import gtk.CellRendererText;
37 private import gtk.ListStore;
38 private import gtk.Widget;
39 private import gtk.CellRenderer;
40 private import gtk.MountOperation;
41 private import gtk.Overlay;
42 
43 private import gdk.Keymap;
44 private import gdk.Event;
45 private import gdk.Color;
46 private import gdk.Cursor;
47 private import gdk.RGBA;
48 private import gdk.Threads;
49 
50 private import gtkc.gtk;
51 private import gtkc.gtktypes;
52 private import gtkc.glib;
53 private import gtkc.glibtypes;
54 
55 private import glib.ConstructionException;
56 private import glib.Str;
57 private import glib.Timeout;
58 private import glib.Idle;
59 
60 private import dcc.engine.conf;
61 private import dcc.engine.tribune;
62 private import dcc.gtkd.tribuneviewer;
63 private import dcc.gtkd.tribuneinput;
64 private import dcc.gtkd.post;
65 
66 
67 extern (C) { char* setlocale(int category, const char* locale); }
68 
69 void main(string[] args) {
70 	setlocale(0, "".toStringz());
71 
72 	string config_file = environment.get("HOME") ~ "/.dcoincoinrc";
73 
74 	if (!config_file.exists()) {
75 		foreach (string prefix; ["/usr", "/usr/local"]) {
76 			string rc = prefix ~ "/share/doc/dcoincoin/dcoincoinrc";
77 			if (rc.exists()) {
78 				try {
79 					rc.copy(config_file);
80 					stderr.writeln("Initialized ", config_file, " with ", rc, ".");
81 					break;
82 				}
83 				catch (Exception e) {
84 					// Nothing special to do here.
85 				}
86 			}
87 		}
88 	}
89 
90 	if (args.length == 2) {
91 		config_file = args[1];
92 	}
93 
94 	if (!config_file.exists()) {
95 		stderr.writeln("Configuration file ", config_file, " does not exist.");
96 		return;
97 	}
98 
99 	Main.init(args);
100 
101 	GtkUI ui = new GtkUI(config_file);
102 	ui.showAll();
103 
104 	if (ui.tribunes.length == 0) {
105 		stderr.writeln("You should try to configure at least one tribune!");
106 	} else {
107 		Main.run();
108 	}
109 }
110 
111 public class DCCIdle {
112 	void delegate() f;
113 	uint idleID;
114 
115 	static DCCIdle[] idles;
116 	
117 	this(void delegate() f) {
118 		this.f = f;
119 		idleID = g_idle_add(cast(GSourceFunc)&idleCallback, cast(void*)this);
120 		idles ~= this;
121 	}
122 	
123 	public void stop() {
124 		g_source_remove(idleID);
125 		this.f = null;
126 	}
127 	
128 	~this() {
129 		stop();
130 	}
131 	
132 	extern(C) static bool idleCallback(DCCIdle idle) {
133 		return idle.callAllListeners();
134 	}
135 	
136 	private bool callAllListeners() {
137 		this.f();
138 		return false;
139 	}
140 }
141 
142 public class DCCTimeout {
143 	void delegate() f;
144 	uint timeoutID;
145 	
146 	this(uint timeout, void delegate() f) {
147 		this.f = f;
148 		timeoutID = g_timeout_add(timeout, cast(GSourceFunc)&timeoutCallback, cast(void*)this);
149 		//GC.removeRange(this);
150 	}
151 	
152 	public void stop() {
153 		g_source_remove(timeoutID);
154 		this.f = null;
155 	}
156 	
157 	~this() {
158 		stop();
159 	}
160 	
161 	extern(C) static bool timeoutCallback(DCCTimeout timeout) {
162 		return timeout.callAllListeners();
163 	}
164 	
165 	private bool callAllListeners() {
166 		this.f();
167 		return true;
168 	}
169 }
170 
171 class GtkUI : MainWindow {
172 	string config_file;
173 	Config config;
174 	GtkTribune[string] tribunes;
175 	ulong active = 0;
176 	string[] tribune_names;
177 	GtkTribune currentTribune;
178 
179 	TribuneMainViewer viewer;
180 	TribuneInput input;
181 	ScrolledWindow inputScroll;
182 	TreeView tribunesList;
183 	ListStore tribunesListStore;
184 	Overlay overlay;
185 	TribunePreviewer preview;
186 
187 	DCCTimeout renderTimeout, reloadTimeout;
188 
189 	GtkPost latestPost;
190 
191 	this(string config_file) {
192 		super("DCoinCoin");
193 
194 		this.config_file = config_file;
195 
196 		this.config = new Config(this.config_file);
197 
198 		foreach (Tribune tribune ; this.config.tribunes) {
199 			GtkTribune gtkTribune = new GtkTribune(this, tribune);
200 			gtkTribune.newPost.connect(&addPost);
201 			this.tribunes[tribune.name] = gtkTribune;
202 			this.tribune_names ~= tribune.name;
203 			this.currentTribune = gtkTribune;
204 		}
205 
206 		this.setup();
207 
208 		foreach (GtkTribune tribune ; this.tribunes) {
209 			this.reloadRemaining[tribune] = 0;
210 		}
211 
212 		this.setCurrentTribune(this.tribunes.values[$-1]);
213 		this.reloadRemaining[this.tribunes.values[$-1]] = 0;
214 
215 		this.reloadTimeout = new DCCTimeout(100, {
216 			this.reloadTick();
217 		});
218 
219 		this.renderTimeout = new DCCTimeout(100, {
220 			this.renderPostsQueue();
221 		});
222 	}
223 
224 	int[GtkTribune] reloadRemaining;
225 
226 	void reloadTick() {
227 		foreach (GtkTribune tribune ; this.tribunes) {
228 			this.reloadRemaining[tribune]--;
229 
230 			if (this.reloadRemaining[tribune] <= 0 && !tribune.updating) {
231 				new Thread({
232 					tribune.fetch_posts({
233 						this.reloadRemaining[tribune] = 100;
234 					});
235 				}).start();
236 			}
237 		}
238 	}
239 
240 	override void showAll() {
241 		super.showAll();
242 		this.inputScroll.setSizeRequest(0, this.input.lineHeight() * 3);
243 	}
244 
245 	void setup() {
246 		this.viewer = this.makeTribuneMainViewer();
247 		this.preview = this.makeTribunePreviewer();
248 
249 		Box mainBox = new Box(GtkOrientation.VERTICAL, 0);
250 		mainBox.packStart(makeMenuBar(), false, false, 0);
251 
252 		Paned paned = new Paned(GtkOrientation.HORIZONTAL);
253 
254 		paned.add1(this.makeTribunesList());
255 		ScrolledWindow scrolledWindow = new ScrolledWindow(this.viewer);
256 		scrolledWindow.setPolicy(GtkPolicyType.NEVER, GtkPolicyType.ALWAYS);
257 
258 		this.overlay = new Overlay();
259 		this.overlay.add(scrolledWindow);
260 		this.overlay.addOverlay(this.preview);
261 
262 		paned.add2(this.overlay);
263 
264 		mainBox.packStart(paned, true, true, 0);
265 
266 		this.input = makeTribuneInput();
267 		this.inputScroll = new ScrolledWindow(this.input);
268 		this.inputScroll.setPolicy(GtkPolicyType.NEVER, GtkPolicyType.ALWAYS);
269 		mainBox.packStart(this.inputScroll, false, false, 0);
270 
271 		this.add(mainBox);
272 	}
273 
274 	void post(string text, void delegate(bool) success) {
275 		bool result = this.currentTribune.tribune.post(text);
276 		success(result);
277 		this.currentTribune.fetch_posts();
278 	}
279 
280 	TribuneInput makeTribuneInput() {
281 		TribuneInput input = new TribuneInput();
282 
283 		input.addOnKeyPress((Event event, Widget source) {
284 			GdkEventKey* key = event.key();
285 
286 			if (Keymap.keyvalName(key.keyval) == "Return") {
287 				string text = input.getBuffer().getText();
288 				input.setProperty("editable", false);
289 				auto post_comment = {
290 					this.post(text, (bool success) {
291 						if (success) {
292 							new DCCIdle({
293 								input.getBuffer().setText("");
294 								input.setProperty("editable", true);
295 							});
296 						} else {
297 							new DCCIdle({
298 								input.setProperty("editable", true);
299 							});
300 						}
301 					});
302 				};
303 				new Thread(post_comment).start();
304 				return true;
305 			}
306 
307 			return false;
308 		});
309 
310 		return input;
311 	}
312 
313 	void updatePost(GtkPost post) {
314 		post.referencedPosts = this.findReferencedPosts(post);
315 
316 		foreach (GtkPost referencedPost ; post.referencedPosts) {
317 			referencedPost.referencingPosts[post] = post;
318 		}
319 
320 		post.checkIfAnswer();
321 
322 		/*
323 		if (this.latestPost && post.post.real_time < (this.latestPost.post.real_time - this.latestPost.post.tribune.last_update)) {
324 			post.post.tribune.unreliable_date = true;
325 			post.post.tribune.time_offset = this.latestPost.post.real_time - this.latestPost.post.tribune.last_update - post.post.time;
326 			post.post.real_time = this.latestPost.post.real_time + 1.msecs;
327 		}
328 		*/
329 
330 		this.latestPost = post;
331 	}
332 
333 	void addPost(GtkPost post) {
334 		this.updatePost(post);
335 		synchronized(this.renderTimeout) {
336 			postsQueue ~= post;
337 		}
338 	}
339 
340 	GtkPost[] postsQueue;
341 
342 	void renderPostsQueue() {
343 		if (this.postsQueue.length > 0) {
344 			bool scroll = viewer.isScrolledDown();
345 			synchronized(this.renderTimeout) {
346 				foreach (GtkPost post; this.postsQueue) {
347 					this.viewer.renderPost(post);
348 				}
349 				this.postsQueue = typeof(this.postsQueue).init;
350 			}
351 			if (scroll) {
352 				viewer.scrollToEnd();
353 			}
354 		}
355 	}
356 
357 	GtkPostSegment[] findReferencesToPost(GtkPost post) {
358 		GtkPostSegment[] segments;
359 
360 		foreach (GtkTribune tribune ; this.tribunes) {
361 			segments ~= tribune.findReferencesToPost(post);
362 		}
363 
364 		return segments;
365 	}
366 
367 	GtkPost[] findReferencedPosts(GtkPost origin) {
368 		GtkPost[] posts;
369 
370 		foreach (GtkTribune tribune ; this.tribunes) {
371 			foreach (GtkPostSegment segment ; origin.segments) {
372 				if (tribune.tribune.matches_name(segment.context.clock.tribune)) {
373 					posts ~= tribune.findPostsByClock(segment);
374 				}
375 			}
376 		}
377 
378 		return posts.uniq.array;
379 	}
380 
381 	Box makeTribunesList() {
382 		ListStore listStore = new ListStore([GType.STRING, GType.STRING, GType.STRING, GType.STRING, GType.INT]);
383 		this.tribunesListStore = listStore;
384 
385 		TreeIter iterTop = listStore.createIter();
386 		
387 		// Please don't be too smart, you're making it difficult to use loops
388 		listStore.remove(iterTop);
389 
390 		foreach (GtkTribune tribune; this.tribunes) {
391 			listStore.append(iterTop);
392 			listStore.set(iterTop, [0, 1, 2, 3], [tribune.tribune.name, tribune.color, "black", tribune.color]);
393 			listStore.setValue(iterTop, 4, 400);
394 
395 			tribune.listStore = listStore;
396 			tribune.iter = iterTop.copy(iterTop);
397 		}
398 
399 		TreeView tribunesList = new TreeView(listStore);
400 		tribunesList.setHeadersVisible(false);
401 		tribunesList.setRulesHint(false);
402 		tribunesList.setActivateOnSingleClick(false);
403 
404 		this.tribunesList = tribunesList;
405 
406 		TreeSelection ts = tribunesList.getSelection();
407 		ts.setMode(SelectionMode.NONE);
408 
409 		tribunesList.addOnButtonPress((Event event, Widget widget) {
410 			switch (event.button.button) {
411 				case 1:
412 					return false;
413 				case 2:
414 					TreeModelIF treeModel = tribunesList.getModel();
415 					TreePath currentPath;
416 					TreeViewColumn currentColumn;
417 					int cellX, cellY;
418 					tribunesList.getPathAtPos(cast(int)event.button.x, cast(int)event.button.y, currentPath, currentColumn, cellX, cellY);
419 
420 					TreeIter iter = new TreeIter();
421 					iter = new TreeIter();
422 					treeModel.getIter(iter, currentPath);
423 
424 					string name = iter.getValueString(0);
425 					if (name in this.tribunes) {
426 						//this.tribunes[name].forceReload();
427 					}
428 
429 					return true;
430 				default:
431 					return false;
432 			}
433 		});
434 
435 		tribunesList.addOnCursorChanged((TreeView treeView) {
436 			TreeModelIF treeModel = tribunesList.getModel();
437 			TreePath currentPath;
438 			TreeViewColumn currentColumn;
439 			tribunesList.getCursor(currentPath, currentColumn);
440 
441 			TreeIter iter = new TreeIter();
442 			iter = new TreeIter();
443 			treeModel.getIter(iter, currentPath);
444 
445 			string name = iter.getValueString(0);
446 			if (name in this.tribunes) {
447 				this.setCurrentTribune(this.tribunes[name]);
448 			}
449 		});
450 
451 		TreeViewColumn column = new TribuneTreeViewColumn("Tribune", new CellRendererText());
452 		tribunesList.appendColumn(column);
453 		column.setResizable(false);
454 		column.setExpand(false);
455 		column.setSizing(GtkTreeViewColumnSizing.FIXED);
456 		column.setFixedWidth(60);
457 
458 		TreeViewColumn column2 = new TribuneEnabledTreeViewColumn("Enabled", new CellRendererText());
459 		tribunesList.appendColumn(column2);
460 		column.setResizable(false);
461 
462 		Box box = new Box(GtkOrientation.VERTICAL, 0);
463 		box.packStart(new TreeView(new ListStore([GType.STRING])), 1, 1, 0);
464 		box.packStart(tribunesList, 0, 0, 0);
465 
466 		return box;
467 	}
468 
469 	void setCurrentTribune(GtkTribune tribune) {
470 		this.currentTribune = tribune;
471 
472 		TreeModelIF treeModel = this.tribunesList.getModel();
473 		TreePath currentPath;
474 		TreeViewColumn currentColumn;
475 		this.tribunesList.getCursor(currentPath, currentColumn);
476 
477 		TreeIter iter = new TreeIter();
478 		treeModel.getIterFirst(iter);
479 		do {
480 			string name = this.tribunesListStore.getValue(iter, 0).getString();
481 
482 			if (name == this.currentTribune.tribune.name) {
483 				this.tribunesListStore.setValue(iter, 4, 1000);
484 			} else {
485 				this.tribunesListStore.setValue(iter, 4, 400);
486 			}
487 		} while (treeModel.iterNext(iter));
488 
489 		this.input.setCurrentTribune(tribune);
490 	}
491 
492 	TribuneMainViewer makeTribuneMainViewer() {
493 		TribuneMainViewer viewer = new TribuneMainViewer();
494 
495 		foreach (GtkTribune gtkTribune; this.tribunes) {
496 			viewer.registerTribune(gtkTribune);
497 		}
498 
499 		viewer.postClockHover.connect(&onPostClockHover);
500 		viewer.postSegmentHover.connect(&onPostSegmentHover);
501 
502 		viewer.postClockClick.connect(&onPostClockClick);
503 		viewer.postLoginClick.connect(&onPostLoginClick);
504 		viewer.postSegmentClick.connect(&onPostSegmentClick);
505 
506 		viewer.postHighlight.connect(&onPostHighlight);
507 		viewer.resetHighlight.connect(&onResetHighlight);
508 
509 		viewer.tribunes = this.tribunes.values;
510 
511 		return viewer;
512 	}
513 
514 	TribunePreviewer makeTribunePreviewer() {
515 		auto preview = new TribunePreviewer();
516 
517 		foreach (GtkTribune gtkTribune; this.tribunes) {
518 			preview.registerTribune(gtkTribune);
519 		}
520 
521 		preview.tribunes = this.tribunes.values;
522 
523 		return preview;
524 	}
525 
526 	void onPostClockHover(GtkPost post) {
527 		this.preview.postInfo(post);
528 		this.preview.show();
529 	}
530 
531 	void onPostSegmentHover(GtkPost post, GtkPostSegment segment) {
532 		if (segment.context.link) {
533 			this.preview.showUrl(segment.context.link_target);
534 			this.preview.show();
535 		}
536 	}
537 
538 	void onPostClockClick(GtkPost post) {
539 		writeln("Clicked on clock: ", post.post.clock);
540 		this.setCurrentTribune(post.tribune);
541 		this.input.insertText(post.post.clock ~ " ");
542 		this.input.grabFocus();
543 	}
544 
545 	void onPostLoginClick(GtkPost post) {
546 		writeln("Clicked on login: ", post.post.timestamp);
547 		this.setCurrentTribune(post.tribune);
548 		this.input.insertText(post.post.login ~ "< ");
549 		this.input.grabFocus();
550 	}
551 
552 	void onPostSegmentClick(GtkPost post, GtkPostSegment segment) {
553 		writeln("Clicked on segment: ", segment.text);
554 		if (segment.context.link) {
555 			writeln("Url is ", segment.context.link_target);
556 			MountOperation.showUri(null, segment.context.link_target, 0);
557 		}
558 	}
559 
560 	void onPostHighlight(GtkPost post) {
561 		this.preview.renderPost(post);
562 		this.preview.show();
563 	}
564 
565 	void onResetHighlight() {
566 		this.preview.hide();
567 	}
568 
569 	MenuBar makeMenuBar() {
570 		AccelGroup accelGroup = new AccelGroup();
571 		this.addAccelGroup(accelGroup);
572 
573 		MenuBar menuBar = new MenuBar();
574 		Menu menu = menuBar.append("_Tribunes");
575 		menu.append(new MenuItem(&onMenuActivate, "_Settings", "tribunes.settings", true, accelGroup, 's'));
576 		menu.append(new MenuItem(&onMenuActivate, "E_xit", "application.exit", true, accelGroup, 'q'));
577 
578 		menu = menuBar.append("_Help");
579 		menu.append(new MenuItem(&onMenuActivate, "_About", "help.about", true, accelGroup, 'a', GdkModifierType.CONTROL_MASK | GdkModifierType.SHIFT_MASK));
580 
581 		return menuBar;
582 	}
583 
584 	void onMenuActivate(MenuItem menuItem) {
585 		string action = menuItem.getActionName();
586 		switch (action) {
587 			default:
588 				MessageDialog d = new MessageDialog(
589 					this,
590 					GtkDialogFlags.MODAL,
591 					MessageType.INFO,
592 					ButtonsType.OK,
593 					"You pressed menu item "~action);
594 				d.run();
595 				d.destroy();
596 
597 				GC.collect();
598 				stderr.writeln("Free: ", GC.stats.freeSize, " - used: ", GC.stats.usedSize);
599 
600 			break;
601 		}
602 
603 	}
604 }
605 
606 class GtkTribuneColor {
607 	string desc;
608 
609 	this(string desc) {
610 		this.desc = desc;
611 	}
612 
613 	RGBA toRGBA() {
614 		return new RGBA(1, 0, 0, 1);
615 	}
616 }
617 
618 class TribuneTreeViewColumn : TreeViewColumn {
619 	this(string header, CellRenderer renderer) {
620 		auto p = gtk_tree_view_column_new_with_attributes(
621 		Str.toStringz(header),
622 		renderer.getCellRendererStruct(),
623 		Str.toStringz("text"),
624 		0,
625 		Str.toStringz("cell-background"),
626 		1,
627 		Str.toStringz("foreground"),
628 		2,
629 		Str.toStringz("weight"),
630 		4,
631 		null);
632 
633 		renderer.setProperty("size-points", 8);
634 		
635 		if(p is null)
636 		{
637 			throw new ConstructionException("null returned by gtk_tree_view_column_new_with_attributes");
638 		}
639 
640 		super(p);
641 	}
642 }
643 
644 class TribuneEnabledTreeViewColumn : TreeViewColumn {
645 	this(string header, CellRenderer renderer) {
646 		auto p = gtk_tree_view_column_new_with_attributes(
647 		Str.toStringz(header),
648 		renderer.getCellRendererStruct(),
649 		Str.toStringz("cell-background"),
650 		3,
651 		null);
652 		
653 		if(p is null)
654 		{
655 			throw new ConstructionException("null returned by gtk_tree_view_column_new_with_attributes");
656 		}
657 
658 		super(p);
659 	}
660 }
661 
662 class GtkTribune {
663 	Tribune tribune;
664 	GtkUI ui;
665 	GtkPost[] posts;
666 	string tag;
667 
668 	string color;
669 
670 	bool updating;
671 
672 	ListStore listStore;
673 	TreeIter iter;
674 
675 	mixin Signal!(GtkPost) newPost;
676 
677 	this(GtkUI ui, Tribune tribune) {
678 		this.ui = ui;
679 		this.tribune = tribune;
680 
681 		this.tribune.new_post.connect(&this.newPostHandler);
682 
683 		this.color = tribune.color;
684 	}
685 
686 	void newPostHandler(Post post) {
687 		GtkPost p = new GtkPost(this, post);
688 		this.posts ~= p;
689 
690 		this.newPost.emit(p);
691 	};
692 
693 	GtkPost[] findPostsByClock(GtkPostSegment segment) {
694 		GtkPost[] posts;
695 
696 		if (!this.tribune.matches_name(segment.context.clock.tribune)) {
697 			return posts;
698 		}
699 
700 		foreach (GtkPost post ; this.posts) {
701 			if (post.post.matches_clock(segment.context.clock)) {
702 				posts ~= post;
703 			}
704 		}
705 
706 		return posts;
707 	}
708 
709 	GtkPostSegment[] findReferencesToPost(GtkPost post) {
710 		GtkPostSegment[] segments;
711 
712 		foreach (GtkPost tested_post ; this.posts) {
713 			foreach (GtkPostSegment segment ; tested_post.segments) {
714 				if (segment.context.clock != Clock.init && post.post.matches_clock(segment.context.clock)) {
715 					segments ~= segment;
716 				}
717 			}
718 		}
719 
720 		return segments;
721 	}
722 
723 	void fetch_posts() {
724 		this.fetch_posts(null);
725 	}
726 
727 	void fetch_posts(void delegate() callback = null) {
728 		this.updating = true;
729 		stderr.writeln("Updating ", this.tribune.name);
730 		try {
731 			this.tribune.fetch_posts();
732 			stderr.writeln("Fetched ", this.tribune.name);
733 		} catch (Exception e) {
734 			stderr.writeln("Not fetched ", this.tribune.name);
735 		}
736 		this.updating = false;
737 		if (callback) {
738 			callback();
739 		}
740 	}
741 }
742