1 /**This file contains the DFL-specific parts of Plot2Kill and is publicly
2  * imported by plot2kill.figure if compiled with -version=dfl.
3  *
4  * Note that since DFL was the first library that Plot2Kill was ported to,
5  * it is considered the native lib and most stuff in this module maps cleanly
6  * and directly to DFL.
7  */
8 module plot2kill.dflwrapper;
9 
10 
11 version(dfl) {
12 
13 import dfl.all, dfl.internal.utf, dfl.internal.winapi;
14 
15 import plot2kill.util;
16 import plot2kill.png;
17 import plot2kill.guiagnosticbase;
18 import plot2kill.figure;
19 
20 /**DFL's implementation of a color object.*/
21 alias dfl.drawing.Color Color;
22 
23 /**DFL's implementation of a line drawing object.*/
24 alias dfl.drawing.Pen Pen;
25 
26 /**DFL's implementation of a filled area drawing object.*/
27 alias dfl.drawing.Brush Brush;
28 
29 /**DFL's object for representing a point.*/
30 alias dfl.drawing.Point Point;
31 
32 /**DFL's implementation of a rectangle object.*/
33 alias dfl.drawing.Rect Rect;
34 
35 /**DFL's size object.*/
36 alias dfl.drawing.Size Size;
37 
38 /**DFL's font class.*/
39 alias dfl.drawing.Font Font;
40 
41 /**Get a color in a GUI framework-agnostic way.*/
42 Color getColor(ubyte red, ubyte green, ubyte blue) {
43     return Color(red, green, blue);
44 }
45 
46 /**Get a font in a GUI framework-agnostic way.*/
47 Font getFont(string fontName, int size) {
48     return new Font(fontName, size);
49 }
50 
51 ///
52 enum TextAlignment {
53     ///
54     Left = 0,
55 
56     ///
57     Center = 1,
58 
59     ///
60     Right = 2
61 }
62 
63 // This calls the relevant lib's method of cleaning up the given object, if
64 // any.
65 void doneWith(T)(T garbage) {
66     static if(is(typeof(garbage.dispose()))) {
67         garbage.dispose();
68     }
69 
70     static if(is(T : Brush) || is(T : Pen)) {
71         // Having too many brushes and pens active seems to wreak havok on DFL.
72         delete garbage;
73     }
74 }
75 
76 /**The DFL-specific parts of the Figure class.  These include wrappers around
77  * the subset of drawing functionality used by Plot2Kill.
78  */
79 class FigureBase : GuiAgnosticBase {
80 private:
81     // This is indexed via a TextAlignment enum.
82     TextFormat[3] textAlignments;
83 
84     Rect roundedRect(double x, double y, double width, double height) {
85         // This code is designed to make sure the right/bottom of the rectangle
86         // always ends up as close as possible to where it was intended to,
87         // even if rounding fscks up the x/y coordinate, and to favor making
88         // the rectangle too big instead of too small since too big looks
89         // less bad.
90 
91         // Round down to make rectangle wider than it should be since overshoot
92         // is less severe aesthetically than undershoot.
93         immutable intX = to!int(x);
94         immutable intY = to!int(y);
95 
96         immutable endX = x + width;
97         immutable endY = y + height;
98 
99         // Since overshoot looks better than undershoot, we want the minimum
100         // value of width and height that will not give undershoot.
101         immutable intWidth = to!int(ceil(endX - intX));
102         immutable intHeight = to!int(ceil(endY - intY));
103 
104         return Rect(intX, intY, intWidth, intHeight);
105     }
106 
107     static Font getRotated(Font font) {
108         LogFont lf;
109 
110         lf.faceName = font.name;
111 
112         lf.lf.lfEscapement  = 900;
113         lf.lf.lfOrientation = 900;
114 
115         lf.lf.lfOutPrecision = OUT_DEFAULT_PRECIS;
116         lf.lf.lfClipPrecision = CLIP_DEFAULT_PRECIS;
117         lf.lf.lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE;
118 
119         return new Font(lf, font.size, font.style, font.unit);
120     }
121 
122 protected:
123     // Fonts tend to be different actual sizes on different GUI libs for a
124     // given nominal size. This adjusts for that factor when setting default
125     // fonts.
126     enum fontSizeAdjust = -3;
127 
128     Graphics context;
129 
130     this() {
131         textAlignments[TextAlignment.Left] = new TextFormat;
132         textAlignments[TextAlignment.Left].alignment
133             = dfl.drawing.TextAlignment.LEFT;
134 
135         textAlignments[TextAlignment.Center] = new TextFormat;
136         textAlignments[TextAlignment.Center].alignment
137             = dfl.drawing.TextAlignment.CENTER;
138 
139         textAlignments[TextAlignment.Right] = new TextFormat;
140         textAlignments[TextAlignment.Right].alignment
141             = dfl.drawing.TextAlignment.RIGHT;
142     }
143 
144 
145 public:
146 
147     // Wrappers around DFL's drawing functionality.  Other ports should have
148     // wrappers with the same compile-time interface.  These are final both
149     // to avoid virtual call overhead and because overriding them would be
150     // rather silly.
151     final void drawLine
152     (Pen pen, double startX, double startY, double endX, double endY) {
153         context.drawLine(pen,
154             roundTo!int(startX + xOffset), roundTo!int(startY + yOffset),
155             roundTo!int(endX + xOffset), roundTo!int(endY + yOffset)
156         );
157     }
158 
159     final void drawLine(Pen pen, PlotPoint start, PlotPoint end) {
160         this.drawLine(pen, start.x, start.y, end.x, end.y);
161     }
162 
163     final void drawRectangle
164     (Pen pen, double x, double y, double width, double height) {
165         auto r = roundedRect(x + xOffset, y + yOffset, width, height);
166         context.drawRectangle
167             (pen, r.x , r.y , r.width, r.height);
168     }
169 
170     final void drawRectangle(Pen pen, PlotRect r) {
171         this.drawRectangle(pen, r.x, r.y, r.width, r.height);
172     }
173 
174     final void fillRectangle
175     (Brush brush, double x, double y, double width, double height) {
176         auto r = roundedRect(x + xOffset, y + yOffset, width, height);
177         context.fillRectangle
178             (brush, r.x, r.y, r.width, r.height);
179     }
180 
181     final void fillRectangle(Brush brush, PlotRect r) {
182         this.fillRectangle(brush, r.x, r.y, r.width, r.height);
183     }
184 
185     final void drawText(
186         string text,
187         Font font,
188         Color pointColor,
189         PlotRect rect,
190         TextAlignment alignment
191     ) {
192         auto offsetRect = Rect(
193             roundTo!int(rect.x + xOffset),
194             roundTo!int(rect.y + yOffset),
195             roundTo!int(rect.width),
196             roundTo!int(rect.height)
197         );
198         context.drawText(
199             text, font, pointColor, offsetRect, textAlignments[alignment]);
200     }
201 
202     final void drawRotatedText(
203         string text,
204         Font font,
205         Color pointColor,
206         PlotRect rect,
207         TextAlignment alignment
208     ) {
209         auto rfont = getRotated(font);
210         scope(exit) doneWith(rfont);
211 
212         auto meas = measureText(text, font);
213         auto slack = max(0, rect.height - meas.width);
214         double toAdd;
215         if(alignment == TextAlignment.Center) {
216             toAdd = slack / 2;
217         } else if(alignment == TextAlignment.Right) {
218             toAdd = slack;
219         }
220 
221         // The rotated text patch is buggy, and will try to wrap words
222         // when it shouldn't.  Make width huge to effectively disable
223         // wrapping.  Also, DFL's Y coord is for the bottom, not the top.
224         // Fix this.
225         auto rect2 = PlotRect(
226             rect.x, rect.y + meas.width + toAdd,
227             10 * this.width, rect.width);
228         drawText(text, rfont, getColor(0, 0, 0), rect2);
229     }
230 
231     final void drawText(
232         string text,
233         Font font,
234         Color pointColor,
235         PlotRect rect
236     ) {
237         auto offsetRect = Rect(
238             roundTo!int(rect.x + xOffset),
239             roundTo!int(rect.y + yOffset),
240             roundTo!int(rect.width), roundTo!int(rect.height)
241         );
242         context.drawText(text, font, pointColor, offsetRect);
243     }
244 
245     final Size measureText
246     (string text, Font font, double maxWidth, TextAlignment alignment) {
247         return context.measureText(text, font, roundTo!int(maxWidth),
248             textAlignments[alignment]);
249     }
250 
251     final Size measureText
252     (string text, Font font, TextAlignment alignment) {
253         return
254             context.measureText(text, font, textAlignments[alignment]);
255     }
256 
257     final Size measureText(string text, Font font, double maxWidth) {
258         return context.measureText(text, font, roundTo!int(maxWidth));
259     }
260 
261     final Size measureText(string text, Font font) {
262         return context.measureText(text, font);
263     }
264 
265     // TODO:  Add support for stuff other than solid brushes.
266 
267     /**Get a brush in a GUI framework-agnostic way.*/
268     final Brush getBrush(Color color) {
269         return new SolidBrush(color);
270     }
271 
272     /**Get a pen in a GUI framework-agnostic way.*/
273     final Pen getPen(Color color, double width = 1) {
274         return new Pen(color, max(roundTo!int(width), 1));
275     }
276 
277     void drawTo(Graphics context) {
278         drawTo(context, this.width, this.height);
279     }
280 
281     // Weird function overloading bugs.  This should be removed.
282     void drawTo(Graphics context, double width, double height) {
283         return drawTo(context, PlotRect(0, 0, width, height));
284     }
285 
286     // Allows drawing at an offset from the origin.
287     void drawTo(Graphics context, PlotRect whereToDraw) {
288         enforceSane(whereToDraw);
289         // Save the default class-level values, make the values passed in the
290         // class-level values, call draw(), then restore the default values.
291         auto oldContext = this.context;
292         auto oldWidth = this._width;
293         auto oldHeight = this._height;
294         auto oldXoffset = this.xOffset;
295         auto oldYoffset = this.yOffset;
296 
297         scope(exit) {
298             this.context = oldContext;
299             this._height = oldHeight;
300             this._width = oldWidth;
301             this.xOffset = oldXoffset;
302             this.yOffset = oldYoffset;
303         }
304 
305         this.context = context;
306         this._width = whereToDraw.width;
307         this._height = whereToDraw.height;
308         this.xOffset = whereToDraw.x;
309         this.yOffset = whereToDraw.y;
310         drawImpl();
311     }
312 
313     /**Saves this figure to a file.  The file type can be one of the
314      * raster formats .png or .bmp.  Saving to vector formats will likely
315      * never be supported on DFL because DFL's drawing backend is GDI, which is
316      * inherently raster-based.  The width and height parameters allow you to
317      * specify explicit width and height parameters for the image file.  If
318      * width and height are left at their default values
319      * of 0, the default width and height of the subclass being saved will
320      * be used.
321      *
322      * Note:  The width and height parameters are provided as doubles for
323      *        consistency with backends that support vector formats.  These
324      *        are simply rounded to the nearest integer for the DFL backend.
325      */
326     void saveToFile
327     (string filename, string type, double width = 0, double height = 0) {
328         // User friendliness:  Remove . if it was included, don't be case sens.
329         type = toLower(type);
330         if(!type.empty && type.front == '.') {
331             type.popFront();
332         }
333 
334         // Check this stuff upfront before we allocate a bunch of resources.
335         enforce(type == "bmp" || type == "png",
336             "Don't now how to save a " ~ type ~ " file on the DFL backend.");
337 
338         enforce(width >= 0 && height >= 0,
339             "Can't save an image w/ negative or NaN width or height.");
340 
341         if(width == 0 || height == 0) {
342             width = this.defaultWindowWidth;
343             height = this.defaultWindowHeight;
344         }
345 
346         immutable iWidth = roundTo!int(width);
347         immutable iHeight = roundTo!int(height);
348 
349         auto graphics = new MemoryGraphics(iWidth, iHeight);
350         scope(exit) doneWith(graphics);
351 
352         this.drawTo(graphics, iWidth, iHeight);
353         File handle = File(filename, "wb");
354         scope(exit) handle.close();
355 
356         auto pix = getPixels(graphics);
357         scope(exit) free(cast(void*) pix.ptr);
358 
359         if(type == "bmp") {
360             writeBitmap(pix, handle, iWidth, iHeight);
361         } else if(type == "png") {
362             writePngFromBitmapPixels(pix, handle, iWidth, iHeight);
363         } else {
364             assert(0);   // Already validated input at beginning of function.
365         }
366     }
367 
368     /**Convenience function that infers the type from the filename extenstion
369      * and defaults to .png if no valid file format extension is found.
370      */
371     void saveToFile(string filename, double width = 0, double height = 0) {
372         auto type = toLower(extensionNoDot(filename));
373 
374         if(type != "bmp") {
375             // Default to png.
376             type = "png";
377         }
378         saveToFile(filename, type, width, height);
379     }
380 
381     ///
382     FigureControl toControl() {
383         return new FigureControl(this);
384     }
385 
386     ///
387     void showAsMain() {
388         Application.run(new DefaultPlotWindow(this.toControl));
389     }
390 }
391 
392 // Used for scatter plots.  Efficiently draws a single character in a lot of
393 // places, centered on a point.  ASSUMPTION:  No drawing commands not related
394 // to drawing scatter plot points are issued between when initialize()
395 // and reset() are called.
396 package struct ScatterCharDrawer {
397 private:
398     string str;
399     PlotSize measurements;
400     Figure fig;
401     Font font;
402     Color color;
403 
404 public:
405     this(dchar c, Font font, Color color, Figure fig) {
406         str = to!string(c);
407         this.fig = fig;
408         this.font = font;
409         this.color = color;
410 
411         auto measInt = fig.measureText(str, font);
412         measurements = PlotSize(measInt.width, measInt.height);
413     }
414 
415     void draw(PlotPoint where) {
416         if(!fig.insideAxes(where)) return;
417         fig.drawText(str, font, color,
418             PlotRect(where.x - measurements.width * 0.5,
419                      where.y - measurements.height * 0.5,
420                      measurements.width,
421                      measurements.height
422             )
423         );
424     }
425 
426     // Dummy function, but other GUI libs require something here.
427     void initialize() {}
428 
429     // Dummy function, but other GUI libs require something here.
430     void restore() {}
431 }
432 
433 // Fudge factors for the space that window borders take up.
434 //
435 // TODO:
436 // Figure out how to get the actual numbers and use them instead of these
437 // stupid fudge factors, though I couldn't find any API in DFL to do that.
438 private enum verticalBorderSize = 38;
439 private enum horizontalBorderSize = 16;
440 
441 class FigureControl : PictureBox {
442 private:
443     FigureBase _figure;
444 
445 package:
446     this(FigureBase fig) {
447         this._figure = fig;
448         this.size = Size(fig.minWindowWidth, fig.minWindowHeight);
449     }
450 
451     void parentResize(Control c, EventArgs ea) {
452 
453 
454         immutable pwid = parent.width - horizontalBorderSize;
455         immutable pheight = parent.height - verticalBorderSize;
456 
457         this.size = Size(parent.width - horizontalBorderSize,
458                          parent.height - verticalBorderSize);
459 
460         draw();
461     }
462 
463 public:
464     /// Event handler to redraw the figure.
465     void drawFigureEvent(Control c, EventArgs ea) {
466         draw();
467     }
468 
469     /**Get the underlying FigureBase object.*/
470     final FigureBase figure() @property {
471         return _figure;
472     }
473 
474     ///
475     void draw() {
476         this.setBounds(0, 0, this.width, this.height);
477         auto context = new MemoryGraphics(this.width, this.height);
478 
479         figure.drawTo(context, this.width, this.height);
480         auto bmp = context.toBitmap;
481         this.image = bmp;
482     }
483 }
484 
485 ///
486 class DefaultPlotWindow : Form {
487 private:
488     FigureControl control;
489     enum string fileFilter = "PNG files (*.png)|*.png|BMP files (*.bmp)|*.bmp";
490 
491     // Why does the save dialog use 1 indexing instead of zero indexing?
492     static immutable string[3] types =
493         ["dummy", "png", "bmp"];
494 
495     void errDialog(string eString) {
496         msgBox("File could not be saved successfully.  " ~ eString);
497 
498     }
499 
500     // Brings up a save menu when the window is right clicked on.
501     void rightClickSave(Control c, MouseEventArgs ea) {
502         if(ea.button != MouseButtons.RIGHT) {
503             return;
504         }
505 
506         auto dialog = new SaveFileDialog;
507         dialog.overwritePrompt = true;
508         dialog.filter = fileFilter;
509         dialog.validateNames = true;
510         dialog.showDialog(this);
511 
512         // For now the only choice is bmp.  Eventually we hope to support png.
513         if(dialog.fileName.length == 0) {
514             // User hit cancel.
515             return;
516         }
517 
518         auto filename = dialog.fileName;
519         auto ext = toLower(extensionNoDot(filename));
520         string type;
521         if(ext == "png" || ext == "bmp") {
522             type = ext;
523         } else {
524             type = types[dialog.filterIndex];
525             filename ~= '.';
526             filename ~= type;
527         }
528 
529         try {
530             control.figure.saveToFile(
531                 filename,
532                 control.width,
533                 control.height
534             );
535         } catch(Exception e) {
536             errDialog(e.toString());
537         }
538     }
539 
540 public:
541     ///
542     this(FigureControl control) {
543         control.dock = DockStyle.FILL;
544         this.control = control;
545         control.size = Size(
546             control.figure.defaultWindowWidth,
547             control.figure.defaultWindowHeight
548         );
549 
550         this.size = Size(
551             control.width + horizontalBorderSize,
552             control.height + verticalBorderSize
553         );
554         this.minimumSize =
555             Size(control.figure.minWindowWidth + horizontalBorderSize,
556                  control.figure.minWindowHeight + verticalBorderSize);
557 
558         this.resize ~= &control.parentResize;
559         this.activated ~= &control.drawFigureEvent;
560         control.mouseDown ~= &rightClickSave;
561         control.parent = this;
562         control.bringToFront();
563     }
564 }
565 
566 private:
567 // This stuff is an attempt at providing support for saving DFL plots to
568 // bitmaps.  It borrows from Tomasz Stachowiak's excellent DirectBitmap code,
569 // which was licensed under the also excellent WTFPL.
570 
571 import dfl.internal.winapi;
572 import std.c.stdlib : malloc, free;
573 
574 // Get the bitmap as an array of pixels.  Returns on the C heap b/c it's a
575 // private function that allodates huge buffers with trivial lifetimes, so
576 // we want to free them immediately.
577 Pixel[] getPixels(MemoryGraphics graphics) {
578     // Calculate bitmap padding.  Bitmaps require the number of bytes per line
579     // to be divisible by 4.
580     int paddingBytes;
581     while((paddingBytes + graphics.width * 3) % 4 > 0) {
582         paddingBytes++;
583     }
584 
585     immutable len = graphics.height * (graphics.width * 3 + paddingBytes);
586     auto pixels = (cast(byte*) malloc(len))[0..len];
587 
588 	BITMAPINFO	bitmapInfo;
589     with (bitmapInfo.bmiHeader) {
590         biSize = bitmapInfo.bmiHeader.sizeof;
591         biWidth = graphics.width;
592         biHeight = graphics.height;
593         biPlanes = 1;
594         biBitCount = 24;
595         biCompression = BI_RGB;
596     }
597 
598     int result = GetDIBits(
599         graphics.handle,
600         graphics.hbitmap,
601         0,
602         graphics.height,
603         pixels.ptr,
604         &bitmapInfo,
605         DIB_RGB_COLORS
606     );
607 
608     enforce(result == graphics.height, "Reading bitmap pixels failed.");
609 
610     if(paddingBytes > 0) {
611         // Remove padding bits.
612         size_t toIndex, fromIndex;
613         foreach(row; 0..graphics.height) {
614             foreach(i; 0..graphics.width * 3) {
615                 pixels[toIndex++] = pixels[fromIndex++];
616             }
617 
618             fromIndex += paddingBytes;
619         }
620     }
621 
622     return (cast(Pixel*) pixels.ptr)[0..graphics.width * graphics.height];
623 }
624 
625 extern(Windows) int GetDIBits(
626   HDC hdc,           // handle to DC
627   HBITMAP hbmp,      // handle to bitmap
628   UINT uStartScan,   // first scan line to set
629   UINT cScanLines,   // number of scan lines to copy
630   LPVOID lpvBits,    // array for bitmap bits
631   LPBITMAPINFO lpbi, // bitmap data buffer
632   UINT uUsage        // RGB or palette index
633 );
634 
635 
636 enum UINT DIB_RGB_COLORS = 0;
637 enum UINT BI_RGB = 0;
638 
639 struct Pixel {
640 	align(1) {
641 		ubyte b, g, r;
642 	}
643 }
644 static assert (Pixel.sizeof == 3);
645 
646 
647 // Bugs:  Doesn't work at all.  I have no idea why.
648 version(none) {
649 /* This class contains an implementation of an ad-hoc message passing
650  * system that allows plots to be thrown up from any thread w/o
651  * an explicit main GUI window.  This is useful for apps that are
652  * basically console apps except for the plots they display.
653  */
654 private class ImplicitMain {
655 
656     __gshared static {
657         Form implicitMain;
658         Button implicitButton;
659         Form[] toShow;
660 
661         void initialize() {
662             synchronized(ImplicitMain.classinfo) {
663                 if(implicitMain !is null) {
664                     return;
665                 }
666 
667                 implicitMain = new Form;
668                // implicitMain.visible = false;
669 
670                 implicitButton = new Button;
671                 implicitButton.click ~= toDelegate(&showToShow);
672                 implicitButton.parent = implicitMain;
673             }
674 
675             static void doIt() {
676                 auto ac = new ApplicationContext;
677                 ac.mainForm = implicitMain;
678                 Application.run(ac);
679             }
680 
681            auto t = new Thread(&doIt);
682            t.isDaemon = true;
683            t.start;
684            doIt;
685         }
686 
687         void showToShow(Control c, EventArgs ea) {
688             synchronized(ImplicitMain.classinfo) {
689                 while(!toShow.empty) {
690                     toShow.back().show();
691                     toShow.popBack();
692                 }
693             }
694         }
695 
696         void addForm(Form form) {
697             synchronized(ImplicitMain.classinfo) {
698                 toShow ~= form;
699                 implicitButton.performClick();
700             }
701         }
702     }
703 }
704 }
705 
706 
707 }