1049 lines
22 KiB
C++
1049 lines
22 KiB
C++
|
|
#include "widgets/textedit/textedit.h"
|
|
#include "widgets/scrollbar/scrollbar.h"
|
|
#include "core/utf8reader.h"
|
|
#include "core/colorf.h"
|
|
#include <algorithm>
|
|
|
|
#ifdef _MSC_VER
|
|
#pragma warning(disable: 4267) // warning C4267: 'initializing': conversion from 'size_t' to 'int', possible loss of data
|
|
#endif
|
|
|
|
TextEdit::TextEdit(Widget* parent) : Widget(parent)
|
|
{
|
|
SetStyleClass("textedit");
|
|
|
|
selectionBG = GetStyleColor("selection-color");
|
|
selectionFG = GetStyleColor("color");
|
|
|
|
timer = new Timer(this);
|
|
timer->FuncExpired = [this]() { OnTimerExpired(); };
|
|
|
|
scroll_timer = new Timer(this);
|
|
scroll_timer->FuncExpired = [this]() { OnScrollTimerExpired(); };
|
|
|
|
SetCursor(StandardCursor::ibeam);
|
|
|
|
CreateComponents();
|
|
}
|
|
|
|
TextEdit::~TextEdit()
|
|
{
|
|
}
|
|
|
|
bool TextEdit::IsReadOnly() const
|
|
{
|
|
return readonly;
|
|
}
|
|
|
|
bool TextEdit::IsLowercase() const
|
|
{
|
|
return lowercase;
|
|
}
|
|
|
|
bool TextEdit::IsUppercase() const
|
|
{
|
|
return uppercase;
|
|
}
|
|
|
|
int TextEdit::GetMaxLength() const
|
|
{
|
|
return max_length;
|
|
}
|
|
|
|
std::string TextEdit::GetLineText(int line) const
|
|
{
|
|
if (line >= 0 && line < (int)lines.size())
|
|
return lines[line].text;
|
|
else
|
|
return std::string();
|
|
}
|
|
|
|
std::string TextEdit::GetText() const
|
|
{
|
|
std::string::size_type size = 0;
|
|
for (size_t i = 0; i < lines.size(); i++)
|
|
size += lines[i].text.size();
|
|
size += lines.size() - 1;
|
|
|
|
std::string text;
|
|
text.reserve(size);
|
|
|
|
for (size_t i = 0; i < lines.size(); i++)
|
|
{
|
|
if (i > 0)
|
|
text.push_back('\n');
|
|
text += lines[i].text;
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
int TextEdit::GetLineCount() const
|
|
{
|
|
return (int)lines.size();
|
|
}
|
|
|
|
std::string TextEdit::GetSelection() const
|
|
{
|
|
std::string::size_type offset = ToOffset(selection_start);
|
|
int start = (int)std::min(offset, offset + selection_length);
|
|
return GetText().substr(start, abs(selection_length));
|
|
}
|
|
|
|
int TextEdit::GetSelectionStart() const
|
|
{
|
|
return (int)ToOffset(selection_start);
|
|
}
|
|
|
|
int TextEdit::GetSelectionLength() const
|
|
{
|
|
return selection_length;
|
|
}
|
|
|
|
int TextEdit::GetCursorPos() const
|
|
{
|
|
return (int)ToOffset(cursor_pos);
|
|
}
|
|
|
|
int TextEdit::GetCursorLineNumber() const
|
|
{
|
|
return cursor_pos.y;
|
|
}
|
|
|
|
void TextEdit::SelectAll()
|
|
{
|
|
std::string::size_type size = 0;
|
|
for (size_t i = 0; i < lines.size(); i++)
|
|
size += lines[i].text.size();
|
|
size += lines.size() - 1;
|
|
SetSelection(0, (int)size);
|
|
}
|
|
|
|
void TextEdit::SetReadOnly(bool enable)
|
|
{
|
|
if (readonly != enable)
|
|
{
|
|
readonly = enable;
|
|
Update();
|
|
}
|
|
}
|
|
|
|
void TextEdit::SetLowercase(bool enable)
|
|
{
|
|
if (lowercase != enable)
|
|
{
|
|
lowercase = enable;
|
|
Update();
|
|
}
|
|
}
|
|
|
|
void TextEdit::SetUppercase(bool enable)
|
|
{
|
|
if (uppercase != enable)
|
|
{
|
|
uppercase = enable;
|
|
Update();
|
|
}
|
|
}
|
|
|
|
void TextEdit::SetMaxLength(int length)
|
|
{
|
|
if (max_length != length)
|
|
{
|
|
max_length = length;
|
|
|
|
std::string::size_type size = 0;
|
|
for (size_t i = 0; i < lines.size(); i++)
|
|
size += lines[i].text.size();
|
|
size += lines.size() - 1;
|
|
|
|
if ((int)size > length)
|
|
{
|
|
if (FuncBeforeEditChanged)
|
|
FuncBeforeEditChanged();
|
|
SetSelection(length, (int)size - length);
|
|
DeleteSelectedText();
|
|
if (FuncAfterEditChanged)
|
|
FuncAfterEditChanged();
|
|
}
|
|
Update();
|
|
}
|
|
}
|
|
|
|
void TextEdit::SetText(const std::string& text)
|
|
{
|
|
lines.clear();
|
|
std::string::size_type start = 0;
|
|
std::string::size_type end = text.find('\n');
|
|
while (end != std::string::npos)
|
|
{
|
|
TextEdit::Line line{this};
|
|
line.text = text.substr(start, end - start);
|
|
lines.push_back(line);
|
|
start = end + 1;
|
|
end = text.find('\n', start);
|
|
}
|
|
TextEdit::Line line{this};
|
|
line.text = text.substr(start);
|
|
lines.push_back(line);
|
|
|
|
clip_start_offset = 0;
|
|
SetCursorPos(0);
|
|
ClearSelection();
|
|
Update();
|
|
}
|
|
|
|
void TextEdit::AddText(const std::string& text)
|
|
{
|
|
std::string::size_type start = 0;
|
|
std::string::size_type end = text.find('\n');
|
|
while (end != std::string::npos)
|
|
{
|
|
TextEdit::Line line{this};
|
|
line.text = text.substr(start, end - start);
|
|
lines.push_back(line);
|
|
start = end + 1;
|
|
end = text.find('\n', start);
|
|
}
|
|
TextEdit::Line line{this};
|
|
line.text = text.substr(start);
|
|
lines.push_back(line);
|
|
|
|
// clip_start_offset = 0;
|
|
// SetCursorPos(0);
|
|
ClearSelection();
|
|
Update();
|
|
}
|
|
|
|
void TextEdit::SetSelection(int pos, int length)
|
|
{
|
|
selection_start = ivec2(pos, 0);
|
|
selection_length = length;
|
|
Update();
|
|
}
|
|
|
|
void TextEdit::ClearSelection()
|
|
{
|
|
SetSelection(0, 0);
|
|
Update();
|
|
}
|
|
|
|
void TextEdit::DeleteSelectedText()
|
|
{
|
|
if (GetSelectionLength() == 0)
|
|
return;
|
|
|
|
std::string::size_type offset = ToOffset(selection_start);
|
|
int start = (int)std::min(offset, offset + selection_length);
|
|
int length = std::abs(selection_length);
|
|
|
|
ClearSelection();
|
|
std::string text = GetText();
|
|
SetText(text.substr(0, start) + text.substr(start + length));
|
|
SetCursorPos(start);
|
|
}
|
|
|
|
void TextEdit::SetCursorPos(int pos)
|
|
{
|
|
cursor_pos = FromOffset(pos);
|
|
Update();
|
|
}
|
|
|
|
void TextEdit::SetInputMask(const std::string& mask)
|
|
{
|
|
input_mask = mask;
|
|
}
|
|
|
|
void TextEdit::SetCursorDrawingEnabled(bool enable)
|
|
{
|
|
if (!readonly)
|
|
timer->Start(500);
|
|
}
|
|
|
|
void TextEdit::SetSelectAllOnFocusGain(bool enable)
|
|
{
|
|
select_all_on_focus_gain = enable;
|
|
}
|
|
|
|
void TextEdit::OnMouseMove(const Point& pos)
|
|
{
|
|
if (mouse_selecting && !ignore_mouse_events)
|
|
{
|
|
if (pos.x < 0.0 || pos.x > GetWidth())
|
|
{
|
|
if (pos.x < 0.0)
|
|
mouse_moves_left = true;
|
|
else
|
|
mouse_moves_left = false;
|
|
|
|
if (!readonly)
|
|
scroll_timer->Start(50, true);
|
|
}
|
|
else
|
|
{
|
|
scroll_timer->Stop();
|
|
cursor_pos = GetCharacterIndex(pos);
|
|
selection_length = ToOffset(cursor_pos) - ToOffset(selection_start);
|
|
Update();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool TextEdit::OnMouseDown(const Point& pos, InputKey key)
|
|
{
|
|
if (key == InputKey::LeftMouse)
|
|
{
|
|
SetPointerCapture();
|
|
mouse_selecting = true;
|
|
cursor_pos = GetCharacterIndex(pos);
|
|
selection_start = cursor_pos;
|
|
selection_length = 0;
|
|
|
|
Update();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TextEdit::OnMouseDoubleclick(const Point& pos, InputKey key)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool TextEdit::OnMouseUp(const Point& pos, InputKey key)
|
|
{
|
|
if (mouse_selecting && key == InputKey::LeftMouse)
|
|
{
|
|
if (ignore_mouse_events) // This prevents text selection from changing from what was set when focus was gained.
|
|
{
|
|
ReleasePointerCapture();
|
|
ignore_mouse_events = false;
|
|
mouse_selecting = false;
|
|
}
|
|
else
|
|
{
|
|
scroll_timer->Stop();
|
|
ReleasePointerCapture();
|
|
mouse_selecting = false;
|
|
ivec2 sel_end = GetCharacterIndex(pos);
|
|
selection_length = ToOffset(sel_end) - ToOffset(selection_start);
|
|
cursor_pos = sel_end;
|
|
SetFocus();
|
|
Update();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void TextEdit::OnKeyChar(std::string chars)
|
|
{
|
|
if (!chars.empty() && !(chars[0] >= 0 && chars[0] < 32))
|
|
{
|
|
if (FuncBeforeEditChanged)
|
|
FuncBeforeEditChanged();
|
|
|
|
DeleteSelectedText();
|
|
ClearSelection();
|
|
if (input_mask.empty())
|
|
{
|
|
// not in any special mode, just insert the string.
|
|
InsertText(cursor_pos, chars);
|
|
cursor_pos.x += chars.size();
|
|
}
|
|
else
|
|
{
|
|
if (InputMaskAcceptsInput(cursor_pos, chars))
|
|
{
|
|
InsertText(cursor_pos, chars);
|
|
cursor_pos.x += chars.size();
|
|
}
|
|
}
|
|
|
|
if (FuncAfterEditChanged)
|
|
FuncAfterEditChanged();
|
|
}
|
|
}
|
|
|
|
void TextEdit::OnKeyDown(InputKey key)
|
|
{
|
|
if (!readonly && key == InputKey::Enter)
|
|
{
|
|
if (FuncEnterPressed)
|
|
{
|
|
FuncEnterPressed();
|
|
}
|
|
else
|
|
{
|
|
ClearSelection();
|
|
InsertText(cursor_pos, "\n");
|
|
SetCursorPos(GetCursorPos() + 1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!readonly) // Do not flash cursor when readonly
|
|
{
|
|
cursor_blink_visible = true;
|
|
timer->Start(500); // don't blink cursor when moving or typing.
|
|
}
|
|
|
|
if (key == InputKey::Enter || key == InputKey::Escape || key == InputKey::Tab)
|
|
{
|
|
// Do not consume these.
|
|
return;
|
|
}
|
|
else if (key == InputKey::A && GetKeyState(InputKey::Ctrl))
|
|
{
|
|
// select all
|
|
SelectAll();
|
|
}
|
|
else if (key == InputKey::C && GetKeyState(InputKey::Ctrl))
|
|
{
|
|
std::string str = GetSelection();
|
|
SetClipboardText(str);
|
|
}
|
|
else if (readonly)
|
|
{
|
|
// Do not consume messages on read only component (only allow CTRL-A and CTRL-C)
|
|
return;
|
|
}
|
|
else if (key == InputKey::Up)
|
|
{
|
|
if (GetKeyState(InputKey::Shift) && selection_length == 0)
|
|
selection_start = cursor_pos;
|
|
|
|
if (cursor_pos.y > 0)
|
|
{
|
|
cursor_pos.y--;
|
|
cursor_pos.x = std::min(lines[cursor_pos.y].text.size(), (size_t)cursor_pos.x);
|
|
}
|
|
|
|
if (GetKeyState(InputKey::Shift))
|
|
{
|
|
selection_length = ToOffset(cursor_pos) - ToOffset(selection_start);
|
|
}
|
|
else
|
|
{
|
|
// Clear the selection if a cursor key is pressed but shift isn't down.
|
|
selection_start = ivec2(0, 0);
|
|
selection_length = 0;
|
|
}
|
|
MoveVerticalScroll();
|
|
Update();
|
|
undo_info.first_text_insert = true;
|
|
}
|
|
else if (key == InputKey::Down)
|
|
{
|
|
if (GetKeyState(InputKey::Shift) && selection_length == 0)
|
|
selection_start = cursor_pos;
|
|
|
|
if (cursor_pos.y < (int)lines.size() - 1)
|
|
{
|
|
cursor_pos.y++;
|
|
cursor_pos.x = std::min(lines[cursor_pos.y].text.size(), (size_t)cursor_pos.x);
|
|
}
|
|
|
|
if (GetKeyState(InputKey::Shift))
|
|
{
|
|
selection_length = ToOffset(cursor_pos) - ToOffset(selection_start);
|
|
}
|
|
else
|
|
{
|
|
// Clear the selection if a cursor key is pressed but shift isn't down.
|
|
selection_start = ivec2(0, 0);
|
|
selection_length = 0;
|
|
}
|
|
MoveVerticalScroll();
|
|
|
|
Update();
|
|
undo_info.first_text_insert = true;
|
|
}
|
|
else if (key == InputKey::Left)
|
|
{
|
|
Move(-1, GetKeyState(InputKey::Shift), GetKeyState(InputKey::Ctrl));
|
|
}
|
|
else if (key == InputKey::Right)
|
|
{
|
|
Move(1, GetKeyState(InputKey::Shift), GetKeyState(InputKey::Ctrl));
|
|
}
|
|
else if (key == InputKey::Backspace)
|
|
{
|
|
Backspace();
|
|
}
|
|
else if (key == InputKey::Delete)
|
|
{
|
|
Del();
|
|
}
|
|
else if (key == InputKey::Home)
|
|
{
|
|
if (GetKeyState(InputKey::Ctrl))
|
|
cursor_pos = ivec2(0, 0);
|
|
else
|
|
cursor_pos.x = 0;
|
|
if (GetKeyState(InputKey::Shift))
|
|
selection_length = ToOffset(cursor_pos) - ToOffset(selection_start);
|
|
else
|
|
ClearSelection();
|
|
Update();
|
|
MoveVerticalScroll();
|
|
}
|
|
else if (key == InputKey::End)
|
|
{
|
|
if (GetKeyState(InputKey::Ctrl))
|
|
cursor_pos = ivec2(lines.back().text.length(), lines.size() - 1);
|
|
else
|
|
cursor_pos.x = lines[cursor_pos.y].text.size();
|
|
|
|
if (GetKeyState(InputKey::Shift))
|
|
selection_length = ToOffset(cursor_pos) - ToOffset(selection_start);
|
|
else
|
|
ClearSelection();
|
|
Update();
|
|
}
|
|
else if (key == InputKey::X && GetKeyState(InputKey::Ctrl))
|
|
{
|
|
std::string str = GetSelection();
|
|
DeleteSelectedText();
|
|
SetClipboardText(str);
|
|
}
|
|
else if (key == InputKey::V && GetKeyState(InputKey::Ctrl))
|
|
{
|
|
std::string str = GetClipboardText();
|
|
std::string::const_iterator end_str = std::remove(str.begin(), str.end(), '\r');
|
|
str.resize(end_str - str.begin());
|
|
DeleteSelectedText();
|
|
|
|
if (input_mask.empty())
|
|
{
|
|
InsertText(cursor_pos, str);
|
|
SetCursorPos(GetCursorPos() + str.length());
|
|
}
|
|
else
|
|
{
|
|
if (InputMaskAcceptsInput(cursor_pos, str))
|
|
{
|
|
InsertText(cursor_pos, str);
|
|
SetCursorPos(GetCursorPos() + str.length());
|
|
}
|
|
}
|
|
MoveVerticalScroll();
|
|
}
|
|
else if (GetKeyState(InputKey::Ctrl) && key == InputKey::Z)
|
|
{
|
|
if (!readonly)
|
|
{
|
|
std::string tmp = undo_info.undo_text;
|
|
undo_info.undo_text = GetText();
|
|
SetText(tmp);
|
|
}
|
|
}
|
|
else if (key == InputKey::Shift)
|
|
{
|
|
if (selection_length == 0)
|
|
selection_start = cursor_pos;
|
|
}
|
|
|
|
if (FuncAfterEditChanged)
|
|
FuncAfterEditChanged();
|
|
}
|
|
|
|
void TextEdit::OnKeyUp(InputKey key)
|
|
{
|
|
}
|
|
|
|
void TextEdit::OnSetFocus()
|
|
{
|
|
if (!readonly)
|
|
timer->Start(500);
|
|
if (select_all_on_focus_gain)
|
|
SelectAll();
|
|
ignore_mouse_events = true;
|
|
cursor_pos.y = lines.size() - 1;
|
|
cursor_pos.x = lines[cursor_pos.y].text.length();
|
|
|
|
Update();
|
|
|
|
if (FuncFocusGained)
|
|
FuncFocusGained();
|
|
}
|
|
|
|
void TextEdit::OnLostFocus()
|
|
{
|
|
timer->Stop();
|
|
ClearSelection();
|
|
|
|
Update();
|
|
|
|
if (FuncFocusLost)
|
|
FuncFocusLost();
|
|
}
|
|
|
|
void TextEdit::CreateComponents()
|
|
{
|
|
vert_scrollbar = new Scrollbar(this);
|
|
vert_scrollbar->FuncScroll = [this]() { OnVerticalScroll(); };
|
|
vert_scrollbar->SetVisible(false);
|
|
vert_scrollbar->SetVertical();
|
|
}
|
|
|
|
void TextEdit::OnVerticalScroll()
|
|
{
|
|
}
|
|
|
|
void TextEdit::UpdateVerticalScroll()
|
|
{
|
|
Rect rect(
|
|
GetWidth() - 16.0/*vert_scrollbar->GetWidth()*/,
|
|
0.0,
|
|
16.0/*vert_scrollbar->GetWidth()*/,
|
|
GetHeight());
|
|
|
|
vert_scrollbar->SetFrameGeometry(rect);
|
|
|
|
double total_height = GetTotalLineHeight();
|
|
double height_per_line = std::max(1.0, total_height / std::max(1.0, (double)lines.size()));
|
|
bool visible = total_height > GetHeight();
|
|
vert_scrollbar->SetRanges((int)std::round(GetHeight() / height_per_line), (int)std::round(total_height / height_per_line));
|
|
vert_scrollbar->SetLineStep(1);
|
|
vert_scrollbar->SetVisible(visible);
|
|
|
|
if (visible == false)
|
|
vert_scrollbar->SetPosition(0);
|
|
}
|
|
|
|
void TextEdit::MoveVerticalScroll()
|
|
{
|
|
double total_height = GetTotalLineHeight();
|
|
double height_per_line = std::max(1.0, total_height / std::max((size_t)1, lines.size()));
|
|
int lines_fit = (int)(GetHeight() / height_per_line);
|
|
if (cursor_pos.y >= vert_scrollbar->GetPosition() + lines_fit)
|
|
{
|
|
vert_scrollbar->SetPosition(cursor_pos.y - lines_fit + 1);
|
|
}
|
|
else if (cursor_pos.y < vert_scrollbar->GetPosition())
|
|
{
|
|
vert_scrollbar->SetPosition(cursor_pos.y);
|
|
}
|
|
}
|
|
|
|
double TextEdit::GetTotalLineHeight()
|
|
{
|
|
double total = 0;
|
|
for (std::vector<Line>::const_iterator iter = lines.begin(); iter != lines.end(); iter++)
|
|
{
|
|
total += iter->layout.GetSize().height;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
void TextEdit::Move(int steps, bool shift, bool ctrl)
|
|
{
|
|
if (shift && selection_length == 0)
|
|
selection_start = cursor_pos;
|
|
|
|
// Jump over words if control is pressed.
|
|
if (ctrl)
|
|
{
|
|
if (steps < 0 && cursor_pos.x == 0 && cursor_pos.y > 0)
|
|
{
|
|
cursor_pos.x = (int)lines[cursor_pos.y - 1].text.size();
|
|
cursor_pos.y--;
|
|
}
|
|
else if (steps > 0 && cursor_pos.x == (int)lines[cursor_pos.y].text.size() && cursor_pos.y + 1 < (int)lines.size())
|
|
{
|
|
cursor_pos.x = 0;
|
|
cursor_pos.y++;
|
|
}
|
|
|
|
ivec2 new_pos;
|
|
if (steps < 0)
|
|
new_pos = FindPreviousBreakCharacter(cursor_pos);
|
|
else
|
|
new_pos = FindNextBreakCharacter(cursor_pos);
|
|
|
|
cursor_pos = new_pos;
|
|
}
|
|
else if (steps < 0 && cursor_pos.x == 0 && cursor_pos.y > 0)
|
|
{
|
|
cursor_pos.x = (int)lines[cursor_pos.y - 1].text.size();
|
|
cursor_pos.y--;
|
|
}
|
|
else if (steps > 0 && cursor_pos.x == (int)lines[cursor_pos.y].text.size() && cursor_pos.y + 1 < (int)lines.size())
|
|
{
|
|
cursor_pos.x = 0;
|
|
cursor_pos.y++;
|
|
}
|
|
else
|
|
{
|
|
UTF8Reader utf8_reader(lines[cursor_pos.y].text.data(), lines[cursor_pos.y].text.length());
|
|
utf8_reader.set_position(cursor_pos.x);
|
|
if (steps > 0)
|
|
{
|
|
for (int i = 0; i < steps; i++)
|
|
utf8_reader.next();
|
|
}
|
|
else if (steps < 0)
|
|
{
|
|
for (int i = 0; i < -steps; i++)
|
|
utf8_reader.prev();
|
|
}
|
|
|
|
cursor_pos.x = (int)utf8_reader.position();
|
|
}
|
|
|
|
if (shift)
|
|
{
|
|
selection_length = (int)ToOffset(cursor_pos) - (int)ToOffset(selection_start);
|
|
}
|
|
else
|
|
{
|
|
// Clear the selection if a cursor key is pressed but shift isn't down.
|
|
selection_start = ivec2(0, 0);
|
|
selection_length = 0;
|
|
}
|
|
|
|
|
|
MoveVerticalScroll();
|
|
Update();
|
|
|
|
undo_info.first_text_insert = true;
|
|
}
|
|
|
|
std::string TextEdit::break_characters = " ::;,.-";
|
|
|
|
TextEdit::ivec2 TextEdit::FindNextBreakCharacter(ivec2 search_start)
|
|
{
|
|
search_start.x++;
|
|
if (search_start.x >= int(lines[search_start.y].text.size()) - 1)
|
|
return ivec2(lines[search_start.y].text.size(), search_start.y);
|
|
|
|
size_t pos = lines[search_start.y].text.find_first_of(break_characters, search_start.x);
|
|
if (pos == std::string::npos)
|
|
return ivec2(lines[search_start.y].text.size(), search_start.y);
|
|
return ivec2((int)pos, search_start.y);
|
|
}
|
|
|
|
TextEdit::ivec2 TextEdit::FindPreviousBreakCharacter(ivec2 search_start)
|
|
{
|
|
search_start.x--;
|
|
if (search_start.x <= 0)
|
|
return ivec2(0, search_start.y);
|
|
size_t pos = lines[search_start.y].text.find_last_of(break_characters, search_start.x);
|
|
if (pos == std::string::npos)
|
|
return ivec2(0, search_start.y);
|
|
return ivec2((int)pos, search_start.y);
|
|
}
|
|
|
|
void TextEdit::InsertText(ivec2 pos, const std::string& str)
|
|
{
|
|
undo_info.first_erase = false;
|
|
if (undo_info.first_text_insert)
|
|
{
|
|
undo_info.undo_text = GetText();
|
|
undo_info.first_text_insert = false;
|
|
}
|
|
|
|
// checking if insert exceeds max length
|
|
if (ToOffset(ivec2((int)lines[lines.size() - 1].text.size(), (int)lines.size() - 1)) + str.length() > (size_t)max_length)
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::string::size_type start = 0;
|
|
while (true)
|
|
{
|
|
std::string::size_type next_newline = str.find('\n', start);
|
|
|
|
lines[pos.y].text.insert(pos.x, str.substr(start, next_newline - start));
|
|
lines[pos.y].invalidated = true;
|
|
|
|
if (next_newline == std::string::npos)
|
|
break;
|
|
|
|
pos.x += next_newline - start;
|
|
|
|
Line line{this};
|
|
line.text = lines[pos.y].text.substr(pos.x);
|
|
lines.insert(lines.begin() + pos.y + 1, line);
|
|
lines[pos.y].text = lines[pos.y].text.substr(0, pos.x);
|
|
lines[pos.y].invalidated = true;
|
|
pos = ivec2(0, pos.y + 1);
|
|
|
|
start = next_newline + 1;
|
|
}
|
|
|
|
MoveVerticalScroll();
|
|
|
|
Update();
|
|
}
|
|
|
|
void TextEdit::Backspace()
|
|
{
|
|
if (undo_info.first_erase)
|
|
{
|
|
undo_info.first_erase = false;
|
|
undo_info.undo_text = GetText();
|
|
}
|
|
|
|
if (GetSelectionLength() != 0)
|
|
{
|
|
DeleteSelectedText();
|
|
ClearSelection();
|
|
Update();
|
|
}
|
|
else
|
|
{
|
|
if (cursor_pos.x > 0)
|
|
{
|
|
UTF8Reader utf8_reader(lines[cursor_pos.y].text.data(), lines[cursor_pos.y].text.length());
|
|
utf8_reader.set_position(cursor_pos.x);
|
|
utf8_reader.prev();
|
|
int length = utf8_reader.char_length();
|
|
lines[cursor_pos.y].text.erase(cursor_pos.x - length, length);
|
|
lines[cursor_pos.y].invalidated = true;
|
|
cursor_pos.x -= length;
|
|
Update();
|
|
}
|
|
else if (cursor_pos.y > 0)
|
|
{
|
|
selection_start = ivec2(lines[cursor_pos.y - 1].text.length(), cursor_pos.y - 1);
|
|
selection_length = 1;
|
|
DeleteSelectedText();
|
|
}
|
|
}
|
|
MoveVerticalScroll();
|
|
}
|
|
|
|
void TextEdit::Del()
|
|
{
|
|
if (undo_info.first_erase)
|
|
{
|
|
undo_info.first_erase = false;
|
|
undo_info.undo_text = GetText();
|
|
}
|
|
|
|
if (GetSelectionLength() != 0)
|
|
{
|
|
DeleteSelectedText();
|
|
ClearSelection();
|
|
Update();
|
|
}
|
|
else
|
|
{
|
|
if (cursor_pos.x < (int)lines[cursor_pos.y].text.size())
|
|
{
|
|
UTF8Reader utf8_reader(lines[cursor_pos.y].text.data(), lines[cursor_pos.y].text.length());
|
|
utf8_reader.set_position(cursor_pos.x);
|
|
int length = utf8_reader.char_length();
|
|
lines[cursor_pos.y].text.erase(cursor_pos.x, length);
|
|
lines[cursor_pos.y].invalidated = true;
|
|
Update();
|
|
}
|
|
else if (cursor_pos.y + 1 < (int)lines.size())
|
|
{
|
|
selection_start = ivec2((int)lines[cursor_pos.y].text.length(), cursor_pos.y);
|
|
selection_length = 1;
|
|
DeleteSelectedText();
|
|
}
|
|
}
|
|
MoveVerticalScroll();
|
|
}
|
|
|
|
void TextEdit::OnTimerExpired()
|
|
{
|
|
if (IsVisible() == false)
|
|
{
|
|
timer->Stop();
|
|
return;
|
|
}
|
|
|
|
if (cursor_blink_visible)
|
|
timer->Start(500);
|
|
else
|
|
timer->Start(500);
|
|
|
|
cursor_blink_visible = !cursor_blink_visible;
|
|
Update();
|
|
}
|
|
|
|
void TextEdit::OnGeometryChanged()
|
|
{
|
|
Canvas* canvas = GetCanvas();
|
|
|
|
for (auto& line : lines)
|
|
{
|
|
line.invalidated = true;
|
|
}
|
|
|
|
vertical_text_align = canvas->verticalTextAlign();
|
|
|
|
clip_start_offset = 0;
|
|
UpdateVerticalScroll();
|
|
}
|
|
|
|
void TextEdit::OnScrollTimerExpired()
|
|
{
|
|
if (mouse_moves_left)
|
|
Move(-1, false, false);
|
|
else
|
|
Move(1, false, false);
|
|
}
|
|
|
|
void TextEdit::OnEnableChanged()
|
|
{
|
|
bool enabled = IsEnabled();
|
|
if (!enabled)
|
|
{
|
|
cursor_blink_visible = false;
|
|
timer->Stop();
|
|
}
|
|
Update();
|
|
}
|
|
|
|
bool TextEdit::InputMaskAcceptsInput(ivec2 cursor_pos, const std::string& str)
|
|
{
|
|
return str.find_first_not_of(input_mask) == std::string::npos;
|
|
}
|
|
|
|
std::string::size_type TextEdit::ToOffset(ivec2 pos) const
|
|
{
|
|
if (pos.y < (int)lines.size())
|
|
{
|
|
std::string::size_type offset = 0;
|
|
for (int line = 0; line < pos.y; line++)
|
|
{
|
|
offset += lines[line].text.size() + 1;
|
|
}
|
|
return offset + std::min((size_t)pos.x, lines[pos.y].text.size());
|
|
}
|
|
else
|
|
{
|
|
std::string::size_type offset = 0;
|
|
for (size_t line = 0; line < lines.size(); line++)
|
|
{
|
|
offset += lines[line].text.size() + 1;
|
|
}
|
|
return offset - 1;
|
|
}
|
|
}
|
|
|
|
TextEdit::ivec2 TextEdit::FromOffset(std::string::size_type offset) const
|
|
{
|
|
int line_offset = 0;
|
|
for (int line = 0; line < (int)lines.size(); line++)
|
|
{
|
|
if (offset <= line_offset + lines[line].text.size())
|
|
{
|
|
return ivec2(offset - line_offset, line);
|
|
}
|
|
line_offset += lines[line].text.size() + 1;
|
|
}
|
|
return ivec2(lines.back().text.size(), lines.size() - 1);
|
|
}
|
|
|
|
double TextEdit::GetTotalHeight()
|
|
{
|
|
Canvas* canvas = GetCanvas();
|
|
LayoutLines(canvas);
|
|
if (!lines.empty())
|
|
{
|
|
return lines.back().box.bottom();
|
|
}
|
|
else
|
|
{
|
|
return GetHeight();
|
|
}
|
|
}
|
|
|
|
void TextEdit::LayoutLines(Canvas* canvas)
|
|
{
|
|
ivec2 sel_start;
|
|
ivec2 sel_end;
|
|
if (selection_length > 0)
|
|
{
|
|
sel_start = selection_start;
|
|
sel_end = FromOffset(ToOffset(selection_start) + selection_length);
|
|
}
|
|
else if (selection_length < 0)
|
|
{
|
|
sel_start = FromOffset(ToOffset(selection_start) + selection_length);
|
|
sel_end = selection_start;
|
|
}
|
|
|
|
Colorf textColor = GetStyleColor("color");
|
|
Point draw_pos;
|
|
for (size_t i = (size_t)vert_scrollbar->GetPosition(); i < lines.size(); i++)
|
|
{
|
|
Line& line = lines[i];
|
|
if (line.invalidated)
|
|
{
|
|
line.layout.Clear();
|
|
if (!line.text.empty())
|
|
line.layout.AddText(line.text, font, textColor);
|
|
else
|
|
line.layout.AddText(" ", font, textColor); // Draw one space character to get the correct height
|
|
line.layout.Layout(canvas, GetWidth());
|
|
line.box = Rect(draw_pos, line.layout.GetSize());
|
|
line.invalidated = false;
|
|
}
|
|
|
|
if (sel_start != sel_end && sel_start.y <= (int)i && sel_end.y >= (int)i)
|
|
{
|
|
line.layout.SetSelectionRange(sel_start.y < (int)i ? 0 : sel_start.x, sel_end.y > (int)i ? line.text.size() : sel_end.x);
|
|
}
|
|
else
|
|
{
|
|
line.layout.SetSelectionRange(0, 0);
|
|
}
|
|
|
|
line.layout.HideCursor();
|
|
if (HasFocus())
|
|
{
|
|
if (cursor_blink_visible && cursor_pos.y == (int)i)
|
|
{
|
|
line.layout.SetCursorPos(cursor_pos.x);
|
|
line.layout.SetCursorColor(textColor);
|
|
line.layout.ShowCursor();
|
|
}
|
|
}
|
|
|
|
line.box.x = draw_pos.x;
|
|
line.box.y = draw_pos.y;
|
|
line.layout.SetPosition(line.box.topLeft());
|
|
|
|
draw_pos = line.box.bottomLeft();
|
|
}
|
|
UpdateVerticalScroll();
|
|
}
|
|
|
|
void TextEdit::OnPaint(Canvas* canvas)
|
|
{
|
|
LayoutLines(canvas);
|
|
for (size_t i = (size_t)vert_scrollbar->GetPosition(); i < lines.size(); i++)
|
|
lines[i].layout.DrawLayout(canvas);
|
|
}
|
|
|
|
TextEdit::ivec2 TextEdit::GetCharacterIndex(Point mouse_wincoords)
|
|
{
|
|
Canvas* canvas = GetCanvas();
|
|
for (size_t i = 0; i < lines.size(); i++)
|
|
{
|
|
Line& line = lines[i];
|
|
if (line.box.top() <= mouse_wincoords.y && line.box.bottom() > mouse_wincoords.y)
|
|
{
|
|
SpanLayout::HitTestResult result = line.layout.HitTest(canvas, mouse_wincoords);
|
|
switch (result.type)
|
|
{
|
|
case SpanLayout::HitTestResult::inside:
|
|
return ivec2(clamp(result.offset, (size_t)0, line.text.size()), i);
|
|
case SpanLayout::HitTestResult::outside_left:
|
|
return ivec2(0, i);
|
|
default:
|
|
case SpanLayout::HitTestResult::outside_right:
|
|
return ivec2(line.text.size(), i);
|
|
}
|
|
}
|
|
}
|
|
|
|
return ivec2(lines.back().text.size(), lines.size() - 1);
|
|
}
|