/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2012 Thomas Braun, Jens Klingen, Robin Krom * * For more information see: http://getgreenshot.org/ * The Greenshot project is hosted on Sourceforge: http://sourceforge.net/projects/greenshot/ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ using Greenshot.Drawing.Fields; using Greenshot.Helpers; using Greenshot.Plugin.Drawing; using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Text; using System.Runtime.Serialization; namespace Greenshot.Drawing { /// /// Description of SpeechbubbleContainer. /// [Serializable] public class SpeechbubbleContainer : TextContainer { private Point _initialGripperPoint; #region TargetGripper serializing code // Only used for serializing the TargetGripper location private Point _storedTargetGripperLocation; /// /// Store the current location of the target gripper /// /// [OnSerializing] private void SetValuesOnSerializing(StreamingContext context) { if (TargetGripper != null) { _storedTargetGripperLocation = TargetGripper.Location; } } /// /// Restore the target gripper /// /// [OnDeserialized] private void SetValuesOnDeserialized(StreamingContext context) { InitTargetGripper(Color.Yellow, _storedTargetGripperLocation); } #endregion TargetGripper serializing code public SpeechbubbleContainer(Surface parent) : base(parent) { } /// /// We set our own field values /// protected override void InitializeFields() { AddField(GetType(), FieldType.LINE_THICKNESS, 2); AddField(GetType(), FieldType.LINE_COLOR, DefaultLineColor); AddField(GetType(), FieldType.SHADOW, false); AddField(GetType(), FieldType.FONT_ITALIC, false); AddField(GetType(), FieldType.FONT_BOLD, false); AddField(GetType(), FieldType.FILL_COLOR, Color.White); AddField(GetType(), FieldType.FONT_FAMILY, FontFamily.GenericSansSerif.Name); AddField(GetType(), FieldType.FONT_SIZE, 20f); AddField(GetType(), FieldType.TEXT_HORIZONTAL_ALIGNMENT, StringAlignment.Center); AddField(GetType(), FieldType.TEXT_VERTICAL_ALIGNMENT, StringAlignment.Center); } /// /// Called from Surface (the _parent) when the drawing begins (mouse-down) /// /// true if the surface doesn't need to handle the event public override bool HandleMouseDown(int mouseX, int mouseY) { if (TargetGripper == null) { _initialGripperPoint = new Point(mouseX, mouseY); InitTargetGripper(Color.Yellow, new Point(mouseX, mouseY)); } return base.HandleMouseDown(mouseX, mouseY); } /// /// Overriding the HandleMouseMove will help us to make sure the tail is always visible. /// Should fix BUG-1682 /// /// /// /// base.HandleMouseMove public override bool HandleMouseMove(int x, int y) { bool returnValue = base.HandleMouseMove(x, y); bool leftAligned = _boundsAfterResize.Right - _boundsAfterResize.Left >= 0; bool topAligned = _boundsAfterResize.Bottom - _boundsAfterResize.Top >= 0; int xOffset = leftAligned ? -20 : 20; int yOffset = topAligned ? -20 : 20; Point newGripperLocation = _initialGripperPoint; newGripperLocation.Offset(xOffset, yOffset); if (TargetGripper.Location != newGripperLocation) { Invalidate(); TargetGripperMove(newGripperLocation.X, newGripperLocation.Y); Invalidate(); } return returnValue; } /// /// The DrawingBound should be so close as possible to the shape, so we don't invalidate to much. /// public override Rectangle DrawingBounds { get { if (Status != EditStatus.UNDRAWN) { int lineThickness = GetFieldValueAsInt(FieldType.LINE_THICKNESS); Color lineColor = GetFieldValueAsColor(FieldType.LINE_COLOR); bool shadow = GetFieldValueAsBool(FieldType.SHADOW); using (Pen pen = new Pen(lineColor, lineThickness)) { int inflateValue = lineThickness + 2 + (shadow ? 6 : 0); using (GraphicsPath tailPath = CreateTail()) { return Rectangle.Inflate(Rectangle.Union(Rectangle.Round(tailPath.GetBounds(new Matrix(), pen)), GuiRectangle.GetGuiRectangle(Left, Top, Width, Height)), inflateValue, inflateValue); } } } return Rectangle.Empty; } } /// /// Helper method to create the bubble GraphicsPath, so we can also calculate the bounds /// /// /// private GraphicsPath CreateBubble(int lineThickness) { GraphicsPath bubble = new GraphicsPath(); Rectangle rect = GuiRectangle.GetGuiRectangle(Left, Top, Width, Height); Rectangle bubbleRect = GuiRectangle.GetGuiRectangle(0, 0, rect.Width, rect.Height); // adapt corner radius to small rectangle dimensions int smallerSideLength = Math.Min(bubbleRect.Width, bubbleRect.Height); int cornerRadius = Math.Min(30, smallerSideLength / 2 - lineThickness); if (cornerRadius > 0) { bubble.AddArc(bubbleRect.X, bubbleRect.Y, cornerRadius, cornerRadius, 180, 90); bubble.AddArc(bubbleRect.X + bubbleRect.Width - cornerRadius, bubbleRect.Y, cornerRadius, cornerRadius, 270, 90); bubble.AddArc(bubbleRect.X + bubbleRect.Width - cornerRadius, bubbleRect.Y + bubbleRect.Height - cornerRadius, cornerRadius, cornerRadius, 0, 90); bubble.AddArc(bubbleRect.X, bubbleRect.Y + bubbleRect.Height - cornerRadius, cornerRadius, cornerRadius, 90, 90); } else { bubble.AddRectangle(bubbleRect); } bubble.CloseAllFigures(); using (Matrix bubbleMatrix = new Matrix()) { bubbleMatrix.Translate(rect.Left, rect.Top); bubble.Transform(bubbleMatrix); } return bubble; } /// /// Helper method to create the tail of the bubble, so we can also calculate the bounds /// /// private GraphicsPath CreateTail() { Rectangle rect = GuiRectangle.GetGuiRectangle(Left, Top, Width, Height); int tailLength = GeometryHelper.Distance2D(rect.Left + (rect.Width / 2), rect.Top + (rect.Height / 2), TargetGripper.Left, TargetGripper.Top); int tailWidth = (Math.Abs(rect.Width) + Math.Abs(rect.Height)) / 20; // This should fix a problem with the tail being to wide tailWidth = Math.Min(Math.Abs(rect.Width) / 2, tailWidth); tailWidth = Math.Min(Math.Abs(rect.Height) / 2, tailWidth); GraphicsPath tail = new GraphicsPath(); tail.AddLine(-tailWidth, 0, tailWidth, 0); tail.AddLine(tailWidth, 0, 0, -tailLength); tail.CloseFigure(); int tailAngle = 90 + (int)GeometryHelper.Angle2D(rect.Left + (rect.Width / 2), rect.Top + (rect.Height / 2), TargetGripper.Left, TargetGripper.Top); using (Matrix tailMatrix = new Matrix()) { tailMatrix.Translate(rect.Left + (rect.Width / 2), rect.Top + (rect.Height / 2)); tailMatrix.Rotate(tailAngle); tail.Transform(tailMatrix); } return tail; } /// /// This is to draw the actual container /// /// /// public override void Draw(Graphics graphics, RenderMode renderMode) { if (TargetGripper == null) { return; } graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.CompositingQuality = CompositingQuality.HighQuality; graphics.PixelOffsetMode = PixelOffsetMode.None; graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; Color lineColor = GetFieldValueAsColor(FieldType.LINE_COLOR); Color fillColor = GetFieldValueAsColor(FieldType.FILL_COLOR); bool shadow = GetFieldValueAsBool(FieldType.SHADOW); int lineThickness = GetFieldValueAsInt(FieldType.LINE_THICKNESS); bool lineVisible = (lineThickness > 0 && Colors.IsVisible(lineColor)); Rectangle rect = GuiRectangle.GetGuiRectangle(Left, Top, Width, Height); if (Selected && renderMode == RenderMode.EDIT) { DrawSelectionBorder(graphics, rect); } GraphicsPath bubble = CreateBubble(lineThickness); GraphicsPath tail = CreateTail(); //draw shadow first if (shadow && (lineVisible || Colors.IsVisible(fillColor))) { const int basealpha = 100; int alpha = basealpha; const int steps = 5; int currentStep = lineVisible ? 1 : 0; using (Matrix shadowMatrix = new Matrix()) using (GraphicsPath bubbleClone = (GraphicsPath)bubble.Clone()) using (GraphicsPath tailClone = (GraphicsPath)tail.Clone()) { shadowMatrix.Translate(1, 1); while (currentStep <= steps) { using (Pen shadowPen = new Pen(Color.FromArgb(alpha, 100, 100, 100))) { shadowPen.Width = lineVisible ? lineThickness : 1; tailClone.Transform(shadowMatrix); graphics.DrawPath(shadowPen, tailClone); bubbleClone.Transform(shadowMatrix); graphics.DrawPath(shadowPen, bubbleClone); } currentStep++; alpha = alpha - (basealpha / steps); } } } GraphicsState state = graphics.Save(); // draw the tail border where the bubble is not visible using (Region clipRegion = new Region(bubble)) { graphics.SetClip(clipRegion, CombineMode.Exclude); using (Pen pen = new Pen(lineColor, lineThickness)) { graphics.DrawPath(pen, tail); } } graphics.Restore(state); if (Colors.IsVisible(fillColor)) { //draw the bubbleshape state = graphics.Save(); using (Brush brush = new SolidBrush(fillColor)) { graphics.FillPath(brush, bubble); } graphics.Restore(state); } if (lineVisible) { //draw the bubble border state = graphics.Save(); // Draw bubble where the Tail is not visible. using (Region clipRegion = new Region(tail)) { graphics.SetClip(clipRegion, CombineMode.Exclude); using (Pen pen = new Pen(lineColor, lineThickness)) { //pen.EndCap = pen.StartCap = LineCap.Round; graphics.DrawPath(pen, bubble); } } graphics.Restore(state); } if (Colors.IsVisible(fillColor)) { // Draw the tail border state = graphics.Save(); using (Brush brush = new SolidBrush(fillColor)) { graphics.FillPath(brush, tail); } graphics.Restore(state); } // cleanup the paths bubble.Dispose(); tail.Dispose(); // Draw the text UpdateFormat(); DrawText(graphics, rect, lineThickness, lineColor, shadow, StringFormat, Text, Font); } public override bool Contains(int x, int y) { if (base.Contains(x, y)) { return true; } Point clickedPoint = new Point(x, y); if (Status != EditStatus.UNDRAWN) { int lineThickness = GetFieldValueAsInt(FieldType.LINE_THICKNESS); Color lineColor = GetFieldValueAsColor(FieldType.LINE_COLOR); using (Pen pen = new Pen(lineColor, lineThickness)) { using (GraphicsPath bubblePath = CreateBubble(lineThickness)) { bubblePath.Widen(pen); if (bubblePath.IsVisible(clickedPoint)) { return true; } } using (GraphicsPath tailPath = CreateTail()) { tailPath.Widen(pen); if (tailPath.IsVisible(clickedPoint)) { return true; } } } } return false; } public override bool ClickableAt(int x, int y) { return Contains(x, y); } } }