windowcapture
исходный код / UI/AnnotationCanvas.cs

AnnotationCanvas.cs

1014 строк · 71,188 байт · модуль UI
   1using System;
   2using System.Collections.Generic;
   3using System.Drawing;
   4using System.Drawing.Drawing2D;
   5using System.Drawing.Imaging;
   6using System.Windows.Forms;
   7using WindowCapture.Models;
   8using WindowCapture.Effects;
   9
  10namespace WindowCapture.UI
  11{
  12    public class AnnotationCanvas : Panel
  13    {
  14        // Reference to editor form
  15        private EditorForm editor;
  16
  17        // Annotations
  18        public List<HighlightRect> Highlights = new List<HighlightRect>();
  19        public List<ArrowAnnotation> Arrows = new List<ArrowAnnotation>();
  20        public List<NumberMarker> Markers = new List<NumberMarker>();
  21        public List<TextBlock> TextBlocks = new List<TextBlock>();
  22        public List<CommentBubble> CommentBubbles = new List<CommentBubble>();
  23        public List<UndoAction> UndoStack = new List<UndoAction>();
  24
  25        // Drawing state
  26        private Point drawStart;
  27        private Point currentEnd;
  28        private bool tracking;
  29        private bool isDrawingRect;
  30        private bool isDrawingArrow;
  31        private int nextMarkerNumber = 1;
  32
  33        // Drag state
  34        private ArrowAnnotation draggingArrow;
  35        private int draggingControlPoint; // 1=cp1, 2=cp2, 3=start, 4=end
  36        private NumberMarker draggingMarker;
  37        private HighlightRect draggingHighlight;
  38        private TextBlock draggingTextBlock;
  39        private CommentBubble draggingBubble;
  40        private Point dragOffset;
  41
  42        // Crop state
  43        private int cropHandle; // 0=none, 1-4=edges, 5-8=corners
  44        private bool isCropping;
  45
  46        // Highlight resize state
  47        private HighlightRect resizingHighlight;
  48        private int resizeHandle; // 0=none, 1-4=edges, 5-8=corners
  49
  50        // Highlight clicked for blur effect (set on MouseDown, processed on MouseUp)
  51        private HighlightRect clickedHighlightForBlur;
  52        private MouseButtons clickedButton;
  53
  54        // Deferred single-click blur toggle. The single-click effect must wait out the
  55        // double-click window so a double-click (motion blur) can cancel it; otherwise every
  56        // double-click first fires the single-click toggle and the two effects fight each other.
  57        private System.Windows.Forms.Timer blurClickTimer;
  58        private HighlightRect pendingBlurHl;
  59        private MouseButtons pendingBlurButton;
  60        private const int DoubleClickMs = 350;
  61
  62        // Text editing
  63        private TextBlock editingTextBlock;
  64        private CommentBubble editingBubble;
  65        private TextBox inputTextBox;
  66        private Action<string> textInputCallback;
  67
  68        // Public property to check if text input is active
  69        public bool IsTextInputActive { get { return inputTextBox != null && inputTextBox.Visible; } }
  70
  71        // Double-click detection
  72        private DateTime lastClickTime;
  73        private Point lastClickPoint;
  74        private MouseButtons lastClickButton;
  75
  76        // Original bitmap for blur restore
  77        private Bitmap originalBitmap;
  78
  79        // Animated GIF support
  80        private Image animatedGifImage;
  81        private bool isAnimatingGif;
  82
  83        // GDI fallback double-buffer
  84        private Bitmap gdiBackBuffer;
  85        private int gdiBackBufferW, gdiBackBufferH;
  86
  87        // Noise texture for Acrylic-style grain (regenerated when intensity changes)
  88        private static Bitmap noiseTexture;
  89        private static int noiseCachedIntensity = -1;
  90
  91        private static Bitmap GetNoiseTexture(int intensity)
  92        {
  93            if (intensity <= 0) return null;
  94            if (noiseTexture != null && noiseCachedIntensity == intensity) return noiseTexture;
  95            if (noiseTexture != null) { noiseTexture.Dispose(); noiseTexture = null; }
  96
  97            const int sz = 128;
  98            var bmp = new Bitmap(sz, sz, PixelFormat.Format32bppArgb);
  99            var rng = new Random(42);
 100            int aMin = Math.Max(1, intensity / 2);
 101            int aMax = Math.Max(aMin + 1, intensity);
 102            var bits = bmp.LockBits(new Rectangle(0, 0, sz, sz), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
 103            unsafe
 104            {
 105                byte* ptr = (byte*)bits.Scan0;
 106                for (int i = 0; i < sz * sz; i++)
 107                {
 108                    byte v = (byte)(rng.Next(160, 255));
 109                    byte a = (byte)rng.Next(aMin, aMax + 1);
 110                    ptr[0] = v; ptr[1] = v; ptr[2] = v; ptr[3] = a;
 111                    ptr += 4;
 112                }
 113            }
 114            bmp.UnlockBits(bits);
 115            noiseTexture = bmp;
 116            noiseCachedIntensity = intensity;
 117            return bmp;
 118        }
 119
 120        /// <summary>Draw noise grain only on the background area (excluding the image rect).</summary>
 121        private static void DrawNoiseBackground(Graphics g, int width, int height, Rectangle imageRect)
 122        {
 123            int intensity = Settings.NoiseIntensity;
 124            if (intensity <= 0) return;
 125            var noise = GetNoiseTexture(intensity);
 126            if (noise == null) return;
 127
 128            // Clip to exclude image area, draw noise only on background
 129            var clip = g.Clip;
 130            using (var region = new Region(new Rectangle(0, 0, width, height)))
 131            {
 132                if (imageRect.Width > 0 && imageRect.Height > 0)
 133                    region.Exclude(imageRect);
 134                g.Clip = region;
 135                using (var tb = new TextureBrush(noise, WrapMode.Tile))
 136                    g.FillRectangle(tb, 0, 0, width, height);
 137            }
 138            g.Clip = clip;
 139        }
 140
 141        // Effect layers (smart filters)
 142        private List<EffectLayer> effectLayers = new List<EffectLayer>();
 143
 144        // Potential drag state (for distinguishing click from drag)
 145        private Point mouseDownPoint;
 146        private bool potentialDrag;
 147        private HighlightRect potentialDragHighlight;
 148
 149        // MMB hold for dim toggle
 150        private HighlightRect mmbHighlight;
 151        private System.Windows.Forms.Timer mmbTimer;
 152
 153        // Panning (drag canvas when zoomed in)
 154        private bool isPanning;
 155        private Point panStart;
 156        private Point canvasStartLocation;
 157
 158        // Cached thumbnail for smooth panning/zooming
 159        private Bitmap cachedThumbnail;
 160        private const int ThumbnailMaxSize = 800; // Max dimension for thumbnail
 161        private bool isZooming;
 162
 163        // D2D rendering mode
 164        private bool d2dActive;
 165        private DateTime lastD2DRetry; // throttle device-loss recovery attempts
 166
 167        // Static Viewport Architecture
 168        private float drawScale = 1.0f;
 169        private float drawX = 0f;
 170        private float drawY = 0f;
 171
 172        public float DrawScale { get { return drawScale; } }
 173        public float DrawX { get { return drawX; } }
 174        public float DrawY { get { return drawY; } }
 175
 176        public void SetViewportTransform(float scale, float x, float y)
 177        {
 178            drawScale = scale;
 179            drawX = x;
 180            drawY = y;
 181            Invalidate();
 182        }
 183
 184        public AnnotationCanvas(EditorForm parent)
 185        {
 186            editor = parent;
 187
 188            // Hardware-accelerated rendering settings
 189            DoubleBuffered = false; // DirectX handles buffering
 190            SetStyle(
 191                ControlStyles.AllPaintingInWmPaint |
 192                ControlStyles.UserPaint |
 193                ControlStyles.ResizeRedraw |
 194                ControlStyles.Opaque |
 195                ControlStyles.SupportsTransparentBackColor,
 196                true);
 197            BackColor = Color.Transparent;
 198            UpdateStyles();
 199
 200            // Create input textbox for text editing
 201            inputTextBox = new TextBox();
 202            inputTextBox.Visible = false;
 203            inputTextBox.BorderStyle = BorderStyle.FixedSingle;
 204            inputTextBox.Font = new Font(Settings.TextBlockFont, Settings.TextBlockFontSize);
 205            inputTextBox.Multiline = true;
 206            inputTextBox.BackColor = Color.FromArgb(30, 32, 38);
 207            inputTextBox.ForeColor = Color.FromArgb(220, 228, 242);
 208            inputTextBox.KeyDown += InputTextBox_KeyDown;
 209            inputTextBox.LostFocus += InputTextBox_LostFocus;
 210            Controls.Add(inputTextBox);
 211
 212            // MMB timer for dim toggle
 213            mmbTimer = new System.Windows.Forms.Timer();
 214            mmbTimer.Interval = 200;
 215            mmbTimer.Tick += MmbTimer_Tick;
 216
 217            // Deferred single-click blur toggle timer
 218            blurClickTimer = new System.Windows.Forms.Timer();
 219            blurClickTimer.Interval = DoubleClickMs;
 220            blurClickTimer.Tick += BlurClickTimer_Tick;
 221        }
 222
 223        private void BlurClickTimer_Tick(object sender, EventArgs e)
 224        {
 225            blurClickTimer.Stop();
 226            var hl = pendingBlurHl;
 227            pendingBlurHl = null;
 228            if (hl == null) return;
 229            // No double-click arrived within the window — apply the single-click blur toggle now.
 230            if (pendingBlurButton == MouseButtons.Left)
 231            {
 232                if (hl.CurrentBlurState == BlurState.BlurInside) RemoveBlurEffect(hl);
 233                else ApplyBlurInside(hl);
 234            }
 235            else if (pendingBlurButton == MouseButtons.Right)
 236            {
 237                if (hl.CurrentBlurState == BlurState.BlurOutside) RemoveBlurEffect(hl);
 238                else ApplyBlurOutside(hl);
 239            }
 240        }
 241
 242        public void SetD2DActive(bool active)
 243        {
 244            d2dActive = active;
 245        }
 246
 247        private const int WM_ERASEBKGND = 0x0014;
 248
 249        protected override void WndProc(ref Message m)
 250        {
 251            if (m.Msg == WM_ERASEBKGND) { m.Result = (IntPtr)1; return; }
 252            base.WndProc(ref m);
 253        }
 254
 255        protected override void OnPaintBackground(PaintEventArgs e)
 256        {
 257            // Suppress background erase to fix flickering
 258        }
 259
 260        private void MmbTimer_Tick(object sender, EventArgs e)
 261        {
 262            mmbTimer.Stop();
 263            if (mmbHighlight != null)
 264            {
 265                // 200ms elapsed - toggle dim
 266                mmbHighlight.DimDisabled = !mmbHighlight.DimDisabled;
 267                Invalidate();
 268                mmbHighlight = null;
 269            }
 270        }
 271
 272        public void SetOriginalBitmap(Bitmap bmp)
 273        {
 274            StopGifAnimation();
 275            if (originalBitmap != null) originalBitmap.Dispose();
 276            originalBitmap = (Bitmap)bmp.Clone();
 277        }
 278
 279        /// <summary>Load an animated GIF. The Image is NOT cloned — animation data preserved.</summary>
 280        public void SetAnimatedGif(Image gif)
 281        {
 282            StopGifAnimation();
 283            if (originalBitmap != null) originalBitmap.Dispose();
 284
 285            animatedGifImage = gif;
 286            originalBitmap = new Bitmap(gif.Width, gif.Height, PixelFormat.Format32bppArgb);
 287            using (var g = Graphics.FromImage(originalBitmap))
 288                g.DrawImage(gif, 0, 0, gif.Width, gif.Height);
 289
 290            if (ImageAnimator.CanAnimate(gif))
 291            {
 292                isAnimatingGif = true;
 293                ImageAnimator.Animate(gif, OnGifFrameChanged);
 294            }
 295        }
 296
 297        private void OnGifFrameChanged(object sender, EventArgs e)
 298        {
 299            // ImageAnimator fires from a background thread — must marshal to UI
 300            if (!IsHandleCreated || IsDisposed) return;
 301            try
 302            {
 303                BeginInvoke((Action)UpdateGifFrame);
 304            }
 305            catch { }
 306        }
 307
 308        private void UpdateGifFrame()
 309        {
 310            if (animatedGifImage == null || originalBitmap == null) return;
 311            try
 312            {
 313                ImageAnimator.UpdateFrames(animatedGifImage);
 314                using (var g = Graphics.FromImage(originalBitmap))
 315                {
 316                    g.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
 317                    g.DrawImage(animatedGifImage, 0, 0, originalBitmap.Width, originalBitmap.Height);
 318                }
 319                // Skip D2D upload per frame — too slow. Force GDI path for GIF rendering.
 320                Invalidate();
 321            }
 322            catch { }
 323        }
 324
 325        public void StopGifAnimation()
 326        {
 327            if (isAnimatingGif && animatedGifImage != null)
 328            {
 329                ImageAnimator.StopAnimate(animatedGifImage, OnGifFrameChanged);
 330                animatedGifImage.Dispose();
 331                animatedGifImage = null;
 332                isAnimatingGif = false;
 333            }
 334        }
 335
 336        // Thumbnail management for smooth panning/zooming
 337        private void EnsureThumbnailCreated()
 338        {
 339            if (cachedThumbnail != null) return;
 340            if (originalBitmap == null) return;
 341
 342            int w = originalBitmap.Width;
 343            int h = originalBitmap.Height;
 344            float scale = 1f;
 345            if (w > ThumbnailMaxSize || h > ThumbnailMaxSize)
 346                scale = Math.Min((float)ThumbnailMaxSize / w, (float)ThumbnailMaxSize / h);
 347
 348            int tw = Math.Max(1, (int)(w * scale));
 349            int th = Math.Max(1, (int)(h * scale));
 350
 351            // Check if any real effects are active
 352            bool hasEffects = false;
 353            foreach (var l in effectLayers)
 354                if (l.Type != EffectType.None) { hasEffects = true; break; }
 355
 356            if (hasEffects && Highlights.Count > 0)
 357            {
 358                // Render effects composite at full res, then scale to thumbnail
 359                using (var composite = RenderEffectsComposite())
 360                {
 361                    cachedThumbnail = new Bitmap(tw, th, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
 362                    using (var g = Graphics.FromImage(cachedThumbnail))
 363                    {
 364                        g.InterpolationMode = InterpolationMode.Bilinear;
 365                        g.DrawImage(composite, 0, 0, tw, th);
 366                    }
 367                }
 368            }
 369            else
 370            {
 371                cachedThumbnail = new Bitmap(tw, th, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
 372                using (var g = Graphics.FromImage(cachedThumbnail))
 373                {
 374                    g.InterpolationMode = InterpolationMode.Bilinear;
 375                    g.DrawImage(originalBitmap, 0, 0, tw, th);
 376                }
 377            }
 378        }
 379
 380        /// <summary>Render image + effects (no annotations) into a full-res bitmap for thumbnail caching.</summary>
 381        private Bitmap RenderEffectsComposite()
 382        {
 383            int w = originalBitmap.Width, h = originalBitmap.Height;
 384            var result = new Bitmap(w, h, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
 385            using (var g = Graphics.FromImage(result))
 386            {
 387                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
 388
 389                bool anyDimDisabled = false;
 390                bool hasOutsideEffect = false;
 391                HighlightRect outsideHL = null;
 392
 393                foreach (var hl in Highlights)
 394                {
 395                    if (hl.DimDisabled) anyDimDisabled = true;
 396                    var layer = FindEffectLayer(hl.Id);
 397                    if (layer != null && (layer.Type == EffectType.BlurOutside || layer.Type == EffectType.MotionBlurOutside))
 398                    { hasOutsideEffect = true; outsideHL = hl; }
 399                }
 400
 401                // Draw base
 402                if (anyDimDisabled)
 403                {
 404                    g.DrawImageUnscaled(originalBitmap, 0, 0);
 405                }
 406                else if (hasOutsideEffect && outsideHL != null)
 407                {
 408                    // BlurOutside: base already has blur+dim everywhere except its own rect.
 409                    var layer = FindEffectLayer(outsideHL.Id);
 410                    var effectBmp = layer.GetCachedResult(originalBitmap);
 411                    if (effectBmp != null)
 412                        g.DrawImageUnscaled(effectBmp, 0, 0);
 413                    else if (editor.BlurredBitmap != null)
 414                        g.DrawImageUnscaled(editor.BlurredBitmap, 0, 0);
 415                    else
 416                        g.DrawImageUnscaled(originalBitmap, 0, 0);
 417                }
 418                else
 419                {
 420                    // Inside-effect or no-effect base. With "dim around selection" on, use the dimmed
 421                    // image (restore loop un-dims the selected regions); otherwise a clear base.
 422                    if (Settings.DimAroundSelection && editor.BlurredBitmap != null)
 423                        g.DrawImageUnscaled(editor.BlurredBitmap, 0, 0);
 424                    else
 425                        g.DrawImageUnscaled(originalBitmap, 0, 0);
 426                }
 427
 428                // Draw highlight regions
 429                foreach (var hl in Highlights)
 430                {
 431                    var layer = FindEffectLayer(hl.Id);
 432                    if (layer != null && (layer.Type == EffectType.BlurOutside || layer.Type == EffectType.MotionBlurOutside))
 433                        continue;
 434
 435                    g.SetClip(hl.Rect);
 436                    if (layer != null && layer.Type != EffectType.None)
 437                    {
 438                        var effectBmp = layer.GetCachedResult(originalBitmap);
 439                        if (effectBmp != null)
 440                            g.DrawImageUnscaled(effectBmp, 0, 0);
 441                        else
 442                            g.DrawImageUnscaled(originalBitmap, 0, 0);
 443                    }
 444                    else
 445                    {
 446                        g.DrawImageUnscaled(originalBitmap, 0, 0);
 447                    }
 448                }
 449                g.ResetClip();
 450            }
 451            return result;
 452        }
 453
 454        private void DisposeThumbnail()
 455        {
 456            if (cachedThumbnail != null) { cachedThumbnail.Dispose(); cachedThumbnail = null; }
 457        }
 458
 459        public void BeginZoomAnimation()
 460        {
 461            isZooming = true;
 462            // Skip thumbnail when effects are active — D2D renders blur/dim via GPU shaders
 463            // which can't be pre-baked into a static bitmap. Let D2D render at full quality.
 464            bool hasEffects = false;
 465            foreach (var l in effectLayers)
 466                if (l.Type != EffectType.None) { hasEffects = true; break; }
 467            // Also skip the (raw) thumbnail when the spotlight dim is active, so pan/zoom renders the
 468            // same dimmed composite as the static view instead of flashing the brighter raw image.
 469            if (!hasEffects && !(Settings.DimAroundSelection && Highlights.Count > 0))
 470                EnsureThumbnailCreated();
 471        }
 472
 473        public void EndZoomAnimation()
 474        {
 475            isZooming = false;
 476            if (!isPanning) DisposeThumbnail();
 477            Invalidate();
 478        }
 479        private bool UseFastRendering { get { return isPanning || isZooming; } }
 480
 481        // Convert viewport coords to image coords (accounting for drawScale/X/Y)
 482        private Point ToImageCoords(Point localPt)
 483        {
 484            return new Point(
 485                (int)Math.Round((localPt.X - drawX) / drawScale),
 486                (int)Math.Round((localPt.Y - drawY) / drawScale));
 487        }
 488
 489        // Convert image coords to viewport coords
 490        private Point ToScreenCoords(Point imgPt)
 491        {
 492            return new Point(
 493                (int)Math.Round(imgPt.X * drawScale + drawX),
 494                (int)Math.Round(imgPt.Y * drawScale + drawY));
 495        }
 496
 497        // Check which crop handle is at position
 498        private int GetCropHandle(Point imgPt)
 499        {
 500            int m = 6; var cr = editor.CropRect;
 501            if (Math.Abs(imgPt.X - cr.Left) < m && Math.Abs(imgPt.Y - cr.Top) < m) return 5;
 502            if (Math.Abs(imgPt.X - cr.Right) < m && Math.Abs(imgPt.Y - cr.Top) < m) return 6;
 503            if (Math.Abs(imgPt.X - cr.Left) < m && Math.Abs(imgPt.Y - cr.Bottom) < m) return 7;
 504            if (Math.Abs(imgPt.X - cr.Right) < m && Math.Abs(imgPt.Y - cr.Bottom) < m) return 8;
 505            if (Math.Abs(imgPt.X - cr.Left) < m && imgPt.Y > cr.Top && imgPt.Y < cr.Bottom) return 1;
 506            if (Math.Abs(imgPt.X - cr.Right) < m && imgPt.Y > cr.Top && imgPt.Y < cr.Bottom) return 2;
 507            if (Math.Abs(imgPt.Y - cr.Top) < m && imgPt.X > cr.Left && imgPt.X < cr.Right) return 3;
 508            if (Math.Abs(imgPt.Y - cr.Bottom) < m && imgPt.X > cr.Left && imgPt.X < cr.Right) return 4;
 509            return 0;
 510        }
 511
 512        private Cursor GetCursorForHandle(int handle)
 513        {
 514            switch (handle)
 515            {
 516                case 1: case 2: return Cursors.SizeWE;
 517                case 3: case 4: return Cursors.SizeNS;
 518                case 5: case 8: return Cursors.SizeNWSE;
 519                case 6: case 7: return Cursors.SizeNESW;
 520                default: return Cursors.Default;
 521            }
 522        }
 523
 524        protected override void OnMouseWheel(MouseEventArgs e)
 525        {
 526            base.OnMouseWheel(e);
 527
 528            // RMB held: resize text/bubble font or arrow width under cursor
 529            if ((Control.MouseButtons & MouseButtons.Right) != 0)
 530            {
 531                var imgPt = ToImageCoords(e.Location);
 532                using (var g = CreateGraphics())
 533                {
 534                    foreach (var tb in TextBlocks)
 535                    {
 536                        if (tb.Contains(imgPt, g))
 537                        {
 538                            float step = e.Delta > 0 ? 1f : -1f;
 539                            float newSize = tb.TextFont.Size + step;
 540                            if (newSize >= 6f && newSize <= 300f)
 541                            {
 542                                tb.TextFont = new Font(tb.TextFont.FontFamily, newSize, tb.TextFont.Style);
 543                                Invalidate();
 544                            }
 545                            return;
 546                        }
 547                    }
 548                    foreach (var bubble in CommentBubbles)
 549                    {
 550                        if (bubble.Contains(imgPt, g))
 551                        {
 552                            float step = e.Delta > 0 ? 1f : -1f;
 553                            float newSize = bubble.TextFont.Size + step;
 554                            if (newSize >= 6f && newSize <= 300f)
 555                            {
 556                                bubble.TextFont = new Font(bubble.TextFont.FontFamily, newSize, bubble.TextFont.Style);
 557                                Invalidate();
 558                            }
 559                            return;
 560                        }
 561                    }
 562                }
 563                foreach (var arr in Arrows)
 564                {
 565                    int cpSize = (int)(14 / drawScale);
 566                    if (arr.GetStartRect(cpSize).Contains(imgPt) || arr.GetEndRect(cpSize).Contains(imgPt) ||
 567                        arr.GetControl1Rect(cpSize).Contains(imgPt) || arr.GetControl2Rect(cpSize).Contains(imgPt))
 568                    {
 569                        float step = e.Delta > 0 ? 0.5f : -0.5f;
 570                        float newWidth = arr.Width + step;
 571                        if (newWidth >= 1f && newWidth <= 20f) { arr.Width = newWidth; Invalidate(); }
 572                        return;
 573                    }
 574                }
 575                // If RMB+wheel not over any element, fall through to zoom
 576            }
 577
 578            // Viewer mode: wheel browses files, Ctrl+wheel zooms
 579            if (editor.IsViewerMode && !editor.IsVideoFile && !editor.IsAudioFile && (Control.ModifierKeys & Keys.Control) == 0)
 580            {
 581                editor.BrowseFile(e.Delta > 0 ? -1 : 1);
 582                return;
 583            }
 584
 585            float delta = e.Delta > 0 ? 0.1f : -0.1f;
 586            editor.ApplyZoom(delta, e.Location);
 587        }
 588
 589        // Window drag from background area
 590        private bool canvasDraggingForm;
 591        private Point canvasDragStart;
 592
 593        protected override void OnMouseDown(MouseEventArgs e)
 594        {
 595            base.OnMouseDown(e);
 596            if (e.Button == MouseButtons.Left && editor.TryClickGlassButton(e.Location)) return;
 597            Focus();
 598
 599            // Check if click is outside the image area → start window drag
 600            if (e.Button == MouseButtons.Left && originalBitmap != null)
 601            {
 602                float iw = originalBitmap.Width * drawScale;
 603                float ih = originalBitmap.Height * drawScale;
 604                var imageRect = new RectangleF(drawX, drawY, iw, ih);
 605                if (!imageRect.Contains(e.Location))
 606                {
 607                    canvasDraggingForm = true;
 608                    canvasDragStart = e.Location;
 609                    Cursor = Cursors.SizeAll;
 610                    return;
 611                }
 612            }
 613
 614            var imgPt = ToImageCoords(e.Location);
 615            bool isDoubleClick = (DateTime.Now - lastClickTime).TotalMilliseconds < DoubleClickMs && Math.Abs(e.Location.X - lastClickPoint.X) < 15 && Math.Abs(e.Location.Y - lastClickPoint.Y) < 15 && e.Button == lastClickButton;
 616            lastClickTime = DateTime.Now; lastClickPoint = e.Location; lastClickButton = e.Button;
 617            if (isDoubleClick) { HandleDoubleClick(e.Button, imgPt); return; }
 618            if (e.Button == MouseButtons.Left)
 619            {
 620                if (editor.AltPressed) return;
 621                int handle = GetCropHandle(imgPt);
 622                if (handle > 0) { cropHandle = handle; isCropping = true; tracking = true; return; }
 623                int cpSize = (int)(14 / drawScale);
 624                foreach (var arr in Arrows)
 625                {
 626                    if (arr.GetStartRect(cpSize).Contains(imgPt)) { draggingArrow = arr; draggingControlPoint = 3; tracking = true; return; }
 627                    if (arr.GetEndRect(cpSize).Contains(imgPt)) { draggingArrow = arr; draggingControlPoint = 4; tracking = true; return; }
 628                    if (arr.GetControl1Rect(cpSize).Contains(imgPt)) { draggingArrow = arr; draggingControlPoint = 1; tracking = true; return; }
 629                    if (arr.GetControl2Rect(cpSize).Contains(imgPt)) { draggingArrow = arr; draggingControlPoint = 2; tracking = true; return; }
 630                }
 631                int markerSize = Settings.MarkerSize;
 632                foreach (var mk in Markers) if (new Rectangle(mk.Location.X - markerSize / 2, mk.Location.Y - markerSize / 2, markerSize, markerSize).Contains(imgPt)) { draggingMarker = mk; dragOffset = new Point(imgPt.X - mk.Location.X, imgPt.Y - mk.Location.Y); tracking = true; return; }
 633                foreach (var hl in Highlights)
 634                {
 635                    int hlHandle = GetHighlightHandle(hl, imgPt);
 636                    if (hlHandle > 0) { resizingHighlight = hl; resizeHandle = hlHandle; tracking = true; return; }
 637                    if (hl.Rect.Contains(imgPt)) { mouseDownPoint = imgPt; potentialDrag = true; potentialDragHighlight = hl; clickedHighlightForBlur = hl; clickedButton = e.Button; return; }
 638                    var outer = new Rectangle(hl.Rect.X - 12, hl.Rect.Y - 12, hl.Rect.Width + 24, hl.Rect.Height + 24);
 639                    if (outer.Contains(imgPt) && !hl.Rect.Contains(imgPt)) { draggingHighlight = hl; dragOffset = new Point(imgPt.X - hl.Rect.X, imgPt.Y - hl.Rect.Y); tracking = true; return; }
 640                }
 641                foreach (var tb in TextBlocks) { using (var g = CreateGraphics()) { var size = g.MeasureString(tb.Text, tb.TextFont); if (new Rectangle(tb.Location.X, tb.Location.Y, (int)size.Width + 10, (int)size.Height + 5).Contains(imgPt)) { draggingTextBlock = tb; dragOffset = new Point(imgPt.X - tb.Location.X, imgPt.Y - tb.Location.Y); tracking = true; return; } } }
 642                using (var g = CreateGraphics()) foreach (var bubble in CommentBubbles) if (bubble.Contains(imgPt, g)) { draggingBubble = bubble; dragOffset = new Point(imgPt.X - bubble.Location.X, imgPt.Y - bubble.Location.Y); tracking = true; return; }
 643                isDrawingRect = true; drawStart = imgPt; currentEnd = imgPt; tracking = true;
 644            }
 645            else if (e.Button == MouseButtons.Right)
 646            {
 647                foreach (var hl in Highlights) if (hl.Rect.Contains(imgPt)) { clickedHighlightForBlur = hl; clickedButton = e.Button; return; }
 648                isDrawingArrow = true; drawStart = imgPt; currentEnd = imgPt; tracking = true;
 649            }
 650            else if (e.Button == MouseButtons.Middle)
 651            {
 652                foreach (var hl in Highlights) if (hl.Rect.Contains(imgPt)) { mmbHighlight = hl; mmbTimer.Start(); return; }
 653                isPanning = true; panStart = e.Location; canvasStartLocation = new Point((int)drawX, (int)drawY); Cursor = Cursors.SizeAll;
 654                bool panHasEffects = false; foreach (var l in effectLayers) if (l.Type != EffectType.None) { panHasEffects = true; break; }
 655                if (!panHasEffects && !(Settings.DimAroundSelection && Highlights.Count > 0)) EnsureThumbnailCreated();
 656            }
 657        }
 658
 659        protected override void OnMouseMove(MouseEventArgs e)
 660        {
 661            base.OnMouseMove(e);
 662            if (canvasDraggingForm)
 663            {
 664                var form = editor;
 665                form.Location = new Point(
 666                    form.Location.X + e.Location.X - canvasDragStart.X,
 667                    form.Location.Y + e.Location.Y - canvasDragStart.Y);
 668                return;
 669            }
 670            if (isPanning)
 671            {
 672                int dx = e.Location.X - panStart.X; int dy = e.Location.Y - panStart.Y;
 673                float newX = canvasStartLocation.X + dx; float newY = canvasStartLocation.Y + dy;
 674                int vw = Width, vh = Height; float imgW = originalBitmap.Width * drawScale, imgH = originalBitmap.Height * drawScale;
 675                if (imgW <= vw) newX = (vw - imgW) / 2f; else newX = Math.Max(vw - imgW, Math.Min(0, newX));
 676                if (imgH <= vh) newY = (vh - imgH) / 2f; else newY = Math.Max(vh - imgH, Math.Min(0, newY));
 677                SetViewportTransform(drawScale, newX, newY); return;
 678            }
 679            var imgPt = ToImageCoords(e.Location);
 680            if (potentialDrag && potentialDragHighlight != null) { if (Math.Abs(imgPt.X - mouseDownPoint.X) > 5 || Math.Abs(imgPt.Y - mouseDownPoint.Y) > 5) { draggingHighlight = potentialDragHighlight; dragOffset = new Point(mouseDownPoint.X - potentialDragHighlight.Rect.X, mouseDownPoint.Y - potentialDragHighlight.Rect.Y); clickedHighlightForBlur = null; potentialDrag = false; potentialDragHighlight = null; tracking = true; } }
 681            if (isCropping && cropHandle > 0)
 682            {
 683                var cr = editor.CropRect; int minSize = 20;
 684                switch (cropHandle)
 685                {
 686                    case 1: cr = new Rectangle(Math.Max(0, Math.Min(imgPt.X, cr.Right - minSize)), cr.Y, cr.Right - Math.Max(0, Math.Min(imgPt.X, cr.Right - minSize)), cr.Height); break;
 687                    case 2: cr = new Rectangle(cr.X, cr.Y, Math.Max(cr.Left + minSize, Math.Min(imgPt.X, editor.Bounds.Width)) - cr.X, cr.Height); break;
 688                    case 3: cr = new Rectangle(cr.X, Math.Max(0, Math.Min(imgPt.Y, cr.Bottom - minSize)), cr.Width, cr.Bottom - Math.Max(0, Math.Min(imgPt.Y, cr.Bottom - minSize))); break;
 689                    case 4: cr = new Rectangle(cr.X, cr.Y, cr.Width, Math.Max(cr.Top + minSize, Math.Min(imgPt.Y, editor.Bounds.Height)) - cr.Y); break;
 690                    case 5: cr = new Rectangle(Math.Max(0, Math.Min(imgPt.X, cr.Right - minSize)), Math.Max(0, Math.Min(imgPt.Y, cr.Bottom - minSize)), cr.Right - Math.Max(0, Math.Min(imgPt.X, cr.Right - minSize)), cr.Bottom - Math.Max(0, Math.Min(imgPt.Y, cr.Bottom - minSize))); break;
 691                    case 6: cr = new Rectangle(cr.X, Math.Max(0, Math.Min(imgPt.Y, cr.Bottom - minSize)), Math.Max(cr.Left + minSize, Math.Min(imgPt.X, editor.Bounds.Width)) - cr.X, cr.Bottom - Math.Max(0, Math.Min(imgPt.Y, cr.Bottom - minSize))); break;
 692                    case 7: cr = new Rectangle(Math.Max(0, Math.Min(imgPt.X, cr.Right - minSize)), cr.Y, cr.Right - Math.Max(0, Math.Min(imgPt.X, cr.Right - minSize)), Math.Max(cr.Top + minSize, Math.Min(imgPt.Y, editor.Bounds.Height)) - cr.Y); break;
 693                    case 8: cr = new Rectangle(cr.X, cr.Y, Math.Max(cr.Left + minSize, Math.Min(imgPt.X, editor.Bounds.Width)) - cr.X, Math.Max(cr.Top + minSize, Math.Min(imgPt.Y, editor.Bounds.Height)) - cr.Y); break;
 694                }
 695                editor.SetCropRect(cr); Invalidate(); return;
 696            }
 697            if (resizingHighlight != null && resizeHandle > 0)
 698            {
 699                var r = resizingHighlight.Rect; int minSize = 20;
 700                switch (resizeHandle)
 701                {
 702                    case 1: resizingHighlight.Rect = new Rectangle(Math.Min(imgPt.X, r.Right - minSize), r.Y, r.Right - Math.Min(imgPt.X, r.Right - minSize), r.Height); break;
 703                    case 2: resizingHighlight.Rect = new Rectangle(r.X, r.Y, Math.Max(r.Left + minSize, imgPt.X) - r.X, r.Height); break;
 704                    case 3: resizingHighlight.Rect = new Rectangle(r.X, Math.Min(imgPt.Y, r.Bottom - minSize), r.Width, r.Bottom - Math.Min(imgPt.Y, r.Bottom - minSize)); break;
 705                    case 4: resizingHighlight.Rect = new Rectangle(r.X, r.Y, r.Width, Math.Max(r.Top + minSize, imgPt.Y) - r.Y); break;
 706                    case 5: resizingHighlight.Rect = new Rectangle(Math.Min(imgPt.X, r.Right - minSize), Math.Min(imgPt.Y, r.Bottom - minSize), r.Right - Math.Min(imgPt.X, r.Right - minSize), r.Bottom - Math.Min(imgPt.Y, r.Bottom - minSize)); break;
 707                    case 6: resizingHighlight.Rect = new Rectangle(r.X, Math.Min(imgPt.Y, r.Bottom - minSize), Math.Max(r.Left + minSize, imgPt.X) - r.X, r.Bottom - Math.Min(imgPt.Y, r.Bottom - minSize)); break;
 708                    case 7: resizingHighlight.Rect = new Rectangle(Math.Min(imgPt.X, r.Right - minSize), r.Y, r.Right - Math.Min(imgPt.X, r.Right - minSize), Math.Max(r.Top + minSize, imgPt.Y) - r.Y); break;
 709                    case 8: resizingHighlight.Rect = new Rectangle(r.X, r.Y, Math.Max(r.Left + minSize, imgPt.X) - r.X, Math.Max(r.Top + minSize, imgPt.Y) - r.Y); break;
 710                }
 711                SyncEffectLayerPosition(resizingHighlight); Invalidate(); return;
 712            }
 713            if (draggingArrow != null) { if (draggingControlPoint == 1) draggingArrow.Control1 = imgPt; else if (draggingControlPoint == 2) draggingArrow.Control2 = imgPt; else if (draggingControlPoint == 3) { int dx = imgPt.X - draggingArrow.Start.X; int dy = imgPt.Y - draggingArrow.Start.Y; draggingArrow.Start = imgPt; draggingArrow.Control1 = new PointF(draggingArrow.Control1.X + dx, draggingArrow.Control1.Y + dy); } else if (draggingControlPoint == 4) { int dx = imgPt.X - draggingArrow.End.X; int dy = imgPt.Y - draggingArrow.End.Y; draggingArrow.End = imgPt; draggingArrow.Control2 = new PointF(draggingArrow.Control2.X + dx, draggingArrow.Control2.Y + dy); } draggingArrow.HasCurve = true; Invalidate(); return; }
 714            if (draggingMarker != null) { draggingMarker.Location = new Point(imgPt.X - dragOffset.X, imgPt.Y - dragOffset.Y); Invalidate(); return; }
 715            if (draggingHighlight != null) { draggingHighlight.Rect = new Rectangle(imgPt.X - dragOffset.X, imgPt.Y - dragOffset.Y, draggingHighlight.Rect.Width, draggingHighlight.Rect.Height); SyncEffectLayerPosition(draggingHighlight); Invalidate(); return; }
 716            if (draggingTextBlock != null) { draggingTextBlock.Location = new Point(imgPt.X - dragOffset.X, imgPt.Y - dragOffset.Y); Invalidate(); return; }
 717            if (draggingBubble != null) { int dx = imgPt.X - dragOffset.X - draggingBubble.Location.X; int dy = imgPt.Y - dragOffset.Y - draggingBubble.Location.Y; draggingBubble.Location = new Point(imgPt.X - dragOffset.X, imgPt.Y - dragOffset.Y); draggingBubble.TailPoint = new Point(draggingBubble.TailPoint.X + dx, draggingBubble.TailPoint.Y + dy); Invalidate(); return; }
 718            if (tracking) { currentEnd = imgPt; Invalidate(); }
 719            if (editor.AltPressed) editor.UpdateAutoDetect(imgPt);
 720            UpdateCursor(imgPt);
 721            editor.UpdateGlassButtonsFromCanvas(e.Location);
 722        }
 723
 724        private void UpdateCursor(Point loc)
 725        {
 726            if (editor.AltPressed) { Cursor = Cursors.Cross; return; }
 727            int handle = GetCropHandle(loc);
 728            if (handle > 0) { Cursor = GetCursorForHandle(handle); return; }
 729            int cpSize = 14;
 730            foreach (var arr in Arrows) if (arr.GetStartRect(cpSize).Contains(loc) || arr.GetEndRect(cpSize).Contains(loc) || arr.GetControl1Rect(cpSize).Contains(loc) || arr.GetControl2Rect(cpSize).Contains(loc)) { Cursor = Cursors.SizeAll; return; }
 731            int markerSize = Settings.MarkerSize;
 732            foreach (var mk in Markers) if (new Rectangle(mk.Location.X - markerSize / 2, mk.Location.Y - markerSize / 2, markerSize, markerSize).Contains(loc)) { Cursor = Cursors.SizeAll; return; }
 733            foreach (var hl in Highlights) { int hlHandle = GetHighlightHandle(hl, loc); if (hlHandle > 0) { Cursor = GetCursorForHandle(hlHandle); return; } if (hl.Rect.Contains(loc)) { Cursor = Cursors.Hand; return; } var outer = new Rectangle(hl.Rect.X - 12, hl.Rect.Y - 12, hl.Rect.Width + 24, hl.Rect.Height + 24); if (outer.Contains(loc)) { Cursor = Cursors.SizeAll; return; } }
 734            Cursor = Cursors.Default;
 735        }
 736
 737        protected override void OnMouseUp(MouseEventArgs e)
 738        {
 739            base.OnMouseUp(e);
 740            if (canvasDraggingForm) { canvasDraggingForm = false; Cursor = Cursors.Default; return; }
 741            var imgPt = ToImageCoords(e.Location); potentialDrag = false; potentialDragHighlight = null;
 742            if (e.Button == MouseButtons.Middle) { if (mmbHighlight != null) { mmbTimer.Stop(); var mk = new NumberMarker(imgPt, nextMarkerNumber++); Markers.Add(mk); UndoStack.Add(new UndoAction("marker", mk)); mmbHighlight = null; Invalidate(); return; } if (isPanning) { int dx = Math.Abs(e.Location.X - panStart.X); int dy = Math.Abs(e.Location.Y - panStart.Y); isPanning = false; Cursor = Cursors.Default; if (!isZooming) DisposeThumbnail(); if (dx < 5 && dy < 5) { var mk = new NumberMarker(imgPt, nextMarkerNumber++); Markers.Add(mk); UndoStack.Add(new UndoAction("marker", mk)); } Invalidate(); return; } }
 743            if (clickedHighlightForBlur != null) { var hl = clickedHighlightForBlur; clickedHighlightForBlur = null; if (hl.Rect.Contains(imgPt)) { pendingBlurHl = hl; pendingBlurButton = clickedButton; blurClickTimer.Stop(); blurClickTimer.Start(); } return; }
 744            if (e.Button == MouseButtons.Left)
 745            {
 746                if (isCropping) { isCropping = false; cropHandle = 0; tracking = false; Invalidate(); return; }
 747                if (resizingHighlight != null) { resizingHighlight = null; resizeHandle = 0; tracking = false; Invalidate(); return; }
 748                if (draggingArrow != null) { draggingArrow = null; draggingControlPoint = 0; tracking = false; Invalidate(); return; }
 749                if (draggingMarker != null) { draggingMarker = null; tracking = false; Invalidate(); return; }
 750                if (draggingHighlight != null) { draggingHighlight = null; tracking = false; Invalidate(); return; }
 751                if (draggingTextBlock != null) { draggingTextBlock = null; tracking = false; Invalidate(); return; }
 752                if (draggingBubble != null) { draggingBubble = null; tracking = false; Invalidate(); return; }
 753                if (isDrawingRect) { var rect = MakeRect(drawStart, imgPt); if (rect.Width > 5 && rect.Height > 5) { var hl = new HighlightRect(rect); Highlights.Add(hl); UndoStack.Add(new UndoAction("highlight", hl)); } isDrawingRect = false; tracking = false; Invalidate(); }
 754            }
 755            else if (e.Button == MouseButtons.Right && isDrawingArrow) { var dx = imgPt.X - drawStart.X; var dy = imgPt.Y - drawStart.Y; if (Math.Sqrt(dx * dx + dy * dy) > 10) { var arr = new ArrowAnnotation(drawStart, imgPt); Arrows.Add(arr); UndoStack.Add(new UndoAction("arrow", arr)); } isDrawingArrow = false; tracking = false; Invalidate(); }
 756        }
 757
 758        private void HandleDoubleClick(MouseButtons button, Point imgPt)
 759        {
 760            isDrawingArrow = false; isDrawingRect = false; tracking = false;
 761            // Cancel any pending single-click blur toggle — this gesture is a double-click.
 762            blurClickTimer.Stop(); pendingBlurHl = null;
 763            if (button == MouseButtons.Left) { foreach (var hl in Highlights) if (hl.Rect.Contains(imgPt)) { if (hl.CurrentBlurState == BlurState.MotionInside) RemoveBlurEffect(hl); else ApplyMotionBlurInside(hl); return; } CreateTextBlock(imgPt); }
 764            else if (button == MouseButtons.Right) { foreach (var hl in Highlights) if (hl.Rect.Contains(imgPt)) { if (hl.CurrentBlurState == BlurState.MotionOutside) RemoveBlurEffect(hl); else ApplyMotionBlurOutside(hl); return; } CreateCommentBubble(imgPt); }
 765        }
 766
 767        private int GetHighlightHandle(HighlightRect hl, Point imgPt) { int m = 8; var r = hl.Rect; if (Math.Abs(imgPt.X - r.Left) < m && Math.Abs(imgPt.Y - r.Top) < m) return 5; if (Math.Abs(imgPt.X - r.Right) < m && Math.Abs(imgPt.Y - r.Top) < m) return 6; if (Math.Abs(imgPt.X - r.Left) < m && Math.Abs(imgPt.Y - r.Bottom) < m) return 7; if (Math.Abs(imgPt.X - r.Right) < m && Math.Abs(imgPt.Y - r.Bottom) < m) return 8; if (Math.Abs(imgPt.X - r.Left) < m && imgPt.Y > r.Top && imgPt.Y < r.Bottom) return 1; if (Math.Abs(imgPt.X - r.Right) < m && imgPt.Y > r.Top && imgPt.Y < r.Bottom) return 2; if (Math.Abs(imgPt.Y - r.Top) < m && imgPt.X > r.Left && imgPt.X < r.Right) return 3; if (Math.Abs(imgPt.Y - r.Bottom) < m && imgPt.X > r.Left && imgPt.X < r.Right) return 4; return 0; }
 768        private EffectLayer FindEffectLayer(int highlightId) { foreach (var layer in effectLayers) if (layer.HighlightId == highlightId) return layer; return null; }
 769        public EffectLayer GetEffectLayer(int highlightId) { return FindEffectLayer(highlightId); }
 770        private void RemoveEffectLayer(int highlightId) { var layer = FindEffectLayer(highlightId); if (layer != null) { layer.Dispose(); effectLayers.Remove(layer); } }
 771        private void ApplyBlurInside(HighlightRect hl) { if (originalBitmap == null) return; RemoveEffectLayer(hl.Id); var layer = new EffectLayer(hl.Id) { Type = EffectType.BlurInside, TargetRect = hl.Rect, Intensity = (int)Math.Round(hl.BlurRadius), FeatherWidth = Settings.FeatherWidth }; effectLayers.Add(layer); hl.CurrentBlurState = BlurState.BlurInside; Invalidate(); }
 772        private void ApplyBlurOutside(HighlightRect hl) { if (originalBitmap == null) return; RemoveEffectLayer(hl.Id); var layer = new EffectLayer(hl.Id) { Type = EffectType.BlurOutside, TargetRect = hl.Rect, Intensity = (int)Math.Round(hl.BlurRadius), FeatherWidth = Settings.FeatherWidth }; effectLayers.Add(layer); hl.CurrentBlurState = BlurState.BlurOutside; Invalidate(); }
 773        private void ApplyMotionBlurInside(HighlightRect hl) { if (originalBitmap == null) return; RemoveEffectLayer(hl.Id); var layer = new EffectLayer(hl.Id) { Type = EffectType.MotionBlurInside, TargetRect = hl.Rect, Intensity = hl.MotionBlurDistance, FeatherWidth = Settings.FeatherWidth }; effectLayers.Add(layer); hl.CurrentBlurState = BlurState.MotionInside; Invalidate(); }
 774        private void ApplyMotionBlurOutside(HighlightRect hl) { if (originalBitmap == null) return; RemoveEffectLayer(hl.Id); var layer = new EffectLayer(hl.Id) { Type = EffectType.MotionBlurOutside, TargetRect = hl.Rect, Intensity = hl.MotionBlurDistance, FeatherWidth = Settings.FeatherWidth }; effectLayers.Add(layer); hl.CurrentBlurState = BlurState.MotionOutside; Invalidate(); }
 775        private void RemoveBlurEffect(HighlightRect hl) { RemoveEffectLayer(hl.Id); hl.CurrentBlurState = BlurState.None; Invalidate(); }
 776        private void SyncEffectLayerPosition(HighlightRect hl) { var layer = FindEffectLayer(hl.Id); if (layer != null) { layer.TargetRect = hl.Rect; layer.Invalidate(); } }
 777        public Bitmap GetCompositeImage() { if (originalBitmap == null) return null; if (effectLayers.Count == 0) return originalBitmap; Bitmap result = null; foreach (var layer in effectLayers) { var effectBitmap = layer.GetCachedResult(originalBitmap); if (effectBitmap != null) result = effectBitmap; } return result ?? originalBitmap; }
 778        public Bitmap GetFullCompositeImage() { if (originalBitmap == null) return null; Bitmap baseImage = GetCompositeImage() ?? originalBitmap; Bitmap result = new Bitmap(baseImage.Width, baseImage.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); using (Graphics g = Graphics.FromImage(result)) { g.SmoothingMode = SmoothingMode.AntiAlias; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; g.InterpolationMode = InterpolationMode.HighQualityBicubic; if (Highlights.Count > 0) { bool hasOutsideEffect = false; HighlightRect outsideEffectHighlight = null; bool anyDimDisabled = false; foreach (var hl in Highlights) { if (hl.DimDisabled) anyDimDisabled = true; var layer = FindEffectLayer(hl.Id); if (layer != null && (layer.Type == EffectType.BlurOutside || layer.Type == EffectType.MotionBlurOutside)) { hasOutsideEffect = true; outsideEffectHighlight = hl; } } if (anyDimDisabled) g.DrawImage(originalBitmap, 0, 0, baseImage.Width, baseImage.Height); else if (hasOutsideEffect && outsideEffectHighlight != null) { var layer = FindEffectLayer(outsideEffectHighlight.Id); var effectBitmap = layer.GetCachedResult(originalBitmap); if (effectBitmap != null) g.DrawImage(effectBitmap, 0, 0, baseImage.Width, baseImage.Height); else g.DrawImage(originalBitmap, 0, 0, baseImage.Width, baseImage.Height); } else if (Settings.DimAroundSelection && editor.BlurredBitmap != null) g.DrawImage(editor.BlurredBitmap, 0, 0, baseImage.Width, baseImage.Height); else g.DrawImage(originalBitmap, 0, 0, baseImage.Width, baseImage.Height); foreach (var hl in Highlights) { var layer = FindEffectLayer(hl.Id); if (layer != null && (layer.Type == EffectType.BlurOutside || layer.Type == EffectType.MotionBlurOutside)) continue; if (layer != null && layer.Type != EffectType.None) { var effectBitmap = layer.GetCachedResult(originalBitmap); if (effectBitmap != null) { g.SetClip(hl.Rect); g.DrawImage(effectBitmap, 0, 0, baseImage.Width, baseImage.Height); } } else { g.SetClip(hl.Rect); g.DrawImage(originalBitmap, 0, 0, baseImage.Width, baseImage.Height); } } g.ResetClip(); } else g.DrawImage(baseImage, 0, 0, baseImage.Width, baseImage.Height); if (Settings.ShowHighlightBorder) foreach (var hl in Highlights) { using (var shadowPen = new Pen(Color.FromArgb(60, 0, 0, 0), Settings.HighlightBorderWidth + 2)) g.DrawRectangle(shadowPen, new Rectangle(hl.Rect.X + 2, hl.Rect.Y + 2, hl.Rect.Width, hl.Rect.Height)); using (var pen = new Pen(Settings.HighlightBorderColor, Settings.HighlightBorderWidth)) g.DrawRectangle(pen, hl.Rect); } foreach (var arr in Arrows) DrawArrowForExport(g, arr); foreach (var mk in Markers) DrawMarker(g, mk); foreach (var tb in TextBlocks) DrawTextBlock(g, tb); foreach (var bubble in CommentBubbles) DrawBubble(g, bubble); } return result; }
 779        private void DrawArrowForExport(Graphics g, ArrowAnnotation arr) { float headLen = 20f, headWidth = 10f; var tangent = arr.GetBezierTangent(1.0f); var endPt = new PointF(arr.End.X, arr.End.Y); var headBase = new PointF(endPt.X - tangent.X * headLen, endPt.Y - tangent.Y * headLen); var headLeft = new PointF(headBase.X + (-tangent.Y * headWidth), headBase.Y + (tangent.X * headWidth)); var headRight = new PointF(headBase.X - (-tangent.Y * headWidth), headBase.Y - (tangent.X * headWidth)); using (var shadowPen = new Pen(Color.FromArgb(Settings.ShadowAlpha, 0, 0, 0), arr.Width + 2)) { shadowPen.StartCap = LineCap.Round; shadowPen.EndCap = LineCap.Round; g.DrawBezier(shadowPen, new PointF(arr.Start.X + Settings.ShadowOffset, arr.Start.Y + Settings.ShadowOffset), new PointF(arr.Control1.X + Settings.ShadowOffset, arr.Control1.Y + Settings.ShadowOffset), new PointF(arr.Control2.X + Settings.ShadowOffset, arr.Control2.Y + Settings.ShadowOffset), new PointF(headBase.X + Settings.ShadowOffset, headBase.Y + Settings.ShadowOffset)); } using (var shadowBrush = new SolidBrush(Color.FromArgb(Settings.ShadowAlpha, 0, 0, 0))) g.FillPolygon(shadowBrush, new PointF[] { new PointF(endPt.X + Settings.ShadowOffset, endPt.Y + Settings.ShadowOffset), new PointF(headLeft.X + Settings.ShadowOffset, headLeft.Y + Settings.ShadowOffset), new PointF(headRight.X + Settings.ShadowOffset, headRight.Y + Settings.ShadowOffset) }); using (var pen = new Pen(arr.Color, arr.Width)) { pen.StartCap = LineCap.Round; pen.EndCap = LineCap.Round; g.DrawBezier(pen, arr.Start, arr.Control1, arr.Control2, headBase); } using (var brush = new SolidBrush(arr.Color)) g.FillPolygon(brush, new PointF[] { endPt, headLeft, headRight }); }
 780        private void CreateTextBlock(Point location) { var textBlock = new TextBlock(location); TextBlocks.Add(textBlock); editingTextBlock = textBlock; ShowTextInput(location, textBlock.TextFont, delegate(string text) { textBlock.Text = text; textBlock.IsEditing = false; if (string.IsNullOrEmpty(text)) TextBlocks.Remove(textBlock); else UndoStack.Add(new UndoAction("text", textBlock)); editingTextBlock = null; Invalidate(); }); }
 781        private void CreateCommentBubble(Point location) { var bubble = new CommentBubble(location); CommentBubbles.Add(bubble); editingBubble = bubble; ShowTextInput(location, bubble.TextFont, delegate(string text) { bubble.Text = text; bubble.IsEditing = false; if (string.IsNullOrEmpty(text)) CommentBubbles.Remove(bubble); else UndoStack.Add(new UndoAction("bubble", bubble)); editingBubble = null; Invalidate(); }); }
 782        private void ShowTextBlockPopup(TextBlock tb, Point controlLocation) { var screenPt = this.PointToScreen(controlLocation); screenPt.X += 10; screenPt.Y += 10; var popup = new TextBlockPopup(tb, screenPt, () => { Invalidate(); }); popup.Show(); }
 783        private void ShowTextInput(Point imgLocation, Font font, Action<string> callback) { textInputCallback = callback; var pt = ToScreenCoords(imgLocation); inputTextBox.Location = pt; inputTextBox.Font = font; inputTextBox.Text = ""; inputTextBox.Size = new Size(200, 60); inputTextBox.Visible = true; inputTextBox.Focus(); }
 784        private void InputTextBox_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Enter && !e.Shift) { e.SuppressKeyPress = true; FinishTextInput(); } else if (e.KeyCode == Keys.Escape) CancelTextInput(); }
 785        private void InputTextBox_LostFocus(object sender, EventArgs e) { if (inputTextBox.Visible) FinishTextInput(); }
 786        private void FinishTextInput() { inputTextBox.Visible = false; if (textInputCallback != null) textInputCallback(inputTextBox.Text.Trim()); textInputCallback = null; }
 787        private void CancelTextInput() { inputTextBox.Visible = false; if (textInputCallback != null) textInputCallback(""); textInputCallback = null; }
 788        private Rectangle MakeRect(Point a, Point b) { return new Rectangle(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y), Math.Abs(a.X - b.X), Math.Abs(a.Y - b.Y)); }
 789        public void ResetMarkerCount() { nextMarkerNumber = 1; }
 790        public void Undo() { if (UndoStack.Count == 0) return; var action = UndoStack[UndoStack.Count - 1]; UndoStack.RemoveAt(UndoStack.Count - 1); switch (action.Type) { case "highlight": var highlight = (HighlightRect)action.Item; RemoveEffectLayer(highlight.Id); Highlights.Remove(highlight); break; case "arrow": Arrows.Remove((ArrowAnnotation)action.Item); break; case "marker": Markers.Remove((NumberMarker)action.Item); nextMarkerNumber = Markers.Count > 0 ? Markers[Markers.Count - 1].Number + 1 : 1; break; case "text": TextBlocks.Remove((TextBlock)action.Item); break; case "bubble": CommentBubbles.Remove((CommentBubble)action.Item); break; } Invalidate(); }
 791
 792        protected override void OnPaint(PaintEventArgs e)
 793        {
 794            if (originalBitmap == null) return;
 795
 796            // GPU was active but the device was lost (GPU reset / RDP / driver update) → try to rebuild
 797            // it (throttled). Only fires on machines where D2D worked before, so no-GPU boxes never retry.
 798            if (d2dActive && !D2DRenderer.IsAvailable && (DateTime.Now - lastD2DRetry).TotalSeconds > 2)
 799            {
 800                lastD2DRetry = DateTime.Now;
 801                try { editor.TryRecoverD2D(); } catch { }
 802            }
 803
 804            var gfx = e.Graphics;
 805            float sw = originalBitmap.Width * drawScale; float sh = originalBitmap.Height * drawScale;
 806            RectangleF destRectF = new RectangleF(drawX, drawY, sw, sh);
 807            Rectangle destRect = new Rectangle((int)Math.Round(drawX), (int)Math.Round(drawY), (int)Math.Round(sw), (int)Math.Round(sh));
 808            bool useThumbnail = UseFastRendering && cachedThumbnail != null;
 809            // Skip D2D for animated GIFs — bitmap changes every frame, GDI draws directly
 810            if (!isAnimatingGif && D2DRenderer.IsAvailable && D2DRenderer.BeginDraw())
 811            {
 812                try
 813                {
 814                    // Background tint from settings (floats 0-1)
 815                    D2DRenderer.Clear(
 816                        Settings.BlurTintColor.R / 255f,
 817                        Settings.BlurTintColor.G / 255f,
 818                        Settings.BlurTintColor.B / 255f,
 819                        Settings.BlurTintAlpha / 255f);
 820                    // Noise grain drawn via GDI interop after image (see below)
 821                    if (useThumbnail) D2DRenderer.DrawImage(destRectF, 1f);
 822                    else if (Highlights.Count > 0)
 823                    {
 824                        bool anyDimDisabled = false, anyOutside = false;
 825                        foreach (var hl in Highlights)
 826                        {
 827                            if (hl.DimDisabled) anyDimDisabled = true;
 828                            var layer = FindEffectLayer(hl.Id);
 829                            if (layer != null && (layer.Type == EffectType.BlurOutside || layer.Type == EffectType.MotionBlurOutside)) anyOutside = true;
 830                        }
 831                        float bgScale = (float)originalBitmap.Width / Settings.ReferenceResolution;
 832                        int scaledBlur = Math.Max(1, (int)Math.Round(Settings.BlurRadius * bgScale));
 833
 834                        if (anyDimDisabled)
 835                        {
 836                            D2DRenderer.DrawImage(destRectF, 1f); // "peek original" toggle
 837                        }
 838                        else if (anyOutside)
 839                        {
 840                            // Blur + dim everything OUTSIDE, then keep EVERY selected region clear
 841                            // (so other highlights aren't blurred/darkened — that was the bug).
 842                            D2DRenderer.DrawImageBlurred(destRectF, scaledBlur);
 843                            D2DRenderer.DrawDimOverlay(destRectF, Settings.DimAlpha / 255f);
 844                            foreach (var hl in Highlights)
 845                            {
 846                                var hlRF = new RectangleF(destRectF.X + hl.Rect.X * drawScale, destRectF.Y + hl.Rect.Y * drawScale, hl.Rect.Width * drawScale, hl.Rect.Height * drawScale);
 847                                var hlLayer = FindEffectLayer(hl.Id);
 848                                bool inside = hlLayer != null && (hlLayer.Type == EffectType.BlurInside || hlLayer.Type == EffectType.MotionBlurInside);
 849                                D2DRenderer.PushClip(hlRF);
 850                                if (inside) D2DRenderer.DrawImageBlurred(destRectF, hlLayer.Intensity);
 851                                else D2DRenderer.DrawImage(destRectF, 1f);
 852                                D2DRenderer.PopClip();
 853                            }
 854                        }
 855                        else
 856                        {
 857                            // No "outside" effect. Clear image as base; optionally dim AROUND the selection
 858                            // (configurable spotlight), and blur INSIDE any inside-effect rects. The
 859                            // selected regions themselves always stay clear/sharp.
 860                            D2DRenderer.DrawImage(destRectF, 1f);
 861                            bool spotlight = Settings.DimAroundSelection;
 862                            if (spotlight) D2DRenderer.DrawDimOverlay(destRectF, Settings.DimAlpha / 255f);
 863                            foreach (var hl in Highlights)
 864                            {
 865                                var hlLayer = FindEffectLayer(hl.Id);
 866                                bool inside = hlLayer != null && (hlLayer.Type == EffectType.BlurInside || hlLayer.Type == EffectType.MotionBlurInside);
 867                                if (!spotlight && !inside) continue; // nothing to restore for this region
 868                                var hlRF = new RectangleF(destRectF.X + hl.Rect.X * drawScale, destRectF.Y + hl.Rect.Y * drawScale, hl.Rect.Width * drawScale, hl.Rect.Height * drawScale);
 869                                D2DRenderer.PushClip(hlRF);
 870                                if (inside) D2DRenderer.DrawImageBlurred(destRectF, hlLayer.Intensity);
 871                                else D2DRenderer.DrawImage(destRectF, 1f); // un-dim the selected region
 872                                D2DRenderer.PopClip();
 873                            }
 874                        }
 875                    }
 876                    else D2DRenderer.DrawImage(destRectF, 1f);
 877                    var g = D2DRenderer.GetGdiGraphics();
 878                    if (g != null) { try {
 879                        DrawNoiseBackground(g, Width, Height, destRect);
 880                        g.SmoothingMode = SmoothingMode.AntiAlias; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; g.TranslateTransform(drawX, drawY); g.ScaleTransform(drawScale, drawScale); DrawAnnotations(g); g.ResetTransform(); DrawUIOverlay(g);
 881                    } finally { D2DRenderer.ReleaseGdiGraphics(g); } }
 882                }
 883                catch (Exception ex) { System.Diagnostics.Debug.WriteLine("D2D OnPaint error: " + ex.Message); }
 884                D2DRenderer.EndDraw(); return;
 885            }
 886            // GDI Fallback with manual double-buffering
 887            if (gdiBackBuffer == null || gdiBackBufferW != Width || gdiBackBufferH != Height)
 888            {
 889                if (gdiBackBuffer != null) gdiBackBuffer.Dispose();
 890                gdiBackBufferW = Math.Max(1, Width);
 891                gdiBackBufferH = Math.Max(1, Height);
 892                gdiBackBuffer = new Bitmap(gdiBackBufferW, gdiBackBufferH, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
 893            }
 894            using (var bg = Graphics.FromImage(gdiBackBuffer))
 895            {
 896                // Background tint from settings (area around the image)
 897                bg.Clear(Color.FromArgb(Settings.BlurTintAlpha, Settings.BlurTintColor));
 898                DrawNoiseBackground(bg, Width, Height, destRect);
 899                bg.SmoothingMode = SmoothingMode.AntiAlias; bg.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; bg.InterpolationMode = useThumbnail ? InterpolationMode.Low : InterpolationMode.HighQualityBicubic;
 900                if (useThumbnail) bg.DrawImage(cachedThumbnail, destRect); else bg.DrawImage(originalBitmap, destRect);
 901                bg.TranslateTransform(drawX, drawY); bg.ScaleTransform(drawScale, drawScale); DrawAnnotations(bg); bg.ResetTransform(); DrawUIOverlay(bg);
 902            }
 903            gfx.DrawImageUnscaled(gdiBackBuffer, 0, 0);
 904        }
 905
 906        private void DrawUIOverlay(Graphics g) { editor.PaintUIOnCanvas(g); }
 907        private void DrawAnnotations(Graphics g) { var cr = editor.CropRect; var imgBounds = new Rectangle(0, 0, editor.Bounds.Width, editor.Bounds.Height); if (cr != imgBounds && cr.Width > 0 && cr.Height > 0) { using (var dimBrush = new SolidBrush(Color.FromArgb(120, 0, 0, 0))) { if (cr.Top > 0) g.FillRectangle(dimBrush, 0, 0, imgBounds.Width, cr.Top); if (cr.Bottom < imgBounds.Height) g.FillRectangle(dimBrush, 0, cr.Bottom, imgBounds.Width, imgBounds.Height - cr.Bottom); if (cr.Left > 0) g.FillRectangle(dimBrush, 0, cr.Top, cr.Left, cr.Height); if (cr.Right < imgBounds.Width) g.FillRectangle(dimBrush, cr.Right, cr.Top, imgBounds.Width - cr.Right, cr.Height); } using (var borderPen = new Pen(Color.FromArgb(180, 255, 255, 255), 1f)) { borderPen.DashStyle = DashStyle.Dash; g.DrawRectangle(borderPen, cr); } } if (Highlights.Count > 0 && Settings.ShowHighlightBorder) { foreach (var hl in Highlights) { using (var shadowPen = new Pen(Color.FromArgb(60, 0, 0, 0), Settings.HighlightBorderWidth + 2)) g.DrawRectangle(shadowPen, new Rectangle(hl.Rect.X + 2, hl.Rect.Y + 2, hl.Rect.Width, hl.Rect.Height)); using (var pen = new Pen(Settings.HighlightBorderColor, Settings.HighlightBorderWidth)) g.DrawRectangle(pen, hl.Rect); DrawHighlightResizeHandles(g, hl.Rect); } } foreach (var arr in Arrows) DrawArrowWithControlPoints(g, arr); foreach (var mk in Markers) DrawMarker(g, mk); foreach (var tb in TextBlocks) DrawTextBlock(g, tb); foreach (var bubble in CommentBubbles) DrawBubble(g, bubble); DrawInProgress(g); DrawCropHandles(g); }
 908
 909        /// <summary>Live preview of the shape currently being dragged, drawn every mouse-move
 910        /// so the user sees the rectangle/arrow grow instead of it appearing only on mouse-up.</summary>
 911        private void DrawInProgress(Graphics g)
 912        {
 913            if (isDrawingRect)
 914            {
 915                var rect = MakeRect(drawStart, currentEnd);
 916                if (rect.Width > 0 || rect.Height > 0)
 917                {
 918                    using (var fill = new SolidBrush(Color.FromArgb(36, Settings.HighlightBorderColor)))
 919                        g.FillRectangle(fill, rect);
 920                    using (var shadowPen = new Pen(Color.FromArgb(60, 0, 0, 0), Settings.HighlightBorderWidth + 2))
 921                        g.DrawRectangle(shadowPen, new Rectangle(rect.X + 2, rect.Y + 2, rect.Width, rect.Height));
 922                    using (var pen = new Pen(Settings.HighlightBorderColor, Math.Max(1f, Settings.HighlightBorderWidth)))
 923                        g.DrawRectangle(pen, rect);
 924                }
 925            }
 926            else if (isDrawingArrow)
 927            {
 928                int dx = currentEnd.X - drawStart.X, dy = currentEnd.Y - drawStart.Y;
 929                if (dx * dx + dy * dy > 9) // ignore the first couple of px (matches the commit threshold)
 930                {
 931                    var preview = new ArrowAnnotation(drawStart, currentEnd);
 932                    DrawArrowForExport(g, preview);
 933                }
 934            }
 935        }
 936        private void DrawArrowWithControlPoints(Graphics g, ArrowAnnotation arr)
 937        {
 938            float headLen = 20f, headWidth = 10f;
 939            var tangent = arr.GetBezierTangent(1.0f);
 940            var endPt = new PointF(arr.End.X, arr.End.Y);
 941            var headBase = new PointF(endPt.X - tangent.X * headLen, endPt.Y - tangent.Y * headLen);
 942            var headLeft = new PointF(headBase.X + (-tangent.Y * headWidth), headBase.Y + (tangent.X * headWidth));
 943            var headRight = new PointF(headBase.X - (-tangent.Y * headWidth), headBase.Y - (tangent.X * headWidth));
 944
 945            // Shadow
 946            using (var shadowPen = new Pen(Color.FromArgb(Settings.ShadowAlpha, 0, 0, 0), arr.Width + 2))
 947            {
 948                shadowPen.StartCap = LineCap.Round; shadowPen.EndCap = LineCap.Round;
 949                g.DrawBezier(shadowPen,
 950                    new PointF(arr.Start.X + Settings.ShadowOffset, arr.Start.Y + Settings.ShadowOffset),
 951                    new PointF(arr.Control1.X + Settings.ShadowOffset, arr.Control1.Y + Settings.ShadowOffset),
 952                    new PointF(arr.Control2.X + Settings.ShadowOffset, arr.Control2.Y + Settings.ShadowOffset),
 953                    new PointF(headBase.X + Settings.ShadowOffset, headBase.Y + Settings.ShadowOffset));
 954            }
 955            using (var shadowBrush = new SolidBrush(Color.FromArgb(Settings.ShadowAlpha, 0, 0, 0)))
 956                g.FillPolygon(shadowBrush, new PointF[] {
 957                    new PointF(endPt.X + Settings.ShadowOffset, endPt.Y + Settings.ShadowOffset),
 958                    new PointF(headLeft.X + Settings.ShadowOffset, headLeft.Y + Settings.ShadowOffset),
 959                    new PointF(headRight.X + Settings.ShadowOffset, headRight.Y + Settings.ShadowOffset) });
 960
 961            // Arrow curve
 962            using (var pen = new Pen(arr.Color, arr.Width))
 963            {
 964                pen.StartCap = LineCap.Round; pen.EndCap = LineCap.Round;
 965                g.DrawBezier(pen, arr.Start, arr.Control1, arr.Control2, headBase);
 966            }
 967            // Arrow head
 968            using (var brush = new SolidBrush(arr.Color))
 969                g.FillPolygon(brush, new PointF[] { endPt, headLeft, headRight });
 970
 971            // Bezier control point handles
 972            int cpSize = 8;
 973            // Tangent lines from endpoints to control points
 974            using (var tangentPen = new Pen(Color.FromArgb(120, 180, 180, 255), 1f))
 975            {
 976                tangentPen.DashStyle = DashStyle.Dash;
 977                g.DrawLine(tangentPen, arr.Start, arr.Control1);
 978                g.DrawLine(tangentPen, arr.End, arr.Control2);
 979            }
 980            // Start point handle (square)
 981            var startRect = arr.GetStartRect(cpSize);
 982            using (var brush = new SolidBrush(Color.FromArgb(200, 100, 200, 255)))
 983                g.FillRectangle(brush, startRect);
 984            using (var pen = new Pen(Color.FromArgb(220, 60, 60, 120), 1f))
 985                g.DrawRectangle(pen, startRect);
 986            // End point handle (square)
 987            var endRect = arr.GetEndRect(cpSize);
 988            using (var brush = new SolidBrush(Color.FromArgb(200, 100, 200, 255)))
 989                g.FillRectangle(brush, endRect);
 990            using (var pen = new Pen(Color.FromArgb(220, 60, 60, 120), 1f))
 991                g.DrawRectangle(pen, endRect);
 992            // Control point 1 handle (circle)
 993            var cp1Rect = arr.GetControl1Rect(cpSize);
 994            using (var brush = new SolidBrush(Color.FromArgb(200, 255, 160, 80)))
 995                g.FillEllipse(brush, cp1Rect);
 996            using (var pen = new Pen(Color.FromArgb(220, 120, 80, 40), 1f))
 997                g.DrawEllipse(pen, cp1Rect);
 998            // Control point 2 handle (circle)
 999            var cp2Rect = arr.GetControl2Rect(cpSize);
1000            using (var brush = new SolidBrush(Color.FromArgb(200, 255, 160, 80)))
1001                g.FillEllipse(brush, cp2Rect);
1002            using (var pen = new Pen(Color.FromArgb(220, 120, 80, 40), 1f))
1003                g.DrawEllipse(pen, cp2Rect);
1004        }
1005        private void DrawMarker(Graphics g, NumberMarker marker) { int size = Settings.MarkerSize; var rect = new Rectangle(marker.Location.X - size / 2, marker.Location.Y - size / 2, size, size); using (var shadowBrush = new SolidBrush(Color.FromArgb(80, 0, 0, 0))) g.FillEllipse(shadowBrush, rect.X + 3, rect.Y + 3, size, size); using (var brush = new SolidBrush(Settings.MarkerColor)) g.FillEllipse(brush, rect); using (var pen = new Pen(Settings.MarkerBorderColor, 2)) g.DrawEllipse(pen, rect); using (var font = new Font(Settings.MarkerFont, size / 2.5f, FontStyle.Bold)) using (var brush = new SolidBrush(Settings.MarkerTextColor)) { StringFormat sf = new StringFormat(); sf.Alignment = StringAlignment.Center; sf.LineAlignment = StringAlignment.Center; g.DrawString(marker.Number.ToString(), font, brush, rect, sf); } }
1006        private void DrawTextBlock(Graphics g, TextBlock tb) { if (tb.ShadowOffset > 0) using (var brush = new SolidBrush(tb.ShadowColor)) g.DrawString(tb.Text, tb.TextFont, brush, tb.Location.X + tb.ShadowOffset, tb.Location.Y + tb.ShadowOffset); using (var brush = new SolidBrush(tb.TextColor)) g.DrawString(tb.Text, tb.TextFont, brush, tb.Location); }
1007        private void DrawBubble(Graphics g, CommentBubble bubble) { var path = bubble.GetBubblePath(g); if (path == null) return; var shadowMatrix = new Matrix(); shadowMatrix.Translate(3, 3); var shadowPath = (GraphicsPath)path.Clone(); shadowPath.Transform(shadowMatrix); using (var shadowBrush = new SolidBrush(Color.FromArgb(60, 0, 0, 0))) g.FillPath(shadowBrush, shadowPath); shadowPath.Dispose(); using (var brush = new SolidBrush(bubble.FillColor)) g.FillPath(brush, path); using (var pen = new Pen(bubble.BorderColor, bubble.BorderWidth)) g.DrawPath(pen, path); var textBounds = bubble.GetTextBounds(g); using (var brush = new SolidBrush(bubble.TextColor)) g.DrawString(bubble.Text, bubble.TextFont, brush, textBounds.Location); path.Dispose(); }
1008        private void DrawCropHandles(Graphics g) { var cr = editor.CropRect; int handleSize = 8; using (var brush = new SolidBrush(Color.White)) using (var pen = new Pen(Color.FromArgb(100, 100, 100), 1)) { DrawHandle(g, brush, pen, cr.Left, cr.Top, handleSize); DrawHandle(g, brush, pen, cr.Right, cr.Top, handleSize); DrawHandle(g, brush, pen, cr.Left, cr.Bottom, handleSize); DrawHandle(g, brush, pen, cr.Right, cr.Bottom, handleSize); DrawHandle(g, brush, pen, cr.Left, cr.Top + cr.Height / 2, handleSize); DrawHandle(g, brush, pen, cr.Right, cr.Top + cr.Height / 2, handleSize); DrawHandle(g, brush, pen, cr.Left + cr.Width / 2, cr.Top, handleSize); DrawHandle(g, brush, pen, cr.Left + cr.Width / 2, cr.Bottom, handleSize); } using (var pen = new Pen(Color.FromArgb(100, 255, 255, 255), 1)) { pen.DashStyle = DashStyle.Dash; g.DrawRectangle(pen, cr); } }
1009        private void DrawHandle(Graphics g, Brush brush, Pen pen, int x, int y, int size) { var rect = new Rectangle(x - size / 2, y - size / 2, size, size); g.FillRectangle(brush, rect); g.DrawRectangle(pen, rect); }
1010        private void DrawHighlightResizeHandles(Graphics g, Rectangle rect) { int handleSize = 6; using (var brush = new SolidBrush(Color.White)) using (var pen = new Pen(Settings.HighlightBorderColor, 1)) { DrawHandle(g, brush, pen, rect.Left, rect.Top, handleSize); DrawHandle(g, brush, pen, rect.Right, rect.Top, handleSize); DrawHandle(g, brush, pen, rect.Left, rect.Bottom, handleSize); DrawHandle(g, brush, pen, rect.Right, rect.Bottom, handleSize); DrawHandle(g, brush, pen, rect.Left + rect.Width / 2, rect.Top, handleSize); DrawHandle(g, brush, pen, rect.Left + rect.Width / 2, rect.Bottom, handleSize); DrawHandle(g, brush, pen, rect.Left, rect.Top + rect.Height / 2, handleSize); DrawHandle(g, brush, pen, rect.Right, rect.Top + rect.Height / 2, handleSize); } }
1011        public void SetThumbnail(Bitmap bmp) { DisposeThumbnail(); if (bmp != null) cachedThumbnail = (Bitmap)bmp.Clone(); }
1012        protected override void Dispose(bool disposing) { if (disposing) { StopGifAnimation(); if (mmbTimer != null) { mmbTimer.Stop(); mmbTimer.Dispose(); } if (blurClickTimer != null) { blurClickTimer.Stop(); blurClickTimer.Dispose(); } DisposeThumbnail(); if (originalBitmap != null) originalBitmap.Dispose(); if (gdiBackBuffer != null) gdiBackBuffer.Dispose(); if (inputTextBox != null) inputTextBox.Dispose(); foreach (var layer in effectLayers) layer.Dispose(); effectLayers.Clear(); } base.Dispose(disposing); }
1013    }
1014}