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_LIST_SELECTED_ITEM_TEXT_COLOR);
277		backColor = ui_color(B_LIST_SELECTED_BACKGROUND_COLOR);
278	} else {
279		textColor = ui_color(B_LIST_ITEM_TEXT_COLOR);
280		backColor = ui_color(B_LIST_BACKGROUND_COLOR);
281	}
282	matchColor = tint_color(backColor, (B_NO_TINT + B_DARKEN_1_TINT) / 2);
283	if (textColor.red + textColor.green + textColor.blue > 128 * 3)
284		nonMatchTextColor = tint_color(textColor, 1.2);
285	else
286		nonMatchTextColor = tint_color(textColor, 0.75);
287	BFont font;
288	font_height fontHeight;
289	owner->GetFont(&font);
290	font.GetHeight(&fontHeight);
291	float xPos = frame.left + 1;
292	float yPos = frame.top + fontHeight.ascent;
293	float w;
294	if (fPreText.Length()) {
295		w = owner->StringWidth(fPreText.String());
296		owner->SetLowColor(backColor);
297		owner->FillRect(BRect(xPos, frame.top, xPos + w - 1, frame.bottom),
298			B_SOLID_LOW);
299		owner->SetHighColor(nonMatchTextColor);
300		owner->DrawString(fPreText.String(), BPoint(xPos, yPos));
301		xPos += w;
302	}
303	if (fMatchText.Length()) {
304		w = owner->StringWidth(fMatchText.String());
305		owner->SetLowColor(matchColor);
306		owner->FillRect(BRect(xPos, frame.top, xPos + w - 1, frame.bottom),
307			B_SOLID_LOW);
308		owner->SetHighColor(textColor);
309		owner->DrawString(fMatchText.String(), BPoint(xPos, yPos));
310		xPos += w;
311	}
312	if (fPostText.Length()) {
313		w = owner->StringWidth(fPostText.String());
314		owner->SetLowColor(backColor);
315		owner->FillRect(BRect(xPos, frame.top, xPos + w - 1, frame.bottom),
316			B_SOLID_LOW);
317		owner->SetHighColor(nonMatchTextColor);
318		owner->DrawString(fPostText.String(), BPoint(xPos, yPos));
319	}
320}
321
322
323// #pragma mark - BDefaultChoiceView
324
325
326BDefaultChoiceView::BDefaultChoiceView()
327	:
328	fWindow(NULL),
329	fListView(NULL),
330	fMaxVisibleChoices(8)
331{
332
333}
334
335
336BDefaultChoiceView::~BDefaultChoiceView()
337{
338	HideChoices();
339}
340
341
342void
343BDefaultChoiceView::SelectChoiceAt(int32 index)
344{
345	if (fListView && fListView->LockLooper()) {
346		if (index < 0)
347			fListView->DeselectAll();
348		else {
349			fListView->Select(index);
350			fListView->ScrollToSelection();
351		}
352		fListView->UnlockLooper();
353	}
354}
355
356
357void
358BDefaultChoiceView::ShowChoices(BAutoCompleter::CompletionStyle* completer)
359{
360	if (!completer)
361		return;
362
363	HideChoices();
364
365	BAutoCompleter::ChoiceModel* choiceModel = completer->GetChoiceModel();
366	BAutoCompleter::EditView* editView = completer->GetEditView();
367
368	if (!editView || !choiceModel || choiceModel->CountChoices() == 0)
369		return;
370
371	fListView = new ListView(completer);
372	int32 count = choiceModel->CountChoices();
373	for(int32 i = 0; i<count; ++i) {
374		fListView->AddItem(
375			new ListItem(choiceModel->ChoiceAt(i))
376		);
377	}
378
379	BScrollView *scrollView = NULL;
380	if (count > fMaxVisibleChoices) {
381		scrollView = new BScrollView("", fListView, B_FOLLOW_NONE, 0, false, true, B_NO_BORDER);
382	}
383
384	fWindow = new BWindow(BRect(0, 0, 100, 100), "", B_BORDERED_WINDOW_LOOK,
385		B_NORMAL_WINDOW_FEEL, B_NOT_MOVABLE | B_WILL_ACCEPT_FIRST_CLICK
386			| B_AVOID_FOCUS | B_ASYNCHRONOUS_CONTROLS);
387	if (scrollView != NULL)
388		fWindow->AddChild(scrollView);
389	else
390		fWindow->AddChild(fListView);
391
392	int32 visibleCount = min_c(count, fMaxVisibleChoices);
393	float listHeight = fListView->ItemFrame(visibleCount - 1).bottom + 1;
394
395	BRect pvRect = editView->GetAdjustmentFrame();
396	BRect listRect = pvRect;
397	listRect.bottom = listRect.top + listHeight - 1;
398	BRect screenRect = BScreen().Frame();
399	if (listRect.bottom + 1 + listHeight <= screenRect.bottom)
400		listRect.OffsetTo(pvRect.left, pvRect.bottom + 1);
401	else
402		listRect.OffsetTo(pvRect.left, pvRect.top - listHeight);
403
404	if (scrollView != NULL) {
405		// Moving here to cut off the scrollbar top
406		scrollView->MoveTo(0, -1);
407		// Adding the 1 and 2 to cut-off the scroll-bar top, right and bottom
408		scrollView->ResizeTo(listRect.Width() + 1, listRect.Height() + 2);
409		// Move here to compensate for the above
410		fListView->MoveTo(0, 1);
411		fListView->ResizeTo(listRect.Width() - B_V_SCROLL_BAR_WIDTH, listRect.Height());
412	} else {
413		fListView->MoveTo(0, 0);
414		fListView->ResizeTo(listRect.Width(), listRect.Height());
415	}
416	fWindow->MoveTo(listRect.left, listRect.top);
417	fWindow->ResizeTo(listRect.Width(), listRect.Height());
418	fWindow->Show();
419}
420
421
422void
423BDefaultChoiceView::HideChoices()
424{
425	if (fWindow && fWindow->Lock()) {
426		fWindow->Quit();
427		fWindow = NULL;
428		fListView = NULL;
429	}
430}
431
432
433bool
434BDefaultChoiceView::ChoicesAreShown()
435{
436	return (fWindow != NULL);
437}
438
439
440int32
441BDefaultChoiceView::CountVisibleChoices() const
442{
443	if (fListView)
444		return min_c(fMaxVisibleChoices, fListView->CountItems());
445	else
446		return 0;
447}
448
449
450void
451BDefaultChoiceView::SetMaxVisibleChoices(int32 choices)
452{
453	if (choices < 1)
454		choices = 1;
455	if (choices == fMaxVisibleChoices)
456		return;
457
458	fMaxVisibleChoices = choices;
459
460	// TODO: Update live?
461}
462
463
464int32
465BDefaultChoiceView::MaxVisibleChoices() const
466{
467	return fMaxVisibleChoices;
468}
469
470