using PaintDotNet.Measurement.Enum; using PaintDotNet.Measurement.HistoryMementos; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Windows.Forms; namespace PaintDotNet.Measurement.Tools { public class TextTool : Tool { private enum EditingMode { NotEditing, EmptyEdit, Editing } private string statusBarTextFormat = PdnResources.GetString("TextTool.StatusText.TextInfo.Format"); private Point startMouseXY; private Point startClickPoint; private bool tracking; private MoveNubRenderer moveNub; private int ignoreRedraw; private RenderArgs ra; private EditingMode mode; private ArrayList lines; private int linePos; private int textPos; private Point clickPoint; private Font font; private TextAlignment alignment; private IrregularSurface saved; private const int cursorInterval = 300; private bool pulseEnabled; private System.DateTime startTime; private bool lastPulseCursorState; private Cursor textToolCursor; private PaintDotNet.Threading.ThreadPool threadPool; private bool enableNub = true; private CompoundHistoryMemento currentHA; private bool controlKeyDown = false; private DateTime controlKeyDownTime = DateTime.MinValue; private readonly TimeSpan controlKeyDownThreshold = new TimeSpan(0, 0, 0, 0, 400); private void AlphaBlendingChangedHandler(object sender, EventArgs e) { if (mode != EditingMode.NotEditing) { RedrawText(true); } } private EventHandler fontChangedDelegate; private void FontChangedHandler(object sender, EventArgs a) { font = AppEnvironment.FontInfo().CreateFont(); if (mode != EditingMode.NotEditing) { this.sizes = null; RedrawText(true); } } private EventHandler fontSmoothingChangedDelegate; private void FontSmoothingChangedHandler(object sender, EventArgs e) { if (mode != EditingMode.NotEditing) { this.sizes = null; RedrawText(true); } } private EventHandler alignmentChangedDelegate; private void AlignmentChangedHandler(object sender, EventArgs a) { alignment = AppEnvironment.TextAlignment(); if (mode != EditingMode.NotEditing) { this.sizes = null; RedrawText(true); } } private EventHandler brushChangedDelegate; private void BrushChangedHandler(object sender, EventArgs a) { if (mode != EditingMode.NotEditing) { RedrawText(true); } } private EventHandler antiAliasChangedDelegate; private void AntiAliasChangedHandler(object sender, EventArgs a) { if (mode != EditingMode.NotEditing) { this.sizes = null; RedrawText(true); } } private EventHandler foreColorChangedDelegate; private void ForeColorChangedHandler(object sender, EventArgs e) { if (mode != EditingMode.NotEditing) { RedrawText(true); } } private void BackColorChangedHandler(object sender, EventArgs e) { if (mode != EditingMode.NotEditing) { RedrawText(true); } } private bool OnBackspaceTyped(Keys keys) { if (!this.DocumentWorkspace.Visible()) { return false; } else if (this.mode != EditingMode.NotEditing) { OnKeyPress(Keys.Back); return true; } else { return false; } } protected override void OnActivate() { PdnBaseForm.RegisterFormHotKey(Keys.Back, OnBackspaceTyped); base.OnActivate(); this.textToolCursor = new Cursor(PdnResources.GetResourceStream("Cursors.TextToolCursor.cur")); this.Cursor = this.textToolCursor; fontChangedDelegate = new EventHandler(FontChangedHandler); fontSmoothingChangedDelegate = new EventHandler(FontSmoothingChangedHandler); alignmentChangedDelegate = new EventHandler(AlignmentChangedHandler); brushChangedDelegate = new EventHandler(BrushChangedHandler); antiAliasChangedDelegate = new EventHandler(AntiAliasChangedHandler); foreColorChangedDelegate = new EventHandler(ForeColorChangedHandler); ra = new RenderArgs(((BitmapLayer)ActiveLayer).Surface); mode = EditingMode.NotEditing; font = AppEnvironment.FontInfo().CreateFont(); alignment = AppEnvironment.TextAlignment(); AppEnvironment.BrushInfoChanged += brushChangedDelegate; AppEnvironment.FontInfoChanged += fontChangedDelegate; AppEnvironment.FontSmoothingChanged += fontSmoothingChangedDelegate; AppEnvironment.TextAlignmentChanged += alignmentChangedDelegate; AppEnvironment.AntiAliasingChanged += antiAliasChangedDelegate; AppEnvironment.PrimaryColorChanged += foreColorChangedDelegate; AppEnvironment.SecondaryColorChanged += new EventHandler(BackColorChangedHandler); AppEnvironment.AlphaBlendingChanged += new EventHandler(AlphaBlendingChangedHandler); this.threadPool = new PaintDotNet.Threading.ThreadPool(); this.moveNub = new MoveNubRenderer(this.RendererList); this.moveNub.Shape = MoveNubShape.Compass; this.moveNub.Size = new SizeF(10, 10); this.moveNub.Visible = false; this.RendererList.Add(this.moveNub, false); } protected override void OnDeactivate() { PdnBaseForm.UnregisterFormHotKey(Keys.Back, OnBackspaceTyped); base.OnDeactivate(); switch (mode) { case EditingMode.Editing: SaveHistoryMemento(); break; case EditingMode.EmptyEdit: RedrawText(false); break; case EditingMode.NotEditing: break; default: throw new InvalidEnumArgumentException("Invalid Editing Mode"); } if (ra != null) { ra.Dispose(); ra = null; } if (saved != null) { saved.Dispose(); saved = null; } AppEnvironment.BrushInfoChanged -= brushChangedDelegate; AppEnvironment.FontInfoChanged -= fontChangedDelegate; AppEnvironment.FontSmoothingChanged -= fontSmoothingChangedDelegate; AppEnvironment.TextAlignmentChanged -= alignmentChangedDelegate; AppEnvironment.AntiAliasingChanged -= antiAliasChangedDelegate; AppEnvironment.PrimaryColorChanged -= foreColorChangedDelegate; AppEnvironment.SecondaryColorChanged -= new EventHandler(BackColorChangedHandler); AppEnvironment.AlphaBlendingChanged -= new EventHandler(AlphaBlendingChangedHandler); StopEditing(); this.threadPool = null; this.RendererList.Remove(this.moveNub); this.moveNub.Dispose(); this.moveNub = null; if (this.textToolCursor != null) { this.textToolCursor.Dispose(); this.textToolCursor = null; } } private void StopEditing() { mode = EditingMode.NotEditing; pulseEnabled = false; lines = null; this.moveNub.Visible = false; } private void StartEditing() { this.linePos = 0; this.textPos = 0; this.lines = new ArrayList(); this.sizes = null; this.lines.Add(string.Empty); this.startTime = DateTime.Now; this.mode = EditingMode.EmptyEdit; this.pulseEnabled = true; UpdateStatusText(); } private void UpdateStatusText() { string text; ImageResource image; if (this.tracking) { text = GetStatusBarXYText(); image = Image; } else { text = PdnResources.GetString("TextTool.StatusText.StartTyping"); image = null; } SetStatus(image, text); } private void PerformEnter() { if (this.lines == null) { return; } string currentLine = (string)this.lines[this.linePos]; if (this.textPos == currentLine.Length) { // If we are at the end of a line, insert an empty line at the next line this.lines.Insert(this.linePos + 1, string.Empty); } else { this.lines.Insert(this.linePos + 1, currentLine.Substring(textPos, currentLine.Length - this.textPos)); this.lines[this.linePos] = ((string)this.lines[this.linePos]).Substring(0, this.textPos); } this.linePos++; this.textPos = 0; this.sizes = null; } private void PerformBackspace() { if (textPos == 0 && linePos > 0) { int ntp = ((string)lines[linePos - 1]).Length; lines[linePos - 1] = ((string)lines[linePos - 1]) + ((string)lines[linePos]); lines.RemoveAt(linePos); linePos--; textPos = ntp; sizes = null; } else if (textPos > 0) { string ln = (string)lines[linePos]; // If we are at the end of a line, we don't need to place a compound string if (textPos == ln.Length) { lines[linePos] = ln.Substring(0, ln.Length - 1); } else { lines[linePos] = ln.Substring(0, textPos - 1) + ln.Substring(textPos); } textPos--; sizes = null; } } private void PerformControlBackspace() { if (textPos == 0 && linePos > 0) { PerformBackspace(); } else if (textPos > 0) { string currentLine = (string)lines[linePos]; int ntp = textPos; if (Char.IsLetterOrDigit(currentLine[ntp - 1])) { while (ntp > 0 && (Char.IsLetterOrDigit(currentLine[ntp - 1]))) { ntp--; } } else if (Char.IsWhiteSpace(currentLine[ntp - 1])) { while (ntp > 0 && (Char.IsWhiteSpace(currentLine[ntp - 1]))) { ntp--; } } else if (Char.IsPunctuation(currentLine[ntp - 1])) { while (ntp > 0 && (Char.IsPunctuation(currentLine[ntp - 1]))) { ntp--; } } else { ntp--; } lines[linePos] = currentLine.Substring(0, ntp) + currentLine.Substring(textPos); textPos = ntp; sizes = null; } } private void PerformDelete() { // Where are we?! if ((linePos == lines.Count - 1) && (textPos == ((string)lines[lines.Count - 1]).Length)) { // If the cursor is at the end of the text block return; } else if (textPos == ((string)lines[linePos]).Length) { // End of a line, must merge strings lines[linePos] = ((string)lines[linePos]) + ((string)lines[linePos + 1]); lines.RemoveAt(linePos + 1); } else { // Middle of a line somewhere lines[linePos] = ((string)lines[linePos]).Substring(0, textPos) + ((string)lines[linePos]).Substring(textPos + 1); } // Check for state change if (lines.Count == 1 && ((string)lines[0]) == "") { mode = EditingMode.EmptyEdit; } sizes = null; } private void PerformControlDelete() { // where are we?! if ((linePos == lines.Count - 1) && (textPos == ((string)lines[lines.Count - 1]).Length)) { // If the cursor is at the end of the text block return; } else if (textPos == ((string)lines[linePos]).Length) { // End of a line, must merge strings lines[linePos] = ((string)lines[linePos]) + ((string)lines[linePos + 1]); lines.RemoveAt(linePos + 1); } else { // Middle of a line somewhere int ntp = textPos; string currentLine = (string)lines[linePos]; if (Char.IsLetterOrDigit(currentLine[ntp])) { while (ntp < currentLine.Length && (Char.IsLetterOrDigit(currentLine[ntp]))) { currentLine = currentLine.Remove(ntp, 1); } } else if (Char.IsWhiteSpace(currentLine[ntp])) { while (ntp < currentLine.Length && (Char.IsWhiteSpace(currentLine[ntp]))) { currentLine = currentLine.Remove(ntp, 1); } } else if (Char.IsPunctuation(currentLine[ntp])) { while (ntp < currentLine.Length && (Char.IsPunctuation(currentLine[ntp]))) { currentLine = currentLine.Remove(ntp, 1); } } else { ntp--; } lines[linePos] = currentLine; } // Check for state change if (lines.Count == 1 && ((string)lines[0]) == "") { mode = EditingMode.EmptyEdit; } sizes = null; } private void PerformLeft() { if (textPos > 0) { textPos--; } else if (textPos == 0 && linePos > 0) { linePos--; textPos = ((string)lines[linePos]).Length; } } private void PerformControlLeft() { if (textPos > 0) { int ntp = textPos; string currentLine = (string)lines[linePos]; if (Char.IsLetterOrDigit(currentLine[ntp - 1])) { while (ntp > 0 && (Char.IsLetterOrDigit(currentLine[ntp - 1]))) { ntp--; } } else if (Char.IsWhiteSpace(currentLine[ntp - 1])) { while (ntp > 0 && (Char.IsWhiteSpace(currentLine[ntp - 1]))) { ntp--; } } else if (ntp > 0 && Char.IsPunctuation(currentLine[ntp - 1])) { while (ntp > 0 && Char.IsPunctuation(currentLine[ntp - 1])) { ntp--; } } else { ntp--; } textPos = ntp; } else if (textPos == 0 && linePos > 0) { linePos--; textPos = ((string)lines[linePos]).Length; } } private void PerformRight() { if (textPos < ((string)lines[linePos]).Length) { textPos++; } else if (textPos == ((string)lines[linePos]).Length && linePos < lines.Count - 1) { linePos++; textPos = 0; } } private void PerformControlRight() { if (textPos < ((string)lines[linePos]).Length) { int ntp = textPos; string currentLine = (string)lines[linePos]; if (Char.IsLetterOrDigit(currentLine[ntp])) { while (ntp < currentLine.Length && (Char.IsLetterOrDigit(currentLine[ntp]))) { ntp++; } } else if (Char.IsWhiteSpace(currentLine[ntp])) { while (ntp < currentLine.Length && (Char.IsWhiteSpace(currentLine[ntp]))) { ntp++; } } else if (ntp > 0 && Char.IsPunctuation(currentLine[ntp])) { while (ntp < currentLine.Length && Char.IsPunctuation(currentLine[ntp])) { ntp++; } } else { ntp++; } textPos = ntp; } else if (textPos == ((string)lines[linePos]).Length && linePos < lines.Count - 1) { linePos++; textPos = 0; } } private void PerformUp() { PointF p = TextPositionToPoint(new Position(linePos, textPos)); p.Y -= this.sizes[0].Height; //font.Height; Position np = PointToTextPosition(p); linePos = np.Line; textPos = np.Offset; } private void PerformDown() { if (linePos == lines.Count - 1) { // last line -> don't do squat } else { PointF p = TextPositionToPoint(new Position(linePos, textPos)); p.Y += this.sizes[0].Height; //font.Height; Position np = PointToTextPosition(p); linePos = np.Line; textPos = np.Offset; } } private Point GetUpperLeft(Size sz, int line) { Point p = clickPoint; p.Y = (int)(p.Y - (0.5 * sz.Height) + (line * sz.Height)); switch (alignment) { case TextAlignment.Center: p.X = (int)(p.X - (0.5) * sz.Width); break; case TextAlignment.Right: p.X = (int)(p.X - sz.Width); break; } return p; } private Size StringSize(string s) { // We measure using a 1x1 device context to avoid performance problems that arise otherwise with large images. using (Surface window = ScratchSurface.CreateWindow(new Rectangle(0, 0, 1, 1))) { using (RenderArgs ra2 = new RenderArgs(window)) { return SystemLayer.Fonts.MeasureString( ra2.Graphics, this.font, s, AppEnvironment.AntiAliasing(), AppEnvironment.FontSmoothing()); } } } private sealed class Position { private int line; public int Line { get { return line; } set { if (value >= 0) { line = value; } else { line = 0; } } } private int offset; public int Offset { get { return offset; } set { if (value >= 0) { offset = value; } else { offset = 0; } } } public Position(int line, int offset) { this.line = line; this.offset = offset; } } private void SaveHistoryMemento() { pulseEnabled = false; RedrawText(false); if (saved != null) { PdnRegion hitTest = Selection.CreateRegion(); hitTest.Intersect(saved.Region); if (!hitTest.IsEmpty()) { BitmapHistoryMemento bha = new BitmapHistoryMemento(Name, Image, DocumentWorkspace, ActiveLayerIndex, saved); if (this.currentHA == null) { //HistoryStack.PushNewMemento(bha); } else { this.currentHA.PushNewAction(bha); this.currentHA = null; } } hitTest.Dispose(); saved.Dispose(); saved = null; } } private void DrawText(Surface dst, Font textFont, string text, Point pt, Size measuredSize, bool antiAliasing, Brush brush) { Rectangle dstRect = new Rectangle(pt, measuredSize); Rectangle dstRectClipped = Rectangle.Intersect(dstRect, ScratchSurface.Bounds); if (dstRectClipped.Width == 0 || dstRectClipped.Height == 0) { return; } using (Surface surface = new Surface(8, 8)) { using (RenderArgs renderArgs = new RenderArgs(surface)) { renderArgs.Graphics.FillRectangle(brush, 0, 0, surface.Width, surface.Height); } DrawText(dst, textFont, text, pt, measuredSize, antiAliasing, surface); } } private unsafe void DrawText(Surface dst, Font textFont, string text, Point pt, Size measuredSize, bool antiAliasing, Surface brush8x8) { Point pt2 = pt; Size measuredSize2 = measuredSize; int offset = (int)textFont.Height; pt.X -= offset; measuredSize.Width += 2 * offset; Rectangle dstRect = new Rectangle(pt, measuredSize); Rectangle dstRectClipped = Rectangle.Intersect(dstRect, ScratchSurface.Bounds); if (dstRectClipped.Width == 0 || dstRectClipped.Height == 0) { return; } // We only use the first 8,8 of brush using (RenderArgs renderArgs = new RenderArgs(this.ScratchSurface)) { renderArgs.Graphics.FillRectangle(Brushes.White, pt.X, pt.Y, measuredSize.Width, measuredSize.Height); if (measuredSize.Width > 0 && measuredSize.Height > 0) { using (Surface s2 = renderArgs.Surface.CreateWindow(dstRectClipped)) { using (RenderArgs renderArgs2 = new RenderArgs(s2)) { SystemLayer.Fonts.DrawText( renderArgs2.Graphics, this.font, text, new Point(dstRect.X - dstRectClipped.X + offset, dstRect.Y - dstRectClipped.Y), AppEnvironment.AntiAliasing(), AppEnvironment.FontSmoothing()); } } } // Mask out anything that isn't within the user's clip region (selected region) using (PdnRegion clip = Selection.CreateRegion()) { clip.Xor(renderArgs.Surface.Bounds); // invert clip.Intersect(new Rectangle(pt, measuredSize)); renderArgs.Graphics.FillRegion(Brushes.White, clip.GetRegionReadOnly()); } int skipX; if (pt.X < 0) { skipX = -pt.X; } else { skipX = 0; } int xEnd = Math.Min(dst.Width, pt.X + measuredSize.Width); bool blending = AppEnvironment.AlphaBlending(); if (dst.IsColumnVisible(pt.X + skipX)) { for (int y = pt.Y; y < pt.Y + measuredSize.Height; ++y) { if (!dst.IsRowVisible(y)) { continue; } ColorBgra* dstPtr = dst.GetPointAddressUnchecked(pt.X + skipX, y); ColorBgra* srcPtr = ScratchSurface.GetPointAddress(pt.X + skipX, y); ColorBgra* brushPtr = brush8x8.GetRowAddressUnchecked(y & 7); for (int x = pt.X + skipX; x < xEnd; ++x) { ColorBgra srcPixel = *srcPtr; ColorBgra dstPixel = *dstPtr; ColorBgra brushPixel = brushPtr[x & 7]; int alpha = ((255 - srcPixel.R) * brushPixel.A) / 255; // we could use srcPixel.R, .G, or .B -- the choice here is arbitrary brushPixel.A = (byte)alpha; if (srcPtr->R == 255) // could use R, G, or B -- arbitrary choice { // do nothing -- leave dst alone } else if (alpha == 255 || !blending) { // copy it straight over *dstPtr = brushPixel; } else { // do expensive blending *dstPtr = UserBlendOps.NormalBlendOp.ApplyStatic(dstPixel, brushPixel); } ++dstPtr; ++srcPtr; } } } } } /// /// Redraws the Text on the screen /// /// /// assumes that the font and the alignment are already set /// /// private void RedrawText(bool cursorOn) { if (this.ignoreRedraw > 0) { return; } if (saved != null) { saved.Draw(ra.Surface); ActiveLayer.Invalidate(saved.Region); saved.Dispose(); saved = null; } // Save the Space behind the lines Rectangle[] rects = new Rectangle[lines.Count + 1]; Point[] localUls = new Point[lines.Count]; // All Lines bool recalcSizes = false; if (this.sizes == null) { recalcSizes = true; this.sizes = new Size[lines.Count + 1]; } if (recalcSizes) { for (int i = 0; i < lines.Count; ++i) { this.threadPool.QueueUserWorkItem(new System.Threading.WaitCallback(this.MeasureText), BoxedConstants.GetInt32(i)); } this.threadPool.Drain(); } for (int i = 0; i < lines.Count; ++i) { Point upperLeft = GetUpperLeft(sizes[i], i); localUls[i] = upperLeft; Rectangle rect = new Rectangle(upperLeft, sizes[i]); rects[i] = rect; } // The Cursor Line string cursorLine = ((string)lines[linePos]).Substring(0, textPos); Size cursorLineSize; Point cursorUL; Rectangle cursorRect; bool emptyCursorLineFlag; if (cursorLine.Length == 0) { emptyCursorLineFlag = true; Size fullLineSize = sizes[linePos]; cursorLineSize = new Size(2, (int)(Math.Ceiling(font.GetHeight()))); cursorUL = GetUpperLeft(fullLineSize, linePos); cursorRect = new Rectangle(cursorUL, cursorLineSize); } else if (cursorLine.Length == ((string)lines[linePos]).Length) { emptyCursorLineFlag = false; cursorLineSize = sizes[linePos]; cursorUL = localUls[linePos]; cursorRect = new Rectangle(cursorUL, cursorLineSize); } else { emptyCursorLineFlag = false; cursorLineSize = StringSize(cursorLine); cursorUL = localUls[linePos]; cursorRect = new Rectangle(cursorUL, cursorLineSize); } rects[lines.Count] = cursorRect; // Account for overhang on italic or fancy fonts int offset = (int)this.font.Height; for (int i = 0; i < rects.Length; ++i) { rects[i].X -= offset; rects[i].Width += 2 * offset; } // Set the saved region using (PdnRegion reg = Utility.RectanglesToRegion(Utility.InflateRectangles(rects, 3))) { saved = new IrregularSurface(ra.Surface, reg); } // Draw the Lines this.uls = localUls; for (int i = 0; i < lines.Count; i++) { threadPool.QueueUserWorkItem(new System.Threading.WaitCallback(this.RenderText), BoxedConstants.GetInt32(i)); } threadPool.Drain(); // Draw the Cursor if (cursorOn) { using (Pen cursorPen = new Pen(Color.FromArgb(255, AppEnvironment.PrimaryColor().ToColor()), 2)) { if (emptyCursorLineFlag) { ra.Graphics.FillRectangle(cursorPen.Brush, cursorRect); } else { ra.Graphics.DrawLine(cursorPen, new Point(cursorRect.Right, cursorRect.Top), new Point(cursorRect.Right, cursorRect.Bottom)); } } } PlaceMoveNub(); UpdateStatusText(); ActiveLayer.Invalidate(saved.Region); Update(); } private string GetStatusBarXYText() { string unitsAbbreviationXY; string xString; string yString; Document.CoordinatesToStrings(AppWorkspace.GetUnits(), this.uls[0].X, this.uls[0].Y, out xString, out yString, out unitsAbbreviationXY); string statusBarText = string.Format( this.statusBarTextFormat, xString, unitsAbbreviationXY, yString, unitsAbbreviationXY); return statusBarText; } // Only used when measuring via background threads private void MeasureText(object lineNumberObj) { int lineNumber = (int)lineNumberObj; this.sizes[lineNumber] = StringSize((string)lines[lineNumber]); } // Only used when rendering via background threads private Point[] uls; private Size[] sizes; private void RenderText(object lineNumberObj) { int lineNumber = (int)lineNumberObj; using (Brush brush = AppEnvironment.CreateBrush(false)) { DrawText(ra.Surface, this.font, (string)this.lines[lineNumber], this.uls[lineNumber], this.sizes[lineNumber], AppEnvironment.AntiAliasing(), brush); } } private void PlaceMoveNub() { if (this.uls != null && this.uls.Length > 0) { Point pt = this.uls[uls.Length - 1]; pt.X += this.sizes[uls.Length - 1].Width; pt.Y += this.sizes[uls.Length - 1].Height; pt.X += (int)(10.0 / DocumentWorkspace.GetRatio()); pt.Y += (int)(10.0 / DocumentWorkspace.GetRatio()); pt.X = (int)Math.Round(Math.Min(this.ra.Surface.Width - this.moveNub.Size.Width, pt.X)); pt.X = (int)Math.Round(Math.Max(this.moveNub.Size.Width, pt.X)); pt.Y = (int)Math.Round(Math.Min(this.ra.Surface.Height - this.moveNub.Size.Height, pt.Y)); pt.Y = (int)Math.Round(Math.Max(this.moveNub.Size.Height, pt.Y)); this.moveNub.Location = pt; } } protected override void OnKeyDown(KeyEventArgs e) { switch (e.KeyCode) { case Keys.Space: if (mode != EditingMode.NotEditing) { // Prevent pan cursor from flicking to 'hand w/ the X' whenever use types a space in their text e.Handled = true; } break; case Keys.ControlKey: if (!this.controlKeyDown) { this.controlKeyDown = true; this.controlKeyDownTime = DateTime.Now; } break; // Make sure these are not used to scroll the document around case Keys.Home | Keys.Shift: case Keys.Home: case Keys.End: case Keys.End | Keys.Shift: case Keys.Next | Keys.Shift: case Keys.Next: case Keys.Prior | Keys.Shift: case Keys.Prior: if (this.mode != EditingMode.NotEditing) { OnKeyPress(e.KeyCode); e.Handled = true; } break; case Keys.Tab: if ((e.Modifiers & Keys.Control) == 0) { if (this.mode != EditingMode.NotEditing) { OnKeyPress(e.KeyCode); e.Handled = true; } } break; case Keys.Back: case Keys.Delete: if (this.mode != EditingMode.NotEditing) { OnKeyPress(e.KeyCode); e.Handled = true; } break; } // Ensure text is on screen when they are typing if (this.mode != EditingMode.NotEditing) { Point p = Point.Truncate(TextPositionToPoint(new Position(linePos, textPos))); Rectangle bounds = Utility.RoundRectangle(DocumentWorkspace.GetVisibleDocumentRectangleF()); bounds.Inflate(-(int)font.Height, -(int)font.Height); if (!bounds.Contains(p)) { PointF newCenterPt = Utility.GetRectangleCenter((RectangleF)bounds); // horizontally off if (p.X > bounds.Right || p.Y < bounds.Left) { newCenterPt.X = p.X; } // vertically off if (p.Y > bounds.Bottom || p.Y < bounds.Top) { newCenterPt.Y = p.Y; } DocumentWorkspace.SetDocumentCenterPointF(newCenterPt); } } base.OnKeyDown(e); } protected override void OnKeyUp(KeyEventArgs e) { switch (e.KeyCode) { case Keys.ControlKey: TimeSpan heldDuration = (DateTime.Now - this.controlKeyDownTime); // If the user taps Ctrl, then we should toggle the visiblity of the moveNub if (heldDuration < this.controlKeyDownThreshold) { this.enableNub = !this.enableNub; } this.controlKeyDown = false; break; } base.OnKeyUp(e); } protected override void OnKeyPress(KeyPressEventArgs e) { switch (e.KeyChar) { case (char)13: // Enter if (tracking) { e.Handled = true; } break; case (char)27: // Escape if (tracking) { e.Handled = true; } else { if (mode == EditingMode.Editing) { SaveHistoryMemento(); } else if (mode == EditingMode.EmptyEdit) { RedrawText(false); } if (mode != EditingMode.NotEditing) { e.Handled = true; StopEditing(); } } break; } if (!e.Handled && mode != EditingMode.NotEditing && !tracking) { e.Handled = true; if (mode == EditingMode.EmptyEdit) { mode = EditingMode.Editing; CompoundHistoryMemento cha = new CompoundHistoryMemento(Name, Image, new List()); this.currentHA = cha; //HistoryStack.PushNewMemento(cha); } if (!char.IsControl(e.KeyChar)) { InsertCharIntoString(e.KeyChar); textPos++; RedrawText(true); } } base.OnKeyPress(e); } protected override void OnKeyPress(Keys keyData) { bool keyHandled = true; Keys key = keyData & Keys.KeyCode; Keys modifier = keyData & Keys.Modifiers; if (tracking) { keyHandled = false; } else if (modifier == Keys.Alt) { // ignore so they can use Alt+#### to type special characters } else if (mode != EditingMode.NotEditing) { switch (key) { case Keys.Back: if (modifier == Keys.Control) { PerformControlBackspace(); } else { PerformBackspace(); } break; case Keys.Delete: if (modifier == Keys.Control) { PerformControlDelete(); } else { PerformDelete(); } break; case Keys.Enter: PerformEnter(); break; case Keys.Left: if (modifier == Keys.Control) { PerformControlLeft(); } else { PerformLeft(); } break; case Keys.Right: if (modifier == Keys.Control) { PerformControlRight(); } else { PerformRight(); } break; case Keys.Up: PerformUp(); break; case Keys.Down: PerformDown(); break; case Keys.Home: if (modifier == Keys.Control) { linePos = 0; } textPos = 0; break; case Keys.End: if (modifier == Keys.Control) { linePos = lines.Count - 1; } textPos = ((string)lines[linePos]).Length; break; default: keyHandled = false; break; } this.startTime = DateTime.Now; if (this.mode != EditingMode.NotEditing && keyHandled) { RedrawText(true); } } if (!keyHandled) { base.OnKeyPress(keyData); } } private PointF TextPositionToPoint(Position p) { PointF pf = new PointF(0, 0); Size sz = StringSize(((string)lines[p.Line]).Substring(0, p.Offset)); Size fullSz = StringSize((string)lines[p.Line]); switch (alignment) { case TextAlignment.Left: pf = new PointF(clickPoint.X + sz.Width, clickPoint.Y + (sz.Height * p.Line)); break; case TextAlignment.Center: pf = new PointF(clickPoint.X + (sz.Width - (fullSz.Width / 2)), clickPoint.Y + (sz.Height * p.Line)); break; case TextAlignment.Right: pf = new PointF(clickPoint.X + (sz.Width - fullSz.Width), clickPoint.Y + (sz.Height * p.Line)); break; default: throw new InvalidEnumArgumentException("Invalid Alignment"); } return pf; } private int FindOffsetPosition(float offset, string line, int lno) { for (int i = 0; i < line.Length; i++) { PointF pf = TextPositionToPoint(new Position(lno, i)); float dx = pf.X - clickPoint.X; if (dx >= offset) { return i; } } return line.Length; } private Position PointToTextPosition(PointF pf) { float dx = pf.X - clickPoint.X; float dy = pf.Y - clickPoint.Y; int line = (int)Math.Floor(dy / (float)this.sizes[0].Height); if (line < 0) { line = 0; } else if (line >= lines.Count) { line = lines.Count - 1; } int offset = FindOffsetPosition(dx, (string)lines[line], line); Position p = new Position(line, offset); if (p.Offset >= ((string)lines[p.Line]).Length) { p.Offset = ((string)lines[p.Line]).Length; } return p; } protected override void OnMouseMove(MouseEventArgs e) { if (tracking) { Point newMouseXY = new Point(e.X, e.Y); Size delta = new Size(newMouseXY.X - startMouseXY.X, newMouseXY.Y - startMouseXY.Y); this.clickPoint = new Point(this.startClickPoint.X + delta.Width, this.startClickPoint.Y + delta.Height); RedrawText(false); UpdateStatusText(); } else { bool touchingNub = this.moveNub.IsPointTouching(new Point(e.X, e.Y), false); if (touchingNub && this.moveNub.Visible) { this.Cursor = this.handCursor; } else { this.Cursor = this.textToolCursor; } } base.OnMouseMove(e); } protected override void OnMouseUp(MouseEventArgs e) { if (tracking) { OnMouseMove(e); tracking = false; UpdateStatusText(); } base.OnMouseUp(e); } protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); bool touchingMoveNub = this.moveNub.IsPointTouching(new Point(e.X, e.Y), false); if (this.mode != EditingMode.NotEditing && (e.Button == MouseButtons.Right || touchingMoveNub)) { this.tracking = true; this.startMouseXY = new Point(e.X, e.Y); this.startClickPoint = this.clickPoint; this.Cursor = this.handCursorMouseDown; UpdateStatusText(); } else if (e.Button == MouseButtons.Left) { if (saved != null) { Rectangle bounds = Utility.GetRegionBounds(saved.Region); bounds.Inflate(font.Height, font.Height); if (lines != null && bounds.Contains(e.X, e.Y)) { Position p = PointToTextPosition(new PointF(e.X, e.Y + (font.Height / 2))); linePos = p.Line; textPos = p.Offset; RedrawText(true); return; } } switch (mode) { case EditingMode.Editing: SaveHistoryMemento(); StopEditing(); break; case EditingMode.EmptyEdit: RedrawText(false); StopEditing(); break; } clickPoint = new Point(e.X, e.Y); StartEditing(); RedrawText(true); } } protected override void OnPulse() { base.OnPulse(); if (!pulseEnabled) { return; } TimeSpan ts = (DateTime.Now - startTime); long ms = Utility.TicksToMs(ts.Ticks); bool pulseCursorState; if (0 == ((ms / cursorInterval) % 2)) { pulseCursorState = true; } else { pulseCursorState = false; } pulseCursorState &= this.Focused; if (IsFormActive) { pulseCursorState &= ((ModifierKeys & Keys.Control) == 0); } if (pulseCursorState != lastPulseCursorState) { RedrawText(pulseCursorState); lastPulseCursorState = pulseCursorState; } if (IsFormActive && (ModifierKeys & Keys.Control) != 0) { // hide the nub while Ctrl is held down this.moveNub.Visible = false; } else { this.moveNub.Visible = true; } // don't show the nub while the user is moving the text around this.moveNub.Visible &= !tracking; // don't show the nub when the user has tapped Ctrl this.moveNub.Visible &= this.enableNub; // Oscillate between 25% and 100% alpha over a period of 2 seconds // Alpha value of 100% is sustained for a large duration of this period const int period = 10000 * 2000; // 10000 ticks per ms, 2000ms per second long tick = ts.Ticks % period; double sin = Math.Sin(((double)tick / (double)period) * (2.0 * Math.PI)); // sin is [-1, +1] sin = Math.Min(0.5, sin); // sin is [-1, +0.5] sin += 1.0; // sin is [0, 1.5] sin /= 2.0; // sin is [0, 0.75] sin += 0.25; // sin is [0.25, 1] if (this.moveNub != null) { int newAlpha = (int)(sin * 255.0); int clampedAlpha = Utility.Clamp(newAlpha, 0, 255); this.moveNub.Alpha = clampedAlpha; } PlaceMoveNub(); } protected override void OnPasteQuery(IDataObject data, out bool canHandle) { base.OnPasteQuery(data, out canHandle); if (data.GetDataPresent(DataFormats.StringFormat, true) && this.Active && this.mode != EditingMode.NotEditing) { canHandle = true; } } protected override void OnPaste(IDataObject data, out bool handled) { base.OnPaste(data, out handled); if (data.GetDataPresent(DataFormats.StringFormat, true) && this.Active && this.mode != EditingMode.NotEditing) { ++this.ignoreRedraw; string text = (string)data.GetData(DataFormats.StringFormat, true); foreach (char c in text) { if (c == '\n') { this.PerformEnter(); } else { this.PerformKeyPress(new KeyPressEventArgs(c)); } } handled = true; --this.ignoreRedraw; this.moveNub.Visible = true; this.RedrawText(false); } } private void InsertCharIntoString(char c) { lines[linePos] = ((string)lines[linePos]).Insert(textPos, c.ToString()); this.sizes = null; } public TextTool(IDocumentWorkspace documentWorkspace) : base(documentWorkspace, PdnResources.GetImageResource("Icons.TextToolIcon.png"), PdnResources.GetString("TextTool.Name"), PdnResources.GetString("TextTool.HelpText"), 't', false, ToolBarConfigItems.Brush | ToolBarConfigItems.Text | ToolBarConfigItems.AlphaBlending | ToolBarConfigItems.Antialiasing) { } } }