2025-11-28 00:35:46 +09:00

1253 lines
38 KiB
C++

// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved
#include <windows.h>
#include <ole2.h>
#include <uiautomation.h>
#include <strsafe.h>
#include "AnnotatedTextControl.h"
#include "FrameProvider.h"
bool AnnotatedTextControl::initialized = false;
TextLine sampleLines [] =
{
{ L"Impressive Title", TextStyle_Title },
{ L"This is a simple introduction to the UI Automation Document Content Provider Sample", TextStyle_Normal },
{ L"Section 1)", TextStyle_Header1 },
{ L"Item b)", TextStyle_Header2 },
{ L"\
This is a long paragraph: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \
Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus \
lectus malesuada libero, sit amet commodo magna eros quis urna. Nunc viverra imperdiet \
enim. Fusce est. Vivamus a tellus. Pellentesque habitant morbi tristique senectus et netus\
et malesuada fames ac turpis egestas. Proin pharetra nonummy pede. Mauris et orci."
, TextStyle_Normal },
{ L"And now to Emphasize a point", TextStyle_Bold },
{ L"In conclusion ...", TextStyle_Normal },
{ L"\
Another long paragraph: aaaabbbbbb abbbbb bbbbccccc ddddddd eeeeee ffff ggggggg hhhh \
iiiijjjj kkklll mmmmnnn ooooppp pqqqqrrr ssssttt uuuvvvv wwwwxxx xxyzzzz. Abcdef abcde \
12345 aaabbbb bbccc ccccddddd dddddddeee eeeeeefff fffff ggggggg hhhhiiiiiiiii jjjjjkkk\
lllm mmm mmmmnn nnnooppppp pqqqqqq qqqrrrr ssssssstt tttuuvv wwwwwwxx xxxxxyzz zzzzz."
, TextStyle_Normal },
};
Annotation sampleAnnotations [] =
{
// id, line, first, len, ----- text ----- author date
{ 0, 0, 0, 10, L"Is it really that impressive?", L"Skeptical Reader", {2011, 2, 2, 8, 14, 30, 0, 0} },
{ 1, 3, 5, 1, L"Where did \"Item a)\" go?", L"Eagle Eyed Editor", {2011, 4, 1,11, 10, 25, 0, 0} },
{ 2, 4, 84, 8, L"I met Maecenas once... cool guy.", L"Vivamus", {1776, 7, 4, 4, 17, 00, 0, 0} },
{ 3, 6, 0, -1, L"Is this really needed at all?", L"Skeptical Reader", {2011, 2, 2, 8, 14, 45, 0, 0} },
{ 4, 6, 15, 9, L"I'm an overlapping comment!", L"Zealous Tester", {2015,11, 6,17, 1, 30, 0, 0} }
};
struct StyleDefinition {
TextStyle style;
float fontSize;
float indent;
float verticalSpacing;
int fontStyle;
long uiaStyleId;
PCWSTR uiaStyleName;
};
// Some parameters for our drawing
const PCWSTR textFontFace = L"Calibri";
const PCWSTR annotationFontFace = L"Arial Narrow";
const Color textColor(255,0,0,0);
const Color annotationColor(255,20,70,200);
const Color annotationTextColor(255,255,255,255);
const float annotationFontSize = 14.0f;
const float annotationPadding = 5.0f;
const float annotationInflation = 3.0f;
const float annotationLineWidth = 1.5f;
const float verticalPadding = 10.0f;
StyleDefinition styleDefinitions [] =
{
// Style, size, indent, vspacing, font style
{ TextStyle_Normal, 12.0f, 30.0f, 3.0f, FontStyleRegular , StyleId_Normal, L"Normal" },
{ TextStyle_Header1, 24.0f, 10.0f, 10.0f, FontStyleRegular , StyleId_Heading1, L"Heading 1" },
{ TextStyle_Header2, 18.0f, 20.0f, 7.0f, FontStyleItalic , StyleId_Heading2, L"Heading 2" },
{ TextStyle_Title, 30.0f, 50.0f, 20.0f, FontStyleBold | FontStyleUnderline, StyleId_Title, L"Title" },
{ TextStyle_Bold, 12.0f, 30.0f, 3.0f, FontStyleBold , StyleId_Emphasis, L"Emphasis"}
};
StyleDefinition* GetStyleDefinition(_In_ TextStyle style)
{
for (int i = 0; i < ARRAYSIZE(styleDefinitions); i++)
{
if (styleDefinitions[i].style == style)
{
return &(styleDefinitions[i]);
}
}
// If the style requested isn't found, return the Normal style
return &(styleDefinitions[0]);
}
HWND AnnotatedTextControl::Create(_In_ HWND parent, _In_ HINSTANCE instance)
{
HWND returnHwnd = NULL;
if (!initialized)
{
initialized = Initialize(instance);
}
if (initialized)
{
AnnotatedTextControl* control = new AnnotatedTextControl(sampleLines, ARRAYSIZE(sampleLines), sampleAnnotations, ARRAYSIZE(sampleAnnotations));
if (control != NULL)
{
control->hwnd = CreateWindow( L"AnnotatedTextControl", L"",
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_VSCROLL,
0, 0, 1, 1, parent, NULL, instance, static_cast<PVOID>(control));
returnHwnd = control->hwnd;
}
}
return returnHwnd;
}
bool AnnotatedTextControl::Initialize(_In_ HINSTANCE instance)
{
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = AnnotatedTextControl::StaticWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = instance;
wc.hIcon = NULL;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = L"AnnotatedTextControl";
if (!RegisterClass(&wc))
{
return false;
}
return true;
}
AnnotatedTextControl::AnnotatedTextControl(_In_reads_(lineCount) TextLine *lines, _In_ int lineCount,
_In_reads_(annotationCount) Annotation *annotations, _In_ int annotationCount)
{
hwnd = NULL;
AnnotatedTextControl::lines = lines;
AnnotatedTextControl::lineCount = lineCount;
AnnotatedTextControl::annotations = annotations;
AnnotatedTextControl::annotationCount = annotationCount;
currentScroll = 0;
maxScroll = 0;
caretPosition.line = 0;
caretPosition.character = 0;
isActive = false;
}
LRESULT CALLBACK AnnotatedTextControl::WndProc(_In_ HWND hwndAnnotatedTextControl, _In_ UINT message, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
LRESULT lResult = 0;
switch (message)
{
case WM_PAINT:
{
PAINTSTRUCT ps;
BeginPaint(hwndAnnotatedTextControl, &ps);
OnPaint(ps.hdc);
EndPaint(hwndAnnotatedTextControl, &ps);
break;
}
case WM_GETOBJECT:
{
IRawElementProviderSimple * provider = new FrameProvider(hwndAnnotatedTextControl, this);
if (provider != NULL)
{
lResult = UiaReturnRawElementProvider(hwndAnnotatedTextControl, wParam, lParam, provider);
provider->Release();
}
break;
}
case WM_SIZE:
{
lResult = OnSize(wParam, lParam);
break;
}
case WM_VSCROLL:
{
lResult = OnScroll(wParam, lParam);
break;
}
case WM_SETFOCUS:
{
lResult = OnSetFocus();
break;
}
case WM_KILLFOCUS:
{
lResult = OnKillFocus();
break;
}
case WM_KEYDOWN:
{
lResult = OnKeyDown(wParam, lParam);
break;
}
case WM_LBUTTONDOWN:
{
lResult = OnLButtonDown(wParam, lParam);
break;
}
default:
lResult = DefWindowProc(hwndAnnotatedTextControl, message, wParam, lParam);
break;
}
return lResult;
}
LRESULT CALLBACK AnnotatedTextControl::StaticWndProc(_In_ HWND hwnd, _In_ UINT message, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
AnnotatedTextControl * pThis = reinterpret_cast<AnnotatedTextControl*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
if (message == WM_NCCREATE)
{
CREATESTRUCT *createStruct = reinterpret_cast<CREATESTRUCT*>(lParam);
pThis = reinterpret_cast<AnnotatedTextControl*>(createStruct->lpCreateParams);
SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(pThis));
}
if (message == WM_NCDESTROY)
{
pThis = NULL;
SetWindowLongPtr(hwnd, GWLP_USERDATA, NULL);
}
if (pThis != NULL)
{
return pThis->WndProc(hwnd, message, wParam, lParam);
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
void AnnotatedTextControl::OnPaint(_In_ HDC hdc)
{
Graphics graphics(hdc);
SolidBrush textBrush(textColor);
const StringFormat* stringFormat = StringFormat::GenericDefault();
graphics.SetCompositingMode(CompositingModeSourceOver);
graphics.SetTextRenderingHint(TextRenderingHintAntiAliasGridFit);
graphics.SetSmoothingMode(SmoothingModeHighQuality);
// Draw each of the lines in turn
for(int i = 0; i < lineCount; i++)
{
StyleDefinition* styleDef = GetStyleDefinition(lines[i].style);
RectF lineRect = GetLinePosition(i, &graphics);
Font font(textFontFace, styleDef->fontSize, styleDef->fontStyle);
graphics.DrawString(lines[i].text, -1, &font, lineRect, stringFormat, &textBrush);
}
SolidBrush annotationBrush(annotationColor);
SolidBrush annotationTextBrush(annotationTextColor);
Pen annotationPen(annotationColor, annotationLineWidth);
graphics.SetTextRenderingHint(TextRenderingHintClearTypeGridFit);
// Draw each of the annotations in turn
for (unsigned int i = 0; i < annotationCount; i++)
{
RectF annotationRect = GetAnnotationPosition(i, &graphics);
RectF annotationRectInflated(annotationRect);
annotationRectInflated.Inflate(annotationInflation, annotationInflation);
graphics.FillRectangle(&annotationBrush, annotationRectInflated);
Font font(annotationFontFace, annotationFontSize);
graphics.DrawString(annotations[i].text, -1, &font, annotationRect, stringFormat, &annotationTextBrush);
// Get the region, and convert it to an array of rectangles
Region *region = GetLineCharactersPosition(annotations[i].line, annotations[i].start_char, annotations[i].length, &graphics);
if (region != NULL)
{
Matrix identity; // Identity is the default matrix
UINT scansCount = region->GetRegionScansCount(&identity);
RectF *regionRects = new RectF[scansCount];
if (regionRects != NULL)
{
int count;
if (region->GetRegionScans(&identity, regionRects, &count) == Ok && count > 0)
{
// First draw the outlines of all of the rectangles
graphics.DrawRectangles(&annotationPen, regionRects, count);
// Next draw a line from the first rectangle to the annotation rectangle;
PointF charactersRightSide(regionRects[0].GetRight(), regionRects[0].GetTop() + regionRects[0].Height / 2.0f);
PointF annotationLeftSide(annotationRectInflated.GetLeft(), annotationRectInflated.GetTop() + annotationRectInflated.Height / 2.0f);
graphics.DrawLine(&annotationPen, charactersRightSide, annotationLeftSide);
}
delete [] regionRects;
}
delete region;
}
else
{
OutputDebugString(L"Failed to get region\n");
}
}
}
LRESULT AnnotatedTextControl::OnSize(_In_ WPARAM wParam, _In_ LPARAM lParam)
{
UNREFERENCED_PARAMETER(wParam);
UNREFERENCED_PARAMETER(lParam);
RECT clientRect;
GetClientRect(hwnd, &clientRect);
int height = GetDesiredHeight();
maxScroll = max(height - clientRect.bottom, 0);
currentScroll = min(maxScroll, currentScroll);
SCROLLINFO si;
si.cbSize = sizeof(si);
si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS;
si.nMin = 0;
si.nMax = maxScroll > 0 ? height : 0;
si.nPage = clientRect.bottom;
si.nPos = currentScroll;
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
return 0;
}
LRESULT AnnotatedTextControl::OnScroll(_In_ WPARAM wParam, _In_ LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
SCROLLINFO si;
si.cbSize = sizeof(si);
si.fMask = SIF_TRACKPOS;
GetScrollInfo(hwnd, SB_VERT, &si);
int newScrollPosition; // new position
switch (LOWORD(wParam))
{
// User clicked the scroll bar shaft above the scroll box.
case SB_PAGEUP:
newScrollPosition = currentScroll - 50;
break;
// User clicked the scroll bar shaft below the scroll box.
case SB_PAGEDOWN:
newScrollPosition = currentScroll + 50;
break;
// User clicked the top arrow.
case SB_LINEUP:
newScrollPosition = currentScroll - 5;
break;
// User clicked the bottom arrow.
case SB_LINEDOWN:
newScrollPosition = currentScroll + 5;
break;
// User dragged the scroll box.
case SB_THUMBPOSITION:
newScrollPosition = si.nTrackPos;
break;
default:
newScrollPosition = currentScroll;
}
// New position must be between 0 and the screen height.
newScrollPosition = max(0, newScrollPosition);
newScrollPosition = min(maxScroll, newScrollPosition);
// If the current position does not change, do not scroll.
if (newScrollPosition != currentScroll)
{
int yDelta = newScrollPosition - currentScroll;
// Reset the current scroll position.
currentScroll = newScrollPosition;
// Scroll the window. (The system repaints most of the
// client area when ScrollWindowEx is called; however, it is
// necessary to call UpdateWindow in order to repaint the
// rectangle of pixels that were invalidated.)
ScrollWindowEx(hwnd, -0, -yDelta, (CONST RECT *) NULL,
(CONST RECT *) NULL, (HRGN) NULL, (PRECT) NULL,
SW_ERASE | SW_INVALIDATE );
UpdateWindow(hwnd);
// Reset the scroll bar.
si.cbSize = sizeof(si);
si.fMask = SIF_POS;
si.nPos = currentScroll;
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
}
return 0;
}
LRESULT AnnotatedTextControl::OnSetFocus()
{
RectF caretScreenPos = GetCaretScreenPosition();
// Create the Caret
CreateCaret(hwnd, (HBITMAP) NULL, 0 /* Use the default width */, static_cast<int>(caretScreenPos.Height));
SetCaretPos(static_cast<int>(caretScreenPos.X), static_cast<int>(caretScreenPos.Y));
ShowCaret(hwnd);
isActive = true;
return 0;
}
LRESULT AnnotatedTextControl::OnKillFocus()
{
DestroyCaret();
isActive = false;
return 0;
}
LRESULT AnnotatedTextControl::OnKeyDown(_In_ WPARAM wParam, _In_ LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
bool changed = false;
EndPoint newCaretPos = caretPosition;
switch (wParam)
{
case VK_HOME: // Home
newCaretPos.line = 0;
newCaretPos.character = 0;
changed = true;
break;
case VK_END: // End
newCaretPos.line = GetLineCount() - 1;
newCaretPos.character = GetLineLength(newCaretPos.line);
changed = true;
break;
case VK_LEFT: // Left arrow
changed = StepCharacter(caretPosition, false, &newCaretPos);
break;
case VK_RIGHT: // Right arrow
changed = StepCharacter(caretPosition, true, &newCaretPos);
break;
case VK_UP: // Up arrow
// Go to the same position in the previous line
newCaretPos.line--;
newCaretPos.line = max(newCaretPos.line, 0);
newCaretPos.character = min(newCaretPos.character, GetLineLength(newCaretPos.line));
changed = true;
break;
case VK_DOWN: // Down arrow
// Go to the same position in the next line
newCaretPos.line++;
newCaretPos.line = min(newCaretPos.line, GetLineCount() - 1);
newCaretPos.character = min(newCaretPos.character, GetLineLength(newCaretPos.line));
changed = true;
break;
default:
break;
}
if (changed)
{
UpdateCaret(newCaretPos);
}
return 0;
}
LRESULT AnnotatedTextControl::OnLButtonDown(_In_ WPARAM wParam, _In_ LPARAM lParam)
{
UNREFERENCED_PARAMETER(wParam);
float xPos = static_cast<float>(LOWORD(lParam));
float yPos = static_cast<float>(HIWORD(lParam));
EndPoint click = SearchForClosestEndPoint(xPos, yPos);
UpdateCaret(click);
return 0;
}
float AnnotatedTextControl::GetTextAreaWidth()
{
RECT clientRect;
GetClientRect(hwnd, &clientRect);
// We split the area, 2/3 for text, 1/3 for annotations
return (2.0f * clientRect.right) / 3.0f;
}
SizeF AnnotatedTextControl::GetLineDimensions(_In_ int line, _In_ Graphics *graphics)
{
if (line < 0 || line >= lineCount)
{
return SizeF();
}
StyleDefinition* styleDef = GetStyleDefinition(lines[line].style);
Font font(textFontFace, styleDef->fontSize, styleDef->fontStyle);
SizeF layoutSize(GetTextAreaWidth() - styleDef->indent, 1200.0f);
const StringFormat* stringFormat = StringFormat::GenericDefault();
SizeF lineSize;
Status status = graphics->MeasureString(lines[line].text, -1, &font, layoutSize, stringFormat, &lineSize);
if (status != Ok)
{
lineSize = SizeF();
}
return lineSize;
}
RectF AnnotatedTextControl::GetLinePosition(_In_ int line, _In_ Graphics *graphics)
{
if (line < 0 || line >= lineCount)
{
return RectF();
}
float yPos = 0.0f;
for(int i = 0; i < line; i++)
{
SizeF lineSize = GetLineDimensions(i, graphics);
yPos += lineSize.Height;
StyleDefinition* styleDef = GetStyleDefinition(lines[i].style);
yPos += styleDef->verticalSpacing;
}
SizeF thisLineSize = GetLineDimensions(line, graphics);
StyleDefinition* thisStyleDef = GetStyleDefinition(lines[line].style);
PointF origin(thisStyleDef->indent, yPos + verticalPadding - currentScroll);
return RectF(origin, thisLineSize);
}
Region * AnnotatedTextControl::GetLineCharactersPosition(_In_ int line, _In_ int first, _In_ int length, _In_ Graphics *graphics)
{
if (line < 0 || line >= lineCount)
{
OutputDebugString(L"GetLineCharactersPosition: Bad line input\n");
return NULL;
}
int strLength = GetLineLength(line);
if (first < 0 || first > strLength)
{
OutputDebugString(L"GetLineCharactersPosition: Bad first char\n");
return NULL;
}
// If the length either goes past the end of the line or is -1 then set it to the end of the line
if (length == -1 || first + length > static_cast<int>(strLength))
{
length = strLength - first;
}
RectF boundingRect = GetLinePosition(line, graphics);
CharacterRange range(first, length);
StringFormat format;
format.SetMeasurableCharacterRanges(1, &range);
StyleDefinition* styleDef = GetStyleDefinition(lines[line].style);
Font font(textFontFace, styleDef->fontSize, styleDef->fontStyle);
// Get the number of ranges that have been set, and allocate memory to
// store the regions that correspond to the ranges.
Region *charRangeRegion = new Region;
if (charRangeRegion != NULL)
{
Status status = graphics->MeasureCharacterRanges(lines[line].text, -1, &font, boundingRect, &format, 1, charRangeRegion);
if (status == Ok)
{
return charRangeRegion;
}
delete charRangeRegion;
}
return NULL;
}
SizeF AnnotatedTextControl::GetAnnotationDimensions(_In_ unsigned int annotation, _In_ Graphics *graphics)
{
if (annotation >= annotationCount)
{
return SizeF();
}
RECT clientRect;
GetClientRect(hwnd, &clientRect);
Font font(annotationFontFace, annotationFontSize);
SizeF layoutSize(clientRect.right - GetTextAreaWidth() - annotationPadding * 2.0f, 1200.0f);
const StringFormat* stringFormat = StringFormat::GenericDefault();
SizeF annotationSize;
Status status = graphics->MeasureString(annotations[annotation].text, -1, &font, layoutSize, stringFormat, &annotationSize);
if (status != Ok)
{
annotationSize = SizeF();
}
return annotationSize;
}
RectF AnnotatedTextControl::GetAnnotationPosition(_In_ unsigned int annotation, _In_ Graphics *graphics)
{
if (annotation >= annotationCount)
{
return RectF();
}
float yPos = annotationPadding;
for(unsigned int i = 0; i < annotation; i++)
{
SizeF annotationSize = GetAnnotationDimensions(i, graphics);
yPos += annotationSize.Height;
yPos += (annotationPadding * 2.0f);
}
SizeF thisLineSize = GetAnnotationDimensions(annotation, graphics);
PointF origin(GetTextAreaWidth() + annotationPadding, yPos + verticalPadding - currentScroll);
return RectF(origin, thisLineSize);
}
int AnnotatedTextControl::GetLineCount()
{
return lineCount;
}
_Post_equal_to_(this->annotationCount)
unsigned int AnnotatedTextControl::GetAnnotationCount() const
{
return annotationCount;
}
TextLine *AnnotatedTextControl::GetLine(_In_ int line)
{
if (line < 0 || line >= lineCount)
{
return NULL;
}
return &lines[line];
}
_When_(annotation < this->annotationCount, _Post_equal_to_(&this->annotations[annotation]))
_When_(annotation >= this->annotationCount, _Ret_null_)
Annotation *AnnotatedTextControl::GetAnnotation(_In_ unsigned int annotation) const
{
if (annotation >= annotationCount)
{
return NULL;
}
return &annotations[annotation];
}
int AnnotatedTextControl::GetDesiredHeight()
{
Graphics graphics(hwnd);
RectF lastText = GetLinePosition(lineCount - 1, &graphics);
RectF lastAnnotation = GetAnnotationPosition(annotationCount - 1, &graphics);
return static_cast<int>(max(lastText.GetBottom(), lastAnnotation.GetBottom()) + currentScroll + verticalPadding);
}
EndPoint AnnotatedTextControl::SearchForClosestEndPoint(_In_ float x, _In_ float y)
{
Graphics graphics(hwnd);
// first find the closest line
int beforeLine = -1;
int inLine = -1;
for (int i = 0; i < lineCount; i++)
{
RectF lineRect = GetLinePosition(i, &graphics);
if (y < lineRect.Y)
{
beforeLine = i;
break;
}
else if (y < lineRect.GetBottom())
{
inLine = i;
break;
}
}
if (beforeLine >= 0)
{
// The point lies between two lines
// the position will be the start of the next line
EndPoint ret = {beforeLine, 0};
return ret;
}
else if (inLine >= 0)
{
// The point is in a specific line, now we need to iterate
// and find out between which two characters
int length = GetLineLength(inLine);
// First get all the bounding rects for the line
// Get the region, and convert it to an array of rectangles
RectF inRect;
Region *region = GetLineCharactersPosition(inLine, 0, length, &graphics);
if (region != NULL)
{
Matrix identity; // Identity is the default matrix
UINT scansCount = region->GetRegionScansCount(&identity);
RectF *regionRects = new RectF[scansCount];
if (regionRects != NULL)
{
int count;
if (region->GetRegionScans(&identity, regionRects, &count) == Ok && count > 0)
{
int beforeOrInRect = -1;
for (int i = 0; i < count; i++)
{
if (y < regionRects[i].GetBottom())
{
beforeOrInRect = i;
break;
}
}
// If it's past the end, it must be close to the end
if (beforeOrInRect < 0)
{
beforeOrInRect = count - 1;
}
inRect = regionRects[beforeOrInRect];
}
delete [] regionRects;
}
delete region;
}
if (inRect.IsEmptyArea())
{
EndPoint ret = {inLine, 0};
return ret;
}
// Essentially do a binary search through the line
int minSearch = 0;
int maxSearch = length;
int searchPoint = length / 2;
// As a binary search, this should take at most Log_2(n)
// so even for a really long (10000 character) string
// this should only take about 14 iterations, if it passes
// 20 it's likely an error
for (int i = 0; i < 20; i++)
{
int direction = 0;
region = GetLineCharactersPosition(inLine, searchPoint, 1, &graphics);
if (region != NULL)
{
RectF character;
if (region->GetBounds(&character, &graphics) == Ok)
{
// First check that we've found the right line
if (inRect.Contains(character))
{
if (x < character.X)
{
direction = -1;
}
else if (x > character.GetRight())
{
direction = 1;
}
}
else
{
if ( character.Y < inRect.Y )
{
direction = 1;
}
else
{
direction = -1;
}
}
}
delete region;
}
int distance = 0;
if (direction == 1)
{
minSearch = searchPoint;
distance = (maxSearch - searchPoint) / 2;
}
else if (direction == -1)
{
maxSearch = searchPoint;
distance = (searchPoint - minSearch) / 2;
}
else
{
// If we didn't move, either it errored out or
// we hit tested to a specific character. In either
// case, searchPoint is our final result
break;
}
distance = max(distance, 1);
distance = distance * direction;
int newPoint = searchPoint + distance;
// The point is between two characters, set it to the second one
// since the address is actually before a specific character
if (newPoint == maxSearch)
{
searchPoint = maxSearch;
break;
}
else if (newPoint == minSearch)
{
break;
}
if (newPoint < 0)
{
// The point is before the start
searchPoint = 0;
break;
}
if (newPoint >= length)
{
// The point is past the end
searchPoint = length;
break;
}
// We haven't found it yet,
// keep iterating
searchPoint = newPoint;
}
EndPoint ret = {inLine, searchPoint};
return ret;
}
else
{
return GetEnd();
}
}
EndPoint AnnotatedTextControl::GetEnd()
{
EndPoint ep = {lineCount - 1, 0 };
ep.character = GetLineLength(ep.line);
return ep;
}
int AnnotatedTextControl::GetLineLength(_In_ int line)
{
size_t strLength;
if (FAILED(StringCchLength(lines[line].text, 10000, &strLength)))
{
strLength = 0;
}
return static_cast<int>(strLength);
}
VARIANT AnnotatedTextControl::GetAttributeAtPoint(_In_ EndPoint start, _In_ TEXTATTRIBUTEID attribute)
{
VARIANT retval;
VariantInit(&retval);
// Many attributes are constant across the range, get them here
if ( attribute == UIA_AnimationStyleAttributeId )
{
retval.vt = VT_I4;
retval.lVal = AnimationStyle_None;
}
else if ( attribute == UIA_BackgroundColorAttributeId )
{
retval.vt = VT_I4;
retval.lVal = GetSysColor(COLOR_WINDOW);
}
else if ( attribute == UIA_BulletStyleAttributeId )
{
retval.vt = VT_I4;
retval.lVal = BulletStyle_None;
}
else if ( attribute == UIA_CapStyleAttributeId )
{
retval.vt = VT_I4;
retval.lVal = CapStyle_None;
}
else if ( attribute == UIA_CultureAttributeId )
{
retval.vt = VT_I4;
retval.lVal = GetThreadLocale();
}
else if ( attribute == UIA_FontNameAttributeId )
{
retval.bstrVal = SysAllocString(textFontFace);
if (retval.bstrVal != NULL)
{
retval.vt = VT_BSTR;
}
}
else if ( attribute == UIA_ForegroundColorAttributeId )
{
retval.vt = VT_I4;
retval.lVal = textColor.ToCOLORREF();
}
else if ( attribute == UIA_HorizontalTextAlignmentAttributeId )
{
retval.vt = VT_I4;
retval.lVal = HorizontalTextAlignment_Left;
}
else if ( attribute == UIA_IndentationTrailingAttributeId )
{
retval.vt = VT_R8;
retval.dblVal = 0.0;
}
else if ( attribute == UIA_IsHiddenAttributeId )
{
retval.vt = VT_BOOL;
retval.boolVal = VARIANT_FALSE;
}
else if ( attribute == UIA_IsReadOnlyAttributeId )
{
retval.vt = VT_BOOL;
retval.boolVal = VARIANT_TRUE;
}
else if ( attribute == UIA_IsSubscriptAttributeId )
{
retval.vt = VT_BOOL;
retval.boolVal = VARIANT_FALSE;
}
else if ( attribute == UIA_IsSuperscriptAttributeId )
{
retval.vt = VT_BOOL;
retval.boolVal = VARIANT_FALSE;
}
else if ( attribute == UIA_MarginBottomAttributeId )
{
retval.vt = VT_R8;
retval.dblVal = verticalPadding;
}
else if ( attribute == UIA_MarginLeadingAttributeId )
{
retval.vt = VT_R8;
retval.dblVal = 0.0;
}
else if ( attribute == UIA_MarginTopAttributeId )
{
retval.vt = VT_R8;
retval.dblVal = verticalPadding;
}
else if ( attribute == UIA_MarginTrailingAttributeId )
{
retval.vt = VT_R8;
retval.dblVal = 0.0;
}
else if ( attribute == UIA_OutlineStylesAttributeId )
{
retval.vt = VT_I4;
retval.lVal = OutlineStyles_None;
}
else if ( attribute == UIA_OverlineColorAttributeId )
{
if (SUCCEEDED(UiaGetReservedNotSupportedValue(&retval.punkVal)))
{
retval.vt = VT_UNKNOWN;
}
}
else if ( attribute == UIA_OverlineStyleAttributeId )
{
retval.vt = VT_I4;
retval.lVal = TextDecorationLineStyle_None;
}
else if ( attribute == UIA_StrikethroughColorAttributeId )
{
if (SUCCEEDED(UiaGetReservedNotSupportedValue(&retval.punkVal)))
{
retval.vt = VT_UNKNOWN;
}
}
else if ( attribute == UIA_StrikethroughStyleAttributeId )
{
retval.vt = VT_I4;
retval.lVal = TextDecorationLineStyle_None;
}
else if ( attribute == UIA_TabsAttributeId )
{
if (SUCCEEDED(UiaGetReservedNotSupportedValue(&retval.punkVal)))
{
retval.vt = VT_UNKNOWN;
}
}
else if ( attribute == UIA_UnderlineColorAttributeId )
{
retval.vt = VT_I4;
retval.lVal = textColor.ToCOLORREF();
}
else if ( attribute == UIA_TextFlowDirectionsAttributeId )
{
retval.vt = VT_I4;
retval.lVal = FlowDirections_RightToLeft;
}
else if ( attribute == UIA_LinkAttributeId )
{
if (SUCCEEDED(UiaGetReservedNotSupportedValue(&retval.punkVal)))
{
retval.vt = VT_UNKNOWN;
}
}
else if ( attribute == UIA_IsActiveAttributeId )
{
retval.vt = VT_BOOL;
retval.boolVal = isActive ? VARIANT_TRUE : VARIANT_FALSE;
}
else if ( attribute == UIA_SelectionActiveEndAttributeId )
{
retval.vt = VT_I4;
retval.lVal = ActiveEnd_None;
}
else if ( attribute == UIA_CaretPositionAttributeId )
{
retval.vt = VT_I4;
if (caretPosition.character == 0)
{
retval.lVal = CaretPosition_BeginningOfLine;
}
else if (caretPosition.character == GetLineLength(caretPosition.line))
{
retval.lVal = CaretPosition_EndOfLine;
}
else
{
retval.lVal = CaretPosition_Unknown;
}
}
else if ( attribute == UIA_CaretBidiModeAttributeId )
{
retval.vt = VT_I4;
retval.lVal = CaretBidiMode_LTR;
}
else if ( attribute == UIA_AnnotationTypesAttributeId ||
attribute == UIA_AnnotationObjectsAttributeId )
{
// Do nothing, we'll handle this at a different level
}
else
{
StyleDefinition* style = GetStyleDefinition(lines[start.line].style);
if ( attribute == UIA_FontSizeAttributeId )
{
retval.vt = VT_R8;
retval.dblVal = style->fontSize;
}
else if ( attribute == UIA_FontWeightAttributeId )
{
retval.vt = VT_I4;
retval.lVal = (style->fontStyle & FontStyleBold) == 0 ? 400 : 700;
}
else if ( attribute == UIA_IndentationFirstLineAttributeId ||
attribute == UIA_IndentationLeadingAttributeId )
{
retval.vt = VT_R8;
retval.dblVal = style->indent;
}
else if ( attribute == UIA_IsItalicAttributeId )
{
retval.vt = VT_BOOL;
retval.boolVal = (style->fontStyle & FontStyleItalic) == 0 ? VARIANT_FALSE : VARIANT_TRUE;
}
else if ( attribute == UIA_UnderlineStyleAttributeId )
{
retval.vt = VT_BOOL;
retval.boolVal = (style->fontStyle & FontStyleUnderline) == 0 ? VARIANT_FALSE : VARIANT_TRUE;
}
else if ( attribute == UIA_StyleNameAttributeId )
{
retval.bstrVal = SysAllocString(style->uiaStyleName);
if (retval.bstrVal != NULL)
{
retval.vt = VT_BSTR;
}
}
else if ( attribute == UIA_StyleIdAttributeId )
{
retval.vt = VT_I4;
retval.lVal = style->uiaStyleId;
}
}
return retval;
}
RectF AnnotatedTextControl::GetCaretScreenPosition()
{
Graphics graphics(hwnd);
bool endOfLine = false;
if (caretPosition.character >= GetLineLength(caretPosition.line))
{
endOfLine = true;
}
Region *region = GetLineCharactersPosition(caretPosition.line, caretPosition.character - (endOfLine ? 1 : 0), 1, &graphics);
if (region != NULL)
{
RectF caretRect;
if (region->GetBounds(&caretRect, &graphics) == Ok)
{
if (endOfLine)
{
caretRect.X += caretRect.Width;
}
caretRect.Width = 0.0f;
return caretRect;
}
}
return RectF();
}
void AnnotatedTextControl::UpdateCaret(_In_ EndPoint newPosition)
{
bool lineChange = (caretPosition.line != newPosition.line);
caretPosition = newPosition;
RectF caretScreenPos = GetCaretScreenPosition();
if (lineChange)
{
// If we change lines resize the caret to the appropriate new height for the line
DestroyCaret();
CreateCaret(hwnd, (HBITMAP) NULL, 0 /* Use the default width */, static_cast<int>(caretScreenPos.Height));
}
SetCaretPos(static_cast<int>(caretScreenPos.X), static_cast<int>(caretScreenPos.Y));
// Fire a UIA event to notify pure UIA clients of the Caret position change
NotifyCaretPositionChanged(hwnd, this);
if (lineChange)
{
ShowCaret(hwnd);
}
}
bool AnnotatedTextControl::StepCharacter(_In_ EndPoint start, _In_ bool forward, _Out_ EndPoint *end)
{
*end = start;
if (forward)
{
if (end->character >= GetLineLength(end->line))
{
if (end->line + 1 >= GetLineCount())
{
return false;
}
end->line++;
end->character = 0;
}
else
{
end->character++;
}
}
else
{
if (end->character <= 0)
{
if (end->line <= 0)
{
return false;
}
end->line--;
end->character = GetLineLength(end->line);
}
else
{
end->character--;
}
}
return true;
}
// This moves forward or backward by line. It target the end of lines, so if
// in the middle of a line, it moves to the end, and if it's at the end of a line
// it moves to the end of the next line.
// When moving backwards it still targets the end of the line, moving to the end of
// the previous line, except when on the first line, where it will move to the
// beginning of the line, as there isn't a previous line.
// This is done so whether we walk forward or backwards, there is a consistent
// span given, from the end of one line, to the end of the next
bool AnnotatedTextControl::StepLine(_In_ EndPoint start, _In_ bool forward, _Out_ EndPoint *end)
{
*end = start;
if (forward)
{
if (end->character >= GetLineLength(end->line))
{
if (end->line + 1 >= GetLineCount())
{
return false;
}
end->line++;
end->character = GetLineLength(end->line);
}
else
{
end->character = GetLineLength(end->line);
}
}
else
{
if (end->line <= 0)
{
if (end->character <= 0)
{
return false;
}
end->character = 0;
}
else
{
end->line--;
end->character = GetLineLength(end->line);
}
}
return true;
}
bool AnnotatedTextControl::IsActive()
{
return isActive;
}
EndPoint AnnotatedTextControl::GetCaretPosition()
{
return caretPosition;
}