diff --git a/src/ui/defaulttrackerwindow.cpp b/src/ui/defaulttrackerwindow.cpp index 79b2bb0f..d9f1bc9b 100644 --- a/src/ui/defaulttrackerwindow.cpp +++ b/src/ui/defaulttrackerwindow.cpp @@ -92,7 +92,7 @@ DefaultTrackerWindow::DefaultTrackerWindow(const char* title, SDL_Surface* icon, true); } - _loadPackWidget = new LoadPackWidget(0,0,0,0,_fontStore); + _loadPackWidget = new LoadPackWidget(0,0,0,0,_fontStore, this); _loadPackWidget->setPosition({0,0}); _loadPackWidget->setSize(_size); _loadPackWidget->setBackground({0,0,0}); @@ -362,9 +362,11 @@ void DefaultTrackerWindow::showOpen() { _loadPackWidget->update(); _loadPackWidget->setVisible(true); + _loadPackWidget->focusFilter(); } void DefaultTrackerWindow::hideOpen() { + _loadPackWidget->releaseFilterFocus(); _loadPackWidget->setVisible(false); } diff --git a/src/ui/loadpackwidget.cpp b/src/ui/loadpackwidget.cpp index c57ff154..fb3179f7 100644 --- a/src/ui/loadpackwidget.cpp +++ b/src/ui/loadpackwidget.cpp @@ -1,15 +1,47 @@ #include "loadpackwidget.h" #include "../core/pack.h" +#include "../core/assets.h" #include "../uilib/hbox.h" #include "../uilib/vbox.h" #include "../uilib/scrollvbox.h" #include "../uilib/label.h" +#include "../uilib/imagebutton.h" #include "defaults.h" // DEFAULT_FONT_* +#include +#include namespace Ui { -LoadPackWidget::LoadPackWidget(int x, int y, int w, int h, FontStore *fontStore) - : SimpleContainer(x,y,w,h), _fontStore(fontStore) +static std::string urlEncode(const std::string& in) +{ + static const char hex[] = "0123456789ABCDEF"; + std::string out; + for (unsigned char c : in) { + if (isalnum(c) || c=='-' || c=='_' || c=='.' || c=='~' || c=='/') + out += (char)c; + else { + out += '%'; + out += hex[(c>>4)&0xf]; + out += hex[c&0xf]; + } + } + return out; +} + +static void openPackDirectory() +{ + for (const auto& searchPath : Pack::getSearchPaths()) { + if (!fs::is_directory(searchPath)) + continue; + std::string url = "file://" + urlEncode(searchPath.u8string()); + if (SDL_OpenURL(url.c_str()) == 0) + return; + fprintf(stderr, "LoadPackWidget: could not open pack directory '%s'\n", searchPath.u8string().c_str()); + } +} + +LoadPackWidget::LoadPackWidget(int x, int y, int w, int h, FontStore *fontStore, Window *window) + : SimpleContainer(x,y,w,h), _fontStore(fontStore), _window(window) { _font = _fontStore->getFont(DEFAULT_FONT_NAME, DEFAULT_FONT_SIZE); _smallFont = _fontStore->getFont(DEFAULT_FONT_NAME, DEFAULT_FONT_SIZE - 2); @@ -18,6 +50,7 @@ LoadPackWidget::LoadPackWidget(int x, int y, int w, int h, FontStore *fontStore) onClick += {this, [this](void*, int, int, int button) { if (button == MouseButton::BUTTON_RIGHT) { setVisible(false); // TODO: fire onAbort ? + if (_filter) _filter->releaseFocus(); } }}; @@ -39,13 +72,97 @@ LoadPackWidget::LoadPackWidget(int x, int y, int w, int h, FontStore *fontStore) hbox->addChild(variants); hbox->setSpacing(1); hbox->setPadding(2); - addChild(hbox); _main = hbox; + + auto filter = new TextField(0,0,0,0, _font, window); + filter->setGrow(1,0); + filter->setPlaceholder("Filter packs..."); + filter->setBackground({16,16,16}); + _filter = filter; + + auto btnClear = new Button(0,0,0,0, _font, "Clear"); + btnClear->setGrow(0,0); + btnClear->setMinSize({64, btnClear->getAutoHeight()+4}); + _btnClearFilter = btnClear; + + btnClear->onClick += {this, [this](void*, int, int, int button) { + if (button == MouseButton::BUTTON_LEFT) { + _filter->clear(); + } + }}; + + auto btnOpenFolder = new ImageButton(0,0,24,0, asset("closed.png")); + btnOpenFolder->setGrow(0,0); + btnOpenFolder->setMinSize({24, 0}); + _btnOpenFolder = btnOpenFolder; + + btnOpenFolder->onClick += {this, [](void*, int, int, int button) { + if (button == MouseButton::BUTTON_LEFT) { + openPackDirectory(); + } + }}; + + filter->onTextChanged += {this, [this](void*, const std::string&) { + refreshPacks(); + }}; + + auto filterBar = new HBox(0,0,0,0); + filterBar->setGrow(1,0); + filterBar->setPadding(2); + filterBar->setSpacing(4); + filterBar->addChild(filter); + filterBar->addChild(btnClear); + filterBar->addChild(btnOpenFolder); + filterBar->setHeight(filter->getHeight()); + _filterBar = filterBar; + + auto root = new VBox(0,0,0,0); + root->setGrow(1,1); + root->setPadding(0); + root->setSpacing(1); + root->addChild(filterBar); + root->addChild(hbox); + _root = root; + addChild(root); + + _packs->onMouseLeave += {this, [this](void*) { + if (_disableHoverSelect && _curPackHover) { + if (_curPackHover == _curPackLabel) { + _curPackHover->setBackground(PACK_BG_ACTIVE); + } else { + _curPackHover->setBackground(PACK_BG_DEFAULT); + } + } + }}; +} + +void LoadPackWidget::focusFilter() +{ + if (_filter) _filter->grabFocus(); +} + +void LoadPackWidget::releaseFilterFocus() +{ + if (_filter) _filter->releaseFocus(); } void LoadPackWidget::update() { - auto availablePacks = Pack::ListAvailable(); + _availablePacks = Pack::ListAvailable(); + if (_filter) _filter->clear(); + refreshPacks(); +} + +static std::string toLower(const std::string& in) +{ + std::string out = in; + for (auto& c : out) + if (c >= 'A' && c <= 'Z') c += 'a'-'A'; + return out; +} + +void LoadPackWidget::refreshPacks() +{ _packs->clearChildren(); _variants->clearChildren(); _curPackHover = nullptr; @@ -53,7 +170,17 @@ void LoadPackWidget::update() _curVariantLabel = nullptr; _disableHoverSelect = false; - for (auto& pack : availablePacks) { + std::string filter; + if (_filter) filter = toLower(_filter->getText()); + + int shown = 0; + for (auto& pack : _availablePacks) { + if (!filter.empty()) { + std::string name = toLower(pack.packName + " " + pack.gameName + " " + pack.version); + if (name.find(filter) == std::string::npos) + continue; + } + shown++; auto lbl = new Label(0, 0, 0, 0, _font, " " + pack.packName + " " + pack.version); // TODO: button instead of label lbl->setGrow(1,0); lbl->setTextAlignment(Label::HAlign::LEFT, Label::VAlign::MIDDLE); @@ -126,20 +253,11 @@ void LoadPackWidget::update() }}; } - _packs->onMouseLeave += {this, [this](void*) { - if (_disableHoverSelect && _curPackHover) { - if (_curPackHover == _curPackLabel) { - _curPackHover->setBackground(PACK_BG_ACTIVE); - } else { - _curPackHover->setBackground(PACK_BG_DEFAULT); - } - } - }}; - - if (availablePacks.empty()) { - auto* lbl = new Label(0, 0, 0, 0, _font, - "No packs installed!\n" - "Drag & drop packs into the window to install them."); + if (shown == 0) { + const char* msg = _availablePacks.empty() ? + "No packs installed!\nDrag & drop packs into the window to install them." : + "No packs found!"; + auto* lbl = new Label(0, 0, 0, 0, _font, msg); lbl->setGrow(1,1); lbl->setTextAlignment(Label::HAlign::LEFT, Label::VAlign::MIDDLE); lbl->setMinSize(lbl->getAutoSize()); @@ -149,7 +267,6 @@ void LoadPackWidget::update() spacer->setGrow(1,1); _packs->addChild(spacer); } - _main->setSize(_size); _main->relayout(); // TODO: have this be done automatically } diff --git a/src/ui/loadpackwidget.h b/src/ui/loadpackwidget.h index 5f94723f..2d8e7135 100644 --- a/src/ui/loadpackwidget.h +++ b/src/ui/loadpackwidget.h @@ -7,23 +7,32 @@ #include "../uilib/vbox.h" #include "../uilib/scrollvbox.h" #include "../uilib/label.h" +#include "../uilib/button.h" +#include "../uilib/imagebutton.h" +#include "../uilib/textfield.h" #include "../uilib/fontstore.h" #include "../core/signal.h" #include #include "../core/fs.h" +#include "../core/pack.h" +#include namespace Ui { +class Window; + class LoadPackWidget : public SimpleContainer { public: using FONT = FontStore::FONT; - LoadPackWidget(int x, int y, int w, int h, FontStore *fontStore); + LoadPackWidget(int x, int y, int w, int h, FontStore *fontStore, Window *window=nullptr); void update(); + void focusFilter(); + void releaseFilterFocus(); Signal onPackSelected; - + virtual void setSize(Size size) override; // TODO: have more intelligent hbox instead protected: @@ -31,6 +40,13 @@ class LoadPackWidget : public SimpleContainer { FONT _font; FONT _smallFont; + Window *_window; + VBox *_root = nullptr; + HBox *_filterBar = nullptr; + TextField *_filter = nullptr; + Button *_btnClearFilter = nullptr; + ImageButton *_btnOpenFolder = nullptr; + ScrollVBox *_packs; VBox *_variants; HBox *_main; @@ -38,6 +54,9 @@ class LoadPackWidget : public SimpleContainer { Label *_curVariantLabel = nullptr; Label *_curPackHover = nullptr; bool _disableHoverSelect = false; + std::vector _availablePacks; + + void refreshPacks(); static constexpr Color PACK_BG_DEFAULT = {32, 32, 32}; static constexpr Color PACK_BG_ACTIVE = {32, 128, 32}; @@ -50,4 +69,3 @@ class LoadPackWidget : public SimpleContainer { } // namespace Ui #endif /* _UI_LOADPACKWIDGET_H */ - diff --git a/src/uilib/textfield.cpp b/src/uilib/textfield.cpp new file mode 100644 index 00000000..981f3111 --- /dev/null +++ b/src/uilib/textfield.cpp @@ -0,0 +1,154 @@ +#include "textfield.h" +#include "window.h" +#include "textutil.h" +#include +#include + +namespace Ui { + +TextField::TextField(int x, int y, int w, int h, FONT font, Window *window) + : Widget(x,y,w,h), _font(font), _window(window) +{ + _backgroundColor = {32,32,32}; + if (_font && h <= 0) { + int textH = 0; + SizeText(_font, " ", nullptr, &textH); + h = textH + 6; + } + if (h > 0) setHeight(h); + setMinSize({0, getHeight()}); + + onClick += {this, [this](void*, int x, int, int button) { + if (button == MouseButton::BUTTON_LEFT) { + grabFocus(); + setCursorToPos(x); + } + }}; + + onKeyDown += {this, [this](void*, int key, int mod) { + (void)mod; + int len = (int)_text.length(); + if (key == SDLK_BACKSPACE) { + if (_cursor > 0) { + _text.erase(_cursor-1, 1); + _cursor--; + onTextChanged.emit(this, _text); + } + } + else if (key == SDLK_DELETE) { + if (_cursor < len) { + _text.erase(_cursor, 1); + onTextChanged.emit(this, _text); + } + } + else if (key == SDLK_LEFT) { + if (_cursor > 0) _cursor--; + } + else if (key == SDLK_RIGHT) { + if (_cursor < len) _cursor++; + } + else if (key == SDLK_HOME) { + _cursor = 0; + } + else if (key == SDLK_END) { + _cursor = len; + } + else if (key == SDLK_RETURN || key == SDLK_KP_ENTER) { + releaseFocus(); + } + else if (key == SDLK_ESCAPE) { + clear(); + releaseFocus(); + } + }}; + + onTextInput += {this, [this](void*, const std::string& text) { + _text.insert(_cursor, text); + _cursor += (int)text.length(); + onTextChanged.emit(this, _text); + }}; +} + +TextField::~TextField() +{ +} + +int TextField::getTextWidth(const std::string& text) const +{ + int w = 0; + if (_font) SizeText(_font, text.c_str(), &w, nullptr); + return w; +} + +void TextField::setCursorToPos(int x) +{ + int len = (int)_text.length(); + int best = 0; + int bestDist = abs(x - getTextWidth(_text)); + for (int i=1; i<=len; i++) { + int dist = abs(x - getTextWidth(_text.substr(0, i))); + if (dist < bestDist) { + bestDist = dist; + best = i; + } + } + _cursor = best; +} + +void TextField::setText(const std::string& text) +{ + if (_text == text) return; + _text = text; + _cursor = (int)_text.length(); + onTextChanged.emit(this, _text); +} + +void TextField::clear() +{ + setText(""); +} + +void TextField::grabFocus() +{ + if (_window) _window->setKeyboardFocus(this); +} + +void TextField::releaseFocus() +{ + if (_window) _window->setKeyboardFocus(nullptr); +} + +void TextField::render(Renderer renderer, int offX, int offY) +{ + const auto& c = _backgroundColor; + SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); + SDL_Rect r = { offX+_pos.left, offY+_pos.top, _size.width, _size.height }; + SDL_RenderFillRect(renderer, &r); + + const std::string& text = _text.empty() ? _placeholder : _text; + Widget::Color color = _text.empty() ? _placeholderColor : _textColor; + if (!text.empty() && _font) { + SDL_Color col = {color.r, color.g, color.b, color.a}; + SDL_Surface* surf = RenderText(_font, text.c_str(), col, Label::HAlign::LEFT); + if (surf) { + SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, surf); + SDL_Rect dest = { offX+_pos.left+2, offY+_pos.top+(_size.height-surf->h)/2, surf->w, surf->h }; + SDL_Rect src = { 0,0,surf->w,surf->h }; + if (dest.w > _size.width-4) { dest.w = _size.width-4; src.w = dest.w; } + if (dest.h > _size.height) { dest.h = _size.height; src.h = dest.h; } + SDL_RenderCopy(renderer, tex, &src, &dest); + SDL_DestroyTexture(tex); + SDL_FreeSurface(surf); + } + } + + if (_window && _window->getKeyboardFocus() == this && (SDL_GetTicks()/500)%2==0) { + int caretX = 2 + getTextWidth(_text.substr(0, _cursor)); + int caretH = _size.height > 4 ? _size.height-4 : 1; + SDL_SetRenderDrawColor(renderer, 0xff, 0xff, 0xff, 0xff); + SDL_Rect caret = { offX+_pos.left+caretX, offY+_pos.top+2, 1, caretH }; + SDL_RenderFillRect(renderer, &caret); + } +} + +} // namespace diff --git a/src/uilib/textfield.h b/src/uilib/textfield.h new file mode 100644 index 00000000..ee284583 --- /dev/null +++ b/src/uilib/textfield.h @@ -0,0 +1,47 @@ +#ifndef _UILIB_TEXTFIELD_H +#define _UILIB_TEXTFIELD_H + +#include +#include +#include "widget.h" + +namespace Ui { + +class Window; + +class TextField : public Widget { +public: + using FONT = TTF_Font*; + TextField(int x, int y, int w, int h, FONT font, Window *window=nullptr); + virtual ~TextField(); + + virtual void render(Renderer renderer, int offX, int offY) override; + + virtual void setText(const std::string& text); + const std::string& getText() const { return _text; } + void clear(); + void setPlaceholder(const std::string& placeholder) { _placeholder = placeholder; } + virtual void setTextColor(Widget::Color c) { _textColor = c; } + virtual void setBackground(Widget::Color color) override { _backgroundColor = color; } + + void grabFocus(); + void releaseFocus(); + + Signal onTextChanged; + +protected: + FONT _font; + std::string _text; + std::string _placeholder; + int _cursor = 0; + Window *_window = nullptr; + Widget::Color _textColor = {255,255,255}; + Widget::Color _placeholderColor = {128,128,128}; + + int getTextWidth(const std::string& text) const; + void setCursorToPos(int x); +}; + +} // namespace Ui + +#endif // _UILIB_TEXTFIELD_H diff --git a/src/uilib/ui.cpp b/src/uilib/ui.cpp index edd81e46..7d40e9b4 100644 --- a/src/uilib/ui.cpp +++ b/src/uilib/ui.cpp @@ -273,9 +273,36 @@ bool Ui::render() } } } + { + auto winIt = _windows.find(ev.key.windowID); + if (winIt != _windows.end()) { + Widget* focus = winIt->second->getKeyboardFocus(); + if (focus) { + focus->onKeyDown.emit(focus, + (int)ev.key.keysym.sym, + (int)(ev.key.keysym.mod & (KMOD_CTRL|KMOD_SHIFT|KMOD_GUI))); + } + } + } + EVENT_UNLOCK(this); + break; + } + case SDL_TEXTINPUT: { + EVENT_LOCK(this); + auto winIt = _windows.find(ev.text.windowID); + if (winIt != _windows.end()) { + Widget* focus = winIt->second->getKeyboardFocus(); + if (focus) { + focus->onTextInput.emit(focus, std::string(ev.text.text)); + } + } EVENT_UNLOCK(this); break; } + case SDL_TEXTEDITING: { + // IME text editing is not supported yet + break; + } case SDL_WINDOWEVENT: { if (ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { EVENT_LOCK(this); diff --git a/src/uilib/widget.h b/src/uilib/widget.h index 195e7504..ff7e7dbb 100644 --- a/src/uilib/widget.h +++ b/src/uilib/widget.h @@ -243,6 +243,8 @@ class Widget { Signal<> onMouseCancel; Signal onScroll; Signal<> onDestroy; + Signal onKeyDown; + Signal onTextInput; #ifndef NDEBUG void dumpInfo() { diff --git a/src/uilib/window.cpp b/src/uilib/window.cpp index 959526b5..3d858694 100644 --- a/src/uilib/window.cpp +++ b/src/uilib/window.cpp @@ -233,6 +233,22 @@ void Window::grabFocus() SDL_SetWindowInputFocus(_win); } +void Window::setKeyboardFocus(Widget* w) +{ + if (_keyboardFocus == w) return; + if (_keyboardFocus) { + SDL_StopTextInput(); + _keyboardFocus->onDestroy -= this; + } + _keyboardFocus = w; + if (_keyboardFocus) { + SDL_StartTextInput(); + _keyboardFocus->onDestroy += {this, [this](void* destroyed) { + if (_keyboardFocus == destroyed) _keyboardFocus = nullptr; + }}; + } +} + bool Window::getAlwaysOnTop() const { return _isAlwaysOnTop; diff --git a/src/uilib/window.h b/src/uilib/window.h index b82f4c2d..f5f7b614 100644 --- a/src/uilib/window.h +++ b/src/uilib/window.h @@ -48,6 +48,7 @@ class Window : public Container { Position _lastMousePos; Widget* _tooltip = nullptr; + Widget* _keyboardFocus = nullptr; bool _isAlwaysOnTop = false; void clear(); @@ -83,6 +84,9 @@ class Window : public Container { bool getAlwaysOnTop() const; virtual void setAlwaysOnTop(bool alwaysOnTop); + void setKeyboardFocus(Widget* w); + Widget* getKeyboardFocus() const { return _keyboardFocus; } + bool isAccelerated(); Signal onDrop; // x, y, type, data