3503 lines
108 KiB
C++
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
|