aseprite/src/ui/entry.cpp

817 lines
18 KiB
C++
Raw Normal View History

// Aseprite UI Library
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
2007-09-19 07:57:02 +08:00
#ifdef HAVE_CONFIG_H
2009-07-13 04:29:16 +08:00
#include "config.h"
#endif
2009-07-13 04:29:16 +08:00
2013-03-31 00:11:49 +08:00
#include "ui/entry.h"
2007-09-19 07:57:02 +08:00
#include "base/bind.h"
#include "base/string.h"
#include "clip/clip.h"
#include "she/draw_text.h"
#include "she/font.h"
#include "she/system.h"
2012-06-18 09:49:58 +08:00
#include "ui/manager.h"
#include "ui/menu.h"
2012-06-18 09:49:58 +08:00
#include "ui/message.h"
2015-12-04 08:50:05 +08:00
#include "ui/size_hint_event.h"
2012-06-18 09:49:58 +08:00
#include "ui/system.h"
#include "ui/theme.h"
#include "ui/widget.h"
2007-09-19 07:57:02 +08:00
#include <cctype>
#include <cstdarg>
#include <cstdio>
2007-09-19 07:57:02 +08:00
namespace ui {
Entry::Entry(const std::size_t maxsize, const char* format, ...)
: Widget(kEntryWidget)
, m_timer(500, this)
, m_maxsize(maxsize)
, m_caret(0)
, m_scroll(0)
, m_select(0)
, m_hidden(false)
, m_state(false)
, m_readonly(false)
, m_password(false)
, m_recent_focused(false)
, m_lock_selection(false)
, m_translate_dead_keys(true)
2007-09-19 07:57:02 +08:00
{
enableFlags(CTRL_RIGHT_CLICK);
2007-09-19 07:57:02 +08:00
// formatted string
char buf[4096];
2007-09-19 07:57:02 +08:00
if (format) {
va_list ap;
va_start(ap, format);
vsprintf(buf, format, ap);
va_end(ap);
2007-09-19 07:57:02 +08:00
}
// empty string
2007-09-19 07:57:02 +08:00
else {
buf[0] = 0;
2007-09-19 07:57:02 +08:00
}
// TODO support for text alignment and multi-line
// widget->align = LEFT | MIDDLE;
2010-12-09 01:28:13 +08:00
setText(buf);
2007-09-19 07:57:02 +08:00
setFocusStop(true);
initTheme();
2007-09-19 07:57:02 +08:00
}
2010-12-09 01:28:13 +08:00
Entry::~Entry()
2007-09-19 07:57:02 +08:00
{
}
void Entry::setMaxTextLength(const std::size_t maxsize)
{
m_maxsize = maxsize;
}
2010-12-09 01:28:13 +08:00
bool Entry::isReadOnly() const
2007-09-19 07:57:02 +08:00
{
2010-12-09 01:28:13 +08:00
return m_readonly;
2007-09-19 07:57:02 +08:00
}
2010-12-09 01:28:13 +08:00
bool Entry::isPassword() const
2007-09-19 07:57:02 +08:00
{
2010-12-09 01:28:13 +08:00
return m_password;
2007-09-19 07:57:02 +08:00
}
2010-12-09 01:28:13 +08:00
void Entry::setReadOnly(bool state)
2007-09-19 07:57:02 +08:00
{
2010-12-09 01:28:13 +08:00
m_readonly = state;
2007-09-19 07:57:02 +08:00
}
2010-12-09 01:28:13 +08:00
void Entry::setPassword(bool state)
2007-09-19 07:57:02 +08:00
{
2010-12-09 01:28:13 +08:00
m_password = state;
2007-09-19 07:57:02 +08:00
}
2010-12-09 01:28:13 +08:00
void Entry::showCaret()
2007-09-19 07:57:02 +08:00
{
2010-12-09 01:28:13 +08:00
m_hidden = false;
invalidate();
2010-12-09 01:28:13 +08:00
}
2007-09-19 07:57:02 +08:00
2010-12-09 01:28:13 +08:00
void Entry::hideCaret()
{
m_hidden = true;
invalidate();
2007-09-19 07:57:02 +08:00
}
2010-12-09 01:28:13 +08:00
void Entry::setCaretPos(int pos)
2007-09-19 07:57:02 +08:00
{
auto utf8_begin = base::utf8_const_iterator(text().begin());
auto utf8_end = base::utf8_const_iterator(text().end());
int textlen = base::utf8_length(text());
2007-09-19 07:57:02 +08:00
m_caret = MID(0, pos, textlen);
2007-09-19 07:57:02 +08:00
// Backward scroll
if (m_scroll > m_caret)
2010-12-09 01:28:13 +08:00
m_scroll = m_caret;
2007-09-19 07:57:02 +08:00
// Forward scroll
--m_scroll;
int c;
while (true) {
c = ++m_scroll;
auto utf8_it = utf8_begin + MID(0, c, textlen);
int x = bounds().x + border().left() - font()->charWidth(' '); // Space for the caret
for (; utf8_it != utf8_end; ++c, ++utf8_it) {
int ch = *utf8_it;
x += font()->charWidth(ch);
if (x >= bounds().x2()-border().right())
2007-09-19 07:57:02 +08:00
break;
}
if (m_caret < c || utf8_it == utf8_end)
break;
}
2007-09-19 07:57:02 +08:00
m_timer.start();
2010-12-09 01:28:13 +08:00
m_state = true;
2007-09-19 07:57:02 +08:00
invalidate();
2007-09-19 07:57:02 +08:00
}
2010-12-09 01:28:13 +08:00
void Entry::selectText(int from, int to)
2007-09-19 07:57:02 +08:00
{
int end = base::utf8_length(text());
2007-09-19 07:57:02 +08:00
2010-12-09 01:28:13 +08:00
m_select = from;
setCaretPos(from); // to move scroll
setCaretPos((to >= 0)? to: end+to+1);
2007-09-19 07:57:02 +08:00
invalidate();
2007-09-19 07:57:02 +08:00
}
void Entry::selectAllText()
{
selectText(0, -1);
}
2010-12-09 01:28:13 +08:00
void Entry::deselectText()
2007-09-19 07:57:02 +08:00
{
2010-12-09 01:28:13 +08:00
m_select = -1;
invalidate();
2007-09-19 07:57:02 +08:00
}
2013-03-30 03:20:32 +08:00
void Entry::setSuffix(const std::string& suffix)
{
m_suffix = suffix;
invalidate();
}
void Entry::setTranslateDeadKeys(bool state)
{
m_translate_dead_keys = state;
}
2010-12-09 01:28:13 +08:00
void Entry::getEntryThemeInfo(int* scroll, int* caret, int* state,
int* selbeg, int* selend)
2007-09-19 07:57:02 +08:00
{
2010-12-09 01:28:13 +08:00
if (scroll) *scroll = m_scroll;
if (caret) *caret = m_caret;
if (state) *state = !m_hidden && m_state;
if ((m_select >= 0) &&
(m_caret != m_select)) {
*selbeg = MIN(m_caret, m_select);
*selend = MAX(m_caret, m_select)-1;
2007-09-19 07:57:02 +08:00
}
else {
*selbeg = -1;
*selend = -1;
}
}
gfx::Rect Entry::getEntryTextBounds() const
{
return onGetEntryTextBounds();
}
bool Entry::onProcessMessage(Message* msg)
2007-09-19 07:57:02 +08:00
{
switch (msg->type()) {
2007-09-19 07:57:02 +08:00
case kTimerMessage:
if (hasFocus() && static_cast<TimerMessage*>(msg)->timer() == &m_timer) {
// Blinking caret
m_state = m_state ? false: true;
invalidate();
2007-09-19 07:57:02 +08:00
}
break;
case kFocusEnterMessage:
m_timer.start();
2010-12-09 01:28:13 +08:00
m_state = true;
invalidate();
2007-09-19 07:57:02 +08:00
if (m_lock_selection) {
m_lock_selection = false;
}
else {
selectAllText();
m_recent_focused = true;
}
// Start processing dead keys
if (m_translate_dead_keys)
she::instance()->setTranslateDeadKeys(true);
2007-09-19 07:57:02 +08:00
break;
case kFocusLeaveMessage:
invalidate();
2007-09-19 07:57:02 +08:00
m_timer.stop();
if (!m_lock_selection)
deselectText();
2010-12-09 01:28:13 +08:00
m_recent_focused = false;
// Stop processing dead keys
if (m_translate_dead_keys)
she::instance()->setTranslateDeadKeys(false);
2007-09-19 07:57:02 +08:00
break;
case kKeyDownMessage:
if (hasFocus() && !isReadOnly()) {
// Command to execute
EntryCmd cmd = EntryCmd::NoOp;
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
KeyScancode scancode = keymsg->scancode();
switch (scancode) {
case kKeyLeft:
2015-05-11 23:12:13 +08:00
if (msg->ctrlPressed() || msg->altPressed())
cmd = EntryCmd::BackwardWord;
2015-05-11 23:12:13 +08:00
else if (msg->cmdPressed())
cmd = EntryCmd::BeginningOfLine;
else
cmd = EntryCmd::BackwardChar;
break;
case kKeyRight:
2015-05-11 23:12:13 +08:00
if (msg->ctrlPressed() || msg->altPressed())
cmd = EntryCmd::ForwardWord;
2015-05-11 23:12:13 +08:00
else if (msg->cmdPressed())
cmd = EntryCmd::EndOfLine;
else
cmd = EntryCmd::ForwardChar;
break;
case kKeyHome:
cmd = EntryCmd::BeginningOfLine;
break;
case kKeyEnd:
cmd = EntryCmd::EndOfLine;
break;
case kKeyDel:
if (msg->shiftPressed())
cmd = EntryCmd::Cut;
else if (msg->ctrlPressed())
cmd = EntryCmd::DeleteForwardToEndOfLine;
else
cmd = EntryCmd::DeleteForward;
break;
case kKeyInsert:
if (msg->shiftPressed())
cmd = EntryCmd::Paste;
else if (msg->ctrlPressed())
cmd = EntryCmd::Copy;
break;
case kKeyBackspace:
if (msg->ctrlPressed())
cmd = EntryCmd::DeleteBackwardWord;
else
cmd = EntryCmd::DeleteBackward;
break;
default:
// Map common macOS/Windows shortcuts for Cut/Copy/Paste/Select all
#if defined __APPLE__
if (msg->onlyCmdPressed())
#else
if (msg->onlyCtrlPressed())
#endif
{
switch (scancode) {
case kKeyX: cmd = EntryCmd::Cut; break;
case kKeyC: cmd = EntryCmd::Copy; break;
case kKeyV: cmd = EntryCmd::Paste; break;
case kKeyA: cmd = EntryCmd::SelectAll; break;
}
}
break;
}
if (cmd == EntryCmd::NoOp) {
if (keymsg->unicodeChar() >= 32) {
executeCmd(EntryCmd::InsertChar, keymsg->unicodeChar(),
(msg->shiftPressed()) ? true: false);
// Select dead-key
if (keymsg->isDeadKey()) {
if (base::from_utf8(text()).size() < m_maxsize)
selectText(m_caret-1, m_caret);
}
return true;
}
// Consume all key down of modifiers only, e.g. so the user
// can press first "Ctrl" key, and then "Ctrl+C"
// combination.
else if (keymsg->scancode() >= kKeyFirstModifierScancode) {
return true;
}
else {
break; // Propagate to manager
}
}
executeCmd(cmd, keymsg->unicodeChar(),
(msg->shiftPressed()) ? true: false);
return true;
2007-09-19 07:57:02 +08:00
}
break;
case kMouseDownMessage:
captureMouse();
2007-09-19 07:57:02 +08:00
case kMouseMoveMessage:
if (hasCapture()) {
gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position();
auto utf8_begin = base::utf8_const_iterator(text().begin());
auto utf8_end = base::utf8_const_iterator(text().end());
int textlen = base::utf8_length(text());
int c, x;
bool move = true;
bool is_dirty = false;
// Backward scroll
if (mousePos.x < bounds().x) {
if (m_scroll > 0) {
m_caret = --m_scroll;
move = false;
is_dirty = true;
invalidate();
}
}
// Forward scroll
else if (mousePos.x >= bounds().x2()) {
if (m_scroll < textlen - getAvailableTextLength()) {
++m_scroll;
x = bounds().x + border().left();
auto utf8_it = utf8_begin + MID(0, m_scroll, textlen);
for (c=m_scroll; utf8_it != utf8_end; ++c, ++utf8_it) {
int ch = (c < textlen ? *utf8_it: ' ');
x += font()->charWidth(ch);
if (x > bounds().x2()-border().right()) {
c--;
break;
}
}
m_caret = MID(0, c, textlen);
move = false;
is_dirty = true;
invalidate();
}
}
c = getCaretFromMouse(static_cast<MouseMessage*>(msg));
if (static_cast<MouseMessage*>(msg)->left() ||
(move && !isPosInSelection(c))) {
// Move caret
if (move) {
if (m_caret != c) {
m_caret = c;
is_dirty = true;
invalidate();
}
}
// Move selection
if (m_recent_focused) {
m_recent_focused = false;
m_select = m_caret;
}
else if (msg->type() == kMouseDownMessage)
m_select = m_caret;
}
// Show the caret
if (is_dirty) {
m_timer.start();
m_state = true;
}
return true;
2007-09-19 07:57:02 +08:00
}
break;
case kMouseUpMessage:
if (hasCapture()) {
releaseMouse();
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
if (mouseMsg->right()) {
// This flag is disabled in kFocusEnterMessage message handler.
m_lock_selection = true;
showEditPopupMenu(mouseMsg->position());
requestFocus();
}
}
return true;
2007-09-19 07:57:02 +08:00
case kDoubleClickMessage:
2010-12-09 01:28:13 +08:00
forwardWord();
m_select = m_caret;
backwardWord();
invalidate();
return true;
2007-09-19 07:57:02 +08:00
case kMouseEnterMessage:
case kMouseLeaveMessage:
// TODO theme stuff
if (isEnabled())
invalidate();
2007-09-19 07:57:02 +08:00
break;
}
2010-12-09 01:28:13 +08:00
return Widget::onProcessMessage(msg);
2007-09-19 07:57:02 +08:00
}
2015-12-04 08:50:05 +08:00
void Entry::onSizeHint(SizeHintEvent& ev)
2007-09-19 07:57:02 +08:00
{
2010-12-09 01:28:13 +08:00
int w =
+ font()->charWidth('w') * MIN(m_maxsize, 6)
+ 2*guiscale()
+ border().width();
w = MIN(w, ui::display_w()/2);
2007-09-19 07:57:02 +08:00
int h =
+ font()->height()
+ border().height();
2007-09-19 07:57:02 +08:00
2015-12-04 08:50:05 +08:00
ev.setSizeHint(w, h);
2010-12-09 01:28:13 +08:00
}
void Entry::onPaint(PaintEvent& ev)
{
theme()->paintEntry(ev);
}
void Entry::onSetText()
{
Widget::onSetText();
int textlen = textLength();
if (m_caret >= 0 && m_caret > textlen)
m_caret = textlen;
}
void Entry::onChange()
2010-12-09 01:28:13 +08:00
{
Change();
2007-09-19 07:57:02 +08:00
}
gfx::Rect Entry::onGetEntryTextBounds() const
{
gfx::Rect bounds = clientBounds();
bounds.x += border().left();
bounds.y += bounds.h/2 - textHeight()/2;
bounds.w -= border().width();
bounds.h = textHeight();
return bounds;
}
int Entry::getCaretFromMouse(MouseMessage* mousemsg)
2007-09-19 07:57:02 +08:00
{
base::utf8_const_iterator utf8_begin = base::utf8_const_iterator(text().begin());
base::utf8_const_iterator utf8_end = base::utf8_const_iterator(text().end());
int caret = m_caret;
int textlen = base::utf8_length(text());
gfx::Rect bounds = getEntryTextBounds().offset(this->bounds().origin());
2007-09-19 07:57:02 +08:00
int mx = mousemsg->position().x;
mx = MID(bounds.x, mx, bounds.x2()-1);
2007-09-19 07:57:02 +08:00
int x = bounds.x;
auto utf8_it = utf8_begin + MID(0, m_scroll, textlen);
int c = m_scroll;
for (; utf8_it != utf8_end; ++c, ++utf8_it) {
int w = font()->charWidth(*utf8_it);
if (x+w >= bounds.x2()-border().right())
2007-09-19 07:57:02 +08:00
break;
if ((mx >= x) && (mx < x+w)) {
2010-12-09 01:28:13 +08:00
caret = c;
2007-09-19 07:57:02 +08:00
break;
}
x += w;
}
if (utf8_it == utf8_end) {
if ((mx >= x) && (mx < bounds.x2())) {
2010-12-09 01:28:13 +08:00
caret = c;
}
}
2007-09-19 07:57:02 +08:00
return MID(0, caret, textlen);
2007-09-19 07:57:02 +08:00
}
void Entry::executeCmd(EntryCmd cmd, int unicodeChar, bool shift_pressed)
{
std::wstring text = base::from_utf8(this->text());
int c, selbeg, selend;
2010-12-09 01:28:13 +08:00
getEntryThemeInfo(NULL, NULL, NULL, &selbeg, &selend);
switch (cmd) {
case EntryCmd::NoOp:
break;
case EntryCmd::InsertChar:
// delete the entire selection
if (selbeg >= 0) {
text.erase(selbeg, selend-selbeg+1);
m_caret = selbeg;
}
// put the character
if (text.size() < m_maxsize) {
ASSERT((std::size_t)m_caret <= text.size());
text.insert(m_caret++, 1, unicodeChar);
}
2010-12-09 01:28:13 +08:00
m_select = -1;
break;
case EntryCmd::BackwardChar:
case EntryCmd::BackwardWord:
// selection
if (shift_pressed) {
if (m_select < 0)
m_select = m_caret;
}
else
m_select = -1;
// backward word
if (cmd == EntryCmd::BackwardWord) {
backwardWord();
}
// backward char
2010-12-09 01:28:13 +08:00
else if (m_caret > 0) {
m_caret--;
}
break;
case EntryCmd::ForwardChar:
case EntryCmd::ForwardWord:
// selection
if (shift_pressed) {
if (m_select < 0)
m_select = m_caret;
}
else
m_select = -1;
// forward word
if (cmd == EntryCmd::ForwardWord) {
forwardWord();
}
// forward char
2010-12-09 01:28:13 +08:00
else if (m_caret < (int)text.size()) {
m_caret++;
}
break;
case EntryCmd::BeginningOfLine:
// selection
if (shift_pressed) {
if (m_select < 0)
m_select = m_caret;
}
else
m_select = -1;
2010-12-09 01:28:13 +08:00
m_caret = 0;
break;
case EntryCmd::EndOfLine:
// selection
if (shift_pressed) {
if (m_select < 0)
m_select = m_caret;
}
else
m_select = -1;
2010-12-09 01:28:13 +08:00
m_caret = text.size();
break;
case EntryCmd::DeleteForward:
case EntryCmd::Cut:
// delete the entire selection
if (selbeg >= 0) {
// *cut* text!
if (cmd == EntryCmd::Cut) {
std::wstring selected = text.substr(selbeg, selend - selbeg + 1);
clip::set_text(base::to_utf8(selected));
}
// remove text
text.erase(selbeg, selend-selbeg+1);
m_caret = selbeg;
}
// delete the next character
else {
if (m_caret < (int)text.size())
text.erase(m_caret, 1);
}
2010-12-09 01:28:13 +08:00
m_select = -1;
break;
case EntryCmd::Paste: {
std::string clipboard;
if (clip::get_text(clipboard)) {
// delete the entire selection
if (selbeg >= 0) {
text.erase(selbeg, selend-selbeg+1);
m_caret = selbeg;
m_select = -1;
}
// paste text
for (c=0; c<base::utf8_length(clipboard); c++) {
if (text.size() < m_maxsize)
text.insert(m_caret+c, 1,
*(base::utf8_const_iterator(clipboard.begin())+c));
else
break;
}
setCaretPos(m_caret+c);
}
break;
}
case EntryCmd::Copy:
if (selbeg >= 0) {
std::wstring selected = text.substr(selbeg, selend - selbeg + 1);
clip::set_text(base::to_utf8(selected));
}
break;
case EntryCmd::DeleteBackward:
// delete the entire selection
if (selbeg >= 0) {
text.erase(selbeg, selend-selbeg+1);
m_caret = selbeg;
}
// delete the previous character
else {
if (m_caret > 0)
text.erase(--m_caret, 1);
}
2010-12-09 01:28:13 +08:00
m_select = -1;
break;
case EntryCmd::DeleteBackwardWord:
m_select = m_caret;
backwardWord();
if (m_caret < m_select)
text.erase(m_caret, m_select-m_caret);
m_select = -1;
break;
case EntryCmd::DeleteForwardToEndOfLine:
text.erase(m_caret);
break;
case EntryCmd::SelectAll:
selectAllText();
break;
}
std::string newText = base::to_utf8(text);
if (newText != this->text()) {
setText(newText.c_str());
onChange();
}
2010-12-09 01:28:13 +08:00
setCaretPos(m_caret);
invalidate();
}
#define IS_WORD_CHAR(ch) \
(!((!ch) || (std::isspace(ch)) || \
((ch) == '/') || ((ch) == '\\')))
2007-09-19 07:57:02 +08:00
2010-12-09 01:28:13 +08:00
void Entry::forwardWord()
2007-09-19 07:57:02 +08:00
{
base::utf8_const_iterator utf8_begin = base::utf8_const_iterator(text().begin());
int textlen = base::utf8_length(text());
2007-09-19 07:57:02 +08:00
int ch;
for (; m_caret < textlen; m_caret++) {
ch = *(utf8_begin + m_caret);
if (IS_WORD_CHAR(ch))
2007-09-19 07:57:02 +08:00
break;
}
for (; m_caret < textlen; m_caret++) {
ch = *(utf8_begin + m_caret);
if (!IS_WORD_CHAR(ch)) {
++m_caret;
2007-09-19 07:57:02 +08:00
break;
}
}
}
2010-12-09 01:28:13 +08:00
void Entry::backwardWord()
2007-09-19 07:57:02 +08:00
{
base::utf8_const_iterator utf8_begin = base::utf8_const_iterator(text().begin());
2007-09-19 07:57:02 +08:00
int ch;
for (--m_caret; m_caret >= 0; --m_caret) {
ch = *(utf8_begin + m_caret);
if (IS_WORD_CHAR(ch))
2007-09-19 07:57:02 +08:00
break;
}
for (; m_caret >= 0; --m_caret) {
ch = *(utf8_begin + m_caret);
if (!IS_WORD_CHAR(ch)) {
++m_caret;
2007-09-19 07:57:02 +08:00
break;
}
}
2010-12-09 01:28:13 +08:00
if (m_caret < 0)
m_caret = 0;
2007-09-19 07:57:02 +08:00
}
int Entry::getAvailableTextLength()
{
return clientChildrenBounds().w / font()->charWidth('w');
}
bool Entry::isPosInSelection(int pos)
{
return (pos >= MIN(m_caret, m_select) && pos <= MAX(m_caret, m_select));
}
void Entry::showEditPopupMenu(const gfx::Point& pt)
{
Menu menu;
MenuItem cut("Cut");
MenuItem copy("Copy");
MenuItem paste("Paste");
menu.addChild(&cut);
menu.addChild(&copy);
menu.addChild(&paste);
cut.Click.connect(base::Bind(&Entry::executeCmd, this, EntryCmd::Cut, 0, false));
copy.Click.connect(base::Bind(&Entry::executeCmd, this, EntryCmd::Copy, 0, false));
paste.Click.connect(base::Bind(&Entry::executeCmd, this, EntryCmd::Paste, 0, false));
if (isReadOnly()) {
cut.setEnabled(false);
paste.setEnabled(false);
}
menu.showPopup(pt);
}
} // namespace ui