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