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 }