1 /*
2  *  "GEDKeeper", the personal genealogical database editor.
3  *  Copyright (C) 2009-2021 by Sergey V. Zhdanovskih.
4  *
5  *  This file is part of "GEDKeeper".
6  *
7  *  This program is free software: you can redistribute it and/or modify
8  *  it under the terms of the GNU General Public License as published by
9  *  the Free Software Foundation, either version 3 of the License, or
10  *  (at your option) any later version.
11  *
12  *  This program is distributed in the hope that it will be useful,
13  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  *  GNU General Public License for more details.
16  *
17  *  You should have received a copy of the GNU General Public License
18  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 using System;
22 using System.ComponentModel;
23 using System.Drawing;
24 using System.Windows.Forms;
25 using BSLib;
26 using GDModel;
27 using GKCore;
28 using GKCore.Charts;
29 using GKCore.Interfaces;
30 using GKCore.Options;
31 using GKUI.Platform;
32 
33 namespace GKUI.Components
34 {
35     /// <summary>
36     ///
37     /// </summary>
38     public sealed class CircleChart : CustomChart, ICircleChart
39     {
40         private enum MouseCaptured
41         {
42             mcNone,
43             mcDrag
44         }
45 
46         private const float ZOOM_LOW_LIMIT = 0.0125f;
47         private const float ZOOM_HIGH_LIMIT = 1000.0f;
48 
49         private static readonly object EventRootChanged;
50         private static readonly object EventZoomChanged;
51 
52         private readonly IContainer fComponents;
53         private readonly CircleChartModel fModel;
54         private readonly ToolTip fToolTip;
55 
56         private CircleChartType fChartType;
57         private string fHint;
58         private float fOffsetX;
59         private float fOffsetY;
60         /* Zoom factors */
61         private float fZoom = 1.0f;
62         /* Mouse capturing. */
63         private MouseCaptured fMouseCaptured;
64         private int fMouseCaptureX;
65         private int fMouseCaptureY;
66 
67 
68         public IBaseWindow Base
69         {
70             get { return fModel.Base; }
71             set { fModel.Base = value; }
72         }
73 
74         public CircleChartType ChartType
75         {
76             get { return fChartType; }
77             set { fChartType = value; }
78         }
79 
80         public CircleChartModel Model
81         {
82             get { return fModel; }
83         }
84 
85         public CircleChartOptions Options
86         {
87             get { return fModel.Options; }
88         }
89 
90         public int VisibleGenerations
91         {
92             get { return fModel.VisibleGenerations; }
93             set {
94                 fModel.VisibleGenerations = value;
95                 Changed();
96             }
97         }
98 
99         public float Zoom
100         {
101             get {
102                 return fZoom;
103             }
104             set {
105                 fZoom = value;
106 
107                 ExtSize boundary = GetImageSize();
108                 SetImageSize(boundary, true);
109                 Invalidate();
110 
111                 DoZoomChanged();
112             }
113         }
114 
115         public event ARootChangedEventHandler RootChanged
116         {
117             add { Events.AddHandler(EventRootChanged, value); }
118             remove { Events.RemoveHandler(EventRootChanged, value); }
119         }
120 
121         public event EventHandler ZoomChanged
122         {
123             add { Events.AddHandler(EventZoomChanged, value); }
124             remove { Events.RemoveHandler(EventZoomChanged, value); }
125         }
126 
127         public GDMIndividualRecord RootPerson
128         {
129             get {
130                 return fModel.RootPerson;
131             }
132             set {
133                 if (fModel.RootPerson != value) {
134                     fModel.RootPerson = value;
135 
136                     NavAdd(value);
137                     Changed();
138 
139                     DoRootChanged(value);
140                 }
141             }
142         }
143 
144 
CircleChart()145         static CircleChart()
146         {
147             EventRootChanged = new object();
148             EventZoomChanged = new object();
149         }
150 
CircleChart()151         public CircleChart()
152         {
153             BorderStyle = BorderStyle.Fixed3D;
154             DoubleBuffered = true;
155 
156             fRenderer = new WFGfxRenderer();
157             fModel = new CircleChartModel();
158             fModel.SetRenderer(fRenderer);
159             fModel.Options = new CircleChartOptions();
160             fModel.Font = AppHost.GfxProvider.CreateFont(Font.Name, Font.Size, false);
161 
162             fComponents = new Container();
163             fToolTip = new ToolTip(fComponents);
164             fToolTip.AutoPopDelay = 5000;
165             fToolTip.InitialDelay = 250;
166             fToolTip.ReshowDelay = 50;
167             fToolTip.ShowAlways = true;
168 
169             fMouseCaptured = MouseCaptured.mcNone;
170         }
171 
Dispose(bool disposing)172         protected override void Dispose(bool disposing)
173         {
174             if (disposing) {
175                 if (fComponents != null) fComponents.Dispose();
176                 fModel.Dispose();
177             }
178             base.Dispose(disposing);
179         }
180 
SetRenderer(ChartRenderer renderer)181         public override void SetRenderer(ChartRenderer renderer)
182         {
183             base.SetRenderer(renderer);
184             fModel.SetRenderer(renderer);
185         }
186 
Changed()187         public void Changed()
188         {
189             fModel.CreateBrushes();
190 
191             switch (fChartType) {
192                 case CircleChartType.Ancestors:
193                     fModel.BuildAncTree();
194                     break;
195 
196                 case CircleChartType.Descendants:
197                     fModel.BuildDescTree();
198                     break;
199             }
200 
201             fModel.AdjustBounds();
202 
203             ExtSize boundary = GetImageSize();
204             SetImageSize(boundary, false);
205         }
206 
DoRootChanged(GDMIndividualRecord person)207         private void DoRootChanged(GDMIndividualRecord person)
208         {
209             var eventHandler = (ARootChangedEventHandler)Events[EventRootChanged];
210             if (eventHandler != null)
211                 eventHandler(this, person);
212         }
213 
DoZoomChanged()214         private void DoZoomChanged()
215         {
216             var eventHandler = (EventHandler)Events[EventZoomChanged];
217             if (eventHandler != null)
218                 eventHandler(this, new EventArgs());
219         }
220 
221         /// <summary>
222         /// Returns a point that must be the center of this chart after it will
223         /// be rendered on a target.
224         /// </summary>
225         /// <param name="target">Rendering target for which the client requires
226         /// the center point.</param>
227         /// <returns>Center point of this chart.</returns>
GetCenter(RenderTarget target)228         private PointF GetCenter(RenderTarget target)
229         {
230             var bounds = fModel.Bounds;
231 
232             fOffsetX = AutoScrollPosition.X;
233             fOffsetY = AutoScrollPosition.Y;
234 
235             if (target == RenderTarget.Screen) {
236 
237                 // Returns the center point of this chart relative to the upper left
238                 // point of this window's client area.
239                 // According to discussion at PR #99 (GitHub), this member centers this
240                 // chart on an axis when there's no scrolling required along that axis.
241                 // And if scrolling is required, this member aligns this chart on its
242                 // left edge.
243                 PointF center = new PointF();
244 
245                 SizeF boundary = new SizeF(fModel.ImageWidth * fZoom, fModel.ImageHeight * fZoom);
246 
247                 if (ClientSize.Width > boundary.Width) {
248                     center.X = Math.Min(ClientSize.Width - bounds.Right * fZoom, ClientSize.Width >> 1);
249                 } else {
250                     center.X = fOffsetX - bounds.Left * fZoom;
251                 }
252 
253                 if (ClientSize.Height > boundary.Height) {
254                     center.Y = Math.Min(ClientSize.Height - bounds.Bottom * fZoom, ClientSize.Height >> 1);
255                 } else {
256                     center.Y = fOffsetY - bounds.Top * fZoom;
257                 }
258 
259                 return center;
260 
261             } else {
262 
263                 // Returns the center point of this chart relative to the upper left
264                 // corner/point of printing canvas.
265                 //return new PointF(fOffsetX - bounds.Left * fZoom, fOffsetY - bounds.Top * fZoom);
266                 return new PointF(-bounds.Left * fZoom, -bounds.Top * fZoom);
267 
268             }
269         }
270 
FindSegment(float mX, float mY)271         private CircleSegment FindSegment(float mX, float mY)
272         {
273             PointF center = GetCenter(RenderTarget.Screen);
274             mX = (mX - center.X) / fZoom;
275             mY = (mY - center.Y) / fZoom;
276             return fModel.FindSegment(mX, mY);
277         }
278 
279         #region Protected inherited methods
280 
SetNavObject(object obj)281         protected override void SetNavObject(object obj)
282         {
283             RootPerson = obj as GDMIndividualRecord;
284         }
285 
OnDoubleClick(EventArgs e)286         protected override void OnDoubleClick(EventArgs e)
287         {
288             if (fChartType == CircleChartType.Ancestors) {
289                 fModel.GroupsMode = !fModel.GroupsMode;
290             }
291 
292             base.OnDoubleClick(e);
293         }
294 
OnPaint(PaintEventArgs e)295         protected override void OnPaint(PaintEventArgs e)
296         {
297             fRenderer.SetTarget(e.Graphics);
298             RenderImage(RenderTarget.Screen);
299 
300             base.OnPaint(e);
301         }
302 
OnResize(EventArgs e)303         protected override void OnResize(EventArgs e)
304         {
305             Changed();
306 
307             base.OnResize(e);
308         }
309 
OnKeyDown(KeyEventArgs e)310         protected override void OnKeyDown(KeyEventArgs e)
311         {
312             switch (e.KeyCode) {
313                 case Keys.Add:
314                 case Keys.Oemplus:
315                     if (Keys.None == ModifierKeys) {
316                         Zoom = Math.Min(fZoom * 1.05f, ZOOM_HIGH_LIMIT);
317                     }
318                     break;
319 
320                 case Keys.Subtract:
321                 case Keys.OemMinus:
322                     if (Keys.None == ModifierKeys) {
323                         Zoom = Math.Max(fZoom * 0.95f, ZOOM_LOW_LIMIT);
324                     }
325                     break;
326 
327                 case Keys.D0:
328                     if (e.Control) {
329                         Zoom = 1.0f;
330                     }
331                     break;
332 
333                 case Keys.Up:
334                     VisibleGenerations += 1;
335                     break;
336 
337                 case Keys.Down:
338                     VisibleGenerations -= 1;
339                     break;
340 
341                 case Keys.Left:
342                 case Keys.Right:
343                     if (fChartType == CircleChartType.Ancestors && fModel.RootPerson != null) {
344                         GDMIndividualRecord father, mother;
345                         fModel.Base.Context.Tree.GetParents(fModel.RootPerson, out father, out mother);
346                         var target = (e.KeyCode == Keys.Left) ? father : mother;
347                         if (target != null) {
348                             RootPerson = target;
349                         }
350                     }
351                     break;
352             }
353 
354             base.OnKeyDown(e);
355         }
356 
OnMouseDown(MouseEventArgs e)357         protected override void OnMouseDown(MouseEventArgs e)
358         {
359             if ((e.Button == MouseButtons.Right) && (HorizontalScroll.Visible || VerticalScroll.Visible)) {
360                 fMouseCaptured = MouseCaptured.mcDrag;
361                 fMouseCaptureX = e.X;
362                 fMouseCaptureY = e.Y;
363                 Cursor = Cursors.SizeAll;
364             }
365 
366             base.OnMouseDown(e);
367         }
368 
OnMouseUp(MouseEventArgs e)369         protected override void OnMouseUp(MouseEventArgs e)
370         {
371             if (fMouseCaptured == MouseCaptured.mcDrag) {
372                 fMouseCaptured = MouseCaptured.mcNone;
373                 Cursor = Cursors.Default;
374             } else if (e.Button == MouseButtons.Left) {
375                 CircleSegment selected = FindSegment(e.X, e.Y);
376                 if (selected != null && selected.IRec != null) {
377                     RootPerson = selected.IRec;
378                 }
379             }
380 
381             base.OnMouseUp(e);
382         }
383 
OnMouseMove(MouseEventArgs e)384         protected override void OnMouseMove(MouseEventArgs e)
385         {
386             switch (fMouseCaptured) {
387                 case MouseCaptured.mcNone:
388                     {
389                         CircleSegment selected = FindSegment(e.X, e.Y);
390 
391                         string hint = "";
392                         if (!Equals(fModel.Selected, selected)) {
393                             fModel.Selected = selected;
394 
395                             if (selected != null && selected.IRec != null) {
396                                 string name = GKUtils.GetNameString(selected.IRec, true, false);
397                                 hint = /*selected.Gen.ToString() + ", " + */name;
398                             }
399 
400                             Invalidate();
401                         }
402 
403                         if (!Equals(fHint, hint)) {
404                             fHint = hint;
405 
406                             if (!string.IsNullOrEmpty(hint)) {
407                                 fToolTip.Show(hint, this, e.X, e.Y, 3000);
408                             }
409                         }
410                     }
411                     break;
412 
413                 case MouseCaptured.mcDrag:
414                     {
415                         AdjustScroll(-(e.X - fMouseCaptureX), -(e.Y - fMouseCaptureY));
416                         fMouseCaptureX = e.X;
417                         fMouseCaptureY = e.Y;
418                     }
419                     break;
420             }
421 
422             base.OnMouseMove(e);
423         }
424 
OnMouseWheel(MouseEventArgs e)425         protected override void OnMouseWheel(MouseEventArgs e)
426         {
427             if (Keys.None != (Keys.Control & ModifierKeys)) {
428                 if (e.Delta < 0) {
429                     Zoom = Math.Max(fZoom * 0.95f, ZOOM_LOW_LIMIT);
430                 } else {
431                     Zoom = Math.Min(fZoom * 1.05f, ZOOM_HIGH_LIMIT);
432                 }
433             }
434 
435             base.OnMouseWheel(e);
436         }
437 
438         #endregion
439 
440         /// <summary>
441         /// Gets boundary of the area that includes all paths of this chart.
442         /// This member does not recalculates boundaries -- it uses calculation
443         /// made by member `Changed`.
444         /// Result includes width of a path's borders.
445         /// </summary>
446         /// <returns>The paths boundary.</returns>
GetImageSize()447         public override ExtSize GetImageSize()
448         {
449             return new ExtSize((int)(fModel.ImageWidth * fZoom), (int)(fModel.ImageHeight * fZoom));
450         }
451 
452         /// <summary>
453         /// Renders this chart on the specified target and the context
454         /// associated with the target.
455         /// </summary>
456         /// <param name="target">Rendering target.</param>
457         /// <param name="forciblyCentered"></param>
RenderImage(RenderTarget target, bool forciblyCentered = false)458         public override void RenderImage(RenderTarget target, bool forciblyCentered = false)
459         {
460             PointF center = GetCenter(target);
461 
462             var backColor = fModel.Options.BrushColor[9];
463             if (target == RenderTarget.Screen) {
464                 fRenderer.DrawRectangle(null, backColor, 0, 0, Width, Height);
465             } else if (target != RenderTarget.Printer) {
466                 fRenderer.DrawRectangle(null, backColor, 0, 0, fModel.ImageWidth, fModel.ImageHeight);
467             }
468 
469             fRenderer.ResetTransform();
470             fRenderer.TranslateTransform(center.X, center.Y);
471 
472             if (target == RenderTarget.Screen) {
473                 fRenderer.ScaleTransform(fZoom, fZoom);
474             } else {
475                 fRenderer.ScaleTransform(1, 1);
476             }
477 
478             switch (fChartType) {
479                 case CircleChartType.Ancestors:
480                     fModel.DrawAncestors();
481                     break;
482 
483                 case CircleChartType.Descendants:
484                     fModel.DrawDescendants();
485                     break;
486             }
487         }
488     }
489 }
490