root/src/apps/webpositive/autocompletion/AutoCompleterDefaultImpl.cpp
/*
 * Copyright 2002-2006, project beam (http://sourceforge.net/projects/beam).
 * All rights reserved. Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Oliver Tappe <beam@hirschkaefer.de>
 */

#include "AutoCompleterDefaultImpl.h"

#include <ListView.h>
#include <Screen.h>
#include <ScrollView.h>
#include <Window.h>


// #pragma mark - BDefaultPatternSelector


void
BDefaultPatternSelector::SelectPatternBounds(const BString& text,
        int32 caretPos, int32* start, int32* length)
{
        if (!start || !length)
                return;
        *start = 0;
        *length = text.Length();
}


// #pragma mark - BDefaultCompletionStyle


BDefaultCompletionStyle::BDefaultCompletionStyle(
                BAutoCompleter::EditView* editView,
                BAutoCompleter::ChoiceModel* choiceModel,
                BAutoCompleter::ChoiceView* choiceView,
                BAutoCompleter::PatternSelector* patternSelector)
        :
        CompletionStyle(editView, choiceModel, choiceView, patternSelector),
        fSelectedIndex(-1),
        fPatternStartPos(0),
        fPatternLength(0),
        fIgnoreEditViewStateChanges(false)
{
}


BDefaultCompletionStyle::~BDefaultCompletionStyle()
{
}


bool
BDefaultCompletionStyle::Select(int32 index)
{
        if (!fChoiceView || !fChoiceModel || index == fSelectedIndex
                || index < -1 || index >= fChoiceModel->CountChoices()) {
                return false;
        }

        fSelectedIndex = index;
        fChoiceView->SelectChoiceAt(index);
        return true;
}


bool
BDefaultCompletionStyle::SelectNext(bool wrap)
{
        if (!fChoiceModel || fChoiceModel->CountChoices() == 0)
                return false;

        int32 newIndex = fSelectedIndex + 1;
        if (newIndex >= fChoiceModel->CountChoices()) {
                if (wrap)
                        newIndex = 0;
                else
                        newIndex = fSelectedIndex;
        }
        return Select(newIndex);
}


bool
BDefaultCompletionStyle::SelectPrevious(bool wrap)
{
        if (!fChoiceModel || fChoiceModel->CountChoices() == 0)
                return false;

        int32 newIndex = fSelectedIndex - 1;
        if (newIndex < 0) {
                if (wrap)
                        newIndex = fChoiceModel->CountChoices() - 1;
                else
                        newIndex = 0;
        }
        return Select(newIndex);
}


bool
BDefaultCompletionStyle::IsChoiceSelected() const
{
        return fSelectedIndex >= 0;
}


int32
BDefaultCompletionStyle::SelectedChoiceIndex() const
{
        return fSelectedIndex;
}


void
BDefaultCompletionStyle::ApplyChoice(bool hideChoices)
{
        if (!fChoiceModel || !fChoiceView || !fEditView || fSelectedIndex < 0)
                return;

        BString completedText(fFullEnteredText);
        completedText.Remove(fPatternStartPos, fPatternLength);
        const BString& choiceStr = fChoiceModel->ChoiceAt(fSelectedIndex)->Text();
        completedText.Insert(choiceStr, fPatternStartPos);

        fIgnoreEditViewStateChanges = true;

        fFullEnteredText = completedText;
        fPatternLength = choiceStr.Length();
        fEditView->SetEditViewState(completedText, 
                fPatternStartPos + choiceStr.Length());

        if (hideChoices) {
                fChoiceView->HideChoices();
                Select(-1);
        }

        fIgnoreEditViewStateChanges = false;
}


void
BDefaultCompletionStyle::CancelChoice()
{
        if (!fChoiceView || !fEditView)
                return;
        if (fChoiceView->ChoicesAreShown()) {
                fIgnoreEditViewStateChanges = true;

                fEditView->SetEditViewState(fFullEnteredText,
                        fPatternStartPos + fPatternLength);
                fChoiceView->HideChoices();
                Select(-1);

                fIgnoreEditViewStateChanges = false;
        }
}

void
BDefaultCompletionStyle::EditViewStateChanged(bool updateChoices)
{
        if (fIgnoreEditViewStateChanges || !fChoiceModel || !fChoiceView
                || !fEditView) {
                return;
        }

        BString text;
        int32 caretPos;
        fEditView->GetEditViewState(text, &caretPos);
        if (fFullEnteredText == text)
                return;

        fFullEnteredText = text;

        if (!updateChoices)
                return;

        fPatternSelector->SelectPatternBounds(text, caretPos, &fPatternStartPos, 
                &fPatternLength);
        BString pattern(text.String() + fPatternStartPos, fPatternLength);
        fChoiceModel->FetchChoicesFor(pattern);

        Select(-1);
        // show a single choice only if it doesn't match the pattern exactly:
        if (fChoiceModel->CountChoices() > 1 || (fChoiceModel->CountChoices() == 1
                        && pattern.ICompare(fChoiceModel->ChoiceAt(0)->Text())) != 0) {
                fChoiceView->ShowChoices(this);
                fChoiceView->SelectChoiceAt(fSelectedIndex);
        } else
                fChoiceView->HideChoices();
}


// #pragma mark - BDefaultChoiceView::ListView


static const int32 MSG_INVOKED = 'invk';


BDefaultChoiceView::ListView::ListView(
                BAutoCompleter::CompletionStyle* completer)
        :
        BListView(BRect(0, 0, 100, 100), "ChoiceViewList"),
        fCompleter(completer)
{
        // we need to check if user clicks outside of window-bounds:
        SetEventMask(B_POINTER_EVENTS);
}


void
BDefaultChoiceView::ListView::AttachedToWindow()
{
        SetTarget(this);
        SetInvocationMessage(new BMessage(MSG_INVOKED));
        BListView::AttachedToWindow();
}


void
BDefaultChoiceView::ListView::SelectionChanged()
{
        fCompleter->Select(CurrentSelection(0));
}


void
BDefaultChoiceView::ListView::MessageReceived(BMessage* message)
{
        switch(message->what) {
                case MSG_INVOKED:
                        fCompleter->ApplyChoice();
                        break;
                default:
                        BListView::MessageReceived(message);
        }
}


void
BDefaultChoiceView::ListView::MouseDown(BPoint point)
{
        if (!Window()->Frame().Contains(ConvertToScreen(point)))
                // click outside of window, so we close it:
                Window()->Quit();
        else
                BListView::MouseDown(point);
}


// #pragma mark - BDefaultChoiceView::ListItem


BDefaultChoiceView::ListItem::ListItem(const BAutoCompleter::Choice* choice)
        :
        BListItem()
{
        fPreText = choice->DisplayText();
        if (choice->MatchLen() > 0) {
                fPreText.MoveInto(fMatchText, choice->MatchPos(), choice->MatchLen());
                fPreText.MoveInto(fPostText, choice->MatchPos(), fPreText.Length());
        }
}


void
BDefaultChoiceView::ListItem::DrawItem(BView* owner, BRect frame,
        bool complete)
{
        rgb_color textColor;
        rgb_color nonMatchTextColor;
        rgb_color backColor;
        rgb_color matchColor;
        if (IsSelected()) {
                textColor = ui_color(B_LIST_SELECTED_ITEM_TEXT_COLOR);
                backColor = ui_color(B_LIST_SELECTED_BACKGROUND_COLOR);
        } else {
                textColor = ui_color(B_LIST_ITEM_TEXT_COLOR);
                backColor = ui_color(B_LIST_BACKGROUND_COLOR);
        }
        matchColor = tint_color(backColor, (B_NO_TINT + B_DARKEN_1_TINT) / 2);
        if (textColor.red + textColor.green + textColor.blue > 128 * 3)
                nonMatchTextColor = tint_color(textColor, 1.2);
        else
                nonMatchTextColor = tint_color(textColor, 0.75);
        BFont font;
        font_height fontHeight;
        owner->GetFont(&font);
        font.GetHeight(&fontHeight);
        float xPos = frame.left + 1;
        float yPos = frame.top + fontHeight.ascent;
        float w;
        if (fPreText.Length()) {
                w = owner->StringWidth(fPreText.String());
                owner->SetLowColor(backColor);
                owner->FillRect(BRect(xPos, frame.top, xPos + w - 1, frame.bottom),
                        B_SOLID_LOW);
                owner->SetHighColor(nonMatchTextColor);
                owner->DrawString(fPreText.String(), BPoint(xPos, yPos));
                xPos += w;
        }
        if (fMatchText.Length()) {
                w = owner->StringWidth(fMatchText.String());
                owner->SetLowColor(matchColor);
                owner->FillRect(BRect(xPos, frame.top, xPos + w - 1, frame.bottom), 
                        B_SOLID_LOW);
                owner->SetHighColor(textColor);
                owner->DrawString(fMatchText.String(), BPoint(xPos, yPos));
                xPos += w;
        }
        if (fPostText.Length()) {
                w = owner->StringWidth(fPostText.String());
                owner->SetLowColor(backColor);
                owner->FillRect(BRect(xPos, frame.top, xPos + w - 1, frame.bottom), 
                        B_SOLID_LOW);
                owner->SetHighColor(nonMatchTextColor);
                owner->DrawString(fPostText.String(), BPoint(xPos, yPos));
        }
}


// #pragma mark - BDefaultChoiceView


BDefaultChoiceView::BDefaultChoiceView()
        :
        fWindow(NULL),
        fListView(NULL),
        fMaxVisibleChoices(8)
{
        
}


BDefaultChoiceView::~BDefaultChoiceView()
{
        HideChoices();
}
        

void
BDefaultChoiceView::SelectChoiceAt(int32 index)
{
        if (fListView && fListView->LockLooper()) {
                if (index < 0)
                        fListView->DeselectAll();
                else {
                        fListView->Select(index);
                        fListView->ScrollToSelection();
                }
                fListView->UnlockLooper();
        }
}


void
BDefaultChoiceView::ShowChoices(BAutoCompleter::CompletionStyle* completer)
{
        if (!completer)
                return;

        HideChoices();

        BAutoCompleter::ChoiceModel* choiceModel = completer->GetChoiceModel();
        BAutoCompleter::EditView* editView = completer->GetEditView();

        if (!editView || !choiceModel || choiceModel->CountChoices() == 0)
                return;

        fListView = new ListView(completer);
        int32 count = choiceModel->CountChoices();
        for(int32 i = 0; i<count; ++i) {
                fListView->AddItem(
                        new ListItem(choiceModel->ChoiceAt(i))
                );
        }

        BScrollView *scrollView = NULL;
        if (count > fMaxVisibleChoices) {
                scrollView = new BScrollView("", fListView, B_FOLLOW_NONE, 0, false, true, B_NO_BORDER);
        }

        fWindow = new BWindow(BRect(0, 0, 100, 100), "", B_BORDERED_WINDOW_LOOK, 
                B_NORMAL_WINDOW_FEEL, B_NOT_MOVABLE | B_WILL_ACCEPT_FIRST_CLICK 
                        | B_AVOID_FOCUS | B_ASYNCHRONOUS_CONTROLS);
        if (scrollView != NULL)
                fWindow->AddChild(scrollView);
        else
                fWindow->AddChild(fListView);

        int32 visibleCount = min_c(count, fMaxVisibleChoices);
        float listHeight = fListView->ItemFrame(visibleCount - 1).bottom + 1;

        BRect pvRect = editView->GetAdjustmentFrame();
        BRect listRect = pvRect;
        listRect.bottom = listRect.top + listHeight - 1;
        BRect screenRect = BScreen().Frame();
        if (listRect.bottom + 1 + listHeight <= screenRect.bottom)
                listRect.OffsetTo(pvRect.left, pvRect.bottom + 1);
        else
                listRect.OffsetTo(pvRect.left, pvRect.top - listHeight);

        if (scrollView != NULL) {
                // Moving here to cut off the scrollbar top
                scrollView->MoveTo(0, -1);
                // Adding the 1 and 2 to cut-off the scroll-bar top, right and bottom
                scrollView->ResizeTo(listRect.Width() + 1, listRect.Height() + 2);
                // Move here to compensate for the above
                fListView->MoveTo(0, 1);
                fListView->ResizeTo(listRect.Width() - B_V_SCROLL_BAR_WIDTH, listRect.Height());
        } else {
                fListView->MoveTo(0, 0);
                fListView->ResizeTo(listRect.Width(), listRect.Height());
        }
        fWindow->MoveTo(listRect.left, listRect.top);
        fWindow->ResizeTo(listRect.Width(), listRect.Height());
        fWindow->Show();
}


void
BDefaultChoiceView::HideChoices()
{
        if (fWindow && fWindow->Lock()) {
                fWindow->Quit();
                fWindow = NULL;
                fListView = NULL;
        }
}


bool
BDefaultChoiceView::ChoicesAreShown()
{
        return (fWindow != NULL);
}


int32
BDefaultChoiceView::CountVisibleChoices() const
{
        if (fListView)
                return min_c(fMaxVisibleChoices, fListView->CountItems());
        else
                return 0;
}


void
BDefaultChoiceView::SetMaxVisibleChoices(int32 choices)
{
        if (choices < 1)
                choices = 1;
        if (choices == fMaxVisibleChoices)
                return;

        fMaxVisibleChoices = choices;

        // TODO: Update live?
}


int32
BDefaultChoiceView::MaxVisibleChoices() const
{
        return fMaxVisibleChoices;
}