1 // Written by Christopher E. Miller
2 // See the included license.txt for copyright and license details.
3 
4 module dfl.splitter;
5 import core.sys.windows.windows;
6 
7 import dfl.control;
8 import dfl.exception;
9 import dfl.base;
10 import dfl.drawing;
11 import dfl.event;
12 
13 class SplitterEventArgs : EventArgs {
14 
15    this(int x, int y, int splitX, int splitY) {
16       _x = x;
17       _y = y;
18       _splitX = splitX;
19       _splitY = splitY;
20    }
21 
22    final @property int x() {
23       return _x;
24    }
25 
26    final @property int y() {
27       return _y;
28    }
29 
30    final @property void splitX(int val) {
31       _splitX = val;
32    }
33 
34    final @property int splitX() {
35       return _splitX;
36    }
37 
38    final @property void splitY(int val) {
39       _splitY = val;
40    }
41 
42    final @property int splitY() {
43       return _splitY;
44    }
45 
46 private:
47    int _x, _y, _splitX, _splitY;
48 }
49 
50 class Splitter : Control {
51    this() {
52       // DMD 0.95: need 'this' to access member dock
53       this.dock = DockStyle.LEFT;
54 
55       if (HBRUSH.init == hbrxor) {
56          inithbrxor();
57       }
58    }
59 
60    /+
61    override @property void anchor(AnchorStyles a) {
62       throw new DflException("Splitter cannot be anchored");
63    }
64 
65    alias Control.anchor anchor; // Overload.
66    +/
67 
68    override @property void dock(DockStyle ds) {
69       switch (ds) {
70       case DockStyle.LEFT:
71       case DockStyle.RIGHT:
72          //cursor = new Cursor(LoadCursorA(null, IDC_SIZEWE), false);
73          cursor = Cursors.vSplit;
74          break;
75 
76       case DockStyle.TOP:
77       case DockStyle.BOTTOM:
78          //cursor = new Cursor(LoadCursorA(null, IDC_SIZENS), false);
79          cursor = Cursors.hSplit;
80          break;
81 
82       default:
83          throw new DflException("Invalid splitter dock");
84       }
85 
86       super.dock(ds);
87    }
88 
89    alias dock = Control.dock; // Overload.
90 
91    package void initsplit(int sx, int sy) {
92       capture = true;
93 
94       downing = true;
95       //downpos = Point(mea.x, mea.y);
96 
97       switch (dock) {
98       case DockStyle.TOP:
99       case DockStyle.BOTTOM:
100          downpos = sy;
101          lastpos = 0;
102          drawxorClient(0, lastpos);
103          break;
104 
105       default: // LEFT / RIGHT.
106          downpos = sx;
107          lastpos = 0;
108          drawxorClient(lastpos, 0);
109       }
110    }
111 
112    final void resumeSplit(int sx, int sy) { // package
113       if (Control.mouseButtons & MouseButtons.LEFT) {
114          initsplit(sx, sy);
115 
116          if (cursor) {
117             Cursor.current = cursor;
118          }
119       }
120    }
121 
122    //
123    final void resumeSplit() { // package
124       Point pt = pointToClient(Cursor.position);
125       return resumeSplit(pt.x, pt.y);
126    }
127 
128    @property void movingGrip(bool byes) {
129       if (mgrip == byes) {
130          return;
131       }
132 
133       this.mgrip = byes;
134 
135       if (created) {
136          invalidate();
137       }
138    }
139 
140    @property bool movingGrip() {
141       return mgrip;
142    }
143 
144    deprecated alias moveingGrip = movingGrip;
145    deprecated alias moveGrip = movingGrip;
146    deprecated alias sizingGrip = movingGrip;
147 
148    protected override void onPaint(PaintEventArgs ea) {
149       super.onPaint(ea);
150 
151       if (mgrip) {
152          ea.graphics.drawMoveGrip(displayRectangle, DockStyle.LEFT == dock
153             || DockStyle.RIGHT == dock);
154       }
155    }
156 
157    protected override void onResize(EventArgs ea) {
158       if (mgrip) {
159          invalidate();
160       }
161 
162       resize(this, ea);
163    }
164 
165    protected override void onMouseDown(MouseEventArgs mea) {
166       super.onMouseDown(mea);
167 
168       if (mea.button == MouseButtons.LEFT && 1 == mea.clicks) {
169          initsplit(mea.x, mea.y);
170       }
171    }
172 
173    protected override void onMouseMove(MouseEventArgs mea) {
174       super.onMouseMove(mea);
175 
176       if (downing) {
177          switch (dock) {
178          case DockStyle.TOP:
179          case DockStyle.BOTTOM:
180             drawxorClient(0, mea.y - downpos, 0, lastpos);
181             lastpos = mea.y - downpos;
182             break;
183 
184          default: // LEFT / RIGHT.
185             drawxorClient(mea.x - downpos, 0, lastpos, 0);
186             lastpos = mea.x - downpos;
187          }
188 
189          scope sea = new SplitterEventArgs(mea.x, mea.y, left, top);
190          onSplitterMoving(sea);
191       }
192    }
193 
194    protected override void onMove(EventArgs ea) {
195       super.onMove(ea);
196 
197       if (downing) { // ?
198          Point curpos = Cursor.position;
199          curpos = pointToClient(curpos);
200          scope sea = new SplitterEventArgs(curpos.x, curpos.y, left, top);
201          onSplitterMoved(sea);
202       }
203    }
204 
205    final Control getSplitControl() { // package
206       Control splat; // Splitted.
207       // DMD 0.95: need 'this' to access member dock
208       //switch(dock())
209       final switch (this.dock()) {
210       case DockStyle.LEFT:
211          foreach (Control ctrl; parent.controls()) {
212             if (DockStyle.LEFT != ctrl.dock) { //if(this.dock != ctrl.dock)
213                continue;
214             }
215             // DMD 0.95: overloads int(Object o) and int(Control ctrl) both match argument list for opEquals
216             //if(ctrl == this)
217             if (ctrl == cast(Control) this) {
218                return splat;
219             }
220             splat = ctrl;
221          }
222          break;
223 
224       case DockStyle.RIGHT:
225          foreach (Control ctrl; parent.controls()) {
226             if (DockStyle.RIGHT != ctrl.dock) { //if(this.dock != ctrl.dock)
227                continue;
228             }
229             // DMD 0.95: overloads int(Object o) and int(Control ctrl) both match argument list for opEquals
230             //if(ctrl == this)
231             if (ctrl == cast(Control) this) {
232                return splat;
233             }
234             splat = ctrl;
235          }
236          break;
237 
238       case DockStyle.TOP:
239          foreach (Control ctrl; parent.controls()) {
240             if (DockStyle.TOP != ctrl.dock) { //if(this.dock != ctrl.dock)
241                continue;
242             }
243             // DMD 0.95: overloads int(Object o) and int(Control ctrl) both match argument list for opEquals
244             //if(ctrl == this)
245             if (ctrl == cast(Control) this) {
246                return splat;
247             }
248             splat = ctrl;
249          }
250          break;
251 
252       case DockStyle.BOTTOM:
253          foreach (Control ctrl; parent.controls()) {
254             if (DockStyle.BOTTOM != ctrl.dock) { //if(this.dock != ctrl.dock)
255                continue;
256             }
257             // DMD 0.95: overloads int(Object o) and int(Control ctrl) both match argument list for opEquals
258             //if(ctrl == this)
259             if (ctrl == cast(Control) this) {
260                return splat;
261             }
262             splat = ctrl;
263          }
264          break;
265 
266       case DockStyle.FILL:
267          assert("DockStyle.FILL is not allowed in Splitter");
268          break;
269 
270       case DockStyle.NONE:
271          assert("DockStyle.NONE is not allowed in Splitter");
272          break;
273       }
274       return null;
275    }
276 
277    protected override void onMouseUp(MouseEventArgs mea) {
278       if (downing) {
279          capture = false;
280 
281          downing = false;
282 
283          if (mea.button != MouseButtons.LEFT) {
284             // Abort.
285             switch (dock) {
286             case DockStyle.TOP:
287             case DockStyle.BOTTOM:
288                drawxorClient(0, lastpos);
289                break;
290 
291             default: // LEFT / RIGHT.
292                drawxorClient(lastpos, 0);
293             }
294             super.onMouseUp(mea);
295             return;
296          }
297 
298          int adj, val, vx;
299          auto splat = getSplitControl(); // Splitted.
300          if (splat) {
301             // DMD 0.95: need 'this' to access member dock
302             //switch(dock())
303             switch (this.dock()) {
304             case DockStyle.LEFT:
305                drawxorClient(lastpos, 0);
306                //val = left - splat.left + mea.x - downpos.x;
307                val = left - splat.left + mea.x - downpos;
308                if (val < msize) {
309                   val = msize;
310                }
311                splat.width = val;
312                break;
313 
314             case DockStyle.RIGHT:
315                drawxorClient(lastpos, 0);
316                //adj = right - splat.left + mea.x - downpos.x;
317                adj = right - splat.left + mea.x - downpos;
318                val = splat.width - adj;
319                vx = splat.left + adj;
320                if (val < msize) {
321                   vx -= msize - val;
322                   val = msize;
323                }
324                splat.bounds = Rect(vx, splat.top, val, splat.height);
325                break;
326 
327             case DockStyle.TOP:
328                drawxorClient(0, lastpos);
329                //val = top - splat.top + mea.y - downpos.y;
330                val = top - splat.top + mea.y - downpos;
331                if (val < msize) {
332                   val = msize;
333                }
334                splat.height = val;
335                break;
336 
337             case DockStyle.BOTTOM:
338                drawxorClient(0, lastpos);
339                //adj = bottom - splat.top + mea.y - downpos.y;
340                adj = bottom - splat.top + mea.y - downpos;
341                val = splat.height - adj;
342                vx = splat.top + adj;
343                if (val < msize) {
344                   vx -= msize - val;
345                   val = msize;
346                }
347                splat.bounds = Rect(splat.left, vx, splat.width, val);
348                break;
349 
350             default:
351             }
352          }
353 
354          // This is needed when the moved control first overlaps the splitter and the splitter
355          // gets bumped over, causing a little area to not be updated correctly.
356          // I'll fix it someday.
357          parent.invalidate(true);
358 
359          // Event..
360       }
361 
362       super.onMouseUp(mea);
363    }
364 
365    /+
366    // Not quite sure how to implement this yet.
367    // Might need to scan all controls until one of:
368    //    Control with opposite dock (right if left dock): stay -mextra- away from it,
369    //    Control with fill dock: that control can't have less than -mextra- width,
370    //    Reached end of child controls: stay -mextra- away from the edge.
371 
372 
373    final @property void minExtra(int min) {
374       mextra = min;
375    }
376 
377 
378    final @property int minExtra() {
379       return mextra;
380    }
381    +/
382 
383    final @property void minSize(int min) {
384       msize = min;
385    }
386 
387    final @property int minSize() {
388       return msize;
389    }
390 
391    final @property void splitPosition(int pos) {
392       auto splat = getSplitControl(); // Splitted.
393       if (splat) {
394          // DMD 0.95: need 'this' to access member dock
395          //switch(dock())
396          switch (this.dock()) {
397          case DockStyle.LEFT:
398          case DockStyle.RIGHT:
399             splat.width = pos;
400             break;
401 
402          case DockStyle.TOP:
403          case DockStyle.BOTTOM:
404             splat.height = pos;
405             break;
406 
407          default:
408          }
409       }
410    }
411 
412    // -1 if not docked to a control.
413    final @property int splitPosition() {
414       auto splat = getSplitControl(); // Splitted.
415       if (splat) {
416          // DMD 0.95: need 'this' to access member dock
417          //switch(dock())
418          switch (this.dock()) {
419          case DockStyle.LEFT:
420          case DockStyle.RIGHT:
421             return splat.width;
422 
423          case DockStyle.TOP:
424          case DockStyle.BOTTOM:
425             return splat.height;
426 
427          default:
428          }
429       }
430       return -1;
431    }
432 
433    //SplitterEventHandler splitterMoved;
434    Event!(Splitter, SplitterEventArgs) splitterMoved;
435    //SplitterEventHandler splitterMoving;
436    Event!(Splitter, SplitterEventArgs) splitterMoving;
437 
438 protected:
439 
440    override @property Size defaultSize() {
441       //return Size(GetSystemMetrics(SM_CXSIZEFRAME), GetSystemMetrics(SM_CYSIZEFRAME));
442       int sx = GetSystemMetrics(SM_CXSIZEFRAME);
443       int sy = GetSystemMetrics(SM_CYSIZEFRAME);
444       // Need a bit extra room for the move-grips.
445       if (sx < 5) {
446          sx = 5;
447       }
448       if (sy < 5) {
449          sy = 5;
450       }
451       return Size(sx, sy);
452    }
453 
454    void onSplitterMoving(SplitterEventArgs sea) {
455       splitterMoving(this, sea);
456    }
457 
458    void onSplitterMoved(SplitterEventArgs sea) {
459       splitterMoving(this, sea);
460    }
461 
462 private:
463 
464    bool downing = false;
465    bool mgrip = true;
466    //Point downpos;
467    int downpos;
468    int lastpos;
469    int msize = 25; // Min size of control that's being sized from the splitter.
470    int mextra = 25; // Min size of the control on the opposite side.
471 
472    static HBRUSH hbrxor;
473 
474    static void inithbrxor() {
475       static ubyte[] bmbits = [
476          0xAA, 0, 0x55, 0, 0xAA, 0, 0x55, 0, 0xAA, 0, 0x55, 0, 0xAA, 0, 0x55, 0,
477       ];
478 
479       HBITMAP hbm;
480       hbm = CreateBitmap(8, 8, 1, 1, bmbits.ptr);
481       hbrxor = CreatePatternBrush(hbm);
482       DeleteObject(hbm);
483    }
484 
485    static void drawxor(HDC hdc, Rect r) {
486       SetBrushOrgEx(hdc, r.x, r.y, null);
487       HGDIOBJ hbrold = SelectObject(hdc, hbrxor);
488       PatBlt(hdc, r.x, r.y, r.width, r.height, PATINVERT);
489       SelectObject(hdc, hbrold);
490    }
491 
492    void drawxorClient(HDC hdc, int x, int y) {
493       POINT pt;
494       pt.x = x;
495       pt.y = y;
496       //ClientToScreen(handle, &pt);
497       MapWindowPoints(handle, parent.handle, &pt, 1);
498 
499       drawxor(hdc, Rect(pt.x, pt.y, width, height));
500    }
501 
502    void drawxorClient(int x, int y, int xold = int.min, int yold = int.min) {
503       HDC hdc;
504       //hdc = GetWindowDC(null);
505       hdc = GetDCEx(parent.handle, null, DCX_CACHE);
506 
507       if (xold != int.min) {
508          drawxorClient(hdc, xold, yold);
509       }
510 
511       drawxorClient(hdc, x, y);
512 
513       ReleaseDC(null, hdc);
514    }
515 }