1/*
2 * Copyright 2002-2006, project beam (http://sourceforge.net/projects/beam).
3 * All rights reserved. Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Oliver Tappe <beam@hirschkaefer.de>
7 */
8
9#include "AutoCompleterDefaultImpl.h"
10
11#include <ListView.h>
12#include <Screen.h>
13#include <ScrollView.h>
14#include <Window.h>
15
16
17// #pragma mark - BDefaultPatternSelector
18
19
20void
21BDefaultPatternSelector::SelectPatternBounds(const BString& text,
22	int32 caretPos, int32* start, int32* length)
23{
24	if (!start || !length)
25		return;
26	*start = 0;
27	*length = text.Length();
28}
29
30
31// #pragma mark - BDefaultCompletionStyle
32
33
34BDefaultCompletionStyle::BDefaultCompletionStyle(
35		BAutoCompleter::EditView* editView,
36		BAutoCompleter::ChoiceModel* choiceModel,
37		BAutoCompleter::ChoiceView* choiceView,
38		BAutoCompleter::PatternSelector* patternSelector)
39	:
40	CompletionStyle(editView, choiceModel, choiceView, patternSelector),
41	fSelectedIndex(-1),
42	fPatternStartPos(0),
43	fPatternLength(0),
44	fIgnoreEditViewStateChanges(false)
45{
46}
47
48
49BDefaultCompletionStyle::~BDefaultCompletionStyle()
50{
51}
52
53
54bool
55BDefaultCompletionStyle::Select(int32 index)
56{
57	if (!fChoiceView || !fChoiceModel || index == fSelectedIndex
58		|| index < -1 || index >= fChoiceModel->CountChoices()) {
59		return false;
60	}
61
62	fSelectedIndex = index;
63	fChoiceView->SelectChoiceAt(index);
64	return true;
65}
66
67
68bool
69BDefaultCompletionStyle::SelectNext(bool wrap)
70{
71	if (!fChoiceModel || fChoiceModel->CountChoices() == 0)
72		return false;
73
74	int32 newIndex = fSelectedIndex + 1;
75	if (newIndex >= fChoiceModel->CountChoices()) {
76		if (wrap)
77			newIndex = 0;
78		else
79			newIndex = fSelectedIndex;
80	}
81	return Select(newIndex);
82}
83
84
85bool
86BDefaultCompletionStyle::SelectPrevious(bool wrap)
87{
88	if (!fChoiceModel || fChoiceModel->CountChoices() == 0)
89		return false;
90
91	int32 newIndex = fSelectedIndex - 1;
92	if (newIndex < 0) {
93		if (wrap)
94			newIndex = fChoiceModel->CountChoices() - 1;
95		else
96			newIndex = 0;
97	}
98	return Select(newIndex);
99}
100
101
102bool
103BDefaultCompletionStyle::IsChoiceSelected() const
104{
105	return fSelectedIndex >= 0;
106}
107
108
109int32
110BDefaultCompletionStyle::SelectedChoiceIndex() const
111{
112	return fSelectedIndex;
113}
114
115
116void
117BDefaultCompletionStyle::ApplyChoice(bool hideChoices)
118{
119	if (!fChoiceModel || !fChoiceView || !fEditView || fSelectedIndex < 0)
120		return;
121
122	BString completedText(fFullEnteredText);
123	completedText.Remove(fPatternStartPos, fPatternLength);
124	const BString& choiceStr = fChoiceModel->ChoiceAt(fSelectedIndex)->Text();
125	completedText.Insert(choiceStr, fPatternStartPos);
126
127	fIgnoreEditViewStateChanges = true;
128
129	fFullEnteredText = completedText;
130	fPatternLength = choiceStr.Length();
131	fEditView->SetEditViewState(completedText,
132		fPatternStartPos + choiceStr.Length());
133
134	if (hideChoices) {
135		fChoiceView->HideChoices();
136		Select(-1);
137	}
138
139	fIgnoreEditViewStateChanges = false;
140}
141
142
143void
144BDefaultCompletionStyle::CancelChoice()
145{
146	if (!fChoiceView || !fEditView)
147		return;
148	if (fChoiceView->ChoicesAreShown()) {
149		fIgnoreEditViewStateChanges = true;
150
151		fEditView->SetEditViewState(fFullEnteredText,
152			fPatternStartPos + fPatternLength);
153		fChoiceView->HideChoices();
154		Select(-1);
155
156		fIgnoreEditViewStateChanges = false;
157	}
158}
159
160void
161BDefaultCompletionStyle::EditViewStateChanged(bool updateChoices)
162{
163	if (fIgnoreEditViewStateChanges || !fChoiceModel || !fChoiceView
164		|| !fEditView) {
165		return;
166	}
167
168	BString text;
169	int32 caretPos;
170	fEditView->GetEditViewState(text, &caretPos);
171	if (fFullEnteredText == text)
172		return;
173
174	fFullEnteredText = text;
175
176	if (!updateChoices)
177		return;
178
179	fPatternSelector->SelectPatternBounds(text, caretPos, &fPatternStartPos,
180		&fPatternLength);
181	BString pattern(text.String() + fPatternStartPos, fPatternLength);
182	fChoiceModel->FetchChoicesFor(pattern);
183
184	Select(-1);
185	// show a single choice only if it doesn't match the pattern exactly:
186	if (fChoiceModel->CountChoices() > 1 || (fChoiceModel->CountChoices() == 1
187			&& pattern.ICompare(fChoiceModel->ChoiceAt(0)->Text())) != 0) {
188		fChoiceView->ShowChoices(this);
189		fChoiceView->SelectChoiceAt(fSelectedIndex);
190	} else
191		fChoiceView->HideChoices();
192}
193
194
195// #pragma mark - BDefaultChoiceView::ListView
196
197
198static const int32 MSG_INVOKED = 'invk';
199
200
201BDefaultChoiceView::ListView::ListView(
202		BAutoCompleter::CompletionStyle* completer)
203	:
204	BListView(BRect(0, 0, 100, 100), "ChoiceViewList"),
205	fCompleter(completer)
206{
207	// we need to check if user clicks outside of window-bounds:
208	SetEventMask(B_POINTER_EVENTS);
209}
210
211
212void
213BDefaultChoiceView::ListView::AttachedToWindow()
214{
215	SetTarget(this);
216	SetInvocationMessage(new BMessage(MSG_INVOKED));
217	BListView::AttachedToWindow();
218}
219
220
221void
222BDefaultChoiceView::ListView::SelectionChanged()
223{
224	fCompleter->Select(CurrentSelection(0));
225}
226
227
228void
229BDefaultChoiceView::ListView::MessageReceived(BMessage* message)
230{
231	switch(message->what) {
232		case MSG_INVOKED:
233			fCompleter->ApplyChoice();
234			break;
235		default:
236			BListView::MessageReceived(message);
237	}
238}
239
240
241void
242BDefaultChoiceView::ListView::MouseDown(BPoint point)
243{
244	if (!Window()->Frame().Contains(ConvertToScreen(point)))
245		// click outside of window, so we close it:
246		Window()->Quit();
247	else
248		BListView::MouseDown(point);
249}
250
251
252// #pragma mark - BDefaultChoiceView::ListItem
253
254
255BDefaultChoiceView::ListItem::ListItem(const BAutoCompleter::Choice* choice)
256	:
257	BListItem()
258{
259	fPreText = choice->DisplayText();
260	if (choice->MatchLen() > 0) {
261		fPreText.MoveInto(fMatchText, choice->MatchPos(), choice->MatchLen());
262		fPreText.MoveInto(fPostText, choice->MatchPos(), fPreText.Length());
263	}
264}
265
266
267void
268BDefaultChoiceView::ListItem::DrawItem(BView* owner, BRect frame,
269	bool complete)
270{
271	rgb_color textColor;
272	rgb_color nonMatchTextColor;
273	rgb_color backColor;
274	rgb_color matchColor;
275	if (IsSelected()) {
276		textColor = ui_color(B_MENU_SELECTED_ITEM_TEXT_COLOR);
277		backColor = ui_color(B_MENU_SELECTED_BACKGROUND_COLOR);
278	} else {
279		textColor = ui_color(B_DOCUMENT_TEXT_COLOR);
280		backColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR);
281	}
282	matchColor = tint_color(backColor, (B_NO_TINT + B_DARKEN_1_TINT) / 2);
283	nonMatchTextColor = tint_color(backColor, 1.7);
284
285	BFont font;
286	font_height fontHeight;
287	owner->GetFont(&font);
288	font.GetHeight(&fontHeight);
289	float xPos = frame.left + 1;
290	float yPos = frame.top + fontHeight.ascent;
291	float w;
292	if (fPreText.Length()) {
293		w = owner->StringWidth(fPreText.String());
294		owner->SetLowColor(backColor);
295		owner->FillRect(BRect(xPos, frame.top, xPos + w - 1, frame.bottom),
296			B_SOLID_LOW);
297		owner->SetHighColor(nonMatchTextColor);
298		owner->DrawString(fPreText.String(), BPoint(xPos, yPos));
299		xPos += w;
300	}
301	if (fMatchText.Length()) {
302		w = owner->StringWidth(fMatchText.String());
303		owner->SetLowColor(matchColor);
304		owner->FillRect(BRect(xPos, frame.top, xPos + w - 1, frame.bottom),
305			B_SOLID_LOW);
306		owner->SetHighColor(textColor);
307		owner->DrawString(fMatchText.String(), BPoint(xPos, yPos));
308		xPos += w;
309	}
310	if (fPostText.Length()) {
311		w = owner->StringWidth(fPostText.String());
312		owner->SetLowColor(backColor);
313		owner->FillRect(BRect(xPos, frame.top, xPos + w - 1, frame.bottom),
314			B_SOLID_LOW);
315		owner->SetHighColor(nonMatchTextColor);
316		owner->DrawString(fPostText.String(), BPoint(xPos, yPos));
317	}
318}
319
320
321// #pragma mark - BDefaultChoiceView
322
323
324BDefaultChoiceView::BDefaultChoiceView()
325	:
326	fWindow(NULL),
327	fListView(NULL),
328	fMaxVisibleChoices(8)
329{
330
331}
332
333
334BDefaultChoiceView::~BDefaultChoiceView()
335{
336	HideChoices();
337}
338
339
340void
341BDefaultChoiceView::SelectChoiceAt(int32 index)
342{
343	if (fListView && fListView->LockLooper()) {
344		if (index < 0)
345			fListView->DeselectAll();
346		else {
347			fListView->Select(index);
348			fListView->ScrollToSelection();
349		}
350		fListView->UnlockLooper();
351	}
352}
353
354
355void
356BDefaultChoiceView::ShowChoices(BAutoCompleter::CompletionStyle* completer)
357{
358	if (!completer)
359		return;
360
361	HideChoices();
362
363	BAutoCompleter::ChoiceModel* choiceModel = completer->GetChoiceModel();
364	BAutoCompleter::EditView* editView = completer->GetEditView();
365
366	if (!editView || !choiceModel || choiceModel->CountChoices() == 0)
367		return;
368
369	fListView = new ListView(completer);
370	int32 count = choiceModel->CountChoices();
371	for(int32 i = 0; i<count; ++i) {
372		fListView->AddItem(
373			new ListItem(choiceModel->ChoiceAt(i))
374		);
375	}
376
377	BScrollView *scrollView = NULL;
378	if (count > fMaxVisibleChoices) {
379		scrollView = new BScrollView("", fListView, B_FOLLOW_NONE, 0, false, true, B_NO_BORDER);
380	}
381
382	fWindow = new BWindow(BRect(0, 0, 100, 100), "", B_BORDERED_WINDOW_LOOK,
383		B_NORMAL_WINDOW_FEEL, B_NOT_MOVABLE | B_WILL_ACCEPT_FIRST_CLICK
384			| B_AVOID_FOCUS | B_ASYNCHRONOUS_CONTROLS);
385	if (scrollView != NULL)
386		fWindow->AddChild(scrollView);
387	else
388		fWindow->AddChild(fListView);
389
390	int32 visibleCount = min_c(count, fMaxVisibleChoices);
391	float listHeight = fListView->ItemFrame(visibleCount - 1).bottom + 1;
392
393	BRect pvRect = editView->GetAdjustmentFrame();
394	BRect listRect = pvRect;
395	listRect.bottom = listRect.top + listHeight - 1;
396	BRect screenRect = BScreen().Frame();
397	if (listRect.bottom + 1 + listHeight <= screenRect.bottom)
398		listRect.OffsetTo(pvRect.left, pvRect.bottom + 1);
399	else
400		listRect.OffsetTo(pvRect.left, pvRect.top - listHeight);
401
402	if (scrollView != NULL) {
403		// Moving here to cut off the scrollbar top
404		scrollView->MoveTo(0, -1);
405		// Adding the 1 and 2 to cut-off the scroll-bar top, right and bottom
406		scrollView->ResizeTo(listRect.Width() + 1, listRect.Height() + 2);
407		// Move here to compensate for the above
408		fListView->MoveTo(0, 1);
409		fListView->ResizeTo(listRect.Width() - B_V_SCROLL_BAR_WIDTH, listRect.Height());
410	} else {
411		fListView->MoveTo(0, 0);
412		fListView->ResizeTo(listRect.Width(), listRect.Height());
413	}
414	fWindow->MoveTo(listRect.left, listRect.top);
415	fWindow->ResizeTo(listRect.Width(), listRect.Height());
416	fWindow->Show();
417}
418
419
420void
421BDefaultChoiceView::HideChoices()
422{
423	if (fWindow && fWindow->Lock()) {
424		fWindow->Quit();
425		fWindow = NULL;
426		fListView = NULL;
427	}
428}
429
430
431bool
432BDefaultChoiceView::ChoicesAreShown()
433{
434	return (fWindow != NULL);
435}
436
437
438int32
439BDefaultChoiceView::CountVisibleChoices() const
440{
441	if (fListView)
442		return min_c(fMaxVisibleChoices, fListView->CountItems());
443	else
444		return 0;
445}
446
447
448void
449BDefaultChoiceView::SetMaxVisibleChoices(int32 choices)
450{
451	if (choices < 1)
452		choices = 1;
453	if (choices == fMaxVisibleChoices)
454		return;
455
456	fMaxVisibleChoices = choices;
457
458	// TODO: Update live?
459}
460
461
462int32
463BDefaultChoiceView::MaxVisibleChoices() const
464{
465	return fMaxVisibleChoices;
466}
467
468