1 module dcc.gtkd.tribuneviewer;
2 
3 private import gtkc.gtk;
4 
5 private import std.stdio;
6 private import std.signals;
7 private import std.conv;
8 private import std.string : format;
9 private import std.algorithm;
10 private import std.datetime : SysTime;
11 
12 private import gtk.TextView;
13 private import gtk.TextBuffer;
14 private import gtk.TextIter;
15 private import gtk.TextMark;
16 private import gtk.Widget;
17 private import gtk.Window;
18 private import gtk.CssProvider;
19 
20 private import gtkc.gtktypes;
21 private import gtkc.gdk;
22 
23 private import gdk.Color;
24 private import gdk.Cursor;
25 private import gdk.Event;
26 private import gdk.Cairo;
27 private import gdk.Rectangle;
28 
29 private import glib.ListSG;
30 
31 private import cairo.Context;
32 
33 private import gobject.Signals;
34 
35 private import dcc.engine.tribune;
36 private import dcc.gtkd.post;
37 private import dcc.gtkd.main;
38 
39 class TribunePreviewer : TribuneViewer {
40 	this() {
41 		super();
42 
43 		this.getBuffer().createTag("status", "paragraph-background", "lightgrey");
44 		this.getBuffer().createTag("light", "foreground", "grey", "weight", PangoWeight.NORMAL);
45 
46 		this.setValign(GtkAlign.START);
47 		this.setBorderWindowSize(GtkTextWindowType.BOTTOM, 2);
48 		this.setBorderWindowSize(GtkTextWindowType.LEFT, 2);
49 		this.setBorderWindowSize(GtkTextWindowType.RIGHT, 2);
50 
51 		this.setIndent(-12);
52 
53 		this.hide();
54 
55 		this.setName("TribunePreview");
56 		auto css = new CssProvider();
57 		this.getStyleContext().addProvider(css, 600);
58 		css.loadFromData(`
59 			#TribunePreview {
60 				background-color: #EEEEEE;
61 			}
62 		`);
63 
64 		Signals.connectData(
65 			this,
66 			"draw",
67 			null,
68 			cast(void*)this,
69 			null,
70 			cast(ConnectFlags)0);
71 	}
72 
73 	private void reset(T)(T p) {
74 		p = T.init;
75 	}
76 
77 	void empty() {
78 		this.reset(this.posts);
79 		this.reset(this.postBegins);
80 		this.reset(this.postEnds);
81 		this.reset(this.segmentBegins);
82 		this.reset(this.segmentEnds);
83 		this.reset(this.postSegmentsOffsets);
84 		this.getBuffer().setText("");
85 	}
86 
87 	override void renderPost(GtkPost post) {
88 		this.empty();
89 
90 		this.setLeftMargin(0);
91 		this.setRightMargin(0);
92 
93 		super.renderPost(post);
94 	}
95 
96 	void showUrl(string url) {
97 		this.empty();
98 
99 		this.setLeftMargin(5);
100 		this.setRightMargin(5);
101 
102 		TextIter iter = new TextIter();
103 		auto buffer = this.getBuffer();
104 		buffer.getStartIter(iter);
105 
106 		buffer.insert(iter, url);
107 	}
108 
109 	void postInfo(GtkPost post) {
110 		this.empty();
111 
112 		this.setLeftMargin(5);
113 		this.setRightMargin(5);
114 
115 		TextIter iter = new TextIter();
116 		auto buffer = this.getBuffer();
117 		buffer.getStartIter(iter);
118 
119 		buffer.insertWithTagsByName(iter, "#", ["light"]);
120 		buffer.insertWithTagsByName(iter, format("%s", post.post.post_id), ["b"]);
121 
122 		buffer.insertWithTagsByName(iter, "   @", ["light"]);
123 		buffer.insertWithTagsByName(iter, format("%s", post.post.tribune.name), ["b"]);
124 
125 		buffer.insertWithTagsByName(iter, "   " ~ post.post.unicodeClock, ["light"]);
126 		buffer.insertWithTagsByName(iter,
127 			format("%04d-%02d-%02d %02d:%02d:%02d",
128 				post.post.time.year,
129 				post.post.time.month,
130 				post.post.time.day,
131 				post.post.time.hour,
132 				post.post.time.minute,
133 				post.post.time.second)
134 			, ["b"]);
135 
136 		buffer.insert(iter, "\n");
137 
138 		buffer.insert(iter, "User-Agent:");
139 		buffer.insertWithTagsByName(iter, format(" %s", post.post.info), ["i"]);
140 
141 		buffer.insert(iter, "\n");
142 		if (post.post.login.length > 0) {
143 			buffer.insert(iter, "Login: ");
144 			buffer.insertWithTagsByName(iter, format("%s", post.post.login), ["b"]);
145 		} else {
146 			buffer.insertWithTagsByName(iter, "(anonymous)", ["i"]);
147 		}
148 	}
149 
150 	override void registerTribune(GtkTribune gtkTribune) {}
151 }
152 
153 class TribuneMainViewer : TribuneViewer {
154 	private GtkPost[string] highlightedPosts;
155 	private GtkPostSegment[GtkPostSegment] highlightedPostSegments;
156 
157 	mixin Signal!(GtkPost) postClockClick;
158 	mixin Signal!(GtkPost) postLoginClick;
159 	mixin Signal!(GtkPost, GtkPostSegment) postSegmentClick;
160 
161 	mixin Signal!(GtkPost) postHover;
162 	mixin Signal!(GtkPost) postClockHover;
163 	mixin Signal!(GtkPost) postLoginHover;
164 	mixin Signal!(GtkPost, GtkPostSegment) postSegmentHover;
165 
166 	mixin Signal!(GtkPost) postHighlight;
167 	mixin Signal!() resetHighlight;
168 
169 	bool onDraw() {
170 		// Draw coloured lines in the left margin to indicate post ownership
171 		auto window = this.getWindow(GtkTextWindowType.WIDGET);
172 
173 		//auto context = this.getWindow(GtkTextWindowType.WIDGET).createContext();
174 
175 		auto p = gdk_cairo_create(window.getWindowStruct());
176 		auto context = new Context(p);
177 
178 		context.setLineWidth(2);
179 
180 		//writeln("Path: ", this.getPath());
181 
182 
183 		GdkRectangle visible;
184 		this.getVisibleRect(visible);
185 
186 		TextIter startIter = new TextIter();
187 		TextIter stopIter = new TextIter();
188 		GdkRectangle startLocation, stopLocation;
189 		int startY, endY, lineHeight;
190 		TextBuffer buffer = this.getBuffer();
191 
192 		auto scrollHeight = this.getVadjustment().getValue();
193 
194 		int i = 0;
195 		foreach (string post_id, GtkPost post; this.posts) {
196 			buffer.getIterAtMark(startIter, this.postBegins[post]);
197 
198 			this.getLineYrange(startIter, startY, lineHeight);
199 
200 			if (startY < visible.y || startY > visible.y + visible.height) {
201 				continue;
202 			}
203 
204 			if (post.post.mine) {
205 				context.setSourceRgb(0.5, 0.2, 0.2);
206 			} else if (post.answer) {
207 				context.setSourceRgb(1, 0.2, 0.2);
208 			} else {
209 				if (post.tribune !in this.tribuneColors) {
210 					Color color = new Color();
211 					Color.parse(post.tribune.color, color);
212 					this.tribuneColors[post.tribune] = color;
213 				}
214 
215 				context.setSourceRgb(
216 					cast(double)this.tribuneColors[post.tribune].red/ushort.max,
217 					cast(double)this.tribuneColors[post.tribune].green/ushort.max,
218 					cast(double)this.tribuneColors[post.tribune].blue/ushort.max,
219 				);
220 			}
221 
222 			buffer.getIterAtMark(stopIter, this.postEnds[post]);
223 			this.getLineYrange(stopIter, endY, lineHeight);
224 
225 			context.moveTo(1, startY - scrollHeight);
226 			context.lineTo(1, endY + lineHeight - scrollHeight);
227 			context.stroke();
228 		}
229 
230 		delete context;
231 	
232 		return false;
233 	}
234 
235 	extern(C) static bool onDrawCallback(GtkWidget* widgetStruct, cairo_t* cr, Widget _widget) {
236 		return (cast(TribuneMainViewer)_widget).onDraw();
237 	}
238 
239 	this() {
240 		super();
241 
242 		this.setIndent(-12);
243 
244 		this.getBuffer().createTag("highlightedpost", "background", "white");
245 
246 		this.addOnButtonRelease(&this.onClick);
247 		this.addOnMotionNotify(&this.onMotion);
248 
249 		this.setBorderWindowSize(GtkTextWindowType.LEFT, 2);
250 
251 		Signals.connectData(
252 			this,
253 			"draw",
254 			cast(GCallback)&this.onDrawCallback,
255 			cast(void*)this,
256 			null,
257 			cast(ConnectFlags)0);
258 	}
259 
260 	bool onClick(Event event, Widget viewer) {
261 		int bufferX, bufferY;
262 
263 		auto adjustment = this.getVadjustment();
264 
265 		this.windowToBufferCoords(GtkTextWindowType.WIDGET, cast(int)event.motion().x, cast(int)event.motion().y, bufferX, bufferY);
266 
267 		TextIter position = new TextIter();
268 		this.getIterAtLocation(position, bufferX, bufferY);
269 
270 		GtkPost post = this.getPostAtIter(position);
271 		if (post) {
272 			int offset = position.getLineOffset();
273 			if (offset <= 8) {
274 				this.postClockClick.emit(post);
275 			} else {
276 				GtkPostSegment segment = post.getSegmentAt(offset - this.postSegmentsOffsets[post]);
277 				if (segment && segment.text && segment.text.length) {
278 					this.postSegmentClick.emit(post, segment);
279 					if (segment.context.clock != Clock.init) {
280 						foreach (GtkPost found_post; this.findPostsByClock(segment)) {
281 							this.scrollToPost(found_post);
282 						}
283 					}
284 				} else if (offset > 9 && offset < this.postSegmentsOffsets[post]) {
285 					this.postLoginClick.emit(post);
286 				}
287 			}
288 		}
289 
290 		return false;
291 	}
292 
293 	void unHighlightEverything() {
294 		this.resetHighlight.emit();
295 
296 		foreach (GtkPost post ; this.highlightedPosts.dup) {
297 			this.unHighlightPost(post);
298 		}
299 
300 		foreach (GtkPostSegment segment ; this.highlightedPostSegments.dup) {
301 			this.unHighlightPostSegment(segment);
302 		}
303 	}
304 
305 	void unHighlightPost(GtkPost post) {
306 		if (post && post.id in this.highlightedPosts && post in this.postBegins && post in this.postEnds) {
307 			TextIter beginIter = new TextIter();
308 			TextIter endIter = new TextIter();
309 
310 			this.getBuffer().getIterAtMark(beginIter, this.postBegins[post]);
311 			this.getBuffer().getIterAtMark(endIter, this.postEnds[post]);
312 
313 			this.getBuffer().removeTagByName("highlightedpost", beginIter, endIter);
314 			this.highlightedPosts.remove(post.id);
315 		}
316 	}
317 
318 	void highlightPost(GtkPost post) {
319 		if (post && post.id !in this.highlightedPosts && post in this.postBegins && post in this.postEnds) {
320 			this.postHighlight.emit(post);
321 
322 			TextIter beginIter = new TextIter();
323 			TextIter endIter = new TextIter();
324 
325 			this.getBuffer().getIterAtMark(beginIter, this.postBegins[post]);
326 			this.getBuffer().getIterAtMark(endIter, this.postEnds[post]);
327 			this.getBuffer().applyTagByName("highlightedpost", beginIter, endIter);
328 
329 			this.highlightedPosts[post.id] = post;
330 
331 			this.highlightPostAnswers(post);
332 		}
333 	}
334 
335 	void highlightPostAnswers(GtkPost post) {
336 		if (post in this.postBegins) {
337 			TextIter beginIter = new TextIter();
338 			TextIter endIter = new TextIter();
339 
340 			this.getBuffer().getIterAtMark(beginIter, this.postBegins[post]);
341 			this.getBuffer().getIterAtMark(endIter, this.postBegins[post]);
342 			endIter.setLineOffset(10);
343 
344 			this.getBuffer().applyTagByName("highlightedpost", beginIter, endIter);
345 			this.highlightedPosts[post.id] = post;
346 
347 			foreach (GtkPostSegment found_segment; this.findReferencesToPost(post)) {
348 				this.highlightPostSegment(found_segment);
349 			}
350 		}
351 	}
352 
353 	void highlightPostSegment(GtkPostSegment segment) {
354 		if (segment !in this.highlightedPostSegments && segment in this.segmentBegins && segment in this.segmentEnds) {
355 			TextIter beginIter = new TextIter();
356 			TextIter endIter = new TextIter();
357 
358 			this.getBuffer().getIterAtMark(beginIter, this.segmentBegins[segment]);
359 			this.getBuffer().getIterAtMark(endIter, this.segmentEnds[segment]);
360 
361 			this.getBuffer().applyTagByName("highlightedpost", beginIter, endIter);
362 			this.highlightedPostSegments[segment] = segment;
363 		}
364 	}
365 
366 	void unHighlightPostSegment(GtkPostSegment segment) {
367 		if (segment in this.highlightedPostSegments && segment in this.segmentBegins && segment in this.segmentEnds) {
368 			TextIter beginIter = new TextIter();
369 			TextIter endIter = new TextIter();
370 
371 			this.getBuffer().getIterAtMark(beginIter, this.segmentBegins[segment]);
372 			this.getBuffer().getIterAtMark(endIter, this.segmentEnds[segment]);
373 
374 			this.getBuffer().removeTagByName("highlightedpost", beginIter, endIter);
375 			this.highlightedPostSegments.remove(segment);
376 		}
377 	}
378 
379 	void highlightClock(GtkPostSegment segment) {
380 		this.highlightPostSegment(segment);
381 
382 		foreach (GtkPost post; this.findPostsByClock(segment)) {
383 			this.highlightPost(post);
384 		}
385 	}
386 
387 	Cursor[GdkCursorType] cursors;
388 	GdkCursorType currentCursor;
389 
390 	GtkPost currentPostHover;
391 	GtkPost currentLoginHover, currentClockHover;
392 	GtkPostSegment currentSegmentHover;
393 
394 	bool onMotion(Event event, Widget viewer) {
395 		int bufferX, bufferY;
396 
397 		this.windowToBufferCoords(GtkTextWindowType.WIDGET, cast(int)event.motion().x, cast(int)event.motion().y, bufferX, bufferY);
398 
399 		TextIter position = new TextIter();
400 		this.getIterAtLocation(position, bufferX, bufferY);
401 
402 		GdkCursorType cursor = GdkCursorType.ARROW;
403 		GtkPost post = this.getPostAtIter(position);
404 		if (post != this.currentPostHover) {
405 			this.postHover.emit(post);
406 
407 			this.currentPostHover = post;
408 
409 			this.currentClockHover = null;
410 			this.currentLoginHover = null;
411 			this.currentSegmentHover = null;
412 
413 			this.unHighlightEverything();
414 		}
415 		if (post) {
416 			int offset = position.getLineOffset();
417 			if (offset <= 8) {
418 				cursor = GdkCursorType.HAND2;
419 				if (post != this.currentClockHover) {
420 					this.unHighlightEverything();
421 					this.highlightPostAnswers(post);
422 					this.postClockHover.emit(post);
423 
424 					this.currentClockHover = post;
425 					this.currentLoginHover = null;
426 					this.currentSegmentHover = null;
427 				}
428 			} else {
429 				GtkPostSegment segment = post.getSegmentAt(offset - this.postSegmentsOffsets[post]);
430 				if (segment && segment.text && segment.text.length) {
431 					if (segment != this.currentSegmentHover) {
432 						this.unHighlightEverything();
433 						if (segment.context.clock != Clock.init) {
434 							this.highlightClock(segment);
435 						}
436 						this.postSegmentHover.emit(post, segment);
437 
438 						this.currentClockHover = null;
439 						this.currentLoginHover = null;
440 						this.currentSegmentHover = segment;
441 					} else if (segment.context.clock != Clock.init || segment.context.link) {
442 						cursor = GdkCursorType.HAND2;
443 					}
444 				} else if (offset > 9 && offset < this.postSegmentsOffsets[post]) {
445 					cursor = GdkCursorType.HAND2;
446 					if (post != this.currentPostHover) {
447 						this.postLoginHover.emit(post);
448 
449 						this.currentLoginHover = post;
450 						this.currentClockHover = null;
451 						this.currentSegmentHover = null;
452 					}
453 				}
454 			}
455 		}
456 
457 		if (cursor !in this.cursors) {
458 			this.cursors[cursor] = new Cursor(cursor);
459 		}
460 
461 		if (cursor != this.currentCursor) {
462 			this.getWindow(GtkTextWindowType.TEXT).setCursor(this.cursors[cursor]);
463 		}
464 
465 		return false;
466 	}
467 }
468 
469 class TribuneViewer : TextView {
470 	private TextMark begin, end;
471 
472 	public GtkTribune[] tribunes;
473 
474 	private GtkPost[string] posts;
475 	private GtkPost[][SysTime] timestamps;
476 
477 
478 	Color[GtkTribune] tribuneColors;
479 
480 	this() {
481 		this.setEditable(false);
482 		this.setCursorVisible(false);
483 		this.setWrapMode(WrapMode.WORD);
484 
485 		TextBuffer buffer = this.getBuffer();
486 
487 		buffer.createTag("mainclock", "foreground-gdk", new Color(50, 50, 50));
488 
489 		buffer.createTag("login", "weight", PangoWeight.BOLD , "foreground-gdk", new Color(0, 0, 100));
490 		buffer.createTag("info",  "style" , PangoStyle.ITALIC, "foreground-gdk", new Color(0, 0, 100));
491 
492 		buffer.createTag("clock", "weight", PangoWeight.BOLD , "foreground-gdk", new Color(0, 0, 100));
493 
494 		buffer.createTag("a", "weight"       , PangoWeight.BOLD,
495 		                      "underline"    , PangoUnderline.SINGLE,
496 		                      "foreground-gdk", new Color(0, 0, 100));
497 
498 		buffer.createTag("b", "weight"       , PangoWeight.BOLD);
499 		buffer.createTag("i", "style"        , PangoStyle.ITALIC);
500 		buffer.createTag("u", "underline"    , PangoUnderline.SINGLE);
501 		buffer.createTag("s", "strikethrough", 1);
502 
503 		TextIter iter = new TextIter();
504 		buffer.getEndIter(iter);
505 		buffer.createMark("end", iter, false);
506 	}
507 
508 	void scrollToPost(GtkPost post) {
509 		if (post in this.postEnds) {
510 			this.scrollToMark(this.postEnds[post], 0, 1, 0, 1);
511 		}
512 	}
513 
514 	GtkPost[] findPostsByClock(GtkPostSegment segment) {
515 		GtkPost[] posts;
516 
517 		foreach (GtkTribune tribune ; this.tribunes) {
518 			if (tribune.tribune.matches_name(segment.context.clock.tribune)) {
519 				posts ~= tribune.findPostsByClock(segment);
520 			}
521 		}
522 
523 		return posts;
524 	}
525 
526 	GtkPostSegment[] findReferencesToPost(GtkPost post) {
527 		GtkPostSegment[] segments;
528 
529 		foreach (GtkTribune tribune ; this.tribunes) {
530 			segments ~= tribune.findReferencesToPost(post);
531 		}
532 
533 		return segments;
534 	}
535 
536 	public GtkPost getPostAtIter(TextIter position) {
537 		TextIter iter = new TextIter ();
538 
539 		this.getBuffer().getIterAtLine(iter, position.getLine());
540 
541 		ListSG marks = iter.getMarks();
542 
543 		for (int i = 0 ; i < marks.length() ; i++) {
544 			TextMark mark = new TextMark (cast (GtkTextMark*) marks.nthData(i));
545 
546 			if (mark.getName() in this.posts) {
547 				return this.posts[mark.getName()];
548 			}
549 		}
550 
551 		return null;
552 	}
553 
554 	void registerTribune(GtkTribune gtkTribune) {
555 		gtkTribune.tag = "tribune" ~ gtkTribune.tribune.name;
556 		this.getBuffer().createTag(gtkTribune.tag, "paragraph-background", gtkTribune.color);
557 	}
558 
559 	void scrollToEnd() {
560 		this.scrollMarkOnscreen(this.getBuffer().getMark("end"));
561 	}
562 
563 	bool isScrolledDown() {
564 		auto adjustment = this.getVadjustment();
565 		return (adjustment.getValue() >= (adjustment.getUpper() - adjustment.getPageSize()) - 120)
566 			|| (adjustment.getPageSize() <= adjustment.getUpper());
567 	}
568 
569 	TextIter getIterForTime(SysTime insert_time) {
570 		TextIter iter = new TextIter();
571 		TextBuffer buffer = this.getBuffer();
572 
573 		auto times = this.timestamps.keys;
574 		times.sort!((a, b) => a < b);
575 
576 		foreach (SysTime time ; times) {
577 			if (time > insert_time) {
578 				GtkPost post = this.timestamps[time][0];
579 				buffer.getIterAtMark(iter, this.postBegins[post]);
580 				iter.backwardChar();
581 				return iter;
582 			}
583 		}
584 
585 		buffer.getEndIter(iter);
586 		return iter;
587 	}
588 
589 	TextMark[GtkPost] postBegins;
590 	TextMark[GtkPost] postEnds;
591 	TextMark[GtkPostSegment] segmentBegins;
592 	TextMark[GtkPostSegment] segmentEnds;
593 	int[GtkPost] postSegmentsOffsets;
594 
595 	void renderPost(GtkPost post) {
596 		GtkPostSegment[] segments = post.segments();
597 
598 		TextBuffer buffer = this.getBuffer();
599 		TextIter iter = this.getIterForTime(post.post.real_time);
600 
601 		if (buffer.getCharCount() > 1) {
602 			buffer.insert(iter, "\n");
603 		}
604 
605 		this.postBegins[post] = buffer.createMark(post.id, iter, true);
606 
607 		buffer.insert(iter, " ");
608 		buffer.insertWithTagsByName(iter, post.post.clock, ["mainclock"]);
609 		buffer.insert(iter, " ");
610 		if (post.post.login && post.post.login.length > 0) {
611 			buffer.insertWithTagsByName(iter, post.post.login, ["login"]);
612 		} else if (post.post.short_info.length > 0) {
613 			buffer.insertWithTagsByName(iter, post.post.short_info, ["info"]);
614 		} else {
615 			buffer.insertWithTagsByName(iter, " ", ["info"]);
616 		}
617 		buffer.insert(iter, " ");
618 
619 		this.postSegmentsOffsets[post] = iter.getLineOffset();
620 
621 		foreach (int i, GtkPostSegment segment; segments) {
622 			TextMark startMark = buffer.createMark("start", iter, true);
623 			TextMark endMark = buffer.createMark("start", iter, false);
624 			this.segmentBegins[segment] = buffer.createMark("start-" ~ post.id ~ ":" ~ to!string(i), iter, true);
625 
626 			buffer.insert(iter, segment.text);
627 
628 			TextIter startIter = new TextIter();
629 			buffer.getIterAtMark(startIter, startMark);
630 
631 			if (segment.context.link) {
632 				buffer.applyTagByName("a", startIter, iter);
633 			}
634 
635 			if (segment.context.bold) {
636 				buffer.applyTagByName("b", startIter, iter);
637 			}
638 
639 			if (segment.context.italic) {
640 				buffer.applyTagByName("i", startIter, iter);
641 			}
642 
643 			if (segment.context.underline) {
644 				buffer.applyTagByName("u", startIter, iter);
645 			}
646 
647 			if (segment.context.strike) {
648 				buffer.applyTagByName("s", startIter, iter);
649 			}
650 
651 			if (segment.context.clock != Clock.init) {
652 				buffer.applyTagByName("clock", startIter, iter);
653 			}
654 
655 			this.segmentEnds[segment] = buffer.createMark("end-" ~ post.id ~ ":" ~ to!string(i), iter, true);
656 		}
657 
658 		this.postEnds[post] = buffer.createMark("end-" ~ post.id, iter, true);
659 
660 		TextIter postStartIter = new TextIter();
661 		buffer.getIterAtMark(postStartIter, this.postBegins[post]);
662 		TextIter postEndIter = new TextIter();
663 		buffer.getIterAtMark(postEndIter, this.postEnds[post]);
664 		buffer.applyTagByName(post.tribune.tag, postStartIter, postEndIter);
665 
666 		this.posts[post.id] = post;
667 		this.timestamps[post.post.real_time] ~= post;
668 
669 		if (!this.begin) {
670 			this.begin = this.postBegins[post];
671 		}
672 
673 		if (!this.end) {
674 			this.end = this.postEnds[post];
675 		}
676 	}
677 }
678