Source code of Windows XP (NT5)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

2036 lines
51 KiB

/*++
Copyright (c) 1997 Microsoft Corporation
Module Name:
textview.c
Abstract:
The code in this module implements a simple text view control
that supports a few HTML-style tags. The caller sets the control
font with WM_SETFONT, then adds text line-by-line by sending
WMX_ADDLINE messages, and finally sends a WMX_GOTO to jump to
a specific bookmark.
WMX_ADDLINE
wParam: Specifies a pointer to a LINEATTRIBS struct, or
NULL if the last line's attributes are to be
used. (If no lines exist and wParam is NULL,
default attributes are used.)
lParam: Specifes a pointer to the text to add.
Return Value: The number of bytes of the lParam string
that are visible in the control.
The text can contain the following HTML-style tags:
<B> - Turns on BOLD
</B> - Turns off BOLD
<U> - Turns on UNDERLINE
</U> - Turns off UNDERLINE
<UL> - Indents text
</UL> - Unindents text
<HR> - A separator line
<A name=bookmark> - Specifies a bookmark (see WMX_GOTO)
<BR> - Adds a line break
Line-feed and carriage-return characters are interpreted as <BR>.
WMX_GOTO
wParam: Unused
lParam: Specifies a pointer to the string specifing the bookmark
to jump to.
Return Value: Always zero.
Author:
Jim Schmidt (jimschm) 28-Oct-1997
Revision History:
jimschm 23-Sep-1998 IDC_HAND collision fix (now IDC_OUR_HAND)
--*/
#include "pch.h"
#include "uip.h"
#include <shellapi.h>
UINT g_HangingIndentPixels;
PCTSTR g_HangingIndentString = TEXT(" ");
UINT g_FarEastFudgeFactor;
extern BOOL g_Terminated;
#define COMMAND_BOLD 1
#define COMMAND_BOLD_END 2
#define COMMAND_UNDERLINE 3
#define COMMAND_UNDERLINE_END 4
#define COMMAND_HORZ_RULE 5
#define COMMAND_ANCHOR 6
#define COMMAND_ANCHOR_END 7
#define COMMAND_INDENT 8
#define COMMAND_UNINDENT 9
#define COMMAND_LIST_ITEM 10
#define COMMAND_LIST_ITEM_END 11
#define COMMAND_LINE_BREAK 12
#define COMMAND_WHITESPACE 13
#define COMMAND_PARAGRAPH 14
#define COMMAND_ESCAPED_CHAR 15
typedef struct {
BYTE Command;
PCTSTR Text;
UINT TextLen;
} TAG, *PTAG;
TAG g_TagList[] = {
{COMMAND_BOLD, TEXT("B"), 0},
{COMMAND_BOLD_END, TEXT("/B"), 0},
{COMMAND_UNDERLINE, TEXT("U"), 0},
{COMMAND_UNDERLINE_END, TEXT("/U"), 0},
{COMMAND_HORZ_RULE, TEXT("HR"), 0},
{COMMAND_ANCHOR, TEXT("A"), 0},
{COMMAND_ANCHOR_END, TEXT("/A"), 0},
{COMMAND_INDENT, TEXT("UL"), 0},
{COMMAND_UNINDENT, TEXT("/UL"), 0},
{COMMAND_LIST_ITEM, TEXT("LI"), 0},
{COMMAND_LIST_ITEM_END, TEXT("/LI"), 0},
{COMMAND_LINE_BREAK, TEXT("BR"), 0},
{COMMAND_PARAGRAPH, TEXT("P"), 0},
{0, NULL, 0}
};
#define MAX_URL 4096
#define MAX_BOOKMARK 128
typedef struct {
RECT Rect;
TCHAR Url[MAX_URL];
TCHAR Bookmark[MAX_BOOKMARK];
} HOTLINK, *PHOTLINK;
PCTSTR
pParseHtmlArgs (
IN PCTSTR Start,
IN PCTSTR End
)
{
PTSTR MultiSz;
PCTSTR ArgStart;
PCTSTR ArgEnd;
PTSTR p;
BOOL Quotes;
UINT Size;
CHARTYPE ch;
Size = (End - Start) * 2;
MultiSz = AllocPathString (Size);
if (!MultiSz) {
return NULL;
}
p = MultiSz;
do {
while (*Start && _istspace (_tcsnextc (Start))) {
Start = _tcsinc (Start);
}
if (*Start == 0) {
break;
}
ArgStart = Start;
ArgEnd = ArgStart;
Quotes = FALSE;
while (*ArgEnd) {
if (_tcsnextc (ArgEnd) == TEXT('\"')) {
Quotes = !Quotes;
} else if (!Quotes) {
ch = _tcsnextc (ArgEnd);
if (_istspace (ch) || ch == TEXT('>')) {
break;
}
}
ArgEnd = _tcsinc (ArgEnd);
}
if (ArgEnd > ArgStart) {
StringCopyAB (p, ArgStart, ArgEnd);
p = GetEndOfString (p) + 1;
}
Start = ArgEnd;
} while (*Start && _tcsnextc (Start) != TEXT('>'));
*p = 0;
MYASSERT ((UINT) (p - MultiSz) < Size);
return MultiSz;
}
PCTSTR
pGetNextHtmlToken (
IN PCTSTR Arg,
OUT PTSTR Key,
IN PCTSTR Delimiters OPTIONAL
)
{
BOOL Quotes;
PCTSTR ArgStart, ArgEnd, ArgEndSpaceTrimmed;
PCTSTR ArgStartPlusOne;
PCTSTR Delim;
CHARTYPE ch;
ArgStart = SkipSpace (Arg);
ArgEnd = ArgStart;
ArgEndSpaceTrimmed = ArgEnd;
Quotes = FALSE;
while (*ArgEnd) {
ch = _tcsnextc (ArgEnd);
if (Delimiters) {
Delim = Delimiters;
while (*Delim) {
if (ch == _tcsnextc (Delim)) {
break;
}
Delim = _tcsinc (Delim);
}
if (*Delim) {
ch = 0;
}
}
if (ch == 0) {
break;
}
if (!_istspace (ch)) {
ArgEndSpaceTrimmed = ArgEnd;
}
ArgEnd = _tcsinc (ArgEnd);
}
//
// Copy arg, stripping surrounding quotes
//
ArgEndSpaceTrimmed = _tcsinc (ArgEndSpaceTrimmed);
if (_tcsnextc (Key) != TEXT('\"')) {
StringCopyAB (Key, ArgStart, ArgEndSpaceTrimmed);
} else {
ArgEndSpaceTrimmed = _tcsdec2 (ArgStart, ArgEndSpaceTrimmed);
if (!ArgEndSpaceTrimmed) {
ArgEndSpaceTrimmed = ArgStart;
} else if (_tcsnextc (ArgEndSpaceTrimmed) != TEXT('\"')) {
ArgEndSpaceTrimmed = _tcsinc (ArgEndSpaceTrimmed);
}
ArgStartPlusOne = _tcsinc (ArgStart);
StringCopyAB (Key, ArgStartPlusOne, ArgEndSpaceTrimmed);
}
if (*ArgEnd) {
ArgEnd = _tcsinc (ArgEnd);
}
return ArgEnd;
}
VOID
pGetHtmlKeyAndValue (
IN PCTSTR Arg,
OUT PTSTR Key,
OUT PTSTR Value
)
{
PTSTR p;
Arg = pGetNextHtmlToken (Arg, Key, TEXT("="));
MYASSERT (Arg);
if (_tcsnextc (Arg) == TEXT('\"')) {
//
// De-quote the arg
//
StringCopy (Value, _tcsinc (Arg));
p = _tcsrchr (Value, 0);
p = _tcsdec2 (Value, p);
if (p && _tcsnextc (p) == TEXT('\"')) {
*p = 0;
}
} else {
StringCopy (Value, Arg);
}
}
PCTSTR
pGetHtmlCommandOrChar (
IN PCTSTR String,
OUT CHARTYPE *Command,
OUT CHARTYPE *Char,
OUT PTSTR *CommandArg OPTIONAL
)
/*++
Routine Description:
pGetHtmlCommandOrChar parses HTML text is String, returing the HTML tag
command, printable character, pointer to the next token, HTML tag arguments
and a pointer to the end of the current token.
Arguments:
String - Specifies the string containing HTML
Command - Receives the HTML tag command (COMMAND_* constant)
or zero if the next token is not a tag
Char - Receives the next printable character, or zero if the next token is
not a printable character. Whitespace is both a command and a
printable character. The rest are either one or the other.
CommandArg - Receives a multi-sz of command arguments
Return Value:
A pointer to the first non-whitespace character of the next token, or
NULL if no more tokens exist.
--*/
{
CHARTYPE ch;
PCTSTR p, q;
BOOL Quoted;
CHARTYPE ch2;
INT i;
PCTSTR semicolon;
if (CommandArg) {
*CommandArg = NULL;
}
ch = _tcsnextc (String);
*Command = 0;
if (ch == 0) {
i = 0;
}
if (_istspace (ch)) {
*Command = COMMAND_WHITESPACE;
ch = TEXT(' ');
} else if (ch == TEXT('&')) {
//
// Convert HTML character escapes:
//
// &lt; &gt; &amp; &quot; &apos; &nbsp;
//
semicolon = _tcschr (String + 1, TEXT(';'));
if (semicolon) {
*Char = 0;
if (StringMatchAB (TEXT("lt"), String + 1, semicolon)) {
*Char = TEXT('<');
} else if (StringMatchAB (TEXT("gt"), String + 1, semicolon)) {
*Char = TEXT('>');
} else if (StringMatchAB (TEXT("amp"), String + 1, semicolon)) {
*Char = TEXT('&');
} else if (StringMatchAB (TEXT("quot"), String + 1, semicolon)) {
*Char = TEXT('\"');
} else if (StringMatchAB (TEXT("apos"), String + 1, semicolon)) {
*Char = TEXT('\'');
} else if (StringMatchAB (TEXT("nbsp"), String + 1, semicolon)) {
*Char = TEXT(' ');
}
if (*Char) {
*Command = COMMAND_ESCAPED_CHAR;
return _tcsinc (semicolon);
}
}
} else if (ch == TEXT('<')) {
p = SkipSpace (_tcsinc (String));
ch = 0;
if (*p) {
q = NULL;
for (i = 0 ; g_TagList[i].Text ; i++) {
if (StringIMatchTcharCount (g_TagList[i].Text, p, g_TagList[i].TextLen)) {
q = p + g_TagList[i].TextLen;
ch2 = _tcsnextc (q);
if (!_istspace (ch2) && ch2 != TEXT('>')) {
continue;
}
Quoted = FALSE;
while (*q) {
ch2 = _tcsnextc (q);
if (ch2 == TEXT('\"')) {
Quoted = !Quoted;
} else if (!Quoted) {
if (ch2 == TEXT('>')) {
break;
}
}
q = _tcsinc (q);
}
if (*q) {
p += g_TagList[i].TextLen;
p = SkipSpace (p);
break;
}
}
}
if (!g_TagList[i].Text) {
//
// Ignore unsupported tag
//
p = _tcschr (String, TEXT('>'));
if (!p) {
p = GetEndOfString (String);
}
} else {
//
// Found a tag
//
String = p;
*Command = g_TagList[i].Command;
p = q;
MYASSERT (p);
if (CommandArg && String < p) {
*CommandArg = (PTSTR) pParseHtmlArgs (String, p);
}
}
}
String = p;
}
*Char = ch;
//
// Skip block of spaces
//
if (_istspace (ch)) {
do {
String = _tcsinc (String);
} while (_istspace (_tcsnextc (String)));
return *String ? String : NULL;
} else if (*String) {
return _tcsinc (String);
}
return NULL;
}
PCTSTR
pStripFormatting (
PCTSTR String
)
{
PTSTR NewString;
PCTSTR Start, p, q;
PTSTR d;
CHARTYPE Char, Command;
UINT tchars;
//
// Duplicate String, removing the <B> and <U> commands
//
NewString = AllocPathString (SizeOfString (String));
Start = String;
d = NewString;
p = Start;
do {
q = pGetHtmlCommandOrChar (p, &Command, &Char, NULL);
if (!Char || Command == COMMAND_WHITESPACE || Command == COMMAND_ESCAPED_CHAR) {
if (Start < p) {
tchars = p - Start;
CopyMemory (d, Start, tchars * sizeof (TCHAR));
d += tchars;
}
if (Char) {
*d++ = (TCHAR) Char;
}
Start = q;
}
p = q;
} while (p);
*d = 0;
return NewString;
}
VOID
pCreateFontsIfNecessary (
HDC hdc,
HFONT BaseFont,
PTEXTMETRIC ptm,
HFONT *hFontNormal,
HFONT *hFontBold,
HFONT *hFontUnderlined
)
{
LOGFONT lf;
TCHAR FaceName[LF_FACESIZE];
SIZE Extent;
if (!BaseFont) {
BaseFont = GetStockObject (DEFAULT_GUI_FONT);
}
if (BaseFont) {
SelectObject (hdc, BaseFont);
}
GetTextMetrics (hdc, ptm);
if (!(*hFontNormal)) {
ZeroMemory (&lf, sizeof (lf));
GetTextFace (hdc, LF_FACESIZE, FaceName);
GetObject (BaseFont, sizeof (lf), &lf);
lf.lfHeight = ptm->tmHeight;
lf.lfWeight = FW_NORMAL;
StringCopy (lf.lfFaceName, FaceName);
*hFontNormal = CreateFontIndirect (&lf);
lf.lfWeight = FW_BOLD;
*hFontBold = CreateFontIndirect (&lf);
lf.lfWeight = FW_NORMAL;
lf.lfUnderline = 1;
*hFontUnderlined = CreateFontIndirect (&lf);
SelectObject (hdc, *hFontNormal);
GetTextExtentPoint32 (hdc, g_HangingIndentString, TcharCount (g_HangingIndentString), &Extent);
g_HangingIndentPixels = Extent.cx;
if (GetACP() == 932) {
g_FarEastFudgeFactor = ptm->tmAveCharWidth;
} else {
g_FarEastFudgeFactor = 0;
}
}
}
INT
pComputeCharBytes (
IN CHARTYPE Char
)
{
if ((WORD) Char > 255) {
return 2;
}
return 1;
}
typedef struct {
BOOL InListItem;
INT InUnorderedList;
BOOL NextLineBlank;
BOOL LastWasBlankLine;
} PARSESTATE, *PPARSESTATE;
PCTSTR
pFindFirstCharThatDoesNotFit (
IN INT Margin,
IN PGROWLIST ListPtr,
IN PGROWBUFFER AttribsList,
IN HASHTABLE BookmarkTable,
IN HWND hwnd,
IN HFONT hFontNormal,
IN HFONT hFontBold,
IN HFONT hFontUnderlined,
IN PCTSTR p,
IN INT x,
OUT PTEXTMETRIC ptm,
OUT PRECT prect,
IN OUT PLINEATTRIBS LineAttribs,
IN OUT PPARSESTATE ParseState
)
{
HDC hdc;
PCTSTR endOfLine;
PCTSTR wordWrapBreakPos;
PCTSTR q;
PCTSTR Arg;
CHARTYPE Char, Command;
SIZE Extent;
UINT Size;
PLINEATTRIBS PrevLineAttribs;
SCROLLINFO si;
PCTSTR FirstChar;
PTSTR CommandBuf;
PTSTR AnchorKey;
PTSTR AnchorVal;
UINT Count;
INT ScrollBarPixels;
BOOL PrevCharMb = FALSE;
BOOL printableFound = FALSE;
BOOL newLine;
BOOL lastWasBlankLine;
TCHAR oneChar;
UINT prevCommand = 0;
if (ParseState->NextLineBlank) {
ParseState->LastWasBlankLine = TRUE;
ParseState->NextLineBlank = FALSE;
return p;
}
lastWasBlankLine = ParseState->LastWasBlankLine;
ParseState->LastWasBlankLine = FALSE;
//
// Get display DC and initialize metrics
//
hdc = CreateDC (TEXT("display"), NULL, NULL, NULL);
ParseState->NextLineBlank = FALSE;
//
// Select font of previous line
//
Size = GrowListGetSize (ListPtr);
if (!Size) {
if (hdc) {
SelectObject (hdc, hFontNormal);
}
LineAttribs->LastCharAttribs = ATTRIB_NORMAL;
} else {
PrevLineAttribs = (PLINEATTRIBS) AttribsList->Buf + (Size - 1);
LineAttribs->LastCharAttribs = PrevLineAttribs->LastCharAttribs;
if (hdc) {
if (PrevLineAttribs->LastCharAttribs == ATTRIB_BOLD) {
SelectObject (hdc, hFontBold);
} else if (PrevLineAttribs->LastCharAttribs == ATTRIB_UNDERLINED) {
SelectObject (hdc, hFontUnderlined);
} else {
SelectObject (hdc, hFontNormal);
}
}
}
//
// Get metrics
//
ZeroMemory (&si, sizeof (si));
si.cbSize = sizeof (si);
si.fMask = SIF_ALL;
GetScrollInfo (hwnd, SB_VERT, &si);
GetTextMetrics (hdc, ptm);
GetClientRect (hwnd, prect);
//
// Account for scroll bar and right margin if scroll bar is
// not yet visible
//
if (si.nMax < (INT) si.nPage || !si.nPage) {
ScrollBarPixels = GetSystemMetrics (SM_CXVSCROLL);
if (prect->right - prect->left > ScrollBarPixels * 2) {
prect->right -= ScrollBarPixels;
}
}
if (prect->right - prect->left > Margin * 2) {
prect->right -= Margin;
}
//
// Adjust if this is a far east system, because GetTextExtent doesn't
// return the correct number of pixels.
//
prect->right -= g_FarEastFudgeFactor;
//
// Count characters until line is completely processed
//
MYASSERT (!_istspace (_tcsnextc (p)));
FirstChar = p;
endOfLine = p;
wordWrapBreakPos = NULL;
while (p && (INT) x < prect->right) {
q = pGetHtmlCommandOrChar (
p, // current string position
&Command,
&Char,
&CommandBuf
);
if (prevCommand == COMMAND_UNINDENT &&
Command != COMMAND_UNINDENT &&
Command != COMMAND_INDENT &&
(Command != COMMAND_WHITESPACE || printableFound) &&
Command != COMMAND_LINE_BREAK &&
Command != COMMAND_PARAGRAPH &&
!ParseState->InUnorderedList &&
!lastWasBlankLine
) {
Command = COMMAND_LINE_BREAK;
q = p;
Char = 0;
}
if (!Char) {
if (q != p) {
if (prevCommand == COMMAND_UNINDENT &&
Command == COMMAND_INDENT &&
!ParseState->InUnorderedList &&
!lastWasBlankLine
) {
Command = COMMAND_LINE_BREAK;
q = p;
} else if (ParseState->InListItem) {
MYASSERT (ParseState->InUnorderedList);
switch (Command) {
case COMMAND_PARAGRAPH:
case COMMAND_LINE_BREAK:
case COMMAND_UNINDENT:
case COMMAND_LIST_ITEM:
//
// Terminate the list item
//
Command = COMMAND_LIST_ITEM_END;
q = p;
break;
}
} else if (Command == COMMAND_INDENT) {
//
// Before indenting, start a new line
//
if (printableFound) {
Command = COMMAND_LINE_BREAK;
q = p;
}
} else if (Command == COMMAND_UNINDENT) {
//
// Before unindenting, complete the current line
//
if (printableFound) {
Command = COMMAND_LINE_BREAK;
q = p;
}
}
}
//
// Process tag
//
newLine = FALSE;
switch (Command) {
case 0:
break;
case COMMAND_BOLD:
if (hdc) {
SelectObject (hdc, hFontBold);
}
LineAttribs->LastCharAttribs = ATTRIB_BOLD;
break;
case COMMAND_UNDERLINE:
if (hdc) {
SelectObject (hdc, hFontUnderlined);
}
LineAttribs->LastCharAttribs = ATTRIB_UNDERLINED;
break;
case COMMAND_PARAGRAPH:
ParseState->NextLineBlank = TRUE;
newLine = TRUE;
break;
case COMMAND_LINE_BREAK:
case COMMAND_HORZ_RULE:
newLine = TRUE;
break;
case COMMAND_INDENT:
ParseState->InUnorderedList += 1;
if ((INT) (LineAttribs->Indent) < ptm->tmHeight * 20) {
LineAttribs->Indent += ptm->tmHeight * 2;
x += ptm->tmHeight * 2;
}
break;
case COMMAND_LIST_ITEM:
if (ParseState->InUnorderedList) {
LineAttribs->HangingIndent += g_HangingIndentPixels;
ParseState->InListItem = TRUE;
}
break;
case COMMAND_LIST_ITEM_END:
if (ParseState->InUnorderedList) {
LineAttribs->HangingIndent -= g_HangingIndentPixels;
ParseState->InListItem = FALSE;
newLine = TRUE;
}
break;
case COMMAND_UNINDENT:
if (ParseState->InUnorderedList) {
MYASSERT (!(ParseState->InListItem));
ParseState->InUnorderedList -= 1;
if ((INT) (LineAttribs->Indent) > ptm->tmHeight * 2) {
LineAttribs->Indent -= ptm->tmHeight * 2;
x -= ptm->tmHeight * 2;
}
//if (!lastWasBlankLine || printableFound) {
// newLine = TRUE;
//}
}
break;
case COMMAND_ANCHOR:
if (CommandBuf) {
Arg = CommandBuf;
while (*Arg) {
//
// Search for a NAME arg
//
Count = TcharCount (Arg);
AnchorKey = AllocText (Count);
AnchorVal = AllocText (Count);
if (!AnchorKey || !AnchorVal) {
FreeText (AnchorKey);
FreeText (AnchorVal);
break;
}
pGetHtmlKeyAndValue (Arg, AnchorKey, AnchorVal);
if (StringIMatch (TEXT("NAME"), AnchorKey)) {
HtAddStringAndData (BookmarkTable, AnchorVal, &Size);
} else if (StringIMatch (TEXT("HREF"), AnchorKey)) {
LineAttribs->AnchorWrap = TRUE;
}
FreeText (AnchorKey);
FreeText (AnchorVal);
Arg += Count + 1;
}
}
break;
case COMMAND_ANCHOR_END:
LineAttribs->AnchorWrap = FALSE;
break;
case COMMAND_BOLD_END:
case COMMAND_UNDERLINE_END:
if (hdc) {
SelectObject (hdc, hFontNormal);
}
LineAttribs->LastCharAttribs = ATTRIB_NORMAL;
break;
}
FreePathString (CommandBuf);
prevCommand = Command;
if (newLine) {
if (q) {
endOfLine = q;
} else {
endOfLine = GetEndOfString (p);
}
wordWrapBreakPos = NULL;
break;
}
} else {
//
// Char is non-zero, so display it
//
if (printableFound || Command != COMMAND_WHITESPACE) {
printableFound = TRUE;
prevCommand = Command;
if (Command) {
oneChar = (TCHAR) Char;
GetTextExtentPoint32 (hdc, &oneChar, 1, &Extent);
} else {
GetTextExtentPoint32 (hdc, p, pComputeCharBytes (Char), &Extent);
}
x += Extent.cx;
if (_istspace (Char)) {
wordWrapBreakPos = q;
} else if (IsLeadByte (*q)) {
if (PrevCharMb && !IsPunct (_tcsnextc (q))) {
wordWrapBreakPos = q;
}
PrevCharMb = TRUE;
} else {
PrevCharMb = FALSE;
}
}
}
if (q) {
endOfLine = q;
} else {
endOfLine = GetEndOfString (endOfLine);
wordWrapBreakPos = NULL;
}
p = q;
}
DeleteDC (hdc);
if (wordWrapBreakPos) {
return wordWrapBreakPos;
}
return endOfLine;
}
VOID
pRegisterHotLink (
IN PGROWBUFFER HotLinkArray,
IN PRECT HotRect,
IN PCTSTR UrlLink,
IN PCTSTR BookmarkLink
)
{
PHOTLINK HotLink;
HotLink = (PHOTLINK) GrowBuffer (HotLinkArray, sizeof (HOTLINK));
if (!HotLink) {
return;
}
HotLink->Rect = *HotRect;
if (UrlLink) {
StringCopy (HotLink->Url, UrlLink);
} else {
HotLink->Url[0] = 0;
}
if (BookmarkLink) {
StringCopy (HotLink->Bookmark, BookmarkLink);
} else {
HotLink->Bookmark[0] = 0;
}
}
PHOTLINK
pFindHotLink (
IN PGROWBUFFER HotLinkArray,
IN UINT x,
IN UINT y
)
{
PHOTLINK HotLink;
UINT u;
HotLink = (PHOTLINK) HotLinkArray->Buf;
for (u = 0 ; u < HotLinkArray->End ; u += sizeof (HOTLINK)) {
if (x >= (UINT) HotLink->Rect.left && x < (UINT) HotLink->Rect.right &&
y >= (UINT) HotLink->Rect.top && y < (UINT) HotLink->Rect.bottom
) {
break;
}
HotLink++;
}
if (u >= HotLinkArray->End) {
HotLink = NULL;
}
return HotLink;
}
BOOL
pLaunchedAddRemovePrograms (
IN PCTSTR CmdLine
)
{
return CmdLine && _tcsstr (CmdLine, TEXT("appwiz.cpl"));
}
typedef struct {
HASHTABLE BookmarkTable;
UINT LineHeight;
HFONT hFont;
HFONT hFontNormal;
HFONT hFontBold;
HFONT hFontUnderlined;
GROWLIST List;
GROWBUFFER AttribsList;
GROWBUFFER HotLinkArray;
BOOL UrlEnabled;
INT Margin;
PARSESTATE ParseState;
} TEXTVIEW_STATE, *PTEXTVIEW_STATE;
LRESULT
CALLBACK
TextViewProc (
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
PAINTSTRUCT ps;
HDC hdc;
TEXTMETRIC tm;
SCROLLINFO si;
RECT rect;
RECT FillRect;
UINT Pos;
UINT End;
INT x, y;
INT i;
PCTSTR TrueStart, Start, Last;
PCTSTR p, q;
UINT Tchars;
CHARTYPE Char, Command;
UINT PrevHangingIndent;
SIZE Extent;
PLINEATTRIBS LineAttribs;
PLINEATTRIBS PrevLineAttribs;
HPEN OldPen;
HBRUSH FillBrush;
HPEN ShadowPen;
HPEN HighlightPen;
PTEXTVIEW_STATE s;
UINT LineHeight;
PHOTLINK HotLink;
BOOL Hot;
RECT HotRect;
PCTSTR UrlLink;
PCTSTR BookmarkLink;
PTSTR CommandBuf;
PCTSTR Arg;
TCHAR Href[MAX_URL];
PTSTR AnchorKey;
PTSTR AnchorVal;
UINT Count;
HKEY TempKey;
DWORD GrowListSize;
PCTSTR ShellArgs;
LONG l;
BOOL b;
PTSTR text;
UINT textSize;
BOOL printableFound;
TCHAR oneChar;
s = (PTEXTVIEW_STATE) GetWindowLong (hwnd, GWL_USERDATA);
if (s) {
LineHeight = s->LineHeight;
} else {
LineHeight = 0;
}
switch (uMsg) {
case WM_CREATE:
for (i = 0 ; g_TagList[i].Text ; i++) {
g_TagList[i].TextLen = TcharCount (g_TagList[i].Text);
}
s = (PTEXTVIEW_STATE) MemAlloc (g_hHeap, HEAP_ZERO_MEMORY, sizeof (TEXTVIEW_STATE));
SetWindowLong (hwnd, GWL_USERDATA, (LONG) s);
TempKey = OpenRegKeyStr (TEXT("HKCR\\.URL"));
if (TempKey) {
CloseRegKey (TempKey);
}
s->UrlEnabled = (TempKey != NULL);
ZeroMemory (&si, sizeof (si));
si.fMask = SIF_RANGE;
s->BookmarkTable = HtAllocWithData (sizeof (DWORD));
if (!s->BookmarkTable) {
return -1;
}
// WM_SETFONT does a bunch of work, including populating the text
SendMessage (
hwnd,
WM_SETFONT,
SendMessage (GetParent (hwnd), WM_GETFONT, 0, 0),
0
);
return 0;
case WM_GETDLGCODE:
return DLGC_WANTARROWS;
case WMX_GOTO:
//
// Determine if text is in bookmark table
//
if (!lParam) {
return 0;
}
if (HtFindStringAndData (s->BookmarkTable, (PCTSTR) lParam, &Pos)) {
PostMessage (
hwnd,
WM_VSCROLL,
MAKELPARAM (SB_THUMBPOSITION, (WORD) Pos),
(LPARAM) hwnd
);
}
return 0;
case WMX_ADDLINE:
//
// Init
//
l = GetWindowLong (hwnd, GWL_STYLE) & WS_BORDER;
if (!l) {
l = GetWindowLong (hwnd, GWL_EXSTYLE) & (WS_EX_DLGMODALFRAME|WS_EX_WINDOWEDGE|WS_EX_CLIENTEDGE|WS_EX_STATICEDGE);
}
if (!s->Margin && l) {
s->Margin = s->LineHeight / 2;
}
Start = (PCTSTR) lParam;
// ignore leading space
TrueStart = Start;
while (_istspace (_tcsnextc (Start))) {
Start = _tcsinc (Start);
}
PrevLineAttribs = NULL;
if (s->AttribsList.End) {
MYASSERT (s->AttribsList.End >= sizeof (LINEATTRIBS));
PrevLineAttribs = (PLINEATTRIBS) (s->AttribsList.Buf +
s->AttribsList.End -
sizeof (LINEATTRIBS)
);
}
//
// Copy line attributes (optional) to our Attribs list; ignore errors.
//
LineAttribs = (PLINEATTRIBS) GrowBuffer (&s->AttribsList, sizeof (LINEATTRIBS));
if (!LineAttribs) {
return 0;
}
//
// Copy previous line's attributes
//
ZeroMemory (LineAttribs, sizeof (LINEATTRIBS));
if (PrevLineAttribs) {
LineAttribs->AnchorWrap = PrevLineAttribs->AnchorWrap;
LineAttribs->Indent = PrevLineAttribs->Indent;
LineAttribs->HangingIndent = PrevLineAttribs->HangingIndent;
PrevHangingIndent = PrevLineAttribs->HangingIndent;
} else {
LineAttribs->Indent = s->Margin;
LineAttribs->HangingIndent = 0;
PrevHangingIndent = 0;
}
x = LineAttribs->Indent + PrevHangingIndent;
//
// Find the first character that does not fit on the line
//
Last = pFindFirstCharThatDoesNotFit (
s->Margin,
&s->List,
&s->AttribsList,
s->BookmarkTable,
hwnd,
s->hFontNormal,
s->hFontBold,
s->hFontUnderlined,
Start,
(INT) x,
&tm,
&rect,
LineAttribs,
&s->ParseState
);
//
// Update the vertical scroll bar
//
MYASSERT (LineHeight);
ZeroMemory (&si, sizeof (si));
si.cbSize = sizeof (si);
si.fMask = SIF_RANGE|SIF_PAGE;
si.nMin = 0;
si.nPage = rect.bottom / LineHeight;
si.nMax = GrowListGetSize (&s->List);
SetScrollInfo (hwnd, SB_VERT, &si, TRUE);
//
// Copy the string (or as much that is visible) to our
// grow list
//
GrowListAppendStringAB (&s->List, Start, Last);
//
// Return the number of bytes copied
//
return Last - TrueStart;
case WMX_ALL_LINES_PAINTED:
//
// Scan all the lines, returning 0 if at least one has Painted == FALSE
//
LineAttribs = (PLINEATTRIBS) s->AttribsList.Buf;
if (!LineAttribs) {
return 1;
}
for (Pos = 0 ; Pos < s->AttribsList.End ; Pos += sizeof (LINEATTRIBS)) {
LineAttribs = (PLINEATTRIBS) (s->AttribsList.Buf + Pos);
if (!LineAttribs->Painted) {
return 0;
}
}
return 1;
case WM_ERASEBKGND:
return 0;
case WM_KEYDOWN:
switch (wParam) {
case VK_DOWN:
PostMessage (hwnd, WM_VSCROLL, SB_LINEDOWN, 0);
return 0;
case VK_UP:
PostMessage (hwnd, WM_VSCROLL, SB_LINEUP, 0);
return 0;
case VK_NEXT:
PostMessage (hwnd, WM_VSCROLL, SB_PAGEDOWN, 0);
return 0;
case VK_PRIOR:
PostMessage (hwnd, WM_VSCROLL, SB_PAGEUP, 0);
return 0;
case VK_HOME:
PostMessage (hwnd, WM_VSCROLL, SB_TOP, 0);
return 0;
case VK_END:
PostMessage (hwnd, WM_VSCROLL, SB_BOTTOM, 0);
return 0;
}
break;
case WM_SETFONT:
s->hFont = (HFONT) wParam;
if (s->hFontNormal) {
DeleteObject (s->hFontNormal);
s->hFontNormal = NULL;
}
if (s->hFontBold) {
DeleteObject (s->hFontBold);
s->hFontBold = NULL;
}
if (s->hFontUnderlined) {
DeleteObject (s->hFontUnderlined);
s->hFontUnderlined = NULL;
}
hdc = CreateDC (TEXT("display"), NULL, NULL, NULL);
if (hdc) {
pCreateFontsIfNecessary (hdc, s->hFont, &tm, &s->hFontNormal, &s->hFontBold, &s->hFontUnderlined);
s->LineHeight = tm.tmHeight;
DeleteDC (hdc);
} else {
s->LineHeight = 0;
}
if (lParam) {
InvalidateRect (hwnd, NULL, FALSE);
}
if (s->AttribsList.End == 0) {
textSize = GetWindowTextLength (hwnd);
text = (PTSTR) MemAllocUninit ((textSize + 1) * sizeof (TCHAR));
GetWindowText (hwnd, text, textSize + 1);
AddStringToTextView (hwnd, text);
FreeMem (text);
}
return 0;
case WM_MOUSEMOVE:
//
// Search array of hit test rectangles
//
x = LOWORD(lParam);
y = HIWORD(lParam);
HotLink = pFindHotLink (&s->HotLinkArray, x, y);
if (HotLink) {
SetCursor (LoadCursor (g_hInst, MAKEINTRESOURCE (IDC_OUR_HAND)));
} else {
SetCursor (LoadCursor (NULL, MAKEINTRESOURCE (IDC_ARROW)));
}
break;
case WM_LBUTTONDOWN:
//
// Search array of hit test rectangles
//
x = LOWORD(lParam);
y = HIWORD(lParam);
HotLink = pFindHotLink (&s->HotLinkArray, x, y);
if (HotLink) {
if (HotLink->Url[0]) {
if (!StringIMatchCharCount (TEXT("file:"), HotLink->Url, 5)) {
ShellExecute (hwnd, TEXT("open"), HotLink->Url, NULL, NULL, 0);
} else {
ShellArgs = &HotLink->Url[5];
if (*ShellArgs == TEXT('/')) {
ShellArgs++;
}
if (*ShellArgs == TEXT('/')) {
ShellArgs++;
}
ShellArgs = ExtractArgZero (ShellArgs, Href);
//
// if we're launching the Add/Remove programs applet,
// warn users they'll have to restart setup (RAID # 293357)
//
b = TRUE;
if (!UNATTENDED() &&
!REPORTONLY() &&
!g_UIQuitSetup &&
pLaunchedAddRemovePrograms (ShellArgs)
) {
if (IDYES == ResourceMessageBox (
hwnd,
MSG_RESTART_IF_CONTINUE_APPWIZCPL,
MB_YESNO | MB_ICONQUESTION,
NULL
)) {
LOG ((LOG_INFORMATION, "User launched Add/Remove Programs applet; setup will terminate"));
PostMessage (GetParent (hwnd), WMX_RESTART_SETUP, FALSE, TRUE);
} else {
b = FALSE;
}
}
if (b) {
ShellExecute (hwnd, TEXT("open"), Href, ShellArgs, NULL, 0);
}
}
} else if (HotLink->Bookmark) {
SendMessage (hwnd, WMX_GOTO, 0, (LPARAM) HotLink->Bookmark);
}
}
break;
case WM_SETTEXT:
SendMessage (hwnd, WMX_CLEANUP, 0, 0);
DefWindowProc (hwnd, uMsg, wParam, lParam);
SendMessage (hwnd, WM_CREATE, 0, 0);
return 0;
case WM_PAINT:
FillBrush = CreateSolidBrush (GetSysColor (COLOR_BTNFACE));
ShadowPen = CreatePen (PS_SOLID, 1, GetSysColor (COLOR_3DSHADOW));
HighlightPen = CreatePen (PS_SOLID, 1, GetSysColor (COLOR_3DHILIGHT));
hdc = BeginPaint (hwnd, &ps);
pCreateFontsIfNecessary (hdc, s->hFont, &tm, &s->hFontNormal, &s->hFontBold, &s->hFontUnderlined);
s->LineHeight = LineHeight = tm.tmHeight;
GetClientRect (hwnd, &rect);
//
// Select colors
//
SetBkColor (hdc, GetSysColor (COLOR_BTNFACE));
SetTextColor (hdc, GetSysColor (COLOR_WINDOWTEXT));
if (FillBrush) {
SelectObject (hdc, FillBrush);
}
SelectObject (hdc, GetStockObject (NULL_PEN));
//
// Get scroll position
//
ZeroMemory (&si, sizeof (si));
si.cbSize = sizeof (si);
si.fMask = SIF_PAGE|SIF_POS;
GetScrollInfo (hwnd, SB_VERT, &si);
End = (UINT) si.nPos + si.nPage + 1;
GrowListSize = GrowListGetSize (&s->List);
End = min (End, GrowListSize);
y = 0;
//
// Select font of previous line. Move up if anchor wrap is on.
//
if (si.nPos) {
do {
LineAttribs = (PLINEATTRIBS) s->AttribsList.Buf + (si.nPos - 1);
if (LineAttribs->AnchorWrap) {
si.nPos--;
y -= LineHeight;
} else {
break;
}
} while (si.nPos > 0);
}
if (si.nPos) {
LineAttribs = (PLINEATTRIBS) s->AttribsList.Buf + (si.nPos - 1);
if (LineAttribs->LastCharAttribs == ATTRIB_BOLD) {
SelectObject (hdc, s->hFontBold);
} else if (LineAttribs->LastCharAttribs == ATTRIB_UNDERLINED) {
SelectObject (hdc, s->hFontUnderlined);
} else {
SelectObject (hdc, s->hFontNormal);
}
} else {
SelectObject (hdc, s->hFontNormal);
}
//
// Paint!
//
Hot = FALSE;
s->HotLinkArray.End = 0;
BookmarkLink = UrlLink = NULL;
for (Pos = (UINT) si.nPos ; Pos < End ; Pos++) {
p = GrowListGetString (&s->List, Pos);
printableFound = FALSE;
LineAttribs = (PLINEATTRIBS) s->AttribsList.Buf + Pos;
//
// Compute hanging indent using previous line
//
if (Pos > 0) {
PrevLineAttribs = (PLINEATTRIBS) ((PLINEATTRIBS) s->AttribsList.Buf + Pos - 1);
PrevHangingIndent = PrevLineAttribs->HangingIndent;
} else {
PrevHangingIndent = 0;
}
//
// Compute starting index
//
if (LineAttribs) {
x = LineAttribs->Indent + PrevHangingIndent;
LineAttribs->Painted = TRUE;
} else {
x = s->Margin;
}
//
// Compute blank area
//
if (x > 0) {
FillRect.left = 0;
FillRect.right = x;
FillRect.top = y;
FillRect.bottom = y + LineHeight;
Rectangle (hdc, FillRect.left, FillRect.top, FillRect.right + 1, FillRect.bottom + 1);
}
//
// Multiline hotlink
//
if (Hot) {
HotRect.left = x;
HotRect.top = y;
}
//
// Compute text block
//
Start = p;
while (p) {
q = pGetHtmlCommandOrChar (p, &Command, &Char, &CommandBuf);
if (!Char || Command == COMMAND_WHITESPACE || Command == COMMAND_ESCAPED_CHAR) {
//
// If this is the end of of a block of text, paint it.
//
MYASSERT (Start);
Tchars = p - Start;
if (Tchars) {
TextOut (hdc, x, y, Start, Tchars);
GetTextExtentPoint32 (hdc, Start, Tchars, &Extent);
x += Extent.cx;
if (x > rect.right) {
break;
}
}
Start = q;
oneChar = 0;
if (printableFound && Command == COMMAND_WHITESPACE) {
oneChar = TEXT(' ');
} else if (Command == COMMAND_ESCAPED_CHAR) {
oneChar = (TCHAR) Char;
}
if (oneChar) {
TextOut (hdc, x, y, &oneChar, 1);
GetTextExtentPoint32 (hdc, &oneChar, 1, &Extent);
x += Extent.cx;
if (x > rect.right) {
break;
}
}
} else {
printableFound = TRUE;
}
switch (Command) {
case 0:
case COMMAND_WHITESPACE:
case COMMAND_ESCAPED_CHAR:
break;
case COMMAND_ANCHOR:
//
// Does this anchor have an HREF?
//
if (CommandBuf) {
Arg = CommandBuf;
Href[0] = 0;
while (Href[0] == 0 && *Arg) {
//
// Search for a HREF arg
//
Count = TcharCount (Arg);
AnchorKey = AllocText (Count);
AnchorVal = AllocText (Count);
if (!AnchorKey || !AnchorVal) {
FreeText (AnchorKey);
FreeText (AnchorVal);
break;
}
pGetHtmlKeyAndValue (Arg, AnchorKey, AnchorVal);
if (StringIMatch (TEXT("HREF"), AnchorKey)) {
_tcssafecpy (Href, AnchorVal, MAX_URL);
}
FreeText (AnchorKey);
FreeText (AnchorVal);
Arg += Count + 1;
}
if (Href[0]) {
//
// Does HREF point to a bookmark?
//
BookmarkLink = UrlLink = NULL;
if (_tcsnextc (Href) == TEXT('#')) {
BookmarkLink = SkipSpace (_tcsinc (Href));
} else {
if (s->UrlEnabled) {
UrlLink = Href;
}
}
//
// If either BookmarkLink or Url is non-NULL, then turn on
// link font and color
//
if (BookmarkLink || UrlLink) {
HotRect.left = x;
HotRect.top = y;
Hot = TRUE;
SelectObject (hdc, s->hFontUnderlined);
SetTextColor (hdc, GetSysColor (COLOR_HIGHLIGHT));
}
}
}
break;
case COMMAND_BOLD:
SelectObject (hdc, s->hFontBold);
break;
case COMMAND_UNDERLINE:
SelectObject (hdc, s->hFontUnderlined);
break;
case COMMAND_HORZ_RULE:
FillRect.left = rect.left;
FillRect.right = rect.right;
FillRect.top = y;
FillRect.bottom = y + LineHeight;
Rectangle (hdc, FillRect.left, FillRect.top, FillRect.right + 1, FillRect.bottom + 1);
OldPen = (HPEN) SelectObject (hdc, ShadowPen);
MoveToEx (hdc, rect.left + 3, y + LineHeight / 2, NULL);
LineTo (hdc, rect.right - 3, y + LineHeight / 2);
SelectObject (hdc, HighlightPen);
MoveToEx (hdc, rect.left + 3, y + LineHeight / 2 + 1, NULL);
LineTo (hdc, rect.right - 3, y + LineHeight / 2 + 1);
SelectObject (hdc, OldPen);
x = rect.right;
break;
case COMMAND_ANCHOR_END:
SelectObject (hdc, s->hFontNormal);
SetTextColor (hdc, GetSysColor (COLOR_WINDOWTEXT));
if (Hot) {
Hot = FALSE;
HotRect.right = x;
HotRect.bottom = y + LineHeight;
pRegisterHotLink (&s->HotLinkArray, &HotRect, UrlLink, BookmarkLink);
}
break;
case COMMAND_BOLD_END:
case COMMAND_UNDERLINE_END:
SelectObject (hdc, s->hFontNormal);
break;
}
FreePathString (CommandBuf);
p = q;
}
//
// Hot link that extends to multiple lines
//
if (Hot) {
HotRect.right = x;
HotRect.bottom = y + LineHeight;
pRegisterHotLink (&s->HotLinkArray, &HotRect, UrlLink, BookmarkLink);
}
//
// Fill blank area to end of line
//
if (x < rect.right) {
FillRect.left = x;
FillRect.right = rect.right;
FillRect.top = y;
FillRect.bottom = y + LineHeight;
Rectangle (hdc, FillRect.left, FillRect.top, FillRect.right + 1, FillRect.bottom + 1);
}
y += LineHeight;
}
//
// Fill blank area to bottom of window
//
if (y < rect.bottom) {
FillRect.left = 0;
FillRect.right = rect.right;
FillRect.top = y;
FillRect.bottom = rect.bottom;
Rectangle (hdc, FillRect.left, FillRect.top, FillRect.right + 1, FillRect.bottom + 1);
}
//
// Cleanup
//
if (FillBrush) {
DeleteObject (FillBrush);
}
if (ShadowPen) {
DeleteObject (ShadowPen);
ShadowPen = NULL;
}
if (HighlightPen) {
DeleteObject (HighlightPen);
HighlightPen = NULL;
}
EndPaint (hwnd, &ps);
return 0;
case WM_VSCROLL:
Pos = HIWORD (wParam);
ZeroMemory (&si, sizeof (si));
si.cbSize = sizeof (si);
si.fMask = SIF_ALL;
GetScrollInfo (hwnd, SB_VERT, &si);
i = si.nMax - (INT) si.nPage + 1;
si.fMask = 0;
switch (LOWORD (wParam)) {
case SB_PAGEDOWN:
if (si.nPos + (INT) si.nPage < i) {
si.nPos += si.nPage;
ScrollWindow (hwnd, 0, -((INT) LineHeight * (INT) si.nPage), NULL, NULL);
si.fMask = SIF_POS;
break;
}
// fall through!
case SB_BOTTOM:
if (si.nPos < i) {
InvalidateRect (hwnd, NULL, FALSE);
si.nPos = i;
si.fMask = SIF_POS;
}
break;
case SB_LINEDOWN:
if (si.nPos < i) {
si.nPos += 1;
ScrollWindow (hwnd, 0, -((INT) LineHeight), NULL, NULL);
si.fMask = SIF_POS;
}
break;
case SB_LINEUP:
if (si.nPos > si.nMin) {
si.nPos -= 1;
ScrollWindow (hwnd, 0, (INT) LineHeight, NULL, NULL);
si.fMask = SIF_POS;
}
break;
case SB_THUMBTRACK:
case SB_THUMBPOSITION:
if ((INT) Pos != si.nPos) {
InvalidateRect (hwnd, NULL, FALSE);
si.nPos = (INT) Pos;
si.fMask = SIF_POS;
}
break;
case SB_PAGEUP:
if (si.nPos >= si.nMin + (INT) si.nPage) {
si.nPos -= si.nPage;
ScrollWindow (hwnd, 0, (INT) LineHeight * si.nPage, NULL, NULL);
si.fMask = SIF_POS;
break;
}
// fall through
case SB_TOP:
if (si.nPos > si.nMin) {
ScrollWindow (hwnd, 0, (INT) LineHeight * si.nPos, NULL, NULL);
si.nPos = si.nMin;
si.fMask = SIF_POS;
}
break;
}
if (si.fMask) {
SetScrollInfo (hwnd, SB_VERT, &si, TRUE);
}
break;
case WM_DESTROY:
case WMX_CLEANUP:
if (!g_Terminated) {
if (s) {
if (s->hFontNormal) {
DeleteObject (s->hFontNormal);
s->hFontNormal = NULL;
}
if (s->hFontBold) {
DeleteObject (s->hFontBold);
s->hFontBold = NULL;
}
if (s->hFontUnderlined) {
DeleteObject (s->hFontUnderlined);
s->hFontUnderlined = NULL;
}
FreeGrowBuffer (&s->AttribsList);
FreeGrowBuffer (&s->HotLinkArray);
FreeGrowList (&s->List);
if (s->BookmarkTable) {
HtFree (s->BookmarkTable);
s->BookmarkTable = NULL;
}
MemFree (g_hHeap, 0, s);
SetWindowLong (hwnd, GWL_USERDATA, 0);
}
}
break;
}
return DefWindowProc (hwnd, uMsg, wParam, lParam);
}
VOID
RegisterTextViewer (
VOID
)
{
static WNDCLASS wc;
if (!wc.lpfnWndProc) {
wc.lpfnWndProc = TextViewProc;
wc.hInstance = g_hInst;
wc.hCursor = NULL;
wc.hbrBackground = (HBRUSH) COLOR_WINDOW;
wc.lpszClassName = S_TEXTVIEW_CLASS;
RegisterClass (&wc);
//
// We don't ever clean this up... and that's OK.
//
}
}
PCTSTR
AddLineToTextView (
HWND hwnd,
PCTSTR Text
)
{
PCTSTR TextEnd;
TextEnd = Text + SendMessage (hwnd, WMX_ADDLINE, 0, (LPARAM) Text);
if (*TextEnd == 0) {
return NULL;
}
return TextEnd;
}
VOID
AddStringToTextView (
IN HWND hwnd,
IN PCTSTR String
)
{
PCTSTR p;
if (!hwnd) {
return;
}
if (String && *String) {
for (p = String ; p ; p = AddLineToTextView (hwnd, p)) {
// empty
}
}
}