2011-10-06 20:53:09 +02:00
|
|
|
unit RichEditViewer;
|
|
|
|
|
2019-11-27 22:26:29 +01:00
|
|
|
{ TRichEditViewer by Jordan Russell and Martijn Laan
|
2011-10-06 20:53:09 +02:00
|
|
|
|
|
|
|
Known problem:
|
|
|
|
If, after assigning rich text to a TRichEditViewer component, you change
|
|
|
|
a property that causes the component's handle to be recreated, all text
|
2019-11-27 22:26:29 +01:00
|
|
|
formatting will be lost (in the interests of code size).
|
2011-10-06 20:53:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface
|
|
|
|
|
|
|
|
uses
|
|
|
|
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
|
2019-11-27 22:26:29 +01:00
|
|
|
StdCtrls, RichEdit, ActiveX;
|
2011-10-06 20:53:09 +02:00
|
|
|
|
|
|
|
type
|
2019-11-27 22:26:29 +01:00
|
|
|
IRichEditOleCallback = interface(IUnknown)
|
|
|
|
['{00020d03-0000-0000-c000-000000000046}']
|
|
|
|
function GetNewStorage(out stg: IStorage): HResult; stdcall;
|
|
|
|
function GetInPlaceContext(out Frame: IOleInPlaceFrame;
|
|
|
|
out Doc: IOleInPlaceUIWindow;
|
|
|
|
lpFrameInfo: POleInPlaceFrameInfo): HResult; stdcall;
|
|
|
|
function ShowContainerUI(fShow: BOOL): HResult; stdcall;
|
|
|
|
function QueryInsertObject(const clsid: TCLSID; const stg: IStorage;
|
|
|
|
cp: Longint): HResult; stdcall;
|
|
|
|
function DeleteObject(const oleobj: IOleObject): HResult; stdcall;
|
|
|
|
function QueryAcceptData(const dataobj: IDataObject;
|
|
|
|
var cfFormat: TClipFormat; reco: DWORD; fReally: BOOL;
|
|
|
|
hMetaPict: HGLOBAL): HResult; stdcall;
|
|
|
|
function ContextSensitiveHelp(fEnterMode: BOOL): HResult; stdcall;
|
|
|
|
function GetClipboardData(const chrg: TCharRange; reco: DWORD;
|
|
|
|
out dataobj: IDataObject): HResult; stdcall;
|
|
|
|
function GetDragDropEffect(fDrag: BOOL; grfKeyState: DWORD;
|
|
|
|
var dwEffect: DWORD): HResult; stdcall;
|
|
|
|
function GetContextMenu(seltype: Word; const oleobj: IOleObject;
|
|
|
|
const chrg: TCharRange; out menu: HMENU): HResult; stdcall;
|
|
|
|
end;
|
|
|
|
|
2021-03-05 18:01:23 +01:00
|
|
|
TRichEditViewerCustomShellExecute = procedure(hWnd: HWND; Operation, FileName, Parameters, Directory: LPWSTR; ShowCmd: Integer); stdcall;
|
|
|
|
|
2011-10-06 20:53:09 +02:00
|
|
|
TRichEditViewer = class(TMemo)
|
|
|
|
private
|
2024-10-02 19:27:34 +02:00
|
|
|
class var
|
|
|
|
FCustomShellExecute: TRichEditViewerCustomShellExecute;
|
|
|
|
var
|
|
|
|
FUseRichEdit: Boolean;
|
|
|
|
FRichEditLoaded: Boolean;
|
|
|
|
FCallback: IRichEditOleCallback;
|
2011-10-06 20:53:09 +02:00
|
|
|
procedure SetRTFTextProp(const Value: AnsiString);
|
|
|
|
procedure SetUseRichEdit(Value: Boolean);
|
|
|
|
procedure UpdateBackgroundColor;
|
|
|
|
procedure CMColorChanged(var Message: TMessage); message CM_COLORCHANGED;
|
|
|
|
procedure CMSysColorChange(var Message: TMessage); message CM_SYSCOLORCHANGE;
|
|
|
|
procedure CNNotify(var Message: TWMNotify); message CN_NOTIFY;
|
|
|
|
protected
|
|
|
|
procedure CreateParams(var Params: TCreateParams); override;
|
|
|
|
procedure CreateWnd; override;
|
|
|
|
public
|
|
|
|
constructor Create(AOwner: TComponent); override;
|
|
|
|
destructor Destroy; override;
|
|
|
|
function SetRTFText(const Value: AnsiString): Integer;
|
|
|
|
property RTFText: AnsiString write SetRTFTextProp;
|
2021-03-05 18:01:23 +01:00
|
|
|
class property CustomShellExecute: TRichEditViewerCustomShellExecute read FCustomShellExecute write FCustomShellExecute;
|
2011-10-06 20:53:09 +02:00
|
|
|
published
|
|
|
|
property UseRichEdit: Boolean read FUseRichEdit write SetUseRichEdit default True;
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure Register;
|
|
|
|
|
|
|
|
implementation
|
|
|
|
|
|
|
|
uses
|
2019-11-27 22:26:29 +01:00
|
|
|
ShellApi, BidiUtils, PathFunc, ComObj;
|
2011-10-06 20:53:09 +02:00
|
|
|
|
|
|
|
const
|
|
|
|
RICHEDIT_CLASSW = 'RichEdit20W';
|
|
|
|
MSFTEDIT_CLASS = 'RICHEDIT50W';
|
|
|
|
EM_AUTOURLDETECT = WM_USER + 91;
|
|
|
|
ENM_LINK = $04000000;
|
|
|
|
EN_LINK = $070b;
|
|
|
|
|
|
|
|
type
|
2019-12-04 16:51:19 +01:00
|
|
|
{ Basic implementation of IRichEditOleCallback to enable the viewing of images and other objects. }
|
|
|
|
TBasicRichEditOleCallback = class(TInterfacedObject, IRichEditOleCallback)
|
2019-11-27 22:26:29 +01:00
|
|
|
public
|
|
|
|
function GetNewStorage(out stg: IStorage): HResult; stdcall;
|
|
|
|
function GetInPlaceContext(out Frame: IOleInPlaceFrame;
|
|
|
|
out Doc: IOleInPlaceUIWindow;
|
|
|
|
lpFrameInfo: POleInPlaceFrameInfo): HResult; stdcall;
|
|
|
|
function ShowContainerUI(fShow: BOOL): HResult; stdcall;
|
|
|
|
function QueryInsertObject(const clsid: TCLSID; const stg: IStorage;
|
|
|
|
cp: Longint): HResult; stdcall;
|
|
|
|
function DeleteObject(const oleobj: IOleObject): HResult; stdcall;
|
|
|
|
function QueryAcceptData(const dataobj: IDataObject;
|
|
|
|
var cfFormat: TClipFormat; reco: DWORD; fReally: BOOL;
|
|
|
|
hMetaPict: HGLOBAL): HResult; stdcall;
|
|
|
|
function ContextSensitiveHelp(fEnterMode: BOOL): HResult; stdcall;
|
|
|
|
function GetClipboardData(const chrg: TCharRange; reco: DWORD;
|
|
|
|
out dataobj: IDataObject): HResult; stdcall;
|
|
|
|
function GetDragDropEffect(fDrag: BOOL; grfKeyState: DWORD;
|
|
|
|
var dwEffect: DWORD): HResult; stdcall;
|
|
|
|
function GetContextMenu(seltype: Word; const oleobj: IOleObject;
|
|
|
|
const chrg: TCharRange; out menu: HMENU): HResult; stdcall;
|
|
|
|
end;
|
|
|
|
|
2011-10-06 20:53:09 +02:00
|
|
|
PEnLink = ^TEnLink;
|
|
|
|
TENLink = record
|
|
|
|
nmhdr: TNMHdr;
|
|
|
|
msg: UINT;
|
|
|
|
wParam: WPARAM;
|
|
|
|
lParam: LPARAM;
|
|
|
|
chrg: TCharRange;
|
|
|
|
end;
|
|
|
|
|
|
|
|
TTextRange = record
|
|
|
|
chrg: TCharRange;
|
2024-03-31 21:10:44 +02:00
|
|
|
lpstrText: PWideChar;
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
|
|
|
|
var
|
|
|
|
RichEditModule: HMODULE;
|
|
|
|
RichEditUseCount: Integer = 0;
|
|
|
|
RichEditVersion: Integer;
|
|
|
|
|
|
|
|
procedure LoadRichEdit;
|
2016-02-07 12:36:38 +01:00
|
|
|
|
|
|
|
function GetSystemDir: String;
|
|
|
|
var
|
|
|
|
Buf: array[0..MAX_PATH-1] of Char;
|
|
|
|
begin
|
|
|
|
GetSystemDirectory(Buf, SizeOf(Buf) div SizeOf(Buf[0]));
|
|
|
|
Result := StrPas(Buf);
|
|
|
|
end;
|
|
|
|
|
2011-10-06 20:53:09 +02:00
|
|
|
begin
|
|
|
|
if RichEditUseCount = 0 then begin
|
|
|
|
RichEditVersion := 4;
|
2016-01-07 14:09:25 +01:00
|
|
|
RichEditModule := LoadLibrary(PChar(AddBackslash(GetSystemDir) + 'MSFTEDIT.DLL'));
|
2011-10-06 20:53:09 +02:00
|
|
|
if RichEditModule = 0 then begin
|
|
|
|
RichEditVersion := 2;
|
2016-01-07 14:09:25 +01:00
|
|
|
RichEditModule := LoadLibrary(PChar(AddBackslash(GetSystemDir) + 'RICHED20.DLL'));
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
end;
|
|
|
|
Inc(RichEditUseCount);
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure UnloadRichEdit;
|
|
|
|
begin
|
|
|
|
if RichEditUseCount > 0 then begin
|
|
|
|
Dec(RichEditUseCount);
|
|
|
|
if RichEditUseCount = 0 then begin
|
|
|
|
FreeLibrary(RichEditModule);
|
|
|
|
RichEditModule := 0;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2019-12-04 16:51:19 +01:00
|
|
|
{ TBasicRichEditOleCallback }
|
2019-11-27 22:26:29 +01:00
|
|
|
|
2019-12-04 16:51:19 +01:00
|
|
|
function TBasicRichEditOleCallback.GetNewStorage(out stg: IStorage): HResult; stdcall;
|
2019-11-27 22:26:29 +01:00
|
|
|
var
|
|
|
|
LockBytes: ILockBytes;
|
|
|
|
begin
|
|
|
|
try
|
|
|
|
OleCheck(CreateILockBytesOnHGlobal(0, True, LockBytes));
|
|
|
|
OleCheck(StgCreateDocfileOnILockBytes(LockBytes, STGM_READWRITE
|
|
|
|
or STGM_SHARE_EXCLUSIVE or STGM_CREATE, 0, stg));
|
|
|
|
Result := S_OK;
|
|
|
|
except
|
|
|
|
Result := E_OUTOFMEMORY;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2019-12-04 16:51:19 +01:00
|
|
|
function TBasicRichEditOleCallback.GetInPlaceContext(out Frame: IOleInPlaceFrame;
|
2019-11-27 22:26:29 +01:00
|
|
|
out Doc: IOleInPlaceUIWindow; lpFrameInfo: POleInPlaceFrameInfo): HResult;
|
|
|
|
begin
|
|
|
|
Result := E_NOTIMPL;
|
|
|
|
end;
|
|
|
|
|
2019-12-04 16:51:19 +01:00
|
|
|
function TBasicRichEditOleCallback.ShowContainerUI(fShow: BOOL): HResult;
|
2019-11-27 22:26:29 +01:00
|
|
|
begin
|
|
|
|
Result := E_NOTIMPL;
|
|
|
|
end;
|
|
|
|
|
2019-12-04 16:51:19 +01:00
|
|
|
function TBasicRichEditOleCallback.QueryInsertObject(const clsid: TCLSID; const stg: IStorage;
|
2019-11-27 22:26:29 +01:00
|
|
|
cp: Longint): HResult;
|
|
|
|
begin
|
|
|
|
Result := S_OK;
|
|
|
|
end;
|
|
|
|
|
2019-12-04 16:51:19 +01:00
|
|
|
function TBasicRichEditOleCallback.DeleteObject(const oleobj: IOleObject): HResult;
|
2019-11-27 22:26:29 +01:00
|
|
|
begin
|
|
|
|
if Assigned(oleobj) then
|
|
|
|
oleobj.Close(OLECLOSE_NOSAVE);
|
|
|
|
Result := S_OK;
|
|
|
|
end;
|
|
|
|
|
2019-12-04 16:51:19 +01:00
|
|
|
function TBasicRichEditOleCallback.QueryAcceptData(const dataobj: IDataObject;
|
2019-11-27 22:26:29 +01:00
|
|
|
var cfFormat: TClipFormat; reco: DWORD; fReally: BOOL;
|
|
|
|
hMetaPict: HGLOBAL): HResult;
|
|
|
|
begin
|
|
|
|
Result := S_OK;
|
|
|
|
end;
|
|
|
|
|
2019-12-04 16:51:19 +01:00
|
|
|
function TBasicRichEditOleCallback.ContextSensitiveHelp(fEnterMode: BOOL): HResult;
|
2019-11-27 22:26:29 +01:00
|
|
|
begin
|
|
|
|
Result := S_OK;
|
|
|
|
end;
|
|
|
|
|
2019-12-04 16:51:19 +01:00
|
|
|
function TBasicRichEditOleCallback.GetClipboardData(const chrg: TCharRange; reco: DWORD;
|
2019-11-27 22:26:29 +01:00
|
|
|
out dataobj: IDataObject): HResult;
|
|
|
|
begin
|
|
|
|
Result := E_NOTIMPL;
|
|
|
|
end;
|
|
|
|
|
2019-12-04 16:51:19 +01:00
|
|
|
function TBasicRichEditOleCallback.GetDragDropEffect(fDrag: BOOL; grfKeyState: DWORD;
|
2019-11-27 22:26:29 +01:00
|
|
|
var dwEffect: DWORD): HResult;
|
|
|
|
begin
|
|
|
|
Result := E_NOTIMPL;
|
|
|
|
end;
|
|
|
|
|
2019-12-04 16:51:19 +01:00
|
|
|
function TBasicRichEditOleCallback.GetContextMenu(seltype: Word;
|
2019-11-27 22:26:29 +01:00
|
|
|
const oleobj: IOleObject; const chrg: TCharRange; out Menu: HMENU): HResult;
|
|
|
|
begin
|
|
|
|
Result := E_NOTIMPL;
|
|
|
|
end;
|
|
|
|
|
2011-10-06 20:53:09 +02:00
|
|
|
{ TRichEditViewer }
|
|
|
|
|
|
|
|
constructor TRichEditViewer.Create(AOwner: TComponent);
|
|
|
|
begin
|
|
|
|
inherited;
|
|
|
|
FUseRichEdit := True;
|
2019-12-04 16:51:19 +01:00
|
|
|
FCallback := TBasicRichEditOleCallback.Create;
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
|
|
|
|
destructor TRichEditViewer.Destroy;
|
|
|
|
begin
|
|
|
|
inherited;
|
|
|
|
{ First do all other deinitialization, then decrement the DLL use count }
|
|
|
|
if FRichEditLoaded then begin
|
|
|
|
FRichEditLoaded := False;
|
|
|
|
UnloadRichEdit;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TRichEditViewer.CreateParams(var Params: TCreateParams);
|
|
|
|
{ Based on code from TCustomRichEdit.CreateParams }
|
|
|
|
begin
|
|
|
|
if UseRichEdit and not FRichEditLoaded then begin
|
|
|
|
{ Increment the DLL use count when UseRichEdit is True, load the DLL }
|
|
|
|
FRichEditLoaded := True;
|
|
|
|
LoadRichEdit;
|
|
|
|
end;
|
|
|
|
inherited;
|
|
|
|
if UseRichEdit then begin
|
|
|
|
if RichEditVersion = 4 then
|
|
|
|
CreateSubClass(Params, MSFTEDIT_CLASS)
|
|
|
|
else
|
|
|
|
CreateSubClass(Params, RICHEDIT_CLASSW);
|
|
|
|
end else
|
|
|
|
{ Inherited handler creates a subclass of 'EDIT'.
|
|
|
|
Must have a unique class name since it uses two different classes
|
|
|
|
depending on the setting of the UseRichEdit property. }
|
|
|
|
StrCat(Params.WinClassName, '/Text'); { don't localize! }
|
|
|
|
SetBiDiStyles(Self, Params);
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TRichEditViewer.CreateWnd;
|
|
|
|
var
|
|
|
|
Mask: LongInt;
|
|
|
|
begin
|
|
|
|
inherited;
|
|
|
|
UpdateBackgroundColor;
|
2019-11-27 22:26:29 +01:00
|
|
|
if FUseRichEdit then begin
|
|
|
|
if RichEditVersion >= 2 then begin
|
|
|
|
Mask := ENM_LINK or SendMessage(Handle, EM_GETEVENTMASK, 0, 0);
|
|
|
|
SendMessage(Handle, EM_SETEVENTMASK, 0, LPARAM(Mask));
|
|
|
|
SendMessage(Handle, EM_AUTOURLDETECT, WPARAM(True), 0);
|
|
|
|
end;
|
|
|
|
SendMessage(Handle, EM_SETOLECALLBACK, 0, LPARAM(FCallback));
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TRichEditViewer.UpdateBackgroundColor;
|
|
|
|
begin
|
|
|
|
if FUseRichEdit and HandleAllocated then
|
|
|
|
SendMessage(Handle, EM_SETBKGNDCOLOR, 0, ColorToRGB(Color));
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TRichEditViewer.SetUseRichEdit(Value: Boolean);
|
|
|
|
begin
|
|
|
|
if FUseRichEdit <> Value then begin
|
|
|
|
FUseRichEdit := Value;
|
|
|
|
RecreateWnd;
|
|
|
|
if not Value and FRichEditLoaded then begin
|
|
|
|
{ Decrement the DLL use count when UseRichEdit is set to False }
|
|
|
|
FRichEditLoaded := False;
|
|
|
|
UnloadRichEdit;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
|
|
|
type
|
|
|
|
PStreamLoadData = ^TStreamLoadData;
|
|
|
|
TStreamLoadData = record
|
|
|
|
Buf: PByte;
|
|
|
|
BytesLeft: Integer;
|
|
|
|
end;
|
|
|
|
|
|
|
|
function StreamLoad(dwCookie: Longint; pbBuff: PByte;
|
|
|
|
cb: Longint; var pcb: Longint): Longint; stdcall;
|
|
|
|
begin
|
|
|
|
Result := 0;
|
|
|
|
with PStreamLoadData(dwCookie)^ do begin
|
|
|
|
if cb > BytesLeft then
|
|
|
|
cb := BytesLeft;
|
|
|
|
Move(Buf^, pbBuff^, cb);
|
|
|
|
Inc(Buf, cb);
|
|
|
|
Dec(BytesLeft, cb);
|
|
|
|
pcb := cb;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
|
|
|
function TRichEditViewer.SetRTFText(const Value: AnsiString): Integer;
|
|
|
|
|
|
|
|
function StreamIn(AFormat: WPARAM): Integer;
|
|
|
|
var
|
|
|
|
Data: TStreamLoadData;
|
|
|
|
EditStream: TEditStream;
|
|
|
|
begin
|
|
|
|
Data.Buf := @Value[1];
|
|
|
|
Data.BytesLeft := Length(Value);
|
|
|
|
{ Check for UTF-16 BOM }
|
|
|
|
if (AFormat and SF_TEXT <> 0) and (Data.BytesLeft >= 2) and
|
|
|
|
(PWord(Pointer(Value))^ = $FEFF) then begin
|
|
|
|
AFormat := AFormat or SF_UNICODE;
|
|
|
|
Inc(Data.Buf, 2);
|
|
|
|
Dec(Data.BytesLeft, 2);
|
|
|
|
end;
|
|
|
|
EditStream.dwCookie := Longint(@Data);
|
|
|
|
EditStream.dwError := 0;
|
|
|
|
EditStream.pfnCallback := @StreamLoad;
|
|
|
|
SendMessage(Handle, EM_STREAMIN, AFormat, LPARAM(@EditStream));
|
|
|
|
Result := EditStream.dwError;
|
|
|
|
end;
|
|
|
|
|
|
|
|
begin
|
|
|
|
if not FUseRichEdit then begin
|
|
|
|
Text := String(Value);
|
|
|
|
Result := 0;
|
|
|
|
end
|
|
|
|
else begin
|
|
|
|
SendMessage(Handle, EM_EXLIMITTEXT, 0, LParam($7FFFFFFE));
|
|
|
|
Result := StreamIn(SF_RTF);
|
|
|
|
if Result <> 0 then
|
|
|
|
Result := StreamIn(SF_TEXT);
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TRichEditViewer.SetRTFTextProp(const Value: AnsiString);
|
|
|
|
begin
|
|
|
|
SetRTFText(Value);
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TRichEditViewer.CMColorChanged(var Message: TMessage);
|
|
|
|
begin
|
|
|
|
inherited;
|
|
|
|
UpdateBackgroundColor;
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TRichEditViewer.CMSysColorChange(var Message: TMessage);
|
|
|
|
begin
|
|
|
|
inherited;
|
|
|
|
UpdateBackgroundColor;
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TRichEditViewer.CNNotify(var Message: TWMNotify);
|
|
|
|
var
|
|
|
|
EnLink: PEnLink;
|
|
|
|
CharRange: TCharRange;
|
|
|
|
TextRange: TTextRange;
|
|
|
|
Len: Integer;
|
|
|
|
URL: String;
|
|
|
|
begin
|
|
|
|
case Message.NMHdr^.code of
|
|
|
|
EN_LINK: begin
|
|
|
|
EnLink := PEnLink(Message.NMHdr);
|
|
|
|
if EnLink.msg = WM_LBUTTONUP then begin
|
|
|
|
CharRange := EnLink.chrg;
|
|
|
|
if (CharRange.cpMin >= 0) and (CharRange.cpMax > CharRange.cpMin) then begin
|
|
|
|
Len := CharRange.cpMax - CharRange.cpMin;
|
|
|
|
Inc(Len); { for null terminator }
|
|
|
|
if Len > 1 then begin
|
|
|
|
SetLength(URL, Len);
|
|
|
|
TextRange.chrg := CharRange;
|
|
|
|
TextRange.lpstrText := PChar(URL);
|
|
|
|
SetLength(URL, SendMessage(Handle, EM_GETTEXTRANGE, 0, LParam(@TextRange)));
|
2021-03-05 18:01:23 +01:00
|
|
|
if URL <> '' then begin
|
|
|
|
if Assigned(FCustomShellExecute) then
|
|
|
|
FCustomShellExecute(Handle, 'open', PChar(URL), nil, nil, SW_SHOWNORMAL)
|
|
|
|
else
|
|
|
|
ShellExecute(Handle, 'open', PChar(URL), nil, nil, SW_SHOWNORMAL);
|
|
|
|
end;
|
2011-10-06 20:53:09 +02:00
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure Register;
|
|
|
|
begin
|
|
|
|
RegisterComponents('JR', [TRichEditViewer]);
|
|
|
|
end;
|
|
|
|
|
|
|
|
end.
|