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