MODULE WMSearchComponents; (** AUTHOR "staubesv"; PURPOSE "Search components"; *)

IMPORT
	Inputs, Strings, Texts, TextUtilities, UTF8Strings, WMWindowManager,
	WMRectangles, WMGraphics, WMMessages, WMComponents, WMStandardComponents, WMTextView, WMEditors;

CONST

	SearchStringMaxLen = 128;

TYPE

	SearchString* = ARRAY SearchStringMaxLen OF CHAR;
	UcsSearchString* = ARRAY SearchStringMaxLen OF Texts.Char32;

	StackData = POINTER TO ARRAY OF LONGINT;

	PositionStack = OBJECT
	VAR
		data: StackData;
		size, top: LONGINT;

		PROCEDURE & Init*;
		BEGIN
			size := 32;
			NEW(data, 32);
		END Init;

		PROCEDURE Push(l: LONGINT);
		BEGIN
			IF top = size THEN Expand END;
			data[top] := l;
			INC(top);
		END Push;

		PROCEDURE Pop(): LONGINT;
		VAR val: LONGINT;
		BEGIN
			IF top > 0 THEN
				DEC(top);
				val := data[top];
			ELSE
				val := -1;
			END;
			RETURN val;
		END Pop;

		PROCEDURE Expand;
		VAR
			newSize, i: LONGINT;
			newData: StackData;
		BEGIN
			newSize := 2*size;
			NEW(newData, newSize);
			FOR i := 0 TO size-1 DO
				data[i] := newData[i];
			END;
			size := newSize;
			data := newData;
		END Expand;

		PROCEDURE Invalidate;
		BEGIN
			IF size > 32 THEN Init END;
			top := 0;
		END Invalidate;

	END PositionStack;

TYPE

	Highlight = POINTER TO RECORD
		this : WMTextView.Highlight;
		next : Highlight;
	END;

	Highlights = OBJECT
	VAR
		textView : WMTextView.TextView;
		highlights : Highlight;

		PROCEDURE Add(from, to : LONGINT);
		VAR h : Highlight;
		BEGIN {EXCLUSIVE}
			NEW(h); h.next := highlights; highlights := h;
			h.this := textView.CreateHighlight();
			h.this.SetColor(SHORT(0FFFF0080H));
			h.this.SetKind(WMTextView.HLOver);
			h.this.SetFromTo(from, to);
		END Add;

		PROCEDURE RemoveAll;
		VAR h : Highlight;
		BEGIN {EXCLUSIVE}
			h := highlights;
			WHILE (h # NIL) DO
				textView.RemoveHighlight(h.this);
				h := h.next;
			END;
			highlights := NIL;
		END RemoveAll;

		PROCEDURE &Init*(textView : WMTextView.TextView);
		BEGIN
			ASSERT(textView # NIL);
			SELF.textView := textView;
		END Init;

	END Highlights;

TYPE

	SearchPanel* = OBJECT(WMComponents.VisualComponent)
	VAR
		wrap, caseSensitive, backwards*, highlightAll : BOOLEAN;

		upperPanel, lowerPanel: WMStandardComponents.Panel;
		searchBtn, replBtn, replAllBtn, closeBtn, wrapBtn, caseSensitiveBtn, directionBtn, markAllBtn : WMStandardComponents.Button;
		searchEdit-, replEdit-: WMEditors.Editor;
		searchLabel, replLabel: WMStandardComponents.Label;

		textView: WMTextView.TextView;
		text: Texts.Text;

		pos, len: LONGINT;
		hitCount : LONGINT;
		posValid : BOOLEAN;
		positionStack: PositionStack;
		highlights : Highlights;

		lastPos : LONGINT;
		lastBackwards : BOOLEAN;

		PROCEDURE & Init*;
		BEGIN
			Init^;
			SetNameAsString(StrSearchPanel);

			wrap := FALSE;
			caseSensitive := TRUE;
			highlightAll := FALSE;

			hitCount := 0;
			backwards := FALSE;
			lastPos := -1; lastBackwards := FALSE;

			NEW(upperPanel);
			upperPanel.alignment.Set(WMComponents.AlignTop);
			upperPanel.bounds.SetHeight(20);
			AddInternalComponent(upperPanel);

			NEW(searchLabel);
			searchLabel.alignment.Set(WMComponents.AlignLeft);
			searchLabel.bounds.SetWidth(40); searchLabel.fillColor.Set(SHORT(0FFFFFFFFH));
			searchLabel.alignH.Set(WMGraphics.AlignCenter);
			searchLabel.SetCaption("Search");
			upperPanel.AddInternalComponent(searchLabel);

			NEW(searchEdit);
			searchEdit.alignment.Set(WMComponents.AlignLeft);
			searchEdit.bounds.SetWidth(200); searchEdit.multiLine.Set(FALSE);
			searchEdit.tv.borders.Set(WMRectangles.MakeRect(3, 3, 1, 1));
			searchEdit.tv.showBorder.Set(TRUE);
			searchEdit.fillColor.Set(SHORT(0FFFFFFFFH));
			searchEdit.onEnter.Add(SearchHandler);
			searchEdit.text.onTextChanged.Add(TextChanged);
			searchEdit.tv.SetExtFocusHandler(FocusHandler);
			upperPanel.AddInternalComponent(searchEdit);

			NEW(replLabel);
			replLabel.alignment.Set(WMComponents.AlignLeft);
			replLabel.bounds.SetWidth(50); replLabel.fillColor.Set(SHORT(0FFFFFFFFH));
			replLabel.alignH.Set(WMGraphics.AlignCenter);
			replLabel.SetCaption("Replace");
			upperPanel.AddInternalComponent(replLabel);

			NEW(replEdit);
			replEdit.alignment.Set(WMComponents.AlignClient);
			replEdit.bounds.SetWidth(150); replEdit.multiLine.Set(FALSE);
			replEdit.tv.borders.Set(WMRectangles.MakeRect(3, 3, 1, 1));
			replEdit.tv.showBorder.Set(TRUE);
			replEdit.fillColor.Set(SHORT(0FFFFFFFFH));
			replEdit.onEnter.Add(ReplaceHandler);
			upperPanel.AddInternalComponent(replEdit);

			NEW(lowerPanel);
			lowerPanel.alignment.Set(WMComponents.AlignTop);
			lowerPanel.bounds.SetHeight(20);
			AddInternalComponent(lowerPanel);

			NEW(searchBtn);
			searchBtn.alignment.Set(WMComponents.AlignLeft);
			searchBtn.caption.SetAOC("Search");
			searchBtn.bounds.SetWidth(80);
			searchBtn.onClick.Add(SearchHandler);
			lowerPanel.AddInternalComponent(searchBtn);

			NEW(replBtn);
			replBtn.alignment.Set(WMComponents.AlignLeft);
			replBtn.caption.SetAOC("Replace");
			replBtn.bounds.SetWidth(80);
			replBtn.onClick.Add(ReplaceHandler);
			lowerPanel.AddInternalComponent(replBtn);

			NEW(replAllBtn);
			replAllBtn.alignment.Set(WMComponents.AlignLeft);
			replAllBtn.caption.SetAOC("Replace All");
			replAllBtn.bounds.SetWidth(80);
			replAllBtn.onClick.Add(ReplaceAllHandler);
			lowerPanel.AddInternalComponent(replAllBtn);

			NEW(wrapBtn);
			wrapBtn.alignment.Set(WMComponents.AlignLeft);
			wrapBtn.caption.SetAOC("Wrap");
			wrapBtn.isToggle.Set(TRUE);
			wrapBtn.SetPressed(wrap);
			wrapBtn.onClick.Add(WrapHandler);
			lowerPanel.AddInternalComponent(wrapBtn);

			NEW(caseSensitiveBtn);
			caseSensitiveBtn.alignment.Set(WMComponents.AlignLeft);
			caseSensitiveBtn.caption.SetAOC("CaseSensitive");
			caseSensitiveBtn.isToggle.Set(TRUE);
			caseSensitiveBtn.SetPressed(caseSensitive);
			caseSensitiveBtn.bounds.SetWidth(80);
			caseSensitiveBtn.onClick.Add(CaseSensitiveHandler);
			lowerPanel.AddInternalComponent(caseSensitiveBtn);

			NEW(directionBtn);
			directionBtn.alignment.Set(WMComponents.AlignLeft);
			directionBtn.caption.SetAOC("Backwards");
			directionBtn.isToggle.Set(TRUE);
			directionBtn.SetPressed(backwards);
			directionBtn.bounds.SetWidth(80);
			directionBtn.onClick.Add(DirectionHandler);
			lowerPanel.AddInternalComponent(directionBtn);

			NEW(markAllBtn);
			markAllBtn.alignment.Set(WMComponents.AlignLeft);
			markAllBtn.bounds.SetWidth(80);
			markAllBtn.caption.SetAOC("Highlight");
			markAllBtn.isToggle.Set(TRUE);
			markAllBtn.SetPressed(highlightAll);
			markAllBtn.onClick.Add(HighlightAllHandler);
			lowerPanel.AddInternalComponent(markAllBtn);

			NEW(closeBtn);
			closeBtn.alignment.Set(WMComponents.AlignLeft);
			closeBtn.caption.SetAOC("Close");
			closeBtn.bounds.SetWidth(80);
			closeBtn.onClick.Add(CloseHandler);
			lowerPanel.AddInternalComponent(closeBtn);

			NEW(positionStack);
		END Init;

		PROCEDURE SetToLastSelection*;
		VAR
			currentSearchString, searchString : SearchString;
			selectionText : Texts.Text; from, to : Texts.TextPosition;
			a, b : LONGINT;
		BEGIN
			searchString := "";
			IF Texts.GetLastSelection(selectionText, from, to) THEN
				selectionText.AcquireRead;
				a := Strings.Min(from.GetPosition(), to.GetPosition());
				b := Strings.Max(from.GetPosition(), to.GetPosition());
				IF ((b - a) <= SearchStringMaxLen) THEN
					TextUtilities.SubTextToStr(selectionText, a, b - a, searchString);
				END;
				Strings.TrimWS(searchString);
				selectionText.ReleaseRead;
			END;
			searchEdit.GetAsString(currentSearchString);
			IF (searchString # "") & (searchString # currentSearchString) THEN
				searchEdit.SetAsString(searchString);
				searchEdit.text.AcquireRead;
				searchEdit.tv.selection.SetFromTo(0, searchEdit.text.GetLength());
				searchEdit.text.ReleaseRead;
			END;
		END SetToLastSelection;

		PROCEDURE SetText*(t: Texts.Text);
		BEGIN
			text := t;
			posValid := FALSE
		END SetText;

		PROCEDURE SetTextView*(tv: WMTextView.TextView);
		BEGIN
			IF (textView # tv) THEN
				textView := tv;
				IF highlights # NIL THEN
					DisableUpdate; highlights.RemoveAll; EnableUpdate; textView.Invalidate;
				END;
				NEW(highlights, textView);
			END;
			posValid := FALSE;
		END SetTextView;

		PROCEDURE ToggleVisibility*;
		VAR searchString : SearchString;
		BEGIN
			IF ~visible.Get() THEN
				visible.Set(TRUE);
				searchEdit.SetAsString("");
				SetToLastSelection;
				searchEdit.SetFocus;
			ELSE
				searchEdit.GetAsString(searchString);
				IF searchString = "" THEN
					replEdit.SetAsString("");
					visible.Set(FALSE);
					IF (textView # NIL) THEN textView.SetFocus; END;
				ELSE
					searchEdit.SetAsString("");
					searchEdit.SetFocus;
				END;
			END;
		END ToggleVisibility;

		PROCEDURE HandlePreviousNext*(forward : BOOLEAN);
		VAR oldBackwards : BOOLEAN;
		BEGIN
			IF (textView # NIL) THEN textView.SetFocus; END;
			IF visible.Get() THEN
				oldBackwards := backwards;
				backwards := ~forward;
				SearchHandler(NIL, NIL);
				backwards := oldBackwards;
			END;
		END HandlePreviousNext;

		PROCEDURE HandleTab*() : BOOLEAN;
		BEGIN
			IF (visible.Get()) THEN
				(* hack - should not access hasFocus field *)
				IF searchEdit.tv.hasFocus THEN
					replEdit.SetFocus;
					replEdit.Invalidate;
					RETURN TRUE;
				ELSIF replEdit.tv.hasFocus THEN
					searchEdit.SetFocus;
					searchEdit.Invalidate;
					RETURN TRUE;
				END;
			END;
			RETURN FALSE;
		END HandleTab;

		PROCEDURE HandleShortcut*(ucs : LONGINT; flags : SET; keysym : LONGINT) : BOOLEAN;

			PROCEDURE ControlKeyDown(flags : SET) : BOOLEAN;
			BEGIN
				RETURN (flags * Inputs.Ctrl # {}) & (flags - Inputs.Ctrl = {});
			END ControlKeyDown;

		BEGIN
			IF (keysym = 06H) & ControlKeyDown(flags)THEN (* CTRL-F *)
				ToggleVisibility;
			ELSIF (keysym= 0EH) & ControlKeyDown(flags) THEN (* CTRL-N *)
				HandlePreviousNext(TRUE);
			ELSIF (keysym = 10H) & ControlKeyDown(flags) THEN (* CTRL-P *)
				HandlePreviousNext(FALSE);
			ELSIF (keysym = Inputs.KsTab) & (flags = {}) THEN (* TAB *)
				RETURN HandleTab();
			ELSE
				RETURN FALSE; (* Key not handled *)
			END;
			RETURN TRUE;
		END HandleShortcut;

		PROCEDURE FocusHandler(hasFocus: BOOLEAN);
		BEGIN
			IF textView = NIL THEN RETURN END;
			IF hasFocus THEN
				pos := textView.cursor.GetPosition();
				positionStack.Invalidate;
			END;
		END FocusHandler;

		PROCEDURE WrapHandler(sender, data: ANY);
		BEGIN
			wrap := ~wrap;
		END WrapHandler;

		PROCEDURE CaseSensitiveHandler(sender, data : ANY);
		BEGIN
			caseSensitive := ~caseSensitive;
		END CaseSensitiveHandler;

		PROCEDURE DirectionHandler(sender, data : ANY);
		BEGIN
			backwards := ~backwards;
		END DirectionHandler;

		PROCEDURE HighlightAllHandler(sender, data : ANY);
		BEGIN
			highlightAll := ~highlightAll;
			IF highlightAll THEN
				DisableUpdate; SearchAndHighlightAll; EnableUpdate; markAllBtn.Invalidate; textView.Invalidate;
			ELSE
				DisableUpdate; RemoveHighlights; EnableUpdate; textView.Invalidate;
				markAllBtn.SetCaption("Highlight");
			END;
		END HighlightAllHandler;

		PROCEDURE TextChanged(sender, data: ANY);
		VAR
			changeInfo: Texts.TextChangeInfo;
			from : LONGINT;
		BEGIN
			IF ~IsCallFromSequencer() THEN
				(* 	We need to use DisableUpdate/EnableUpdate in order to get reasonable performance... This requires
					the call to come from the component's sequencer *)
				sequencer.ScheduleEvent(SELF.TextChanged, sender, data)
			ELSE
				IF data IS Texts.TextChangeInfo THEN
					changeInfo := data(Texts.TextChangeInfo);
					IF (changeInfo.op = Texts.OpInsert) & (changeInfo.len = 1) THEN
						positionStack.Push(pos);
						IF highlightAll THEN DisableUpdate; RemoveHighlights; SearchAndHighlightAll; EnableUpdate; markAllBtn.Invalidate; textView.Invalidate; END;
						SearchAndHighlight(pos);
					ELSIF (changeInfo.op = Texts.OpDelete) & (changeInfo.len = 1) THEN
						from := positionStack.Pop();
						IF from = 1 THEN from := pos END;
						IF highlightAll THEN DisableUpdate; RemoveHighlights; SearchAndHighlightAll; EnableUpdate; markAllBtn.Invalidate; textView.Invalidate;END;
						SearchAndHighlight(from);
					ELSE
						positionStack.Invalidate();
						IF (textView # NIL) THEN  textView.selection.SetFromTo(0, 0); END;
						IF highlightAll THEN DisableUpdate; RemoveHighlights; EnableUpdate; markAllBtn.Invalidate; textView.Invalidate; END;
					END;
				END;
			END;
		END TextChanged;

		PROCEDURE SearchHandler*(sender, data: ANY);
		BEGIN
			IF textView = NIL THEN RETURN END;
			SearchAndHighlight(textView.cursor.GetPosition());
		END SearchHandler;

		PROCEDURE ReplaceHandler(sender, data: ANY);
		VAR replStr : SearchString; ucsStr: UcsSearchString; idx: LONGINT;
		BEGIN
			IF text = NIL THEN RETURN END;
			IF posValid THEN
				replEdit.GetAsString(replStr);
				idx := 0;
				UTF8Strings.UTF8toUnicode(replStr, ucsStr, idx);
				text.AcquireWrite();
				Replace(ucsStr);
				text.ReleaseWrite();
				Highlight;
				SearchHandler(sender, data);
			END;
		END ReplaceHandler;

		PROCEDURE ReplaceAllHandler(sender, data: ANY);
		VAR
			searchStr, replStr : SearchString;
			ucsSearchStr, ucsReplStr : UcsSearchString;
			oldBackwards : BOOLEAN;
			idx: LONGINT;
		BEGIN
			IF text = NIL THEN RETURN END;
			replEdit.GetAsString(replStr);
			idx := 0;
			UTF8Strings.UTF8toUnicode(replStr, ucsReplStr, idx);
			searchEdit.GetAsString(searchStr);
			idx := 0;
			UTF8Strings.UTF8toUnicode(searchStr, ucsSearchStr, idx);
			text.AcquireWrite();
			text.AcquireRead();
			oldBackwards := backwards;
			backwards := FALSE;
			Search(ucsSearchStr, 0);
			WHILE posValid DO
				Replace(ucsReplStr);
				Search(ucsSearchStr, pos + len);
			END;
			backwards := oldBackwards;
			text.ReleaseRead();
			text.ReleaseWrite();
		END ReplaceAllHandler;

		PROCEDURE Replace(CONST ucsStr: ARRAY OF Texts.Char32);
		BEGIN
			text.Delete(pos, len);
			text.InsertUCS32(pos, ucsStr);
			len := TextUtilities.UCS32StrLength(ucsStr);
			posValid := FALSE;
		END Replace;

		PROCEDURE Search(CONST ucsStr: ARRAY OF Texts.Char32; from: LONGINT);
		BEGIN
			IF ucsStr[0] = 0 THEN posValid := FALSE; RETURN; END;
			IF caseSensitive & ~backwards THEN
				pos := TextUtilities.Pos(ucsStr, from, text);
			ELSE
				(* We want to search the text that's on the left hand side of the cursor. If we start searching at position 'from',
				we also consider the character at the current cursor position, which is on the left hand side of the cursro *)
				IF (backwards) & (from > 1) THEN DEC(from); END;

				pos := TextUtilities.GenericPos(ucsStr, from, text, ~caseSensitive, backwards);
			END;
			len := TextUtilities.UCS32StrLength(ucsStr);
			IF pos > -1 THEN posValid := TRUE
			ELSE posValid := FALSE
			END;
		END Search;

		PROCEDURE SearchAndHighlight(from: LONGINT);
		VAR searchStr : SearchString; ucsStr: UcsSearchString; length, idx : LONGINT;
		BEGIN
			IF text = NIL THEN RETURN END;
			searchEdit.GetAsString(searchStr);
			IF searchStr # "" THEN
				idx := 0;
				UTF8Strings.UTF8toUnicode(searchStr, ucsStr, idx);
				text.AcquireRead();
				Search(ucsStr, from);
				(* Detect whether we have found the last search result again but in different search direction *)
				IF (((pos = lastPos) OR (lastPos = -1)) & (lastBackwards # backwards))  THEN
					length := TextUtilities.UCS32StrLength(ucsStr);
					IF backwards THEN
						IF from >= length - 1 THEN
							Search(ucsStr, from - Strings.Length(searchStr));
						END;
					ELSE
						IF from + length < text.GetLength() THEN
							Search(ucsStr, from + Strings.Length(searchStr));
						END;
					END;
				END;
				IF (pos = -1) & wrap THEN
					IF backwards THEN
						Search(ucsStr, text.GetLength() - 1);
					ELSE
						Search(ucsStr, 0);
					END;
				END;
				text.ReleaseRead();
			END;
			lastPos := pos; lastBackwards := backwards;
			Highlight;
		END SearchAndHighlight;

		PROCEDURE SearchAndHighlightAll;
		VAR
			searchString : SearchString;
			ucsStr : UcsSearchString;
			caption : ARRAY 32 OF CHAR; nbr : ARRAY 12 OF CHAR;
			length, idx, pos : LONGINT; from : LONGINT;
		BEGIN
			IF (text = NIL) OR (highlights = NIL) THEN RETURN; END;
			hitCount := 0;
			searchEdit.GetAsString(searchString);
			IF searchString # "" THEN
				idx := 0;
				UTF8Strings.UTF8toUnicode(searchString, ucsStr, idx);
				length := TextUtilities.UCS32StrLength(ucsStr);
				from := 0; pos := 0;
				WHILE(pos >=0) DO
					text.AcquireRead;
					IF caseSensitive THEN pos := TextUtilities.Pos(ucsStr, from, text);
					ELSE pos := TextUtilities.GenericPos(ucsStr, from, text, ~caseSensitive, FALSE);
					END;
					text.ReleaseRead;
					IF pos >= 0 THEN
						INC(hitCount);
						highlights.Add(pos, pos + length);
						from := pos + 1;
					END;
				END;
			END;
			caption := "Highlight:";
			Strings.IntToStr(hitCount, nbr); Strings.Append(caption, nbr);
			markAllBtn.caption.SetAOC(caption);
		END SearchAndHighlightAll;

		PROCEDURE RemoveHighlights;
		BEGIN
			IF (highlights # NIL) THEN highlights.RemoveAll; END;
		END RemoveHighlights;

		PROCEDURE Highlight;
		VAR searchString : SearchString;
		BEGIN
			IF textView = NIL THEN RETURN END;
			IF posValid THEN
				textView.selection.SetFrom(pos);
				textView.selection.SetTo(pos + len);
				IF backwards THEN
					textView.cursor.SetPosition(pos);
				ELSE
					textView.cursor.SetPosition(pos + len);
				END;
			END;
			searchEdit.GetAsString(searchString);
			IF (searchString = "") THEN textView.selection.SetFromTo(0, 0); END;
		END Highlight;

		PROCEDURE CloseHandler(sender, data: ANY);
		BEGIN
			IF highlights # NIL THEN
				DisableUpdate; RemoveHighlights; EnableUpdate; textView.Invalidate;
			END;
			visible.Set(FALSE);
			IF textView # NIL THEN  textView.selection.SetFromTo(0, 0); END;
		END CloseHandler;

		PROCEDURE SetSettings*(wrap, caseSensitive, backwards, highlightAll : BOOLEAN);
		BEGIN
			SELF.wrap := wrap;
			SELF.caseSensitive := caseSensitive;
			SELF.backwards := backwards;
			SELF.highlightAll := highlightAll;

			wrapBtn.SetPressed(wrap);
			caseSensitiveBtn.SetPressed(caseSensitive);
			directionBtn.SetPressed(backwards);
			markAllBtn.SetPressed(highlightAll);
		END SetSettings;

		PROCEDURE GetSettings*(VAR wrap, caseSensitive, backwards, highlightAll : BOOLEAN);
		BEGIN
			wrap := SELF.wrap;
			caseSensitive := SELF.caseSensitive;
			backwards := SELF.backwards;
			highlightAll := SELF.highlightAll;
		END GetSettings;

		PROCEDURE Finalize;
		BEGIN
			Finalize^;
			IF searchEdit # NIL THEN searchEdit.text.onTextChanged.Remove(TextChanged); END;
		END Finalize;

	END SearchPanel;

TYPE

	SearchWindow* = OBJECT(WMComponents.FormWindow)
	VAR
		searchPanel : SearchPanel;
		hasBeenClosed- : BOOLEAN;

		PROCEDURE SetTextView*(textView : WMTextView.TextView; text : Texts.Text);
		BEGIN
			ASSERT((textView # NIL) & (text # NIL));
			searchPanel.SetText(text);
			searchPanel.SetTextView(textView);
		END SetTextView;

		PROCEDURE &New*(textView : WMTextView.TextView; text : Texts.Text);
		BEGIN
			ASSERT((textView # NIL) & (text # NIL));
			hasBeenClosed := FALSE;
			NEW(searchPanel);
			searchPanel.fillColor.Set(WMGraphics.White);
			searchPanel.alignment.Set(WMComponents.AlignClient);
			searchPanel.bounds.SetExtents(700, 40);
			searchPanel.closeBtn.visible.Set(FALSE);
			SetTextView(textView, text);
			Init(searchPanel.bounds.GetWidth(), searchPanel.bounds.GetHeight(), FALSE);
			SetContent(searchPanel);
			SetTitle(Strings.NewString("Search"));
			WMWindowManager.DefaultAddWindow(SELF);
		END New;

		PROCEDURE Close;
		BEGIN
			Close^;
			IF searchPanel.highlights # NIL THEN
				searchPanel.RemoveHighlights; searchPanel.textView.Invalidate;
			END;
			searchPanel.textView.selection.SetFromTo(0, 0);
			hasBeenClosed := TRUE;
		END Close;

		PROCEDURE HandleShortcut(ucs : LONGINT; flags : SET; keysym : LONGINT) : BOOLEAN;

			PROCEDURE ControlKeyDown(flags : SET) : BOOLEAN;
			BEGIN
				RETURN (flags * Inputs.Ctrl # {}) & (flags - Inputs.Ctrl = {});
			END ControlKeyDown;

			PROCEDURE HandlePreviousNext(forward : BOOLEAN);
			VAR oldBackwards : BOOLEAN;
			BEGIN
				IF searchPanel.visible.Get() THEN
					oldBackwards := searchPanel.backwards;
					searchPanel.backwards := ~forward;
					searchPanel.SearchHandler(NIL, NIL);
					searchPanel.backwards := oldBackwards;
				END;
			END HandlePreviousNext;

		BEGIN
			IF (keysym = 06H) & ControlKeyDown(flags)THEN (* CTRL-F *)
				searchPanel.searchEdit.SetAsString("");
				searchPanel.searchEdit.SetFocus;
			ELSIF (keysym= 0EH) & ControlKeyDown(flags) THEN (* CTRL-N *)
				HandlePreviousNext(TRUE);
			ELSIF (keysym = 10H) & ControlKeyDown(flags) THEN (* CTRL-P *)
				HandlePreviousNext(FALSE);
			ELSE
				RETURN FALSE; (* Key not handled *)
			END;
			RETURN TRUE;
		END HandleShortcut;

		PROCEDURE Handle(VAR m: WMMessages.Message);
		BEGIN
			IF m.msgType = WMMessages.MsgKey THEN
				IF ~HandleShortcut(m.x, m.flags, m.y) THEN
					Handle^(m);
				END;
			ELSE Handle^(m)
			END
		END Handle;

	END SearchWindow;

VAR
	StrSearchPanel : Strings.String;

BEGIN
	StrSearchPanel := Strings.NewString("SearchPanel");
END WMSearchComponents.