1 /** 2 This file contains the GTK-specific parts of Plot2Kill and is publicly 3 imported by plot2kill.figure if compiled with GtkD. 4 5 Note: The functions that need GTK to be initialized for their use automatically 6 call gtk.Main.initCheck() to provide sane default initialization. This 7 is for convenience in programs that throw up a few simple plots and 8 otherwise don't have a GUI. If gtk.Main.init() is called before 9 calling any function in this module that requires GTK to be initialized, 10 the first call's settings take precedence and the calls from this 11 module have no effect. 12 13 Copyright (C) 2010-2011 David Simcha 14 15 License: 16 17 Boost Software License - Version 1.0 - August 17th, 2003 18 19 Permission is hereby granted, free of charge, to any person or organization 20 obtaining a copy of the software and accompanying documentation covered by 21 this license (the "Software") to use, reproduce, display, distribute, 22 execute, and transmit the Software, and to prepare derivative works of the 23 Software, and to permit third-parties to whom the Software is furnished to 24 do so, all subject to the following: 25 26 The copyright notices in the Software and this entire statement, including 27 the above license grant, this restriction and the following disclaimer, 28 must be included in all copies of the Software, in whole or in part, and 29 all derivative works of the Software, unless such copies or derivative 30 works are solely in the form of machine-executable object code generated by 31 a source language processor. 32 33 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 34 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 35 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 36 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 37 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 38 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 39 DEALINGS IN THE SOFTWARE. 40 */ 41 module plot2kill.gtkwrapper; 42 43 version(dfl) { 44 } else { 45 46 import plot2kill.util; 47 import plot2kill.guiagnosticbase; 48 import plot2kill.subplot; 49 import plot2kill.figure; 50 import plot2kill.plot; 51 52 alias std..string.CaseSensitive CaseSensitive; 53 54 static import std.file; 55 56 import gdk.Color, gtk.Widget, gtk.DrawingArea, gdk.Event, 57 gtk.MainWindow, gtk.Main, gdk.Window, gtk.Container, gtk.Window, 58 gtk.Image, gdk.Pixbuf, gtk.FileChooserDialog, gtk.Dialog, 59 gtk.FileFilter, gobject.ObjectG, cairo.Context, cairo.FontFace, 60 gtkc.cairotypes, cairo.PdfSurface, cairo.SvgSurface, 61 cairo.PostScriptSurface, cairo.Surface, cairo.ImageSurface, 62 gtk.MessageDialog, gtk.Menu, gtk.MenuItem, 63 gtk.Entry, gtk.HBox, gtk.Label, gtk.FontSelectionDialog, gtk.RadioButton, 64 gtk.HSeparator, gtk.CheckButton, gtk.SeparatorMenuItem, gtkc.gtk; 65 66 // Default initialize GTK. 67 package void defaultInit() { 68 string[] args; 69 Main.initCheck(args); 70 } 71 72 /**GTK's implementation of a color object.*/ 73 struct Color { 74 ubyte r; 75 ubyte g; 76 ubyte b; 77 } 78 79 /**Holds context for drawing lines.*/ 80 struct Pen { 81 Color color; 82 double lineWidth; 83 } 84 85 /**Holds context for drawing rectangles.*/ 86 struct Brush { 87 Color color; 88 } 89 90 /// 91 struct Point { 92 /// 93 int x; 94 95 /// 96 int y; 97 } 98 99 /// 100 struct Rect { 101 /// 102 int x; 103 104 /// 105 int y; 106 107 /// 108 int width; 109 110 /// 111 int height; 112 } 113 114 /// 115 struct Size { 116 /// 117 int width; 118 119 /// 120 int height; 121 } 122 123 /**Holds font information.*/ 124 alias cairo.FontFace.FontFace font; 125 126 /**Get a color in a GUI framework-agnostic way.*/ 127 Color getColor(ubyte red, ubyte green, ubyte blue) { 128 return Color(red, green, blue); 129 } 130 131 /**Get a font in a GUI framework-agnostic way.*/ 132 struct Font { 133 FontFace face; 134 string name; 135 double size; 136 } 137 138 Font getFont(string fontName, double size) { 139 auto slant = (fontName.indexOf("oblique", CaseSensitive.no) > -1) ? 140 cairo_font_slant_t.OBLIQUE : cairo_font_slant_t.NORMAL; 141 auto weight = (fontName.indexOf("bold", CaseSensitive.no) > -1) ? 142 cairo_font_weight_t.BOLD : cairo_font_weight_t.NORMAL; 143 144 145 return Font( 146 Context.toyFontFaceCreate( 147 fontName, 148 slant, 149 weight 150 ), fontName, size 151 ); 152 } 153 154 155 /// 156 enum TextAlignment { 157 /// 158 Left = 0, 159 160 /// 161 Center = 1, 162 163 /// 164 Right = 2 165 } 166 167 // This calls the relevant lib's method of cleaning up the given object, if 168 // any. 169 void doneWith(T)(T garbage) { 170 static if(is(T : gdk.Pixbuf.Pixbuf) || is (T : gtk.Image.Image)) { 171 // Most things seem to manage themselves fine, but these objects 172 // leak like a seive. 173 garbage.unref(); 174 175 // Since we're already in here be dragons territory, we may as well: 176 core.memory.GC.free(cast(void*) garbage); 177 } else static if(is(T : cairo.Context.Context) || is(T : cairo.Surface.Surface)) { 178 179 static if(is(T : cairo.Surface.Surface)) { 180 garbage.finish(); 181 } 182 //garbage.destroy(); 183 } 184 } 185 186 /**The base class for both FigureBase and Subplot. Holds common functionality 187 * like saving and text drawing. 188 */ 189 abstract class FigureBase : GuiAgnosticBase { 190 private: 191 enum ubyteMax = cast(double) ubyte.max; 192 193 // See drawLine() for an explanation of these variables. 194 PlotPoint[] prevLine; 195 Pen lastLinePen; 196 197 void saveImplPixmap 198 (string filename, string type, double width, double height) { 199 plot2kill.gtkwrapper.defaultInit(); 200 /+int w = roundTo!int(width); 201 int h = roundTo!int(height); 202 203 auto image = new Image(null, w, h, 24); 204 scope(exit) doneWith(image); 205 206 auto c = new Context(image); 207 scope(exit) doneWith(c); 208 209 this.drawTo(c, PlotRect(0, 0, w, h)); 210 auto pixbuf = new Pixbuf(image, 0, 0, w, h); 211 scope(exit) doneWith(pixbuf); 212 213 int result = pixbuf.savev(filename, type, null, null); 214 enforce(result, "File not saved successfully.");+/ 215 } 216 217 void saveImplSurface 218 (string filename, string type, double width, double height) { 219 Surface surf; 220 switch(type) { 221 case "pdf": 222 surf = PdfSurface.create(filename, width, height); 223 break; 224 case "eps": 225 surf = PostScriptSurface.create(filename, width, height); 226 break; 227 case "svg": 228 surf = SvgSurface.create(filename, width, height); 229 break; 230 case "png": 231 surf = ImageSurface.create(cairo_format_t.RGB24, 232 roundTo!int(width), roundTo!int(height)); 233 break; 234 default: 235 enforce(0, "Invalid file format: " ~ type); 236 } 237 238 enforce(surf, "Couldn't save file because surface couldn't be created."); 239 auto context = Context.create(surf); 240 241 this.drawTo(context, PlotRect(0,0, width, height)); 242 surf.flush(); 243 244 if(type == "png") { 245 // So sue me for the cast. 246 auto result = (cast(ImageSurface) surf).writeToPng(filename); 247 enforce(result == cairo_status_t.SUCCESS, text( 248 "Unsuccessfully wrote png. Error: ", result)); 249 } 250 251 // This should really be a scope(exit) but using scope(exit) instead 252 // of putting this line down here segfaults on Linux 64 for reasons 253 // I don't understand. 254 surf.finish(); 255 } 256 257 void saveImplSvgz(string filename, double width, double height) { 258 // An svgz file is just an SVG that's been compressed with gzip. 259 260 static extern(C) cairo_status_t 261 writeFunc(void* gzVoid, ubyte* dataPtr, uint len) { 262 uchar[] data = dataPtr[0..len]; 263 264 auto gz = cast(Gzip*) gzVoid; 265 gz.addData(data); 266 return cairo_status_t.SUCCESS; 267 } 268 269 auto gz = Gzip(filename); 270 scope(exit) gz.finish(); 271 272 auto surf = SvgSurface.createForStream 273 (&writeFunc, cast(void*) &gz, width, height); 274 275 scope(exit) doneWith(surf); 276 auto context = Context.create(surf); 277 scope(exit) doneWith(context); 278 279 this.drawTo(context, PlotRect(0,0, width, height)); 280 surf.flush(); 281 } 282 283 void finishLine() { 284 if(!prevLine.length) return; 285 assert(prevLine.length > 1); 286 287 context.save(); 288 scope(exit) context.restore(); 289 context.newPath(); 290 291 auto c = lastLinePen.color; 292 context.setSourceRgb(c.r / ubyteMax, c.g / ubyteMax, c.b / ubyteMax); 293 context.setLineWidth(lastLinePen.lineWidth); 294 295 // If we're joining lines, it's always on a LineGraph or something, 296 // where miter creates weird artifacts. Bevel looks best. 297 context.setLineJoin(cairo_line_join_t.BEVEL); 298 299 context.moveTo(prevLine.front.x + xOffset, prevLine.front.y + yOffset); 300 foreach(i; 1..prevLine.length) { 301 auto point = prevLine[i]; 302 context.lineTo(point.x + xOffset, point.y + yOffset); 303 } 304 305 context.stroke(); 306 prevLine.length = 0; 307 assumeSafeAppend(prevLine); 308 } 309 310 protected: 311 // Fonts tend to be different actual sizes on different GUI libs for a 312 // given nominal size. This adjusts for that factor when setting default 313 // fonts. 314 enum fontSizeAdjust = 0; 315 316 Context context; 317 318 public: 319 // These are undocumented FOR A REASON: They aren't part of the public 320 // API, but package is so broken it's not usable. All this stuff w/o 321 // ddoc should only be messed with if you're a developer of this lib, 322 // not if you want to use it as a black box. 323 324 final void drawLine 325 (Pen pen, double startX, double startY, double endX, double endY) { 326 /* HACK ALERT: The front end to this library is designed for each line 327 * to be drawn as a discrete unit, but for line joining purposes, 328 * lines need to be drawn in a single path in Cairo. Therefore, 329 * we save stuff here and only draw it when moving to a new continuous 330 * line. 331 */ 332 if(prevLine.length > 0) { 333 if(startX != prevLine.back.x || startY != prevLine.back.y 334 || pen != lastLinePen) { 335 finishLine(); 336 } 337 } 338 339 // This can change by calling finishLine(). Need to check it again 340 // instead of just using an else block. 341 if(prevLine.length == 0) { 342 prevLine ~= PlotPoint(startX, startY); 343 lastLinePen = pen; 344 } 345 346 prevLine ~= PlotPoint(endX, endY); 347 } 348 349 final void drawLine(Pen pen, PlotPoint start, PlotPoint end) { 350 this.drawLine(pen, start.x, start.y, end.x, end.y); 351 } 352 353 final void drawRectangle 354 (Pen pen, double x, double y, double width, double height) { 355 context.save(); 356 scope(exit) context.restore(); 357 context.newPath(); 358 359 auto c = pen.color; 360 context.setSourceRgb(c.r / ubyteMax, c.g / ubyteMax, c.b / ubyteMax); 361 context.setLineWidth(pen.lineWidth); 362 context.rectangle(x + xOffset, y + yOffset, width, height); 363 context.stroke(); 364 } 365 366 final void drawRectangle(Pen pen, Rect r) { 367 this.drawRectangle(pen, r.x, r.y, width, height); 368 } 369 370 final void fillRectangle 371 (Brush brush, double x, double y, double width, double height) { 372 context.save(); 373 scope(exit) context.restore(); 374 context.newPath(); 375 376 auto c = brush.color; 377 enum ubyteMax = cast(double) ubyte.max; 378 context.setSourceRgb(c.r / ubyteMax, c.g / ubyteMax, c.b / ubyteMax); 379 context.rectangle(x + xOffset, y + yOffset, width, height); 380 context.fill(); 381 } 382 383 final void fillRectangle(Brush brush, Rect r) { 384 this.fillRectangle(brush, r.x, r.y, r.width, r.height); 385 } 386 387 final void drawText( 388 string text, 389 Font font, 390 Color pointColor, 391 PlotRect rect, 392 TextAlignment alignment 393 ) { 394 context.save(); 395 scope(exit) context.restore(); 396 context.newPath(); 397 398 drawTextCurrentContext(text, font, pointColor, rect, alignment); 399 } 400 401 final void drawTextCurrentContext( 402 string text, 403 Font font, 404 Color pointColor, 405 PlotRect rect, 406 TextAlignment alignment 407 ) { 408 alias rect r; // save typing 409 auto measurements = measureText(text, font); 410 411 // The height added by stuff below the baseline for letters like "g" 412 // and "y" throws off aligning text vertically. Use the height of 413 // "A", which is a tall letter with nothing below baseline, to 414 // figure out where to start writing. 415 immutable standardLetterHeight = measureText("A", font).height; 416 417 if(measurements.width > rect.width) { 418 alignment = TextAlignment.Left; 419 } 420 421 if(alignment == TextAlignment.Left) { 422 r = PlotRect( 423 r.x, 424 r.y + standardLetterHeight, 425 r.width, 426 r.height 427 ); 428 } else if(alignment == TextAlignment.Center) { 429 r = PlotRect( 430 r.x + (r.width - measurements.width) / 2, 431 r.y + standardLetterHeight, 432 r.width, r.height 433 ); 434 } else if(alignment == TextAlignment.Right) { 435 r = PlotRect( 436 r.x + (r.width - measurements.width), 437 r.y + standardLetterHeight, 438 r.width, r.height 439 ); 440 } else { 441 assert(0); 442 } 443 444 //context.rectangle(r.x, r.y - measurements.height, r.width, r.height); 445 //context.clip(); 446 context.setFontSize(font.size); 447 context.setFontFace(font.face); 448 449 alias pointColor c; 450 context.setSourceRgb(c.r / ubyteMax, c.g / ubyteMax, c.b / ubyteMax); 451 452 context.setLineWidth(0.5); 453 context.moveTo(r.x + xOffset, r.y + yOffset); 454 context.textPath(text); 455 context.fill(); 456 } 457 458 final void drawText( 459 string text, 460 Font font, 461 Color pointColor, 462 PlotRect rect 463 ) { 464 drawText(text, font, pointColor, rect, TextAlignment.Left); 465 } 466 467 final void drawRotatedText( 468 string text, 469 Font font, 470 Color pointColor, 471 PlotRect rect, 472 TextAlignment alignment 473 ) { 474 context.save(); 475 scope(exit) context.restore; 476 context.newPath(); 477 478 alias rect r; // save typing 479 auto measurements = measureText(text, font); 480 481 // The height added by stuff below the baseline for letters like "g" 482 // and "y" throws off aligning text vertically. Use the height of 483 // "A", which is a tall letter with nothing below baseline, to 484 // figure out where to start writing. 485 immutable standardLetterHeight = measureText("A", font).height; 486 487 immutable slack = rect.height - measurements.width; 488 if(slack < 0) { 489 alignment = TextAlignment.Left; 490 } 491 492 if(alignment == TextAlignment.Left) { 493 r = PlotRect( 494 r.x + standardLetterHeight, 495 r.y + r.height, 496 r.width, 497 r.height 498 ); 499 } else if(alignment == TextAlignment.Center) { 500 r = PlotRect( 501 r.x + standardLetterHeight, 502 r.y + r.height - slack / 2, 503 r.width, r.height 504 ); 505 } else if(alignment == TextAlignment.Right) { 506 r = PlotRect( 507 r.x + standardLetterHeight, 508 r.y + r.height - slack, 509 r.width, r.height 510 ); 511 } else { 512 assert(0); 513 } 514 //context.rectangle(r.x, r.y - measurements.height, r.width, r.height); 515 //context.clip(); 516 context.setFontSize(font.size); 517 context.setFontFace(font.face); 518 519 alias pointColor c; 520 context.setSourceRgb(c.r / ubyteMax, c.g / ubyteMax, c.b / ubyteMax); 521 522 context.setLineWidth(0.5); 523 context.moveTo(r.x + xOffset, r.y + yOffset); 524 context.rotate(PI * 1.5); 525 context.textPath(text); 526 context.fill(); 527 } 528 529 final void drawRotatedText( 530 string text, 531 Font font, 532 Color pointColor, 533 PlotRect rect 534 ) { 535 drawRotatedText(text, font, pointColor, rect, TextAlignment.Left); 536 } 537 538 // BUGS: Ignores maxWidth. 539 final PlotSize measureText 540 (string text, Font font, double maxWidth, TextAlignment alignment) { 541 return measureText(text, font); 542 } 543 544 // BUGS: Ignores maxWidth. 545 final PlotSize measureText(string text, Font font, double maxWidth) { 546 return measureText(text, font); 547 548 } 549 550 final PlotSize measureText(string text, Font font) { 551 context.save(); 552 scope(exit) context.restore(); 553 554 context.setLineWidth(1); 555 context.setFontSize(font.size); 556 context.setFontFace(font.face); 557 cairo_text_extents_t ext; 558 559 context.textExtents(text, &ext); 560 return PlotSize(ext.width, ext.height); 561 } 562 563 // TODO: Add support for stuff other than solid brushes. 564 /*Get a brush in a GUI framework-agnostic way.*/ 565 static Brush getBrush(Color color) { 566 return Brush(color); 567 } 568 569 /*Get a pen in a GUI framework-agnostic way.*/ 570 static Pen getPen(Color color, double width = 1) { 571 return Pen(color, width); 572 } 573 574 void drawTo(Context context) { 575 drawTo(context, this.width, this.height); 576 } 577 578 void drawTo(Context context, double width, double height) { 579 return drawTo(context, PlotRect(0, 0, width, height)); 580 } 581 582 // Allows drawing at an offset from the origin. 583 void drawTo(Context context, PlotRect whereToDraw) { 584 enforceSane(whereToDraw); 585 // Save the default class-level values, make the values passed in the 586 // class-level values, call drawImpl(), then restore the default values. 587 auto oldContext = this.context; 588 auto oldWidth = this._width; 589 auto oldHeight = this._height; 590 auto oldXoffset = this.xOffset; 591 auto oldYoffset = this.yOffset; 592 593 scope(exit) { 594 this.context = oldContext; 595 this._height = oldHeight; 596 this._width = oldWidth; 597 this.xOffset = oldXoffset; 598 this.yOffset = oldYoffset; 599 } 600 601 this.context = context; 602 this._width = whereToDraw.width; 603 this._height = whereToDraw.height; 604 this.xOffset = whereToDraw.x; 605 this.yOffset = whereToDraw.y; 606 drawImpl(); 607 finishLine(); 608 } 609 610 /**Saves this figure to a file. The file type can be one of either the 611 * raster formats .png, .jpg, .tiff, and .bmp, or the vector formats 612 * .pdf, .svg and .eps. The width and height parameters allow you to 613 * specify explicit width and height parameters for the image file. If 614 * width and height are left at their default values 615 * of 0, the default width and height of the subclass being saved will 616 * be used. 617 * 618 * Bugs: .jpg, .tiff and .bmp formats rely on Pixmap objects, meaning 619 * you can't save them to a file unless you have a screen and 620 * have called Main.init(), even though saving should have 621 * nothing to do with X or screens. 622 */ 623 void saveToFile 624 (string filename, string type, double width = 0, double height = 0) { 625 // User friendliness: Remove . if it was included, don't be case sens. 626 type = toLower(type); 627 if(!type.empty && type.front == '.') { 628 type.popFront(); 629 } 630 if(type == "jpg") { 631 type = "jpeg"; 632 } 633 634 if(width == 0 || height == 0) { 635 width = this.defaultWindowWidth; 636 height = this.defaultWindowHeight; 637 } 638 639 if(type == "eps" || type == "pdf" || type == "svg" || type == "png") { 640 return saveImplSurface(filename, type, width, height); 641 } else if(type == "svgz") { 642 return saveImplSvgz(filename, width, height); 643 } else { 644 enforce(type == "tiff" || type == "bmp" || type == "jpeg", 645 "Invalid format: " ~ type); 646 return saveImplPixmap(filename, type, width, height); 647 } 648 } 649 650 /**Convenience function that infers the type from the filename extenstion 651 * and defaults to .png if no valid file format extension is found. 652 */ 653 void saveToFile(string filename, double width = 0, double height = 0) { 654 auto type = toLower(extensionNoDot(filename)); 655 656 try { 657 saveToFile(filename, type, width, height); 658 } catch { 659 // Default to png. 660 saveToFile(filename, "png", width, height); 661 } 662 } 663 664 /**Creates a Widget that will have this object drawn to it. This Widget 665 * can be displayed in a window. 666 */ 667 FigureWidget toWidget() { 668 defaultInit(); 669 return new FigureWidget(this); 670 } 671 672 /**Draw and display the figure as a main form. This is useful in 673 * otherwise console-based apps that want to display a few plots. 674 * However, you can't have another main form up at the same time. 675 */ 676 void showAsMain() { 677 auto mw = new DefaultPlotWindow!(MainWindow)(this.toWidget); 678 Main.run(); 679 } 680 681 /**Returns a default plot window with this figure in it.*/ 682 gtk.Window.Window getDefaultWindow() { 683 return new DefaultPlotWindow!(gtk.Window.Window)(this.toWidget); 684 } 685 } 686 687 // Used for scatter plots. Efficiently draws a single character in a lot of 688 // places, centered on a point. ASSUMPTION: No drawing commands not related 689 // to drawing scatter plot points are issued between when initialize() 690 // and reset() are called. 691 package struct ScatterCharDrawer { 692 private: 693 string str; 694 PlotSize halfMeasurements; 695 Figure fig; 696 Font font; 697 float red, green, blue; 698 699 public: 700 this(dchar c, Font font, Color color, Figure fig) { 701 str = to!string(c); 702 this.fig = fig; 703 this.font = font; 704 705 red = color.r / cast(float) ubyte.max; 706 green = color.g / cast(float) ubyte.max; 707 blue = color.b / cast(float) ubyte.max; 708 709 710 auto measurements = fig.measureText(str, font); 711 halfMeasurements = PlotSize(measurements.width / 2, 712 measurements.height / 2); 713 } 714 715 void draw(PlotPoint where) { 716 with(fig) { 717 if(!insideAxes(where)) return; 718 context.moveTo(where.x + xOffset - halfMeasurements.width, 719 where.y + yOffset + halfMeasurements.height); 720 context.textPath(str); 721 context.fill(); 722 } 723 } 724 725 // Initialize the Cairo context to the settings we need. 726 void initialize() { 727 with(fig) { 728 context.save(); 729 730 // Set up a clip region. 731 context.moveTo(leftMargin + xOffset, topMargin + yOffset); 732 context.lineTo(leftMargin + xOffset, 733 fig.height - bottomMargin + yOffset); 734 context.lineTo(fig.width - rightMargin + xOffset, 735 fig.height - bottomMargin + yOffset); 736 context.lineTo(fig.width - rightMargin + xOffset, 737 topMargin + yOffset); 738 context.lineTo(leftMargin + xOffset, topMargin + yOffset); 739 context.clip(); 740 741 context.setFontSize(font.size); 742 context.setFontFace(font.face); 743 context.setSourceRgb(red, green, blue); 744 context.setLineWidth(0.5); 745 } 746 } 747 748 // Restore the Cairo context to the old settings. 749 void restore() { 750 fig.context.restore(); 751 } 752 } 753 754 /* 755 This class allows a legend symbol to be drawn in a small area. This really 756 needs to be refactored to separate the GUI wrapping code from FigureBase, 757 but I'm too lazy to do it for now. 758 */ 759 private class LegendSymbolDrawer : FigureBase { 760 Plot plot; 761 762 this(Plot plot) { 763 this._width = legendSymbolSize; 764 this._height = legendSymbolSize; 765 this.plot = plot; 766 } 767 768 override void drawImpl() { 769 try { 770 auto rect = Rect(0, 0, legendSymbolSize, legendSymbolSize); 771 auto prect = PlotRect(0, 0, legendSymbolSize, legendSymbolSize); 772 auto brush = getBrush(getColor(255, 255, 255)); 773 scope(exit) doneWith(brush); 774 fillRectangle(brush, rect); 775 plot.drawLegendSymbol(this, prect); 776 } catch(Exception) { 777 // Legend not implemented for plot type. This is ok to ignore. 778 } 779 } 780 781 override int defaultWindowWidth() { return legendSymbolSize; } 782 override int defaultWindowHeight() { return legendSymbolSize; } 783 override int minWindowWidth() { return legendSymbolSize; } 784 override int minWindowHeight() { return legendSymbolSize; } 785 } 786 787 788 /**The default widget for displaying Figure and Subplot objects on screen. 789 * This class has no public constructor or static factory method because the 790 * proper way to instantiate this object is via the toWidget properties 791 * of FigureBase and Subplot. 792 */ 793 class FigureWidget : DrawingArea { 794 private: 795 FigureBase _figure; 796 Surface _surface; 797 798 package: 799 this(FigureBase fig) { 800 super(); 801 this._figure = fig; 802 this.addOnDraw(&onDraw); 803 this.addOnSizeAllocate(&onSizeAllocate); 804 this.setSizeRequest(fig.minWindowWidth, fig.minWindowHeight); 805 } 806 807 void onSizeAllocate(GtkAllocation* allocation, Widget widget) { 808 auto width = allocation.width; 809 auto height = allocation.height; 810 this._surface = ImageSurface.create(CairoFormat.ARGB32, width, height); 811 draw(width, height); 812 } 813 814 bool onDraw(Scoped!Context context, Widget drawingArea) { 815 context.setSourceSurface(this._surface, 0, 0); 816 context.paint(); 817 return true; 818 } 819 820 void draw(double w, double h) { 821 enforce(getParent() !is null, this.classinfo.name); 822 figure.drawTo(Context.create(this._surface), w, h); 823 } 824 825 public: 826 /**Get the underlying FigureBase object.*/ 827 final FigureBase figure() @property { 828 return _figure; 829 } 830 831 /**If set as an addOnSizeAllocate callback, this will resize this control 832 * to the size of its parent window when the parent window is resized. 833 */ 834 void parentSizeChanged(GtkAllocation* alloc, Widget widget) { 835 if(this.getWidth != alloc.width || this.getHeight != alloc.height) { 836 this.setSizeRequest(alloc.width, alloc.height); 837 } 838 } 839 840 /**Draw the figure to the internal drawing area.*/ 841 final void draw() { 842 draw(this.getWidth, this.getHeight); 843 } 844 845 } 846 847 // Convenience subclass of Dialog that has the entries for title, xlabel 848 // and ylabel available in a way that's actually easy to get to. Also 849 // encapsulates the building code. 850 private class LabelDialog : Dialog { 851 Entry titleEntry, xLabelEntry, yLabelEntry; 852 853 this(FigureWidget widget) { 854 super(); 855 setTitle("Labels"); 856 auto content = this.getContentArea(); 857 858 auto fb = widget.figure; 859 auto sp = cast(Subplot) fb; 860 if(sp) { 861 auto zoomed = sp.zoomedFigure; 862 if(zoomed) fb = zoomed; 863 } 864 865 // For some reason GTK complains about null text. Fix it here. 866 static string fixNull(string s) { 867 return (s.length == 0) ? "\0" : s; 868 } 869 870 titleEntry = new Entry( 871 fixNull(fb.title()) 872 ); 873 xLabelEntry = new Entry( 874 fixNull(fb.xLabel()) 875 ); 876 yLabelEntry = new Entry( 877 fixNull(fb.yLabel()) 878 ); 879 880 titleEntry.setActivatesDefault(1); 881 xLabelEntry.setActivatesDefault(1); 882 yLabelEntry.setActivatesDefault(1); 883 884 auto titleBox = new HBox(0, 5); 885 titleBox.add(new Label("Title ")); 886 titleBox.add(titleEntry); 887 888 auto xLabelBox = new HBox(0, 5); 889 xLabelBox.add(new Label("X Label")); 890 xLabelBox.add(xLabelEntry); 891 892 auto yLabelBox = new HBox(0, 5); 893 yLabelBox.add(new Label("Y Label")); 894 yLabelBox.add(yLabelEntry); 895 896 content.add(titleBox); 897 content.add(xLabelBox); 898 content.add(yLabelBox); 899 900 this.addButtons([StockID.OK, StockID.CANCEL], 901 [GtkResponseType.OK, 902 GtkResponseType.CANCEL] 903 ); 904 this.setDefaultResponse(GtkResponseType.OK); 905 this.setResizable(0); 906 } 907 } 908 909 private class ZoomDialog : Dialog { 910 Entry topEntry, bottomEntry, leftEntry, rightEntry; 911 912 this(Figure fig) { 913 super(); 914 setTitle("Zoom"); 915 auto content = this.getContentArea(); 916 917 topEntry = new Entry(to!string(fig.topMost)); 918 bottomEntry = new Entry(to!string(fig.bottomMost)); 919 leftEntry = new Entry(to!string(fig.leftMost)); 920 rightEntry = new Entry(to!string(fig.rightMost)); 921 922 topEntry.setActivatesDefault(1); 923 bottomEntry.setActivatesDefault(1); 924 leftEntry.setActivatesDefault(1); 925 rightEntry.setActivatesDefault(1); 926 927 auto topBox = new HBox(0, 5); 928 topBox.add(new Label("Y Max")); 929 topBox.add(topEntry); 930 931 auto bottomBox = new HBox(0, 5); 932 bottomBox.add(new Label("Y Min")); 933 bottomBox.add(bottomEntry); 934 935 auto leftBox = new HBox(0, 5); 936 leftBox.add(new Label("X Min")); 937 leftBox.add(leftEntry); 938 939 auto rightBox = new HBox(0, 5); 940 rightBox.add(new Label("X Max")); 941 rightBox.add(rightEntry); 942 943 content.add(leftBox); 944 content.add(rightBox); 945 content.add(bottomBox); 946 content.add(topBox); 947 948 this.addButtons([StockID.OK, StockID.CANCEL], 949 [GtkResponseType.OK, 950 GtkResponseType.CANCEL] 951 ); 952 this.setDefaultResponse(GtkResponseType.OK); 953 this.addButtons(["Default"], [cast(GtkResponseType) 1]); 954 this.setResizable(0); 955 } 956 } 957 958 private class LegendDialog : Dialog { 959 Entry[] entries; 960 RadioButton topRadio, bottomRadio, leftRadio,rightRadio; 961 962 this(Figure fig) { 963 super(); 964 setTitle("Legend"); 965 auto content = this.getContentArea(); 966 967 content.add(new Label("Position")); 968 auto posBox1 = new HBox(1, 5); 969 auto posBox2 = new HBox(1, 5); 970 topRadio = new RadioButton("Top"); 971 bottomRadio = new RadioButton(topRadio, "Bottom"); 972 leftRadio = new RadioButton(topRadio, "Left"); 973 rightRadio = new RadioButton(topRadio, "Right"); 974 975 final switch(fig.legendLocation()) { 976 case LegendLocation.left: 977 leftRadio.setActive(1); 978 break; 979 case LegendLocation.right: 980 rightRadio.setActive(1); 981 break; 982 case LegendLocation.top: 983 topRadio.setActive(1); 984 break; 985 case LegendLocation.bottom: 986 bottomRadio.setActive(1); 987 break; 988 } 989 990 posBox1.add(topRadio); 991 posBox1.add(bottomRadio); 992 posBox2.add(leftRadio); 993 posBox2.add(rightRadio); 994 995 content.add(posBox1); 996 content.add(posBox2); 997 content.add(new HSeparator); 998 999 foreach(plot; fig.plotData) if(plot.hasLegend()) { 1000 auto symbolDrawer = new LegendSymbolDrawer(plot); 1001 auto widget = new FigureWidget(symbolDrawer); 1002 1003 auto ltext = plot.legendText(); 1004 if(!ltext.length) ltext = "\0"; 1005 auto box = new HBox(0, 5); 1006 box.add(widget); 1007 1008 auto entry = new Entry(ltext); 1009 entry.setActivatesDefault(1); 1010 entries ~= entry; 1011 box.add(entry); 1012 content.add(box); 1013 } 1014 1015 this.addButtons([StockID.OK, StockID.CANCEL], 1016 [GtkResponseType.OK, 1017 GtkResponseType.CANCEL] 1018 ); 1019 this.setDefaultResponse(GtkResponseType.OK); 1020 this.setResizable(0); 1021 } 1022 } 1023 1024 class TickDialog(char xy) : Dialog { 1025 Entry locEntry, labelEntry, gridEntry; 1026 CheckButton rotateButton, gridLineButton; 1027 enum upperXY = cast(char) (xy + ('X' - 'x')); 1028 enum grid = (xy == 'x') ? "vertical" : "horizontal"; 1029 1030 this(Figure fig) { 1031 super(); 1032 setTitle(upperXY ~ " Ticks"); 1033 auto content = this.getContentArea(); 1034 1035 auto instructions = new Label( 1036 "Enter labels and locations as comma-separated lists. Commas may\n" ~ 1037 "be escaped using the \\ character. Labels may be left blank, in\n" ~ 1038 "which case they will be set to the string representations of locations.\n\n" ~ 1039 "The default button causes the default heuristics for tick locations\n" ~ 1040 "to be used, but still updates the grid lines and rotated label text\n" 1041 "settings." 1042 ); 1043 content.add(instructions); 1044 1045 content.add(new HSeparator()); 1046 1047 auto locBox = new HBox(0, 5); 1048 locBox.add(new Label("Locations")); 1049 locEntry = new Entry(); 1050 1051 auto stringLocs = mixin("to!(string[])(fig." ~ xy ~ "AxisLocations)"); 1052 auto joined = std..string.join(stringLocs, ", "); 1053 locEntry.setText(joined); 1054 locEntry.setSizeRequest(400, locEntry.getHeight()); 1055 locEntry.setActivatesDefault(1); 1056 locBox.add(locEntry); 1057 content.add(locBox); 1058 1059 auto labelBox = new HBox(0, 5); 1060 labelBox.add(new Label("Labels ")); 1061 labelEntry = new Entry(); 1062 1063 auto labelText = mixin("fig." ~ xy ~ "AxisText"); 1064 string flattened; 1065 1066 foreach(i, elem; labelText) { 1067 elem = elem.replace(r"\", r"\\").replace(",", r"\,"); 1068 flattened ~= elem; 1069 if(i < labelText.length - 1) { 1070 flattened ~= ", "; 1071 } 1072 } 1073 1074 labelEntry.setText(flattened); 1075 labelEntry.setSizeRequest(400, locEntry.getHeight()); 1076 labelEntry.setActivatesDefault(1); 1077 labelBox.add(labelEntry); 1078 content.add(labelBox); 1079 1080 rotateButton = new CheckButton("Rotate Label Text"); 1081 immutable rotated = mixin("fig.rotated" ~ upperXY ~ "Tick()"); 1082 rotateButton.setActive(cast(int) rotated); 1083 1084 gridLineButton = new CheckButton("Grid Lines"); 1085 immutable grid = mixin("fig." ~ grid ~ "Grid()"); 1086 gridLineButton.setActive(cast(int) grid); 1087 1088 content.add(new HSeparator()); 1089 auto checkHbox = new HBox(0, 5); 1090 checkHbox.add(rotateButton); 1091 checkHbox.add(gridLineButton); 1092 1093 auto intensLabel = new Label("Gridline Intensity (0-255)"); 1094 intensLabel.setJustify(GtkJustification.RIGHT); 1095 checkHbox.add(intensLabel); 1096 1097 gridEntry = new Entry(); 1098 gridEntry.setMaxLength(3); 1099 gridEntry.setWidthChars(3); 1100 gridEntry.setText(to!string(fig.gridIntensity())); 1101 checkHbox.add(gridEntry); 1102 content.add(checkHbox); 1103 1104 this.addButtons([StockID.OK, StockID.CANCEL], 1105 [GtkResponseType.OK, 1106 GtkResponseType.CANCEL] 1107 ); 1108 this.setDefaultResponse(GtkResponseType.OK); 1109 this.addButtons(["Default"], [cast(GtkResponseType) 1]); 1110 this.setResizable(0); 1111 } 1112 } 1113 1114 /**Default plot window. It's a subclass of either Window or MainWindow 1115 * depending on the template parameter. 1116 */ 1117 template DefaultPlotWindow(Base) 1118 if(is(Base == gtk.Window.Window) || is(Base == gtk.MainWindow.MainWindow)) { 1119 1120 /// 1121 class DefaultPlotWindow : Base { 1122 private: 1123 FigureWidget widget; 1124 Menu rightClickMenu; 1125 1126 static immutable string[9] saveTypes = 1127 ["*.png", "*.bmp", "*.tiff", "*.jpg", "*.jpeg", "*.eps", 1128 "*.pdf", "*.svg", "*.svgz"]; 1129 1130 // Based on using print statements to figure it out. If anyone can 1131 // find the right documentation and wants to convert this to a proper 1132 // enum, feel free. 1133 enum rightClick = 3; 1134 1135 bool isValidExt(string ext) { 1136 foreach(t; saveTypes) { 1137 if(ext == t[2..$]) { 1138 return true; 1139 } 1140 } 1141 1142 return false; 1143 } 1144 1145 Menu buildRightClickMenu() { 1146 auto ret = new Menu(); 1147 1148 auto saveItem = new MenuItem(&popupSaveDialog, "_Save..."); 1149 ret.append(saveItem); 1150 1151 auto labelItem = new MenuItem(&popupLabelDialog, "_Labels..."); 1152 ret.append(labelItem); 1153 1154 auto legendItem = new MenuItem(&popupLegendDialog, "Le_gend..."); 1155 ret.append(legendItem); 1156 1157 auto zoomItem = new MenuItem(&popupZoomDialog, "_Zoom..."); 1158 ret.append(zoomItem); 1159 1160 ret.append(new SeparatorMenuItem()); 1161 auto xTickItem = new MenuItem(&popupTickDialog!'x', "_X Ticks..."); 1162 auto yTickItem = new MenuItem(&popupTickDialog!'y', "_Y Ticks..."); 1163 ret.append(xTickItem); 1164 ret.append(yTickItem); 1165 ret.append(new SeparatorMenuItem()); 1166 1167 auto fontSubmenu = new Menu(); 1168 fontSubmenu.append( new MenuItem(&doFont!"titleFont", "_Title")); 1169 fontSubmenu.append( new MenuItem(&doFont!"xLabelFont", "_X Label")); 1170 fontSubmenu.append( new MenuItem(&doFont!"yLabelFont", "_Y Label")); 1171 fontSubmenu.append( new MenuItem(&doFont!"axesFont", "_Axes")); 1172 fontSubmenu.append( new MenuItem(&doFont!"legendFont", "_Legend")); 1173 1174 ret.appendSubmenu("_Fonts", fontSubmenu); 1175 1176 ret.showAll(); 1177 return ret; 1178 } 1179 1180 void doFont(string which)(MenuItem menuItem) { 1181 auto fb = widget.figure; 1182 1183 auto sp = cast(Subplot) fb; 1184 if(sp) { 1185 auto zoomed = sp.zoomedFigure; 1186 if(zoomed) { 1187 fb = zoomed; 1188 } 1189 } 1190 1191 static if(which == "axesFont" || which == "legendFont") { 1192 auto toChange = cast(Figure) fb; 1193 if(!toChange) { 1194 errorMessage("Can't change axes, legend fonts on a Subplot."); 1195 return; 1196 } 1197 } else { 1198 alias fb toChange; 1199 } 1200 1201 auto dialog = new FontSelectionDialog(which); 1202 auto oldFont = mixin("toChange." ~ which); 1203 dialog.setFontName(text(oldFont.name, ' ', oldFont.size)); 1204 1205 void doChanges(int responseID, Dialog d) { 1206 if(responseID != GtkResponseType.OK) { 1207 return; 1208 } 1209 1210 auto newName = dialog.getFontName(); 1211 auto ns = newName.split(); 1212 enforce(ns.length >= 2); 1213 auto baseName = join(ns[0..$ - 1], " "); 1214 auto size = to!double(ns[$ - 1]); 1215 auto newFont = getFont(baseName, size); 1216 mixin("toChange." ~ which ~ "(newFont);"); 1217 } 1218 1219 dialog.addOnResponse(&doChanges); 1220 dialog.run(); 1221 dialog.destroy(); 1222 1223 widget.queueDraw(); 1224 } 1225 1226 void popupTickDialog(char xy)(MenuItem menuItem) { 1227 auto fb = widget.figure; 1228 auto sp = cast(Subplot) fb; 1229 1230 Figure fig; 1231 if(sp) { 1232 fig = cast(Figure) sp.zoomedFigure; 1233 } else { 1234 fig = cast(Figure) fb; 1235 } 1236 1237 if(!fig) { 1238 errorMessage("Cannot change " ~ xy ~ " ticks on a subplot."); 1239 return; 1240 } 1241 1242 auto dialog = new TickDialog!xy(fig); 1243 1244 void changeTicks(int responseID, Dialog dummy) { 1245 if(responseID == GtkResponseType.CANCEL) { 1246 dialog.destroy(); 1247 return; 1248 } 1249 1250 auto rotation = cast(bool) dialog.rotateButton.getActive(); 1251 auto grid = cast(bool) dialog.gridLineButton.getActive(); 1252 1253 ubyte gridIntens; 1254 try { 1255 gridIntens = to!ubyte(dialog.gridEntry.getText()); 1256 } catch(ConvException) { 1257 errorMessage("Grid intensity must be a numeric, 0-255."); 1258 return; 1259 } 1260 1261 enum upperXY = dialog.upperXY; 1262 enum gridStr = dialog.grid; 1263 mixin("fig.rotated" ~ upperXY ~ "Tick(rotation);"); 1264 mixin("fig." ~ gridStr ~ "Grid(grid);"); 1265 fig.gridIntensity(gridIntens); 1266 1267 if(responseID == 1) { // Set to default. 1268 mixin("fig.default" ~ upperXY ~ "Tick();"); 1269 dialog.destroy(); 1270 queueDraw(); 1271 return; 1272 } 1273 1274 double[] locations; 1275 auto locText = dialog.locEntry.getText(); 1276 auto locSplit = splitEscape(locText); 1277 try { 1278 locations = to!(double[])(locSplit); 1279 } catch(ConvException) { 1280 errorMessage("Locations must be numeric."); 1281 return; 1282 } 1283 1284 if(!filter!(not!isFinite)(locations).empty) { 1285 errorMessage("Locations must be finite, not NaN or infinity."); 1286 return; 1287 } 1288 1289 1290 auto labelText = dialog.labelEntry.getText(); 1291 if(labelText.strip().length == 0) { 1292 labelText = locText; 1293 } 1294 1295 auto labels = splitEscape(labelText); 1296 if(labels.length != locations.length) { 1297 errorMessage("Locations and labels must be same length."); 1298 return; 1299 } 1300 1301 mixin("fig." ~ xy ~ "TickLabels(locations, labels);"); 1302 dialog.destroy(); 1303 queueDraw(); 1304 } 1305 1306 dialog.addOnResponse(&changeTicks); 1307 dialog.showAll(); 1308 dialog.run(); 1309 } 1310 1311 void popupLegendDialog(MenuItem menuItem) { 1312 auto fb = widget.figure; 1313 auto sp = cast(Subplot) fb; 1314 1315 Figure fig; 1316 if(sp) { 1317 fig = cast(Figure) sp.zoomedFigure; 1318 } else { 1319 fig = cast(Figure) fb; 1320 } 1321 1322 if(!fig) { 1323 errorMessage("Cannot change legend on a subplot."); 1324 return; 1325 } 1326 1327 auto dialog = new LegendDialog(fig); 1328 1329 void changeLegend(int responseID, Dialog dummy) { 1330 if(responseID != GtkResponseType.OK) { 1331 return; 1332 } 1333 1334 foreach(i, plot; fig.plotData) if(plot.hasLegend()) { 1335 auto entryText = dialog.entries[i].getText(); 1336 if(entryText == "\0") entryText = ""; 1337 plot.legendText(entryText); 1338 } 1339 1340 if(dialog.topRadio.getActive()) { 1341 fig.legendLocation(LegendLocation.top); 1342 } else if(dialog.bottomRadio.getActive()) { 1343 fig.legendLocation(LegendLocation.bottom); 1344 } else if(dialog.leftRadio.getActive()) { 1345 fig.legendLocation(LegendLocation.left); 1346 } else if(dialog.rightRadio.getActive()) { 1347 fig.legendLocation(LegendLocation.right); 1348 } else { 1349 assert(0); 1350 } 1351 1352 queueDraw(); 1353 } 1354 1355 dialog.addOnResponse(&changeLegend); 1356 dialog.showAll(); 1357 dialog.run(); 1358 dialog.destroy(); 1359 } 1360 1361 void popupLabelDialog(MenuItem menuItem) { 1362 auto dialog = new LabelDialog(widget); 1363 dialog.addOnResponse(&changeLabels); 1364 dialog.showAll(); 1365 dialog.run(); 1366 } 1367 1368 void errorMessage(string msg) { 1369 auto msgbox = new MessageDialog(this, 1370 GtkDialogFlags.DESTROY_WITH_PARENT, 1371 GtkMessageType.ERROR, 1372 GtkButtonsType.CLOSE, msg); 1373 msgbox.addOnResponse(&closeError); 1374 msgbox.run(); 1375 return; 1376 } 1377 1378 void subplotZoomError() { 1379 errorMessage("Cannot zoom to coordinates on a subplot."); 1380 } 1381 1382 void popupZoomDialog(MenuItem menuItem) { 1383 auto sp = cast(Subplot) widget.figure; 1384 if(sp && cast(Figure) sp.zoomedFigure is null) { 1385 subplotZoomError(); 1386 return; 1387 } 1388 1389 Figure fig; 1390 if(sp) { 1391 fig = cast(Figure) sp.zoomedFigure; // Already checked for null. 1392 } else { 1393 fig = cast(Figure) widget.figure; 1394 } 1395 assert(fig); 1396 1397 auto dialog = new ZoomDialog(fig); 1398 dialog.addOnResponse(&changeZoom); 1399 dialog.showAll(); 1400 dialog.run(); 1401 } 1402 1403 // Change labels in response to a label dialog ok. 1404 void changeLabels(int responseID, Dialog dialog) { 1405 if(responseID != GtkResponseType.OK) { 1406 dialog.destroy(); 1407 return; 1408 } 1409 1410 auto ldialog = cast(LabelDialog) dialog; 1411 enforce(ldialog); 1412 1413 auto fb = widget.figure; 1414 auto sp = cast(Subplot) fb; 1415 1416 if(sp) { 1417 auto zoomed = sp.zoomedFigure; 1418 if(zoomed) fb = zoomed; 1419 } 1420 1421 fb.title = ldialog.titleEntry.getText(); 1422 fb.xLabel = ldialog.xLabelEntry.getText(); 1423 fb.yLabel = ldialog.yLabelEntry.getText(); 1424 1425 widget.queueDraw(); 1426 dialog.destroy(); 1427 } 1428 1429 void changeZoom(int responseID, Dialog dialog) { 1430 auto zdialog = cast(ZoomDialog) dialog; 1431 enforce(zdialog); 1432 1433 auto fb = widget.figure; 1434 Figure fig; 1435 auto sp = cast(Subplot) fb; 1436 1437 if(sp) { 1438 auto zoomed = cast(Figure) sp.zoomedFigure; 1439 if(zoomed) { 1440 fig = zoomed; 1441 } else { 1442 subplotZoomError(); 1443 return; 1444 } 1445 } else { 1446 fig = cast(Figure) fb; 1447 enforce(fig); 1448 } 1449 1450 if(responseID == 1) { 1451 fig.defaultZoom(); 1452 } else if(responseID == GtkResponseType.OK) { 1453 double newXMin, newYMin, newXMax, newYMax; 1454 try { 1455 newXMin = to!double(zdialog.leftEntry.getText().strip()); 1456 newXMax = to!double(zdialog.rightEntry.getText().strip()); 1457 newYMin = to!double(zdialog.bottomEntry.getText().strip()); 1458 newYMax = to!double(zdialog.topEntry.getText().strip()); 1459 } catch(ConvException) { 1460 errorMessage("Limits must be numeric."); 1461 return; 1462 } 1463 1464 if(newXMin >= newXMax) { 1465 errorMessage("X Min must be less than X Max."); 1466 return; 1467 } 1468 1469 if(newYMin >= newYMax) { 1470 errorMessage("Y Min must be less than Y Max."); 1471 return; 1472 } 1473 1474 if(!isFinite(newYMin) || !isFinite(newYMax) || 1475 !isFinite(newXMin) || !isFinite(newXMax)) { 1476 errorMessage("Limits must be finite, not infinity or NaN."); 1477 return; 1478 } 1479 1480 fig.xLim(newXMin, newXMax); 1481 fig.yLim(newYMin, newYMax); 1482 } 1483 1484 dialog.destroy(); 1485 widget.queueDraw(); 1486 } 1487 1488 void closeError(int response, Dialog d) { 1489 enforce(response == GtkResponseType.CLOSE); 1490 d.destroy(); 1491 } 1492 1493 void fileError(string eString) { 1494 errorMessage("File could not be successfully written. " ~ eString); 1495 } 1496 1497 // Bring up menu on right click. 1498 bool clickEvent(Event event, Widget widget) { 1499 if(event.button.button != rightClick) { 1500 return false; 1501 } 1502 1503 rightClickMenu.popup(null, null, null, null, rightClick, 1504 gtk_get_current_event_time()); 1505 1506 return true; 1507 } 1508 1509 void saveDialogResponse(int response, Dialog d) { 1510 auto fc = cast(FileChooserDialog) d; 1511 assert(fc); 1512 1513 if(response != GtkResponseType.OK) { 1514 d.destroy(); 1515 return; 1516 } 1517 1518 string name = fc.getFilename(); 1519 auto ext = toLower(extensionNoDot(name)); 1520 1521 string fileType; 1522 if(isValidExt(ext)) { 1523 fileType = ext; 1524 } else { 1525 fileType = fc.getFilter().getName(); 1526 name ~= '.'; 1527 name ~= fileType; 1528 } 1529 1530 try { 1531 widget.figure.saveToFile 1532 (name, fileType, widget.getWidth, widget.getHeight); 1533 } catch(Exception e) { 1534 fileError(e.toString()); 1535 } 1536 1537 d.destroy(); 1538 } 1539 1540 void popupSaveDialogImpl(MenuItem menuItem) { 1541 auto fc = new FileChooserDialog("Save plot...", this, GtkFileChooserAction.SAVE); 1542 fc.setDoOverwriteConfirmation(1); // Why isn't this the default? 1543 fc.addOnResponse(&saveDialogResponse); 1544 1545 foreach(ext; saveTypes) { 1546 auto filter = new FileFilter(); 1547 filter.setName(ext[2..$]); 1548 filter.addPattern(ext); 1549 fc.addFilter(filter); 1550 } 1551 1552 fc.run(); 1553 } 1554 1555 void popupSaveDialog(MenuItem menuItem) { 1556 popupSaveDialogImpl(menuItem); 1557 } 1558 1559 public: 1560 /// 1561 this(FigureWidget widget) { 1562 super("Plot Window. Right-click to save plot."); 1563 this.widget = widget; 1564 this.add(widget); 1565 widget.setSizeRequest( 1566 widget.figure.defaultWindowWidth, 1567 widget.figure.defaultWindowHeight 1568 ); 1569 this.resize(widget.getWidth, widget.getHeight); 1570 this.setSizeRequest( 1571 widget.figure.minWindowWidth, 1572 widget.figure.minWindowHeight 1573 ); 1574 1575 this.addOnButtonPress(&clickEvent); 1576 this.rightClickMenu = buildRightClickMenu(); 1577 1578 widget.addOnSizeAllocate(&widget.parentSizeChanged); 1579 widget.showAll(); 1580 widget.queueDraw(); 1581 this.showAll(); 1582 } 1583 } 1584 } 1585 1586 }