www.pudn.com > sudoku.rar > PuzzleGrid.cs
//--------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// File: PuzzleGrid.cs
//
// Description: Core control for displaying and interacting with a puzzle.
//
//--------------------------------------------------------------------------
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Diagnostics;
using System.Collections;
using System.Globalization;
using System.Windows.Forms;
using System.ComponentModel;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using Microsoft.Ink;
using Microsoft.Sudoku.Nullables;
using Microsoft.Sudoku.Collections;
using Microsoft.Sudoku.Techniques;
using Microsoft.Sudoku.Utilities;
namespace Microsoft.Sudoku.Controls
{
/// Control for displaying and interacting with a PuzzleState.
[ToolboxBitmap(typeof(DataGrid))]
internal sealed class PuzzleGrid : Control
{
#region Component Designer generated code
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this._processStrokesTimer = new System.Windows.Forms.Timer(this.components);
//
// _processStrokesTimer
//
this._processStrokesTimer.Interval = 500;
this._processStrokesTimer.Tick += new System.EventHandler(this.StrokesTimerTick);
//
// PuzzleGrid
//
this.BackColor = System.Drawing.Color.Transparent;
this.ForeColor = System.Drawing.Color.Black;
}
#endregion
#region Member Variables
/// Required designer variable.
private System.ComponentModel.IContainer components = null;
/// Used to time stroke entry so that strokes can be recognized as a group as a number.
private System.Windows.Forms.Timer _processStrokesTimer;
/// The state currently being displayed in the grid.
private PuzzleState _state;
/// Original state of the puzzle.
private PuzzleState _originalState;
/// The original state of a newly generated puzzle, used for comparison.
private PuzzleState _solvedOriginalState;
/// Whether to highlight cells that can only be a specific number.
private bool _showSuggestedCells;
/// Whether to show where incorrect numbers have been added to the grid.
private bool _showIncorrectNumbers;
/// The puzzle difficulty level used for showing hints.
private PuzzleDifficulty _difficultyLevel = PuzzleDifficulty.Easy;
/// Maintains a history of undo information.
private PuzzleStateStack _undoStates = new PuzzleStateStack();
/// Currently selected cell in the grid.
private NullablePoint _selectedCell;
/// InkOverlay used to receive Tablet ink.
private InkOverlay _inkOverlay;
/// Tablet recognizer context for parsing user input into numbers.
private RecognizerContext _recognizerCtx;
/// ID used to retrieve the custom scratchpad strokes collection.
private const string ScratchpadStrokesID = "ScratchpadStrokesID"; // does not need to be localized
/// ID used to retrieve the custom normal strokes collection.
private const string NormalStrokesID = "NormalStrokesID"; // does not need to be localized
/// Color used for normal-mode ink.
private Color _inkColor;
/// Color used for scratchpad-mode ink.
private Color _scratchpadInkColor;
/// Color used for scratchpad-mode ink when in normal mode.
private Color _scratchpadInkInNormalModeColor;
#endregion
#region Setup and Shutdown
/// Initialize the PuzzleGrid.
public PuzzleGrid()
{
SetStyle(
ControlStyles.UserPaint | ControlStyles.DoubleBuffer |
ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw |
ControlStyles.SupportsTransparentBackColor, true);
InitializeComponent();
_userValueBrush = new SolidBrush(Color.Blue);
_incorrectValueBrush = new SolidBrush(Color.FromArgb(255,0,0));
_originalValueBrush = new SolidBrush(Color.FromArgb(0,0,0));
_inkColor = Color.Blue;
_scratchpadInkColor = Color.Black;
_scratchpadInkInNormalModeColor = Color.Gray;
_centerNumberFormat = new StringFormat();
_centerNumberFormat.Alignment = StringAlignment.Center;
_centerNumberFormat.LineAlignment = StringAlignment.Center;
}
/// Clean up any resources being used.
/// true if managed resources should be disposed; otherwise, false.
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (components != null) components.Dispose();
DisableTabletSupport();
DisposeDrawingObjects();
}
base.Dispose(disposing);
}
/// Disposes all objects involved with drawing the grid.
private void DisposeDrawingObjects()
{
if (_userValueBrush != null) _userValueBrush.Dispose();
if (_incorrectValueBrush != null) _incorrectValueBrush.Dispose();
if (_originalValueBrush != null) _originalValueBrush.Dispose();
if (_centerNumberFormat != null) _centerNumberFormat.Dispose();
_userValueBrush = null;
_originalValueBrush = null;
_centerNumberFormat = null;
}
/// Refocus when the control is enabled. Nothing else should ever have focus.
/// The event arguments.
protected override void OnEnabledChanged(EventArgs e)
{
base.OnEnabledChanged(e);
if (Enabled) Focus();
}
#endregion
#region Puzzle State
/// Gets or sets the current puzzle state.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public PuzzleState State
{
get
{
if (_state == null)
{
_state = new PuzzleState();
OnStateChanged();
}
return _state;
}
set
{
if (value == null) value = new PuzzleState();
if (value != _state)
{
PuzzleState oldState = _state;
if (oldState != null)
{
oldState.StateChanged -= new EventHandler(HandlePuzzleStateChanged);
oldState.RaiseStateChangedEvent = false;
}
_state = value;
_state.RaiseStateChangedEvent = true;
_state.StateChanged += new EventHandler(HandlePuzzleStateChanged);
OnStateChanged();
Invalidate();
}
}
}
/// Gets or sets the original state of the puzzle.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
internal PuzzleState OriginalState
{
get { return _originalState; }
set { SetOriginalPuzzleCheckpoint(value); }
}
/// Gets the collection of undo states.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
internal PuzzleStateStack UndoStates
{
get { return _undoStates; }
set
{
if (value == null) throw new ArgumentNullException("value");
_undoStates = value;
}
}
/// Event raised when the grid's puzzle state is modified.
public event EventHandler StateChanged;
/// Raises the StateChanged event on the main UI thread.
private void OnStateChanged()
{
_suggestedCell = null;
EventHandler handler = StateChanged;
if (handler != null)
{
if (InvokeRequired) Invoke(handler, new object[] { this, EventArgs.Empty });
else handler(this, EventArgs.Empty);
}
}
/// Raises the StateChanged event in response to the puzzle state's StateChanged event.
/// The modified puzzle state.
/// The event args.
private void HandlePuzzleStateChanged(object sender, EventArgs e)
{
OnStateChanged();
}
/// Gets whether the specified cell can be modified based on the original state checkpoint.
/// The cell to be verified.
/// true if the cell can be modified; otherwise, false.
private bool CanModifyCell(Point cell)
{
return (cell.X >= 0 && cell.X < State.GridSize && cell.Y >= 0 && cell.Y < State.GridSize) &&
(_originalState == null || !_originalState[cell].HasValue);
}
/// Gets whether the specified cell can be cleared.
/// The cell to test.
/// Whether the cell can be cleared (it can be modified and it has a value).
private bool CanClearCell(Point cell)
{
return CanModifyCell(cell) && State[cell].HasValue;
}
/// Gets the grid cell containing the specified point in the control.
/// The point, typically a mouse location, for which to find the grid cell.
/// The location of the grid cell corresponding to the specified point.
public Point GetCellFromLocation(Point point)
{
if (point.X < 0 || point.Y < 0) return new Point(-1,-1);
Rectangle rect = BoardRectangle;
if (rect.Width <= 0 || rect.Height <= 0) return new Point(-1, -1);
float cellWidth = rect.Width / (float)_boardWidth * (_cellImageWidth+1);
float gapWidth = rect.Width / (float)_boardWidth * (_gapImageWidth-1);
int x = 0, y = 0;
float pos = point.Y - rect.X;
while(pos >= cellWidth)
{
pos -= cellWidth;
++x;
if (x % _boxSize == 0) pos -= gapWidth;
}
pos = point.X - rect.Y;
while(pos >= cellWidth)
{
pos -= cellWidth;
++y;
if (y % _boxSize == 0) pos -= gapWidth;
}
return new Point(x,y);
}
#endregion
#region Hints
/// Gets or sets whether to highlight cells that can only be a specific number.
public bool ShowSuggestedCells
{
get { return _showSuggestedCells; }
set
{
if (value != _showSuggestedCells)
{
_showSuggestedCells = value;
Invalidate();
}
}
}
/// Gets or sets whether to display the possible numbers in each cell.
public bool ShowIncorrectNumbers
{
get { return _showIncorrectNumbers; }
set
{
if (value != _showIncorrectNumbers)
{
_showIncorrectNumbers = value;
Invalidate();
}
}
}
#endregion
#region Puzzle Interaction
/// Gets whether the puzzle has been modified.
public bool PuzzleHasBeenModified
{
get
{
PuzzleStatus status = State.Status;
return status != PuzzleStatus.Solved &&
(HasScratchpadStrokes || (OriginalState != null && !State.Equals(OriginalState)));
}
}
/// Generates a new puzzle and sets it to be the current puzzle in the grid.
/// Options to use for the puzzle generation.
/// Resets the undo states for the grid.
public void GenerateNewPuzzle(GeneratorOptions options)
{
LoadNewPuzzle(new Generator(options).Generate());
}
/// Gets or sets the puzzle difficulty level used for showing hints.
public PuzzleDifficulty PossibleNumbersDifficultyLevel
{
get { return _difficultyLevel; }
set
{
if (!Enum.IsDefined(typeof(PuzzleDifficulty), value)) throw new ArgumentOutOfRangeException("value");
if (_difficultyLevel != value)
{
_difficultyLevel = value;
_suggestedCell = null;
Invalidate();
}
}
}
/// Loads a new puzzle and sets it to be the current puzzle in the grid.
/// The puzzle to load.
public void LoadNewPuzzle(PuzzleState state)
{
ClearUndoCheckpoints();
ClearOriginalPuzzleCheckpoint();
SetOriginalPuzzleCheckpoint(state.Clone());
State = state;
ResetTabletSupportForNewPuzzle();
_selectedCell = FirstEmptyCell;
}
/// Restores a puzzle based on the state from a serialized game.
/// The current puzzle state.
/// The original puzzle state.
/// The undo state stack.
/// The grid's ink data.
public void RestorePuzzle(
PuzzleState state, PuzzleState originalState, PuzzleStateStack undoStates, byte [] inkData)
{
OriginalState = originalState;
UndoStates = undoStates;
InkData = inkData;
ResizeScratchpadInk();
State = state;
_selectedCell = FirstEmptyCell;
}
/// Gets the position of the first empty cell in the puzzle, or the [0,0] cell if none are empty.
private Point FirstEmptyCell
{
get
{
PuzzleState state = State;
if (state == null) return new Point(0,0);
for(int i=0; iDeletes all scratchpad ink on the grid.
private void ResetTabletSupportForNewPuzzle()
{
if (_inkOverlay != null)
{
using(Strokes scratchpadStrokes = ScratchpadStrokes)
{
_inkOverlay.Ink.DeleteStrokes(scratchpadStrokes);
scratchpadStrokes.Clear();
}
}
}
/// Creates a checkpoint used to determine where cells are invalid in the puzzle.
public void SetOriginalPuzzleCheckpoint(PuzzleState original)
{
_originalState = original;
if (original != null)
{
SolverOptions options = new SolverOptions();
options.MaximumSolutionsToFind = 2;
SolverResults results = Solver.Solve(original, options);
if (results.Status == PuzzleStatus.Solved && results.Puzzles.Count == 1)
{
_solvedOriginalState = results.Puzzle;
}
else _solvedOriginalState = null;
}
}
/// Clears the original puzzle checkpoint.
public void ClearOriginalPuzzleCheckpoint()
{
_originalState = null;
_solvedOriginalState = null;
}
/// Clears the undo puzzle checkpoints.
public void ClearUndoCheckpoints()
{
_undoStates.Clear();
}
/// Creates an undo checkpoint such that the current grid state can be reached through the undo mechanism.
public void SetUndoCheckpoint()
{
if (State != null)
{
_undoStates.Push(GetClonedStateForUndo());
}
}
/// Deep clones the current puzzle state, also storing the current InkData into its Tag.
/// The PuzzleState clone.
private PuzzleState GetClonedStateForUndo()
{
PuzzleState state = State.Clone();
state.Tag = InkData;
return state;
}
/// Gets whether the overlay is currently collecting ink.
public bool CollectingInk { get { return _inkOverlay != null && _inkOverlay.CollectingInk; } }
/// Reverts to a previously saved state.
public void Undo()
{
if (_undoStates.Count > 0 && !CollectingInk)
{
State = _undoStates.Pop();
if (State.Tag is byte[])
{
InkData = (byte[])State.Tag;
State.Tag = null;
}
OnStateChanged();
Invalidate();
}
}
/// Attempts to solve the current puzzle, update the state in the grid, and return the results.
/// The results from attempting to solve the puzzle.
private SolverResults SolvePuzzle()
{
// If it's already solved, nothing to do
if (State.Status == PuzzleStatus.Solved) return new SolverResults(PuzzleStatus.Solved, State, 0, null);
// Otherwise, try to solve it.
SolverOptions options = new SolverOptions();
options.MaximumSolutionsToFind = 1u; // this means that if there are multiple solutions, we'll find and use the first
options.AllowBruteForce = true;
options.EliminationTechniques = new TechniqueCollection(new NakedSingleTechnique());
SolverResults results = Solver.Solve(State, options);
if (results.Status == PuzzleStatus.Solved && results.Puzzles.Count == 1)
{
SetUndoCheckpoint();
State = results.Puzzle;
}
return results;
}
#endregion
#region Drawing
/// The width/height of a box. This would need to become variable if differently sized puzzles were supported.
private const int _boxSize = 3;
/// The width of the underlying board image.
private const int _boardWidth = 518;
/// The height of the underlying board image.
private const int _boardHeight = 518;
/// The width of a cell in the underlying board image.
private const int _cellImageWidth = 54;
/// The height of a cell in the underlying board image.
private const int _cellImageHeight = 54;
/// The width of a the gap between cells in the underlying board image.
private const int _cellGapWidth = 1;
/// The height of a the gap between cells in the underlying board image.
private const int _cellGapHeight = 1;
/// The width of a the gap between boxes in the underlying board image.
private const int _gapImageWidth = 13;
/// The height of a the gap between boxes in the underlying board image.
private const int _gapImageHeight = 13;
/// Gets the bounding rectangle for a specific cell.
/// The client rectange.
/// The target cell.
/// The bounding rectangle.
public static RectangleF GetCellRectangle(Rectangle rect, Point cell)
{
float width = (_cellImageWidth + _cellGapWidth) / (float)_boardWidth * rect.Width;
float height = (_cellImageHeight + _cellGapHeight) / (float)_boardHeight * rect.Height;
RectangleF cellRect = new RectangleF(
rect.X + (cell.Y*width) + ((cell.Y/_boxSize)*((_gapImageWidth-1)/(float)_boardWidth*rect.Width)),
rect.Y + (cell.X*height) + ((cell.X/_boxSize)*((_gapImageHeight-1)/(float)_boardHeight*rect.Height)),
_cellImageWidth / (float)_boardWidth * rect.Width,
_cellImageHeight / (float)_boardHeight * rect.Height);
return cellRect;
}
/// Raises the Paint event.
/// A PaintEventArgs that contains the event data.
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
DrawToGraphics(e.Graphics, e.ClipRectangle);
}
/// Brush used to draw text and other objects with the font color.
private Brush _userValueBrush;
/// Brush used to draw incorrect values on the grid.
private Brush _incorrectValueBrush;
/// Brush used to draw text and other objects with a variation of the font color.
private Brush _originalValueBrush;
/// StringFormat used to center text within a rectangle.
private StringFormat _centerNumberFormat;
/// The location of the cell to suggest as a "try this one next" hint.
private NullablePoint _suggestedCell = null;
/// Gets the location of the cell to suggest as a "try this one next" hint.
private NullablePoint SuggestedCell
{
get
{
if (_suggestedCell == null)
{
// First, see if any cells are the only one left to be filled in their
// row, column, or box. Regardless of techniques in use, only one
// cell without a value in a row, column, or box really must be the easiest.
NullablePoint foundEmpty = null;
for(byte row=0; rowGets the current rectangle for the playing board.
public Rectangle BoardRectangle
{
get
{
Rectangle rect = ClientRectangle;
int amount = rect.Width / 51;
if (amount < 2) amount = 2;
rect.Inflate(-amount, -amount);
return rect;
}
}
/// The font size, in points, for numbers on the grid.
private float _cachedEmSize = -1;
/// Clears the cached font size when the font is changed.
protected override void OnFontChanged(EventArgs e)
{
_cachedEmSize = -1;
base.OnFontChanged (e);
}
/// Draws the playing grid onto the specified graphics object and within the specified bounding rectangle.
/// The graphics object onto
public void DrawToGraphics(Graphics graphics, Rectangle clipRectangle)
{
if (graphics == null) throw new ArgumentNullException("graphics");
Rectangle rect = BoardRectangle;
// Draw the underlying board images
graphics.DrawImage(ResourceHelper.BoardBackgroundImage, 0, 0, Width, Height);
graphics.DrawImage(ResourceHelper.BoardImage, rect);
// Precompute some important sizes, such as the width and height of
// a cell in the grid, positioning information for drawing possible numbers,
// and the em size to use for drawing text.
RectangleF genericCellRect = GetCellRectangle(rect, new Point(0,0));
float cellWidth = genericCellRect.Width;
float cellHeight = genericCellRect.Height;
// Get the em size for the current font based on the current board size.
// This is cached, and the cache is only cleared when the board is resized or when the font is changed.
float emSize;
if (_cachedEmSize < 0) _cachedEmSize = GraphicsHelpers.GetMaximumEMSize(ResourceHelper.FontSizingString, graphics, this.Font.FontFamily, FontStyle.Bold, cellWidth, cellHeight);
emSize = _cachedEmSize;
bool showSuggestedCells = ShowSuggestedCells;
// Draw cell images
using (Font setNumberFont = new Font(this.Font.FontFamily, emSize, FontStyle.Bold))
{
for (int i = 0; i < State.GridSize; i++)
{
for (int j = 0; j < State.GridSize; j++)
{
RectangleF cellRect = GetCellRectangle(rect, new Point(i,j));
if (clipRectangle.IntersectsWith(Rectangle.Ceiling(cellRect)))
{
if (State.Status != PuzzleStatus.Solved)
{
if (_selectedCell.HasValue && _selectedCell.Value.X == i && _selectedCell.Value.Y == j &&
_mode != PuzzleGridMode.Scratchpad)
{
Image selectedCellImage;
if (i == 0 && j == 0) selectedCellImage = ResourceHelper.CellActiveUpperLeft;
else if (i == 0 && j == State.GridSize-1) selectedCellImage = ResourceHelper.CellActiveUpperRight;
else if (i == State.GridSize-1 && j == 0) selectedCellImage = ResourceHelper.CellActiveLowerLeft;
else if (i == State.GridSize-1 && j == State.GridSize-1) selectedCellImage = ResourceHelper.CellActiveLowerRight;
else selectedCellImage = ResourceHelper.CellActiveSquare;
graphics.DrawImage(selectedCellImage, cellRect.X, cellRect.Y, cellRect.Width, cellRect.Height);
}
else if (showSuggestedCells && SuggestedCell.HasValue &&
SuggestedCell.Value.X == i && SuggestedCell.Value.Y == j)
{
Image suggestedCellImage;
if (i == 0 && j == 0) suggestedCellImage = ResourceHelper.CellHintUpperLeft;
else if (i == 0 && j == State.GridSize-1) suggestedCellImage = ResourceHelper.CellHintUpperRight;
else if (i == State.GridSize-1 && j == 0) suggestedCellImage = ResourceHelper.CellHintLowerLeft;
else if (i == State.GridSize-1 && j == State.GridSize-1) suggestedCellImage = ResourceHelper.CellHintLowerRight;
else suggestedCellImage = ResourceHelper.CellHintSquare;
graphics.DrawImage(suggestedCellImage, cellRect.X, cellRect.Y, cellRect.Width, cellRect.Height);
}
}
// If a cell has a value, then draw that value
if (State[i, j].HasValue)
{
Brush b;
if (ShowIncorrectNumbers &&
State[i, j].HasValue && _solvedOriginalState != null &&
State[i, j].Value != _solvedOriginalState[i, j].Value)
{
b = _incorrectValueBrush;
}
else if (_originalState != null && _originalState[i,j].HasValue)
{
b = _originalValueBrush;
}
else b = _userValueBrush;
graphics.DrawString((State[i, j] + 1).ToString(CultureInfo.InvariantCulture), setNumberFont, b,
cellRect, _centerNumberFormat);
}
}
}
}
}
// Draw the scratchpad ink
try
{
RenderInk(graphics);
}
catch(COMException) {}
}
/// Draws the scratchpad ink.
/// The graphics onto which ink is drawn.
private void RenderInk(Graphics graphics)
{
if (_inkOverlay != null)
{
using(Strokes strokes = _inkOverlay.Ink.Strokes)
{
if (strokes.Count > 0)
{
_inkOverlay.Renderer.Draw(graphics, strokes);
}
}
}
}
#endregion
#region Keyboard and Mouse Interaction
/// Numeric touch mode value for when we're in touch mode.
private byte _touchModeValue;
/// The current mode of the grid (pen, eraser, touch, etc.).
private PuzzleGridMode _mode;
/// Gets or sets the numeric touch-mode value for when we're in touch mode.
public byte TouchModeValue
{
get { return _touchModeValue; }
set { _touchModeValue = value; }
}
/// Stats the PuzzleGrid can be in.
public enum PuzzleGridMode
{
Pen,
Eraser,
Touch,
Scratchpad
}
/// Gets or sets the mode of the PuzzleGrid.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public PuzzleGridMode Mode
{
get { return _mode; }
set
{
if (_mode != value)
{
_mode = value;
switch(value)
{
case PuzzleGridMode.Pen:
case PuzzleGridMode.Eraser:
SetTemporaryTabletSupportEnabled(true);
SetupScratchpadMode(false);
AdaptToCursorAndMode(false);
break;
case PuzzleGridMode.Scratchpad:
SetTemporaryTabletSupportEnabled(true);
SetupScratchpadMode(true);
AdaptToCursorAndMode(false);
break;
case PuzzleGridMode.Touch:
SetTemporaryTabletSupportEnabled(false);
SetupScratchpadMode(false);
break;
default:
throw new InvalidEnumArgumentException("value", (int)value, typeof(PuzzleGridMode));
}
Invalidate();
}
}
}
/// Determines whether the specified key is a regular input key or a special key that requires processing.
/// A Keys value.
/// Whether the specified Keys should be treated as an input key.
protected override bool IsInputKey(Keys keyData)
{
// We want to allow the control to handle up, down, left, right, so that
// the user can move around the selected cell marker with the arrow keys
switch (keyData)
{
case Keys.Up:
case Keys.Down:
case Keys.Left:
case Keys.Right:
return true;
}
return base.IsInputKey(keyData);
}
/// Handles the KeyDown event.
/// Event args
protected override void OnKeyDown(KeyEventArgs e)
{
Keys k = e.KeyCode;
bool handled = true;
bool invalidate = true;
if ((_mode != PuzzleGridMode.Scratchpad) && !CollectingInk)
{
Point selectedCell = _selectedCell.Value;
// Alt+Shift+2, in homage to Windows Solitaire :-)
if (k == Keys.D2 && e.Shift && e.Alt)
{
SolvePuzzle();
}
// Digits are treated as numbers and stored into the grid
else if ((k >= Keys.D1 && k <= Keys.D9) || (k >= Keys.NumPad1 & k <= Keys.NumPad9))
{
if (State != null && CanModifyCell(selectedCell))
{
SetStateCell(selectedCell, (k >= Keys.NumPad1 && k <= Keys.NumPad9) ?
(byte)(k - Keys.NumPad1) : (byte)(k - Keys.D1));
}
}
// Delete, space, and a variety of other keys are used to clear out the contents of a grid cell
else if (k == Keys.Delete || k == Keys.Space || k == Keys.Back)
{
if (State != null && CanClearCell(selectedCell))
{
ClearStateCellWithInvalidation(selectedCell);
invalidate = false;
}
}
else if (k == Keys.Enter && _mode == PuzzleGridMode.Touch && _touchModeValue > 0)
{
if (State != null && CanModifyCell(selectedCell))
{
SetStateCell(selectedCell, (byte)(_touchModeValue-1));
}
}
// And the arrow keys are used to move around the grid
else if (k == Keys.Down && selectedCell.X + 1 < State.GridSize)
{
InvalidateCell(selectedCell);
selectedCell.X++;
InvalidateCell(selectedCell);
invalidate = false;
}
else if (k == Keys.Up && selectedCell.X - 1 >= 0)
{
InvalidateCell(selectedCell);
selectedCell.X--;
InvalidateCell(selectedCell);
invalidate = false;
}
else if (k == Keys.Right && selectedCell.Y + 1 < State.GridSize)
{
InvalidateCell(selectedCell);
selectedCell.Y++;
InvalidateCell(selectedCell);
invalidate = false;
}
else if (k == Keys.Left && selectedCell.Y - 1 >= 0)
{
InvalidateCell(selectedCell);
selectedCell.Y--;
InvalidateCell(selectedCell);
invalidate = false;
}
// Otherwise, not handled
else handled = false;
// If it was handled, update the necessary state
if (handled)
{
_selectedCell = selectedCell;
e.Handled = true;
if (invalidate) Invalidate();
}
}
if (!e.Handled) base.OnKeyDown(e);
}
/// Update the selected cell when the mouse is clicked down.
/// The mouse event args.
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (_mode != PuzzleGridMode.Scratchpad)
{
Point cell = GetCellFromLocation(new Point(e.X, e.Y));
if (_mode == PuzzleGridMode.Touch && CanModifyCell(cell))
{
if (_touchModeValue > 0)
{
SetStateCell(cell, (byte)(_touchModeValue-1));
Invalidate();
}
}
if (_selectedCell.HasValue) InvalidateCell(_selectedCell.Value);
SetSelectedCell(cell);
InvalidateCell(cell);
}
}
/// Sets the selected cell to the specified cell, if it's valid.
/// The cell to be selected.
private void SetSelectedCell(Point cell)
{
if (cell.X >= 0 && cell.X < State.GridSize &&
cell.Y >= 0 && cell.Y < State.GridSize &&
_selectedCell != cell)
{
_selectedCell = cell;
}
}
#endregion
#region Tablet Support
/// Enables or disables the ink overlay.
/// Whether to enable or disable the overlay.
public void SetTemporaryTabletSupportEnabled(bool enabled)
{
if (_inkOverlay != null &&
_inkOverlay.Enabled != enabled) _inkOverlay.Enabled = enabled;
}
/// Turns on support for the Tablet PC input device.
public void EnableTabletSupport()
{
if (_inkOverlay == null)
{
// Get the recognizer to use and configure it.
Recognizer defaultRecognizer = PlatformDetection.GetDefaultRecognizer();
if (defaultRecognizer != null)
{
_recognizerCtx = defaultRecognizer.CreateRecognizerContext();
_recognizerCtx.Factoid = Factoid.Digit;
}
// Create the overlay and configure it
bool gestureRecognizerInstalled = PlatformDetection.GestureRecognizerInstalled;
_inkOverlay = new InkOverlay(this, true);
_inkOverlay.Ink.CustomStrokes.Add(ScratchpadStrokesID, _inkOverlay.Ink.CreateStrokes());
_inkOverlay.Ink.CustomStrokes.Add(NormalStrokesID, _inkOverlay.Ink.CreateStrokes());
_inkOverlay.DefaultDrawingAttributes.Color = _inkColor;
_inkOverlay.CollectionMode = gestureRecognizerInstalled ? CollectionMode.InkAndGesture : CollectionMode.InkOnly;
_inkOverlay.AutoRedraw = false;
_inkOverlay.DynamicRendering = true;
if (gestureRecognizerInstalled)
{
_inkOverlay.SetGestureStatus(ApplicationGesture.AllGestures, false);
_inkOverlay.SetGestureStatus(ApplicationGesture.Scratchout, true);
_inkOverlay.Gesture += new InkCollectorGestureEventHandler(HandleGesture);
}
_inkOverlay.Stroke += new InkCollectorStrokeEventHandler(HandleStroke);
_inkOverlay.CursorInRange += new InkCollectorCursorInRangeEventHandler(HandleCursorInRange);
_inkOverlay.NewPackets += new InkCollectorNewPacketsEventHandler(HandleNewPackets);
_inkOverlay.NewInAirPackets += new InkCollectorNewInAirPacketsEventHandler(HandleNewInAirPackets);
_inkOverlay.StrokesDeleting += new InkOverlayStrokesDeletingEventHandler(HandleStrokesDeleting);
_inkOverlay.Enabled = true;
Invalidate();
}
}
/// Disables support for the Tablet PC input device.
/// True if tablet support could be disabled; otherwise, false.
public void DisableTabletSupport()
{
if (_inkOverlay != null)
{
_inkOverlay.Dispose();
_inkOverlay = null;
if (_recognizerCtx != null) _recognizerCtx.Dispose();
_recognizerCtx = null;
Invalidate();
}
}
/// Gets the current collection of normal strokes.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
private Strokes NormalStrokes
{
get { return _inkOverlay.Ink.CustomStrokes[NormalStrokesID]; }
}
/// Gets the current collection of scratchpad strokes.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
private Strokes ScratchpadStrokes
{
get { return _inkOverlay.Ink.CustomStrokes[ScratchpadStrokesID]; }
}
/// Gets whether the overlay currently has any scratchpad strokes.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
private bool HasScratchpadStrokes
{
get
{
using(Strokes strokes = ScratchpadStrokes)
{
return strokes.Count > 0;
}
}
}
/// Sets the editing mode of the overlay based on which side of the pen is being used.
/// The overlay.
/// The event arguments.
void HandleCursorInRange(object sender, InkCollectorCursorInRangeEventArgs e)
{
AdaptToCursorAndMode(e.Cursor.Inverted);
}
/// Adapts the overlay to the current cursor state and mode.
/// Whether the cursor is known to be currently inverted.
internal void AdaptToCursorAndMode(bool cursorKnownToBeInverted)
{
if (_inkOverlay == null) return;
// Is the cursor upside down (eraser) or are we explicitly in eraser mode?
bool invertedOrEraserMode = cursorKnownToBeInverted || _mode == PuzzleGridMode.Eraser;
switch(_inkOverlay.EditingMode)
{
// If we're currently in ink mode but the cursor has become
// inverted or eraser mode has been set explicitly,
// swap into delete mode
case InkOverlayEditingMode.Ink:
if (invertedOrEraserMode)
{
_processStrokesTimer.Stop();
using(Strokes normalStrokes = NormalStrokes)
{
_inkOverlay.Ink.DeleteStrokes(normalStrokes);
normalStrokes.Clear();
}
_inkOverlay.Enabled = false;
_inkOverlay.EditingMode = InkOverlayEditingMode.Delete;
_inkOverlay.Enabled = true;
}
break;
// If we're currently in delete mode but the cursor
// is not inverted and we're not in eraser mode,
// swap back into ink mode
case InkOverlayEditingMode.Delete:
if(!invertedOrEraserMode)
{
_processStrokesTimer.Stop();
using(Strokes normalStrokes = NormalStrokes)
{
_inkOverlay.Ink.DeleteStrokes(normalStrokes);
normalStrokes.Clear();
}
_inkOverlay.Enabled = false;
_inkOverlay.EditingMode = InkOverlayEditingMode.Ink;
_inkOverlay.Enabled = true;
}
break;
}
}
/// Determines in which cell a stroke was made.
/// The stroke to analyze.
/// The location of the cell under the stroke.
private Point GetCellFromStroke(Stroke s)
{
Rectangle rect = s.GetBoundingBox();
InkToPixelSpace(ref rect);
Point cell = GetCellFromLocation(new Point((rect.Left + rect.Right) / 2, (rect.Top + rect.Bottom) / 2));
return cell;
}
/// Responds to a gesture made on the overlay.
/// The overlay.
/// The event arguments.
void HandleGesture(object sender, InkCollectorGestureEventArgs e)
{
if (InvokeRequired)
{
Invoke(new InkCollectorGestureEventHandler(HandleGesture), new object[]{sender, e});
return;
}
if (_mode == PuzzleGridMode.Scratchpad)
{
HandleGestureInScratchpadMode(sender, e);
}
else
{
HandleGestureInNormalMode(sender, e);
}
}
/// Responds to a gesture made on the overlay while in normal mode.
/// The overlay.
/// The event arguments.
void HandleGestureInNormalMode(object sender, InkCollectorGestureEventArgs e)
{
try
{
switch (e.Gestures[0].Id)
{
// If the scratchout gesture was made over a cell
// that can be modified, delete its contents
case ApplicationGesture.Scratchout:
Point cell = GetCellFromStroke(e.Strokes[0]);
_inkOverlay.Ink.DeleteStrokes(e.Strokes);
RecognizePreviousNormalStrokes();
if (CanClearCell(cell)) ClearStateCell(cell);
break;
default:
e.Cancel = true;
break;
}
}
catch (COMException) { }
Invalidate();
}
/// Responds to a gesture made on the overlay while in scratchpad mode.
/// The overlay.
/// The event arguments.
void HandleGestureInScratchpadMode(object sender, InkCollectorGestureEventArgs e)
{
if (e.Strokes.Count > 0)
{
switch (e.Gestures[0].Id)
{
// Erase any strokes in the bounding box of the gesture
case ApplicationGesture.Scratchout:
Rectangle intersectRect = e.Strokes[0].GetBoundingBox();
EraseScratchpadStrokes(intersectRect);
break;
default:
e.Cancel = true;
break;
}
}
Invalidate();
}
/// Erases any scratchpad strokes that intersect with the specified rectangle.
/// The rectangle to test for intersection, in ink space coordinates.
private void EraseScratchpadStrokes(Rectangle intersectRect)
{
using(Strokes strokes = ScratchpadStrokes)
{
for(int i=strokes.Count-1; i>=0; --i)
{
Stroke s = strokes[i];
if (s.GetBoundingBox().IntersectsWith(intersectRect))
{
strokes.RemoveAt(i);
_inkOverlay.Ink.DeleteStroke(s);
}
}
}
}
/// Gets or sets the current ink data for the overlay.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public byte [] InkData
{
get
{
return _inkOverlay != null ?
_inkOverlay.Ink.Save(PersistenceFormat.InkSerializedFormat) :
new byte[0];
}
set
{
if (_inkOverlay != null)
{
if (value != null && value.Length > 0)
{
InkOverlayEditingMode mode = _inkOverlay.EditingMode;
bool overlayWasEnabled = _inkOverlay.Enabled;
_inkOverlay.Enabled = false;
// Load the ink data into a new Ink and setup the overlay
// to use that new Ink
Microsoft.Ink.Ink ink = new Microsoft.Ink.Ink();
ink.Load(value);
Microsoft.Ink.Ink oldInk = _inkOverlay.Ink;
_inkOverlay.Ink = ink;
if (oldInk != null) oldInk.Dispose();
// Normal strokes are sometimes captured into undo data, and before they're
// added to the normal collection. So when restoring ink, remove any strokes
// that aren't in the scratchpad collection.
using(Strokes goodStrokes = ScratchpadStrokes)
using(Strokes strokes = _inkOverlay.Ink.Strokes)
{
Hashtable scratchpadStrokesIndex = new Hashtable(goodStrokes.Count);
foreach(Stroke s in goodStrokes)
{
scratchpadStrokesIndex.Add(s.Id, s);
}
if (strokes.Count > 0)
{
foreach(Stroke s in strokes)
{
if (!scratchpadStrokesIndex.ContainsKey(s.Id))
{
_inkOverlay.Ink.DeleteStroke(s);
}
}
}
}
using(Strokes normalStrokes = NormalStrokes) normalStrokes.Clear();
_inkOverlay.EditingMode = mode;
if (overlayWasEnabled)
{
_inkOverlay.Enabled = true;
}
// Update the scratchpad ink we just restored to fit the current grid size
ResizeScratchpadInk();
}
SetupScratchpadMode(_mode == PuzzleGridMode.Scratchpad);
}
}
}
/// Sets up for scratchpad mode or for leaving scratchpad mode.
/// true if entering scratchpad mode; otherwise, false.
private void SetupScratchpadMode(bool enterMode)
{
if (_inkOverlay != null)
{
_processStrokesTimer.Stop();
using(Strokes normalStrokes = NormalStrokes)
{
_inkOverlay.Ink.DeleteStrokes(normalStrokes);
normalStrokes.Clear();
}
if (enterMode)
{
using(Strokes strokes = ScratchpadStrokes)
{
foreach(Stroke s in strokes)
{
s.DrawingAttributes.Color = _scratchpadInkColor;
}
}
}
else
{
using(Strokes strokes = ScratchpadStrokes)
{
foreach(Stroke s in strokes)
{
s.DrawingAttributes.Color = _scratchpadInkInNormalModeColor;
}
}
}
_inkOverlay.DefaultDrawingAttributes.Color = enterMode ? _scratchpadInkColor : _inkColor;
Invalidate();
}
}
/// Handles a stroke made on the overlay.
/// The overlay.
/// The event arguments.
void HandleStroke(object sender, InkCollectorStrokeEventArgs e)
{
if (InvokeRequired)
{
Invoke(new InkCollectorStrokeEventHandler(HandleStroke), new object[]{sender, e});
return;
}
if (_mode == PuzzleGridMode.Scratchpad)
{
HandleStrokeInScratchpadMode(sender, e);
}
else
{
HandleStrokeInNormalMode(sender, e);
}
}
/// Handles a stroke being deleted from the overlay.
/// The overlay.
/// The event arguments.
private void HandleStrokesDeleting(object sender, InkOverlayStrokesDeletingEventArgs e)
{
SetUndoCheckpoint();
using(Strokes normalStrokes = NormalStrokes)
{
normalStrokes.Remove(e.StrokesToDelete);
}
using(Strokes scratchpadStrokes = ScratchpadStrokes)
{
scratchpadStrokes.Remove(e.StrokesToDelete);
}
}
/// GUID used to track the original X coordinate of a scratchpad stroke.
private static readonly Guid OriginalStrokeBoundRectXGuid = new Guid("45ea29c2-8afa-4d81-8196-62ba29f33075");
/// GUID used to track the original Y coordinate of a scratchpad stroke.
private static readonly Guid OriginalStrokeBoundRectYGuid = new Guid("45ea29c2-8afa-4d81-8196-62ba29f33076");
/// GUID used to track the original width of a scratchpad stroke.
private static readonly Guid OriginalStrokeBoundRectWidthGuid = new Guid("45ea29c2-8afa-4d81-8196-62ba29f33077");
/// GUID used to track the original height of a scratchpad stroke.
private static readonly Guid OriginalStrokeBoundRectHeightGuid = new Guid("45ea29c2-8afa-4d81-8196-62ba29f33078");
/// GUID used to track the width of the puzzle grid when a scratchpad stroke was made.
private static readonly Guid OriginalClientRectWidthGuid = new Guid("45ea29c2-8afa-4d81-8196-62ba29f33079");
/// GUID used to track the height of the puzzle grid when a scratchpad stroke was made.
private static readonly Guid OriginalClientRectHeightGuid = new Guid("45ea29c2-8afa-4d81-8196-62ba29f3307A");
/// Handles a stroke made on the overlay.
/// The overlay.
/// The event arguments.
void HandleStrokeInScratchpadMode(object sender, InkCollectorStrokeEventArgs e)
{
try
{
Stroke s = e.Stroke;
Rectangle boundingBox = s.GetBoundingBox();
switch (_inkOverlay.EditingMode)
{
case InkOverlayEditingMode.Ink:
// Since there's no "pre-stroke" event, workaround to get undo checkpoints working correctly
TabletPropertyDescriptionCollection tpdc = new TabletPropertyDescriptionCollection();
foreach (Guid g in s.PacketDescription)
{
TabletPropertyDescription tpd = new TabletPropertyDescription(g, e.Cursor.Tablet.GetPropertyMetrics(g));
tpdc.Add(tpd);
}
int[] packetData = s.GetPacketData();
_inkOverlay.Ink.DeleteStroke(s);
SetUndoCheckpoint();
s = _inkOverlay.Ink.CreateStroke(packetData, tpdc);
s.DrawingAttributes.Color = _scratchpadInkColor;
using(Graphics graphics = CreateGraphics())
{
// Every scratchpad stroke has associated with it at creation time the current size of the
// stroke and the current size of the grid. Every time the grid is resized, this information
// allows the stroke to be resized accordingly.
InkSpaceToPixelSpace(graphics, ref boundingBox);
s.ExtendedProperties.Add(OriginalStrokeBoundRectXGuid, boundingBox.X);
s.ExtendedProperties.Add(OriginalStrokeBoundRectYGuid, boundingBox.Y);
s.ExtendedProperties.Add(OriginalStrokeBoundRectWidthGuid, boundingBox.Width);
s.ExtendedProperties.Add(OriginalStrokeBoundRectHeightGuid, boundingBox.Height);
s.ExtendedProperties.Add(OriginalClientRectWidthGuid, ClientRectangle.Width);
s.ExtendedProperties.Add(OriginalClientRectHeightGuid, ClientRectangle.Height);
}
using(Strokes scratchpadStrokes = ScratchpadStrokes)
{
scratchpadStrokes.Add(s);
}
OnStateChanged();
break;
case InkOverlayEditingMode.Delete:
break;
}
}
catch (COMException) { }
}
/// Resizes the control.
/// The event arguments.
protected override void OnResize(EventArgs e)
{
_cachedEmSize = -1;
ResizeScratchpadInk();
base.OnResize(e);
}
/// Resizes all ink currently on the scratchpad to match the current size of the puzzle grid.
internal void ResizeScratchpadInk()
{
if (_inkOverlay != null)
{
Rectangle currentClientRect = ClientRectangle;
using(Strokes scratchpadStrokes = ScratchpadStrokes)
{
if (scratchpadStrokes != null)
{
// Every scratchpad stroke has associated with it at creation time the current size of the
// stroke and the current size of the grid. Every time the grid is resized, this information
// allows the stroke to be resized accordingly.
foreach(Stroke s in scratchpadStrokes)
{
int originalBoundsX = (int)s.ExtendedProperties[OriginalStrokeBoundRectXGuid].Data;
int originalBoundsY = (int)s.ExtendedProperties[OriginalStrokeBoundRectYGuid].Data;
int originalBoundsWidth = (int)s.ExtendedProperties[OriginalStrokeBoundRectWidthGuid].Data;
int originalBoundsHeight = (int)s.ExtendedProperties[OriginalStrokeBoundRectHeightGuid].Data;
int originalClientRectWidth = (int)s.ExtendedProperties[OriginalClientRectWidthGuid].Data;
int originalClientRectHeight = (int)s.ExtendedProperties[OriginalClientRectHeightGuid].Data;
double scaleX = currentClientRect.Width / (double)originalClientRectWidth;
double scaleY = currentClientRect.Height / (double)originalClientRectHeight;
Rectangle newBounds = new Rectangle(
(int)(originalBoundsX*scaleX), (int)(originalBoundsY*scaleY),
(int)(originalBoundsWidth*scaleX), (int)(originalBoundsHeight*scaleY));
using(Graphics graphics = CreateGraphics())
{
// Rescale the stroke
PixelSpaceToInkSpace(graphics, ref newBounds);
s.ScaleToRectangle(newBounds);
}
}
}
}
}
}
/// Converts a rectangle from ink space to control space.
/// The rectangle to convert.
private void InkToPixelSpace(ref Rectangle rect)
{
using(Graphics g = CreateGraphics()) InkSpaceToPixelSpace(g, ref rect);
}
/// Converts a rectangle from ink space to control space.
/// The graphics object to use for the conversion.
/// The rectangle to convert.
private void InkSpaceToPixelSpace(Graphics graphics, ref Rectangle rect)
{
Point topLeft = new Point(rect.Left, rect.Top);
Point widthHeight = new Point(rect.Width, rect.Height);
_inkOverlay.Renderer.InkSpaceToPixel(graphics, ref topLeft);
_inkOverlay.Renderer.InkSpaceToPixel(graphics, ref widthHeight);
rect = new Rectangle(topLeft.X, topLeft.Y, widthHeight.X, widthHeight.Y);
}
/// Converts a rectangle from control space to ink space.
/// The graphics object to use for the conversion.
/// The rectangle to convert.
private void PixelSpaceToInkSpace(Graphics graphics, ref Rectangle rect)
{
Point topLeft = new Point(rect.Left, rect.Top);
Point widthHeight = new Point(rect.Width, rect.Height);
_inkOverlay.Renderer.PixelToInkSpace(graphics, ref topLeft);
_inkOverlay.Renderer.PixelToInkSpace(graphics, ref widthHeight);
rect = new Rectangle(topLeft.X, topLeft.Y, widthHeight.X, widthHeight.Y);
}
/// Handles a stroke made on the overlay.
/// The overlay.
/// The event arguments.
void HandleStrokeInNormalMode(object sender, InkCollectorStrokeEventArgs e)
{
e.Stroke.DrawingAttributes.Color = _inkColor;
try
{
// Get the cell under the stroke
Point currentStrokeCell = GetCellFromStroke(e.Stroke);
// If there were previously unprocessed strokes made in
// a different cell, process those separately before
// processing this one.
RecognizePreviousNormalStrokesInDifferentCell(currentStrokeCell);
switch (_inkOverlay.EditingMode)
{
case InkOverlayEditingMode.Ink:
// Only care about "significant" strokes in order to better avoid user error
const float MINIMINUM_BOUNDING_RATIO_TO_RECOGNIZE = .15f;
Rectangle boundingBox = e.Stroke.GetBoundingBox();
InkToPixelSpace(ref boundingBox);
RectangleF cellRect = GetCellRectangle(ClientRectangle, Point.Empty);
if (boundingBox.Width < cellRect.Width * MINIMINUM_BOUNDING_RATIO_TO_RECOGNIZE &&
boundingBox.Height < cellRect.Height * MINIMINUM_BOUNDING_RATIO_TO_RECOGNIZE)
{
e.Cancel = true;
using(Strokes normalStrokes = NormalStrokes)
{
if (normalStrokes.Count > 0) _processStrokesTimer.Start();
}
}
else
{
// Store the stroke
using(Strokes normalStrokes = NormalStrokes)
{
normalStrokes.Add(e.Stroke);
}
// Restart the timer after each stroke is made
_processStrokesTimer.Stop();
_processStrokesTimer.Start();
}
break;
case InkOverlayEditingMode.Delete:
// Delete the current cell's contents
if (CanClearCell(currentStrokeCell)) ClearStateCell(currentStrokeCell);
using(Strokes normalStrokes = NormalStrokes)
{
_inkOverlay.Ink.DeleteStrokes(normalStrokes);
normalStrokes.Clear();
}
e.Cancel = true;
Invalidate();
break;
}
}
catch (COMException)
{
using(Strokes normalStrokes = NormalStrokes)
{
_inkOverlay.Ink.DeleteStrokes(normalStrokes);
normalStrokes.Clear();
}
}
}
/// Handles the arrival of new in-air packets.
private void HandleNewInAirPackets(object sender, InkCollectorNewInAirPacketsEventArgs e)
{
if (e.PacketData.Length >= 2)
{
RecognizePreviousCellFromPacket(new Point(e.PacketData[0], e.PacketData[1]));
}
}
/// Recognizes the previous normal strokes that aren't in the cell specified by the ink-space point.
/// The ink-space point.
private void RecognizePreviousCellFromPacket(Point tabletPoint)
{
Point cell = TabletToCell(tabletPoint);
using(Strokes normalStrokes = NormalStrokes)
{
if (normalStrokes.Count > 0)
{
Point otherCell = GetCellFromStroke(normalStrokes[0]);
if (cell != otherCell)
{
RecognizePreviousNormalStrokes();
}
}
}
}
/// Gets the cell containing an ink-space point.
/// The point.
/// The cell.
private Point TabletToCell(Point p)
{
using (Graphics g = CreateGraphics())
{
_inkOverlay.Renderer.InkSpaceToPixel(g, ref p);
return GetCellFromLocation(p);
}
}
/// Handles the arrival of new data packets.
/// The overlay.
/// The event arguments
void HandleNewPackets(object sender, InkCollectorNewPacketsEventArgs e)
{
_processStrokesTimer.Stop();
if (_mode == PuzzleGridMode.Eraser || e.Cursor.Inverted)
{
if (e.PacketData.Length >= 2)
{
Point cell = TabletToCell(new Point(e.PacketData[0], e.PacketData[1]));
if (CanClearCell(cell) && State[cell].HasValue)
{
ClearStateCellWithInvalidation(cell);
}
if (_selectedCell.HasValue)
{
InvalidateCell(_selectedCell.Value);
}
SetSelectedCell(cell);
InvalidateCell(_selectedCell.Value);
}
}
else if (_mode == PuzzleGridMode.Pen)
{
if (e.PacketData.Length >= 2)
{
RecognizePreviousCellFromPacket(new Point(e.PacketData[0], e.PacketData[1]));
}
}
}
/// Recognize the current strokes after the last stroke is made.
/// The timer.
/// The event arguments.
private void StrokesTimerTick(object sender, EventArgs e)
{
_processStrokesTimer.Stop();
if (_inkOverlay != null)
{
// Recognize the strokes
using(Strokes strokes = NormalStrokes)
{
if (strokes.Count > 0)
{
byte number = 0;
bool recognized = false;
Point cell = GetCellFromStroke(strokes[0]);
if (CanModifyCell(cell))
{
recognized = RecognizeStrokes(strokes, out number);
}
// Delete the strokes
_inkOverlay.Ink.DeleteStrokes(strokes);
strokes.Clear();
// Set the recognized number if available
if (recognized)
{
SetStateCell(cell, (byte)(number-1));
}
}
}
// Refresh
Invalidate();
}
}
/// Recognize all but the last stroke if they were made in different cells.
/// The current stroke's cell.
private void RecognizePreviousNormalStrokesInDifferentCell(Point currentStrokeCell)
{
// Get the current set of strokes
using(Strokes strokes = NormalStrokes)
{
// If there are multiple strokes, including this one, then make sure
// they're all in the same cell. If they're not, recognize all
// but the latest stroke, and then proceed as normal. This lets people
// quickly write multiple numbers into multiple boxes, while still
// allowing for multiple stroke recognition of digits.
if (strokes.Count > 0)
{
Point firstStrokeCell = GetCellFromStroke(strokes[0]);
if (firstStrokeCell != currentStrokeCell)
{
RecognizePreviousNormalStrokes();
}
}
}
}
/// Recognize all but the last stroke.
private void RecognizePreviousNormalStrokes()
{
// Get the current set of strokes
using(Strokes strokes = NormalStrokes)
{
if (strokes.Count > 0)
{
// Recognize the remaining strokes
byte number = 0;
bool recognized = false;
Point cell = GetCellFromStroke(strokes[0]);
if (CanModifyCell(cell))
{
recognized = RecognizeStrokes(strokes, out number);
}
// Delete them
_inkOverlay.Ink.DeleteStrokes(strokes);
strokes.Clear();
// If we were able to recognize them, set the number
if (recognized)
{
SetStateCell(cell, (byte)(number-1));
Invalidate();
}
}
}
}
/// Recognize the current set of strokes as a digit, if possible.
/// The strokes to recognize.
/// The recognized number, if one is recognized.
private bool RecognizeStrokes(Strokes strokes, out byte number)
{
number = 0;
// Make sure we have something to recognize and something to recognize it with
if (_recognizerCtx != null && strokes.Count > 0)
{
try
{
// Store the strokes into the recognizer
_recognizerCtx.Strokes = strokes;
_recognizerCtx.EndInkInput();
// Recognize the strokes
RecognitionStatus rs;
RecognitionResult rr = _recognizerCtx.Recognize(out rs);
if (rr != null && rs == RecognitionStatus.NoError)
{
// Was it a digit from 1 through 9?
string inputNumberText = rr.TopString;
if (inputNumberText != null && inputNumberText.Length > 0)
{
try
{
number = byte.Parse(inputNumberText, CultureInfo.InvariantCulture);
}
catch(OverflowException){}
catch(FormatException){}
if (number >= 1 && number <= 9) return true;
}
}
}
catch (COMException) { }
}
return false;
}
/// Sets the contents of a cell in the current puzzle.
/// The cell to be set.
/// The value to which the cell should be set.
/// Sets an undo checkpoint and deletes any scratchpad strokes for the specified cell.
private void SetStateCell(Point cell, byte number)
{
if (State[cell] != number)
{
SetUndoCheckpoint();
ClearTabletStateCell(cell);
State[cell] = number;
}
}
/// Clears a cell of all tablet strokes centered within that cell.
/// The cell to be cleared.
private void ClearTabletStateCell(Point cell)
{
if (_inkOverlay != null)
{
// Delete any scratchpad strokes in that cell
using(Strokes strokes = ScratchpadStrokes)
{
for(int i=strokes.Count-1; i>=0; --i)
{
Stroke s = strokes[i];
if (GetCellFromStroke(s) == cell)
{
strokes.RemoveAt(i);
_inkOverlay.Ink.DeleteStroke(s);
}
}
}
}
}
/// Clears the contents of a cell in the current puzzle.
/// The cell to be cleared.
/// Sets an undo checkpoint.
private void ClearStateCell(Point cell)
{
if (State[cell].HasValue)
{
SetUndoCheckpoint();
State[cell] = null;
}
}
/// Invalidates the specified cell.
/// The cell to be repainted.
private void InvalidateCell(Point cell)
{
if (cell.X >= 0 && cell.X < State.GridSize &&
cell.Y >= 0 && cell.Y < State.GridSize)
{
const int BUFFER_DIST = 2;
RectangleF rect = GetCellRectangle(BoardRectangle, cell);
rect.Inflate(BUFFER_DIST,BUFFER_DIST);
Invalidate(Rectangle.Ceiling(rect));
}
}
/// Clears a cell and invalidates it.
/// The cell to be cleared and repainted.
private void ClearStateCellWithInvalidation(Point cell)
{
bool showSuggestedCells = ShowSuggestedCells;
if (showSuggestedCells) InvalidateCell(SuggestedCell);
ClearStateCell(cell);
InvalidateCell(cell);
if (showSuggestedCells) InvalidateCell(SuggestedCell);
}
#endregion
}
}