diff --git a/Greenshot/Drawing/Adorners/AbstractAdorner.cs b/Greenshot/Drawing/Adorners/AbstractAdorner.cs index 0612753aa..70a56f29e 100644 --- a/Greenshot/Drawing/Adorners/AbstractAdorner.cs +++ b/Greenshot/Drawing/Adorners/AbstractAdorner.cs @@ -116,6 +116,18 @@ namespace Greenshot.Drawing.Adorners } } + /// + /// Return the bounds of the Adorner as displayed on the parent Surface + /// + protected virtual Rectangle BoundsOnSurface + { + get + { + Point displayLocation = Owner.Parent.ToSurfaceCoordinates(Location); + return new Rectangle(displayLocation.X - _size.Width / 2, displayLocation.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..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 = Bounds; + 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 e75126f10..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 = Bounds; + 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 8ca0a91ef..67c01a533 100644 --- a/Greenshot/Drawing/Adorners/TargetAdorner.cs +++ b/Greenshot/Drawing/Adorners/TargetAdorner.cs @@ -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; } } @@ -96,7 +96,7 @@ namespace Greenshot.Drawing.Adorners { Graphics targetGraphics = paintEventArgs.Graphics; - var bounds = Bounds; + 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 2dc9761c0..58cee17b1 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 Rectangle.Empty; + } + } + } 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..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; @@ -298,34 +297,7 @@ namespace Greenshot.Drawing public virtual void Invalidate() { if (Status != EditStatus.UNDRAWN) { - _parent?.Invalidate(DrawingBounds); - } - } - - 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; + _parent?.InvalidateElements(DrawingBounds); } } diff --git a/Greenshot/Drawing/DrawableContainerList.cs b/Greenshot/Drawing/DrawableContainerList.cs index d3c325f5b..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. /// @@ -286,7 +310,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, @@ -523,9 +547,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 +566,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/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 bd56db3a2..6c89238f0 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 * @@ -298,10 +298,39 @@ namespace Greenshot.Drawing set { _image = value; - Size = _image.Size; + UpdateSize(); } } + [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 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(inverse, 0, 0, inverse, 0, 0); + UpdateSize(); + } + } + + + /// + /// 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 +476,6 @@ namespace Greenshot.Drawing // Set new values Image = newImage; - Size = newImage.Size; _modified = true; } @@ -779,9 +807,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); @@ -792,8 +820,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); @@ -967,13 +993,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) { @@ -1086,6 +1112,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 +1125,7 @@ namespace Greenshot.Drawing /// private void SurfaceMouseDown(object sender, MouseEventArgs e) { + e = InverseZoomMouseCoordinates(e); // Handle Adorners var adorner = FindActiveAdorner(e); @@ -1187,6 +1220,7 @@ namespace Greenshot.Drawing /// private void SurfaceMouseUp(object sender, MouseEventArgs e) { + e = InverseZoomMouseCoordinates(e); // Handle Adorners var adorner = FindActiveAdorner(e); @@ -1276,6 +1310,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 +1407,25 @@ namespace Greenshot.Drawing return GetImage(RenderMode.EXPORT); } + 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 * 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, 1)); + /// /// This is the event handler for the Paint Event, try to draw as little as possible! /// @@ -1379,14 +1434,34 @@ 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; } - if (_elements.HasIntersectingFilters(clipRectangle)) + // 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); + + if (_elements.HasIntersectingFilters(imageClipRectangle) || _zoomFactor > Fraction.Identity) { if (_buffer != null) { @@ -1409,18 +1484,44 @@ 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(Rectangle.Round(targetGraphics.ClipBounds), _zoomFactor.Inverse(), 2)); + _elements.Draw(graphics, _buffer, RenderMode.EDIT, imageClipRectangle); + } + if (_zoomFactor == Fraction.Identity) + { + targetGraphics.DrawImage(_buffer, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); + } + else + { + targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); + if (_zoomFactor > Fraction.Identity) + { + DrawSharpImage(targetGraphics, _buffer, imageClipRectangle); + } + else + { + DrawSmoothImage(targetGraphics, _buffer, imageClipRectangle); + } + targetGraphics.ResetTransform(); } - targetGraphics.DrawImage(_buffer, clipRectangle, clipRectangle, GraphicsUnit.Pixel); } else { - DrawBackground(targetGraphics, clipRectangle); - targetGraphics.DrawImage(Image, clipRectangle, clipRectangle, GraphicsUnit.Pixel); - _elements.Draw(targetGraphics, null, RenderMode.EDIT, clipRectangle); + DrawBackground(targetGraphics, targetClipRectangle); + 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(); + } } // No clipping for the adorners @@ -1435,6 +1536,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 @@ -1742,43 +1869,79 @@ 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). + /// + public 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 + ); + } + + /// + /// 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 /// @@ -2034,5 +2197,57 @@ 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[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/Greenshot/Drawing/TextContainer.cs b/Greenshot/Drawing/TextContainer.cs index 0f4866d71..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,12 +443,34 @@ 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() { - if (_textBox == null) + if (_textBox == null || Parent == null) { return; } @@ -442,22 +484,20 @@ namespace Greenshot.Drawing correction = -1; } Rectangle absRectangle = GuiRectangle.GetGuiRectangle(Left, Top, Width, Height); - _textBox.Left = absRectangle.Left + lineWidth; - _textBox.Top = absRectangle.Top + lineWidth; + Rectangle displayRectangle = Parent.ToSurfaceCoordinates(absRectangle); + _textBox.Left = displayRectangle.X + lineWidth; + _textBox.Top = displayRectangle.Y + lineWidth; if (lineThickness <= 1) { lineWidth = 0; } - _textBox.Width = absRectangle.Width - 2 * lineWidth + correction; - _textBox.Height = absRectangle.Height - 2 * lineWidth + correction; - } - - public override void ApplyBounds(RectangleF newBounds) - { - base.ApplyBounds(newBounds); - UpdateTextBoxPosition(); + _textBox.Width = displayRectangle.Width - 2 * lineWidth + correction; + _textBox.Height = displayRectangle.Height - 2 * lineWidth + correction; } + /// + /// Set TextBox text align and fore color according to field values. + /// private void UpdateTextBoxFormat() { if (_textBox == null) diff --git a/Greenshot/Forms/ImageEditorForm.Designer.cs b/Greenshot/Forms/ImageEditorForm.Designer.cs index 9a653cc21..04da3d0a0 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+9"; + 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 = "1/4"; + 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 = "1/2"; + 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 = "2/3"; + 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 = "3/4"; + 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+0"; + this.zoomActualSizeMenuItem.Size = new System.Drawing.Size(209, 22); + this.zoomActualSizeMenuItem.Tag = "1/1"; + 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 = "2/1"; + 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 = "3/1"; + 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 = "4/1"; + 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 = "6/1"; + 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..d397b8bc8 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -66,6 +66,11 @@ 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 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. /// @@ -420,20 +425,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 +859,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.NumPad0: // Ctrl + Num0 + case Keys.D0: // Ctrl + 0 + ZoomSetValueMenuItemClick(zoomActualSizeMenuItem, e); + break; + case Keys.NumPad9: // Ctrl + Num9 + case Keys.D9: // Ctrl + 9 + 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 +1462,178 @@ 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 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; + 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.Right - Left, workingArea.Bottom - Top); + } else { + return workingArea.Size; + } + } + + private void ZoomInMenuItemClick(object sender, EventArgs e) { + 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 zoomValue = Surface.ZoomFactor; + 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; + var nextValue = Fraction.Parse((string)senderMenuItem.Tag); + + ZoomSetValue(nextValue); + } + + 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(Fraction scale, int source, int boundary) + => (int)(source * scale) <= boundary; + + 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); + } + + private void ZoomSetValue(Fraction value) { + var surface = Surface as Surface; + var panel = surface?.Parent as Panel; + if (panel == null) + { + return; + } + if (value == Surface.ZoomFactor) + { + return; + } + + // Store scroll position + var rc = surface.GetVisibleRectangle(); // use visible rc by default + var size = surface.Size; + if (value > Surface.ZoomFactor) // being smart on zoom-in + { + var selection = surface.GetSelectionRectangle(); + selection.Intersect(rc); + if (selection != Rectangle.Empty) + { + rc = selection; // 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; + Size = GetOptimalWindowSize(); + AlignCanvasPositionAfterResize(); + + // Update zoom controls + 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; + } + } + + // 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 + ); + } } } 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 000000000..21cf71d63 Binary files /dev/null and b/Greenshot/icons/fugue/magnifier-zoom-actual.png differ diff --git a/Greenshot/icons/fugue/magnifier-zoom-fit.png b/Greenshot/icons/fugue/magnifier-zoom-fit.png new file mode 100644 index 000000000..8364359fd Binary files /dev/null and b/Greenshot/icons/fugue/magnifier-zoom-fit.png differ diff --git a/Greenshot/icons/fugue/magnifier-zoom-in.png b/Greenshot/icons/fugue/magnifier-zoom-in.png new file mode 100644 index 000000000..800ec1205 Binary files /dev/null and b/Greenshot/icons/fugue/magnifier-zoom-in.png differ diff --git a/Greenshot/icons/fugue/magnifier-zoom-out.png b/Greenshot/icons/fugue/magnifier-zoom-out.png new file mode 100644 index 000000000..3ec2b7a5d Binary files /dev/null and b/Greenshot/icons/fugue/magnifier-zoom-out.png differ diff --git a/Greenshot/icons/fugue/magnifier-zoom.png b/Greenshot/icons/fugue/magnifier-zoom.png new file mode 100644 index 000000000..941b10356 Binary files /dev/null and b/Greenshot/icons/fugue/magnifier-zoom.png differ 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); 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/Drawing/Container.cs b/GreenshotPlugin/Interfaces/Drawing/Container.cs index b2b6e3d19..f4065801a 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); @@ -146,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); diff --git a/GreenshotPlugin/Interfaces/ISurface.cs b/GreenshotPlugin/Interfaces/ISurface.cs index 44a150364..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; @@ -84,8 +85,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 +95,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); @@ -147,8 +148,13 @@ namespace GreenshotPlugin.Interfaces /// /// 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; @@ -186,9 +192,32 @@ namespace GreenshotPlugin.Interfaces get; set; } - int Width { get; } - int Height { get; } + + /// + /// Zoom value applied to the surface. + /// + Fraction ZoomFactor { get; set; } + /// + /// Translate a point from image coorditate space to surface coordinate space. + /// + /// 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); + /// + /// 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); } -} \ No newline at end of file +}