Files
hot-step-cpp-ROCm/engine/vendor/vst3sdk/vstgui4/vstgui/lib/ctexteditor.cpp
T
2026-08-16 18:24:52 +07:00

3503 lines
108 KiB
C++

// This file is part of VSTGUI. It is subject to the license terms
// in the LICENSE file found in the top-level directory of this
// distribution and at http://github.com/steinbergmedia/vstgui/LICENSE
#include "ccolor.h"
#include "cdrawcontext.h"
#include "cdropsource.h"
#include "cframe.h"
#include "controls/cscrollbar.h"
#include "cscrollview.h"
#include "ctexteditor.h"
#include "cgraphicspath.h"
#include "events.h"
#include "iviewlistener.h"
#include "cvstguitimer.h"
#include "finally.h"
#include "platform/iplatformfont.h"
#include "platform/iplatformframe.h"
#include "platform/iplatformtextinputclient.h"
#include "platform/platformfactory.h"
#include "platform/platform_macos.h"
#include "platform/platform_win32.h"
#include "controls/cbuttons.h"
#include "controls/ctextedit.h"
#include "animation/timingfunctions.h"
#include "animation/animations.h"
#include <optional>
#include <codecvt>
#include <locale>
#include <string>
#include <bitset>
#include <cwctype>
#include <cassert>
//------------------------------------------------------------------------
namespace VSTGUI {
namespace TextEditor {
using CharT = char32_t;
#define STB_TEXTEDIT_CHARTYPE CharT
#define STB_TEXTEDIT_POSITIONTYPE int32_t
#define STB_TEXTEDIT_STRING const TextEditorView
#define STB_TEXTEDIT_KEYTYPE uint32_t
#define STB_TEXTEDIT_UNDOSTATECOUNT 0
#define STB_TEXTEDIT_UNDOCHARCOUNT 0
#include "../thirdparty/stb_textedit.h"
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#elif defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable:4996)
#endif
using StringConvert = std::wstring_convert<std::codecvt_utf8<CharT>, CharT>;
//------------------------------------------------------------------------
inline std::u32string convert (const char* text, size_t numChars)
{
return StringConvert {}.from_bytes (text, text + numChars);
}
//------------------------------------------------------------------------
inline std::u32string convert (const std::string& str) { return StringConvert {}.from_bytes (str); }
//------------------------------------------------------------------------
inline std::string convert (const char32_t* text, size_t numChars)
{
return StringConvert {}.to_bytes (text, text + numChars);
}
//------------------------------------------------------------------------
inline std::string convert (const std::u32string& str) { return StringConvert {}.to_bytes (str); }
#ifdef __clang__
#pragma clang diagnostic pop
#elif defined(_MSC_VER)
#pragma warning(pop)
#endif
using String = std::u32string;
using StringView = std::u32string_view;
//------------------------------------------------------------------------
struct Range
{
size_t start {0};
size_t length {0};
explicit operator bool () const { return length > 0; }
size_t end () const { return start + length; }
};
//------------------------------------------------------------------------
bool operator== (const Range& r1, const Range& r2)
{
return r1.start == r2.start && r1.length == r2.length;
}
//------------------------------------------------------------------------
bool operator!= (const Range& r1, const Range& r2)
{
return r1.start != r2.start || r1.length != r2.length;
}
//------------------------------------------------------------------------
inline Range makeRange (size_t start, size_t end)
{
if (start > end)
std::swap (start, end);
return Range {static_cast<size_t> (start), static_cast<size_t> (end - start)};
}
//------------------------------------------------------------------------
inline Range makeRange (const STB_TexteditState& state)
{
return makeRange (state.select_start, state.select_end);
}
//------------------------------------------------------------------------
inline size_t replaceTabs (std::string& str, uint32_t tabWidth, size_t lineOffset)
{
if (tabWidth < 1)
return 0;
auto numReplacedChars = 0u;
auto whiteSpace = ' ';
std::string::size_type pos = std::string::npos;
while ((pos = str.find_first_of ('\t')) != std::string::npos)
{
auto numWhiteSpace = tabWidth - ((pos + lineOffset) % tabWidth);
std::string s (numWhiteSpace, whiteSpace);
str.replace (pos, 1, s);
numReplacedChars += numWhiteSpace - 1;
}
return numReplacedChars;
}
//------------------------------------------------------------------------
inline void convertWinLineEndingsToUnixLineEndings (String& text)
{
auto it = text.begin ();
if (it == text.end ())
return;
auto lastChar = it;
++it;
for (; it != text.end (); ++it)
{
if (*lastChar == '\r' && *it == '\n')
{
it = text.erase (lastChar);
++it;
if (it == text.end ())
break;
}
lastChar = it;
}
}
//------------------------------------------------------------------------
inline bool isStopChar (char32_t character)
{
auto ch = static_cast<wint_t> (character);
return std::iswpunct (ch) || std::iswcntrl (ch) || std::iswspace (ch);
};
//------------------------------------------------------------------------
struct Line
{
Range range;
UTF8String text;
CCoord width {};
};
using Lines = std::vector<Line>;
//------------------------------------------------------------------------
struct TextModel
{
String text;
Lines lines;
};
//------------------------------------------------------------------------
struct NewLineProcessor
{
static void update (Lines& lines, StringView text, size_t startRow);
};
struct LineNumberView;
struct FindPanelController;
static constexpr CPoint MouseOutsidePos = {std::numeric_limits<CCoord>::max (),
std::numeric_limits<CCoord>::max ()};
//------------------------------------------------------------------------
struct Key
{
char32_t character;
VirtualKey virt;
Modifiers modifiers;
bool operator== (const KeyboardEvent& event) const
{
return event.character == character && event.virt == virt && event.modifiers == modifiers;
}
};
using CommandKeyArray = std::array<Key, static_cast<size_t> (ITextEditor::Command::TakeFocus) + 1>;
//------------------------------------------------------------------------
struct TextEditorView : public CView,
public ITextEditor,
public TextEditorColorization::IEditorExt,
public IFocusDrawing,
public ViewEventListenerAdapter
{
TextEditorView (ITextEditorController* controller);
void beforeDelete () override;
void drawRect (CDrawContext* pContext, const CRect& dirtyRect) override;
bool attached (CView* parent) override;
bool removed (CView* parent) override;
void parentSizeChanged () override;
void looseFocus () override;
void takeFocus () override;
void onKeyboardEvent (KeyboardEvent& event) override;
void onMouseDownEvent (MouseDownEvent& event) override;
void onMouseMoveEvent (MouseMoveEvent& event) override;
void onMouseUpEvent (MouseUpEvent& event) override;
void onMouseCancelEvent (MouseCancelEvent& event) override;
void onMouseEnterEvent (MouseEnterEvent& event) override;
void onMouseExitEvent (MouseExitEvent& event) override;
void viewOnEvent (CView* view, Event& event) override;
int32_t deleteChars (size_t pos, size_t num) const;
int32_t insertChars (size_t pos, const CharT* text, size_t num) const;
void layout (StbTexteditRow* row, size_t start_i) const;
float getCharWidth (size_t row, size_t pos) const;
CharT getChar (int32_t pos) const;
int32_t getLength () const;
size_t moveToWordPrevious (size_t pos) const;
size_t moveToWordNext (size_t pos) const;
// STB
static int32_t deleteChars (const TextEditorView* self, size_t pos, size_t num)
{
return self->deleteChars (pos, num);
}
static int32_t insertChars (const TextEditorView* self, size_t pos, const CharT* text,
size_t num)
{
return self->insertChars (pos, text, num);
}
static void layout (StbTexteditRow* row, const TextEditorView* self, size_t start_i)
{
self->layout (row, start_i);
}
static float getCharWidth (const TextEditorView* self, size_t row, size_t pos)
{
return self->getCharWidth (row, pos);
}
static CharT getChar (const TextEditorView* self, int32_t pos) { return self->getChar (pos); }
static int32_t getLength (const TextEditorView* self) { return self->getLength (); }
static int moveToWordPrevious (const TextEditorView* self, size_t pos)
{
return static_cast<int> (self->moveToWordPrevious (pos));
}
static int moveToWordNext (const TextEditorView* self, size_t pos)
{
return static_cast<int> (self->moveToWordNext (pos));
}
static CharT* createUndoRecord (const TextEditorView* self, size_t pos, size_t insert_len,
size_t delete_len)
{
return self->createUndoRecord (pos, insert_len, delete_len);
}
static void undo (const TextEditorView* self) { self->doUndo (); }
static void redo (const TextEditorView* self) { self->doRedo (); }
protected:
// IFocusDrawing
bool drawFocusOnTop () override;
bool getFocusPath (CGraphicsPath& outPath) override;
// ITextEditor
bool setPlainText (std::string_view utf8Text, bool clearSelection) const override;
std::string getPlainText () const override;
void resetController () const override;
void setStyle (const Style& style) const override;
bool canHandleCommand (Command cmd) const override;
bool handleCommand (Command cmd) const override;
bool setCommandKeyBinding (Command cmd, char32_t character, VirtualKey virt,
Modifiers modifiers) const override;
void setFindOptions (FindOptions opt) const override;
void setFindString (std::string_view utf8Text) const override;
// TextEditorHighlighting::IEditorExt
bool readText (size_t startOffset, size_t length,
const ReadCallbackFunc& callback) const override;
size_t getTextLength () const override;
// commandos
bool doShifting (bool right) const;
void selectAll () const;
bool doCut () const;
bool doCopy () const;
bool doPaste () const;
bool useSelectionForFind () const;
bool doFind (bool forward = true, size_t oldPos = String::npos) const;
bool showFindPanel () const;
bool gotoLine (size_t lineNo) const;
String::size_type doFindCaseSensitive (bool forward) const;
String::size_type doFindIgnoreCase (bool forward) const;
// undo / redo
CharT* createUndoRecord (size_t pos, size_t insertLen, size_t deleteLen) const;
void checkCurrentUndoGroup (bool force) const;
void doUndo () const;
void doRedo () const;
template<bool iterateForward>
void doUndoRedo () const;
void flushUndoList () const;
void clearUndoList () const;
private:
template<typename Proc>
bool callSTB (Proc proc) const;
enum Dirty
{
UI = 1 << 0,
Layout = 1 << 1,
All = UI | Layout,
};
void validateLineStarts (Lines::const_iterator it, const Lines::const_iterator& end) const;
void invalidateSingleLine (size_t pos, int32_t numChars) const;
void invalidateSingleLine (Lines::iterator& line, int32_t numChars) const;
void invalidateLines (size_t pos, int32_t numChars) const;
void invalidate (Dirty what = Dirty::UI) const;
void invalidLine (size_t index, bool completeWidth = false) const;
void invalidLine (Lines::const_iterator it, bool completeWidth = false) const;
void invalidateRect (CRect r) const;
void invalidSelectedLines () const;
CCoord updateLineText (Lines::iterator& line) const;
CRect calculateLineRect (size_t index) const;
CRect calculateLineRect (Lines::const_iterator it) const;
CCoord calculateMaxWidth () const;
CRect calculateSelectedLinesRect () const;
void updateLineNumbersView () const;
void layoutRows () const;
void onCursorChanged (int oldCursorPos, int newCursorPos) const;
void onSelectionChanged (Range newSel, bool forceInvalidation = false) const;
void selectOnDoubleClick (uint32_t clickCount) const;
template<bool forward>
void selectPair (size_t startPos, char32_t closingChar) const;
void updateSelectionOnDoubleClickMove (uint32_t clickCount) const;
void insertNewLine () const;
/** will return the last line if pos not found instead of end */
template<typename T>
T findLine (T begin, T end, size_t pos) const;
CRect calculateCursorRect (int cursor) const;
CRect invalidCursorRect () const;
void toggleCursorVisibility () const;
void restartBlinkTimer () const;
void onStyleChanged () const;
void setFindString (String&& text) const;
bool isReadOnlyMode () const;
TextEditorView& mutableThis () const { return *const_cast<TextEditorView*> (this); }
public:
//------------------------------------------------------------------------
struct UndoRecord
{
size_t position {};
size_t deleted {0};
String characters;
};
using UndoRecords = std::vector<UndoRecord>;
//------------------------------------------------------------------------
struct UndoGroup
{
uint64_t time {0};
UndoRecords record;
};
using UndoList = std::vector<UndoGroup>;
//------------------------------------------------------------------------
struct ModelData
{
TextModel model;
std::shared_ptr<Style> style {std::make_shared<Style> ()};
ITextEditorController* controller {nullptr};
const IFontPainter* fontPainer {nullptr};
FindPanelController* findPanelController {nullptr};
CScrollView* scrollView {nullptr};
SharedPointer<LineNumberView> lineNumberView;
SharedPointer<CVSTGUITimer> blinkTimer;
CRect cursorRect {};
CRect lastDrawnCursorRect {};
Range selectedLines {};
CPoint lastMouse {MouseOutsidePos};
CCoord maxHeight {0};
CCoord maxWidth {200.};
CCoord horizontalLineMargin {0};
CCoord lineHeight {style->font->getSize () + style->lineSpacing};
CCoord fontAscent {0.};
CCoord fontDescent {0.};
STB_TexteditState editState {};
STB_TexteditState editStateOnMouseDown {};
bool mouseIsDown {false};
bool isInsertingText {false};
bool cursorIsVisible {false};
bool isInUndoRedo {false};
float cursorAlpha {0.f};
Lines::const_iterator stbInternalIterator;
FindOptions findOptions;
String findString;
CommandKeyArray commandKeys;
UndoList undoList {UndoGroup ()};
UndoList::iterator undoPos;
UndoGroup currentUndoGroup;
};
private:
mutable ModelData md;
struct CocoaTextInputClient : ICocoaTextInputClient
{
TextEditorView& view;
Range markedRange {};
std::u32string markedText;
TextInputClientCancelCallback cancelCallback;
CocoaTextInputClient (TextEditorView& view) : view (view) {}
void cancel ();
void insertText (const std::u32string& string, TextRange range) override;
void setMarkedText (const std::u32string& string, TextRange selectedRange,
TextRange replacementRange) override;
bool hasMarkedText () override;
void unmarkText () override;
TextRange getMarkedRange () override;
TextRange getSelectedRange () override;
CRect firstRectForCharacterRange (TextRange range, TextRange& actualRange) override;
std::u32string substringForRange (TextRange range, TextRange& actualRange) override;
size_t characterIndexForPoint (CPoint pos) override;
void setCancelCallback (const TextInputClientCancelCallback& callback) override;
};
std::unique_ptr<CocoaTextInputClient> cocoaTextInputClient;
struct IMETextInputClient : IIMETextInputClient
{
TextEditorView& view;
Range markedRange {};
std::u32string markedText;
IMETextInputClient (TextEditorView& view) : view (view) {}
bool ime_queryCharacterPosition (CharPosition& cp) override;
void ime_setMarkedText (const std::u32string& string) override;
void ime_insertText (const std::u32string& string) override;
void ime_unmarkText () override;
bool hasMarkedText () const { return markedText.empty () == false; }
};
std::unique_ptr<IMETextInputClient> imeTextInputClient;
};
#define VIRTUAL_KEY_BIT 0x80000000
#define STB_TEXTEDIT_K_SHIFT 0x40000000
#define STB_TEXTEDIT_K_CONTROL 0x20000000
#define STB_TEXTEDIT_K_ALT 0x10000000
// key-bindings
#define STB_TEXTEDIT_K_LEFT (VIRTUAL_KEY_BIT | VKEY_LEFT)
#define STB_TEXTEDIT_K_RIGHT (VIRTUAL_KEY_BIT | VKEY_RIGHT)
#define STB_TEXTEDIT_K_UP (VIRTUAL_KEY_BIT | VKEY_UP)
#define STB_TEXTEDIT_K_DOWN (VIRTUAL_KEY_BIT | VKEY_DOWN)
#if MAC
#define STB_TEXTEDIT_K_LINESTART (STB_TEXTEDIT_K_CONTROL | STB_TEXTEDIT_K_LEFT)
#define STB_TEXTEDIT_K_LINEEND (STB_TEXTEDIT_K_CONTROL | STB_TEXTEDIT_K_RIGHT)
#define STB_TEXTEDIT_K_WORDLEFT (STB_TEXTEDIT_K_ALT | STB_TEXTEDIT_K_LEFT)
#define STB_TEXTEDIT_K_WORDRIGHT (STB_TEXTEDIT_K_ALT | STB_TEXTEDIT_K_RIGHT)
#define STB_TEXTEDIT_K_TEXTSTART (STB_TEXTEDIT_K_CONTROL | STB_TEXTEDIT_K_UP)
#define STB_TEXTEDIT_K_TEXTEND (STB_TEXTEDIT_K_CONTROL | STB_TEXTEDIT_K_DOWN)
#else
#define STB_TEXTEDIT_K_LINESTART (VIRTUAL_KEY_BIT | VKEY_HOME)
#define STB_TEXTEDIT_K_LINEEND (VIRTUAL_KEY_BIT | VKEY_END)
#define STB_TEXTEDIT_K_WORDLEFT (STB_TEXTEDIT_K_LEFT | STB_TEXTEDIT_K_CONTROL)
#define STB_TEXTEDIT_K_WORDRIGHT (STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_CONTROL)
#define STB_TEXTEDIT_K_TEXTSTART (STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_CONTROL)
#define STB_TEXTEDIT_K_TEXTEND (STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_CONTROL)
#endif
#define STB_TEXTEDIT_K_DELETE (VIRTUAL_KEY_BIT | VKEY_DELETE)
#define STB_TEXTEDIT_K_BACKSPACE (VIRTUAL_KEY_BIT | VKEY_BACK)
#define STB_TEXTEDIT_K_UNDO (STB_TEXTEDIT_K_CONTROL | 'z')
#define STB_TEXTEDIT_K_REDO (STB_TEXTEDIT_K_CONTROL | STB_TEXTEDIT_K_SHIFT | 'z')
#define STB_TEXTEDIT_K_INSERT (VIRTUAL_KEY_BIT | VKEY_INSERT)
#define STB_TEXTEDIT_K_PGUP (VIRTUAL_KEY_BIT | VKEY_PAGEUP)
#define STB_TEXTEDIT_K_PGDOWN (VIRTUAL_KEY_BIT | VKEY_PAGEDOWN)
// functions
#define STB_TEXTEDIT_STRINGLEN(tc) TextEditorView::getLength (tc)
#define STB_TEXTEDIT_LAYOUTROW TextEditorView::layout
#define STB_TEXTEDIT_GETWIDTH(tc, n, i) TextEditorView::getCharWidth (tc, n, i)
#define STB_TEXTEDIT_KEYTOTEXT(key) \
((key & VIRTUAL_KEY_BIT) ? 0 : ((key & STB_TEXTEDIT_K_CONTROL) ? 0 : (key & (~0xF0000000))));
#define STB_TEXTEDIT_GETCHAR(tc, i) TextEditorView::getChar (tc, i)
#define STB_TEXTEDIT_NEWLINE '\n'
#define STB_TEXTEDIT_DELETECHARS TextEditorView::deleteChars
#define STB_TEXTEDIT_INSERTCHARS TextEditorView::insertChars
#define STB_TEXTEDIT_MOVEWORDLEFT TextEditorView::moveToWordPrevious
#define STB_TEXTEDIT_MOVEWORDRIGHT TextEditorView::moveToWordNext
#define STB_TEXTEDIT_CUSTOM_CREATEUNDO TextEditorView::createUndoRecord
#define STB_TEXTEDIT_CUSTOM_UNDO TextEditorView::undo
#define STB_TEXTEDIT_CUSTOM_REDO TextEditorView::redo
#define STB_TEXTEDIT_GETWIDTH_NEWLINE -1.f
#define STB_TEXTEDIT_IMPLEMENTATION
#include "../thirdparty/stb_textedit.h"
#undef STB_TEXTEDIT_IMPLEMENTATION
//------------------------------------------------------------------------
struct LineNumberView : CView,
ViewListenerAdapter
{
LineNumberView (CView* textEditorView);
~LineNumberView () noexcept override;
void drawRect (CDrawContext* context, const CRect& dirtyRect) override;
void setStyle (const std::shared_ptr<ITextEditor::Style>& newStyle, CCoord newLineHeight);
void setNumLines (size_t numberOfLines);
void setSelectedLines (Range range);
private:
void viewSizeChanged (CView* view, const CRect& oldSize) override;
void viewWillDelete (CView* view) override;
std::shared_ptr<ITextEditor::Style> style;
CCoord lineHeight {};
size_t numLines {0};
Range selectedLines {};
CView* textEditorView {nullptr};
};
//------------------------------------------------------------------------
struct FindPanelController : IControlListener,
ViewListenerAdapter,
ViewEventListenerAdapter
{
using RemoveFindPanelFunc = std::function<void ()>;
static CViewContainer* makeFindPanelView (CRect rect, TextEditorView::ModelData& md,
const ITextEditor& editor, RemoveFindPanelFunc&& f);
void setFindString (StringView text);
void setFindOptions (ITextEditor::FindOptions opt);
private:
enum Tag
{
Textfield,
CloseButton,
WholeWords,
CaseSensitive,
FindPrevious,
FindNext,
};
FindPanelController (const ITextEditor& editor);
~FindPanelController () noexcept;
void valueChanged (CControl* control) override;
void viewLostFocus (CView* view) override;
void viewWillDelete (CView* view) override;
void viewOnEvent (CView* view, Event& event) override;
const ITextEditor& editor;
CTextEdit* editfield {nullptr};
CControl* closeBox {nullptr};
CControl* caseSensitiveButton {nullptr};
CControl* wholeWordButton {nullptr};
ITextEditor::FindOptions findOptions {};
CommandKeyArray commandKeys;
RemoveFindPanelFunc removeFindPanelFunc;
};
static constexpr size_t Index (ITextEditor::Command cmd) { return static_cast<size_t> (cmd); }
//------------------------------------------------------------------------
TextEditorView::TextEditorView (ITextEditorController* controller) : CView ({0, 0, 10, 10})
{
md.controller = controller;
md.undoPos = md.undoList.end ();
setWantsFocus (true);
onStyleChanged ();
stb_textedit_initialize_state (&md.editState, false);
std::fill (md.commandKeys.begin (), md.commandKeys.end (), Key ());
md.commandKeys[Index (Command::SelectAll)] = {u'a', VirtualKey::None, {ModifierKey::Control}};
md.commandKeys[Index (Command::Cut)] = {u'x', VirtualKey::None, {ModifierKey::Control}};
md.commandKeys[Index (Command::Copy)] = {u'c', VirtualKey::None, {ModifierKey::Control}};
md.commandKeys[Index (Command::Paste)] = {u'v', VirtualKey::None, {ModifierKey::Control}};
md.commandKeys[Index (Command::Undo)] = {u'z', VirtualKey::None, {ModifierKey::Control}};
md.commandKeys[Index (Command::Redo)] = {
u'z', VirtualKey::None, {ModifierKey::Control, ModifierKey::Shift}};
#if MAC
md.commandKeys[Index (Command::FindNext)] = {u'g', VirtualKey::None, {ModifierKey::Control}};
md.commandKeys[Index (Command::FindPrevious)] = {
u'g', VirtualKey::None, {ModifierKey::Control, ModifierKey::Shift}};
md.commandKeys[Index (Command::UseSelectionForFind)] = {
u'e', VirtualKey::None, {ModifierKey::Control}};
md.commandKeys[Index (Command::ShiftLeft)] = {u'[', VirtualKey::None, {ModifierKey::Control}};
md.commandKeys[Index (Command::ShiftRight)] = {u']', VirtualKey::None, {ModifierKey::Control}};
#else
md.commandKeys[Index (Command::FindNext)] = {0, VirtualKey::F3, {}};
md.commandKeys[Index (Command::FindPrevious)] = {0, VirtualKey::F3, {ModifierKey::Shift}};
md.commandKeys[Index (Command::UseSelectionForFind)] = {
0, VirtualKey::F3, {ModifierKey::Control}};
md.commandKeys[Index (Command::ShiftLeft)] = {0, VirtualKey::Tab, {ModifierKey::Shift}};
md.commandKeys[Index (Command::ShiftRight)] = {0, VirtualKey::Tab, {}};
#endif
md.commandKeys[Index (Command::ShowFindPanel)] = {
u'f', VirtualKey::None, {ModifierKey::Control}};
md.controller->onTextEditorCreated (*this);
}
//------------------------------------------------------------------------
void TextEditorView::beforeDelete ()
{
if (md.controller)
md.controller->onTextEditorDestroyed (*this);
CView::beforeDelete ();
}
//------------------------------------------------------------------------
bool TextEditorView::attached (CView* parent)
{
if (CView::attached (parent))
{
if (auto sv = dynamic_cast<CScrollView*> (parent->getParentView ()))
{
md.scrollView = sv;
layoutRows ();
md.scrollView->registerViewEventListener (this);
if (auto sb = md.scrollView->getVerticalScrollbar ())
sb->registerViewEventListener (this);
if (md.style->flags & Style::Flags::ShowLineNumbers)
{
md.lineNumberView = makeOwned<LineNumberView> (this);
md.lineNumberView->setStyle (md.style, md.lineHeight);
updateLineNumbersView ();
md.scrollView->setEdgeView (CScrollView::Edge::Left, md.lineNumberView);
md.lineNumberView->remember ();
}
}
return true;
}
return false;
}
//------------------------------------------------------------------------
bool TextEditorView::removed (CView* parent)
{
if (md.scrollView)
{
md.scrollView->unregisterViewEventListener (this);
if (auto sb = md.scrollView->getVerticalScrollbar ())
sb->unregisterViewEventListener (this);
}
md.scrollView = nullptr;
md.blinkTimer = nullptr;
md.lineNumberView = nullptr;
return CView::removed (parent);
}
//------------------------------------------------------------------------
void TextEditorView::takeFocus ()
{
if (md.lastMouse != MouseOutsidePos)
getFrame ()->setCursor (kCursorIBeam);
restartBlinkTimer ();
if (!isReadOnlyMode ())
{
#if MAC_COCOA
auto pf = getFrame ()->getPlatformFrame ();
if (auto cocoaFrame = dynamic_cast<ICocoaPlatformFrame*> (pf))
{
cocoaTextInputClient = std::make_unique<CocoaTextInputClient> (*this);
cocoaFrame->setTextInputClient (cocoaTextInputClient.get ());
}
#elif WINDOWS
auto pf = getFrame ()->getPlatformFrame ();
if (auto winFrame = dynamic_cast<IWin32PlatformFrame*> (pf))
{
imeTextInputClient = std::make_unique<IMETextInputClient> (*this);
winFrame->setTextInputClient (imeTextInputClient.get ());
}
#endif
}
CView::takeFocus ();
}
//------------------------------------------------------------------------
void TextEditorView::looseFocus ()
{
md.blinkTimer = nullptr;
md.cursorIsVisible = true;
toggleCursorVisibility ();
#if MAC_COCOA
auto pf = getFrame ()->getPlatformFrame ();
if (auto cocoaFrame = dynamic_cast<ICocoaPlatformFrame*> (pf))
{
cocoaFrame->setTextInputClient (nullptr);
cocoaTextInputClient.reset ();
}
#elif WINDOWS
auto pf = getFrame ()->getPlatformFrame ();
if (auto winFrame = dynamic_cast<IWin32PlatformFrame*> (pf))
{
winFrame->setTextInputClient (nullptr);
imeTextInputClient.reset ();
}
#endif
CView::looseFocus ();
}
//------------------------------------------------------------------------
void TextEditorView::parentSizeChanged ()
{
if (md.scrollView)
{
auto func = [This = shared (this), makeCursorVisible = md.isInsertingText] () mutable {
if (!This->isAttached () || !This->md.scrollView)
return;
auto viewSize = This->getViewSize ();
viewSize.setHeight (This->md.maxHeight);
viewSize.setWidth (This->md.maxWidth);
auto containerSize = This->md.scrollView->calculateOptimalContainerSize ();
auto origContainerSizeWidth = containerSize.getWidth ();
auto origContainerSizeHeight = containerSize.getHeight ();
if (origContainerSizeWidth <= This->md.maxWidth)
containerSize.bottom -= This->md.scrollView->getScrollbarWidth ();
if (origContainerSizeHeight <= This->md.maxHeight)
containerSize.right -= This->md.scrollView->getScrollbarWidth ();
// test again something could have changed above
if (containerSize.getWidth () <= This->md.maxWidth &&
origContainerSizeHeight == containerSize.getHeight ())
containerSize.bottom -= This->md.scrollView->getScrollbarWidth ();
if (containerSize.getHeight () <= This->md.maxHeight &&
origContainerSizeWidth == containerSize.getWidth ())
containerSize.right -= This->md.scrollView->getScrollbarWidth ();
if (containerSize.getWidth () > This->md.maxWidth)
{
viewSize.setWidth (containerSize.getWidth ());
}
if (containerSize.getHeight () > This->md.maxHeight)
{
viewSize.setHeight (containerSize.getHeight ());
}
This->setViewSize (viewSize);
if (makeCursorVisible)
This->onCursorChanged (-1, This->md.editState.cursor);
This->md.editState.row_count_per_page = static_cast<int> (
std::ceil (This->getVisibleViewSize ().getHeight () / This->md.lineHeight));
};
auto frame = getFrame ();
if (frame && frame->inEventProcessing ())
{
frame->doAfterEventProcessing (std::move (func));
return;
}
else
{
Call::later (std::move (func));
}
}
else
{
auto viewSize = getViewSize ();
viewSize.setHeight (md.maxHeight);
viewSize.setWidth (md.maxWidth);
setViewSize (viewSize);
}
}
//------------------------------------------------------------------------
bool TextEditorView::drawFocusOnTop () { return true; }
//------------------------------------------------------------------------
bool TextEditorView::getFocusPath (CGraphicsPath& outPath)
{
auto r = getVisibleViewSize ();
outPath.addRect (r);
CCoord focusWidth = getFrame ()->getFocusWidth ();
r.inset (focusWidth, focusWidth);
outPath.addRect (r);
return true;
}
//------------------------------------------------------------------------
void TextEditorView::viewOnEvent (CView* view, Event& event)
{
if (event.type != EventType::MouseWheel)
return;
auto& wheelEvent = castMouseWheelEvent (event);
CPoint scrollOffset {};
if (wheelEvent.flags & MouseWheelEvent::Flags::PreciseDeltas)
{
scrollOffset.x += std::ceil (wheelEvent.deltaX / 0.1);
scrollOffset.y -= std::ceil (wheelEvent.deltaY / 0.1);
}
else
{
scrollOffset.x += wheelEvent.deltaX * md.lineHeight;
scrollOffset.y -= wheelEvent.deltaY * md.lineHeight;
}
if (wheelEvent.flags & MouseWheelEvent::DirectionInvertedFromDevice)
{
scrollOffset.y *= -1.;
}
auto viewSize = getVisibleViewSize ();
viewSize.offset (scrollOffset);
md.scrollView->makeRectVisible (viewSize);
event.consumed = true;
}
//------------------------------------------------------------------------
void TextEditorView::onStyleChanged () const
{
if (auto pf = md.style->font->getPlatformFont ())
{
md.fontAscent = pf->getAscent ();
md.fontDescent = pf->getDescent ();
md.lineHeight =
std::round ((md.fontAscent + md.fontDescent + md.style->lineSpacing) * 2.) / 2.;
}
else
{
md.lineHeight = md.style->font->getSize () + md.style->lineSpacing;
}
md.fontPainer = md.style->font->getFontPainter ();
if (md.lineNumberView)
{
if (md.style->flags & Style::Flags::ShowLineNumbers)
{
md.lineNumberView->setStyle (md.style, md.lineHeight);
updateLineNumbersView ();
}
else if (md.scrollView)
{
md.scrollView->setEdgeView (CScrollView::Edge::Left, nullptr);
md.lineNumberView = nullptr;
}
}
else if (md.style->flags & Style::Flags::ShowLineNumbers && md.scrollView)
{
md.lineNumberView = makeOwned<LineNumberView> (&mutableThis ());
md.lineNumberView->setStyle (md.style, md.lineHeight);
updateLineNumbersView ();
md.scrollView->setEdgeView (CScrollView::Edge::Left, md.lineNumberView);
md.lineNumberView->remember ();
}
md.horizontalLineMargin = md.style->leftMargin * 2.;
md.model.lines.clear ();
invalidate (Dirty::All);
}
//------------------------------------------------------------------------
void TextEditorView::setStyle (const Style& newStyle) const
{
if (!md.style->font)
return;
md.style = std::make_shared<Style> (newStyle);
onStyleChanged ();
}
//------------------------------------------------------------------------
bool TextEditorView::setPlainText (std::string_view utf8Text, bool clearSelection) const
{
clearUndoList ();
md.model.text = convert (utf8Text.data (), utf8Text.size ());
convertWinLineEndingsToUnixLineEndings (md.model.text);
if (clearSelection)
{
md.editState.cursor = md.editState.select_start = md.editState.select_end = 0;
onSelectionChanged (makeRange (md.editState), true);
}
invalidate (Dirty::All);
if (md.lineNumberView)
updateLineNumbersView ();
return true;
}
//------------------------------------------------------------------------
std::string TextEditorView::getPlainText () const { return convert (md.model.text); }
//------------------------------------------------------------------------
void TextEditorView::resetController () const { md.controller = nullptr; }
//------------------------------------------------------------------------
bool TextEditorView::canHandleCommand (Command cmd) const
{
switch (cmd)
{
case Command::SelectAll:
return true;
case Command::UseSelectionForFind:
[[fallthrough]];
case Command::Cut:
{
if (isReadOnlyMode ())
return false;
[[fallthrough]];
}
case Command::Copy:
{
return md.editState.select_start != md.editState.select_end;
}
case Command::Paste:
{
if (isReadOnlyMode ())
return false;
if (auto clipboard = getFrame ()->getClipboard ())
{
auto count = clipboard->getCount ();
for (auto i = 0u; i < count; ++i)
{
if (clipboard->getDataType (i) == IDataPackage::kText)
return true;
}
}
return false;
}
case Command::Undo:
{
// TODO:
return !isReadOnlyMode ();
}
case Command::Redo:
{
// TODO:
return !isReadOnlyMode ();
}
case Command::ShiftLeft:
return !isReadOnlyMode ();
case Command::ShiftRight:
return !isReadOnlyMode ();
case Command::TakeFocus:
return true;
case Command::FindNext:
[[fallthrough]];
case Command::FindPrevious:
{
return !md.findString.empty ();
}
case Command::ShowFindPanel:
return md.scrollView != nullptr;
}
return false;
}
//------------------------------------------------------------------------
bool TextEditorView::handleCommand (Command cmd) const
{
auto doFinally = finally ([&] () {
if (getFrame ()->getFocusView () == this)
{
restartBlinkTimer ();
}
});
if (cocoaTextInputClient && cocoaTextInputClient->hasMarkedText ())
cocoaTextInputClient->cancel ();
switch (cmd)
{
case Command::ShiftLeft:
return doShifting (false);
case Command::ShiftRight:
return doShifting (true);
case Command::SelectAll:
selectAll ();
return true;
case Command::Cut:
return doCut ();
case Command::Copy:
return doCopy ();
case Command::Paste:
return doPaste ();
case Command::Undo:
return callSTB ([this] () { stb_text_undo (this, &md.editState); });
case Command::Redo:
return callSTB ([this] () { stb_text_redo (this, &md.editState); });
case Command::FindNext:
return doFind (true);
case Command::FindPrevious:
return doFind (false);
case Command::UseSelectionForFind:
return useSelectionForFind ();
case Command::ShowFindPanel:
return showFindPanel ();
case Command::TakeFocus:
{
if (auto frame = getFrame ())
{
if (frame->getFocusView () != this)
frame->setFocusView (&mutableThis ());
}
return true;
}
}
return false;
}
//------------------------------------------------------------------------
bool TextEditorView::setCommandKeyBinding (Command cmd, char32_t character, VirtualKey virt,
Modifiers modifiers) const
{
md.commandKeys[Index (cmd)] = {character, virt, modifiers};
return true;
}
//------------------------------------------------------------------------
bool TextEditorView::readText (size_t startOffset, size_t length,
const ReadCallbackFunc& callback) const
{
if (startOffset >= md.model.text.length () || md.model.lines.empty ())
return false;
if (startOffset + length >= md.model.text.length ())
length = md.model.text.length () - startOffset;
callback ({md.model.text.data () + startOffset, length});
return true;
}
//------------------------------------------------------------------------
size_t TextEditorView::getTextLength () const { return md.model.text.length (); }
//------------------------------------------------------------------------
inline Range toLineSelection (const Range& line, size_t selStart, size_t selEnd)
{
if (selStart == selEnd)
return {};
if (selStart > selEnd)
std::swap (selStart, selEnd);
if (selEnd < line.start)
return {};
if (selStart >= line.start + line.length)
return {};
size_t lineStart = line.start;
size_t lineLength = selEnd - selStart;
if (selStart > line.start)
lineStart = selStart;
else if (selStart < line.start)
lineLength -= line.start - selStart;
if (lineStart + lineLength > line.start + line.length)
lineLength = line.start + line.length - lineStart;
return {lineStart - line.start, lineLength};
}
//------------------------------------------------------------------------
void TextEditorView::drawRect (CDrawContext* context, const CRect& dirtyRect)
{
context->setFillColor (md.style->backColor);
context->setDrawMode (kAntiAliasing);
context->drawRect (dirtyRect, kDrawFilled);
bool drawCursorLineHighlight = (md.editState.select_start == md.editState.select_end) &&
(md.style->flags & Style::Flags::HighlightCursorLine);
auto styleProvider = dynamic_cast<TextEditorColorization::IStyleProvider*> (md.controller);
if (styleProvider)
styleProvider->beginDraw (*this);
CCoord x = getViewSize ().left + md.style->leftMargin;
CCoord y = getViewSize ().top - md.style->lineSpacing;
for (auto index = 0u; index < md.model.lines.size (); ++index)
{
const auto& line = md.model.lines[index];
if (y > dirtyRect.bottom)
break;
y += md.lineHeight;
if (y < dirtyRect.top)
continue;
if (drawCursorLineHighlight && md.selectedLines.start == index)
{
auto lineRect = calculateLineRect (index);
lineRect.right = getViewSize ().right;
context->setFillColor (md.style->highlightCursorLineColor);
context->drawRect (lineRect, kDrawFilled);
}
context->setFontColor (md.style->textColor);
context->setFont (md.style->font);
auto selRange =
toLineSelection (line.range, md.editState.select_start, md.editState.select_end);
if (selRange)
{
size_t selOffset = static_cast<size_t> (selRange.start);
auto selX = x;
if (selRange.start != 0)
{
auto t = md.model.text.substr (line.range.start, selRange.start);
auto nonSelectedText = convert (t);
selOffset += replaceTabs (nonSelectedText, md.style->tabWidth, 0u);
// selOffset -= selOffset / md.style->tabWidth;
selX += context->getStringWidth (nonSelectedText.data ());
}
CRect r (selX, y - md.fontAscent, selX, y + md.fontDescent);
if (selRange.start + selRange.length >= line.range.length &&
(line.range.length > 0 && md.model.text[line.range.end () - 1] == u'\n'))
r.right = getViewSize ().right;
else
{
auto t = md.model.text.substr (line.range.start + selRange.start, selRange.length);
auto selectedText = convert (t);
replaceTabs (selectedText, md.style->tabWidth, selOffset);
r.setWidth (context->getStringWidth (selectedText.data ()));
}
r.inset (0, -md.style->lineSpacing / 2.);
if ((cocoaTextInputClient && cocoaTextInputClient->hasMarkedText ()) ||
(imeTextInputClient && imeTextInputClient->hasMarkedText ()))
{
context->setLineWidth (1.);
context->setFrameColor (md.style->selectionBackColor);
context->drawRect (r, kDrawStroked);
}
else
{
context->setFillColor (md.style->selectionBackColor);
context->drawRect (r, kDrawFilled);
}
}
if (styleProvider)
{
auto lineX = x;
auto styles = styleProvider->getStyles (*this, line.range.start, line.range.length);
size_t start = line.range.start;
size_t end = 0u;
std::string tmpStr;
for (const auto& style : styles)
{
if (style.start != start)
{
tmpStr = convert (md.model.text.substr (start, style.start - start));
replaceTabs (tmpStr, md.style->tabWidth,
static_cast<size_t> (start - line.range.start));
context->setFontColor (md.style->textColor);
context->drawString (tmpStr.data (), {lineX, y});
lineX += context->getStringWidth (tmpStr.data ());
}
start = style.start < line.range.start ? line.range.start : style.start;
end = start + style.length;
if (start == end)
continue;
if (end > line.range.start + line.range.length)
end = line.range.start + line.range.length;
tmpStr = convert (md.model.text.substr (start, end - start));
replaceTabs (tmpStr, md.style->tabWidth,
static_cast<size_t> (start - line.range.start));
context->setFontColor (style.color);
context->drawString (tmpStr.data (), {lineX, y});
lineX += context->getStringWidth (tmpStr.data ());
start += style.length;
}
}
else
{
context->drawString (line.text.getPlatformString (), {x, y});
}
}
if (styleProvider)
styleProvider->endDraw (*this);
// cursor
if ((md.cursorIsVisible || md.cursorAlpha != 0.f) &&
md.editState.select_start == md.editState.select_end)
{
auto cr = md.cursorRect;
auto alpha = context->getGlobalAlpha ();
context->setGlobalAlpha (alpha * md.cursorAlpha);
cr.offset (getViewSize ().getTopLeft ());
context->setFillColor (md.style->cursorColor);
context->drawRect (cr, kDrawFilled);
context->setGlobalAlpha (alpha);
md.lastDrawnCursorRect = cr;
}
else
md.lastDrawnCursorRect = {};
}
//------------------------------------------------------------------------
void TextEditorView::invalidLine (size_t index, bool completeWidth) const
{
auto rectOfLine = calculateLineRect (index);
if (completeWidth)
rectOfLine.right = getViewSize ().right;
invalidateRect (rectOfLine);
}
//------------------------------------------------------------------------
void TextEditorView::invalidLine (Lines::const_iterator it, bool completeWidth) const
{
auto rectOfLine = calculateLineRect (it);
if (completeWidth)
rectOfLine.right = getViewSize ().right;
invalidateRect (rectOfLine);
}
//------------------------------------------------------------------------
void TextEditorView::invalidSelectedLines () const
{
if (md.selectedLines.length == 0 && (md.style->flags & Style::Flags::HighlightCursorLine))
invalidLine (md.selectedLines.start, true);
invalidateRect (calculateSelectedLinesRect ());
}
//------------------------------------------------------------------------
CRect TextEditorView::calculateLineRect (size_t index) const
{
auto width = index < md.model.lines.size () ? md.model.lines[index].width : md.maxWidth;
CRect r {0., -md.style->lineSpacing, std::ceil (width + md.style->leftMargin),
-md.style->lineSpacing};
r.offset (getViewSize ().left, md.lineHeight * (index + 1) + getViewSize ().top);
r.top = r.top - md.fontAscent;
r.bottom = r.bottom + md.fontDescent;
r.inset (0, -md.style->lineSpacing / 2.);
return r;
}
//------------------------------------------------------------------------
CRect TextEditorView::calculateLineRect (Lines::const_iterator it) const
{
auto lineIndex = std::distance (md.model.lines.cbegin (), it);
return calculateLineRect (lineIndex);
}
//------------------------------------------------------------------------
CRect TextEditorView::calculateSelectedLinesRect () const
{
CRect result;
for (auto index = md.selectedLines.start; index < md.selectedLines.end (); ++index)
{
auto r = calculateLineRect (index);
if (result.isEmpty ())
result = r;
else
result.unite (r);
}
result.setWidth (getWidth ());
return result;
}
//------------------------------------------------------------------------
CRect TextEditorView::calculateCursorRect (int cursor) const
{
auto cursorWidth = md.lineHeight / 10.;
CRect r {-cursorWidth / 2., -md.style->lineSpacing, cursorWidth / 2., -md.style->lineSpacing};
for (const auto& line : md.model.lines)
{
r.offset (0., md.lineHeight);
if (static_cast<size_t> (cursor) <= line.range.end ())
{
if (static_cast<size_t> (cursor) == line.range.start)
break;
if (md.model.text[cursor - 1] != '\n')
{
auto lineTextToCursor =
convert (md.model.text.data () + line.range.start, cursor - line.range.start);
replaceTabs (lineTextToCursor, md.style->tabWidth, 0u);
auto platformText = getPlatformFactory ().createString (lineTextToCursor.data ());
auto width = md.fontPainer->getStringWidth (nullptr, platformText);
r.offset (width, 0);
break;
}
}
}
r.offset (md.style->leftMargin, 0.);
r.top = r.top - md.fontAscent;
r.bottom = r.bottom + md.fontDescent;
r.inset (0, -md.style->lineSpacing / 2.);
return r;
}
//------------------------------------------------------------------------
CRect TextEditorView::invalidCursorRect () const
{
auto r = md.cursorRect;
r.offset (getViewSize ().getTopLeft ());
invalidateRect (r);
return r;
}
//------------------------------------------------------------------------
void TextEditorView::toggleCursorVisibility () const
{
using namespace Animation;
mutableThis ().addAnimation (
"CursorAlphaBlend",
new FuncAnimation ([] (CView* view, IdStringPtr name) {},
[this] (CView* view, IdStringPtr name, float pos) {
md.cursorAlpha = md.cursorIsVisible ? 1.f - pos : pos;
invalidCursorRect ();
},
[this] (CView* view, IdStringPtr name, bool wasCanceled) {
if (!wasCanceled)
md.cursorIsVisible = !md.cursorIsVisible;
md.cursorAlpha = md.cursorIsVisible ? 1.f : 0.f;
invalidCursorRect ();
}),
CubicBezierTimingFunction::make (md.cursorIsVisible ? CubicBezierTimingFunction::EasyOut
: CubicBezierTimingFunction::EasyIn,
md.style->cursorBlinkTime / 2));
}
//------------------------------------------------------------------------
void TextEditorView::restartBlinkTimer () const
{
if (md.style->cursorBlinkTime && md.editState.select_start == md.editState.select_end)
{
mutableThis ().removeAnimation ("CursorAlphaBlend");
md.cursorIsVisible = true;
md.cursorAlpha = 1.f;
invalidCursorRect ();
md.blinkTimer = makeOwned<CVSTGUITimer> ([this] (auto timer) { toggleCursorVisibility (); },
md.style->cursorBlinkTime);
}
}
//------------------------------------------------------------------------
void TextEditorView::onKeyboardEvent (KeyboardEvent& event)
{
if (event.type == EventType::KeyUp)
return;
if (event.character == 0 && event.virt == VirtualKey::None)
return;
auto doFinally = finally ([&] () {
if (event.consumed)
{
if (getFrame ()->getFocusView () == this)
{
restartBlinkTimer ();
}
}
});
for (auto index = 0u; index < md.commandKeys.size (); ++index)
{
const auto& cmd = md.commandKeys[index];
if (cmd == event)
{
handleCommand (static_cast<Command> (index));
event.consumed = true;
return;
}
}
auto key = event.character;
if (key)
{
if (auto txt = getFrame ()->getPlatformFrame ()->convertCurrentKeyEventToText ())
{
auto tmp = convert (txt->getString ());
key = tmp[0];
}
if (isStopChar (static_cast<char32_t> (key)))
checkCurrentUndoGroup (true);
}
if (event.virt != VirtualKey::None)
{
switch (event.virt)
{
case VirtualKey::Home:
{
#if !MAC
if (event.modifiers.has (ModifierKey::Control) == false)
{
key = static_cast<uint32_t> (event.virt) | VIRTUAL_KEY_BIT;
break;
}
#endif
gotoLine (0);
event.consumed = true;
break;
}
case VirtualKey::End:
{
#if !MAC
if (event.modifiers.has (ModifierKey::Control) == false)
{
key = static_cast<uint32_t> (event.virt) | VIRTUAL_KEY_BIT;
break;
}
#endif
gotoLine (md.model.lines.size () - 1);
event.consumed = true;
break;
}
case VirtualKey::Space:
{
checkCurrentUndoGroup (true);
key = 0x20;
break;
}
case VirtualKey::Tab:
{
checkCurrentUndoGroup (true);
key = '\t';
break;
}
case VirtualKey::Return:
{
checkCurrentUndoGroup (true);
insertNewLine ();
event.consumed = true;
return;
}
default:
{
key = static_cast<uint32_t> (event.virt) | VIRTUAL_KEY_BIT;
break;
}
}
}
if (event.modifiers.has (ModifierKey::Control))
key |= STB_TEXTEDIT_K_CONTROL;
if (event.modifiers.has (ModifierKey::Alt))
key |= STB_TEXTEDIT_K_ALT;
if (event.modifiers.has (ModifierKey::Shift))
key |= STB_TEXTEDIT_K_SHIFT;
if (callSTB ([&] () { stb_textedit_key (this, &md.editState, key); }))
event.consumed = true;
if (!event.consumed)
{
if (event.virt == VirtualKey::Up || event.virt == VirtualKey::Down ||
event.virt == VirtualKey::PageUp || event.virt == VirtualKey::PageDown ||
event.virt == VirtualKey::Left || event.virt == VirtualKey::Right ||
event.virt == VirtualKey::Home || event.virt == VirtualKey::End ||
event.virt == VirtualKey::Delete || event.virt == VirtualKey::Back)
{
event.consumed = true;
}
}
}
//------------------------------------------------------------------------
void TextEditorView::onMouseDownEvent (MouseDownEvent& event)
{
if (!event.buttonState.isLeft ())
return;
if (cocoaTextInputClient && cocoaTextInputClient->hasMarkedText ())
cocoaTextInputClient->cancel ();
getFrame ()->setFocusView (this);
md.mouseIsDown = true;
md.editStateOnMouseDown = md.editState;
if (event.clickCount > 1)
{
selectOnDoubleClick (event.clickCount);
event.consumed = true;
return;
}
CPoint pos (event.mousePosition);
pos -= getViewSize ().getTopLeft ();
auto tm = getGlobalTransform (true);
pos.x /= tm.m11;
pos.y /= tm.m22;
callSTB ([&] () {
stb_textedit_click (this, &md.editState, static_cast<float> (pos.x),
static_cast<float> (pos.y));
});
if (event.modifiers.is (ModifierKey::Shift))
{
md.editState.select_start = md.editStateOnMouseDown.select_start;
md.editState.select_end = md.editState.cursor;
onSelectionChanged (makeRange (md.editState));
}
event.consumed = true;
}
//------------------------------------------------------------------------
void TextEditorView::onMouseMoveEvent (MouseMoveEvent& event)
{
md.lastMouse = event.mousePosition;
if (!md.mouseIsDown)
return;
CPoint pos (event.mousePosition);
pos -= getViewSize ().getTopLeft ();
auto tm = getGlobalTransform (true);
pos.x /= tm.m11;
pos.y /= tm.m22;
callSTB ([&] () {
stb_textedit_drag (this, &md.editState, static_cast<float> (pos.x),
static_cast<float> (pos.y));
});
if (event.clickCount > 1)
updateSelectionOnDoubleClickMove (event.clickCount);
event.consumed = true;
}
//------------------------------------------------------------------------
void TextEditorView::onMouseUpEvent (MouseUpEvent& event)
{
if (!md.mouseIsDown)
return;
restartBlinkTimer ();
event.consumed = true;
md.mouseIsDown = false;
}
//------------------------------------------------------------------------
void TextEditorView::onMouseCancelEvent (MouseCancelEvent& event)
{
if (md.mouseIsDown)
md.mouseIsDown = false;
}
//------------------------------------------------------------------------
void TextEditorView::onMouseEnterEvent (MouseEnterEvent& event)
{
if (getFrame ()->getFocusView () == this)
getFrame ()->setCursor (CCursorType::kCursorIBeam);
}
//------------------------------------------------------------------------
void TextEditorView::onMouseExitEvent (MouseExitEvent& event)
{
getFrame ()->setCursor (CCursorType::kCursorDefault);
md.lastMouse = MouseOutsidePos;
}
//------------------------------------------------------------------------
CCoord TextEditorView::calculateMaxWidth () const
{
auto width = 0.;
for (const auto& line : md.model.lines)
{
if (line.width > width)
width = line.width;
}
return width;
}
//------------------------------------------------------------------------
void TextEditorView::invalidateRect (CRect r) const { mutableThis ().invalidRect (r); }
//------------------------------------------------------------------------
void TextEditorView::invalidate (Dirty what) const
{
if (what & Dirty::Layout)
{
layoutRows ();
}
if (what & Dirty::UI)
{
onCursorChanged (md.editState.cursor, md.editState.cursor);
mutableThis ().invalid ();
}
}
//------------------------------------------------------------------------
CCoord TextEditorView::updateLineText (Lines::iterator& line) const
{
auto newText = convert (md.model.text.data () + line->range.start, line->range.length);
replaceTabs (newText, md.style->tabWidth, 0u);
if (newText != line->text)
{
line->text = std::move (newText);
line->width = md.fontPainer->getStringWidth (nullptr, line->text.getPlatformString ());
}
return line->width;
}
//------------------------------------------------------------------------
void TextEditorView::updateLineNumbersView () const
{
if (!md.lineNumberView)
return;
UTF8String str;
auto lineCount = md.model.lines.size ();
while (lineCount > 0)
{
str += "9";
lineCount /= 10;
}
auto lineNumbersWidth = md.style->lineNumbersFont->getFontPainter ()->getStringWidth (
nullptr, str.getPlatformString ());
lineNumbersWidth += (md.style->lineNumberLeftMargin + md.style->lineNumberRightMargin);
lineNumbersWidth = std::ceil (lineNumbersWidth);
auto vs = md.lineNumberView->getViewSize ();
if (vs.getWidth () != lineNumbersWidth)
vs.setWidth (lineNumbersWidth);
md.lineNumberView->setViewSize (vs);
md.lineNumberView->setNumLines (md.model.lines.size ());
}
//------------------------------------------------------------------------
void TextEditorView::layoutRows () const
{
NewLineProcessor::update (md.model.lines, md.model.text, 0);
CCoord width = 0.;
for (auto it = md.model.lines.begin (); it != md.model.lines.end (); ++it)
{
auto w = updateLineText (it);
if (w > width)
width = w;
}
md.maxWidth = std::ceil (width + md.horizontalLineMargin);
md.maxHeight = md.model.lines.size () * md.lineHeight + md.fontDescent;
mutableThis ().parentSizeChanged ();
}
//------------------------------------------------------------------------
void NewLineProcessor::update (Lines& lines, StringView text, size_t startRow)
{
auto start = text.begin ();
if (lines.empty ())
lines.resize (1);
if (startRow && startRow < lines.size ())
{
std::advance (start, lines[startRow].range.start);
}
auto end = text.end ();
size_t startIndex = lines[startRow].range.start;
size_t row = startRow;
for (auto it = start; it != end; ++it)
{
if ((*it) == '\n')
{
if (row >= lines.size ())
lines.emplace_back (Line ());
lines[row].range = {startIndex, static_cast<size_t> (std::distance (start, it)) + 1};
startIndex = lines[row].range.start + lines[row].range.length;
std::advance (start, lines[row].range.length);
++row;
}
}
lines.resize (row + 1);
lines[row].range = {startIndex, static_cast<size_t> (std::distance (start, end))};
}
//------------------------------------------------------------------------
void TextEditorView::insertNewLine () const
{
auto cursor = md.editState.cursor;
auto currentLine = findLine (md.model.lines.begin (), md.model.lines.end (), cursor);
vstgui_assert (currentLine != md.model.lines.end ());
std::u32string insertStr = U"\n";
auto isWhiteSpace = [] (char32_t character) {
return character == u'\t' || character == u' ';
};
for (auto index = static_cast<int> (currentLine->range.start); index < cursor; ++index)
{
if (isWhiteSpace (md.model.text[index]))
{
insertStr += md.model.text[index];
continue;
}
break;
}
callSTB ([&] () {
stb_textedit_paste (this, &md.editState, insertStr.data (),
static_cast<int32_t> (insertStr.size ()));
});
}
//------------------------------------------------------------------------
void TextEditorView::validateLineStarts (Lines::const_iterator it,
const Lines::const_iterator& end) const
{
#if DEBUG
auto prev = it;
while (++it != end)
{
vstgui_assert (it->range.start > prev->range.start);
vstgui_assert (it->range.start == prev->range.end ());
prev = it;
}
#endif
}
//------------------------------------------------------------------------
void TextEditorView::invalidateSingleLine (size_t pos, int32_t numChars) const
{
auto line = findLine (md.model.lines.begin (), md.model.lines.end (), pos);
invalidateSingleLine (line, numChars);
}
//------------------------------------------------------------------------
void TextEditorView::invalidateSingleLine (Lines::iterator& line, int32_t numChars) const
{
if (line == md.model.lines.end ())
return;
auto wasMaxWidth = std::ceil (line->width + md.horizontalLineMargin) == md.maxWidth;
if (numChars < 0)
{
invalidLine (line, true);
}
line->range.length += numChars;
auto width = std::ceil (updateLineText (line) + md.horizontalLineMargin);
if (width > md.maxWidth)
{
md.maxWidth = width;
mutableThis ().parentSizeChanged ();
}
invalidLine (line);
if (numChars)
{
auto it = line;
++it;
for (; it != md.model.lines.end (); ++it)
it->range.start += numChars;
}
if (wasMaxWidth && width != md.maxWidth)
{
auto newMaxWidth = std::ceil (calculateMaxWidth () + md.horizontalLineMargin);
if (md.maxWidth != newMaxWidth)
{
md.maxWidth = newMaxWidth;
mutableThis ().parentSizeChanged ();
}
}
}
//------------------------------------------------------------------------
void TextEditorView::invalidateLines (size_t pos, int32_t numChars) const
{
auto line = findLine (md.model.lines.begin (), md.model.lines.end (), pos);
vstgui_assert (line != md.model.lines.end ());
bool numLinesChanged = false;
if (numChars > 0)
{
int32_t counter = 0;
for (auto index = pos; index < pos + numChars; ++index)
{
if (md.model.text[index] == u'\n')
{
auto change = static_cast<int32_t> (index - line->range.start) -
static_cast<int32_t> (line->range.length);
auto nextLineLength = -change + counter;
if (line->range.length > 0)
{
line->range.length -= 1;
change++;
}
invalidateSingleLine (line, change + 1);
counter = 0;
Range nextLineRange {line->range.end (), static_cast<size_t> (nextLineLength)};
line = md.model.lines.emplace (++line, Line {nextLineRange, "", 0.});
updateLineText (line);
auto nextLine = line;
auto prevLine = line;
while (++nextLine != md.model.lines.end ())
{
nextLine->range.start = prevLine->range.end ();
prevLine = nextLine;
}
numLinesChanged = true;
}
else
{
++counter;
}
}
if (counter > 0)
invalidateSingleLine (line, counter);
}
else
{
int32_t counter = std::abs (numChars);
auto lineIndex = std::distance (md.model.lines.begin (), line);
while (counter > 0)
{
auto numDel = std::min (counter, static_cast<int32_t> (line->range.end ()) -
static_cast<int32_t> (pos));
vstgui_assert (md.model.text.size () > (pos + numDel - 1));
bool isNewLine = md.model.text[pos + numDel - 1] == u'\n';
md.model.text.erase (pos, numDel);
invalidLine (lineIndex++, true);
if (isNewLine)
{
auto nextLine = line;
++nextLine;
nextLine->range.length += line->range.length - numDel;
nextLine->range.start = line->range.start;
nextLine = line = md.model.lines.erase (line);
numLinesChanged = true;
invalidateSingleLine (nextLine, 0);
auto prevLine = nextLine;
while (++nextLine != md.model.lines.end ())
{
nextLine->range.start = prevLine->range.end ();
prevLine = nextLine;
}
}
else
{
invalidateSingleLine (pos, -static_cast<int32_t> (numDel));
}
counter -= numDel;
}
if (numLinesChanged)
{
auto width = std::ceil (calculateMaxWidth () + md.horizontalLineMargin);
if (width != md.maxWidth)
md.maxWidth = width;
}
}
if (numLinesChanged)
{
md.maxHeight = md.model.lines.size () * md.lineHeight + md.fontDescent;
updateLineNumbersView ();
if (numChars < 0)
invalidate ();
else
{
for (auto l = findLine (md.model.lines.begin (), md.model.lines.end (), pos);
l != md.model.lines.end (); ++l)
invalidLine (l, true);
}
mutableThis ().parentSizeChanged ();
}
validateLineStarts (md.model.lines.begin (), md.model.lines.end ());
}
//------------------------------------------------------------------------
int32_t TextEditorView::deleteChars (size_t pos, size_t num) const
{
if (isReadOnlyMode ())
return false;
if (pos >= md.model.text.size ())
return false;
invalidateLines (pos, -static_cast<int32_t> (num));
if (md.controller)
md.controller->onTextEditorTextChanged (*this);
return true;
}
//------------------------------------------------------------------------
int32_t TextEditorView::insertChars (size_t pos, const CharT* chars, size_t num) const
{
if (isReadOnlyMode ())
return false;
md.isInsertingText = true;
if (pos >= md.model.text.size ())
pos = md.model.text.size ();
md.model.text.insert (pos, chars, num);
invalidateLines (pos, static_cast<int32_t> (num));
md.isInsertingText = false;
if (md.controller)
md.controller->onTextEditorTextChanged (*this);
return true;
}
//------------------------------------------------------------------------
template<typename T>
T TextEditorView::findLine (T begin, T end, size_t pos) const
{
vstgui_assert (begin != end);
auto result =
std::upper_bound (begin, end, Line {{pos, 0}}, [&] (const auto& a, const auto& b) {
return a.range.end () < b.range.end ();
});
if (result == end)
--result;
return result;
}
//------------------------------------------------------------------------
void TextEditorView::layout (StbTexteditRow* row, size_t start_i) const
{
*row = {};
if (static_cast<size_t> (start_i) >= md.model.text.size ())
return;
if (md.stbInternalIterator == md.model.lines.end ())
md.stbInternalIterator = md.model.lines.begin ();
row->baseline_y_delta = static_cast<float> (md.lineHeight);
row->ymin = static_cast<float> (-md.fontDescent);
row->ymax = static_cast<float> (md.lineHeight);
row->x0 = static_cast<float> (md.style->leftMargin);
row->x1 = static_cast<float> (getWidth ());
if (start_i == md.stbInternalIterator->range.start)
{
row->num_chars = static_cast<int> (md.stbInternalIterator->range.length);
++md.stbInternalIterator;
return;
}
md.stbInternalIterator = md.model.lines.begin ();
auto line = findLine (md.model.lines.begin (), md.model.lines.end (), start_i);
vstgui_assert (line != md.model.lines.end ());
row->num_chars = static_cast<int> (line->range.length);
}
//------------------------------------------------------------------------
float TextEditorView::getCharWidth (size_t row, size_t pos) const
{
if (row + pos >= md.model.text.length ())
return STB_TEXTEDIT_GETWIDTH_NEWLINE;
auto platformFont = md.style->font->getPlatformFont ();
vstgui_assert (platformFont);
auto fontPainter = platformFont->getPainter ();
vstgui_assert (fontPainter);
auto c = md.model.text[row + pos];
UTF8String str (convert (&c, 1));
if (str[0] == '\n')
return STB_TEXTEDIT_GETWIDTH_NEWLINE;
if (md.style->tabWidth > 0 && str[0] == '\t')
{
auto posOffset = 0;
auto numTabs = 0u;
size_t numSpaces = md.style->tabWidth;
for (auto index = 0u; index <= pos; ++index)
{
if (md.model.text[row + index] == '\t')
{
numSpaces =
md.style->tabWidth - (((index - numTabs) + posOffset) % md.style->tabWidth);
posOffset += numSpaces;
++numTabs;
}
}
str = std::string (numSpaces, ' ');
}
auto width = fontPainter->getStringWidth (nullptr, str.getPlatformString (), true);
return static_cast<float> (width / getGlobalTransform (true).m11);
}
//------------------------------------------------------------------------
CharT TextEditorView::getChar (int32_t pos) const
{
if (pos < getLength ())
return md.model.text[pos];
return 0;
}
//------------------------------------------------------------------------
int32_t TextEditorView::getLength () const
{
return static_cast<int32_t> (md.model.text.length ());
}
//------------------------------------------------------------------------
void TextEditorView::onSelectionChanged (Range newSel, bool forceInvalidation) const
{
Range newSelectedLines = {};
auto line = findLine (md.model.lines.begin (), md.model.lines.end (), newSel.start);
vstgui_assert (line != md.model.lines.end ());
newSelectedLines.start = std::distance (md.model.lines.begin (), line);
if (newSel.length > 0)
{
auto endLine = ++(findLine (md.model.lines.begin (), md.model.lines.end (), newSel.end ()));
newSelectedLines.length = std::distance (line, endLine);
}
if (forceInvalidation || newSelectedLines != md.selectedLines)
{
invalidSelectedLines ();
md.selectedLines = newSelectedLines;
invalidSelectedLines ();
}
if (md.lineNumberView)
md.lineNumberView->setSelectedLines (newSelectedLines);
if (md.selectedLines.length > 0)
{
md.blinkTimer = nullptr;
md.cursorIsVisible = false;
}
}
//------------------------------------------------------------------------
template<bool forward, typename iterator_t>
inline bool findStopChar (iterator_t& it, iterator_t end)
{
for (; it != end; forward ? ++it : --it)
{
auto cursorChar = *it;
if (isStopChar (cursorChar))
return true;
}
return false;
}
//------------------------------------------------------------------------
template<bool forward>
void TextEditorView::selectPair (size_t startPos, char32_t closingChar) const
{
auto it = md.model.text.begin ();
std::advance (it, startPos);
auto openChar = *it;
int32_t numOpenChars = 0;
while (true)
{
if (*it == openChar)
++numOpenChars;
if (*it == closingChar)
{
if (--numOpenChars == 0)
{
md.editState.select_start = static_cast<int> (startPos);
md.editState.select_end =
static_cast<int> (std::distance (md.model.text.begin (), it)) + 1;
if constexpr (!forward)
{
md.editState.select_start += 1;
md.editState.select_end -= 1;
}
onSelectionChanged (makeRange (md.editState), true);
return;
}
}
if constexpr (forward)
{
++it;
if (it == md.model.text.end ())
break;
}
else
{
if (it == md.model.text.begin ())
break;
--it;
}
}
}
//------------------------------------------------------------------------
void TextEditorView::selectOnDoubleClick (uint32_t clickCount) const
{
auto cursor = static_cast<size_t> (md.editState.cursor);
if (cursor >= md.model.text.size ())
return;
if (clickCount > 2)
{
auto currentLine =
findLine (md.model.lines.begin (), md.model.lines.end (), md.editState.cursor);
vstgui_assert (currentLine != md.model.lines.end ());
md.editState.select_start = static_cast<int> (currentLine->range.start);
md.editState.select_end = static_cast<int> (currentLine->range.end ());
onSelectionChanged (makeRange (md.editState), true);
md.editStateOnMouseDown = md.editState;
return;
}
auto cursorChar = md.model.text[cursor];
switch (cursorChar)
{
case u'(':
{
selectPair<true> (cursor, ')');
return;
}
case u')':
{
selectPair<false> (cursor, '(');
return;
}
case u'{':
{
selectPair<true> (cursor, '}');
return;
}
case u'}':
{
selectPair<false> (cursor, '{');
return;
}
case u'[':
{
selectPair<true> (cursor, ']');
return;
}
case u']':
{
selectPair<false> (cursor, '[');
return;
}
case u'<':
{
selectPair<true> (cursor, '>');
return;
}
case u'>':
{
selectPair<false> (cursor, '<');
return;
}
};
if (isStopChar (cursorChar))
return;
if (cursor > 0)
{
auto it = md.model.text.begin ();
std::advance (it, cursor - 1);
if (findStopChar<false> (it, md.model.text.begin ()))
++it;
md.editState.select_start = static_cast<int> (std::distance (md.model.text.begin (), it));
}
if (cursor < md.model.text.size ())
{
auto it = md.model.text.begin ();
std::advance (it, cursor + 1);
findStopChar<true> (it, md.model.text.end ());
md.editState.select_end = static_cast<int> (std::distance (md.model.text.begin (), it));
}
md.editStateOnMouseDown = md.editState;
onSelectionChanged (makeRange (md.editState), true);
}
//------------------------------------------------------------------------
void TextEditorView::updateSelectionOnDoubleClickMove (uint32_t clickCount) const
{
auto cursor = static_cast<size_t> (md.editState.cursor);
if (cursor >= md.model.text.size ())
return;
if (clickCount > 2)
{
auto currentLine = findLine (md.model.lines.begin (), md.model.lines.end (), cursor);
vstgui_assert (currentLine != md.model.lines.end ());
if (currentLine->range.start >= static_cast<size_t> (md.editStateOnMouseDown.select_start))
{
md.editState.select_start = md.editStateOnMouseDown.select_start;
md.editState.select_end = static_cast<int> (currentLine->range.end ());
}
else
{
md.editState.select_start = static_cast<int> (currentLine->range.start);
md.editState.select_end = md.editStateOnMouseDown.select_end;
}
onSelectionChanged (makeRange (md.editState), true);
return;
}
if (cursor < static_cast<size_t> (md.editStateOnMouseDown.select_start))
{
auto it = md.model.text.begin ();
std::advance (it, cursor - 1);
if (findStopChar<false> (it, md.model.text.begin ()))
++it;
auto start = static_cast<int> (std::distance (md.model.text.begin (), it));
if (start < md.editStateOnMouseDown.select_start)
md.editState.select_start = start;
else
md.editState.select_start = md.editStateOnMouseDown.select_start;
md.editState.select_end = md.editStateOnMouseDown.select_end;
}
else
{
auto it = md.model.text.begin ();
std::advance (it, cursor + 1);
findStopChar<true> (it, md.model.text.end ());
auto end = static_cast<int> (std::distance (md.model.text.begin (), it));
if (end > md.editStateOnMouseDown.select_end)
md.editState.select_end = end;
else
md.editState.select_end = md.editStateOnMouseDown.select_end;
md.editState.select_start = md.editStateOnMouseDown.select_start;
}
onSelectionChanged (makeRange (md.editState), true);
}
//------------------------------------------------------------------------
void TextEditorView::onCursorChanged (int oldCursorPos, int newCursorPos) const
{
if (!md.lastDrawnCursorRect.isEmpty ())
invalidateRect (md.lastDrawnCursorRect);
md.cursorRect = calculateCursorRect (newCursorPos);
auto r = invalidCursorRect ();
if (md.scrollView)
{
if (md.editState.select_end != md.editState.select_start)
{
auto endRect = calculateCursorRect (md.editState.select_end);
endRect.offset (getViewSize ().getTopLeft ());
r.unite (endRect);
}
r.bottom += md.fontDescent;
r.inset (-md.style->leftMargin, 0);
md.scrollView->makeRectVisible (r);
}
}
//------------------------------------------------------------------------
size_t TextEditorView::moveToWordPrevious (size_t pos) const
{
if (pos == 0)
return 0;
--pos;
auto it = md.model.text.begin ();
std::advance (it, pos);
auto firstIsStopChar = isStopChar (*it);
for (; it != md.model.text.begin (); --it)
{
auto cursorChar = *it;
if (isStopChar (cursorChar) != firstIsStopChar)
{
++it;
break;
}
}
return std::distance (md.model.text.begin (), it);
}
//------------------------------------------------------------------------
size_t TextEditorView::moveToWordNext (size_t pos) const
{
if (pos >= md.model.text.size ())
return pos;
++pos;
auto it = md.model.text.begin ();
std::advance (it, pos);
auto firstIsStopChar = isStopChar (*it);
for (; it != md.model.text.end (); ++it)
{
auto cursorChar = *it;
if (isStopChar (cursorChar) != firstIsStopChar)
break;
}
return std::distance (md.model.text.begin (), it);
}
//-----------------------------------------------------------------------------
template<typename Proc>
bool TextEditorView::callSTB (Proc proc) const
{
md.stbInternalIterator = md.model.lines.begin ();
auto oldState = md.editState;
auto oldTextLength = md.model.text.length ();
proc ();
if (oldTextLength != md.model.text.length () ||
memcmp (&oldState, &md.editState, sizeof (STB_TexteditState)) != 0)
{
if (oldState.select_start != md.editState.select_start ||
oldState.select_end != md.editState.select_end)
onSelectionChanged (makeRange (md.editState), true);
if (oldState.cursor != md.editState.cursor)
{
onCursorChanged (oldState.cursor, md.editState.cursor);
auto selRange = makeRange (md.editState);
if (selRange.length == 0)
onSelectionChanged (makeRange (md.editState.cursor, md.editState.cursor));
}
if (md.editState.select_start == md.editState.select_end)
md.editState.select_start = md.editState.select_end = md.editState.cursor;
return true;
}
return false;
}
//------------------------------------------------------------------------
bool TextEditorView::doShifting (bool right) const
{
if (isReadOnlyMode ())
return false;
if (md.editState.select_start > md.editState.select_end)
std::swap (md.editState.select_start, md.editState.select_end);
auto hasSelection = md.editState.select_start != md.editState.select_end;
if (!hasSelection)
md.editState.select_start = md.editState.select_end = md.editState.cursor;
auto lineStart =
findLine (md.model.lines.begin (), md.model.lines.end (), md.editState.select_start);
vstgui_assert (lineStart != md.model.lines.end ());
auto lineEnd = !hasSelection
? lineStart
: findLine (lineStart, md.model.lines.end (), md.editState.select_end - 1);
vstgui_assert (lineEnd != md.model.lines.end ());
++lineEnd;
auto originCursor = md.editState.cursor;
auto originSelectStart = md.editState.select_start;
auto originSelectEnd = md.editState.select_end;
md.editState.select_start = md.editState.select_end = originCursor;
auto numChanges = 0;
if (right)
{
while (lineStart != lineEnd)
{
--lineEnd;
md.editState.cursor = static_cast<int> (lineEnd->range.start);
md.editState.select_start = md.editState.select_end = md.editState.cursor;
callSTB ([&] () { stb_textedit_key (this, &md.editState, u'\t'); });
++numChanges;
}
md.editState.select_start = originSelectStart + 1;
md.editState.select_end = originSelectEnd + numChanges;
}
else
{
while (lineStart != lineEnd)
{
--lineEnd;
md.editState.cursor = static_cast<int> (lineEnd->range.start);
if (md.model.text[md.editState.cursor] == u'\t')
{
md.editState.select_start = md.editState.cursor;
md.editState.select_end = md.editState.cursor + 1;
callSTB ([&] () { stb_textedit_key (this, &md.editState, STB_TEXTEDIT_K_DELETE); });
++numChanges;
}
}
md.editState.select_start = originSelectStart - (numChanges ? 1 : 0);
md.editState.select_end = originSelectEnd - numChanges;
}
if (hasSelection)
{
if (originSelectStart == static_cast<int> (lineStart->range.start))
md.editState.select_start = originSelectStart;
}
md.editState.cursor = md.editState.select_start;
onCursorChanged (originCursor, md.editState.cursor);
onSelectionChanged (makeRange (md.editState));
return true;
}
//------------------------------------------------------------------------
void TextEditorView::selectAll () const
{
md.editState.select_start = 0;
md.editState.select_end = getLength ();
onSelectionChanged (makeRange (md.editState), true);
}
//------------------------------------------------------------------------
bool TextEditorView::doCut () const
{
if (isReadOnlyMode ())
return false;
if (doCopy ())
{
callSTB ([&] () { stb_textedit_cut (this, &md.editState); });
return true;
}
return false;
}
//------------------------------------------------------------------------
bool TextEditorView::doCopy () const
{
if (md.editState.select_start == md.editState.select_end)
return false;
auto start = md.editState.select_start;
auto end = md.editState.select_end;
if (start > end)
std::swap (start, end);
auto txt = convert (md.model.text.data () + start, end - start);
auto dataPackage =
CDropSource::create (txt.data (), static_cast<uint32_t> (txt.size ()), IDataPackage::kText);
getFrame ()->setClipboard (dataPackage);
return true;
}
//------------------------------------------------------------------------
bool TextEditorView::doPaste () const
{
if (isReadOnlyMode ())
return false;
if (auto clipboard = getFrame ()->getClipboard ())
{
auto count = clipboard->getCount ();
for (auto i = 0u; i < count; ++i)
{
const void* buffer;
IDataPackage::Type dataType;
auto size = clipboard->getData (i, buffer, dataType);
if (dataType == IDataPackage::kText)
{
auto txt = reinterpret_cast<const char*> (buffer);
try
{
auto uText = convert (txt, size);
convertWinLineEndingsToUnixLineEndings (uText);
callSTB ([&] () {
stb_textedit_paste (this, &md.editState, uText.data (),
static_cast<int32_t> (uText.size ()));
});
return true;
}
catch (...)
{
return false;
}
}
}
}
return false;
}
//------------------------------------------------------------------------
bool TextEditorView::useSelectionForFind () const
{
if (auto range = makeRange (md.editState))
{
setFindString (md.model.text.substr (range.start, range.length));
return true;
}
return false;
}
//------------------------------------------------------------------------
bool TextEditorView::doFind (bool forward, size_t oldPos) const
{
if (md.findString.empty ())
return false;
std::optional<size_t> firstFound = {};
auto pos = String::npos;
while (true)
{
if (md.findOptions & FindOption::CaseSensitive)
pos = doFindCaseSensitive (forward);
else
pos = doFindIgnoreCase (forward);
if (pos == String::npos)
break;
auto oldCursor = md.editState.cursor;
md.editState.cursor = static_cast<int> (pos);
md.editState.select_start = md.editState.cursor;
md.editState.select_end = md.editState.cursor + static_cast<int> (md.findString.length ());
if (md.findOptions & FindOption::WholeWords)
{
if ((pos > 0 && !isStopChar (md.model.text[pos - 1])) ||
(pos + md.findString.length () < md.model.text.size () &&
!isStopChar (md.model.text[pos + md.findString.length ()])))
{
if (firstFound)
{
if (pos == *firstFound)
break;
}
else
{
firstFound = {pos};
}
continue;
}
}
onCursorChanged (oldCursor, md.editState.cursor);
onSelectionChanged ({makeRange (md.editState)}, true);
return true;
}
if (md.editState.select_start != md.editState.select_end)
{
md.editState.select_end = md.editState.select_start;
onSelectionChanged ({makeRange (md.editState)}, true);
}
return false;
}
//------------------------------------------------------------------------
String::size_type TextEditorView::doFindCaseSensitive (bool forward) const
{
auto pos = String::npos;
if (forward)
{
auto cursor = static_cast<size_t> (md.editState.select_end);
if (cursor > md.model.text.length ())
cursor = 0;
auto text = StringView (md.model.text.data (), md.model.text.length ());
pos = text.find (md.findString.data (), cursor, md.findString.length ());
if (pos == String::npos && cursor != 0 /*wrap around*/)
pos = text.find (md.findString.data (), 0, md.findString.length ());
}
else
{
auto cursor = md.editState.select_start - md.findString.length ();
if (cursor < 0)
cursor = String::npos;
auto text = StringView (md.model.text.data (), md.model.text.length ());
pos = text.rfind (md.findString.data (), cursor);
if (pos == String::npos /*wrap around*/)
pos = text.rfind (md.findString.data (), pos);
}
return pos;
}
//------------------------------------------------------------------------
String::size_type TextEditorView::doFindIgnoreCase (bool forward) const
{
auto pos = String::npos;
if (forward)
{
auto cursor = static_cast<size_t> (md.editState.select_end);
if (cursor > md.model.text.length ())
cursor = 0;
auto text = StringView (md.model.text.data () + cursor, md.model.text.length () - cursor);
auto it = std::search (
text.begin (), text.end (), md.findString.begin (), md.findString.end (),
[] (auto ch1, auto ch2) { return std::toupper (ch1) == std::toupper (ch2); });
if (it != text.end ())
{
pos = cursor + std::distance (text.begin (), it);
}
else if (cursor != 0) // wrap arround
{
text = StringView (md.model.text.data (), cursor);
it = std::search (
text.begin (), text.end (), md.findString.begin (), md.findString.end (),
[] (auto ch1, auto ch2) { return std::toupper (ch1) == std::toupper (ch2); });
if (it != text.end ())
{
pos = std::distance (text.begin (), it);
}
}
}
else
{
auto cursor = md.editState.select_start - md.findString.length ();
if (cursor < 0)
cursor = md.model.text.length ();
auto text = StringView (md.model.text.data (), cursor);
auto it = std::search (
text.rbegin (), text.rend (), md.findString.rbegin (), md.findString.rend (),
[] (auto ch1, auto ch2) { return std::toupper (ch1) == std::toupper (ch2); });
if (it != text.rend ())
{
pos = cursor - (std::distance (text.rbegin (), it) + md.findString.length ());
}
else if (cursor < md.model.text.length ()) // wrap arround
{
text = StringView (md.model.text.data () + cursor, md.model.text.length () - cursor);
it = std::search (
text.rbegin (), text.rend (), md.findString.rbegin (), md.findString.rend (),
[] (auto ch1, auto ch2) { return std::toupper (ch1) == std::toupper (ch2); });
if (it != text.rend ())
{
pos = (md.model.text.length () - md.findString.length ()) -
std::distance (text.rbegin (), it);
}
}
}
return pos;
}
//------------------------------------------------------------------------
FindPanelController::FindPanelController (const ITextEditor& editor) : editor (editor) {}
//------------------------------------------------------------------------
FindPanelController::~FindPanelController () noexcept
{
if (caseSensitiveButton)
{
caseSensitiveButton->setListener (nullptr);
caseSensitiveButton->unregisterViewEventListener (this);
caseSensitiveButton->unregisterViewListener (this);
}
if (wholeWordButton)
{
wholeWordButton->setListener (nullptr);
wholeWordButton->unregisterViewEventListener (this);
wholeWordButton->unregisterViewListener (this);
}
if (closeBox)
{
closeBox->setListener (nullptr);
closeBox->unregisterViewListener (this);
}
if (editfield)
{
editfield->setListener (nullptr);
editfield->unregisterViewEventListener (this);
editfield->unregisterViewListener (this);
}
}
//------------------------------------------------------------------------
void FindPanelController::valueChanged (CControl* control)
{
switch (control->getTag ())
{
case FindPanelController::Textfield:
{
editor.setFindString (editfield->getText ().getString ());
break;
}
case FindPanelController::CloseButton:
{
removeFindPanelFunc ();
break;
}
case FindPanelController::FindPrevious:
{
if (control->getValue () != control->getMax ())
editor.handleCommand (ITextEditor::Command::FindPrevious);
break;
}
case FindPanelController::FindNext:
{
if (control->getValue () != control->getMax ())
editor.handleCommand (ITextEditor::Command::FindNext);
break;
}
case FindPanelController::CaseSensitive:
{
if (control->getValue () == control->getMax ())
findOptions.add (ITextEditor::FindOption::CaseSensitive);
else
findOptions.remove (ITextEditor::FindOption::CaseSensitive);
editor.setFindOptions (findOptions);
break;
}
case FindPanelController::WholeWords:
{
if (control->getValue () == control->getMax ())
findOptions.add (ITextEditor::FindOption::WholeWords);
else
findOptions.remove (ITextEditor::FindOption::WholeWords);
editor.setFindOptions (findOptions);
break;
}
}
}
//------------------------------------------------------------------------
void FindPanelController::viewLostFocus (CView* view)
{
if (view == editfield)
{
editor.setFindString (editfield->getText ().getString ());
if (editfield->bWasReturnPressed)
{
editor.handleCommand (ITextEditor::Command::TakeFocus);
editfield->bWasReturnPressed = false;
}
}
}
//------------------------------------------------------------------------
void FindPanelController::viewOnEvent (CView* view, Event& event)
{
if (event.type == EventType::KeyDown)
{
auto& keyEvent = castKeyboardEvent (event);
if (keyEvent.virt == VirtualKey::Escape)
{
removeFindPanelFunc ();
event.consumed = true;
}
else if (commandKeys[Index (ITextEditor::Command::FindNext)] == keyEvent)
{
editor.handleCommand (ITextEditor::Command::FindNext);
event.consumed = true;
}
else if (commandKeys[Index (ITextEditor::Command::FindPrevious)] == keyEvent)
{
editor.handleCommand (ITextEditor::Command::FindPrevious);
event.consumed = true;
}
}
}
//------------------------------------------------------------------------
void FindPanelController::viewWillDelete (CView* view)
{
view->unregisterViewListener (this);
if (view == closeBox)
closeBox = nullptr;
else if (view == editfield)
editfield = nullptr;
else if (view == caseSensitiveButton)
{
caseSensitiveButton->unregisterViewEventListener (this);
caseSensitiveButton = nullptr;
}
else if (view == wholeWordButton)
{
wholeWordButton->unregisterViewEventListener (this);
wholeWordButton = nullptr;
}
else if (view->asViewContainer ())
delete this;
}
//------------------------------------------------------------------------
void FindPanelController::setFindString (StringView text)
{
if (editfield)
editfield->setText (convert (text.data (), text.size ()).data ());
}
//------------------------------------------------------------------------
void FindPanelController::setFindOptions (ITextEditor::FindOptions opt) { findOptions = opt; }
//------------------------------------------------------------------------
CViewContainer* FindPanelController::makeFindPanelView (CRect vcr, TextEditorView::ModelData& md,
const ITextEditor& editor,
RemoveFindPanelFunc&& f)
{
auto controller = new FindPanelController (editor);
controller->removeFindPanelFunc = std::move (f);
controller->commandKeys = md.commandKeys;
controller->setFindOptions (md.findOptions);
auto margin = (vcr.getHeight () - md.lineHeight) / 2.;
auto buttonWidth = vcr.getHeight ();
CViewContainer* findPanel = new CViewContainer (vcr);
findPanel->setTransparency (true);
vcr.inset (margin, margin);
auto tefr = vcr;
tefr.left += buttonWidth + margin;
tefr.right -= 6. * (buttonWidth + margin) + margin;
CTextEdit* editfield = new CTextEdit (tefr, controller, FindPanelController::Textfield);
editfield->setPlaceholderString ("Find");
editfield->setTextInset ({margin, 0});
editfield->setImmediateTextChange (true);
editfield->setAutosizeFlags (kAutosizeLeft | kAutosizeRight | kAutosizeTop);
editfield->setHoriAlign (kLeftText);
editfield->setFrameColor (md.style->frameColor);
editfield->setFrameWidth (-1.);
editfield->setBackColor (md.style->backColor);
editfield->setFont (md.style->font);
editfield->setFontColor (md.style->textColor);
auto styleButton = [&] (CTextButton* button) {
button->setFont (md.style->font);
button->setAutosizeFlags (kAutosizeTop | kAutosizeRight);
button->setFrameWidth (1.);
button->setRoundRadius (2.);
auto gradient =
owned (CGradient::create (0., 1., md.style->backColor, md.style->backColor));
button->setGradient (gradient);
auto isOnOffStyle = button->getStyle () == CTextButton::Style::kOnOffStyle;
if (isOnOffStyle)
gradient = owned (CGradient::create (0., 1., md.style->textColor, md.style->textColor));
button->setGradientHighlighted (gradient);
button->setTextColor (md.style->textColor);
auto color = md.style->frameColor;
color.alpha /= 2;
button->setTextColorHighlighted (isOnOffStyle ? md.style->backColor : md.style->textColor);
button->setFrameColor (md.style->frameColor);
button->setFrameColorHighlighted (color);
};
auto cbfr = vcr;
cbfr.right = tefr.left - margin;
auto closeBox = new CTextButton (cbfr, controller, FindPanelController::CloseButton);
closeBox->setTitle ("X");
styleButton (closeBox);
closeBox->setAutosizeFlags (kAutosizeLeft | kAutosizeTop);
auto icfr = tefr;
icfr.left = tefr.right + margin;
icfr.right = icfr.left + buttonWidth * 2;
auto caseSensitiveButton =
new CTextButton (icfr, controller, FindPanelController::CaseSensitive);
caseSensitiveButton->setTitle ("Aa");
caseSensitiveButton->setStyle (CTextButton::Style::kOnOffStyle);
caseSensitiveButton->setValue (md.findOptions & ITextEditor::FindOption::CaseSensitive ? 1.f
: 0.f);
styleButton (caseSensitiveButton);
icfr.offset (margin + buttonWidth * 2, 0);
auto wholeWordButton = new CTextButton (icfr, controller, FindPanelController::WholeWords);
wholeWordButton->setTitle ("|w|");
wholeWordButton->setStyle (CTextButton::Style::kOnOffStyle);
wholeWordButton->setValue (md.findOptions & ITextEditor::FindOption::WholeWords ? 1.f : 0.f);
styleButton (wholeWordButton);
auto tbfr = icfr;
tbfr.left = tbfr.right + margin;
tbfr.right = tbfr.left + buttonWidth;
auto prevButton = new CTextButton (tbfr, controller, FindPanelController::FindPrevious);
prevButton->setTitle ("<");
styleButton (prevButton);
tbfr.offset (margin + buttonWidth, 0);
auto nextButton = new CTextButton (tbfr, controller, FindPanelController::FindNext);
nextButton->setTitle (">");
styleButton (nextButton);
editfield->registerViewListener (controller);
editfield->registerViewEventListener (controller);
closeBox->registerViewListener (controller);
caseSensitiveButton->registerViewListener (controller);
wholeWordButton->registerViewListener (controller);
findPanel->registerViewListener (controller);
wholeWordButton->registerViewEventListener (controller);
findPanel->registerViewEventListener (controller);
controller->editfield = editfield;
controller->closeBox = closeBox;
controller->caseSensitiveButton = caseSensitiveButton;
controller->wholeWordButton = wholeWordButton;
findPanel->addView (closeBox);
findPanel->addView (editfield);
findPanel->addView (caseSensitiveButton);
findPanel->addView (wholeWordButton);
findPanel->addView (prevButton);
findPanel->addView (nextButton);
findPanel->setInitialFocusView (editfield);
controller->setFindString (md.findString);
md.findPanelController = controller;
return findPanel;
}
//------------------------------------------------------------------------
bool TextEditorView::showFindPanel () const
{
if (!md.scrollView)
return false;
if (auto view = md.scrollView->getEdgeView (CScrollView::Edge::Top))
{
if (auto frame = getFrame ())
{
if (auto container = view->asViewContainer ())
container->advanceNextFocusView (nullptr);
else
frame->setFocusView (view);
}
return true;
}
auto margin = 3.;
auto vcr = md.scrollView->getVisibleClientRect ();
vcr.originize ();
vcr.setHeight (md.lineHeight + margin * 2);
using namespace Animation;
auto findPanel = FindPanelController::makeFindPanelView (vcr, md, *this, [&] () {
md.findPanelController = nullptr;
if (auto panel = md.scrollView->getEdgeView (CScrollView::Edge::Top))
{
auto size = panel->getViewSize ();
size.setHeight (1.);
if (auto frame = panel->getFrame ())
frame->setFocusView (nullptr);
panel->addAnimation (
"ResizeAnimation", new ViewSizeAnimation (size, false),
CubicBezierTimingFunction::make (CubicBezierTimingFunction::EasyInOut, 120),
[&] (auto, auto, auto) {
md.scrollView->setEdgeView (CScrollView::Edge::Top, nullptr);
handleCommand (Command::TakeFocus);
});
}
});
md.scrollView->setEdgeView (CScrollView::Edge::Top, findPanel);
if (auto frame = getFrame ())
frame->setFocusView (nullptr);
findPanel->addAnimation (
"ResizeAnimation", new ViewSizeAnimation (findPanel->getViewSize (), false),
CubicBezierTimingFunction::make (CubicBezierTimingFunction::EasyInOut, 120),
[panel = shared (findPanel)] (auto, auto, auto) {
if (panel->isAttached ())
panel->advanceNextFocusView (nullptr);
});
vcr = findPanel->getViewSize ();
vcr.setHeight (1.);
findPanel->setViewSize (vcr);
return true;
}
//------------------------------------------------------------------------
void TextEditorView::setFindOptions (FindOptions opt) const
{
md.findOptions = opt;
if (md.findPanelController)
{
md.findPanelController->setFindOptions (md.findOptions);
md.editState.select_end = md.editState.select_start;
onSelectionChanged (makeRange (md.editState));
doFind ();
}
}
//------------------------------------------------------------------------
void TextEditorView::setFindString (std::string_view utf8Text) const
{
setFindString (convert (utf8Text.data (), utf8Text.size ()));
}
//------------------------------------------------------------------------
void TextEditorView::setFindString (String&& text) const
{
md.findString = std::move (text);
if (auto frame = getFrame ())
{
if (frame->getFocusView () != this)
{
md.editState.select_end = md.editState.select_start;
onSelectionChanged (makeRange (md.editState));
doFind ();
}
else if (md.findPanelController)
{
md.findPanelController->setFindString (md.findString);
}
}
}
//------------------------------------------------------------------------
bool TextEditorView::isReadOnlyMode () const
{
return md.style->flags & Style::Flags::ReadOnlyMode;
}
//------------------------------------------------------------------------
bool TextEditorView::gotoLine (size_t lineNo) const
{
if (md.model.lines.size () <= lineNo)
return false;
auto originCursor = md.editState.cursor;
md.editState.select_start = static_cast<int> (md.model.lines[lineNo].range.start);
md.editState.select_end = md.editState.select_start;
md.editState.cursor = md.editState.select_start;
onCursorChanged (originCursor, md.editState.cursor);
onSelectionChanged (makeRange (md.editState));
return true;
}
//------------------------------------------------------------------------
void TextEditorView::flushUndoList () const
{
auto pos = md.undoPos + 1;
if (pos != md.undoList.end ())
{
md.undoList.erase (pos, md.undoList.end ());
}
}
//------------------------------------------------------------------------
void TextEditorView::clearUndoList () const
{
checkCurrentUndoGroup (true);
md.undoList.clear ();
md.undoList.emplace_back (UndoGroup ());
md.undoPos = md.undoList.end ();
}
//------------------------------------------------------------------------
void TextEditorView::checkCurrentUndoGroup (bool force) const
{
auto currentTime = getPlatformFactory ().getTicks ();
if (force || (md.currentUndoGroup.time > 0 && md.currentUndoGroup.time < currentTime - 500))
{
if (md.currentUndoGroup.record.empty ())
return;
if (md.currentUndoGroup.time == 0)
md.currentUndoGroup.time = currentTime;
md.undoList.emplace_back (std::move (md.currentUndoGroup));
md.undoPos = md.undoList.end ();
--md.undoPos;
md.currentUndoGroup = {};
}
if (!force || md.currentUndoGroup.time != 0)
md.currentUndoGroup.time = currentTime;
}
//------------------------------------------------------------------------
CharT* TextEditorView::createUndoRecord (size_t pos, size_t insertLen, size_t deleteLen) const
{
if (isReadOnlyMode ())
return nullptr;
if (md.isInUndoRedo)
return nullptr;
checkCurrentUndoGroup (false);
if (md.undoPos != md.undoList.end ())
flushUndoList ();
md.currentUndoGroup.record.emplace_back (UndoRecord {});
auto& record = md.currentUndoGroup.record.back ();
record.position = pos;
record.deleted = deleteLen;
if (insertLen)
{
record.characters.resize (insertLen);
return record.characters.data ();
}
return nullptr;
}
//------------------------------------------------------------------------
template<bool iterateForward>
void TextEditorView::doUndoRedo () const
{
auto begin = [this] () {
if constexpr (iterateForward)
return md.undoPos->record.begin ();
else
return md.undoPos->record.rbegin ();
};
auto end = [this] () {
if constexpr (iterateForward)
return md.undoPos->record.end ();
else
return md.undoPos->record.rend ();
};
md.isInUndoRedo = true;
for (auto it = begin (); it != end (); ++it)
{
md.editState.cursor = md.editState.select_start = static_cast<int> (it->position);
if (it->deleted > 0)
{
md.editState.select_end = md.editState.select_start + static_cast<int> (it->deleted);
it->characters = md.model.text.substr (
md.editState.select_start, md.editState.select_end - md.editState.select_start);
it->deleted = 0;
callSTB ([&] () { stb_textedit_cut (this, &md.editState); });
}
else if (it->characters.empty () == false)
{
md.editState.select_end = md.editState.select_start;
callSTB ([&] () {
stb_textedit_paste (this, &md.editState, it->characters.data (),
static_cast<int> (it->characters.size ()));
});
it->deleted = it->characters.size ();
it->characters.clear ();
}
}
md.isInUndoRedo = false;
}
//------------------------------------------------------------------------
void TextEditorView::doUndo () const
{
checkCurrentUndoGroup (true);
if (md.undoPos == md.undoList.end () || md.undoPos == md.undoList.begin ())
return;
doUndoRedo<false> ();
--md.undoPos;
}
//------------------------------------------------------------------------
void TextEditorView::doRedo () const
{
checkCurrentUndoGroup (true);
if (std::distance (md.undoPos, md.undoList.end ()) <= 1)
return;
++md.undoPos;
if (md.undoPos == md.undoList.end ())
return;
doUndoRedo<true> ();
}
//------------------------------------------------------------------------
//--CocoaTextInputClient
void TextEditorView::CocoaTextInputClient::insertText (const std::u32string& string,
TextRange range)
{
if (markedText.empty () == false)
view.doUndo ();
else
view.checkCurrentUndoGroup (false);
if (range.length > 0 && range.position < view.md.model.text.size ())
{
view.md.editState.select_start = static_cast<int> (range.position);
view.md.editState.select_end = static_cast<int> (range.position + range.length);
view.md.editState.cursor = view.md.editState.select_start;
}
view.callSTB ([&] () {
stb_textedit_paste (&view, &view.md.editState, string.data (),
static_cast<int> (string.length ()));
});
view.restartBlinkTimer ();
}
//------------------------------------------------------------------------
void TextEditorView::CocoaTextInputClient::setMarkedText (const std::u32string& string,
TextRange selectedRange,
TextRange replacementRange)
{
if (markedText.empty () == false)
{
view.doUndo ();
}
else
{
if (view.md.editState.select_start != view.md.editState.select_end)
{
view.callSTB ([&] () { stb_textedit_delete_selection (&view, &view.md.editState); });
}
view.checkCurrentUndoGroup (true);
}
if (replacementRange.length > 0 && replacementRange.position < view.md.model.text.size ())
{
view.md.editState.select_start = static_cast<int> (replacementRange.position);
view.md.editState.select_end =
static_cast<int> (replacementRange.position + replacementRange.length);
view.md.editState.cursor = view.md.editState.select_end;
}
auto oldCursor = view.md.editState.cursor;
auto selectionStart = view.md.editState.select_start;
view.callSTB ([&] () {
stb_textedit_paste (&view, &view.md.editState, string.data (),
static_cast<int> (string.length ()));
});
view.md.editState.cursor = view.md.editState.select_start = selectionStart;
view.md.editState.select_end = static_cast<int> (selectionStart + string.length ());
view.onCursorChanged (oldCursor, view.md.editState.cursor);
view.onSelectionChanged (makeRange (view.md.editState));
markedText = string;
markedRange = {selectedRange.position, selectedRange.length};
}
//------------------------------------------------------------------------
bool TextEditorView::CocoaTextInputClient::hasMarkedText () { return !markedText.empty (); }
//------------------------------------------------------------------------
void TextEditorView::CocoaTextInputClient::unmarkText ()
{
markedText = {};
markedRange = {};
}
//------------------------------------------------------------------------
auto TextEditorView::CocoaTextInputClient::getMarkedRange () -> TextRange
{
return {markedRange.start, markedRange.length};
}
//------------------------------------------------------------------------
auto TextEditorView::CocoaTextInputClient::getSelectedRange () -> TextRange
{
auto start = static_cast<size_t> (view.md.editState.select_start);
auto end = static_cast<size_t> (view.md.editState.select_end);
if (end < start)
std::swap (start, end);
else if (start == end && start > 0)
return {start - 1, 1};
return {start, end - start};
}
//------------------------------------------------------------------------
CRect TextEditorView::CocoaTextInputClient::firstRectForCharacterRange (TextRange range,
TextRange& actualRange)
{
auto it = view.findLine (view.md.model.lines.begin (), view.md.model.lines.end (),
view.md.editState.cursor);
auto r = view.calculateLineRect (it);
actualRange = {static_cast<size_t> (view.md.editState.cursor), 0};
if (static_cast<int> (it->range.start) != view.md.editState.cursor)
{
auto t =
view.md.model.text.substr (it->range.start, view.md.editState.cursor - it->range.start);
auto nonSelectedText = convert (t);
replaceTabs (nonSelectedText, view.md.style->tabWidth, 0u);
auto str = getPlatformFactory ().createString (nonSelectedText.data ());
r.left += view.md.style->font->getFontPainter ()->getStringWidth (nullptr, str);
r.right = r.left;
}
return view.translateToGlobal (r);
}
//------------------------------------------------------------------------
std::u32string TextEditorView::CocoaTextInputClient::substringForRange (TextRange range,
TextRange& actualRange)
{
if (range.position >= view.md.model.text.size ())
{
auto line = view.findLine (view.md.model.lines.begin (), view.md.model.lines.end (),
view.md.editState.cursor);
assert (line != view.md.model.lines.end ());
range.position = line->range.start;
range.length = line->range.length;
actualRange = range;
return view.md.model.text.substr (range.position, range.length);
}
if (range.position < view.md.model.text.size ())
{
auto length = std::min (range.length, view.md.model.text.size () - range.position);
if (length > 0)
{
actualRange.position = range.position;
actualRange.length = length;
return view.md.model.text.substr (range.position, length);
}
}
return {};
}
//------------------------------------------------------------------------
size_t TextEditorView::CocoaTextInputClient::characterIndexForPoint (CPoint pos)
{
view.frameToLocal (pos);
if (view.hitTest (pos))
{
pos -= view.getViewSize ().getTopLeft ();
pos.y -= view.md.style->lineSpacing;
if (pos.y >= 0.)
{
auto lineIndex = static_cast<size_t> (std::floor (pos.y / view.md.lineHeight));
if (lineIndex < view.md.model.lines.size ())
{
pos.x += view.md.style->leftMargin;
const auto& line = view.md.model.lines[lineIndex];
if (pos.x >= 0. || pos.x <= line.width)
{
size_t index = 0u;
UTF8CharacterIterator it (line.text.data ());
while (it != it.back ())
{
auto substr = line.text.getString ().substr (
0, reinterpret_cast<UTF8StringPtr> (it.next ()) - line.text.data ());
auto platformText = getPlatformFactory ().createString (substr.data ());
auto width = view.md.fontPainer->getStringWidth (nullptr, platformText);
if (width > pos.x)
return line.range.start + index;
++index;
}
return line.range.start;
}
}
}
}
return std::numeric_limits<size_t>::max ();
}
//------------------------------------------------------------------------
void TextEditorView::CocoaTextInputClient::setCancelCallback (
const TextInputClientCancelCallback& callback)
{
cancelCallback = callback;
}
//------------------------------------------------------------------------
void TextEditorView::CocoaTextInputClient::cancel ()
{
if (hasMarkedText ())
{
view.doUndo ();
view.flushUndoList ();
}
unmarkText ();
if (cancelCallback)
cancelCallback ();
}
//------------------------------------------------------------------------
// IIMETextInputClient
//------------------------------------------------------------------------
bool TextEditorView::IMETextInputClient::ime_queryCharacterPosition (CharPosition& cp)
{
auto it = view.findLine (view.md.model.lines.begin (), view.md.model.lines.end (),
view.md.editState.cursor);
auto r = view.calculateLineRect (it);
if (static_cast<int> (it->range.start) != view.md.editState.cursor)
{
auto t =
view.md.model.text.substr (it->range.start, view.md.editState.cursor - it->range.start);
auto nonSelectedText = convert (t);
replaceTabs (nonSelectedText, view.md.style->tabWidth, 0u);
auto str = getPlatformFactory ().createString (nonSelectedText.data ());
r.left += view.md.style->font->getFontPainter ()->getStringWidth (nullptr, str);
r.right = r.left;
}
r = view.translateToGlobal (r);
cp.position = r.getTopLeft ();
cp.lineHeight = view.md.lineHeight * view.getFrame ()->getScaleFactor ();
cp.documentRect = view.translateToGlobal (view.getVisibleViewSize ());
return true;
}
//------------------------------------------------------------------------
void TextEditorView::IMETextInputClient::ime_setMarkedText (const std::u32string& string)
{
if (markedRange.length > 0)
view.deleteChars (markedRange.start, markedRange.length);
view.insertChars (view.md.editState.select_start, string.data (), string.size ());
markedRange.start = view.md.editState.select_start;
markedRange.length = string.size ();
view.md.editState.select_end =
static_cast<int> (view.md.editState.select_start + string.size ());
}
//------------------------------------------------------------------------
void TextEditorView::IMETextInputClient::ime_insertText (const std::u32string& string)
{
if (markedRange.length > 0)
view.deleteChars (markedRange.start, markedRange.length);
markedRange = {};
view.md.editState.select_start = view.md.editState.select_end = view.md.editState.cursor;
view.callSTB ([&] () {
stb_textedit_paste (&view, &view.md.editState, string.data (),
static_cast<int32_t> (string.size ()));
});
}
//------------------------------------------------------------------------
void TextEditorView::IMETextInputClient::ime_unmarkText ()
{
if (markedRange.length > 0)
view.deleteChars (markedRange.start, markedRange.length);
markedRange = {};
view.md.editState.select_start = view.md.editState.select_end = view.md.editState.cursor;
}
//------------------------------------------------------------------------
//------------------------------------------------------------------------
//------------------------------------------------------------------------
LineNumberView::LineNumberView (CView* textEditorView) : CView ({}), textEditorView (textEditorView)
{
textEditorView->registerViewListener (this);
}
//------------------------------------------------------------------------
LineNumberView::~LineNumberView () noexcept
{
if (textEditorView)
textEditorView->unregisterViewListener (this);
}
//------------------------------------------------------------------------
void LineNumberView::viewSizeChanged (CView* view, const CRect& oldSize)
{
vstgui_assert (view == textEditorView);
invalid ();
}
//------------------------------------------------------------------------
void LineNumberView::viewWillDelete (CView* view)
{
vstgui_assert (view == textEditorView);
textEditorView->unregisterViewListener (this);
textEditorView = nullptr;
}
//------------------------------------------------------------------------
void LineNumberView::drawRect (CDrawContext* context, const CRect& _dirtyRect)
{
CRect dirtyRect = _dirtyRect;
context->setFillColor (style->backColor);
context->setDrawMode (kAntiAliasing);
context->drawRect (dirtyRect, kDrawFilled);
CCoord x = getViewSize ().right;
CCoord y = getViewSize ().top - style->lineSpacing + textEditorView->getViewSize ().top;
for (auto index = 0u; index < numLines; ++index)
{
if (y > dirtyRect.bottom)
break;
y += lineHeight;
if (y < dirtyRect.top)
continue;
context->setFontColor (style->textColor);
context->setFont (style->font);
bool lineIsSelected = index >= selectedLines.start && index < selectedLines.end ();
context->setFontColor (lineIsSelected ? style->lineNumberTextSelectedColor
: style->lineNumberTextColor);
context->setFont (style->lineNumbersFont);
auto lineNoStr = toString (index + 1);
auto lineNoWidth = style->lineNumbersFont->getFontPainter ()->getStringWidth (
context->getPlatformDeviceContext (), lineNoStr.getPlatformString ());
context->drawString (lineNoStr.getPlatformString (),
{x - (lineNoWidth + style->lineNumberRightMargin), y});
}
auto cr = getViewSize ();
cr.left = (cr.right - 1.);
context->setFillColor (style->lineNumberLine);
context->drawRect (cr, kDrawFilled);
}
//------------------------------------------------------------------------
void LineNumberView::setStyle (const std::shared_ptr<ITextEditor::Style>& newStyle,
CCoord newLineHeight)
{
style = newStyle;
lineHeight = newLineHeight;
invalid ();
}
//------------------------------------------------------------------------
void LineNumberView::setNumLines (size_t numberOfLines)
{
numLines = numberOfLines;
invalid ();
}
//------------------------------------------------------------------------
void LineNumberView::setSelectedLines (Range range)
{
if (range != selectedLines)
{
selectedLines = range;
if (selectedLines.length == 0)
selectedLines.length = 1;
invalid ();
}
}
//------------------------------------------------------------------------
} // TextEditor
//------------------------------------------------------------------------
//------------------------------------------------------------------------
//------------------------------------------------------------------------
CView* createNewTextEditor (const CRect& r, ITextEditorController* controller)
{
vstgui_assert (controller != nullptr, "you need to call this with a controller");
if (!controller)
return nullptr;
return new TextEditor::TextEditorView (controller);
}
//------------------------------------------------------------------------
} // VSTGUI
#undef STB_TEXTEDIT_CHARTYPE
#undef STB_TEXTEDIT_POSITIONTYPE
#undef STB_TEXTEDIT_STRING
#undef STB_TEXTEDIT_KEYTYPE
#undef INCLUDE_STB_TEXTEDIT_H
#undef VIRTUAL_KEY_BIT
#undef STB_TEXTEDIT_K_SHIFT
#undef STB_TEXTEDIT_K_CONTROL
#undef STB_TEXTEDIT_K_ALT
// key-bindings
#undef STB_TEXTEDIT_K_LEFT
#undef STB_TEXTEDIT_K_RIGHT
#undef STB_TEXTEDIT_K_UP
#undef STB_TEXTEDIT_K_DOWN
#undef STB_TEXTEDIT_K_LINESTART
#undef STB_TEXTEDIT_K_LINEEND
#undef STB_TEXTEDIT_K_WORDLEFT
#undef STB_TEXTEDIT_K_WORDRIGHT
#undef STB_TEXTEDIT_K_TEXTSTART
#undef STB_TEXTEDIT_K_TEXTEND
#undef STB_TEXTEDIT_K_DELETE
#undef STB_TEXTEDIT_K_BACKSPACE
#undef STB_TEXTEDIT_K_UNDO
#undef STB_TEXTEDIT_K_REDO
#undef STB_TEXTEDIT_K_INSERT
#undef STB_TEXTEDIT_K_PGUP
#undef STB_TEXTEDIT_K_PGDOWN
// functions
#undef STB_TEXTEDIT_STRINGLEN
#undef STB_TEXTEDIT_LAYOUTROW
#undef STB_TEXTEDIT_GETWIDTH
#undef STB_TEXTEDIT_KEYTOTEXT
#undef STB_TEXTEDIT_GETCHAR
#undef STB_TEXTEDIT_NEWLINE
#undef STB_TEXTEDIT_DELETECHARS
#undef STB_TEXTEDIT_INSERTCHARS
#undef STB_TEXTEDIT_MOVEWORDLEFT
#undef STB_TEXTEDIT_MOVEWORDRIGHT
#undef STB_TEXTEDIT_GETWIDTH_NEWLINE
#undef STB_TEXTEDIT_CUSTOM_CREATEUNDO
#undef STB_TEXTEDIT_CUSTOM_UNDO
#undef STB_TEXTEDIT_CUSTOM_REDO
#undef STB_TEXTEDIT_UNDOSTATECOUNT
#undef STB_TEXTEDIT_UNDOCHARCOUNT