From 8b45489b1123ea33adec779b32ce8dfb0f1a6a1a Mon Sep 17 00:00:00 2001 From: Killy Date: Sun, 26 Apr 2020 16:50:17 +0300 Subject: [PATCH 01/25] Zoom feature for the editor --- Greenshot/Drawing/Adorners/AbstractAdorner.cs | 17 ++ Greenshot/Drawing/Adorners/MoveAdorner.cs | 2 +- Greenshot/Drawing/Adorners/ResizeAdorner.cs | 2 +- Greenshot/Drawing/Adorners/TargetAdorner.cs | 2 +- Greenshot/Drawing/CropContainer.cs | 17 +- Greenshot/Drawing/DrawableContainer.cs | 2 +- Greenshot/Drawing/DrawableContainerList.cs | 2 +- Greenshot/Drawing/Surface.cs | 100 +++++++-- Greenshot/Drawing/TextContainer.cs | 14 +- Greenshot/Forms/ImageEditorForm.Designer.cs | 212 +++++++++++++++++- Greenshot/Forms/ImageEditorForm.cs | 167 ++++++++++++-- Greenshot/Forms/ImageEditorForm.resx | 81 +++++++ .../icons/fugue/magnifier-zoom-actual.png | Bin 0 -> 742 bytes Greenshot/icons/fugue/magnifier-zoom-fit.png | Bin 0 -> 726 bytes Greenshot/icons/fugue/magnifier-zoom-in.png | Bin 0 -> 733 bytes Greenshot/icons/fugue/magnifier-zoom-out.png | Bin 0 -> 707 bytes Greenshot/icons/fugue/magnifier-zoom.png | Bin 0 -> 676 bytes GreenshotPlugin/Interfaces/ISurface.cs | 19 +- 18 files changed, 585 insertions(+), 52 deletions(-) create mode 100644 Greenshot/icons/fugue/magnifier-zoom-actual.png create mode 100644 Greenshot/icons/fugue/magnifier-zoom-fit.png create mode 100644 Greenshot/icons/fugue/magnifier-zoom-in.png create mode 100644 Greenshot/icons/fugue/magnifier-zoom-out.png create mode 100644 Greenshot/icons/fugue/magnifier-zoom.png diff --git a/Greenshot/Drawing/Adorners/AbstractAdorner.cs b/Greenshot/Drawing/Adorners/AbstractAdorner.cs index 0612753aa..fc3d72371 100644 --- a/Greenshot/Drawing/Adorners/AbstractAdorner.cs +++ b/Greenshot/Drawing/Adorners/AbstractAdorner.cs @@ -116,6 +116,23 @@ namespace Greenshot.Drawing.Adorners } } + /// + /// Return the bounds of the Adorner as displayed on the parent Surface + /// + protected virtual Rectangle SurfaceBounds + { + get + { + Point[] points = { Location }; + var matrix = Owner.Parent.ZoomMatrix; + if (!matrix.IsIdentity) + { + matrix.TransformPoints(points); + } + return new Rectangle(points[0].X - _size.Width / 2, points[0].Y - _size.Height / 2, _size.Width, _size.Height); + } + } + /// /// The adorner is active if the edit status is not idle or undrawn /// diff --git a/Greenshot/Drawing/Adorners/MoveAdorner.cs b/Greenshot/Drawing/Adorners/MoveAdorner.cs index ec3dcf2d3..21c42a9fa 100644 --- a/Greenshot/Drawing/Adorners/MoveAdorner.cs +++ b/Greenshot/Drawing/Adorners/MoveAdorner.cs @@ -142,7 +142,7 @@ namespace Greenshot.Drawing.Adorners { Graphics targetGraphics = paintEventArgs.Graphics; - var bounds = Bounds; + var bounds = SurfaceBounds; GraphicsState state = targetGraphics.Save(); targetGraphics.SmoothingMode = SmoothingMode.None; diff --git a/Greenshot/Drawing/Adorners/ResizeAdorner.cs b/Greenshot/Drawing/Adorners/ResizeAdorner.cs index e75126f10..ef61b17bc 100644 --- a/Greenshot/Drawing/Adorners/ResizeAdorner.cs +++ b/Greenshot/Drawing/Adorners/ResizeAdorner.cs @@ -169,7 +169,7 @@ namespace Greenshot.Drawing.Adorners { Graphics targetGraphics = paintEventArgs.Graphics; - var bounds = Bounds; + var bounds = SurfaceBounds; GraphicsState state = targetGraphics.Save(); targetGraphics.SmoothingMode = SmoothingMode.None; diff --git a/Greenshot/Drawing/Adorners/TargetAdorner.cs b/Greenshot/Drawing/Adorners/TargetAdorner.cs index 8ca0a91ef..23cf29d09 100644 --- a/Greenshot/Drawing/Adorners/TargetAdorner.cs +++ b/Greenshot/Drawing/Adorners/TargetAdorner.cs @@ -96,7 +96,7 @@ namespace Greenshot.Drawing.Adorners { Graphics targetGraphics = paintEventArgs.Graphics; - var bounds = Bounds; + var bounds = SurfaceBounds; targetGraphics.FillRectangle(Brushes.Green, bounds.X, bounds.Y, bounds.Width, bounds.Height); } diff --git a/Greenshot/Drawing/CropContainer.cs b/Greenshot/Drawing/CropContainer.cs index 2dc9761c0..f5288074d 100644 --- a/Greenshot/Drawing/CropContainer.cs +++ b/Greenshot/Drawing/CropContainer.cs @@ -56,7 +56,15 @@ namespace Greenshot.Drawing { /// We need to override the DrawingBound, return a rectangle in the size of the image, to make sure this element is always draw /// (we create a transparent brown over the complete picture) /// - public override Rectangle DrawingBounds => new Rectangle(0,0,_parent?.Width??0, _parent?.Height ?? 0); + public override Rectangle DrawingBounds { + get { + if (_parent?.Image is Image image) { + return new Rectangle(0, 0, image.Width, image.Height); + } else { + return new Rectangle(); + } + } + } public override void Draw(Graphics g, RenderMode rm) { if (_parent == null) @@ -67,17 +75,18 @@ namespace Greenshot.Drawing { using Brush cropBrush = new SolidBrush(Color.FromArgb(100, 150, 150, 100)); Rectangle cropRectangle = GuiRectangle.GetGuiRectangle(Left, Top, Width, Height); Rectangle selectionRect = new Rectangle(cropRectangle.Left - 1, cropRectangle.Top - 1, cropRectangle.Width + 1, cropRectangle.Height + 1); + Size imageSize = _parent.Image.Size; DrawSelectionBorder(g, selectionRect); // top - g.FillRectangle(cropBrush, new Rectangle(0, 0, _parent.Width, cropRectangle.Top)); + g.FillRectangle(cropBrush, new Rectangle(0, 0, imageSize.Width, cropRectangle.Top)); // left g.FillRectangle(cropBrush, new Rectangle(0, cropRectangle.Top, cropRectangle.Left, cropRectangle.Height)); // right - g.FillRectangle(cropBrush, new Rectangle(cropRectangle.Left + cropRectangle.Width, cropRectangle.Top, _parent.Width - (cropRectangle.Left + cropRectangle.Width), cropRectangle.Height)); + g.FillRectangle(cropBrush, new Rectangle(cropRectangle.Left + cropRectangle.Width, cropRectangle.Top, imageSize.Width - (cropRectangle.Left + cropRectangle.Width), cropRectangle.Height)); // bottom - g.FillRectangle(cropBrush, new Rectangle(0, cropRectangle.Top + cropRectangle.Height, _parent.Width, _parent.Height - (cropRectangle.Top + cropRectangle.Height))); + g.FillRectangle(cropBrush, new Rectangle(0, cropRectangle.Top + cropRectangle.Height, imageSize.Width, imageSize.Height - (cropRectangle.Top + cropRectangle.Height))); } public override bool HasContextMenu { diff --git a/Greenshot/Drawing/DrawableContainer.cs b/Greenshot/Drawing/DrawableContainer.cs index 8d805b1f0..65ff42de3 100644 --- a/Greenshot/Drawing/DrawableContainer.cs +++ b/Greenshot/Drawing/DrawableContainer.cs @@ -298,7 +298,7 @@ namespace Greenshot.Drawing public virtual void Invalidate() { if (Status != EditStatus.UNDRAWN) { - _parent?.Invalidate(DrawingBounds); + _parent?.InvalidateElements(DrawingBounds); } } diff --git a/Greenshot/Drawing/DrawableContainerList.cs b/Greenshot/Drawing/DrawableContainerList.cs index d3c325f5b..17b879538 100644 --- a/Greenshot/Drawing/DrawableContainerList.cs +++ b/Greenshot/Drawing/DrawableContainerList.cs @@ -286,7 +286,7 @@ namespace Greenshot.Drawing { { region = Rectangle.Union(region, dc.DrawingBounds); } - Parent.Invalidate(region); + Parent.InvalidateElements(region); } /// /// Indicates whether the given list of elements can be pulled up, diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index bd56db3a2..5c38bea8e 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -298,10 +298,34 @@ namespace Greenshot.Drawing set { _image = value; - Size = _image.Size; + UpdateSize(); } } + [NonSerialized] + private float _zoomFactor = 1.0f; + public float ZoomFactor + { + get => _zoomFactor; + set + { + _zoomFactor = value; + ZoomMatrix = new Matrix(_zoomFactor, 0, 0, _zoomFactor, 0, 0); + UpdateSize(); + } + } + + public Matrix ZoomMatrix { get; private set; } = new Matrix(1, 0, 0, 1, 0, 0); + + /// + /// Sets the surface size as zoomed image size. + /// + private void UpdateSize() + { + var size = _image.Size; + Size = new Size((int)(size.Width * _zoomFactor), (int)(size.Height * _zoomFactor)); + } + /// /// The field aggregator is that which is used to have access to all the fields inside the currently selected elements. /// e.g. used to decided if and which line thickness is shown when multiple elements are selected. @@ -447,7 +471,6 @@ namespace Greenshot.Drawing // Set new values Image = newImage; - Size = newImage.Size; _modified = true; } @@ -1086,6 +1109,12 @@ namespace Greenshot.Drawing return null; } + /// + /// Translate mouse coordinates as if they were applied directly to unscaled image. + /// + private MouseEventArgs InverseZoomMouseCoordinates(MouseEventArgs e) + => new MouseEventArgs(e.Button, e.Clicks, (int)(e.X / _zoomFactor), (int)(e.Y / _zoomFactor), e.Delta); + /// /// This event handler is called when someone presses the mouse on a surface. /// @@ -1093,6 +1122,7 @@ namespace Greenshot.Drawing /// private void SurfaceMouseDown(object sender, MouseEventArgs e) { + e = InverseZoomMouseCoordinates(e); // Handle Adorners var adorner = FindActiveAdorner(e); @@ -1187,6 +1217,7 @@ namespace Greenshot.Drawing /// private void SurfaceMouseUp(object sender, MouseEventArgs e) { + e = InverseZoomMouseCoordinates(e); // Handle Adorners var adorner = FindActiveAdorner(e); @@ -1276,6 +1307,8 @@ namespace Greenshot.Drawing /// private void SurfaceMouseMove(object sender, MouseEventArgs e) { + e = InverseZoomMouseCoordinates(e); + // Handle Adorners var adorner = FindActiveAdorner(e); if (adorner != null) @@ -1371,6 +1404,25 @@ namespace Greenshot.Drawing return GetImage(RenderMode.EXPORT); } + private Rectangle ZoomClipRectangle(Rectangle rc, double scale) + => new Rectangle( + (int)(rc.X * scale), + (int)(rc.Y * scale), + (int)((rc.Width + 1) * scale) + 1, // making sure to redraw enough pixels when moving scaled image + (int)((rc.Height + 1) * scale) + 1 + ); + + private RectangleF ZoomClipRectangle(RectangleF rc, double scale) + => new RectangleF( + (float)Math.Floor(rc.X * scale), + (float)Math.Floor(rc.Y * scale), + (float)Math.Ceiling(rc.Width * scale), + (float)Math.Ceiling(rc.Height * scale) + ); + + public void InvalidateElements(Rectangle rc) + => Invalidate(ZoomClipRectangle(rc, _zoomFactor)); + /// /// This is the event handler for the Paint Event, try to draw as little as possible! /// @@ -1379,14 +1431,16 @@ namespace Greenshot.Drawing private void SurfacePaint(object sender, PaintEventArgs paintEventArgs) { Graphics targetGraphics = paintEventArgs.Graphics; - Rectangle clipRectangle = paintEventArgs.ClipRectangle; - if (Rectangle.Empty.Equals(clipRectangle)) + Rectangle targetClipRectangle = paintEventArgs.ClipRectangle; + if (Rectangle.Empty.Equals(targetClipRectangle)) { LOG.Debug("Empty cliprectangle??"); return; } + Rectangle imageClipRectangle = ZoomClipRectangle(targetClipRectangle, 1.0 / _zoomFactor); - if (_elements.HasIntersectingFilters(clipRectangle)) + bool isZoomedIn = ZoomFactor > 1f; + if (_elements.HasIntersectingFilters(imageClipRectangle) || isZoomedIn) { if (_buffer != null) { @@ -1409,18 +1463,38 @@ namespace Greenshot.Drawing //graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; //graphics.CompositingQuality = CompositingQuality.HighQuality; //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - DrawBackground(graphics, clipRectangle); - graphics.DrawImage(Image, clipRectangle, clipRectangle, GraphicsUnit.Pixel); - graphics.SetClip(targetGraphics); - _elements.Draw(graphics, _buffer, RenderMode.EDIT, clipRectangle); + DrawBackground(graphics, imageClipRectangle); + graphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + graphics.SetClip(ZoomClipRectangle(targetGraphics.ClipBounds, 1.0 / _zoomFactor)); + _elements.Draw(graphics, _buffer, RenderMode.EDIT, imageClipRectangle); } - targetGraphics.DrawImage(_buffer, clipRectangle, clipRectangle, GraphicsUnit.Pixel); + targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); + if (isZoomedIn) + { + var state = targetGraphics.Save(); + targetGraphics.SmoothingMode = SmoothingMode.None; + targetGraphics.InterpolationMode = InterpolationMode.NearestNeighbor; + targetGraphics.CompositingQuality = CompositingQuality.HighQuality; + targetGraphics.PixelOffsetMode = PixelOffsetMode.None; + + targetGraphics.DrawImage(_buffer, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + + targetGraphics.Restore(state); + } + else + { + targetGraphics.DrawImage(_buffer, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + } + targetGraphics.ResetTransform(); } else { - DrawBackground(targetGraphics, clipRectangle); - targetGraphics.DrawImage(Image, clipRectangle, clipRectangle, GraphicsUnit.Pixel); - _elements.Draw(targetGraphics, null, RenderMode.EDIT, clipRectangle); + DrawBackground(targetGraphics, targetClipRectangle); + + targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); + targetGraphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); + targetGraphics.ResetTransform(); } // No clipping for the adorners diff --git a/Greenshot/Drawing/TextContainer.cs b/Greenshot/Drawing/TextContainer.cs index 0f4866d71..bd4c75274 100644 --- a/Greenshot/Drawing/TextContainer.cs +++ b/Greenshot/Drawing/TextContainer.cs @@ -442,14 +442,20 @@ namespace Greenshot.Drawing correction = -1; } Rectangle absRectangle = GuiRectangle.GetGuiRectangle(Left, Top, Width, Height); - _textBox.Left = absRectangle.Left + lineWidth; - _textBox.Top = absRectangle.Top + lineWidth; + Point[] points = { absRectangle.Location, absRectangle.Location + absRectangle.Size }; + var matrix = Parent.ZoomMatrix; + if (!matrix.IsIdentity) + { + matrix.TransformPoints(points); + } + _textBox.Left = points[0].X + lineWidth; + _textBox.Top = points[0].Y + lineWidth; if (lineThickness <= 1) { lineWidth = 0; } - _textBox.Width = absRectangle.Width - 2 * lineWidth + correction; - _textBox.Height = absRectangle.Height - 2 * lineWidth + correction; + _textBox.Width = points[1].X - points[0].X - 2 * lineWidth + correction; + _textBox.Height = points[1].Y - points[0].Y - 2 * lineWidth + correction; } public override void ApplyBounds(RectangleF newBounds) diff --git a/Greenshot/Forms/ImageEditorForm.Designer.cs b/Greenshot/Forms/ImageEditorForm.Designer.cs index 9a653cc21..5789a19a6 100644 --- a/Greenshot/Forms/ImageEditorForm.Designer.cs +++ b/Greenshot/Forms/ImageEditorForm.Designer.cs @@ -194,6 +194,26 @@ namespace Greenshot { this.alignLeftToolStripMenuItem = new GreenshotPlugin.Controls.GreenshotToolStripMenuItem(); this.alignCenterToolStripMenuItem = new GreenshotPlugin.Controls.GreenshotToolStripMenuItem(); this.alignRightToolStripMenuItem = new GreenshotPlugin.Controls.GreenshotToolStripMenuItem(); + this.zoomMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); + this.zoomInMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zoomOutMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zoomMenuSeparator1 = new System.Windows.Forms.ToolStripSeparator(); + this.zoomBestFitMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zoomMenuSeparator2 = new System.Windows.Forms.ToolStripSeparator(); + this.zoom25MenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zoom50MenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zoom66MenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zoom75MenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zoomMenuSeparator3 = new System.Windows.Forms.ToolStripSeparator(); + this.zoomActualSizeMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zoomMenuSeparator4 = new System.Windows.Forms.ToolStripSeparator(); + this.zoom200MenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zoom300MenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zoom400MenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zoom600MenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zoomStatusDropDownBtn = new System.Windows.Forms.ToolStripDropDownButton(); + this.zoomMainMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.statusStripSpacer = new System.Windows.Forms.ToolStripStatusLabel(); this.topToolStripContainer.BottomToolStripPanel.SuspendLayout(); this.topToolStripContainer.ContentPanel.SuspendLayout(); this.topToolStripContainer.LeftToolStripPanel.SuspendLayout(); @@ -203,6 +223,7 @@ namespace Greenshot { this.tableLayoutPanel1.SuspendLayout(); this.toolsToolStrip.SuspendLayout(); this.menuStrip1.SuspendLayout(); + this.zoomMenuStrip.SuspendLayout(); this.destinationsToolStrip.SuspendLayout(); this.propertiesToolStrip.SuspendLayout(); this.fileSavedStatusContextMenu.SuspendLayout(); @@ -238,7 +259,9 @@ namespace Greenshot { this.statusStrip1.Dock = System.Windows.Forms.DockStyle.None; this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.dimensionsLabel, - this.statusLabel}); + this.statusLabel, + this.statusStripSpacer, + this.zoomStatusDropDownBtn}); this.statusStrip1.Location = new System.Drawing.Point(0, 0); this.statusStrip1.Name = "statusStrip1"; this.statusStrip1.Size = new System.Drawing.Size(785, 24); @@ -538,6 +561,7 @@ namespace Greenshot { this.editToolStripMenuItem, this.objectToolStripMenuItem, this.pluginToolStripMenuItem, + this.zoomMainMenuItem, this.helpToolStripMenuItem}); this.menuStrip1.Name = "menuStrip1"; this.menuStrip1.BackColor = System.Drawing.SystemColors.Control; @@ -1624,6 +1648,171 @@ namespace Greenshot { this.alignRightToolStripMenuItem.Name = "alignRightToolStripMenuItem"; this.alignRightToolStripMenuItem.Tag = System.Drawing.StringAlignment.Far; // + // zoomMainMenuItem + // + this.zoomMainMenuItem.DropDown = this.zoomMenuStrip; + this.zoomMainMenuItem.Name = "zoomMainMenuItem"; + this.zoomMainMenuItem.Size = new System.Drawing.Size(51, 20); + this.zoomMainMenuItem.Text = "Zoom"; + // + // zoomMenuStrip + // + this.zoomMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.zoomInMenuItem, + this.zoomOutMenuItem, + this.zoomMenuSeparator1, + this.zoomBestFitMenuItem, + this.zoomMenuSeparator2, + this.zoom25MenuItem, + this.zoom50MenuItem, + this.zoom66MenuItem, + this.zoom75MenuItem, + this.zoomMenuSeparator3, + this.zoomActualSizeMenuItem, + this.zoomMenuSeparator4, + this.zoom200MenuItem, + this.zoom300MenuItem, + this.zoom400MenuItem, + this.zoom600MenuItem}); + this.zoomMenuStrip.Name = "zoomMenuStrip"; + this.zoomMenuStrip.Size = new System.Drawing.Size(210, 292); + // + // zoomInMenuItem + // + this.zoomInMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("zoomInMenuItem.Image"))); + this.zoomInMenuItem.Name = "zoomInMenuItem"; + this.zoomInMenuItem.ShortcutKeyDisplayString = "Ctrl++"; + this.zoomInMenuItem.Size = new System.Drawing.Size(209, 22); + this.zoomInMenuItem.Text = "Zoom In"; + this.zoomInMenuItem.Click += new System.EventHandler(this.ZoomInMenuItemClick); + // + // zoomOutMenuItem + // + this.zoomOutMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("zoomOutMenuItem.Image"))); + this.zoomOutMenuItem.Name = "zoomOutMenuItem"; + this.zoomOutMenuItem.ShortcutKeyDisplayString = "Ctrl+-"; + this.zoomOutMenuItem.Size = new System.Drawing.Size(209, 22); + this.zoomOutMenuItem.Text = "Zoom Out"; + this.zoomOutMenuItem.Click += new System.EventHandler(this.ZoomOutMenuItemClick); + // + // zoomMenuSeparator1 + // + this.zoomMenuSeparator1.Name = "zoomMenuSeparator1"; + this.zoomMenuSeparator1.Size = new System.Drawing.Size(206, 6); + // + // zoomBestFitMenuItem + // + this.zoomBestFitMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("zoomBestFitMenuItem.Image"))); + this.zoomBestFitMenuItem.Name = "zoomBestFitMenuItem"; + this.zoomBestFitMenuItem.ShortcutKeyDisplayString = "Ctrl+*"; + this.zoomBestFitMenuItem.Size = new System.Drawing.Size(209, 22); + this.zoomBestFitMenuItem.Text = "Best Fit"; + this.zoomBestFitMenuItem.Click += new System.EventHandler(this.ZoomBestFitMenuItemClick); + // + // zoomMenuSeparator2 + // + this.zoomMenuSeparator2.Name = "zoomMenuSeparator2"; + this.zoomMenuSeparator2.Size = new System.Drawing.Size(206, 6); + // + // zoom25MenuItem + // + this.zoom25MenuItem.Name = "zoom25MenuItem"; + this.zoom25MenuItem.Size = new System.Drawing.Size(209, 22); + this.zoom25MenuItem.Tag = "25"; + this.zoom25MenuItem.Text = "25%"; + this.zoom25MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); + // + // zoom50MenuItem + // + this.zoom50MenuItem.Name = "zoom50MenuItem"; + this.zoom50MenuItem.Size = new System.Drawing.Size(209, 22); + this.zoom50MenuItem.Tag = "50"; + this.zoom50MenuItem.Text = "50%"; + this.zoom50MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); + // + // zoom66MenuItem + // + this.zoom66MenuItem.Name = "zoom66MenuItem"; + this.zoom66MenuItem.Size = new System.Drawing.Size(209, 22); + this.zoom66MenuItem.Tag = "66"; + this.zoom66MenuItem.Text = "66%"; + this.zoom66MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); + // + // zoom75MenuItem + // + this.zoom75MenuItem.Name = "zoom75MenuItem"; + this.zoom75MenuItem.Size = new System.Drawing.Size(209, 22); + this.zoom75MenuItem.Tag = "75"; + this.zoom75MenuItem.Text = "75%"; + this.zoom75MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); + // + // zoomMenuSeparator3 + // + this.zoomMenuSeparator3.Name = "zoomMenuSeparator3"; + this.zoomMenuSeparator3.Size = new System.Drawing.Size(206, 6); + // + // zoomActualSizeMenuItem + // + this.zoomActualSizeMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("zoomActualSizeMenuItem.Image"))); + this.zoomActualSizeMenuItem.Name = "zoomActualSizeMenuItem"; + this.zoomActualSizeMenuItem.ShortcutKeyDisplayString = "Ctrl+/"; + this.zoomActualSizeMenuItem.Size = new System.Drawing.Size(209, 22); + this.zoomActualSizeMenuItem.Tag = "100"; + this.zoomActualSizeMenuItem.Text = "100% - Actual Size"; + this.zoomActualSizeMenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); + // + // zoomMenuSeparator4 + // + this.zoomMenuSeparator4.Name = "zoomMenuSeparator4"; + this.zoomMenuSeparator4.Size = new System.Drawing.Size(206, 6); + // + // zoom200MenuItem + // + this.zoom200MenuItem.Name = "zoom200MenuItem"; + this.zoom200MenuItem.Size = new System.Drawing.Size(209, 22); + this.zoom200MenuItem.Tag = "200"; + this.zoom200MenuItem.Text = "200%"; + this.zoom200MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); + // + // zoom300MenuItem + // + this.zoom300MenuItem.Name = "zoom300MenuItem"; + this.zoom300MenuItem.Size = new System.Drawing.Size(209, 22); + this.zoom300MenuItem.Tag = "300"; + this.zoom300MenuItem.Text = "300%"; + this.zoom300MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); + // + // zoom400MenuItem + // + this.zoom400MenuItem.Name = "zoom400MenuItem"; + this.zoom400MenuItem.Size = new System.Drawing.Size(209, 22); + this.zoom400MenuItem.Tag = "400"; + this.zoom400MenuItem.Text = "400%"; + this.zoom400MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); + // + // zoom600MenuItem + // + this.zoom600MenuItem.Name = "zoom600MenuItem"; + this.zoom600MenuItem.Size = new System.Drawing.Size(209, 22); + this.zoom600MenuItem.Tag = "600"; + this.zoom600MenuItem.Text = "600%"; + this.zoom600MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); + // + // statusStripSpacer + // + this.statusStripSpacer.Name = "statusStripSpacer"; + this.statusStripSpacer.Size = new System.Drawing.Size(599, 19); + this.statusStripSpacer.Spring = true; + // + // zoomStatusDropDownBtn + // + this.zoomStatusDropDownBtn.DropDown = this.zoomMenuStrip; + this.zoomStatusDropDownBtn.Image = ((System.Drawing.Image)(resources.GetObject("zoomStatusDropDownBtn.Image"))); + this.zoomStatusDropDownBtn.ImageTransparentColor = System.Drawing.Color.Magenta; + this.zoomStatusDropDownBtn.Name = "zoomStatusDropDownBtn"; + this.zoomStatusDropDownBtn.Size = new System.Drawing.Size(64, 22); + this.zoomStatusDropDownBtn.Text = "100%"; + // // ImageEditorForm // this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); @@ -1645,6 +1834,7 @@ namespace Greenshot { this.statusStrip1.ResumeLayout(true); this.tableLayoutPanel1.ResumeLayout(true); this.toolsToolStrip.ResumeLayout(true); + this.zoomMenuStrip.ResumeLayout(false); this.menuStrip1.ResumeLayout(true); this.destinationsToolStrip.ResumeLayout(true); this.propertiesToolStrip.ResumeLayout(true); @@ -1794,5 +1984,25 @@ namespace Greenshot { private Greenshot.Controls.ToolStripColorButton btnLineColor; private GreenshotPlugin.Controls.GreenshotToolStripMenuItem autoCropToolStripMenuItem; private System.Windows.Forms.ToolStripSeparator toolStripSeparator17; + private System.Windows.Forms.ContextMenuStrip zoomMenuStrip; + private System.Windows.Forms.ToolStripMenuItem zoomInMenuItem; + private System.Windows.Forms.ToolStripMenuItem zoomOutMenuItem; + private System.Windows.Forms.ToolStripSeparator zoomMenuSeparator1; + private System.Windows.Forms.ToolStripMenuItem zoomBestFitMenuItem; + private System.Windows.Forms.ToolStripSeparator zoomMenuSeparator2; + private System.Windows.Forms.ToolStripMenuItem zoom25MenuItem; + private System.Windows.Forms.ToolStripMenuItem zoom50MenuItem; + private System.Windows.Forms.ToolStripMenuItem zoom66MenuItem; + private System.Windows.Forms.ToolStripMenuItem zoom75MenuItem; + private System.Windows.Forms.ToolStripSeparator zoomMenuSeparator3; + private System.Windows.Forms.ToolStripMenuItem zoomActualSizeMenuItem; + private System.Windows.Forms.ToolStripSeparator zoomMenuSeparator4; + private System.Windows.Forms.ToolStripMenuItem zoom200MenuItem; + private System.Windows.Forms.ToolStripMenuItem zoom300MenuItem; + private System.Windows.Forms.ToolStripMenuItem zoom400MenuItem; + private System.Windows.Forms.ToolStripMenuItem zoom600MenuItem; + private System.Windows.Forms.ToolStripDropDownButton zoomStatusDropDownBtn; + private System.Windows.Forms.ToolStripMenuItem zoomMainMenuItem; + private System.Windows.Forms.ToolStripStatusLabel statusStripSpacer; } } diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index bcb8c275c..dd9ec94e1 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -66,6 +66,12 @@ namespace Greenshot { // whether part of the editor controls are disabled depending on selected item(s) private bool _controlsDisabledDueToConfirmable; + /// + /// All provided zoom values (in percents) in ascending order. + /// + private readonly int[] ZOOM_VALUES = new[] { 25, 50, 66, 75, 100, 200, 300, 400, 600 }; + private int _zoomValue = 100; + /// /// An Implementation for the IImageEditor, this way Plugins have access to the HWND handles wich can be used with Win32 API calls. /// @@ -420,20 +426,14 @@ namespace Greenshot { /// /// /// - private void SurfaceSizeChanged(object sender, EventArgs e) { - if (EditorConfiguration.MatchSizeToCapture) { - // Set editor's initial size to the size of the surface plus the size of the chrome - Size imageSize = Surface.Image.Size; - Size currentFormSize = Size; - Size currentImageClientSize = panel1.ClientSize; - int minimumFormWidth = 650; - int minimumFormHeight = 530; - int newWidth = Math.Max(minimumFormWidth, currentFormSize.Width - currentImageClientSize.Width + imageSize.Width); - int newHeight = Math.Max(minimumFormHeight, currentFormSize.Height - currentImageClientSize.Height + imageSize.Height); - Size = new Size(newWidth, newHeight); + private void SurfaceSizeChanged(object sender, EventArgs e) + { + if (EditorConfiguration.MatchSizeToCapture) + { + Size = GetOptimalWindowSize(); } dimensionsLabel.Text = Surface.Image.Width + "x" + Surface.Image.Height; - ImageEditorFormResize(sender, new EventArgs()); + AlignCanvasPositionAfterResize(); } public ISurface Surface { @@ -860,15 +860,34 @@ namespace Greenshot { case Keys.Oemcomma: // Rotate CCW Ctrl + , RotateCcwToolstripButtonClick(sender, e); break; - case Keys.OemPeriod: // Rotate CW Ctrl + . + case Keys.OemPeriod: // Rotate CW Ctrl + . RotateCwToolstripButtonClick(sender, e); break; - case Keys.Add: // Ctrl + + - case Keys.Oemplus: // Ctrl + + + case Keys.Add: // Ctrl + Num+ + case Keys.Oemplus: // Ctrl + + + ZoomInMenuItemClick(sender, e); + break; + case Keys.Subtract: // Ctrl + Num- + case Keys.OemMinus: // Ctrl + - + ZoomOutMenuItemClick(sender, e); + break; + case Keys.Divide: // Ctrl + Num/ + case Keys.OemQuestion: // Ctrl + / (?) + ZoomSetValueMenuItemClick(zoomActualSizeMenuItem, e); + break; + case Keys.Multiply: // Ctrl + Num* + case Keys.D8: // Ctrl + 8 (*) + ZoomBestFitMenuItemClick(sender, e); + break; + } + } else if (e.Modifiers.Equals(Keys.Control | Keys.Shift)) { + switch (e.KeyCode) { + case Keys.Add: // Ctrl + Shift + Num+ + case Keys.Oemplus: // Ctrl + Shift + + EnlargeCanvasToolStripMenuItemClick(sender, e); break; - case Keys.Subtract: // Ctrl + - - case Keys.OemMinus: // Ctrl + - + case Keys.Subtract: // Ctrl + Shift + Num- + case Keys.OemMinus: // Ctrl + Shift + - ShrinkCanvasToolStripMenuItemClick(sender, e); break; } @@ -1444,28 +1463,130 @@ namespace Greenshot { } private void ImageEditorFormResize(object sender, EventArgs e) { + AlignCanvasPositionAfterResize(); + } + + private void AlignCanvasPositionAfterResize() { if (Surface?.Image == null || panel1 == null) { return; } - Size imageSize = Surface.Image.Size; - Size currentClientSize = panel1.ClientSize; var canvas = Surface as Control; + Size canvasSize = canvas.Size; + Size currentClientSize = panel1.ClientSize; Panel panel = (Panel) canvas?.Parent; if (panel == null) { return; } int offsetX = -panel.HorizontalScroll.Value; int offsetY = -panel.VerticalScroll.Value; - if (currentClientSize.Width > imageSize.Width) { - canvas.Left = offsetX + (currentClientSize.Width - imageSize.Width) / 2; + if (currentClientSize.Width > canvasSize.Width) { + canvas.Left = offsetX + (currentClientSize.Width - canvasSize.Width) / 2; } else { canvas.Left = offsetX + 0; } - if (currentClientSize.Height > imageSize.Height) { - canvas.Top = offsetY + (currentClientSize.Height - imageSize.Height) / 2; + if (currentClientSize.Height > canvasSize.Height) { + canvas.Top = offsetY + (currentClientSize.Height - canvasSize.Height) / 2; } else { canvas.Top = offsetY + 0; } } + + /// + /// Compute a size as a sum of surface size and chrome. + /// Upper bound is working area of the screen. Lower bound is fixed value. + /// + private Size GetOptimalWindowSize() { + var surfaceSize = (Surface as Control).Size; + var chromeSize = GetChromeSize(); + var newWidth = chromeSize.Width + surfaceSize.Width; + var newHeight = chromeSize.Height + surfaceSize.Height; + + // Upper bound. Don't make it bigger than the available working area. + var screen = Screen.FromControl(this); + var workingArea = screen.WorkingArea; + newWidth = Math.Min(newWidth, workingArea.Width); + newHeight = Math.Min(newHeight, workingArea.Height); + + // Lower bound. Don't make it smaller than a fixed value. + int minimumFormWidth = 650; + int minimumFormHeight = 530; + newWidth = Math.Max(minimumFormWidth, newWidth); + newHeight = Math.Max(minimumFormHeight, newHeight); + + return new Size(newWidth, newHeight); + } + + private Size GetChromeSize() + => Size - panel1.ClientSize; + + /// + /// Compute a size that the form can take without getting out of working area of the screen. + /// + private Size GetAvailableScreenSpace() { + var screen = Screen.FromControl(this); + var screenBounds = screen.Bounds; + var workingArea = screen.WorkingArea; + if (Left > screenBounds.Left && Top > screenBounds.Top) { + return new Size(workingArea.Width - Left, workingArea.Height - Top); + } else { + return workingArea.Size; + } + } + + private void ZoomInMenuItemClick(object sender, EventArgs e) { + var nextIndex = Array.FindIndex(ZOOM_VALUES, v => v > _zoomValue); + var nextValue = nextIndex < 0 ? ZOOM_VALUES[ZOOM_VALUES.Length - 1] : ZOOM_VALUES[nextIndex]; + + ZoomSetValue(nextValue); + } + + private void ZoomOutMenuItemClick(object sender, EventArgs e) { + var nextIndex = Array.FindLastIndex(ZOOM_VALUES, v => v < _zoomValue); + var nextValue = nextIndex < 0 ? ZOOM_VALUES[0] : ZOOM_VALUES[nextIndex]; + + ZoomSetValue(nextValue); + } + + private void ZoomSetValueMenuItemClick(object sender, EventArgs e) { + var senderMenuItem = (ToolStripMenuItem)sender; + int zoomPercent = int.Parse((string)senderMenuItem.Tag); + + ZoomSetValue(zoomPercent); + } + + private void ZoomBestFitMenuItemClick(object sender, EventArgs e) { + var maxWindowSize = GetAvailableScreenSpace(); + var chromeSize = GetChromeSize(); + var maxImageSize = maxWindowSize - chromeSize; + var imageSize = Surface.Image.Size; + + static bool isFit(int zoom, int source, int boundary) + => source * zoom / 100 <= boundary; + + var nextValue = Array.FindLast( + ZOOM_VALUES, + zoom => isFit(zoom, imageSize.Width, maxImageSize.Width) + && isFit(zoom, imageSize.Height, maxImageSize.Height) + ); + + ZoomSetValue(nextValue); + } + + private void ZoomSetValue(int value) { + _zoomValue = value; + Surface.ZoomFactor = 1f * value / 100; + + // Update zoom controls + string valueString = value.ToString(); + zoomStatusDropDownBtn.Text = valueString + "%"; + foreach (var item in zoomMenuStrip.Items) { + if (item is ToolStripMenuItem menuItem) { + menuItem.Checked = menuItem.Tag as string == valueString; + } + } + + Size = GetOptimalWindowSize(); + AlignCanvasPositionAfterResize(); + } } } diff --git a/Greenshot/Forms/ImageEditorForm.resx b/Greenshot/Forms/ImageEditorForm.resx index e4188604b..025543b10 100644 --- a/Greenshot/Forms/ImageEditorForm.resx +++ b/Greenshot/Forms/ImageEditorForm.resx @@ -937,6 +937,84 @@ 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 + AAAAAElFTkSuQmCC + + + + + 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 @@ -1026,4 +1104,7 @@ 17, 17 + + 782, 17 + \ No newline at end of file diff --git a/Greenshot/icons/fugue/magnifier-zoom-actual.png b/Greenshot/icons/fugue/magnifier-zoom-actual.png new file mode 100644 index 0000000000000000000000000000000000000000..21cf71d635eba0f9646931cbe60c96972a285ac4 GIT binary patch literal 742 zcmV)l(v^w`AQ8JqUw}r_G&HU#Qq<#yg(0so zO-lat&+;<%hp_CWo#kl5vfLaV)&Sz0bd zYbr|7BtR6)br1wT>~?`dUoUIyaaddn_cz+du77Z{kmhI#P{;i?o976%MRa-sK@b`- zGU(-s#Fyvg;^NJtJmI&>ijv9+LTTHJ4#0d2;-;()Q?CiO%NO{Mae$_wEkJWjEEz%4)^@ z>K*TZ23rVel{9P!9>5cLF%$|f27?o~Tl#LiiN&v7jm67#lgKdUh^ki7BnJK!e8S;q zyD&RDHx~?EzE>=6KeZ~Q3`H^T_l+so{j-C=UICqVyFH^bGjr3y;H8Jz?6>Eb&VU1$ zo8YtJPg1FkB}^s20ZmVU|E$-;!vl7n=hL{9w|h>$u=>vg6nVD)v7Z7A Y06Z)@+8V}-SpWb407*qoM6N<$g5B>}-T(jq literal 0 HcmV?d00001 diff --git a/Greenshot/icons/fugue/magnifier-zoom-fit.png b/Greenshot/icons/fugue/magnifier-zoom-fit.png new file mode 100644 index 0000000000000000000000000000000000000000..8364359fdea5b43e86d72af9e429679c0dd59064 GIT binary patch literal 726 zcmV;{0xA88P)~8 zhaein|M}?}<0fHXUh&(Fch!X_uh z!v)kQ4R*o5e@y83>GK!ge}m*K!?!W8voi7Uak5H+Xb>L+p1=6^2c!;c04pmuI^K8s z{6`_6jZ7fEp5JbsAK(8`Rq^;8nMnnuTFdsbq@m1Z#yWGG3{PkqPjQ{`gtiLW>20Hu{@16Ua>B=3Z z7algeKjoxYcpg9h^L5{euUuOWe&l9hE!qWr$3{lL}q5L?mvIN-T>)gLDFm?Dk|<*TwGZh8ynX9{{6c}ygxtOW@KbOgpo`c z{=!)-kXZQVt*or>T3A?_9~&Dy{qf^R%YnkT8HfO21AhMesaaLkmY}I_yve{tO>vL!1h_O%NOeA7nl{+Odc3RS}`sS6>vt2Ys?m5c{B%Du^4kDh$_3 zpR|;=h-;JnBx$l6?+u%{3~}IrkCU8xzVF_1&$+m&s@TQwI;JMrX@*;2Yh68G-xUW2I*?`ClFWGgwV%zF$vcrFzuNp zAd2D+GTe0t0408I!ufS1Kw11A>wdG*U~^bObDx{!yPR?%k*vpM1>^`sXf_)=!1LUq z&x=+42WXSaN>PYBdHtQ6CJBbXG5lD_ZVw#9f1o@~faiHV#z%ro&Baho6J>BKKsBQ{ z@{$6&+8xDoUuiLOkZwABauNTpa-TB((m<;5G$P8#hI zptka)jywz}a3&I2yvee&w_N@kuhN-oSJRosj!85aEHOoCj?S~}rF+HV_a`u1!1iF8fFw!5rKQ{R zb8}Z7=5k*WFqFmiXqo_gSue-NhMgS8qU%0C5g)VBYzUjhsOn}0<&?S0Lw P00000NkvXXu0mjfX;4x~ literal 0 HcmV?d00001 diff --git a/Greenshot/icons/fugue/magnifier-zoom-out.png b/Greenshot/icons/fugue/magnifier-zoom-out.png new file mode 100644 index 0000000000000000000000000000000000000000..3ec2b7a5d32c41a3313a95bec8f326ba356b3b46 GIT binary patch literal 707 zcmV;!0zCbRP)6dkX{uLg1LH81P=-xq6aa85~3GVVpJ&FNG@s! zNl@F}{OxXbJL~MymSlUd;DZM<`~A#&ytkO5C?FUN0;2yEI52S?Y=y;Q1tybe6vy!> zx`t7@QI=7rA%tL`SA;_OMsw{bsh_Vkj-q=zJt(xBCxazf_)9LA_qrcMu*A3>TbM+GV5(hbY4y zu-%c_iJQr{i} ztCCUuST`t2uvw|LWuz->1{7t{8>DlexWitLwXxRdNEC??-5^u|SkAK!q)TfCF8&Ux z0s?g>)RgGZgZKH_MG{>T!iW!_8)9Zza2gEuIJtBFikIrF9Gf{I4&Hyv z;-#{odgi!I^bC>KQblg1vn}I`x161%>=CH63ZzavhBokGBoe*JFtfLuBR5`Wa@VeA za*du%BnWd{mRki?Lw1Re4h#&QUtEmE80PZb_4Tz!Hjyo281Zh$nu3k()8-W5IiJrz zy|56QXP8U(N~NDq(B~r9MQ8@1C3;;}Y5)Y3gBsBm4002ovPDHLkV1nfvIIaKy literal 0 HcmV?d00001 diff --git a/Greenshot/icons/fugue/magnifier-zoom.png b/Greenshot/icons/fugue/magnifier-zoom.png new file mode 100644 index 0000000000000000000000000000000000000000..941b1035673a42d2c0dfe636ef27fd1d7236775f GIT binary patch literal 676 zcmV;V0$crwP);C@z`;D((zc8>c-~j*s{|o&2^LwAVGRsS2O^(~@ za_lc(zWw*(#O3e0=dS(O4wNqfDq9aW;Kz?2D4Kz`YW(~6x6jL&|E8Wg8<(62r!W&E z1Eaho6Pv6A?|&hI-?!Io|Je8Y&)@4zAPrx>d`7{aK7A}zk^lcvRgs-Tkei(qq#15F zkZrEZ$EU8u@CKv~?1E39KB4=-Nmo<$CXnWM_Ws+?XYUz)qAM`e=KHf_-vcM87ck_d zWhA*C!Z@1Je0=CZ#KFS+6R1xb?1g{-n9%Xl=P$qihHB=;7Rb+EeES1Z2R49}l^Y%J zJAMA65Hk}K^KYOsbj|<%{bxLR_JaUO9oP$!-#dVU{}@1N0)&6F>CQcVI&z(f&bO!1 zLY)6tks_O!iIL^Zr7u4Yoctlk&d%4(f*Jq+ /// This returns false if the container is deleted but still in the undo stack bool IsOnSurface(IDrawableContainer container); - void Invalidate(Rectangle rectangleToInvalidate); void Invalidate(); + /// + /// Invalidates the specified region of the Surface. + /// Takes care of the Surface zoom level, accepts rectangle in the coordinate space of the Image. + /// + /// Bounding rectangle for updated elements, in the coordinate space of the Image. + void InvalidateElements(Rectangle rectangleToInvalidate); bool Modified { get; @@ -189,6 +195,15 @@ namespace GreenshotPlugin.Interfaces int Width { get; } int Height { get; } + /// + /// Zoom value applied to the surface. 1.0f for actual size (100%). + /// + float ZoomFactor { get; set; } + /// + /// Matrix representing zoom applied to the surface. + /// + Matrix ZoomMatrix { get; } + void MakeUndoable(IMemento memento, bool allowMerge); } -} \ No newline at end of file +} From 136953aa4e16f4c7150becaea546f70f9622872a Mon Sep 17 00:00:00 2001 From: Killy Date: Mon, 27 Apr 2020 01:13:22 +0300 Subject: [PATCH 02/25] Fixes for some identified issues - better interface for coordinate translation; - correct context menu location; - speech bubble tail can be dragged over the whole image. --- Greenshot/Drawing/Adorners/AbstractAdorner.cs | 11 ++------ Greenshot/Drawing/Adorners/TargetAdorner.cs | 22 +++++++-------- Greenshot/Drawing/DrawableContainerList.cs | 7 ++--- Greenshot/Drawing/Surface.cs | 28 +++++++++++++++++-- Greenshot/Drawing/TextContainer.cs | 15 ++++------ GreenshotPlugin/Interfaces/ISurface.cs | 11 ++++++-- 6 files changed, 55 insertions(+), 39 deletions(-) diff --git a/Greenshot/Drawing/Adorners/AbstractAdorner.cs b/Greenshot/Drawing/Adorners/AbstractAdorner.cs index fc3d72371..51ae7f14f 100644 --- a/Greenshot/Drawing/Adorners/AbstractAdorner.cs +++ b/Greenshot/Drawing/Adorners/AbstractAdorner.cs @@ -1,4 +1,4 @@ -/* +/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2020 Thomas Braun, Jens Klingen, Robin Krom * @@ -123,13 +123,8 @@ namespace Greenshot.Drawing.Adorners { get { - Point[] points = { Location }; - var matrix = Owner.Parent.ZoomMatrix; - if (!matrix.IsIdentity) - { - matrix.TransformPoints(points); - } - return new Rectangle(points[0].X - _size.Width / 2, points[0].Y - _size.Height / 2, _size.Width, _size.Height); + Point displayLocation = Owner.Parent.ToSurfaceCoordinates(Location); + return new Rectangle(displayLocation.X - _size.Width / 2, displayLocation.Y - _size.Height / 2, _size.Width, _size.Height); } } diff --git a/Greenshot/Drawing/Adorners/TargetAdorner.cs b/Greenshot/Drawing/Adorners/TargetAdorner.cs index 23cf29d09..4011754f7 100644 --- a/Greenshot/Drawing/Adorners/TargetAdorner.cs +++ b/Greenshot/Drawing/Adorners/TargetAdorner.cs @@ -1,4 +1,4 @@ -/* +/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2020 Thomas Braun, Jens Klingen, Robin Krom * @@ -61,26 +61,26 @@ namespace Greenshot.Drawing.Adorners Owner.Invalidate(); Point newGripperLocation = new Point(mouseEventArgs.X, mouseEventArgs.Y); - Rectangle surfaceBounds = new Rectangle(0, 0, Owner.Parent.Width, Owner.Parent.Height); + Rectangle imageBounds = new Rectangle(0, 0, Owner.Parent.Image.Width, Owner.Parent.Image.Height); // Check if gripper inside the parent (surface), if not we need to move it inside // This was made for BUG-1682 - if (!surfaceBounds.Contains(newGripperLocation)) + if (!imageBounds.Contains(newGripperLocation)) { - if (newGripperLocation.X > surfaceBounds.Right) + if (newGripperLocation.X > imageBounds.Right) { - newGripperLocation.X = surfaceBounds.Right - 5; + newGripperLocation.X = imageBounds.Right - 5; } - if (newGripperLocation.X < surfaceBounds.Left) + if (newGripperLocation.X < imageBounds.Left) { - newGripperLocation.X = surfaceBounds.Left; + newGripperLocation.X = imageBounds.Left; } - if (newGripperLocation.Y > surfaceBounds.Bottom) + if (newGripperLocation.Y > imageBounds.Bottom) { - newGripperLocation.Y = surfaceBounds.Bottom - 5; + newGripperLocation.Y = imageBounds.Bottom - 5; } - if (newGripperLocation.Y < surfaceBounds.Top) + if (newGripperLocation.Y < imageBounds.Top) { - newGripperLocation.Y = surfaceBounds.Top; + newGripperLocation.Y = imageBounds.Top; } } diff --git a/Greenshot/Drawing/DrawableContainerList.cs b/Greenshot/Drawing/DrawableContainerList.cs index 17b879538..9451b06ea 100644 --- a/Greenshot/Drawing/DrawableContainerList.cs +++ b/Greenshot/Drawing/DrawableContainerList.cs @@ -523,9 +523,9 @@ namespace Greenshot.Drawing { } } - public virtual void ShowContextMenu(MouseEventArgs e, ISurface surface) + public virtual void ShowContextMenu(MouseEventArgs e, ISurface iSurface) { - if (!(surface is Surface)) + if (!(iSurface is Surface surface)) { return; } @@ -542,8 +542,7 @@ namespace Greenshot.Drawing { ContextMenuStrip menu = new ContextMenuStrip(); AddContextMenuItems(menu, surface); if (menu.Items.Count > 0) { - // TODO: cast should be somehow avoided - menu.Show((Surface)surface, e.Location); + menu.Show(surface, surface.ToSurfaceCoordinates(e.Location)); while (true) { if (menu.Visible) { Application.DoEvents(); diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index 5c38bea8e..0057a0838 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -1,4 +1,4 @@ -/* +/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2020 Thomas Braun, Jens Klingen, Robin Krom * @@ -302,6 +302,8 @@ namespace Greenshot.Drawing } } + [NonSerialized] + private Matrix _zoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); [NonSerialized] private float _zoomFactor = 1.0f; public float ZoomFactor @@ -310,12 +312,11 @@ namespace Greenshot.Drawing set { _zoomFactor = value; - ZoomMatrix = new Matrix(_zoomFactor, 0, 0, _zoomFactor, 0, 0); + _zoomMatrix = new Matrix(_zoomFactor, 0, 0, _zoomFactor, 0, 0); UpdateSize(); } } - public Matrix ZoomMatrix { get; private set; } = new Matrix(1, 0, 0, 1, 0, 0); /// /// Sets the surface size as zoomed image size. @@ -2108,5 +2109,26 @@ namespace Greenshot.Drawing { return _elements.Contains(container); } + + public Point ToSurfaceCoordinates(Point point) + { + Point[] points = { point }; + _zoomMatrix.TransformPoints(points); + return points[0]; + } + + public Rectangle ToSurfaceCoordinates(Rectangle rc) + { + if (_zoomMatrix.IsIdentity) + { + return rc; + } + else + { + Point[] points = { rc.Location, rc.Location + rc.Size }; + _zoomMatrix.TransformPoints(points); + return new Rectangle(points[0].X, points[0].Y, points[1].X - points[0].X, points[1].Y - points[1].Y); + } + } } } diff --git a/Greenshot/Drawing/TextContainer.cs b/Greenshot/Drawing/TextContainer.cs index bd4c75274..726ab5b7f 100644 --- a/Greenshot/Drawing/TextContainer.cs +++ b/Greenshot/Drawing/TextContainer.cs @@ -442,20 +442,15 @@ namespace Greenshot.Drawing correction = -1; } Rectangle absRectangle = GuiRectangle.GetGuiRectangle(Left, Top, Width, Height); - Point[] points = { absRectangle.Location, absRectangle.Location + absRectangle.Size }; - var matrix = Parent.ZoomMatrix; - if (!matrix.IsIdentity) - { - matrix.TransformPoints(points); - } - _textBox.Left = points[0].X + lineWidth; - _textBox.Top = points[0].Y + lineWidth; + Rectangle displayRectangle = Parent.ToSurfaceCoordinates(absRectangle); + _textBox.Left = displayRectangle.X + lineWidth; + _textBox.Top = displayRectangle.Y + lineWidth; if (lineThickness <= 1) { lineWidth = 0; } - _textBox.Width = points[1].X - points[0].X - 2 * lineWidth + correction; - _textBox.Height = points[1].Y - points[0].Y - 2 * lineWidth + correction; + _textBox.Width = displayRectangle.Width - 2 * lineWidth + correction; + _textBox.Height = displayRectangle.Height - 2 * lineWidth + correction; } public override void ApplyBounds(RectangleF newBounds) diff --git a/GreenshotPlugin/Interfaces/ISurface.cs b/GreenshotPlugin/Interfaces/ISurface.cs index 8ab44c810..a94415cbc 100644 --- a/GreenshotPlugin/Interfaces/ISurface.cs +++ b/GreenshotPlugin/Interfaces/ISurface.cs @@ -21,7 +21,6 @@ using System; using System.Drawing; -using System.Drawing.Drawing2D; using System.IO; using System.Windows.Forms; using GreenshotPlugin.Effects; @@ -200,9 +199,15 @@ namespace GreenshotPlugin.Interfaces /// float ZoomFactor { get; set; } /// - /// Matrix representing zoom applied to the surface. + /// Translate a point from image coorditate space to surface coordinate space. /// - Matrix ZoomMatrix { get; } + /// A point in the coordinate space of the image. + Point ToSurfaceCoordinates(Point point); + /// + /// Translate a rectangle from image coorditate space to surface coordinate space. + /// + /// A rectangle in the coordinate space of the image. + Rectangle ToSurfaceCoordinates(Rectangle rc); void MakeUndoable(IMemento memento, bool allowMerge); } From 79742d1b4d01a11b2dc6d10e7a0074d6824ecc43 Mon Sep 17 00:00:00 2001 From: Killy Date: Mon, 27 Apr 2020 01:14:52 +0300 Subject: [PATCH 03/25] Code review nitpicks --- Greenshot/Drawing/Adorners/AbstractAdorner.cs | 4 ++-- Greenshot/Drawing/Adorners/MoveAdorner.cs | 2 +- Greenshot/Drawing/Adorners/ResizeAdorner.cs | 2 +- Greenshot/Drawing/Adorners/TargetAdorner.cs | 4 ++-- Greenshot/Drawing/CropContainer.cs | 2 +- Greenshot/Drawing/Surface.cs | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Greenshot/Drawing/Adorners/AbstractAdorner.cs b/Greenshot/Drawing/Adorners/AbstractAdorner.cs index 51ae7f14f..70a56f29e 100644 --- a/Greenshot/Drawing/Adorners/AbstractAdorner.cs +++ b/Greenshot/Drawing/Adorners/AbstractAdorner.cs @@ -1,4 +1,4 @@ -/* +/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2020 Thomas Braun, Jens Klingen, Robin Krom * @@ -119,7 +119,7 @@ namespace Greenshot.Drawing.Adorners /// /// Return the bounds of the Adorner as displayed on the parent Surface /// - protected virtual Rectangle SurfaceBounds + protected virtual Rectangle BoundsOnSurface { get { diff --git a/Greenshot/Drawing/Adorners/MoveAdorner.cs b/Greenshot/Drawing/Adorners/MoveAdorner.cs index 21c42a9fa..d07a25ef7 100644 --- a/Greenshot/Drawing/Adorners/MoveAdorner.cs +++ b/Greenshot/Drawing/Adorners/MoveAdorner.cs @@ -142,7 +142,7 @@ namespace Greenshot.Drawing.Adorners { Graphics targetGraphics = paintEventArgs.Graphics; - var bounds = SurfaceBounds; + var bounds = BoundsOnSurface; GraphicsState state = targetGraphics.Save(); targetGraphics.SmoothingMode = SmoothingMode.None; diff --git a/Greenshot/Drawing/Adorners/ResizeAdorner.cs b/Greenshot/Drawing/Adorners/ResizeAdorner.cs index ef61b17bc..dce78b461 100644 --- a/Greenshot/Drawing/Adorners/ResizeAdorner.cs +++ b/Greenshot/Drawing/Adorners/ResizeAdorner.cs @@ -169,7 +169,7 @@ namespace Greenshot.Drawing.Adorners { Graphics targetGraphics = paintEventArgs.Graphics; - var bounds = SurfaceBounds; + var bounds = BoundsOnSurface; GraphicsState state = targetGraphics.Save(); targetGraphics.SmoothingMode = SmoothingMode.None; diff --git a/Greenshot/Drawing/Adorners/TargetAdorner.cs b/Greenshot/Drawing/Adorners/TargetAdorner.cs index 4011754f7..67c01a533 100644 --- a/Greenshot/Drawing/Adorners/TargetAdorner.cs +++ b/Greenshot/Drawing/Adorners/TargetAdorner.cs @@ -1,4 +1,4 @@ -/* +/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2020 Thomas Braun, Jens Klingen, Robin Krom * @@ -96,7 +96,7 @@ namespace Greenshot.Drawing.Adorners { Graphics targetGraphics = paintEventArgs.Graphics; - var bounds = SurfaceBounds; + var bounds = BoundsOnSurface; targetGraphics.FillRectangle(Brushes.Green, bounds.X, bounds.Y, bounds.Width, bounds.Height); } diff --git a/Greenshot/Drawing/CropContainer.cs b/Greenshot/Drawing/CropContainer.cs index f5288074d..58cee17b1 100644 --- a/Greenshot/Drawing/CropContainer.cs +++ b/Greenshot/Drawing/CropContainer.cs @@ -61,7 +61,7 @@ namespace Greenshot.Drawing { if (_parent?.Image is Image image) { return new Rectangle(0, 0, image.Width, image.Height); } else { - return new Rectangle(); + return Rectangle.Empty; } } } diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index 0057a0838..3d1f309a3 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -1,4 +1,4 @@ -/* +/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2020 Thomas Braun, Jens Klingen, Robin Krom * @@ -1405,7 +1405,7 @@ namespace Greenshot.Drawing return GetImage(RenderMode.EXPORT); } - private Rectangle ZoomClipRectangle(Rectangle rc, double scale) + private static Rectangle ZoomClipRectangle(Rectangle rc, double scale) => new Rectangle( (int)(rc.X * scale), (int)(rc.Y * scale), @@ -1413,7 +1413,7 @@ namespace Greenshot.Drawing (int)((rc.Height + 1) * scale) + 1 ); - private RectangleF ZoomClipRectangle(RectangleF rc, double scale) + private static RectangleF ZoomClipRectangle(RectangleF rc, double scale) => new RectangleF( (float)Math.Floor(rc.X * scale), (float)Math.Floor(rc.Y * scale), From 41082c2be15c94a8a3dfe9fd0e9db06c99139a96 Mon Sep 17 00:00:00 2001 From: Killy Date: Mon, 27 Apr 2020 01:18:05 +0300 Subject: [PATCH 04/25] Image size in OneNoteExporter There is now a distinction between Surface size and Surface.Image size. --- GreenshotOfficePlugin/OfficeExport/OneNoteExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GreenshotOfficePlugin/OfficeExport/OneNoteExporter.cs b/GreenshotOfficePlugin/OfficeExport/OneNoteExporter.cs index 23525b608..611594334 100644 --- a/GreenshotOfficePlugin/OfficeExport/OneNoteExporter.cs +++ b/GreenshotOfficePlugin/OfficeExport/OneNoteExporter.cs @@ -95,7 +95,7 @@ namespace GreenshotOfficePlugin.OfficeExport var pngOutputSettings = new SurfaceOutputSettings(OutputFormat.png, 100, false); ImageOutput.SaveToStream(surfaceToUpload, pngStream, pngOutputSettings); var base64String = Convert.ToBase64String(pngStream.GetBuffer()); - var imageXmlStr = string.Format(XmlImageContent, base64String, surfaceToUpload.Width, surfaceToUpload.Height); + var imageXmlStr = string.Format(XmlImageContent, base64String, surfaceToUpload.Image.Width, surfaceToUpload.Image.Height); var pageChangesXml = string.Format(XmlOutline, imageXmlStr, page.Id, OnenoteNamespace2010, page.Name); LOG.InfoFormat("Sending XML: {0}", pageChangesXml); oneNoteApplication.ComObject.UpdatePageContent(pageChangesXml, DateTime.MinValue, XMLSchema.xs2010, false); From 95c759e3fd62eaed79a5bbbde218e1a0c7e00bd8 Mon Sep 17 00:00:00 2001 From: Killy Date: Mon, 27 Apr 2020 01:19:38 +0300 Subject: [PATCH 05/25] Changed shortcuts - Ctrl+0, Ctrl+9 --- Greenshot/Forms/ImageEditorForm.Designer.cs | 4 ++-- Greenshot/Forms/ImageEditorForm.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Greenshot/Forms/ImageEditorForm.Designer.cs b/Greenshot/Forms/ImageEditorForm.Designer.cs index 5789a19a6..606b72773 100644 --- a/Greenshot/Forms/ImageEditorForm.Designer.cs +++ b/Greenshot/Forms/ImageEditorForm.Designer.cs @@ -1704,7 +1704,7 @@ namespace Greenshot { // this.zoomBestFitMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("zoomBestFitMenuItem.Image"))); this.zoomBestFitMenuItem.Name = "zoomBestFitMenuItem"; - this.zoomBestFitMenuItem.ShortcutKeyDisplayString = "Ctrl+*"; + this.zoomBestFitMenuItem.ShortcutKeyDisplayString = "Ctrl+9"; this.zoomBestFitMenuItem.Size = new System.Drawing.Size(209, 22); this.zoomBestFitMenuItem.Text = "Best Fit"; this.zoomBestFitMenuItem.Click += new System.EventHandler(this.ZoomBestFitMenuItemClick); @@ -1755,7 +1755,7 @@ namespace Greenshot { // this.zoomActualSizeMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("zoomActualSizeMenuItem.Image"))); this.zoomActualSizeMenuItem.Name = "zoomActualSizeMenuItem"; - this.zoomActualSizeMenuItem.ShortcutKeyDisplayString = "Ctrl+/"; + this.zoomActualSizeMenuItem.ShortcutKeyDisplayString = "Ctrl+0"; this.zoomActualSizeMenuItem.Size = new System.Drawing.Size(209, 22); this.zoomActualSizeMenuItem.Tag = "100"; this.zoomActualSizeMenuItem.Text = "100% - Actual Size"; diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index dd9ec94e1..3396acd15 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -871,12 +871,12 @@ namespace Greenshot { case Keys.OemMinus: // Ctrl + - ZoomOutMenuItemClick(sender, e); break; - case Keys.Divide: // Ctrl + Num/ - case Keys.OemQuestion: // Ctrl + / (?) + case Keys.NumPad0: // Ctrl + Num0 + case Keys.D0: // Ctrl + 0 ZoomSetValueMenuItemClick(zoomActualSizeMenuItem, e); break; - case Keys.Multiply: // Ctrl + Num* - case Keys.D8: // Ctrl + 8 (*) + case Keys.NumPad9: // Ctrl + Num9 + case Keys.D9: // Ctrl + 9 ZoomBestFitMenuItemClick(sender, e); break; } From ec2c46daa852d5692bf891ca1700ac6c267abfd3 Mon Sep 17 00:00:00 2001 From: Killy Date: Mon, 27 Apr 2020 01:23:23 +0300 Subject: [PATCH 06/25] GetOptimalWindowSize - bound by available screen space Limiting by working area size was not good enough - having whole chrome on screen is important. --- Greenshot/Forms/ImageEditorForm.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index 3396acd15..4609ff4a8 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -1502,10 +1502,9 @@ namespace Greenshot { var newHeight = chromeSize.Height + surfaceSize.Height; // Upper bound. Don't make it bigger than the available working area. - var screen = Screen.FromControl(this); - var workingArea = screen.WorkingArea; - newWidth = Math.Min(newWidth, workingArea.Width); - newHeight = Math.Min(newHeight, workingArea.Height); + var maxWindowSize = GetAvailableScreenSpace(); + newWidth = Math.Min(newWidth, maxWindowSize.Width); + newHeight = Math.Min(newHeight, maxWindowSize.Height); // Lower bound. Don't make it smaller than a fixed value. int minimumFormWidth = 650; From 3d39241f82799b741afb039e65783f6d0e52f9c0 Mon Sep 17 00:00:00 2001 From: Killy Date: Mon, 27 Apr 2020 16:00:58 +0300 Subject: [PATCH 07/25] More fixes for Surface.Image size - crop works properly; - Freehand drawings are selectable everywhere at all zoom levels. Known issue: freehand selection outline is not drawn --- Greenshot/Drawing/FreehandContainer.cs | 13 ++++++++++--- Greenshot/Drawing/Surface.cs | 8 ++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Greenshot/Drawing/FreehandContainer.cs b/Greenshot/Drawing/FreehandContainer.cs index 129ba4822..c7d5cf1e8 100644 --- a/Greenshot/Drawing/FreehandContainer.cs +++ b/Greenshot/Drawing/FreehandContainer.cs @@ -47,8 +47,8 @@ namespace Greenshot.Drawing { /// Constructor /// public FreehandContainer(Surface parent) : base(parent) { - Width = parent.Width; - Height = parent.Height; + Width = parent.Image.Width; + Height = parent.Image.Height; Top = 0; Left = 0; } @@ -236,7 +236,14 @@ namespace Greenshot.Drawing { int safetymargin = 10; return new Rectangle(myBounds.Left + Left - (safetymargin+lineThickness), myBounds.Top + Top - (safetymargin+lineThickness), myBounds.Width + 2*(lineThickness+safetymargin), myBounds.Height + 2*(lineThickness+safetymargin)); } - return new Rectangle(0, 0, _parent?.Width??0, _parent?.Height?? 0); + if (_parent?.Image is Image image) + { + return new Rectangle(0, 0, image.Width, image.Height); + } + else + { + return Rectangle.Empty; + } } } diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index 3d1f309a3..e9d26ef05 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -991,13 +991,13 @@ namespace Greenshot.Drawing { cropRectangle = new Rectangle(cropRectangle.Left, 0, cropRectangle.Width, cropRectangle.Height + cropRectangle.Top); } - if (cropRectangle.Left + cropRectangle.Width > Width) + if (cropRectangle.Left + cropRectangle.Width > Image.Width) { - cropRectangle = new Rectangle(cropRectangle.Left, cropRectangle.Top, Width - cropRectangle.Left, cropRectangle.Height); + cropRectangle = new Rectangle(cropRectangle.Left, cropRectangle.Top, Image.Width - cropRectangle.Left, cropRectangle.Height); } - if (cropRectangle.Top + cropRectangle.Height > Height) + if (cropRectangle.Top + cropRectangle.Height > Image.Height) { - cropRectangle = new Rectangle(cropRectangle.Left, cropRectangle.Top, cropRectangle.Width, Height - cropRectangle.Top); + cropRectangle = new Rectangle(cropRectangle.Left, cropRectangle.Top, cropRectangle.Width, Image.Height - cropRectangle.Top); } if (cropRectangle.Height > 0 && cropRectangle.Width > 0) { From 169dbdccec9e117d6bcaabb26bcbf66e90fc2471 Mon Sep 17 00:00:00 2001 From: Killy Date: Mon, 27 Apr 2020 19:15:08 +0300 Subject: [PATCH 08/25] Rework for paste from clipboard - location is determined by the same code for images and text; - use visible area to determine the location. --- Greenshot/Drawing/DrawableContainer.cs | 27 ----- Greenshot/Drawing/Surface.cs | 99 +++++++++++++++---- .../Interfaces/Drawing/Container.cs | 1 - GreenshotPlugin/Interfaces/ISurface.cs | 16 ++- 4 files changed, 93 insertions(+), 50 deletions(-) diff --git a/Greenshot/Drawing/DrawableContainer.cs b/Greenshot/Drawing/DrawableContainer.cs index 65ff42de3..e7caf7ea3 100644 --- a/Greenshot/Drawing/DrawableContainer.cs +++ b/Greenshot/Drawing/DrawableContainer.cs @@ -302,33 +302,6 @@ namespace Greenshot.Drawing } } - public void AlignToParent(HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment) { - if (_parent == null) - { - return; - } - int lineThickness = GetFieldValueAsInt(FieldType.LINE_THICKNESS); - if (horizontalAlignment == HorizontalAlignment.Left) { - Left = lineThickness/2; - } - if (horizontalAlignment == HorizontalAlignment.Right) { - Left = _parent.Width - Width - lineThickness/2; - } - if (horizontalAlignment == HorizontalAlignment.Center) { - Left = (_parent.Width / 2) - (Width / 2) - lineThickness/2; - } - - if (verticalAlignment == VerticalAlignment.TOP) { - Top = lineThickness/2; - } - if (verticalAlignment == VerticalAlignment.BOTTOM) { - Top = _parent.Height - Height - lineThickness/2; - } - if (verticalAlignment == VerticalAlignment.CENTER) { - Top = (_parent.Height / 2) - (Height / 2) - lineThickness/2; - } - } - public virtual bool InitContent() { return true; } public virtual void OnDoubleClick() {} diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index e9d26ef05..4435a7f39 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -305,6 +305,8 @@ namespace Greenshot.Drawing [NonSerialized] private Matrix _zoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); [NonSerialized] + private Matrix _inverseZoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); + [NonSerialized] private float _zoomFactor = 1.0f; public float ZoomFactor { @@ -313,6 +315,7 @@ namespace Greenshot.Drawing { _zoomFactor = value; _zoomMatrix = new Matrix(_zoomFactor, 0, 0, _zoomFactor, 0, 0); + _inverseZoomMatrix = new Matrix(1f / _zoomFactor, 0, 0, 1f / _zoomFactor, 0, 0); UpdateSize(); } } @@ -803,9 +806,9 @@ namespace Greenshot.Drawing return cursorContainer; } - public ITextContainer AddTextContainer(string text, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment, FontFamily family, float size, bool italic, bool bold, bool shadow, int borderSize, Color color, Color fillColor) + public ITextContainer AddTextContainer(string text, int x, int y, FontFamily family, float size, bool italic, bool bold, bool shadow, int borderSize, Color color, Color fillColor) { - TextContainer textContainer = new TextContainer(this) {Text = text}; + TextContainer textContainer = new TextContainer(this) {Text = text, Left = x, Top = y}; textContainer.SetFieldValue(FieldType.FONT_FAMILY, family.Name); textContainer.SetFieldValue(FieldType.FONT_BOLD, bold); textContainer.SetFieldValue(FieldType.FONT_ITALIC, italic); @@ -816,8 +819,6 @@ namespace Greenshot.Drawing textContainer.SetFieldValue(FieldType.SHADOW, shadow); // Make sure the Text fits textContainer.FitToText(); - // Align to Surface - textContainer.AlignToParent(horizontalAlignment, verticalAlignment); //AggregatedProperties.UpdateElement(textContainer); AddElement(textContainer); @@ -1817,43 +1818,72 @@ namespace Greenshot.Drawing } else if (ClipboardHelper.ContainsImage(clipboard)) { - int x = 10; - int y = 10; - - // FEATURE-995: Added a check for the current mouse cursor location, to paste the image on that location. - var mousePositionOnControl = PointToClient(MousePosition); - if (ClientRectangle.Contains(mousePositionOnControl)) - { - x = mousePositionOnControl.X; - y = mousePositionOnControl.Y; - } + Point pasteLocation = GetPasteLocation(0.1f, 0.1f); foreach (Image clipboardImage in ClipboardHelper.GetImages(clipboard)) { if (clipboardImage != null) { DeselectAllElements(); - IImageContainer container = AddImageContainer(clipboardImage as Bitmap, x, y); + IImageContainer container = AddImageContainer(clipboardImage as Bitmap, pasteLocation.X, pasteLocation.Y); SelectElement(container); clipboardImage.Dispose(); - x += 10; - y += 10; + pasteLocation.X += 10; + pasteLocation.Y += 10; } } } else if (ClipboardHelper.ContainsText(clipboard)) { + Point pasteLocation = GetPasteLocation(0.4f, 0.4f); + string text = ClipboardHelper.GetText(clipboard); if (text != null) { DeselectAllElements(); - ITextContainer textContainer = AddTextContainer(text, HorizontalAlignment.Center, VerticalAlignment.CENTER, + ITextContainer textContainer = AddTextContainer(text, pasteLocation.X, pasteLocation.Y, FontFamily.GenericSansSerif, 12f, false, false, false, 2, Color.Black, Color.Transparent); SelectElement(textContainer); } } } + /// + /// Find a location to paste elements. + /// If mouse is over the surface - use it's position, otherwise use the visible area. + /// Return a point in image coordinate space. + /// + /// 0.0f for the left edge of visible area, 1.0f for the right edge of visible area. + /// 0.0f for the top edge of visible area, 1.0f for the bottom edge of visible area. + private Point GetPasteLocation(float horizontalRatio = 0.5f, float verticalRatio = 0.5f) + { + var point = PointToClient(MousePosition); + var rc = GetVisibleRectangle(); + if (!rc.Contains(point)) + { + point = new Point( + rc.Left + (int)(rc.Width * horizontalRatio), + rc.Top + (int)(rc.Height * verticalRatio) + ); + } + return ToImageCoordinates(point); + } + + /// + /// Get the rectangle bounding the part of this Surface currently visible in the editor (in surface coordinate space). + /// + private Rectangle GetVisibleRectangle() + { + var bounds = Bounds; + var clientArea = Parent.ClientRectangle; + return new Rectangle( + Math.Max(0, -bounds.Left), + Math.Max(0, -bounds.Top), + clientArea.Width, + clientArea.Height + ); + } + /// /// Duplicate all the selecteded elements /// @@ -2127,7 +2157,38 @@ namespace Greenshot.Drawing { Point[] points = { rc.Location, rc.Location + rc.Size }; _zoomMatrix.TransformPoints(points); - return new Rectangle(points[0].X, points[0].Y, points[1].X - points[0].X, points[1].Y - points[1].Y); + return new Rectangle( + points[0].X, + points[0].Y, + points[1].X - points[0].X, + points[1].Y - points[0].Y + ); + } + } + + public Point ToImageCoordinates(Point point) + { + Point[] points = { point }; + _inverseZoomMatrix.TransformPoints(points); + return points[0]; + } + + public Rectangle ToImageCoordinates(Rectangle rc) + { + if (_inverseZoomMatrix.IsIdentity) + { + return rc; + } + else + { + Point[] points = { rc.Location, rc.Location + rc.Size }; + _inverseZoomMatrix.TransformPoints(points); + return new Rectangle( + points[0].X, + points[0].Y, + points[1].X - points[0].X, + points[1].Y - points[0].Y + ); } } } diff --git a/GreenshotPlugin/Interfaces/Drawing/Container.cs b/GreenshotPlugin/Interfaces/Drawing/Container.cs index b2b6e3d19..35a6cf50a 100644 --- a/GreenshotPlugin/Interfaces/Drawing/Container.cs +++ b/GreenshotPlugin/Interfaces/Drawing/Container.cs @@ -102,7 +102,6 @@ namespace GreenshotPlugin.Interfaces.Drawing get; set; } - void AlignToParent(HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment); void Invalidate(); bool ClickableAt(int x, int y); void MoveBy(int x, int y); diff --git a/GreenshotPlugin/Interfaces/ISurface.cs b/GreenshotPlugin/Interfaces/ISurface.cs index a94415cbc..e32247188 100644 --- a/GreenshotPlugin/Interfaces/ISurface.cs +++ b/GreenshotPlugin/Interfaces/ISurface.cs @@ -84,8 +84,8 @@ namespace GreenshotPlugin.Interfaces /// The TextContainer will be "re"sized to the text size. /// /// String to show - /// Left, Center, Right - /// TOP, CENTER, BOTTOM + /// Where to put the container, X coordinate in the Image coordinate space + /// Where to put the container, Y coordinate in the Image coordinate space /// FontFamily /// Font Size in float /// bool true if italic @@ -94,7 +94,7 @@ namespace GreenshotPlugin.Interfaces /// size of border (0 for none) /// Color of string /// Color of background (e.g. Color.Transparent) - ITextContainer AddTextContainer(string text, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment, FontFamily family, float size, bool italic, bool bold, bool shadow, int borderSize, Color color, Color fillColor); + ITextContainer AddTextContainer(string text, int x, int y, FontFamily family, float size, bool italic, bool bold, bool shadow, int borderSize, Color color, Color fillColor); IImageContainer AddImageContainer(Image image, int x, int y); ICursorContainer AddCursorContainer(Cursor cursor, int x, int y); @@ -208,6 +208,16 @@ namespace GreenshotPlugin.Interfaces /// /// A rectangle in the coordinate space of the image. Rectangle ToSurfaceCoordinates(Rectangle rc); + /// + /// Translate a point from surface coorditate space to image coordinate space. + /// + /// A point in the coordinate space of the surface. + Point ToImageCoordinates(Point point); + /// + /// Translate a rectangle from surface coorditate space to image coordinate space. + /// + /// A rectangle in the coordinate space of the surface. + Rectangle ToImageCoordinates(Rectangle rc); void MakeUndoable(IMemento memento, bool allowMerge); } From 195b5c3ab7579e850e1816c4ec72aaae2ebe46ae Mon Sep 17 00:00:00 2001 From: Killy Date: Mon, 27 Apr 2020 19:15:36 +0300 Subject: [PATCH 09/25] Fix NPE --- Greenshot/Drawing/TextContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Greenshot/Drawing/TextContainer.cs b/Greenshot/Drawing/TextContainer.cs index 726ab5b7f..c73424908 100644 --- a/Greenshot/Drawing/TextContainer.cs +++ b/Greenshot/Drawing/TextContainer.cs @@ -428,7 +428,7 @@ namespace Greenshot.Drawing /// private void UpdateTextBoxPosition() { - if (_textBox == null) + if (_textBox == null || Parent == null) { return; } From 2be1898c53f90c4769e38d55685f3688f2795c91 Mon Sep 17 00:00:00 2001 From: Killy Date: Mon, 27 Apr 2020 19:30:22 +0300 Subject: [PATCH 10/25] Width and Height shouldn't be exposed through ISurface anymore --- GreenshotPlugin/Interfaces/ISurface.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/GreenshotPlugin/Interfaces/ISurface.cs b/GreenshotPlugin/Interfaces/ISurface.cs index e32247188..73b31b05d 100644 --- a/GreenshotPlugin/Interfaces/ISurface.cs +++ b/GreenshotPlugin/Interfaces/ISurface.cs @@ -191,8 +191,6 @@ namespace GreenshotPlugin.Interfaces get; set; } - int Width { get; } - int Height { get; } /// /// Zoom value applied to the surface. 1.0f for actual size (100%). From 5fe700bc840c1aac874ac474ae03c362d1d1a615 Mon Sep 17 00:00:00 2001 From: MXI Date: Mon, 27 Apr 2020 21:00:59 +0300 Subject: [PATCH 11/25] Fix GetAvailableScreenSpace on secondary screens Co-Authored-By: jklingen --- Greenshot/Forms/ImageEditorForm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index 4609ff4a8..d5de0b5c9 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -1526,7 +1526,7 @@ namespace Greenshot { var screenBounds = screen.Bounds; var workingArea = screen.WorkingArea; if (Left > screenBounds.Left && Top > screenBounds.Top) { - return new Size(workingArea.Width - Left, workingArea.Height - Top); + return new Size(workingArea.Right - Left, workingArea.Bottom - Top); } else { return workingArea.Size; } From d47271e7e14c848145411a2ef5820424f3c62d7b Mon Sep 17 00:00:00 2001 From: Killy Date: Tue, 28 Apr 2020 18:55:43 +0300 Subject: [PATCH 12/25] Fix rendering quality at small zoom levels when redrawing small parts --- Greenshot/Drawing/Surface.cs | 42 +++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index 4435a7f39..09bbe028e 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -1,4 +1,4 @@ -/* +/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2020 Thomas Braun, Jens Klingen, Robin Krom * @@ -1473,19 +1473,11 @@ namespace Greenshot.Drawing targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); if (isZoomedIn) { - var state = targetGraphics.Save(); - targetGraphics.SmoothingMode = SmoothingMode.None; - targetGraphics.InterpolationMode = InterpolationMode.NearestNeighbor; - targetGraphics.CompositingQuality = CompositingQuality.HighQuality; - targetGraphics.PixelOffsetMode = PixelOffsetMode.None; - - targetGraphics.DrawImage(_buffer, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - - targetGraphics.Restore(state); + DrawSharpImage(targetGraphics, _buffer, imageClipRectangle); } else { - targetGraphics.DrawImage(_buffer, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + DrawSmoothImage(targetGraphics, _buffer, imageClipRectangle); } targetGraphics.ResetTransform(); } @@ -1494,7 +1486,7 @@ namespace Greenshot.Drawing DrawBackground(targetGraphics, targetClipRectangle); targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); - targetGraphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + DrawSmoothImage(targetGraphics, Image, imageClipRectangle); _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); targetGraphics.ResetTransform(); } @@ -1511,6 +1503,32 @@ namespace Greenshot.Drawing } } + private void DrawSmoothImage(Graphics targetGraphics, Image image, Rectangle imageClipRectangle) + { + var state = targetGraphics.Save(); + targetGraphics.SmoothingMode = SmoothingMode.HighQuality; + targetGraphics.InterpolationMode = InterpolationMode.HighQualityBilinear; + targetGraphics.CompositingQuality = CompositingQuality.HighQuality; + targetGraphics.PixelOffsetMode = PixelOffsetMode.None; + + targetGraphics.DrawImage(image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + + targetGraphics.Restore(state); + } + + private void DrawSharpImage(Graphics targetGraphics, Image image, Rectangle imageClipRectangle) + { + var state = targetGraphics.Save(); + targetGraphics.SmoothingMode = SmoothingMode.None; + targetGraphics.InterpolationMode = InterpolationMode.NearestNeighbor; + targetGraphics.CompositingQuality = CompositingQuality.HighQuality; + targetGraphics.PixelOffsetMode = PixelOffsetMode.None; + + targetGraphics.DrawImage(image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + + targetGraphics.Restore(state); + } + private void DrawBackground(Graphics targetGraphics, Rectangle clipRectangle) { // check if we need to draw the checkerboard From 4c0277be90fc31ab5119ba23f716f5a0ad5f771e Mon Sep 17 00:00:00 2001 From: Killy Date: Tue, 28 Apr 2020 19:19:17 +0300 Subject: [PATCH 13/25] Robust clip region resize --- Greenshot/Drawing/Surface.cs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index 09bbe028e..a6da5b4ed 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -1406,24 +1406,24 @@ namespace Greenshot.Drawing return GetImage(RenderMode.EXPORT); } - private static Rectangle ZoomClipRectangle(Rectangle rc, double scale) - => new Rectangle( + private static Rectangle ZoomClipRectangle(Rectangle rc, double scale, int inflateAmount = 0) + { + rc = new Rectangle( (int)(rc.X * scale), (int)(rc.Y * scale), - (int)((rc.Width + 1) * scale) + 1, // making sure to redraw enough pixels when moving scaled image - (int)((rc.Height + 1) * scale) + 1 - ); - - private static RectangleF ZoomClipRectangle(RectangleF rc, double scale) - => new RectangleF( - (float)Math.Floor(rc.X * scale), - (float)Math.Floor(rc.Y * scale), - (float)Math.Ceiling(rc.Width * scale), - (float)Math.Ceiling(rc.Height * scale) + (int)(rc.Width * scale) + 1, + (int)(rc.Height * scale) + 1 ); + if (scale > 1) + { + inflateAmount = (int)(inflateAmount * scale); + } + rc.Inflate(inflateAmount, inflateAmount); + return rc; + } public void InvalidateElements(Rectangle rc) - => Invalidate(ZoomClipRectangle(rc, _zoomFactor)); + => Invalidate(ZoomClipRectangle(rc, _zoomFactor, 1)); /// /// This is the event handler for the Paint Event, try to draw as little as possible! @@ -1439,7 +1439,7 @@ namespace Greenshot.Drawing LOG.Debug("Empty cliprectangle??"); return; } - Rectangle imageClipRectangle = ZoomClipRectangle(targetClipRectangle, 1.0 / _zoomFactor); + Rectangle imageClipRectangle = ZoomClipRectangle(targetClipRectangle, 1.0 / _zoomFactor, 2); bool isZoomedIn = ZoomFactor > 1f; if (_elements.HasIntersectingFilters(imageClipRectangle) || isZoomedIn) @@ -1467,7 +1467,7 @@ namespace Greenshot.Drawing //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; DrawBackground(graphics, imageClipRectangle); graphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - graphics.SetClip(ZoomClipRectangle(targetGraphics.ClipBounds, 1.0 / _zoomFactor)); + graphics.SetClip(ZoomClipRectangle(Rectangle.Round(targetGraphics.ClipBounds), 1.0 / _zoomFactor, 2)); _elements.Draw(graphics, _buffer, RenderMode.EDIT, imageClipRectangle); } targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); From e6e2ed523ab166529dd15a08eabe8fab65e31071 Mon Sep 17 00:00:00 2001 From: Killy Date: Tue, 28 Apr 2020 21:43:01 +0300 Subject: [PATCH 14/25] Keep center of visible area static on zoom when possible --- Greenshot/Drawing/Surface.cs | 2 +- Greenshot/Forms/ImageEditorForm.cs | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index a6da5b4ed..339e4b02c 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -1890,7 +1890,7 @@ namespace Greenshot.Drawing /// /// Get the rectangle bounding the part of this Surface currently visible in the editor (in surface coordinate space). /// - private Rectangle GetVisibleRectangle() + public Rectangle GetVisibleRectangle() { var bounds = Bounds; var clientArea = Parent.ClientRectangle; diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index d5de0b5c9..ec53969f5 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -1572,8 +1572,24 @@ namespace Greenshot { } private void ZoomSetValue(int value) { + var surface = Surface as Surface; + var panel = surface?.Parent as Panel; + if (panel == null) + { + return; + } + + // Store old scroll position + var rc = surface.GetVisibleRectangle(); + var size = surface.Size; + var horizontalCenter = 1.0 * (rc.Left + rc.Width / 2) / size.Width; + var verticalCenter = 1.0 * (rc.Top + rc.Height / 2) / size.Height; + + // Set the new zoom value _zoomValue = value; Surface.ZoomFactor = 1f * value / 100; + Size = GetOptimalWindowSize(); + AlignCanvasPositionAfterResize(); // Update zoom controls string valueString = value.ToString(); @@ -1584,8 +1600,13 @@ namespace Greenshot { } } - Size = GetOptimalWindowSize(); - AlignCanvasPositionAfterResize(); + // Restore scroll position + rc = surface.GetVisibleRectangle(); + size = surface.Size; + panel.AutoScrollPosition = new Point( + (int)(horizontalCenter * size.Width) - rc.Width / 2, + (int)(verticalCenter * size.Height) - rc.Height / 2 + ); } } } From 1ef2df9602dc155279e78024c7247e2618905446 Mon Sep 17 00:00:00 2001 From: Killy Date: Tue, 28 Apr 2020 21:46:45 +0300 Subject: [PATCH 15/25] Unneeded using after 169dbdc --- Greenshot/Drawing/DrawableContainer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Greenshot/Drawing/DrawableContainer.cs b/Greenshot/Drawing/DrawableContainer.cs index e7caf7ea3..33339ee00 100644 --- a/Greenshot/Drawing/DrawableContainer.cs +++ b/Greenshot/Drawing/DrawableContainer.cs @@ -33,7 +33,6 @@ using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Runtime.Serialization; -using System.Windows.Forms; using GreenshotPlugin.IniFile; using GreenshotPlugin.Interfaces; using GreenshotPlugin.Interfaces.Drawing.Adorners; From 99742ad05ad203c44b7ef8ed7a857a3de08d47dd Mon Sep 17 00:00:00 2001 From: Killy Date: Wed, 29 Apr 2020 23:23:29 +0300 Subject: [PATCH 16/25] Fix for Best Fit on images bigger than 4 times the available space --- Greenshot/Forms/ImageEditorForm.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index ec53969f5..334a72dc3 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -1562,11 +1562,12 @@ namespace Greenshot { static bool isFit(int zoom, int source, int boundary) => source * zoom / 100 <= boundary; - var nextValue = Array.FindLast( + var nextIndex = Array.FindLastIndex( ZOOM_VALUES, zoom => isFit(zoom, imageSize.Width, maxImageSize.Width) && isFit(zoom, imageSize.Height, maxImageSize.Height) ); + var nextValue = nextIndex < 0 ? ZOOM_VALUES[0] : ZOOM_VALUES[nextIndex]; ZoomSetValue(nextValue); } From 464e5e872f723b2cd0df343c5b82c309088a41c6 Mon Sep 17 00:00:00 2001 From: Killy Date: Thu, 30 Apr 2020 17:39:03 +0300 Subject: [PATCH 17/25] Fix weird scroll position when going from no scroll zoom value Also prefer top left corner in that situation - it is less disorienting when working with screenshots. --- Greenshot/Forms/ImageEditorForm.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index 334a72dc3..ca0cb1a7b 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -1581,10 +1581,19 @@ namespace Greenshot { } // Store old scroll position + // When no scroll is currently needed - prefer top left corner. + var horizontalCenter = 0.0; + var verticalCenter = 0.0; var rc = surface.GetVisibleRectangle(); var size = surface.Size; - var horizontalCenter = 1.0 * (rc.Left + rc.Width / 2) / size.Width; - var verticalCenter = 1.0 * (rc.Top + rc.Height / 2) / size.Height; + if (size.Width > rc.Width) + { + horizontalCenter = 1.0 * (rc.Left + rc.Width / 2) / size.Width; + } + if (size.Height > rc.Height) + { + verticalCenter = 1.0 * (rc.Top + rc.Height / 2) / size.Height; + } // Set the new zoom value _zoomValue = value; From dcf75fd081b6b0792069fa7a88c4cccf0b8ee626 Mon Sep 17 00:00:00 2001 From: Killy Date: Thu, 30 Apr 2020 17:57:36 +0300 Subject: [PATCH 18/25] Use Fractions to represent zoom factor The goal is to be able to get as close as possible to perfect 66.(6)% (2/3) zoom factor, and also remove types mismatch between the editor form and the surface. --- Greenshot/Drawing/Surface.cs | 13 +- Greenshot/Forms/ImageEditorForm.Designer.cs | 18 +-- Greenshot/Forms/ImageEditorForm.cs | 24 ++-- GreenshotPlugin/Core/Fraction.cs | 152 ++++++++++++++++++++ GreenshotPlugin/Interfaces/ISurface.cs | 5 +- 5 files changed, 183 insertions(+), 29 deletions(-) create mode 100644 GreenshotPlugin/Core/Fraction.cs diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index 339e4b02c..811466cf7 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -307,15 +307,16 @@ namespace Greenshot.Drawing [NonSerialized] private Matrix _inverseZoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); [NonSerialized] - private float _zoomFactor = 1.0f; - public float ZoomFactor + private Fraction _zoomFactor = Fraction.Identity; + public Fraction ZoomFactor { get => _zoomFactor; set { _zoomFactor = value; + var inverse = _zoomFactor.Inverse(); _zoomMatrix = new Matrix(_zoomFactor, 0, 0, _zoomFactor, 0, 0); - _inverseZoomMatrix = new Matrix(1f / _zoomFactor, 0, 0, 1f / _zoomFactor, 0, 0); + _inverseZoomMatrix = new Matrix(inverse, 0, 0, inverse, 0, 0); UpdateSize(); } } @@ -1439,9 +1440,9 @@ namespace Greenshot.Drawing LOG.Debug("Empty cliprectangle??"); return; } - Rectangle imageClipRectangle = ZoomClipRectangle(targetClipRectangle, 1.0 / _zoomFactor, 2); + Rectangle imageClipRectangle = ZoomClipRectangle(targetClipRectangle, _zoomFactor.Inverse(), 2); - bool isZoomedIn = ZoomFactor > 1f; + bool isZoomedIn = _zoomFactor > Fraction.Identity; if (_elements.HasIntersectingFilters(imageClipRectangle) || isZoomedIn) { if (_buffer != null) @@ -1467,7 +1468,7 @@ namespace Greenshot.Drawing //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; DrawBackground(graphics, imageClipRectangle); graphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - graphics.SetClip(ZoomClipRectangle(Rectangle.Round(targetGraphics.ClipBounds), 1.0 / _zoomFactor, 2)); + graphics.SetClip(ZoomClipRectangle(Rectangle.Round(targetGraphics.ClipBounds), _zoomFactor.Inverse(), 2)); _elements.Draw(graphics, _buffer, RenderMode.EDIT, imageClipRectangle); } targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); diff --git a/Greenshot/Forms/ImageEditorForm.Designer.cs b/Greenshot/Forms/ImageEditorForm.Designer.cs index 606b72773..04da3d0a0 100644 --- a/Greenshot/Forms/ImageEditorForm.Designer.cs +++ b/Greenshot/Forms/ImageEditorForm.Designer.cs @@ -1718,7 +1718,7 @@ namespace Greenshot { // this.zoom25MenuItem.Name = "zoom25MenuItem"; this.zoom25MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom25MenuItem.Tag = "25"; + this.zoom25MenuItem.Tag = "1/4"; this.zoom25MenuItem.Text = "25%"; this.zoom25MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1726,7 +1726,7 @@ namespace Greenshot { // this.zoom50MenuItem.Name = "zoom50MenuItem"; this.zoom50MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom50MenuItem.Tag = "50"; + this.zoom50MenuItem.Tag = "1/2"; this.zoom50MenuItem.Text = "50%"; this.zoom50MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1734,7 +1734,7 @@ namespace Greenshot { // this.zoom66MenuItem.Name = "zoom66MenuItem"; this.zoom66MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom66MenuItem.Tag = "66"; + this.zoom66MenuItem.Tag = "2/3"; this.zoom66MenuItem.Text = "66%"; this.zoom66MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1742,7 +1742,7 @@ namespace Greenshot { // this.zoom75MenuItem.Name = "zoom75MenuItem"; this.zoom75MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom75MenuItem.Tag = "75"; + this.zoom75MenuItem.Tag = "3/4"; this.zoom75MenuItem.Text = "75%"; this.zoom75MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1757,7 +1757,7 @@ namespace Greenshot { this.zoomActualSizeMenuItem.Name = "zoomActualSizeMenuItem"; this.zoomActualSizeMenuItem.ShortcutKeyDisplayString = "Ctrl+0"; this.zoomActualSizeMenuItem.Size = new System.Drawing.Size(209, 22); - this.zoomActualSizeMenuItem.Tag = "100"; + this.zoomActualSizeMenuItem.Tag = "1/1"; this.zoomActualSizeMenuItem.Text = "100% - Actual Size"; this.zoomActualSizeMenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1770,7 +1770,7 @@ namespace Greenshot { // this.zoom200MenuItem.Name = "zoom200MenuItem"; this.zoom200MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom200MenuItem.Tag = "200"; + this.zoom200MenuItem.Tag = "2/1"; this.zoom200MenuItem.Text = "200%"; this.zoom200MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1778,7 +1778,7 @@ namespace Greenshot { // this.zoom300MenuItem.Name = "zoom300MenuItem"; this.zoom300MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom300MenuItem.Tag = "300"; + this.zoom300MenuItem.Tag = "3/1"; this.zoom300MenuItem.Text = "300%"; this.zoom300MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1786,7 +1786,7 @@ namespace Greenshot { // this.zoom400MenuItem.Name = "zoom400MenuItem"; this.zoom400MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom400MenuItem.Tag = "400"; + this.zoom400MenuItem.Tag = "4/1"; this.zoom400MenuItem.Text = "400%"; this.zoom400MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1794,7 +1794,7 @@ namespace Greenshot { // this.zoom600MenuItem.Name = "zoom600MenuItem"; this.zoom600MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom600MenuItem.Tag = "600"; + this.zoom600MenuItem.Tag = "6/1"; this.zoom600MenuItem.Text = "600%"; this.zoom600MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index ca0cb1a7b..0b5c40d42 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -69,8 +69,7 @@ namespace Greenshot { /// /// All provided zoom values (in percents) in ascending order. /// - private readonly int[] ZOOM_VALUES = new[] { 25, 50, 66, 75, 100, 200, 300, 400, 600 }; - private int _zoomValue = 100; + private readonly Fraction[] ZOOM_VALUES = new Fraction[] { (1, 4), (1, 2), (2, 3), (3, 4), (1 ,1), (2, 1), (3, 1), (4, 1), (6, 1) }; /// /// An Implementation for the IImageEditor, this way Plugins have access to the HWND handles wich can be used with Win32 API calls. @@ -1533,14 +1532,16 @@ namespace Greenshot { } private void ZoomInMenuItemClick(object sender, EventArgs e) { - var nextIndex = Array.FindIndex(ZOOM_VALUES, v => v > _zoomValue); + var zoomValue = Surface.ZoomFactor; + var nextIndex = Array.FindIndex(ZOOM_VALUES, v => v > zoomValue); var nextValue = nextIndex < 0 ? ZOOM_VALUES[ZOOM_VALUES.Length - 1] : ZOOM_VALUES[nextIndex]; ZoomSetValue(nextValue); } private void ZoomOutMenuItemClick(object sender, EventArgs e) { - var nextIndex = Array.FindLastIndex(ZOOM_VALUES, v => v < _zoomValue); + var zoomValue = Surface.ZoomFactor; + var nextIndex = Array.FindLastIndex(ZOOM_VALUES, v => v < zoomValue); var nextValue = nextIndex < 0 ? ZOOM_VALUES[0] : ZOOM_VALUES[nextIndex]; ZoomSetValue(nextValue); @@ -1548,7 +1549,7 @@ namespace Greenshot { private void ZoomSetValueMenuItemClick(object sender, EventArgs e) { var senderMenuItem = (ToolStripMenuItem)sender; - int zoomPercent = int.Parse((string)senderMenuItem.Tag); + var zoomPercent = Fraction.Parse((string)senderMenuItem.Tag); ZoomSetValue(zoomPercent); } @@ -1559,8 +1560,8 @@ namespace Greenshot { var maxImageSize = maxWindowSize - chromeSize; var imageSize = Surface.Image.Size; - static bool isFit(int zoom, int source, int boundary) - => source * zoom / 100 <= boundary; + static bool isFit(Fraction scale, int source, int boundary) + => (int)(source * scale) <= boundary; var nextIndex = Array.FindLastIndex( ZOOM_VALUES, @@ -1572,7 +1573,7 @@ namespace Greenshot { ZoomSetValue(nextValue); } - private void ZoomSetValue(int value) { + private void ZoomSetValue(Fraction value) { var surface = Surface as Surface; var panel = surface?.Parent as Panel; if (panel == null) @@ -1596,14 +1597,13 @@ namespace Greenshot { } // Set the new zoom value - _zoomValue = value; - Surface.ZoomFactor = 1f * value / 100; + Surface.ZoomFactor = value; Size = GetOptimalWindowSize(); AlignCanvasPositionAfterResize(); // Update zoom controls - string valueString = value.ToString(); - zoomStatusDropDownBtn.Text = valueString + "%"; + zoomStatusDropDownBtn.Text = ((int)(100 * (double)value)).ToString() + "%"; + var valueString = value.ToString(); foreach (var item in zoomMenuStrip.Items) { if (item is ToolStripMenuItem menuItem) { menuItem.Checked = menuItem.Tag as string == valueString; diff --git a/GreenshotPlugin/Core/Fraction.cs b/GreenshotPlugin/Core/Fraction.cs new file mode 100644 index 000000000..312b91bb8 --- /dev/null +++ b/GreenshotPlugin/Core/Fraction.cs @@ -0,0 +1,152 @@ +using System; +using System.Text.RegularExpressions; + +namespace GreenshotPlugin.Core +{ + /// + /// Basic Fraction (Rational) numbers with features only needed to represent scale factors. + /// + public readonly struct Fraction : IEquatable, IComparable + { + public static Fraction Identity { get; } = new Fraction(1, 1); + + public uint Numerator { get; } + public uint Denominator { get; } + + public Fraction(uint numerator, uint denominator) + { + if (denominator == 0) + { + throw new ArgumentException("Can't divide by zero.", nameof(denominator)); + } + if (numerator == 0) + { + throw new ArgumentException("Zero is not supported by this implementation.", nameof(numerator)); + } + var gcd = GreatestCommonDivisor(numerator, denominator); + Numerator = numerator / gcd; + Denominator = denominator / gcd; + } + + public Fraction Inverse() + => new Fraction(Denominator, Numerator); + + #region Parse + + private static readonly Regex PARSE_REGEX = new Regex(@"^([1-9][0-9]*)\/([1-9][0-9]*)$", RegexOptions.Compiled); + public static bool TryParse(string str, out Fraction result) + { + var match = PARSE_REGEX.Match(str); + if (!match.Success) + { + result = Identity; + return false; + } + var numerator = uint.Parse(match.Groups[1].Value); + var denominator = uint.Parse(match.Groups[2].Value); + result = new Fraction(numerator, denominator); + return true; + } + + public static Fraction Parse(string str) + => TryParse(str, out var result) + ? result + : throw new ArgumentException($"Could not parse the input \"{str}\".", nameof(str)); + + #endregion + + #region Overrides, interface implementations + + public override string ToString() + => $"{Numerator}/{Denominator}"; + + public override bool Equals(object obj) + => obj is Fraction fraction && Equals(fraction); + + public bool Equals(Fraction other) + => Numerator == other.Numerator && Denominator == other.Denominator; + + public override int GetHashCode() + { + unchecked + { + int hashCode = -1534900553; + hashCode = hashCode * -1521134295 + Numerator.GetHashCode(); + hashCode = hashCode * -1521134295 + Denominator.GetHashCode(); + return hashCode; + } + } + + public int CompareTo(Fraction other) + => (int)(Numerator * other.Denominator) - (int)(other.Numerator * Denominator); + + #endregion + + #region Equality operators + + public static bool operator ==(Fraction left, Fraction right) + => left.Equals(right); + + public static bool operator !=(Fraction left, Fraction right) + => !(left == right); + + #endregion + + #region Comparison operators + + public static bool operator <(Fraction left, Fraction right) + => left.CompareTo(right) < 0; + + public static bool operator <=(Fraction left, Fraction right) + => left.CompareTo(right) <= 0; + + public static bool operator >(Fraction left, Fraction right) + => left.CompareTo(right) > 0; + + public static bool operator >=(Fraction left, Fraction right) + => left.CompareTo(right) >= 0; + + #endregion + + #region Scale operators + + public static Fraction operator *(Fraction left, Fraction right) + => new Fraction(left.Numerator * right.Numerator, left.Denominator * right.Denominator); + + public static Fraction operator *(Fraction left, uint right) + => new Fraction(left.Numerator * right, left.Denominator); + + public static Fraction operator *(uint left, Fraction right) + => new Fraction(left * right.Numerator, right.Denominator); + + public static Fraction operator /(Fraction left, Fraction right) + => new Fraction(left.Numerator * right.Denominator, left.Denominator * right.Numerator); + + public static Fraction operator /(Fraction left, uint right) + => new Fraction(left.Numerator, left.Denominator * right); + + public static Fraction operator /(uint left, Fraction right) + => new Fraction(left * right.Denominator, right.Numerator); + + #endregion + + #region Type conversion operators + + public static implicit operator double(Fraction fraction) + => 1.0 * fraction.Numerator / fraction.Denominator; + + public static implicit operator float(Fraction fraction) + => 1.0f * fraction.Numerator / fraction.Denominator; + + public static implicit operator Fraction(uint number) + => new Fraction(number, 1u); + + public static implicit operator Fraction((uint numerator, uint demoninator) tuple) + => new Fraction(tuple.numerator, tuple.demoninator); + + #endregion + + private static uint GreatestCommonDivisor(uint a, uint b) + => (b != 0) ? GreatestCommonDivisor(b, a % b) : a; + } +} diff --git a/GreenshotPlugin/Interfaces/ISurface.cs b/GreenshotPlugin/Interfaces/ISurface.cs index 73b31b05d..068ab0f29 100644 --- a/GreenshotPlugin/Interfaces/ISurface.cs +++ b/GreenshotPlugin/Interfaces/ISurface.cs @@ -23,6 +23,7 @@ using System; using System.Drawing; using System.IO; using System.Windows.Forms; +using GreenshotPlugin.Core; using GreenshotPlugin.Effects; using GreenshotPlugin.Interfaces.Drawing; @@ -193,9 +194,9 @@ namespace GreenshotPlugin.Interfaces } /// - /// Zoom value applied to the surface. 1.0f for actual size (100%). + /// Zoom value applied to the surface. /// - float ZoomFactor { get; set; } + Fraction ZoomFactor { get; set; } /// /// Translate a point from image coorditate space to surface coordinate space. /// From bac1ff4ba06e855239658688cf8e23002efb248c Mon Sep 17 00:00:00 2001 From: Killy Date: Thu, 30 Apr 2020 18:30:51 +0300 Subject: [PATCH 19/25] Fix for pixel jerk --- Greenshot/Drawing/Surface.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index 811466cf7..2ea5e0b00 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -1440,6 +1440,25 @@ namespace Greenshot.Drawing LOG.Debug("Empty cliprectangle??"); return; } + + // Correction to prevent rounding errors at certain zoom levels. + // When zooming to N/M, clip rectangle top and left coordinates should be multiples of N. + if (_zoomFactor.Numerator > 1 && _zoomFactor.Denominator > 1) + { + int horizontalCorrection = targetClipRectangle.Left % (int)_zoomFactor.Numerator; + int verticalCorrection = targetClipRectangle.Top % (int)_zoomFactor.Numerator; + if (horizontalCorrection != 0) + { + targetClipRectangle.X -= horizontalCorrection; + targetClipRectangle.Width += horizontalCorrection; + } + if (verticalCorrection != 0) + { + targetClipRectangle.Y -= verticalCorrection; + targetClipRectangle.Height += verticalCorrection; + } + } + Rectangle imageClipRectangle = ZoomClipRectangle(targetClipRectangle, _zoomFactor.Inverse(), 2); bool isZoomedIn = _zoomFactor > Fraction.Identity; From f494385ef73d45527357b5eabb79a40dffbed0e7 Mon Sep 17 00:00:00 2001 From: Killy Date: Thu, 30 Apr 2020 19:16:05 +0300 Subject: [PATCH 20/25] Naming consistency - not a percentage value anymore --- Greenshot/Forms/ImageEditorForm.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index 0b5c40d42..1e177e428 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -1549,9 +1549,9 @@ namespace Greenshot { private void ZoomSetValueMenuItemClick(object sender, EventArgs e) { var senderMenuItem = (ToolStripMenuItem)sender; - var zoomPercent = Fraction.Parse((string)senderMenuItem.Tag); + var nextValue = Fraction.Parse((string)senderMenuItem.Tag); - ZoomSetValue(zoomPercent); + ZoomSetValue(nextValue); } private void ZoomBestFitMenuItemClick(object sender, EventArgs e) { From d93c9d6a3a24c18a0490b72e3ddfbc48849ca221 Mon Sep 17 00:00:00 2001 From: Killy Date: Fri, 1 May 2020 00:46:27 +0300 Subject: [PATCH 21/25] TextContainer's TextBox on zoom - fix for position, update font size --- Greenshot/Drawing/TextContainer.cs | 59 +++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/Greenshot/Drawing/TextContainer.cs b/Greenshot/Drawing/TextContainer.cs index c73424908..939dd3de6 100644 --- a/Greenshot/Drawing/TextContainer.cs +++ b/Greenshot/Drawing/TextContainer.cs @@ -22,6 +22,7 @@ using Greenshot.Drawing.Fields; using Greenshot.Helpers; using Greenshot.Memento; +using GreenshotPlugin.Core; using GreenshotPlugin.Interfaces.Drawing; using System; using System.ComponentModel; @@ -155,6 +156,24 @@ namespace Greenshot.Drawing FieldChanged += TextContainer_FieldChanged; } + protected override void SwitchParent(Surface newParent) + { + _parent.SizeChanged -= Parent_SizeChanged; + base.SwitchParent(newParent); + _parent.SizeChanged += Parent_SizeChanged; + } + + private void Parent_SizeChanged(object sender, EventArgs e) + { + UpdateTextBoxPosition(); + UpdateTextBoxFont(); + } + + public override void ApplyBounds(RectangleF newBounds) + { + base.ApplyBounds(newBounds); + UpdateTextBoxPosition(); + } public override void Invalidate() { @@ -255,7 +274,8 @@ namespace Greenshot.Drawing AcceptsTab = true, AcceptsReturn = true, BorderStyle = BorderStyle.None, - Visible = false + Visible = false, + Font = new Font(FontFamily.GenericSansSerif, 1) // just need something non-default here }; _textBox.DataBindings.Add("Text", this, "Text", false, DataSourceUpdateMode.OnPropertyChanged); @@ -388,7 +408,6 @@ namespace Greenshot.Drawing var newFont = CreateFont(fontFamily, fontBold, fontItalic, fontSize); _font?.Dispose(); _font = newFont; - _textBox.Font = _font; } catch (Exception ex) { @@ -400,7 +419,6 @@ namespace Greenshot.Drawing var newFont = CreateFont(fontFamily, fontBold, fontItalic, fontSize); _font?.Dispose(); _font = newFont; - _textBox.Font = _font; } catch (Exception) { @@ -413,6 +431,8 @@ namespace Greenshot.Drawing } } + UpdateTextBoxFont(); + UpdateAlignment(); } @@ -423,7 +443,29 @@ namespace Greenshot.Drawing } /// - /// This will create the textbox exactly to the inner size of the element + /// Set TextBox font according to the TextContainer font and the parent zoom factor. + /// + private void UpdateTextBoxFont() + { + if (_textBox == null || _font == null) + { + return; + } + + var textBoxFontScale = _parent?.ZoomFactor ?? Fraction.Identity; + + var newFont = new Font( + _font.FontFamily, + _font.Size * textBoxFontScale, + _font.Style, + GraphicsUnit.Pixel + ); + _textBox.Font.Dispose(); + _textBox.Font = newFont; + } + + /// + /// This will align the textbox exactly to the inner size of the element /// is a bit of a hack, but for now it seems to work... /// private void UpdateTextBoxPosition() @@ -453,12 +495,9 @@ namespace Greenshot.Drawing _textBox.Height = displayRectangle.Height - 2 * lineWidth + correction; } - public override void ApplyBounds(RectangleF newBounds) - { - base.ApplyBounds(newBounds); - UpdateTextBoxPosition(); - } - + /// + /// Set TextBox text align and fore color according to field values. + /// private void UpdateTextBoxFormat() { if (_textBox == null) From ef5b5deb7ade72376f7adef6081125ab90cec90d Mon Sep 17 00:00:00 2001 From: Killy Date: Fri, 1 May 2020 18:16:33 +0300 Subject: [PATCH 22/25] Smarter zoom - keep selected elements in sight --- Greenshot/Drawing/DrawableContainerList.cs | 24 ++++++++++++ Greenshot/Drawing/Surface.cs | 7 ++++ Greenshot/Forms/ImageEditorForm.cs | 38 +++++++++++++------ .../Interfaces/Drawing/Container.cs | 4 ++ 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/Greenshot/Drawing/DrawableContainerList.cs b/Greenshot/Drawing/DrawableContainerList.cs index 9451b06ea..3d200598e 100644 --- a/Greenshot/Drawing/DrawableContainerList.cs +++ b/Greenshot/Drawing/DrawableContainerList.cs @@ -235,6 +235,30 @@ namespace Greenshot.Drawing { return false; } + /// + /// A rectangle containing DrawingBounds of all drawableContainers in this list, + /// or empty rectangle if nothing is there. + /// + public Rectangle DrawingBounds + { + get + { + if (Count == 0) + { + return Rectangle.Empty; + } + else + { + var result = this[0].DrawingBounds; + for (int i = 1; i < Count; i++) + { + result = Rectangle.Union(result, this[i].DrawingBounds); + } + return result; + } + } + } + /// /// Triggers all elements in the list ot be redrawn. /// diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index 2ea5e0b00..8b33b6a56 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -1922,6 +1922,13 @@ namespace Greenshot.Drawing ); } + /// + /// Get the rectangle bounding all selected elements (in surface coordinates space), + /// or empty rectangle if nothing is selcted. + /// + public Rectangle GetSelectionRectangle() + => ToSurfaceCoordinates(selectedElements.DrawingBounds); + /// /// Duplicate all the selecteded elements /// diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index 1e177e428..8bf59df27 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -1580,21 +1580,37 @@ namespace Greenshot { { return; } + if (value == Surface.ZoomFactor) + { + return; + } - // Store old scroll position - // When no scroll is currently needed - prefer top left corner. - var horizontalCenter = 0.0; - var verticalCenter = 0.0; - var rc = surface.GetVisibleRectangle(); + // Store scroll position + var rc = surface.GetVisibleRectangle(); // use visible rc by default var size = surface.Size; - if (size.Width > rc.Width) + if (value > Surface.ZoomFactor) // being smart on zoom-in { - horizontalCenter = 1.0 * (rc.Left + rc.Width / 2) / size.Width; - } - if (size.Height > rc.Height) - { - verticalCenter = 1.0 * (rc.Top + rc.Height / 2) / size.Height; + var sel = surface.GetSelectionRectangle(); + if (sel != Rectangle.Empty) + { + rc.Intersect(sel); // zoom to visible part of selection + } + else + { + // if image fits completely to currently visible rc and there are no things to focus on + // - prefer top left corner to zoom-in as less disorienting for screenshots + if (size.Width < rc.Width) + { + rc.Width = 0; + } + if (size.Height < rc.Height) + { + rc.Height = 0; + } + } } + var horizontalCenter = 1.0 * (rc.Left + rc.Width / 2) / size.Width; + var verticalCenter = 1.0 * (rc.Top + rc.Height / 2) / size.Height; // Set the new zoom value Surface.ZoomFactor = value; diff --git a/GreenshotPlugin/Interfaces/Drawing/Container.cs b/GreenshotPlugin/Interfaces/Drawing/Container.cs index 35a6cf50a..f4065801a 100644 --- a/GreenshotPlugin/Interfaces/Drawing/Container.cs +++ b/GreenshotPlugin/Interfaces/Drawing/Container.cs @@ -145,6 +145,10 @@ namespace GreenshotPlugin.Interfaces.Drawing get; set; } + Rectangle DrawingBounds + { + get; + } void MakeBoundsChangeUndoable(bool allowMerge); void Transform(Matrix matrix); void MoveBy(int dx, int dy); From 0fce43404d4a87c28713cfa9fdf10c845142e8c1 Mon Sep 17 00:00:00 2001 From: Killy Date: Fri, 1 May 2020 18:45:10 +0300 Subject: [PATCH 23/25] Fix: prevent image blurring at 100% zoom Even at 100% GDI+ manages to do some smoothing. Also minor optimization - not messing with Graphics state when not needed. --- Greenshot/Drawing/Surface.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index 8b33b6a56..9f2a839cd 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -1461,8 +1461,7 @@ namespace Greenshot.Drawing Rectangle imageClipRectangle = ZoomClipRectangle(targetClipRectangle, _zoomFactor.Inverse(), 2); - bool isZoomedIn = _zoomFactor > Fraction.Identity; - if (_elements.HasIntersectingFilters(imageClipRectangle) || isZoomedIn) + if (_elements.HasIntersectingFilters(imageClipRectangle) || _zoomFactor > Fraction.Identity) { if (_buffer != null) { @@ -1491,7 +1490,11 @@ namespace Greenshot.Drawing _elements.Draw(graphics, _buffer, RenderMode.EDIT, imageClipRectangle); } targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); - if (isZoomedIn) + if (_zoomFactor == Fraction.Identity) + { + targetGraphics.DrawImage(_buffer, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + } + else if(_zoomFactor > Fraction.Identity) { DrawSharpImage(targetGraphics, _buffer, imageClipRectangle); } @@ -1506,7 +1509,14 @@ namespace Greenshot.Drawing DrawBackground(targetGraphics, targetClipRectangle); targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); - DrawSmoothImage(targetGraphics, Image, imageClipRectangle); + if (_zoomFactor == Fraction.Identity) + { + targetGraphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + } + else + { + DrawSmoothImage(targetGraphics, Image, imageClipRectangle); + } _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); targetGraphics.ResetTransform(); } From b2a6fc877403388eb007fe8272242c964a0c0a1e Mon Sep 17 00:00:00 2001 From: Killy Date: Fri, 1 May 2020 18:53:08 +0300 Subject: [PATCH 24/25] No ScaleTransform is needed at 100% zoom --- Greenshot/Drawing/Surface.cs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index 9f2a839cd..6c89238f0 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -1489,36 +1489,39 @@ namespace Greenshot.Drawing graphics.SetClip(ZoomClipRectangle(Rectangle.Round(targetGraphics.ClipBounds), _zoomFactor.Inverse(), 2)); _elements.Draw(graphics, _buffer, RenderMode.EDIT, imageClipRectangle); } - targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); if (_zoomFactor == Fraction.Identity) { targetGraphics.DrawImage(_buffer, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); } - else if(_zoomFactor > Fraction.Identity) - { - DrawSharpImage(targetGraphics, _buffer, imageClipRectangle); - } else { - DrawSmoothImage(targetGraphics, _buffer, imageClipRectangle); + targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); + if (_zoomFactor > Fraction.Identity) + { + DrawSharpImage(targetGraphics, _buffer, imageClipRectangle); + } + else + { + DrawSmoothImage(targetGraphics, _buffer, imageClipRectangle); + } + targetGraphics.ResetTransform(); } - targetGraphics.ResetTransform(); } else { DrawBackground(targetGraphics, targetClipRectangle); - - targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); if (_zoomFactor == Fraction.Identity) { targetGraphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); } else { + targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); DrawSmoothImage(targetGraphics, Image, imageClipRectangle); + _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); + targetGraphics.ResetTransform(); } - _elements.Draw(targetGraphics, null, RenderMode.EDIT, imageClipRectangle); - targetGraphics.ResetTransform(); } // No clipping for the adorners From 79ea558dbbdede0d1fcd5a1f2a7520d8d800648b Mon Sep 17 00:00:00 2001 From: Killy Date: Sat, 2 May 2020 01:52:05 +0300 Subject: [PATCH 25/25] Fix: zoom-in when selection is out of sight --- Greenshot/Forms/ImageEditorForm.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index 8bf59df27..d397b8bc8 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -1590,10 +1590,11 @@ namespace Greenshot { var size = surface.Size; if (value > Surface.ZoomFactor) // being smart on zoom-in { - var sel = surface.GetSelectionRectangle(); - if (sel != Rectangle.Empty) + var selection = surface.GetSelectionRectangle(); + selection.Intersect(rc); + if (selection != Rectangle.Empty) { - rc.Intersect(sel); // zoom to visible part of selection + rc = selection; // zoom to visible part of selection } else {