PageRenderTime 51ms CodeModel.GetById 2ms app.highlight 42ms RepoModel.GetById 1ms app.codeStats 1ms

/src/NUnit/UiException/Controls/SplitterBox.cs

#
C# | 517 lines | 334 code | 96 blank | 87 comment | 35 complexity | 6b8b5d50ce51f3ff3895722f25470181 MD5 | raw file
  1// ****************************************************************
  2// This is free software licensed under the NUnit license. You may
  3// obtain a copy of the license at http://nunit.org
  4// ****************************************************************
  5
  6using System;
  7using System.Collections.Generic;
  8using System.Text;
  9using System.Windows.Forms;
 10using System.Drawing;
 11using NUnit.UiException.Properties;
 12using System.Diagnostics;
 13
 14//
 15// This re-implements SplitContainer. Why re-inventing the wheel?
 16// Well... I faced some strange behaviors in SplitContainer in particular
 17// when I started to provide a custom paint method. It seems to me
 18// that there is a kind of defect that affects how the Invalidate or
 19// paint event is called. In some situations I faced a SplitContainer
 20// that didn't redraw itself while having some parts of its window
 21// dirty. I didn't found out the cause of the problem.
 22//
 23// Another feature that is quite annoying is the unability to change
 24// the mouse cursor while hovering some special areas of the splitter
 25// bar. Maybe there is a trick or something but the normal way doesn't
 26// look like to work.
 27//
 28
 29namespace NUnit.UiException.Controls
 30{
 31    /// <summary>
 32    /// Implements a place holder that can be splitted either horizontally or vertically.
 33    /// The SplitterBox is layouted with two place holders, respectively named Control1
 34    /// and Control2 where clients can put their controls.
 35    /// 
 36    /// Unlike SplitContainer, the place holders in SplitterBox are the client controls
 37    /// itself. The direct consequence is the layout policy will be to dock the client
 38    /// controls in filling the maximum possible space.
 39    /// 
 40    /// SplitterBox also add three buttons on the splitter bar that to change the split
 41    /// orientation and collapse either Control1 or Control2. The example below shows
 42    /// how to intialize and set up SplitterBox with two controls.
 43    /// <code>
 44    /// // creates a new SplitterBox, with a vertical split
 45    /// // and position splitter to appear in the middle of the window
 46    /// SplitterBox splitter = new SplitterBox();
 47    /// splitter.Orientation = Orientation.Vertical;
 48    /// splitter.SplitterDistance = 0.5f;
 49    /// splitter.Control1 = oneControl;
 50    /// splitter.Control2 = anotherControl;
 51    /// </code>
 52    /// </summary>
 53    public class SplitterBox : Control
 54    {
 55        public static readonly int SPLITTER_SIZE = 9;
 56        public static readonly int SPLITTER_HALFSIZE = SPLITTER_SIZE / 2;
 57        public static readonly int BUTTON_SIZE = 13;
 58        
 59        private Control _emptyControl1;
 60        private Control _emptyControl2;
 61
 62        private Control _control1;
 63        private Control _control2;
 64
 65        private Orientation _orientation;        
 66        private float _x;
 67        private float _y;
 68
 69        private Rectangle _splitterRectangle;
 70        private Rectangle _collapse1Rectangle;
 71        private Rectangle _collapse2Rectangle;
 72        private Rectangle _directionRectangle;
 73
 74        private bool _movingSplitter;
 75
 76        private Brush _brush;
 77        private Pen _pen;
 78
 79        private Rectangle _rVerticalCollapse1;
 80        private Rectangle _rVerticalDirection;
 81        private Rectangle _rVerticalCollapse2;
 82
 83        private Rectangle _rHorizontalCollapse1;
 84        private Rectangle _rHorizontalDirection;
 85        private Rectangle _rHorizontalCollapse2;
 86
 87        public event EventHandler OrientationChanged;
 88        public event EventHandler SplitterDistanceChanged;
 89
 90        /// <summary>
 91        /// Creates a new SplitterBox.
 92        /// </summary>
 93        public SplitterBox()
 94        {
 95            _brush = new SolidBrush(Color.FromArgb(146, 180, 224));
 96            _pen = new Pen(Color.FromArgb(103, 136, 190));
 97
 98            _rVerticalCollapse1 = new Rectangle(0, 0, 9, 13);
 99            _rVerticalDirection = new Rectangle(10, 0, 9, 13);
100            _rVerticalCollapse2 = new Rectangle(20, 0, 9, 13);
101
102            _rHorizontalCollapse1 = new Rectangle(0, 24, 13, 9);
103            _rHorizontalDirection = new Rectangle(14, 14, 13, 9);
104            _rHorizontalCollapse2 = new Rectangle(0, 14, 13, 9);     
105
106            _emptyControl1 = new Control();
107            _emptyControl2 = new Control();
108
109            Width = 150;
110            Height = 150;
111
112            _control1 = _emptyControl1;
113            _control2 = _emptyControl2;
114
115            Controls.Add(_control1);
116            Controls.Add(_control2);
117
118            _x = _y = 0.5f;
119
120            Orientation = Orientation.Vertical;
121
122            DoLayout();
123
124            return;
125        }
126
127        /// <summary>
128        /// Gets or sets the orientation of the splitter in the SplitterBox.
129        /// </summary>
130        public Orientation Orientation
131        {
132            get { return (_orientation); }
133            set { 
134                _orientation = value;
135                DoLayout();
136            }
137        }
138
139        /// <summary>
140        /// Gets or sets the splitter distance expressed as a float number in the
141        /// range [0 - 1]. A value of 0 collapses Control1 and makes Control2 take
142        /// the whole space in the window. A value of 1 collapses Control2 and makes
143        /// Control1 take the whole space in the window. A value of 0.5 makes the
144        /// splitter appear in the middle of the window.
145        /// 
146        /// Values that don't fall in [0 - 1] are automatically clipped to this range.
147        /// </summary>
148        public float SplitterDistance
149        {
150            get { return (_orientation == Orientation.Vertical ? _x : _y); }
151            set {
152                value = Math.Max(0, Math.Min(1, value));
153                if (_orientation == Orientation.Vertical)
154                    _x = value;
155                else
156                    _y = value;
157                DoLayout();
158            }
159        }
160
161        /// <summary>
162        /// Gets or sets the "first" control to be shown. This control will appear
163        /// either at the top or on the left when the orientation is respectively
164        /// vertical or horizontal.
165        ///   If the value is not null, the control will automatically be added
166        /// to the SplitterBox's hierarchy of controls.
167        ///   If the value is null, the former control is removed and replaced
168        /// by a default and empty area.
169        /// </summary>
170        public Control Control1
171        {
172            get { return (_control1); }
173            set 
174            {
175                if (_control1 == value)
176                    return;
177
178                Controls.Remove(_control1);
179                if (value == null)
180                    value = _emptyControl1;
181                _control1 = value;
182                Controls.Add(value);
183                DoLayout();
184
185                return;
186            }
187        }
188
189        /// <summary>
190        /// Gets or sets the "second" control to be shown. This control will appear
191        /// either at the bottom or on the right when the orientation is respectively
192        /// vertical or horizontal.
193        ///   If the value is not null, the control will automatically be added
194        /// to the SplitterBox's hierarchy of controls.
195        ///   If the value is null, the former control is removed and replaced
196        /// by a default and empty area.
197        /// </summary>
198        public Control Control2
199        {
200            get { return (_control2); }
201            set 
202            {
203                if (_control2 == value)
204                    return;
205
206                if (value == null)
207                    value = _emptyControl2;
208
209                Controls.Remove(_control2);
210                _control2 = value;
211                Controls.Add(value);
212                DoLayout();
213
214                return;
215            }
216        }
217
218        /// <summary>
219        /// Gets the rectangle occupied with the splitter.
220        /// </summary>
221        public Rectangle SplitterRectangle
222        {
223            get { return (_splitterRectangle); }            
224        }
225
226        /// <summary>
227        /// Sets a new location for the splitter expressed as client coordinate.
228        /// </summary>
229        /// <param name="x">The new location in pixels when orientation is set to Vertical.</param>
230        /// <param name="y">The new location in pixels when orientation is set to Horizontal.</param>
231        public void PointToSplit(int x, int y)
232        {
233            if (_orientation == Orientation.Vertical)
234            {
235                x = Math.Max(0, Math.Min(Width, x));
236                _x = (float)x / (float)Width;
237            }
238            else
239            {
240                y = Math.Max(0, Math.Min(Height, y));
241                _y = (float)y / (float)Height;
242            }
243
244            DoLayout();
245
246            return;
247        }
248
249        /// <summary>
250        /// Collapses Control1.
251        /// </summary>
252        public void CollapseControl1()
253        {
254            PointToSplit(0, 0);
255        }
256
257        /// <summary>
258        /// Collapses Control2.
259        /// </summary>
260        public void CollapseControl2()
261        {
262            PointToSplit(Width, Height);
263        }
264
265        protected Rectangle Collapse1Rectangle
266        {
267            get { return (_collapse1Rectangle); }
268        }
269
270        protected Rectangle Collapse2Rectangle
271        {
272            get { return (_collapse2Rectangle); }
273        }
274
275        protected Rectangle DirectionRectangle
276        {
277            get { return (_directionRectangle); }
278        }        
279
280        private void HorizontalLayout()
281        {
282            int x = (Width - 41) / 2;
283            int y;
284            int top;
285
286            top = (int)Math.Max(0, _y * Height - SPLITTER_HALFSIZE);
287            top = Math.Min(top, Height - SPLITTER_SIZE);
288
289            _splitterRectangle = new Rectangle(0, top, Width, SPLITTER_SIZE);
290
291            y = _splitterRectangle.Top;
292
293            _collapse1Rectangle = new Rectangle(x, y, BUTTON_SIZE, SPLITTER_SIZE);
294            _directionRectangle = new Rectangle(_collapse1Rectangle.Right + 2, y, BUTTON_SIZE, SPLITTER_SIZE);
295            _collapse2Rectangle = new Rectangle(_directionRectangle.Right + 2, y, BUTTON_SIZE, SPLITTER_SIZE);
296
297            _control1.SetBounds(0, 0, Width, _splitterRectangle.Top);
298            _control2.SetBounds(0, _splitterRectangle.Bottom, Width, Height - _splitterRectangle.Bottom);
299
300            return;
301        }
302
303        private void VerticalLayout()
304        {
305            int y = (Height - 41) / 2;
306            int left;
307            int x;
308
309            left = (int)Math.Max(0, _x * Width - SPLITTER_HALFSIZE);
310            left = Math.Min(left, Width - SPLITTER_SIZE);
311
312            _splitterRectangle = new Rectangle(left, 0, SPLITTER_SIZE, Height);
313
314            x = _splitterRectangle.Left;
315
316            _collapse1Rectangle = new Rectangle(x, y, SPLITTER_SIZE, BUTTON_SIZE);
317            _directionRectangle = new Rectangle(x, _collapse1Rectangle.Bottom + 2, SPLITTER_SIZE, BUTTON_SIZE);
318            _collapse2Rectangle = new Rectangle(x, _directionRectangle.Bottom + 2, SPLITTER_SIZE, BUTTON_SIZE);
319
320            _control1.SetBounds(0, 0, _splitterRectangle.Left, Height);
321            _control2.SetBounds(_splitterRectangle.Right, 0, Width - _splitterRectangle.Right, Height);
322
323            return;
324        }
325
326        private void DoLayout()
327        {
328            if (_orientation == Orientation.Vertical)
329                VerticalLayout();
330            else
331                HorizontalLayout();
332
333            Invalidate();
334
335            return;
336        }
337
338        protected override void OnSizeChanged(EventArgs e)
339        {
340            if (_control1 != null)
341                DoLayout();
342            base.OnSizeChanged(e);
343        }
344
345        protected override void OnMouseLeave(EventArgs e)
346        {
347            Cursor = Cursors.Default;
348            base.OnMouseLeave(e);
349        }
350
351        protected override void OnMouseDown(MouseEventArgs e)
352        {
353            UpdateCursor(e.X, e.Y);
354
355            if (_splitterRectangle.Contains(e.X, e.Y))
356            {
357                if (HoveringButtons(e.X, e.Y))
358                    return;
359
360                _movingSplitter = true;
361            }
362
363            base.OnMouseDown(e);
364
365            return;
366        }
367
368        protected override void OnMouseMove(MouseEventArgs e)
369        {
370            UpdateCursor(e.X, e.Y);
371
372            if (_movingSplitter == true)
373            {                
374                PointToSplit(e.X, e.Y);
375                Invalidate();
376            }
377
378            base.OnMouseMove(e);
379
380            return;
381        }
382
383        protected override void OnMouseUp(MouseEventArgs e)
384        {
385            bool wasMovingSplitter;
386
387            UpdateCursor(e.X, e.Y);
388
389            wasMovingSplitter = _movingSplitter;
390            _movingSplitter = false;
391
392            if (wasMovingSplitter)
393                FireSplitterDistanceChanged();
394            else
395            {
396                if (_collapse1Rectangle.Contains(e.X, e.Y))
397                {
398                    CollapseControl1();
399                    FireSplitterDistanceChanged();
400                    return;
401                }
402
403                if (_collapse2Rectangle.Contains(e.X, e.Y))
404                {
405                    CollapseControl2();
406                    FireSplitterDistanceChanged();
407                    return;
408                }
409
410                if (_directionRectangle.Contains(e.X, e.Y))
411                {
412                    Orientation = (_orientation == Orientation.Vertical) ?
413                            Orientation.Horizontal :
414                            Orientation.Vertical;
415
416                    FireOrientationChanged();
417
418                    return;
419                }
420            }
421
422            base.OnMouseUp(e);
423
424            return;
425        }
426
427        private void FireOrientationChanged()
428        {
429            if (OrientationChanged != null)
430                OrientationChanged(this, EventArgs.Empty);
431        }
432
433        private void FireSplitterDistanceChanged()
434        {
435            if (SplitterDistanceChanged != null)
436                SplitterDistanceChanged(this, EventArgs.Empty);
437        }
438
439        protected override void OnPaint(PaintEventArgs e)
440        {
441            e.Graphics.FillRectangle(_brush, _splitterRectangle);
442
443            if (Orientation == Orientation.Vertical)
444            {
445                e.Graphics.DrawLine(_pen, _splitterRectangle.Left, 0,
446                    SplitterRectangle.Left, _splitterRectangle.Height);
447                e.Graphics.DrawLine(_pen, _splitterRectangle.Right - 1, 0,
448                    SplitterRectangle.Right - 1, _splitterRectangle.Height);
449
450                e.Graphics.DrawImage(Resources.ImageSplitterBox,
451                    _collapse1Rectangle,
452                    _rVerticalCollapse1,
453                    GraphicsUnit.Pixel);
454
455                e.Graphics.DrawImage(Resources.ImageSplitterBox,
456                    _directionRectangle,
457                    _rVerticalDirection,
458                    GraphicsUnit.Pixel);
459
460                e.Graphics.DrawImage(Resources.ImageSplitterBox,
461                    _collapse2Rectangle,
462                    _rVerticalCollapse2,
463                    GraphicsUnit.Pixel);
464            }
465            else
466            {
467                e.Graphics.DrawLine(_pen, 0, _splitterRectangle.Top,
468                    Width, _splitterRectangle.Top);
469                e.Graphics.DrawLine(_pen, 0, _splitterRectangle.Bottom - 1,
470                    Width, _splitterRectangle.Bottom - 1);
471
472                e.Graphics.DrawImage(Resources.ImageSplitterBox,
473                    _collapse1Rectangle,
474                    _rHorizontalCollapse1,
475                    GraphicsUnit.Pixel);
476
477                e.Graphics.DrawImage(Resources.ImageSplitterBox,
478                    _directionRectangle,
479                    _rHorizontalDirection,
480                    GraphicsUnit.Pixel);
481
482                e.Graphics.DrawImage(Resources.ImageSplitterBox,
483                    _collapse2Rectangle,
484                    _rHorizontalCollapse2,
485                    GraphicsUnit.Pixel);
486            }
487
488            base.OnPaint(e);
489
490            return;
491        }
492
493        private bool HoveringButtons(int x, int y)
494        {
495            if (!SplitterRectangle.Contains(x, y))
496                return (false);
497
498            return (_collapse1Rectangle.Contains(x, y) ||
499                    _collapse2Rectangle.Contains(x, y) ||
500                    _directionRectangle.Contains(x, y));
501        }
502
503        private void UpdateCursor(int x, int y)
504        {
505            if (!SplitterRectangle.Contains(x, y) ||
506                HoveringButtons(x, y))
507            {
508                Cursor = Cursors.Default;
509                return;
510            }
511
512            Cursor = (Orientation == Orientation.Vertical ? Cursors.VSplit : Cursors.HSplit);
513
514            return;
515        }
516    }
517}