greenshot/Greenshot/Drawing/DrawableContainer.cs

699 lines
No EOL
22 KiB
C#

/*
* Greenshot - a free and open source screenshot tool
* Copyright (C) 2007-2013 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 <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using Greenshot.Drawing.Fields;
using Greenshot.Drawing.Filters;
using Greenshot.Helpers;
using Greenshot.Plugin;
using Greenshot.Plugin.Drawing;
using Greenshot.Memento;
using System.Drawing.Drawing2D;
using Greenshot.Configuration;
using Greenshot.IniFile;
namespace Greenshot.Drawing {
/// <summary>
/// represents a rectangle, ellipse, label or whatever. Can contain filters, too.
/// serializable for clipboard support
/// Subclasses should fulfill INotifyPropertyChanged contract, i.e. call
/// OnPropertyChanged whenever a public property has been changed.
/// </summary>
[Serializable()]
public abstract class DrawableContainer : AbstractFieldHolderWithChildren, INotifyPropertyChanged, IDrawableContainer {
private static readonly log4net.ILog LOG = log4net.LogManager.GetLogger(typeof(DrawableContainer));
protected static readonly EditorConfiguration editorConfig = IniConfig.GetIniSection<EditorConfiguration>();
private bool isMadeUndoable = false;
public virtual void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
if (disposing) {
for (int i = 0; i < grippers.Length; i++) {
grippers[i].Dispose();
grippers[i] = null;
}
FieldAggregator aggProps = parent.FieldAggregator;
aggProps.UnbindElement(this);
}
}
~DrawableContainer() {
Dispose(false);
}
[NonSerialized]
private PropertyChangedEventHandler propertyChanged;
public event PropertyChangedEventHandler PropertyChanged {
add { propertyChanged += value; }
remove{ propertyChanged -= value; }
}
public List<IFilter> Filters {
get {
List<IFilter> ret = new List<IFilter>();
foreach(IFieldHolder c in Children) {
if (c is IFilter) {
ret.Add(c as IFilter);
}
}
return ret;
}
}
[NonSerialized]
internal Surface parent;
public ISurface Parent {
get { return parent; }
set { SwitchParent((Surface)value); }
}
[NonSerialized]
protected Gripper[] grippers;
private bool layoutSuspended = false;
[NonSerialized]
private bool selected = false;
public bool Selected {
get {return selected;}
set {
selected = value;
OnPropertyChanged("Selected");
}
}
[NonSerialized]
private EditStatus status = EditStatus.UNDRAWN;
public EditStatus Status {
get {
return status;
}
set {
status = value;
}
}
private int left = 0;
public int Left {
get { return left; }
set {
if(value != left) {
left = value;
DoLayout();
}
}
}
private int top = 0;
public int Top {
get { return top; }
set {
if(value != top) {
top = value;
DoLayout();
}
}
}
private int width = 0;
public int Width {
get { return width; }
set {
if(value != width) {
width = value;
DoLayout();
}
}
}
private int height = 0;
public int Height {
get { return height; }
set {
if(value != height) {
height = value;
DoLayout();
}
}
}
public Point Location {
get {
return new Point(left, top);
}
}
public Size Size {
get {
return new Size(width, height);
}
}
[NonSerialized]
/// <summary>
/// will store current bounds of this DrawableContainer before starting a resize
/// </summary>
private Rectangle boundsBeforeResize = Rectangle.Empty;
[NonSerialized]
/// <summary>
/// "workbench" rectangle - used for calculatoing bounds during resizing (to be applied to this DrawableContainer afterwards)
/// </summary>
private RectangleF boundsAfterResize = RectangleF.Empty;
public Rectangle Bounds {
get { return GuiRectangle.GetGuiRectangle(Left, Top, Width, Height); }
set {
Left = round(value.Left);
Top = round(value.Top);
Width = round(value.Width);
Height = round(value.Height);
}
}
public virtual void ApplyBounds(RectangleF newBounds) {
Left = round(newBounds.Left);
Top = round(newBounds.Top);
Width = round(newBounds.Width);
Height = round(newBounds.Height);
}
public DrawableContainer(Surface parent) {
this.parent = parent;
InitControls();
}
public void Add(IFilter filter) {
AddChild(filter);
}
public void Remove(IFilter filter) {
RemoveChild(filter);
}
private int round(float f) {
if(float.IsPositiveInfinity(f) || f>int.MaxValue/2) return int.MaxValue/2;
else if (float.IsNegativeInfinity(f) || f<int.MinValue/2) return int.MinValue/2;
return (int)Math.Round(f);
}
private int round(double d) {
if(Double.IsPositiveInfinity(d) || d>int.MaxValue/2) return int.MaxValue/2;
else if (Double.IsNegativeInfinity(d) || d<int.MinValue/2) return int.MinValue/2;
else return (int)Math.Round(d);
}
private bool accountForShadowChange = false;
public virtual Rectangle DrawingBounds {
get {
foreach(IFilter filter in Filters) {
if (filter.Invert) {
return new Rectangle(Point.Empty, parent.Image.Size);
}
}
// Take a base safetymargin
int lineThickness = 5;
if (HasField(FieldType.LINE_THICKNESS)) {
lineThickness += GetFieldValueAsInt(FieldType.LINE_THICKNESS);
}
int offset = lineThickness/2;
int shadow = 0;
if (accountForShadowChange || (HasField(FieldType.SHADOW) && GetFieldValueAsBool(FieldType.SHADOW))){
accountForShadowChange = false;
shadow += 10;
}
return new Rectangle(Bounds.Left-offset, Bounds.Top-offset, Bounds.Width+lineThickness+shadow, Bounds.Height+lineThickness+shadow);
}
}
public virtual void Invalidate() {
parent.Invalidate(DrawingBounds);
}
public void AlignToParent(HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment) {
int lineThickness = GetFieldValueAsInt(FieldType.LINE_THICKNESS);
if (horizontalAlignment == HorizontalAlignment.Left) {
Left = lineThickness/2;
}
if (horizontalAlignment == HorizontalAlignment.Right) {
Left = parent.Width - this.Width - lineThickness/2;
}
if (horizontalAlignment == HorizontalAlignment.Center) {
Left = (parent.Width / 2) - (this.Width / 2) - lineThickness/2;
}
if (verticalAlignment == VerticalAlignment.TOP) {
Top = lineThickness/2;
}
if (verticalAlignment == VerticalAlignment.BOTTOM) {
Top = parent.Height - this.Height - lineThickness/2;
}
if (verticalAlignment == VerticalAlignment.CENTER) {
Top = (parent.Height / 2) - (this.Height / 2) - lineThickness/2;
}
}
public virtual bool InitContent() { return true; }
public virtual void OnDoubleClick() {}
private void InitControls() {
InitGrippers();
DoLayout();
}
protected void InitGrippers() {
grippers = new Gripper[8];
for(int i=0; i<grippers.Length; i++) {
grippers[i] = new Gripper();
grippers[i].Position = i;
grippers[i].MouseDown += new MouseEventHandler(gripperMouseDown);
grippers[i].MouseUp += new MouseEventHandler(gripperMouseUp);
grippers[i].MouseMove += new MouseEventHandler(gripperMouseMove);
grippers[i].Visible = false;
grippers[i].Parent = parent;
}
grippers[Gripper.POSITION_TOP_CENTER].Cursor = Cursors.SizeNS;
grippers[Gripper.POSITION_MIDDLE_RIGHT].Cursor = Cursors.SizeWE;
grippers[Gripper.POSITION_BOTTOM_CENTER].Cursor = Cursors.SizeNS;
grippers[Gripper.POSITION_MIDDLE_LEFT].Cursor = Cursors.SizeWE;
if (parent != null) {
parent.Controls.AddRange(grippers); // otherwise we'll attach them in switchParent
}
}
public void SuspendLayout() {
layoutSuspended = true;
}
public void ResumeLayout() {
layoutSuspended = false;
DoLayout();
}
protected virtual void DoLayout() {
if (!layoutSuspended) {
int[] xChoords = new int[]{this.Left-2,this.Left+this.Width/2-2,this.Left+this.Width-2};
int[] yChoords = new int[]{this.Top-2,this.Top+this.Height/2-2,this.Top+this.Height-2};
grippers[Gripper.POSITION_TOP_LEFT].Left = xChoords[0]; grippers[Gripper.POSITION_TOP_LEFT].Top = yChoords[0];
grippers[Gripper.POSITION_TOP_CENTER].Left = xChoords[1]; grippers[Gripper.POSITION_TOP_CENTER].Top = yChoords[0];
grippers[Gripper.POSITION_TOP_RIGHT].Left = xChoords[2]; grippers[Gripper.POSITION_TOP_RIGHT].Top = yChoords[0];
grippers[Gripper.POSITION_MIDDLE_RIGHT].Left = xChoords[2]; grippers[Gripper.POSITION_MIDDLE_RIGHT].Top = yChoords[1];
grippers[Gripper.POSITION_BOTTOM_RIGHT].Left = xChoords[2]; grippers[Gripper.POSITION_BOTTOM_RIGHT].Top = yChoords[2];
grippers[Gripper.POSITION_BOTTOM_CENTER].Left = xChoords[1]; grippers[Gripper.POSITION_BOTTOM_CENTER].Top = yChoords[2];
grippers[Gripper.POSITION_BOTTOM_LEFT].Left = xChoords[0]; grippers[Gripper.POSITION_BOTTOM_LEFT].Top = yChoords[2];
grippers[Gripper.POSITION_MIDDLE_LEFT].Left = xChoords[0]; grippers[Gripper.POSITION_MIDDLE_LEFT].Top = yChoords[1];
if((grippers[Gripper.POSITION_TOP_LEFT].Left < grippers[Gripper.POSITION_BOTTOM_RIGHT].Left && grippers[Gripper.POSITION_TOP_LEFT].Top < grippers[Gripper.POSITION_BOTTOM_RIGHT].Top) ||
grippers[Gripper.POSITION_TOP_LEFT].Left > grippers[Gripper.POSITION_BOTTOM_RIGHT].Left && grippers[Gripper.POSITION_TOP_LEFT].Top > grippers[Gripper.POSITION_BOTTOM_RIGHT].Top) {
grippers[Gripper.POSITION_TOP_LEFT].Cursor = Cursors.SizeNWSE;
grippers[Gripper.POSITION_TOP_RIGHT].Cursor = Cursors.SizeNESW;
grippers[Gripper.POSITION_BOTTOM_RIGHT].Cursor = Cursors.SizeNWSE;
grippers[Gripper.POSITION_BOTTOM_LEFT].Cursor = Cursors.SizeNESW;
} else if((grippers[Gripper.POSITION_TOP_LEFT].Left > grippers[Gripper.POSITION_BOTTOM_RIGHT].Left && grippers[Gripper.POSITION_TOP_LEFT].Top < grippers[Gripper.POSITION_BOTTOM_RIGHT].Top) ||
grippers[Gripper.POSITION_TOP_LEFT].Left < grippers[Gripper.POSITION_BOTTOM_RIGHT].Left && grippers[Gripper.POSITION_TOP_LEFT].Top > grippers[Gripper.POSITION_BOTTOM_RIGHT].Top) {
grippers[Gripper.POSITION_TOP_LEFT].Cursor = Cursors.SizeNESW;
grippers[Gripper.POSITION_TOP_RIGHT].Cursor = Cursors.SizeNWSE;
grippers[Gripper.POSITION_BOTTOM_RIGHT].Cursor = Cursors.SizeNESW;
grippers[Gripper.POSITION_BOTTOM_LEFT].Cursor = Cursors.SizeNWSE;
} else if (grippers[Gripper.POSITION_TOP_LEFT].Left == grippers[Gripper.POSITION_BOTTOM_RIGHT].Left) {
grippers[Gripper.POSITION_TOP_LEFT].Cursor = Cursors.SizeNS;
grippers[Gripper.POSITION_BOTTOM_RIGHT].Cursor = Cursors.SizeNS;
} else if (grippers[Gripper.POSITION_TOP_LEFT].Top == grippers[Gripper.POSITION_BOTTOM_RIGHT].Top) {
grippers[Gripper.POSITION_TOP_LEFT].Cursor = Cursors.SizeWE;
grippers[Gripper.POSITION_BOTTOM_RIGHT].Cursor = Cursors.SizeWE;
}
}
}
int mx;
int my;
private void gripperMouseDown(object sender, MouseEventArgs e) {
mx = e.X;
my = e.Y;
Status = EditStatus.RESIZING;
boundsBeforeResize = new Rectangle(left, top, width, height);
boundsAfterResize = new RectangleF(boundsBeforeResize.Left, boundsBeforeResize.Top, boundsBeforeResize.Width, boundsBeforeResize.Height);
isMadeUndoable = false;
}
private void gripperMouseUp(object sender, MouseEventArgs e) {
Status = EditStatus.IDLE;
boundsBeforeResize = Rectangle.Empty;
boundsAfterResize = RectangleF.Empty;
isMadeUndoable = false;
Invalidate();
}
private void gripperMouseMove(object sender, MouseEventArgs e) {
if(Status.Equals(EditStatus.RESIZING)) {
// check if we already made this undoable
if (!isMadeUndoable) {
// don't allow another undo until we are finished with this move
isMadeUndoable = true;
// Make undo-able
MakeBoundsChangeUndoable(false);
}
Invalidate();
SuspendLayout();
Gripper gr = (Gripper)sender;
int absX = gr.Left + e.X;
int absY = gr.Top + e.Y;
// reset "workbench" rectangle to current bounds
boundsAfterResize.X = boundsBeforeResize.X;
boundsAfterResize.Y = boundsBeforeResize.Y;
boundsAfterResize.Width = boundsBeforeResize.Width;
boundsAfterResize.Height = boundsBeforeResize.Height;
// calculate scaled rectangle
ScaleHelper.Scale(ref boundsAfterResize, gr.Position, new PointF(absX, absY), ScaleHelper.GetScaleOptions());
// apply scaled bounds to this DrawableContainer
ApplyBounds(boundsAfterResize);
ResumeLayout();
Invalidate();
}
}
private void childLabelMouseMove(object sender, MouseEventArgs e) {
if (Status.Equals(EditStatus.RESIZING)) {
Invalidate();
SuspendLayout();
this.Left += e.X - mx;
this.Top += e.Y - my;
ResumeLayout();
Invalidate();
}
}
public bool hasFilters {
get {
return Filters.Count > 0;
}
}
public abstract void Draw(Graphics graphics, RenderMode renderMode);
public virtual void DrawContent(Graphics graphics, Bitmap bmp, RenderMode renderMode, Rectangle clipRectangle) {
if (Children.Count > 0) {
if (Status != EditStatus.IDLE) {
DrawSelectionBorder(graphics, Bounds);
} else {
if (clipRectangle.Width != 0 && clipRectangle.Height != 0) {
foreach(IFilter filter in Filters) {
if (filter.Invert) {
filter.Apply(graphics, bmp, Bounds, renderMode);
} else {
Rectangle drawingRect = new Rectangle(Bounds.Location, Bounds.Size);
drawingRect.Intersect(clipRectangle);
filter.Apply(graphics, bmp, drawingRect, renderMode);
}
}
}
}
}
Draw(graphics, renderMode);
}
public virtual bool Contains(int x, int y) {
return Bounds.Contains(x , y);
}
public virtual bool ClickableAt(int x, int y) {
Rectangle r = GuiRectangle.GetGuiRectangle(Left, Top, Width, Height);
r.Inflate(5, 5);
return r.Contains(x, y);
}
protected void DrawSelectionBorder(Graphics g, Rectangle rect) {
using (Pen pen = new Pen(Color.MediumSeaGreen)) {
pen.DashPattern = new float[]{1,2};
pen.Width = 1;
g.DrawRectangle(pen, rect);
}
}
public virtual void ShowGrippers() {
for (int i=0; i<grippers.Length; i++) {
if(grippers[i].Enabled) grippers[i].Show();
else grippers[i].Hide();
}
this.ResumeLayout();
}
public void HideGrippers() {
this.SuspendLayout();
for (int i=0; i<grippers.Length; i++) {
grippers[i].Hide();
}
}
public void ResizeTo(int width, int height, int anchorPosition) {
this.SuspendLayout();
Width = width;
Height = height;
this.ResumeLayout();
}
/// <summary>
/// Make a following bounds change on this drawablecontainer undoable!
/// </summary>
/// <param name="allowMerge">true means allow the moves to be merged</param>
public void MakeBoundsChangeUndoable(bool allowMerge) {
this.parent.MakeUndoable(new DrawableContainerBoundsChangeMemento(this), allowMerge);
}
public void MoveBy(int dx, int dy) {
this.SuspendLayout();
this.Left += dx;
this.Top += dy;
this.ResumeLayout();
}
/// <summary>
/// A handler for the MouseDown, used if you don't want the surface to handle this for you
/// </summary>
/// <param name="x">current mouse x</param>
/// <param name="y">current mouse y</param>
/// <returns>true if the event is handled, false if the surface needs to handle it</returns>
public virtual bool HandleMouseDown(int x, int y) {
Left = boundsBeforeResize.X = x;
Top = boundsBeforeResize.Y = y;
return true;
}
/// <summary>
/// A handler for the MouseMove, used if you don't want the surface to handle this for you
/// </summary>
/// <param name="x">current mouse x</param>
/// <param name="y">current mouse y</param>
/// <returns>true if the event is handled, false if the surface needs to handle it</returns>
public virtual bool HandleMouseMove(int x, int y) {
Invalidate();
SuspendLayout();
// reset "workrbench" rectangle to current bounds
boundsAfterResize.X = boundsBeforeResize.Left;
boundsAfterResize.Y = boundsBeforeResize.Top;
boundsAfterResize.Width = x - boundsAfterResize.Left;
boundsAfterResize.Height = y - boundsAfterResize.Top;
ScaleHelper.Scale(boundsBeforeResize, x, y, ref boundsAfterResize, GetAngleRoundProcessor());
// apply scaled bounds to this DrawableContainer
ApplyBounds(boundsAfterResize);
ResumeLayout();
Invalidate();
return true;
}
/// <summary>
/// A handler for the MouseUp
/// </summary>
/// <param name="x">current mouse x</param>
/// <param name="y">current mouse y</param>
public virtual void HandleMouseUp(int x, int y) {
}
private void SwitchParent(Surface newParent) {
if (parent != null) {
for (int i=0; i<grippers.Length; i++) {
parent.Controls.Remove(grippers[i]);
}
} else if(grippers == null) {
InitControls();
}
parent = newParent;
parent.Controls.AddRange(grippers);
foreach(IFilter filter in Filters) {
filter.Parent = this;
}
}
// drawablecontainers are regarded equal if they are of the same type and their bounds are equal. this should be sufficient.
public override bool Equals(object obj) {
bool ret = false;
if (obj != null && GetType().Equals(obj.GetType())) {
DrawableContainer other = obj as DrawableContainer;
if (left==other.left && top==other.top && width==other.width && height==other.height) {
ret = true;
}
}
return ret;
}
public override int GetHashCode() {
return left.GetHashCode() ^ top.GetHashCode() ^ width.GetHashCode() ^ height.GetHashCode() ^ GetFields().GetHashCode();
}
protected void OnPropertyChanged(string propertyName) {
if (propertyChanged != null) {
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
Invalidate();
}
}
/// <summary>
/// This method will be called before a field is changes.
/// Using this makes it possible to invalidate the object as is before changing.
/// </summary>
/// <param name="fieldToBeChanged">The field to be changed</param>
/// <param name="newValue">The new value</param>
public virtual void BeforeFieldChange(Field fieldToBeChanged, object newValue) {
parent.MakeUndoable(new ChangeFieldHolderMemento(this, fieldToBeChanged), true);
Invalidate();
}
/// <summary>
/// Handle the field changed event, this should invalidate the correct bounds (e.g. when shadow comes or goes more pixels!)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void HandleFieldChanged(object sender, FieldChangedEventArgs e) {
LOG.DebugFormat("Field {0} changed", e.Field.FieldType);
if (e.Field.FieldType == FieldType.SHADOW) {
accountForShadowChange = true;
}
Invalidate();
}
public virtual bool CanRotate {
get {
return true;
}
}
public virtual void Rotate(RotateFlipType rotateFlipType) {
// somehow the rotation is the wrong way?
int angle = 90;
if (RotateFlipType.Rotate90FlipNone == rotateFlipType) {
angle = 270;
}
Rectangle beforeBounds = new Rectangle(Left, Top, Width, Height);
LOG.DebugFormat("Bounds before: {0}", beforeBounds);
GraphicsPath translatePath = new GraphicsPath();
translatePath.AddRectangle(beforeBounds);
Matrix rotateMatrix = new Matrix();
rotateMatrix.RotateAt(angle, new PointF(parent.Width >> 1, parent.Height >> 1));
translatePath.Transform(rotateMatrix);
RectangleF newBounds = translatePath.GetBounds();
LOG.DebugFormat("New bounds by using graphics path: {0}", newBounds);
int ox = 0;
int oy = 0;
int centerX = parent.Width >> 1;
int centerY = parent.Height >> 1;
// Transform from screen to normal coordinates
int px = Left - centerX;
int py = centerY - Top;
double theta = Math.PI * angle / 180.0;
double x1 = Math.Cos(theta) * (px - ox) - Math.Sin(theta) * (py - oy) + ox;
double y1 = Math.Sin(theta) * (px - ox) + Math.Cos(theta) * (py - oy) + oy;
// Transform from screen to normal coordinates
px = (Left + Width) - centerX;
py = centerY - (Top + Height);
double x2 = Math.Cos(theta) * (px - ox) - Math.Sin(theta) * (py - oy) + ox;
double y2 = Math.Sin(theta) * (px - ox) + Math.Cos(theta) * (py - oy) + oy;
// Transform back to screen coordinates, as we rotate the bitmap we need to switch the center X&Y
x1 += centerY;
y1 = centerX - y1;
x2 += centerY;
y2 = centerX - y2;
// Calculate to rectangle
double newWidth = x2 - x1;
double newHeight = y2 - y1;
RectangleF newRectangle = new RectangleF(
(float)x1,
(float)y1,
(float)newWidth,
(float)newHeight
);
ApplyBounds(newRectangle);
LOG.DebugFormat("New bounds by using old method: {0}", newRectangle);
}
protected virtual ScaleHelper.IDoubleProcessor GetAngleRoundProcessor() {
return ScaleHelper.ShapeAngleRoundBehavior.Instance;
}
public virtual bool hasContextMenu {
get {
return true;
}
}
public virtual bool hasDefaultSize {
get {
return false;
}
}
public virtual Size DefaultSize {
get {
throw new NotSupportedException("Object doesn't have a default size");
}
}
}
}