|
@@ -0,0 +1,878 @@
|
|
|
|
+unit fresnel.edit;
|
|
|
|
+
|
|
|
|
+{$mode objfpc}{$H+}
|
|
|
|
+
|
|
|
|
+interface
|
|
|
|
+
|
|
|
|
+uses
|
|
|
|
+ Classes, fpImage, fpCSSResParser, fpCSSTree, fresnel.CursorTimer,
|
|
|
|
+ Fresnel.TextLayouter, Fresnel.Classes, Fresnel.Dom, Fresnel.Keys, Fresnel.Controls,
|
|
|
|
+ FCL.events, Fresnel.Events, Utf8Utils;
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+Type
|
|
|
|
+
|
|
|
|
+ { TEdit }
|
|
|
|
+
|
|
|
|
+ TEdit = Class(TReplacedElement, IBlinkControl)
|
|
|
|
+ const
|
|
|
|
+ NotWordChars = [#0..'/', ':'..'@', '['..'^', '`', '{'..'~'];
|
|
|
|
+ CursorWidth = 2;
|
|
|
|
+ private
|
|
|
|
+ class var _FresnelEditTypeID : TCSSNumericalID;
|
|
|
|
+ class constructor Init;
|
|
|
|
+ class function IsSpecialChar(aUnicodeChar: string): boolean;
|
|
|
|
+ private
|
|
|
|
+ FEnabled: Boolean;
|
|
|
|
+ FMaxLength: Integer;
|
|
|
|
+ FReadOnly: Boolean;
|
|
|
|
+ // Selection start, stop index - 0 based.
|
|
|
|
+ FSelectionEnd: Integer;
|
|
|
|
+ FSelectionStart: Integer;
|
|
|
|
+ // Value
|
|
|
|
+ FValue: TFresnelCaption;
|
|
|
|
+ // Text to show if value is empty
|
|
|
|
+ FPlaceHolder : TFresnelCaption;
|
|
|
|
+ // Actually visible text
|
|
|
|
+ FVisibleText : TFresnelCaption;
|
|
|
|
+ // Selection start/end position in pixels
|
|
|
|
+ FSelectionStartX : TFresnelLength;
|
|
|
|
+ FSelectionEndX : TFresnelLength;
|
|
|
|
+ // Cursor position in pixels
|
|
|
|
+ FCursorX : TFresnelLength;
|
|
|
|
+ // Cursor position, characters, 0 based
|
|
|
|
+ FCursorPos : Integer;
|
|
|
|
+ // Start drawing text at
|
|
|
|
+ FDrawOffset : TFresnelLength;
|
|
|
|
+ // Visible text offset in pixels.
|
|
|
|
+ FTextOffset : TFresnelLength;
|
|
|
|
+ // Should we draw the cursor ?
|
|
|
|
+ FCursorVisible : Boolean;
|
|
|
|
+ // Do we need to recalculate the text parameters ?
|
|
|
|
+ FRecalcParams: Boolean;
|
|
|
|
+ procedure DoDeleteSelection; virtual;
|
|
|
|
+ function GetDrawText: TFresnelCaption;
|
|
|
|
+ function GetSelectionText: String;
|
|
|
|
+ function NextWordEndOffset: Integer;
|
|
|
|
+ function PrevWordStartOffset: Integer;
|
|
|
|
+ procedure SetEnabled(const aValue: Boolean);
|
|
|
|
+ procedure SetMaxLength(const aValue: Integer);
|
|
|
|
+ procedure SetPlaceHolder(const aValue: TFresnelCaption);
|
|
|
|
+ procedure SetReadOnly(const aValue: Boolean);
|
|
|
|
+ procedure SetSelectionEnd(const aValue: Integer);
|
|
|
|
+ procedure SetSelectionStart(const aValue: Integer);
|
|
|
|
+ Procedure NormalizeSelection;
|
|
|
|
+ protected
|
|
|
|
+ // char index -> X pos
|
|
|
|
+ function CalcXOffset(aCharPos: Integer; aUseOffset: Boolean=true; aUseDrawText : Boolean = False): TFresnelLength;
|
|
|
|
+ // X pos -> char index
|
|
|
|
+ function CalcCharOffset(aXOffset: TFresnelLength): Integer;
|
|
|
|
+ //
|
|
|
|
+ // Recalculate index etc.
|
|
|
|
+ //
|
|
|
|
+ // Indicate that params have changed: Size, cursor or selection pos.
|
|
|
|
+ procedure EditParamsChanged;
|
|
|
|
+ // If params need to be recalculated, then recalculate
|
|
|
|
+ procedure MaybeRecalcParams;
|
|
|
|
+ // Calculate offsets: cursor pos, text offset
|
|
|
|
+ procedure CalcOffsets; virtual;
|
|
|
|
+ // Calc selection
|
|
|
|
+ procedure CalcTextDrawInfo; virtual;
|
|
|
|
+ // Calculate character size.
|
|
|
|
+ class function CalcCharSize(aFont: IFresnelFont; aUnicodeChar: String): TFresnelLength;
|
|
|
|
+ // blink cursor
|
|
|
|
+ procedure Blink(aVisible : Boolean); virtual;
|
|
|
|
+ function DoDispatchEvent(aEvent: TAbstractEvent): Integer; override;
|
|
|
|
+ procedure HandleFocusChange(GotFocus: Boolean);
|
|
|
|
+ procedure HandleKeyDown(aEvent: TFresnelKeyEvent); virtual;
|
|
|
|
+ procedure HandleMouseDown(aEvent : TFresnelMouseEvent); virtual;
|
|
|
|
+ procedure HandleMouseMove(aEvent: TFresnelMouseEvent); virtual;
|
|
|
|
+ procedure HandleMouseUp(aEvent : TFresnelMouseEvent); virtual;
|
|
|
|
+ procedure SetValue(const aValue: TFresnelCaption); virtual;
|
|
|
|
+ procedure SetName(const aNewName: TComponentName); override;
|
|
|
|
+ procedure DoRender(aRenderer: IFresnelRenderer); override;
|
|
|
|
+ Public
|
|
|
|
+ class function HandleFocus: Boolean; override;
|
|
|
|
+ class function CSSTypeID: TCSSNumericalID; override;
|
|
|
|
+ class function CSSTypeName: TCSSString; override;
|
|
|
|
+ class function GetCSSTypeStyle: TCSSString; override;
|
|
|
|
+ public
|
|
|
|
+ constructor Create(aOwner : TComponent); override;
|
|
|
|
+ function GetIntrinsicContentSize(aMode: TFresnelLayoutMode; aMaxWidth: TFresnelLength; aMaxHeight: TFresnelLength): TFresnelPoint; override;
|
|
|
|
+ function CanFocus: Boolean; override;
|
|
|
|
+ procedure DeleteSelection;
|
|
|
|
+ procedure SetSelection(const aStart,aEnd : Integer);
|
|
|
|
+ property Value: TFresnelCaption read FValue write SetValue;
|
|
|
|
+ property ReadOnly : Boolean Read FReadOnly Write SetReadOnly;
|
|
|
|
+ property Enabled : Boolean Read FEnabled Write SetEnabled;
|
|
|
|
+ property PlaceHolder : TFresnelCaption Read FPlaceHolder Write SetPlaceHolder;
|
|
|
|
+ // Cursor position, 0 based, in characters (codepoints)
|
|
|
|
+ Property Curs : Integer Read FSelectionStart Write SetSelectionStart;
|
|
|
|
+ // SelectionStart, 0 based, in characters (codepoints)
|
|
|
|
+ Property SelectionStart : Integer Read FSelectionStart Write SetSelectionStart;
|
|
|
|
+ // SelectionEnd, 0 based, in characters (codepoints)
|
|
|
|
+ Property SelectionEnd : Integer Read FSelectionEnd Write SetSelectionEnd;
|
|
|
|
+ // Max length
|
|
|
|
+ Property MaxLength : Integer Read FMaxLength Write SetMaxLength;
|
|
|
|
+ // Get selection
|
|
|
|
+ Property SelectionText : String Read GetSelectionText;
|
|
|
|
+ end;
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+implementation
|
|
|
|
+
|
|
|
|
+uses math;
|
|
|
|
+
|
|
|
|
+function TEdit.GetIntrinsicContentSize(aMode: TFresnelLayoutMode; aMaxWidth: TFresnelLength;
|
|
|
|
+ aMaxHeight: TFresnelLength): TFresnelPoint;
|
|
|
|
+
|
|
|
|
+var
|
|
|
|
+ lFont : IFresnelFont;
|
|
|
|
+ lSize : TFresnelPoint;
|
|
|
|
+ lVertPadding,lHorzPadding : TFresnelLength;
|
|
|
|
+begin
|
|
|
|
+ lFont:=GetFont;
|
|
|
|
+ // todo writing-mode
|
|
|
|
+ if IsNan(aMaxHeight) then ;
|
|
|
|
+ lHorzPadding:=GetComputedLength(fcaPaddingLeft)+GetComputedLength(fcaPaddingRight);
|
|
|
|
+ lVertPadding:=GetComputedLength(fcaPaddingTop)+GetComputedLength(fcaPaddingBottom);
|
|
|
|
+ Lsize:=LFont.TextSize('W');
|
|
|
|
+ case aMode of
|
|
|
|
+ flmMaxHeight,
|
|
|
|
+ flmMinHeight:
|
|
|
|
+ begin
|
|
|
|
+ LSize.Offset(lHorzPadding,lVertPadding);
|
|
|
|
+ Result:=LSize;
|
|
|
|
+ end;
|
|
|
|
+ flmMinWidth,
|
|
|
|
+ flmMaxWidth:
|
|
|
|
+ begin
|
|
|
|
+ if MaxLength<>0 then
|
|
|
|
+ lSize.X:=lSize.X*MaxLength
|
|
|
|
+ else
|
|
|
|
+ LSize.X:=aMaxWidth;
|
|
|
|
+ LSize.Offset(lHorzPadding,lVertPadding);
|
|
|
|
+ Result:=lSize;
|
|
|
|
+ end;
|
|
|
|
+ end;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.DeleteSelection;
|
|
|
|
+
|
|
|
|
+var
|
|
|
|
+ lPrev : TFresnelCaption;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ if ReadOnly or (FSelectionStart = FselectionEnd) then
|
|
|
|
+ Exit;
|
|
|
|
+ lPrev:=FValue;
|
|
|
|
+ DoDeleteSelection;
|
|
|
|
+ if (lPrev <> FValue) then
|
|
|
|
+ EventDispatcher.DispatchEvent(evtChange);
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.DoDeleteSelection;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ if FSelectionStart=FSelectionEnd then
|
|
|
|
+ exit;
|
|
|
|
+ if FSelectionStart > FSelectionEnd then
|
|
|
|
+ begin
|
|
|
|
+ Delete(FValue,1 + FSelectionEnd, FSelectionStart-FSelectionEnd);
|
|
|
|
+ FSelectionStart:=FSelectionEnd;
|
|
|
|
+ end
|
|
|
|
+ else
|
|
|
|
+ begin
|
|
|
|
+ Delete(FValue,1 + FSelectionStart, FSelectionEnd-FSelectionStart);
|
|
|
|
+ FSelectionEnd:=FSelectionStart;
|
|
|
|
+ end;
|
|
|
|
+ EditParamsChanged;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+function TEdit.PrevWordStartOffset : Integer;
|
|
|
|
+begin
|
|
|
|
+ Result:=FSelectionEnd;
|
|
|
|
+ Result:=Result-1;
|
|
|
|
+ // word search. not_word all have ord <128, so we can use bytewise offset
|
|
|
|
+ while (Result>0) and (FValue[Result+1] in NotWordChars) do
|
|
|
|
+ Dec(Result);
|
|
|
|
+ while (Result>0) and not (FValue[Result+1] in NotWordChars) do
|
|
|
|
+ Dec(Result);
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.SetEnabled(const aValue: Boolean);
|
|
|
|
+begin
|
|
|
|
+ if FEnabled=aValue then Exit;
|
|
|
|
+ FEnabled:=aValue;
|
|
|
|
+ DomChanged;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.SetMaxLength(const aValue: Integer);
|
|
|
|
+begin
|
|
|
|
+ if FMaxLength=aValue then Exit;
|
|
|
|
+ FMaxLength:=aValue;
|
|
|
|
+ DomChanged; // Min length may change
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+function TEdit.NextWordEndOffset : Integer;
|
|
|
|
+// Offset in bytes
|
|
|
|
+var
|
|
|
|
+ lLen : Integer;
|
|
|
|
+begin
|
|
|
|
+ // word search. not_word all have ord <128 so we can use bytewise offset
|
|
|
|
+ lLen:=Length(FValue);
|
|
|
|
+ Result:=FSelectionEnd;
|
|
|
|
+ while (Result<lLen) and (FValue[Result+1] in NotWordChars) do
|
|
|
|
+ Inc(Result);
|
|
|
|
+ while (Result<lLen) and not (FValue[Result+1] in NotWordChars) do
|
|
|
|
+ Inc(Result);
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.HandleKeyDown(aEvent: TFresnelKeyEvent);
|
|
|
|
+
|
|
|
|
+var
|
|
|
|
+ lKeyUTF8 : String;
|
|
|
|
+ lKeyLen : Integer;
|
|
|
|
+ lChanged : Boolean;
|
|
|
|
+ lOffset : Integer;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ lChanged:=False;
|
|
|
|
+ // Printable character has numkey >0
|
|
|
|
+ if (aEvent.NumKey>0) and not (aEvent.CtrlKey) then
|
|
|
|
+ begin
|
|
|
|
+ if ReadOnly then
|
|
|
|
+ exit;
|
|
|
|
+ DoDeleteSelection;
|
|
|
|
+ lKeyUTF8:=UnicodeToUTF8(aEvent.NumKey);
|
|
|
|
+ Insert(lKeyUTF8,FValue,FCursorPos+1);
|
|
|
|
+ lKeyLen:=Length(lKeyUTF8);
|
|
|
|
+ if FCursorPos<=FSelectionEnd then
|
|
|
|
+ Inc(FSelectionEnd,lKeyLen);
|
|
|
|
+ if FCursorPos<=FSelectionStart then
|
|
|
|
+ Inc(FSelectionStart,lKeyLen);
|
|
|
|
+ Inc(FCursorPos,lKeyLen);
|
|
|
|
+ EventDispatcher.DispatchEvent(evtChange);
|
|
|
|
+ lChanged:=True;
|
|
|
|
+ end
|
|
|
|
+ else
|
|
|
|
+ begin
|
|
|
|
+ lChanged:=True;
|
|
|
|
+ Case aEvent.NumKey of
|
|
|
|
+ TKeyCodes.BackSpace:
|
|
|
|
+ begin
|
|
|
|
+ if FSelectionStart=FSelectionEnd then
|
|
|
|
+ begin
|
|
|
|
+ lOffset:=UTF8FindNearestCharStart(PChar(FValue),Length(FValue),FCursorPos);
|
|
|
|
+ lKeyLen:=UTF8CodepointSize(@FValue[lOffset+1]);
|
|
|
|
+ Delete(FValue,lOffSet+1,lKeyLen);
|
|
|
|
+ FCursorPos:=lOffSet;
|
|
|
|
+ FSelectionStart:=FCursorPos;
|
|
|
|
+ FSelectionEnd:=FCursorPos;
|
|
|
|
+ end
|
|
|
|
+ else
|
|
|
|
+ begin
|
|
|
|
+ lKeyLen:=Abs(FSelectionStart-FSelectionEnd);
|
|
|
|
+ DoDeleteSelection;
|
|
|
|
+ FSelectionEnd:=FSelectionStart;
|
|
|
|
+ FCursorPos:=FSelectionStart;
|
|
|
|
+ end;
|
|
|
|
+ end;
|
|
|
|
+ TKeyCodes.Delete:
|
|
|
|
+ begin
|
|
|
|
+ if FSelectionStart=FSelectionEnd then
|
|
|
|
+ begin
|
|
|
|
+ lKeyLen:=UTF8CodepointSize(@FValue[FCursorPos+1]);
|
|
|
|
+ Delete(FValue,FCursorPos+1,lKeyLen);
|
|
|
|
+ end
|
|
|
|
+ else
|
|
|
|
+ begin
|
|
|
|
+ lKeyLen:=Abs(FSelectionStart-FSelectionEnd);
|
|
|
|
+ DoDeleteSelection;
|
|
|
|
+ if FSelectionStart<FSelectionEnd then
|
|
|
|
+ FSelectionEnd:=FSelectionStart
|
|
|
|
+ else
|
|
|
|
+ FSelectionStart:=FSelectionEnd;
|
|
|
|
+ end;
|
|
|
|
+ FCursorPos:=FSelectionEnd;
|
|
|
|
+ end;
|
|
|
|
+ TKeyCodes.ArrowLeft :
|
|
|
|
+ begin
|
|
|
|
+ if FCursorPos>0 then
|
|
|
|
+ begin
|
|
|
|
+ if aEvent.CtrlKey then
|
|
|
|
+ lOffset:=PrevWordStartOffset-FCursorPos
|
|
|
|
+ else
|
|
|
|
+ lOffset:=-1; // Todo: calc length of codepoint in bytes
|
|
|
|
+ FCursorPos:=FCursorPos+lOffset;
|
|
|
|
+ FSelectionEnd:=FCursorPos;
|
|
|
|
+ if not aEvent.ShiftKey then
|
|
|
|
+ FSelectionStart:=FSelectionEnd;
|
|
|
|
+ end;
|
|
|
|
+ end;
|
|
|
|
+ TKeyCodes.ArrowRight :
|
|
|
|
+ begin
|
|
|
|
+ if FCursorPos<Length(FValue) then
|
|
|
|
+ begin
|
|
|
|
+ if aEvent.CtrlKey then
|
|
|
|
+ lOffset:=NextWordEndOffset-FCursorPos
|
|
|
|
+ else
|
|
|
|
+ lOffset:=1;
|
|
|
|
+ Inc(FCursorPos,lOffset);
|
|
|
|
+ FSelectionEnd:=FCursorPos;
|
|
|
|
+ if not aEvent.ShiftKey then
|
|
|
|
+ FSelectionStart:=FSelectionEnd;
|
|
|
|
+ end
|
|
|
|
+ end;
|
|
|
|
+ TKeyCodes.Home:
|
|
|
|
+ begin
|
|
|
|
+ FCursorPos:=0;
|
|
|
|
+ FSelectionEnd:=0;
|
|
|
|
+ if not aEvent.ShiftKey then
|
|
|
|
+ FSelectionStart:=0;
|
|
|
|
+ end;
|
|
|
|
+ TKeyCodes.End_:
|
|
|
|
+ begin
|
|
|
|
+ FCursorPos:=Length(FValue);
|
|
|
|
+ FSelectionEnd:=FCursorPos;
|
|
|
|
+ if not aEvent.ShiftKey then
|
|
|
|
+ FSelectionStart:=FCursorPos;
|
|
|
|
+ end;
|
|
|
|
+ else
|
|
|
|
+ lChanged:=False;
|
|
|
|
+ end;
|
|
|
|
+ end;
|
|
|
|
+ if lChanged then
|
|
|
|
+ EditParamsChanged;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.HandleMouseMove(aEvent : TFresnelMouseEvent);
|
|
|
|
+var
|
|
|
|
+ lNewPos : integer;
|
|
|
|
+begin
|
|
|
|
+ if not (mbMain in aEvent.Buttons) then
|
|
|
|
+ exit;
|
|
|
|
+ lNewPos:=CalcCharOffset(aEvent.ControlX);
|
|
|
|
+ if FCursorPos<>lNewPos then
|
|
|
|
+ begin
|
|
|
|
+ FCursorPos:=lNewPos;
|
|
|
|
+ FSelectionEnd:=FCursorPos;
|
|
|
|
+ EditParamsChanged;
|
|
|
|
+ end;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.HandleMouseUp(aEvent: TFresnelMouseEvent);
|
|
|
|
+begin
|
|
|
|
+ // Nothing for the moment;
|
|
|
|
+ // When popup menus are handled, then here we should show the popup if it was the right button.
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.HandleMouseDown(aEvent: TFresnelMouseEvent);
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ MaybeRecalcParams;
|
|
|
|
+ FCursorPos:=CalcCharOffset(aEvent.ControlX);
|
|
|
|
+ if aEvent.ShiftKey then
|
|
|
|
+ begin
|
|
|
|
+ FSelectionEnd:=FCursorPos;
|
|
|
|
+ NormalizeSelection;
|
|
|
|
+ end
|
|
|
|
+ else
|
|
|
|
+ begin
|
|
|
|
+ FSelectionStart:=FCursorPos;
|
|
|
|
+ FSelectionEnd:=FCursorPos;
|
|
|
|
+ end;
|
|
|
|
+ EditParamsChanged;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.EditParamsChanged;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ FRecalcParams:=True;
|
|
|
|
+ NormalizeSelection;
|
|
|
|
+ DomChanged;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.SetValue(const aValue: TFresnelCaption);
|
|
|
|
+begin
|
|
|
|
+ if FValue=aValue then Exit;
|
|
|
|
+ FValue:=aValue;
|
|
|
|
+ EditParamsChanged;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.SetName(const aNewName: TComponentName);
|
|
|
|
+var
|
|
|
|
+ ChangeValue: Boolean;
|
|
|
|
+begin
|
|
|
|
+ if Name=aNewName then exit;
|
|
|
|
+ ChangeValue :=
|
|
|
|
+ not (csLoading in ComponentState)
|
|
|
|
+ and (Name = Value)
|
|
|
|
+ and ((Owner = nil) or not (csLoading in Owner.ComponentState));
|
|
|
|
+ inherited SetName(aNewName);
|
|
|
|
+ if ChangeValue then
|
|
|
|
+ Value := aNewName;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+Function TEdit.GetDrawText : TFresnelCaption;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ if FValue='' then
|
|
|
|
+ Result:=FPlaceHolder
|
|
|
|
+ else
|
|
|
|
+ Result:=FValue;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+function TEdit.GetSelectionText: String;
|
|
|
|
+begin
|
|
|
|
+ Result:=UTF8Copy(FValue,FSelectionStart,FSelectionEnd-FSelectionStart);
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.Blink(aVisible: Boolean);
|
|
|
|
+begin
|
|
|
|
+ FCursorVisible:=IsFocused and aVisible;
|
|
|
|
+ DomChanged;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+class function TEdit.IsSpecialChar(aUnicodeChar : string) : boolean;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ Result:=(aUnicodeChar=' ') or (aUnicodeChar='.');
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+class function TEdit.CalcCharSize(aFont : IFresnelFont; aUnicodeChar: String) : TFresnelLength;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ if IsSpecialChar(aUnicodeChar) then
|
|
|
|
+ Result:=aFont.TextSize('W'+aUnicodeChar+'W').x-aFont.TextSize('WW').X
|
|
|
|
+ else
|
|
|
|
+ Result:=aFont.TextSize(aUnicodeChar).x;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.CalcTextDrawInfo;
|
|
|
|
+
|
|
|
|
+// This combines various calculations in one.
|
|
|
|
+
|
|
|
|
+var
|
|
|
|
+ lUnicodeChar : string; // current character as UTF8 string
|
|
|
|
+ lCharNum : Integer; // Current character position (in codepoints)
|
|
|
|
+ lPos: integer; // Current pos in string.
|
|
|
|
+ lPrevPos : integer; // Previous pos in string.
|
|
|
|
+ lFirstVisibleIndex,lLastVisibleIndex: integer; // visible characters' start/end in utf-8 string, bytes
|
|
|
|
+ lText: string; // text to draw.
|
|
|
|
+ bestfx, bestlx: TFresnelLength;
|
|
|
|
+ lCharX: TFresnelLength; // character X position relative to widget
|
|
|
|
+ lTotalWidth: TFresnelLength; // total characters width, that becomes FCursorPx relative to the beginning of the text
|
|
|
|
+ lPreviousWidth: TFresnelLength; // total width on the previous step
|
|
|
|
+ lVisibleStartX, lVisibleEndX: TFresnelLength; // visible area start and end, pixels
|
|
|
|
+ lLeftSideMargin,lRightSideMargin : TFresnelLength;
|
|
|
|
+ lFont : IFresnelFont;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ lFont:=GetFont;
|
|
|
|
+ lLeftSideMargin:=GetComputedLength(fcaPaddingLeft);
|
|
|
|
+ lRightSideMargin:=GetComputedLength(fcaPaddingRight);
|
|
|
|
+ lVisibleStartX := lLeftSideMargin;
|
|
|
|
+ lVisibleEndX := RenderedContentBox.Width - lRightSideMargin;
|
|
|
|
+ FSelectionStartX := lVisibleEndX; // because we stop the search
|
|
|
|
+ FSelectionEndX := lVisibleEndX; // after last visible character is found
|
|
|
|
+ bestfx := -MaxInt + 1 + lVisibleStartX;
|
|
|
|
+ bestlx := MaxInt + 1 + lVisibleEndX;
|
|
|
|
+
|
|
|
|
+ lText := GetDrawText;
|
|
|
|
+ lUnicodeChar := '';
|
|
|
|
+ lTotalWidth := 0.0;
|
|
|
|
+ lPos := 0;
|
|
|
|
+ lCharNum := 0;
|
|
|
|
+ FDrawOffset := 0;
|
|
|
|
+ while lPos <= Length(ltext) do
|
|
|
|
+ begin
|
|
|
|
+ lPrevPos := lPos;
|
|
|
|
+ lpos := UTF8CharAtBytePos(lText, lpos, lUnicodeChar);
|
|
|
|
+ lPreviousWidth := lTotalWidth;
|
|
|
|
+ lTotalWidth := lTotalWidth + CalcCharSize(lFont, lUnicodeChar);
|
|
|
|
+ // Character position relative to edit margin. Text offset was calculated using cursor position.
|
|
|
|
+ lCharX := lTotalWidth - FTextOffset + lLeftSideMargin;
|
|
|
|
+
|
|
|
|
+ // Adjust selection coordinates
|
|
|
|
+ if lCharNum = FSelectionStart then
|
|
|
|
+ FSelectionStartX := lCharX;
|
|
|
|
+ if lCharNum = FSelectionEnd then
|
|
|
|
+ FSelectionEndX := lCharX;
|
|
|
|
+
|
|
|
|
+ // search for the first/last visible characters
|
|
|
|
+ if abs(lCharX - lVisibleStartX) < abs(bestfx - lVisibleStartX) then
|
|
|
|
+ begin
|
|
|
|
+ bestfx := lCharX;
|
|
|
|
+ lFirstVisibleIndex := lPrevPos;
|
|
|
|
+ FDrawOffset := lPreviousWidth;
|
|
|
|
+ end;
|
|
|
|
+ // in small edit field the same character can be both the first and the last, so no 'else' allowed
|
|
|
|
+ if abs(lCharX - lVisibleEndX) < abs(bestlx - lVisibleEndX) then
|
|
|
|
+ begin
|
|
|
|
+ bestlx := lCharX;
|
|
|
|
+ lLastVisibleIndex := UTF8CharAtBytePos(Ltext, lPos, lUnicodeChar); // plus one more character
|
|
|
|
+ end
|
|
|
|
+ else
|
|
|
|
+ begin
|
|
|
|
+ Writeln('Premature break at ',lCharNum);
|
|
|
|
+ break; // we can safely break after last visible character is found
|
|
|
|
+ end;
|
|
|
|
+ Inc(lCharNum);
|
|
|
|
+ end;
|
|
|
|
+
|
|
|
|
+ if FSelectionStartX < lVisibleStartX then
|
|
|
|
+ FSelectionStartX := lVisibleStartX;
|
|
|
|
+ if FSelectionEndX > lVisibleEndX then
|
|
|
|
+ FSelectionEndX := lVisibleEndX;
|
|
|
|
+
|
|
|
|
+ FVisibleText := Copy(lText, lFirstVisibleIndex, lLastVisibleIndex - lFirstVisibleIndex);
|
|
|
|
+ FDrawOffset := FTextOffset - FDrawOffset;
|
|
|
|
+ Write('Value : "',FValue,'", Visible : "',FVisibleText,'"');
|
|
|
|
+ Write(', Sel: [',FSelectionStart,' - ',FSelectionEnd,']');
|
|
|
|
+ Write(', SelX: [',FSelectionStartX,' - ',FSelectionEndX,']');
|
|
|
|
+ WriteLn(', Cur: ',FCursorPos,', Cur X: [',FCursorX,']');
|
|
|
|
+
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+Function TEdit.CalcCharOffset(aXOffset: TFresnelLength) : Integer;
|
|
|
|
+
|
|
|
|
+var
|
|
|
|
+ lText: string;
|
|
|
|
+ lChar: string; // current character (UTF8)
|
|
|
|
+ lCharNum : Integer; // Character index (in codepoints)
|
|
|
|
+ lClosestX: TFresnelLength; // X position closest to cursor X)
|
|
|
|
+ lCurrWidth: TFresnelLength; // Running total character width
|
|
|
|
+ lPos: integer; // Character loop pos
|
|
|
|
+ lFont : IFresnelFont;
|
|
|
|
+ lLeftSideMargin : TFresnelLength;
|
|
|
|
+ lCharWidth : TFresnelLength;
|
|
|
|
+begin
|
|
|
|
+ lLeftSideMargin:=GetComputedLength(fcaPaddingLeft);
|
|
|
|
+ lFont:=GetFont;
|
|
|
|
+ if aXOffset > 0 then
|
|
|
|
+ lClosestX := -MaxInt
|
|
|
|
+ else
|
|
|
|
+ lClosestX := +MaxInt;
|
|
|
|
+ lText := Value;
|
|
|
|
+ lChar := '';
|
|
|
|
+ lCurrWidth := 0;
|
|
|
|
+ lPos := 0;
|
|
|
|
+ lCharNum := 0;
|
|
|
|
+ // Correct for margin and text offset.
|
|
|
|
+ aXOffset:=aXOffset + FTextOffset - lLeftSideMargin;
|
|
|
|
+ while (lPos <= Length(lText)) do
|
|
|
|
+ begin
|
|
|
|
+ lPos:=UTF8CharAtBytePos(lText,lPos,lChar);
|
|
|
|
+ lCharWidth:=CalcCharSize(lFont,lChar);
|
|
|
|
+ lCurrWidth := lCurrWidth + lCharWidth;
|
|
|
|
+ if abs(lCurrWidth - aXOffset) < abs(lClosestX - aXOffset) then
|
|
|
|
+ begin
|
|
|
|
+ // We're getting closer to the actual char.
|
|
|
|
+ lClosestX := lCurrWidth;
|
|
|
|
+ Result := lCharNum;
|
|
|
|
+ end
|
|
|
|
+ else
|
|
|
|
+ // We're moving away from the char, so break
|
|
|
|
+ break;
|
|
|
|
+ Inc(lCharNum);
|
|
|
|
+ end;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+Function TEdit.CalcXOffset(aCharPos: Integer; aUseOffset: Boolean = true; aUseDrawText : Boolean = False) : TFresnelLength;
|
|
|
|
+
|
|
|
|
+var
|
|
|
|
+ lText: string;
|
|
|
|
+ lChar: string; // current character (UTF8)
|
|
|
|
+ lCharNum : Integer; // Character index (in codepoints)
|
|
|
|
+ lPos: integer; // Character loop pos
|
|
|
|
+ lFont : IFresnelFont;
|
|
|
|
+ lCharWidth : TFresnelLength;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ lFont := GetFont;
|
|
|
|
+ if aUseDrawText then
|
|
|
|
+ lText := GetDrawText
|
|
|
|
+ else
|
|
|
|
+ lText := Value;
|
|
|
|
+ lChar := '';
|
|
|
|
+ lPos := 0;
|
|
|
|
+ lCharNum := 0;
|
|
|
|
+ if aUseOffset then
|
|
|
|
+ Result:=GetComputedLength(fcaPaddingLeft)-FTextOffset
|
|
|
|
+ else
|
|
|
|
+ Result:=0;
|
|
|
|
+ While (lCharNum<=aCharPos) and (lPos<=Length(lText)) do
|
|
|
|
+ begin
|
|
|
|
+ lPos := UTF8CharAtBytePos(lText,lPos,lChar);
|
|
|
|
+ lCharWidth:=CalcCharSize(lFont,lChar);
|
|
|
|
+ Result:=Result+lCharWidth;
|
|
|
|
+ Inc(lCharNum);
|
|
|
|
+ end;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.CalcOffsets;
|
|
|
|
+{
|
|
|
|
+ FCursorPos -> FCursorX;
|
|
|
|
+ FCursorX -> FTextOffset
|
|
|
|
+}
|
|
|
|
+var
|
|
|
|
+ lVisibleWidth,
|
|
|
|
+ lLeftSideMargin,
|
|
|
|
+ lRightSideMargin,
|
|
|
|
+ lMaxWidth,
|
|
|
|
+ lCursorX: TFresnelLength;
|
|
|
|
+ r: TFresnelRect;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ lLeftSideMargin:=GetComputedLength(fcaPaddingLeft);
|
|
|
|
+ lRightSideMargin:=GetComputedLength(fcaPaddingRight);
|
|
|
|
+ lCursorX:=CalcXOffset(FCursorPos,False,True);
|
|
|
|
+ r := RenderedContentBox;
|
|
|
|
+ lVisibleWidth := (r.Width - (lLeftSideMargin+lRightSideMargin));
|
|
|
|
+ lMaxWidth:=(lCursorX - (lVisibleWidth + CursorWidth) ); // 2 = Cursor width
|
|
|
|
+ if FTextOffset < lMaxWidth then
|
|
|
|
+ FTextOffset := lMaxWidth
|
|
|
|
+ else if FTextOffset>lCursorX then
|
|
|
|
+ begin
|
|
|
|
+ FTextOffset := lCursorX;
|
|
|
|
+ if lCursorX <> 0 then
|
|
|
|
+ FTextOffset:=FTextOffset-CursorWidth;
|
|
|
|
+ end;
|
|
|
|
+ FCursorX := lCursorX - FTextOffset + lLeftSideMargin;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+procedure TEdit.SetPlaceHolder(const aValue: TFresnelCaption);
|
|
|
|
+begin
|
|
|
|
+ if FPlaceHolder=aValue then Exit;
|
|
|
|
+ FPlaceHolder:=aValue;
|
|
|
|
+ // only invalidate if the text is empty.
|
|
|
|
+ if (FValue='') then
|
|
|
|
+ DomChanged;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.SetReadOnly(const aValue: Boolean);
|
|
|
|
+begin
|
|
|
|
+ if FReadOnly=aValue then Exit;
|
|
|
|
+ FReadOnly:=aValue;
|
|
|
|
+ DomChanged; // CSS with Attribute selector can check for readonly
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.SetSelectionEnd(const aValue: Integer);
|
|
|
|
+begin
|
|
|
|
+ if FSelectionEnd=aValue then Exit;
|
|
|
|
+ FSelectionEnd:=aValue;
|
|
|
|
+ if FSelectionEnd<0 then FSelectionEnd:=0;
|
|
|
|
+ if FSelectionEnd>Length(FValue) then FSelectionEnd:=Length(FValue);
|
|
|
|
+ EditParamsChanged;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.SetSelectionStart(const aValue: Integer);
|
|
|
|
+begin
|
|
|
|
+ if FSelectionStart=aValue then Exit;
|
|
|
|
+ if FSelectionStart<0 then FSelectionEnd:=0;
|
|
|
|
+ if FSelectionStart>Length(FValue) then FSelectionStart:=Length(FValue);
|
|
|
|
+ EditParamsChanged;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.NormalizeSelection;
|
|
|
|
+var
|
|
|
|
+ lTmp : Integer;
|
|
|
|
+begin
|
|
|
|
+ if FSelectionStart>FSelectionEnd then
|
|
|
|
+ begin
|
|
|
|
+ lTmp:=FSelectionStart;
|
|
|
|
+ FSelectionStart:=FSelectionEnd;
|
|
|
|
+ FSelectionEnd:=lTmp;
|
|
|
|
+ end;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.HandleFocusChange(GotFocus: Boolean);
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ if GotFocus then
|
|
|
|
+ CursorTimer.BlinkControl:=Self
|
|
|
|
+ else
|
|
|
|
+ CursorTimer.BlinkControl:=Nil;
|
|
|
|
+ DomChanged
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+function TEdit.DoDispatchEvent(aEvent: TAbstractEvent): Integer;
|
|
|
|
+
|
|
|
|
+var
|
|
|
|
+ lFresnelEvt : TFresnelEvent absolute aEvent;
|
|
|
|
+ lKeyEvent : TFresnelKeyEvent absolute aEvent;
|
|
|
|
+ lMouseEvent : TFresnelMouseEvent absolute aEvent;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ Result:=inherited DoDispatchEvent(aEvent);
|
|
|
|
+ if not (aEvent is TFresnelEvent) then
|
|
|
|
+ exit;
|
|
|
|
+ if lFresnelEvt.DefaultPrevented then
|
|
|
|
+ exit;
|
|
|
|
+ Case aEvent.EventID of
|
|
|
|
+ evtBlur,
|
|
|
|
+ evtFocus:
|
|
|
|
+ HandleFocusChange(aEvent.EventID=evtFocus);
|
|
|
|
+ evtKeyDown
|
|
|
|
+ {, evtKeyUp
|
|
|
|
+ , evtKeyPress
|
|
|
|
+ }:
|
|
|
|
+ if (aEvent is TFresnelKeyEvent) then
|
|
|
|
+ begin
|
|
|
|
+ if aEvent.EventID=evtKeyDown then
|
|
|
|
+ HandleKeyDown(lKeyEvent);
|
|
|
|
+ end;
|
|
|
|
+ evtMouseDown:
|
|
|
|
+ if (aEvent is TFresnelMouseEvent) then
|
|
|
|
+ HandleMouseDown(lMouseEvent);
|
|
|
|
+ evtMouseMove:
|
|
|
|
+ if (aEvent is TFresnelMouseEvent) then
|
|
|
|
+ HandleMouseMove(lMouseEvent);
|
|
|
|
+ evtMouseUp:
|
|
|
|
+ if (aEvent is TFresnelMouseEvent) then
|
|
|
|
+ HandleMouseUp(lMouseEvent);
|
|
|
|
+
|
|
|
|
+ else
|
|
|
|
+ // not handled here...
|
|
|
|
+ end;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.MaybeRecalcParams;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ if not FRecalcParams then
|
|
|
|
+ exit;
|
|
|
|
+ CalcOffsets;
|
|
|
|
+ CalcTextDrawInfo;
|
|
|
|
+ FRecalcParams:=False;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.DoRender(aRenderer: IFresnelRenderer);
|
|
|
|
+
|
|
|
|
+var
|
|
|
|
+ lCaption : string;
|
|
|
|
+ lBackColor, lColorFP, lShadowColor: TFPColor;
|
|
|
|
+ lTopSideMargin, lLeftSideMargin, lRadius, lOffsetX, lOffsetY : TFresnelLength;
|
|
|
|
+ lHaveShadow : Boolean;
|
|
|
|
+ R,RSel : TFresnelRect;
|
|
|
|
+ SelWidth : TFresnelLength;
|
|
|
|
+
|
|
|
|
+ Procedure DrawCursor;
|
|
|
|
+ var
|
|
|
|
+ lCur : TFresnelRect;
|
|
|
|
+ begin
|
|
|
|
+ if not FCursorVisible then exit;
|
|
|
|
+ lCur:=RenderedContentBox;
|
|
|
|
+ lCur.Left:=lCur.Left+FCursorX; // CursorX has leftmargin
|
|
|
|
+ lCur.Right:=lCur.Left+CursorWidth;
|
|
|
|
+ aRenderer.FillRect(colBlack,lCur);
|
|
|
|
+ end;
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ // These will calculate FVisibleText etc.
|
|
|
|
+ MaybeRecalcParams;
|
|
|
|
+ // lRightSideMargin:=GetComputedLength(fcaPaddingRight);
|
|
|
|
+ lCaption:=FVisibleText;
|
|
|
|
+ if lCaption='' then
|
|
|
|
+ begin
|
|
|
|
+ DrawCursor;
|
|
|
|
+ exit;
|
|
|
|
+ end;
|
|
|
|
+ lLeftSideMargin:=GetComputedLength(fcaPaddingLeft);
|
|
|
|
+ lColorFP:=GetComputedColor(fcaColor,colTransparent);
|
|
|
|
+ lBackColor:=GetComputedColor(fcaBackgroundColor,colTransparent);
|
|
|
|
+ if lColorFP.Alpha=alphaTransparent then
|
|
|
|
+ exit;
|
|
|
|
+ R:=RenderedContentBox;
|
|
|
|
+ aRenderer.FillRect(lBackColor,R);
|
|
|
|
+ // Selection background.
|
|
|
|
+ RSel:=R;
|
|
|
|
+ SelWidth:=Abs(FSelectionStartX-FSelectionEndX);
|
|
|
|
+ RSel.Left:=RSel.Left+lLeftSideMargin+FSelectionStartX;
|
|
|
|
+ RSel.Right:=RSel.Left+SelWidth;
|
|
|
|
+ lBackColor:=fpimage.colDkBlue; // GetComputedColor(fcaSelectionBackGroundColor,colTransparent);
|
|
|
|
+ aRenderer.FillRect(fpimage.colDkBlue,RSel);
|
|
|
|
+ lHaveShadow:=GetComputedTextShadow(lOffsetX, lOffsetY, lRadius, lShadowColor);
|
|
|
|
+ if lHaveShadow then
|
|
|
|
+ aRenderer.AddTextShadow(lOffsetX,lOffsetY,lShadowColor,lRadius);
|
|
|
|
+ lLeftSideMargin:=GetComputedLength(fcaPaddingLeft);
|
|
|
|
+ lTopSideMargin:=GetComputedLength(fcaPaddingTop);
|
|
|
|
+ if FSelectionStart<>FSelectionEnd then
|
|
|
|
+ begin
|
|
|
|
+ if FSelectionStart>0 then
|
|
|
|
+ aRenderer.TextOut(R.Left+lLeftSideMargin,R.Top+lTopSideMargin,Font,lColorFP,Copy(lCaption,1,1+FSelectionStart));
|
|
|
|
+ aRenderer.TextOut(RSel.Left,RSel.Top+lTopSideMargin,Font,colWhite,Copy(lCaption,1+FSelectionStart,FSelectionEnd-FSelectionStart));
|
|
|
|
+ if FSelectionEnd<Length(lCaption) then
|
|
|
|
+ aRenderer.TextOut(RSel.Right,Rsel.Top+lTopSideMargin,Font,lColorFP,Copy(lCaption,1+FSelectionEnd));
|
|
|
|
+ end
|
|
|
|
+ else
|
|
|
|
+ begin
|
|
|
|
+ aRenderer.TextOut(R.Left+lLeftSideMargin,R.Top+lTopSideMargin,Font,lColorFP,lCaption);
|
|
|
|
+ end;
|
|
|
|
+
|
|
|
|
+ if FCursorVisible then
|
|
|
|
+ DrawCursor;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TEdit.SetSelection(const aStart, aEnd: Integer);
|
|
|
|
+var
|
|
|
|
+ lLen : Integer;
|
|
|
|
+begin
|
|
|
|
+ if (FSelectionEnd=aEnd) and (FSelectionStart=aStart) then Exit;
|
|
|
|
+ FSelectionEnd:=aEnd;
|
|
|
|
+ FSelectionStart:=aStart;
|
|
|
|
+ if FSelectionEnd<0 then FSelectionEnd:=0;
|
|
|
|
+ if FSelectionStart<0 then FSelectionStart:=0;
|
|
|
|
+ lLen:=Length(FValue);
|
|
|
|
+ if FSelectionEnd>lLen then FSelectionEnd:=lLen;
|
|
|
|
+ if FSelectionStart>lLen then FSelectionStart:=lLen;
|
|
|
|
+ EditParamsChanged;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+class function TEdit.HandleFocus: Boolean;
|
|
|
|
+begin
|
|
|
|
+ Result:=True;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+function TEdit.CanFocus: Boolean;
|
|
|
|
+begin
|
|
|
|
+ Result:=Enabled;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+class constructor TEdit.Init;
|
|
|
|
+
|
|
|
|
+begin
|
|
|
|
+ _FresnelEditTypeID:=CSSRegistry.AddType(CSSTypeName).Index;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+class function TEdit.CSSTypeID: TCSSNumericalID;
|
|
|
|
+begin
|
|
|
|
+ Result:=_FresnelEditTypeID;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+class function TEdit.CSSTypeName: TCSSString;
|
|
|
|
+begin
|
|
|
|
+ Result:='edit';
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+class function TEdit.GetCSSTypeStyle: TCSSString;
|
|
|
|
+begin
|
|
|
|
+ // needs checking. Normally you cannot have controls inside an edit.
|
|
|
|
+ Result:='edit { padding: 3px; ' +
|
|
|
|
+ ' border: 1px solid black; ' +
|
|
|
|
+ ' display: inline flow; }';
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+constructor TEdit.Create(aOwner: TComponent);
|
|
|
|
+begin
|
|
|
|
+ inherited Create(aOwner);
|
|
|
|
+ FEnabled:=True;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+end.
|
|
|
|
+
|