Support editing utf-8 text in TextBox

aka "Flüxbøx"

βµγ, pardon,
BUG: 720
This commit is contained in:
Thomas Lübking 2016-09-04 00:08:04 +02:00 committed by Mathias Gumz
parent 2047b1a2ba
commit cdd6861aff
5 changed files with 113 additions and 23 deletions

View file

@ -45,6 +45,9 @@ App::App(const char *displayname):m_done(false), m_display(0) {
if (s_app != 0)
throw std::string("Can't create more than one instance of FbTk::App");
s_app = this;
// with recent versions of X11 one needs to specify XSetLocaleModifiers,
// otherwise no extra events won't be generated.
const bool setmodifiers = XSetLocaleModifiers("@im=none");
// this allows the use of std::string.c_str(), which returns
// a blank string, rather than a null string, so we make them equivalent
if (displayname != 0 && displayname[0] == '\0')
@ -59,6 +62,11 @@ App::App(const char *displayname):m_done(false), m_display(0) {
}
FbStringUtil::init();
m_xim = 0;
if (setmodifiers && FbStringUtil::haveUTF8()) {
m_xim = XOpenIM(m_display, NULL, NULL, NULL);
}
}
App::~App() {

View file

@ -53,6 +53,7 @@ public:
/// forces an end to event loop
void end();
bool done() const { return m_done; }
const XIM &inputModule() const { return m_xim; }
// the setenv()-routine is not everywhere available and
// putenv() doesnt manage the strings in the environment
@ -62,6 +63,7 @@ private:
static App *s_app;
bool m_done;
Display *m_display;
XIM m_xim;
};
} // end namespace FbTk

View file

@ -161,7 +161,8 @@ void EventManager::dispatch(Window win, XEvent &ev, bool parent) {
switch (ev.type) {
case KeyPress:
evhand->keyPressEvent(ev.xkey);
if (!XFilterEvent(&ev, win))
evhand->keyPressEvent(ev.xkey);
break;
case KeyRelease:
evhand->keyReleaseEvent(ev.xkey);

View file

@ -43,18 +43,22 @@
#include <X11/keysym.h>
#include <X11/Xutil.h>
#include <iostream>
namespace FbTk {
TextBox::TextBox(int screen_num,
const Font &font, const std::string &text):
FbWindow(screen_num, 0, 0, 1, 1, ExposureMask | KeyPressMask | ButtonPressMask),
FbWindow(screen_num, 0, 0, 1, 1, ExposureMask | KeyPressMask | ButtonPressMask | FocusChangeMask | KeymapStateMask),
m_font(&font),
m_text(text),
m_gc(0),
m_cursor_pos(0),
m_start_pos(0),
m_end_pos(0),
m_select_pos(std::string::npos) {
m_select_pos(std::string::npos),
m_xic(0) {
FbTk::EventManager::instance()->add(*this, *this);
}
@ -68,7 +72,8 @@ TextBox::TextBox(const FbWindow &parent,
m_cursor_pos(0),
m_start_pos(0),
m_end_pos(0),
m_select_pos(std::string::npos) {
m_select_pos(std::string::npos),
m_xic(0) {
FbTk::EventManager::instance()->add(*this, *this);
}
@ -111,36 +116,43 @@ void TextBox::cursorEnd() {
}
void TextBox::cursorForward() {
if (m_start_pos + cursorPosition() < m_end_pos)
m_cursor_pos++;
StringRange r = charRange(m_start_pos + cursorPosition());
const std::string::size_type s = r.end - r.begin + 1;
if (r.end < m_end_pos)
m_cursor_pos = r.end + 1 - m_start_pos;
else if (m_end_pos < text().size()) {
m_cursor_pos++;
m_end_pos++;
m_cursor_pos = r.end + 1 - m_start_pos;
m_end_pos += s;
adjustStartPos();
}
}
void TextBox::cursorBackward() {
if (cursorPosition())
m_cursor_pos--;
else if (m_start_pos) {
m_start_pos--;
adjustEndPos();
if (m_start_pos || cursorPosition()) {
StringRange r = charRange(m_start_pos + cursorPosition() - 1);
const std::string::size_type s = r.end - r.begin + 1;
if (cursorPosition())
m_cursor_pos = r.begin - m_start_pos;
else if (m_start_pos) {
m_start_pos -= s;
adjustEndPos();
}
}
}
void TextBox::backspace() {
if (hasSelection())
return deleteForward();
if (m_start_pos || cursorPosition()) {
FbString t = text();
t.erase(m_start_pos + cursorPosition() - 1, 1);
StringRange r = charRange(m_start_pos + cursorPosition() - 1);
const std::string::size_type s = r.end - r.begin + 1;
t.erase(r.begin, s);
m_text.setLogical(t);
if (cursorPosition())
setCursorPosition(cursorPosition() - 1);
setCursorPosition(r.begin - m_start_pos);
else
m_start_pos--;
m_start_pos -= s;
adjustEndPos();
}
}
@ -148,10 +160,15 @@ void TextBox::backspace() {
void TextBox::deleteForward() {
std::string::size_type pos = m_start_pos + m_cursor_pos;
int length = 1;
if (hasSelection()) {
bool selected = false;
if (selected = hasSelection()) {
pos = std::min(m_start_pos + m_cursor_pos, m_select_pos);
length = std::max(m_start_pos + m_cursor_pos, m_select_pos) - pos;
m_cursor_pos = pos - m_start_pos;
} else {
StringRange r = charRange(pos);
pos = r.begin;
length = r.end - r.begin + 1;
}
if (pos < m_end_pos) {
FbString t = text();
@ -159,7 +176,7 @@ void TextBox::deleteForward() {
m_text.setLogical(t);
adjustEndPos();
}
if (length > 1)
if (selected && length > 1)
adjustStartPos();
}
@ -168,7 +185,8 @@ void TextBox::insertText(const std::string &val) {
deleteForward();
FbString t = text();
t.insert(m_start_pos + cursorPosition(), val);
std::string::size_type pos = m_start_pos + m_cursor_pos;
t.insert(pos ? charRange(pos - 1).end + 1 : pos, val);
m_text.setLogical(t);
m_cursor_pos += val.size();
m_end_pos += val.size();
@ -267,12 +285,17 @@ void TextBox::buttonPressEvent(XButtonEvent &event) {
}
void TextBox::keyPressEvent(XKeyEvent &event) {
event.state = KeyUtil::instance().cleanMods(event.state);
KeySym ks;
char keychar[1];
XLookupString(&event, keychar, 1, &ks, 0);
char keychar[20];
int count = 1;
if (m_xic)
count = Xutf8LookupString(m_xic, &event, keychar, 20, &ks, 0);
else
XLookupString(&event, keychar, 1, &ks, 0);
// a modifier key by itself doesn't do anything
if (IsModifierKey(ks)) return;
@ -401,6 +424,17 @@ void TextBox::keyPressEvent(XKeyEvent &event) {
break;
}
}
if (count > 1 && count < 20) {
wchar_t wc;
count = mbrtowc(&wc, keychar, count, 0);
if (count > 0 && iswprint(wc)) {
keychar[count] = '\0';
std::string val;
val += (char*)keychar;
insertText(val);
m_select_pos = std::string::npos;
}
}
if (isprint(keychar[0])) {
std::string val;
val += keychar[0];
@ -412,6 +446,23 @@ void TextBox::keyPressEvent(XKeyEvent &event) {
clear();
}
void TextBox::handleEvent(XEvent &event) {
if (event.type == KeymapNotify) {
XRefreshKeyboardMapping(&event.xmapping);
} else if (event.type == FocusIn) {
if (!m_xic && App::instance()->inputModule())
m_xic = XCreateIC(App::instance()->inputModule(), XNInputStyle, XIMPreeditNothing | XIMStatusNothing, XNClientWindow, window(), NULL);
if (m_xic)
XSetICFocus(m_xic);
} else if (event.type == FocusIn) {
if (m_xic) {
XUnsetICFocus(m_xic);
XFree(m_xic);
m_xic = 0;
}
}
}
void TextBox::setCursorPosition(int pos) {
m_cursor_pos = pos < 0 ? 0 : pos;
if (m_cursor_pos > text().size())
@ -527,4 +578,27 @@ void TextBox::selectAll() {
select(0, m_text.visual().size());
}
TextBox::StringRange TextBox::charRange(std::string::size_type pos) const {
auto isUtf8Head = [](char c) {
const char indicator = (1<<7)|(1<<6); // first byte starts 11, following 10
return (c & indicator) == indicator;
};
StringRange range = {pos, pos};
FbString t = text();
if (pos < 0 || pos >= t.size() || t.at(pos) > 0) // invalid pos or ASCII
return range;
while (range.begin > 0 && t.at(range.begin) < 0 && !isUtf8Head(t.at(range.begin)))
--range.begin;
if (isUtf8Head(t.at(range.end))) // if pos is a utf-8 head, move into the range
++range.end;
while (range.end < t.size() - 1 && t.at(range.end + 1) < 0 && !isUtf8Head(t.at(range.end + 1)))
++range.end;
return range;
}
} // end namespace FbTk

View file

@ -58,6 +58,7 @@ public:
void exposeEvent(XExposeEvent &event);
void buttonPressEvent(XButtonEvent &event);
void keyPressEvent(XKeyEvent &event);
void handleEvent(XEvent &event);
const FbString &text() const { return m_text.logical(); }
const Font &font() const { return *m_font; }
@ -79,10 +80,14 @@ private:
void adjustPos();
typedef struct { std::string::size_type begin, end; } StringRange;
StringRange charRange(std::string::size_type pos) const;
const FbTk::Font *m_font;
BiDiString m_text;
GC m_gc;
std::string::size_type m_cursor_pos, m_start_pos, m_end_pos, m_select_pos;
XIC m_xic;
};
} // end namespace FbTk