root/src/apps/haikudepot/textview/TextEditor.cpp
/*
 * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
 * All rights reserved. Distributed under the terms of the MIT License.
 */

#include "TextEditor.h"

#include <algorithm>
#include <stdio.h>


TextEditor::TextEditor()
        :
        fDocument(),
        fLayout(),
        fSelection(),
        fCaretAnchorX(0.0f),
        fStyleAtCaret(),
        fEditingEnabled(true)
{
}


TextEditor::TextEditor(const TextEditor& other)
        :
        fDocument(other.fDocument),
        fLayout(other.fLayout),
        fSelection(other.fSelection),
        fCaretAnchorX(other.fCaretAnchorX),
        fStyleAtCaret(other.fStyleAtCaret),
        fEditingEnabled(other.fEditingEnabled)
{
}


TextEditor::~TextEditor()
{
}


TextEditor&
TextEditor::operator=(const TextEditor& other)
{
        if (this == &other)
                return *this;

        fDocument = other.fDocument;
        fLayout = other.fLayout;
        fSelection = other.fSelection;
        fCaretAnchorX = other.fCaretAnchorX;
        fStyleAtCaret = other.fStyleAtCaret;
        fEditingEnabled = other.fEditingEnabled;
        return *this;
}


bool
TextEditor::operator==(const TextEditor& other) const
{
        if (this == &other)
                return true;

        return fDocument == other.fDocument
                && fLayout == other.fLayout
                && fSelection == other.fSelection
                && fCaretAnchorX == other.fCaretAnchorX
                && fStyleAtCaret == other.fStyleAtCaret
                && fEditingEnabled == other.fEditingEnabled;
}


bool
TextEditor::operator!=(const TextEditor& other) const
{
        return !(*this == other);
}


// #pragma mark -


void
TextEditor::SetDocument(const TextDocumentRef& ref)
{
        fDocument = ref;
        SetSelection(TextSelection());
}


void
TextEditor::SetLayout(const TextDocumentLayoutRef& ref)
{
        fLayout = ref;
        SetSelection(TextSelection());
}


void
TextEditor::SetEditingEnabled(bool enabled)
{
        fEditingEnabled = enabled;
}


void
TextEditor::SetCaret(BPoint location, bool extendSelection)
{
        if (!fDocument.IsSet() || !fLayout.IsSet())
                return;

        bool rightOfChar = false;
        int32 caretOffset = fLayout->TextOffsetAt(location.x, location.y,
                rightOfChar);

        if (rightOfChar)
                caretOffset++;

        _SetCaretOffset(caretOffset, true, extendSelection, true);
}


void
TextEditor::SelectAll()
{
        if (!fDocument.IsSet())
                return;

        SetSelection(TextSelection(0, fDocument->Length()));
}


void
TextEditor::SetSelection(TextSelection selection)
{
        _SetSelection(selection.Caret(), selection.Anchor(), true, true);
}


void
TextEditor::SetCharacterStyle(::CharacterStyle style)
{
        if (fStyleAtCaret == style)
                return;

        fStyleAtCaret = style;

        if (HasSelection()) {
                // TODO: Apply style to selection range
        }
}


void
TextEditor::KeyDown(KeyEvent event)
{
        if (!fDocument.IsSet())
                return;

        bool select = (event.modifiers & B_SHIFT_KEY) != 0;

        switch (event.key) {
                case B_UP_ARROW:
                        LineUp(select);
                        break;

                case B_DOWN_ARROW:
                        LineDown(select);
                        break;

                case B_LEFT_ARROW:
                        if (HasSelection() && !select) {
                                _SetCaretOffset(
                                        std::min(fSelection.Caret(), fSelection.Anchor()),
                                        true, false, true);
                        } else
                                _SetCaretOffset(fSelection.Caret() - 1, true, select, true);
                        break;

                case B_RIGHT_ARROW:
                        if (HasSelection() && !select) {
                                _SetCaretOffset(
                                        std::max(fSelection.Caret(), fSelection.Anchor()),
                                        true, false, true);
                        } else
                                _SetCaretOffset(fSelection.Caret() + 1, true, select, true);
                        break;

                case B_HOME:
                        LineStart(select);
                        break;

                case B_END:
                        LineEnd(select);
                        break;

                case B_ENTER:
                        Insert(fSelection.Caret(), "\n");
                        break;

                case B_TAB:
                        // TODO: Tab support in TextLayout
                        Insert(fSelection.Caret(), " ");
                        break;

                case B_ESCAPE:
                        break;

                case B_BACKSPACE:
                        if (HasSelection()) {
                                Remove(SelectionStart(), SelectionLength());
                        } else {
                                if (fSelection.Caret() > 0)
                                        Remove(fSelection.Caret() - 1, 1);
                        }
                        break;

                case B_DELETE:
                        if (HasSelection()) {
                                Remove(SelectionStart(), SelectionLength());
                        } else {
                                if (fSelection.Caret() < fDocument->Length())
                                        Remove(fSelection.Caret(), 1);
                        }
                        break;

                case B_INSERT:
                        // TODO: Toggle insert mode (or maybe just don't support it)
                        break;

                case B_PAGE_UP:
                case B_PAGE_DOWN:
                case B_SUBSTITUTE:
                case B_FUNCTION_KEY:
                case B_KATAKANA_HIRAGANA:
                case B_HANKAKU_ZENKAKU:
                        break;

                default:
                        if (event.bytes != NULL && event.length > 0) {
                                // Handle null-termintating the string
                                BString text(event.bytes, event.length);

                                Replace(SelectionStart(), SelectionLength(), text);
                        }
                        break;
        }
}


status_t
TextEditor::Insert(int32 offset, const BString& string)
{
        if (!fEditingEnabled || !fDocument.IsSet())
                return B_ERROR;

        status_t ret = fDocument->Insert(offset, string, fStyleAtCaret);

        if (ret == B_OK) {
                _SetCaretOffset(offset + string.CountChars(), true, false, true);
        }

        return ret;
}


status_t
TextEditor::Remove(int32 offset, int32 length)
{
        if (!fEditingEnabled || !fDocument.IsSet())
                return B_ERROR;

        status_t ret = fDocument->Remove(offset, length);

        if (ret == B_OK) {
                _SetCaretOffset(offset, true, false, true);
        }

        return ret;
}


status_t
TextEditor::Replace(int32 offset, int32 length, const BString& string)
{
        if (!fEditingEnabled || !fDocument.IsSet())
                return B_ERROR;

        status_t ret = fDocument->Replace(offset, length, string);

        if (ret == B_OK) {
                _SetCaretOffset(offset + string.CountChars(), true, false, true);
        }

        return ret;
}


// #pragma mark -


void
TextEditor::LineUp(bool select)
{
        if (!fLayout.IsSet())
                return;

        int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
        _MoveToLine(lineIndex - 1, select);
}


void
TextEditor::LineDown(bool select)
{
        if (!fLayout.IsSet())
                return;

        int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
        _MoveToLine(lineIndex + 1, select);
}


void
TextEditor::LineStart(bool select)
{
        if (!fLayout.IsSet())
                return;

        int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
        _SetCaretOffset(fLayout->FirstOffsetOnLine(lineIndex), true, select,
                true);
}


void
TextEditor::LineEnd(bool select)
{
        if (!fLayout.IsSet())
                return;

        int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
        _SetCaretOffset(fLayout->LastOffsetOnLine(lineIndex), true, select,
                true);
}


// #pragma mark -


bool
TextEditor::HasSelection() const
{
        return SelectionLength() > 0;
}


int32
TextEditor::SelectionStart() const
{
        return std::min(fSelection.Caret(), fSelection.Anchor());
}


int32
TextEditor::SelectionEnd() const
{
        return std::max(fSelection.Caret(), fSelection.Anchor());
}


int32
TextEditor::SelectionLength() const
{
        return SelectionEnd() - SelectionStart();
}


// #pragma mark - private


// _MoveToLine
void
TextEditor::_MoveToLine(int32 lineIndex, bool select)
{
        if (lineIndex < 0) {
                // Move to beginning of line instead. Most editors do. Some only when
                // selecting. Note that we are not updating the horizontal anchor here,
                // even though the horizontal caret position changes. Most editors
                // return to the previous horizonal offset when moving back down from
                // the beginning of the line.
                _SetCaretOffset(0, false, select, true);
                return;
        }
        if (lineIndex >= fLayout->CountLines()) {
                // Move to end of line instead, see above for why we do not update the
                // horizontal anchor.
                _SetCaretOffset(fDocument->Length(), false, select, true);
                return;
        }

        float x1;
        float y1;
        float x2;
        float y2;
        fLayout->GetLineBounds(lineIndex , x1, y1, x2, y2);

        bool rightOfCenter;
        int32 textOffset = fLayout->TextOffsetAt(fCaretAnchorX, (y1 + y2) / 2,
                rightOfCenter);

        if (rightOfCenter)
                textOffset++;

        _SetCaretOffset(textOffset, false, select, true);
}

void
TextEditor::_SetCaretOffset(int32 offset, bool updateAnchor,
        bool lockSelectionAnchor, bool updateSelectionStyle)
{
        if (!fDocument.IsSet())
                return;

        if (offset < 0)
                offset = 0;
        int32 textLength = fDocument->Length();
        if (offset > textLength)
                offset = textLength;

        int32 caret = offset;
        int32 anchor = lockSelectionAnchor ? fSelection.Anchor() : offset;
        _SetSelection(caret, anchor, updateAnchor, updateSelectionStyle);
}


void
TextEditor::_SetSelection(int32 caret, int32 anchor, bool updateAnchor,
        bool updateSelectionStyle)
{
        if (!fLayout.IsSet())
                return;

        if (caret == fSelection.Caret() && anchor == fSelection.Anchor())
                return;

        fSelection.SetCaret(caret);
        fSelection.SetAnchor(anchor);

        if (updateAnchor) {
                float x1;
                float y1;
                float x2;
                float y2;

                fLayout->GetTextBounds(caret, x1, y1, x2, y2);
                fCaretAnchorX = x1;
        }

        if (updateSelectionStyle)
                _UpdateStyleAtCaret();
}


void
TextEditor::_UpdateStyleAtCaret()
{
        if (!fDocument.IsSet())
                return;

        int32 offset = fSelection.Caret() - 1;
        if (offset < 0)
                offset = 0;
        SetCharacterStyle(fDocument->CharacterStyleAt(offset));
}