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