#include "../include/Chatbar.hpp" #include "../include/ResourceCache.hpp" #include "../include/Settings.hpp" #include "../include/Channel.hpp" #include "../include/ChannelSidePanel.hpp" #include "../include/UsersSidePanel.hpp" #include "../include/Command.hpp" #include #include #include using namespace std; namespace dchat { const float FONT_SIZE = 24; const float BOX_PADDING_X = 15.0f; const float BOX_PADDING_Y = 5.0f; const int BLINK_TIME_VISIBLE_MS = 500; const int BLINK_TIME_INVISIBLE_MS = 500; const float PADDING_SIDE = 20.0f; Chatbar::Chatbar() : text("", ResourceCache::getFont("fonts/Roboto-Regular.ttf"), FONT_SIZE * Settings::getScaling()), caretIndex(0), focused(true) { text.setFillColor(sf::Color(240, 240, 240)); background.setFillColor(sf::Color(60, 60, 60)); } void Chatbar::addChar(sf::Uint32 codePoint) { auto str = text.getString(); str.insert(caretIndex, codePoint); text.setString(str); ++caretIndex; caretOffset = text.findCharacterPos(caretIndex) - text.getPosition(); blinkTimer.restart(); } void Chatbar::addString(const string &strToAdd) { if(strToAdd.empty()) return; auto str = text.getString(); str.insert(caretIndex, sf::String::fromUtf8(strToAdd.begin(), strToAdd.end())); text.setString(str); caretIndex += strToAdd.size(); caretOffset = text.findCharacterPos(caretIndex) - text.getPosition(); blinkTimer.restart(); } const sf::String& Chatbar::getString() const { return text.getString(); } void Chatbar::removePreviousChar() { if(caretIndex > 0) { auto str = text.getString(); str.erase(caretIndex - 1); text.setString(str); --caretIndex; caretOffset = text.findCharacterPos(caretIndex) - text.getPosition(); } blinkTimer.restart(); } void Chatbar::removeNextChar() { if(caretIndex < text.getString().getSize()) { auto str = text.getString(); str.erase(caretIndex); text.setString(str); } blinkTimer.restart(); } void Chatbar::clear() { text.setString(""); caretIndex = 0; caretOffset.x = 0.0f; caretOffset.y = 0.0f; blinkTimer.restart(); } void Chatbar::moveCaretLeft() { caretIndex = max(0, caretIndex - 1); // TODO: Use glyph size to optimize this, no need to iterate all glyphs caretOffset = text.findCharacterPos(caretIndex) - text.getPosition(); blinkTimer.restart(); } void Chatbar::moveCaretRight() { caretIndex = min((int)text.getString().getSize(), caretIndex + 1); caretOffset = text.findCharacterPos(caretIndex) - text.getPosition(); blinkTimer.restart(); } bool Chatbar::isFocused() const { return focused; } class ArgParseException : public std::runtime_error { public: ArgParseException(const string &errMsg) : std::runtime_error(errMsg) {} }; string stringRemoveQuotes(const StringView &str) { string result; result.reserve(str.size); for(usize i = 0; i < str.size; ++i) { char c = str[i]; if(c != '\'' && c != '"') result += c; } return result; } vector splitCommandArgs(const StringView &str) { vector result; ssize offset = 0; char quoteChar = '\0'; for(ssize i = 0; i < str.size; ++i) { char c = str[i]; if(c == '\'' || c == '"') { if(c == quoteChar) quoteChar = '\0'; else quoteChar = c; } else if(quoteChar == '\0' && c == ' ') { ssize substrLength = i - offset; if(substrLength > 0) { StringView substrView(str.data + offset, substrLength); string substr = stringRemoveQuotes(substrView); if(!substr.empty()) result.emplace_back(move(substr)); } offset = i + 1; } } if(quoteChar != '\0') { string errMsg = "Reached end of command before end of quote ("; errMsg += quoteChar; errMsg += ")"; throw ArgParseException(errMsg); } ssize strLeft = str.size - offset; if(strLeft > 0) { StringView substrView(str.data + offset, strLeft); string substr = stringRemoveQuotes(substrView); if(!substr.empty()) result.emplace_back(move(substr)); } return result; } void Chatbar::processChatCommand(const StringView &cmd) { vector args; try { args = splitCommandArgs(cmd); } catch(ArgParseException &e) { fprintf(stderr, "Failed to parse command arguments, reason: %s\n", e.what()); return; } if(!args.empty()) { string command = args.front(); args.erase(args.begin()); if(!Command::call(command, args)) fprintf(stderr, "No such command: %s\n", command.c_str()); } } string getClipboard() { string result; TinyProcessLib::Process process("xsel -o -b", "", [&result](const char *bytes, size_t n) { result.append(bytes, n); }); if(process.get_exit_status() != 0) fprintf(stderr, "Failed to get clipboard content\n"); return result; } void Chatbar::processEvent(const sf::Event &event, Channel *channel) { if(!focused) return; if(event.type == sf::Event::TextEntered) { if(event.text.unicode == 8) // backspace removePreviousChar(); else if(event.text.unicode == 13) // enter { if(!getString().isEmpty()) { if(sf::Keyboard::isKeyPressed(sf::Keyboard::Key::LShift) || sf::Keyboard::isKeyPressed(sf::Keyboard::Key::RShift)) { addChar('\n'); } else { auto chatbarMsgUtf8 = getString().toUtf8(); string msg; msg.resize(chatbarMsgUtf8.size()); memcpy(&msg[0], chatbarMsgUtf8.data(), chatbarMsgUtf8.size()); if(msg[0] == '/') processChatCommand(StringView(msg.data() + 1, msg.size() - 1)); else channel->addMessage(msg); clear(); } } } else if(event.text.unicode == 127) // delete { removeNextChar(); } else if(event.text.unicode == 22) // ctrl+v { // TODO: Instead of calling external xsel, use sfml clipboard functionality (in new sfml version) string clipboard = getClipboard(); addString(clipboard); } else { addChar(event.text.unicode); } } else if(event.type == sf::Event::KeyPressed) { if(event.key.code == sf::Keyboard::Left) moveCaretLeft(); else if(event.key.code == sf::Keyboard::Right) moveCaretRight(); } } void Chatbar::draw(sf::RenderWindow &window) { auto windowSize = window.getSize(); sf::Vector2f backgroundSize(floor(windowSize.x - ChannelSidePanel::getWidth() - UsersSidePanel::getWidth() - PADDING_SIDE * 2.0f), floor(text.getCharacterSize() * 1.7f + BOX_PADDING_Y * 2.0f)); sf::Vector2f backgroundPos(floor(ChannelSidePanel::getWidth() + PADDING_SIDE), floor(windowSize.y - backgroundSize.y - 20.0f)); background.setSize(backgroundSize); background.setPosition(backgroundPos); text.setPosition(floor(backgroundPos.x + BOX_PADDING_X), floor(backgroundPos.y + backgroundSize.y * 0.5f - text.getCharacterSize() * 0.5f)); window.draw(background); window.draw(text); int blinkElapsedTime = blinkTimer.getElapsedTime().asMilliseconds(); if(focused && blinkElapsedTime <= BLINK_TIME_VISIBLE_MS) { sf::RectangleShape caretShape(sf::Vector2f(2.0f, backgroundSize.y - BOX_PADDING_Y * 2.0f)); caretShape.setPosition(floor(text.getPosition().x + caretOffset.x), (caretOffset.y + backgroundPos.y + BOX_PADDING_Y)); window.draw(caretShape); } if(blinkElapsedTime > BLINK_TIME_VISIBLE_MS + BLINK_TIME_INVISIBLE_MS) blinkTimer.restart(); } }