From c3a78724c7bd20413425ee014f0fc3810f0a86c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 26 Feb 2026 16:06:35 +0100 Subject: [PATCH] Add an on-screen keyboard in the popup inputbox --- Common/UI/PopupScreens.cpp | 98 ++++++++++++++++++++++- Common/UI/PopupScreens.h | 7 ++ Common/UI/View.cpp | 76 +++++++++++++----- Common/UI/View.h | 8 +- Common/UI/ViewGroup.cpp | 12 +++ Common/UI/ViewGroup.h | 4 + UI/UIAtlas.cpp | 2 + assets/ui_images/images.svg | 152 +++++++++++++++++++++++++++++++++--- 8 files changed, 326 insertions(+), 33 deletions(-) diff --git a/Common/UI/PopupScreens.cpp b/Common/UI/PopupScreens.cpp index 1361708a94..2f43168553 100644 --- a/Common/UI/PopupScreens.cpp +++ b/Common/UI/PopupScreens.cpp @@ -800,18 +800,112 @@ std::string PopupTextInputChoice::ValueText() const { return *value_; } +LinearLayout *CreateSoftKeyboard(TextEdit *edit, bool *upperCase) { + LinearLayout *keyboard = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + // TODO: Make something a bit more international... Although we don't need that for domain names. + static struct { + std::string_view v; const char *tag; + } kbRows[] = { + {"1234567890-=", "A"}, + {"qwertyuiop'[]", "L"}, + {"asdfghjkl()", "L"}, + {"zxcvbnm,.", "L"}, + {"QWERTYUIOP'[]", "U"}, + {"ASDFGHJKL()", "U"}, + {"ZXCVBNM,.", "U"}, + {"", "A"}, + }; + static const float space[] = { + 0.0f, 10.0f, 20.0f, 30.0f, 10.0f, 20.0f, 30.0f, 30.0f, + }; + + static_assert(ARRAY_SIZE(kbRows) == ARRAY_SIZE(space)); + + keyboard->SetSpacing(5.0f); + for (int i = 0; i < ARRAY_SIZE(kbRows); i++) { + LinearLayout *row = keyboard->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT))); + row->SetSpacing(5.0f); + row->Add(new Spacer(space[i])); + row->SetTag(kbRows[i].tag); + for (int j = 0; j < kbRows[i].v.length(); j++) { + char c = kbRows[i].v[j]; + row->Add(new Button(StringFromFormat("%c", c), new LinearLayoutParams(40.0f, 50.0f)))->OnClick.Add([edit, c](EventParams &) { + edit->InsertAtCaret(StringFromFormat("%c", c).c_str()); + }); + } + + bool visible = false; + switch (kbRows[i].tag[0]) { + case 'L': visible = !(*upperCase); break; + case 'U': visible = *upperCase; break; + case 'A': visible = true; break; + default: visible = false; break; + } + row->SetVisibility(visible ? V_VISIBLE : V_GONE); + + switch (i) { + case 0: + // Add backspace button at the end of the first row. + row->Add(new Button("", ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(60.0f, 50.0f)))->OnClick.Add([edit](EventParams &) { + edit->Backspace(); + }); + break; + case 7: + // Special keys. + row->Add(new Button("Aa", new LinearLayoutParams(80.0f, 50.0f)))->OnClick.Add([edit, keyboard, upperCase](EventParams &) { + *upperCase = !(*upperCase); + // Work through visibility. + for (int i = 0; i < keyboard->GetNumSubviews(); i++) { + LinearLayout *row = (LinearLayout *)keyboard->GetViewByIndex(i); + switch (row->Tag()[0]) { + case 'L': row->SetVisibility(*upperCase ? V_GONE : V_VISIBLE); break; + case 'U': row->SetVisibility(*upperCase ? V_VISIBLE : V_GONE); break; + case 'A': row->SetVisibility(V_VISIBLE); break; + default: row->SetVisibility(V_GONE); break; + } + } + }); + + row->Add(new Button("Space", new LinearLayoutParams(200.0f, 50.0f)))->OnClick.Add([edit](EventParams &) { + edit->SetText(edit->GetText() + " "); + }); + row->Add(new Button("", ImageID("I_ARROW_LEFT"), new LinearLayoutParams(60.0f, 50.0f)))->OnClick.Add([edit](EventParams &) { + edit->MoveLeft(); + }); + row->Add(new Button("", ImageID("I_ARROW_RIGHT"), new LinearLayoutParams(60.0f, 50.0f)))->OnClick.Add([edit](EventParams &) { + edit->MoveRight(); + }); + break; + } + } + return keyboard; +} + void TextEditPopupScreen::CreatePopupContents(UI::ViewGroup *parent) { using namespace UI; UIContext &dc = *screenManager()->getUIContext(); textEditValue_ = *value_; - LinearLayout *lin = parent->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams((UI::Size)300, WRAP_CONTENT))); - edit_ = new TextEdit(textEditValue_, Title(), placeholder_, new LinearLayoutParams(1.0f)); + LinearLayout *lin = parent->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); + edit_ = new TextEdit(textEditValue_, Title(), placeholder_, new LinearLayoutParams(1.0f, Gravity::G_VCENTER, Margins(8))); edit_->SetMaxLen(maxLen_); edit_->SetTextColor(dc.GetTheme().popupStyle.fgColor); edit_->SetPasswordMasking(passwordMasking_); lin->Add(edit_); + parent->Add(new Spacer(8.0f)); + + keyboard_ = parent->Add(CreateSoftKeyboard(edit_, &upperCase_)); + if (System_GetPropertyBool(SYSPROP_HAS_KEYBOARD)) { + keyboard_->SetVisibility(V_GONE); + lin->Add(new Spacer(5.0f)); + showKeyboardChoice_ = lin->Add(new Choice(ImageID("I_KEYBOARD"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT))); + showKeyboardChoice_->OnClick.Add([this](EventParams &) { + keyboard_->SetVisibility(V_VISIBLE); + showKeyboardChoice_->SetEnabled(false); + }); + } + UI::SetFocusedView(edit_); } diff --git a/Common/UI/PopupScreens.h b/Common/UI/PopupScreens.h index b3337d7c04..b2d838d625 100644 --- a/Common/UI/PopupScreens.h +++ b/Common/UI/PopupScreens.h @@ -220,6 +220,8 @@ private: bool liveUpdate_; }; +LinearLayout *CreateSoftKeyboard(TextEdit *edit, bool *upperCase); + class TextEditPopupScreen : public PopupScreen { public: TextEditPopupScreen(std::string *value, std::string_view placeholder, std::string_view title, int maxLen) @@ -235,12 +237,17 @@ public: Event OnChange; private: + virtual UI::Size PopupWidth() const override { return 600; } + void OnCompleted(DialogResult result) override; TextEdit *edit_ = nullptr; + LinearLayout *keyboard_ = nullptr; + Choice *showKeyboardChoice_ = nullptr; std::string *value_; std::string textEditValue_; std::string placeholder_; int maxLen_; + bool upperCase_ = false; bool passwordMasking_ = false; }; diff --git a/Common/UI/View.cpp b/Common/UI/View.cpp index aa943187a6..8cc84f4925 100644 --- a/Common/UI/View.cpp +++ b/Common/UI/View.cpp @@ -1206,19 +1206,34 @@ void TextEdit::Draw(UIContext &dc) { dc.DrawTextRect(textToDisplay, textBounds, textColor, ALIGN_VCENTER | ALIGN_LEFT | align_); } - if (HasFocus()) { - // Hack to find the caret position. Might want to find a better way... - dc.MeasureText(dc.GetTheme().uiFont, 1.0f, 1.0f, textToDisplay.substr(0, caret_), &w, &h, ALIGN_VCENTER | ALIGN_LEFT | align_); - float caretX = w - scrollPos_; - if (caretX > bounds_.w) { - scrollPos_ += caretX - bounds_.w; - } - if (caretX < 0) { - scrollPos_ += caretX; - } - caretX += textX; - dc.FillRect(UI::Drawable(textColor), Bounds(caretX - 1, bounds_.y + 2, 3, bounds_.h - 4)); + // Hack to find the caret position. Might want to find a better way... + dc.MeasureText(dc.GetTheme().uiFont, 1.0f, 1.0f, textToDisplay.substr(0, caret_), &w, &h, ALIGN_VCENTER | ALIGN_LEFT | align_); + float caretX = w - scrollPos_; + if (caretX > bounds_.w) { + scrollPos_ += caretX - bounds_.w; } + if (caretX < 0) { + scrollPos_ += caretX; + } + caretX += textX; + dc.FillRect(UI::Drawable(textColor), Bounds(caretX - 1, bounds_.y + 2, 3, bounds_.h - 4)); + + if (selectAtX_ >= 0) { + caret_ = -1; + for (int i = 0; i <= text_.size(); i++) { + dc.MeasureText(dc.GetTheme().uiFont, 1.0f, 1.0f, textToDisplay.substr(0, i), &w, &h, ALIGN_VCENTER | ALIGN_LEFT | align_); + float charX = w - scrollPos_; + if (charX >= selectAtX_ - 3) { + caret_ = i; + break; + } + } + if (caret_ == -1) { + caret_ = (int)text_.size(); + } + selectAtX_ = -1; + } + dc.PopScissor(); } @@ -1250,12 +1265,38 @@ bool TextEdit::Touch(const TouchInput &touch) { if (touch.flags & TouchInputFlags::DOWN) { if (bounds_.Contains(touch.x, touch.y)) { SetFocusedView(this, true); + int relativeX = touch.x - bounds_.x + scrollPos_; + selectAtX_ = relativeX; return true; } } return false; } +void TextEdit::MoveLeft() { + if (caret_ > 0) { + u8_dec(text_.c_str(), &caret_); + } +} + +void TextEdit::MoveRight() { + if (caret_ < (int)text_.size()) { + u8_inc(text_.c_str(), &caret_); + } +} + +bool TextEdit::Backspace() { + if (caret_ > 0) { + int begCaret = caret_; + u8_dec(text_.c_str(), &begCaret); + undo_ = text_; + text_.erase(text_.begin() + begCaret, text_.begin() + caret_); + caret_--; + return true; + } + return false; +} + bool TextEdit::Key(const KeyInput &input) { if (!HasFocus()) return false; @@ -1268,10 +1309,10 @@ bool TextEdit::Key(const KeyInput &input) { ctrlDown_ = true; break; case NKCODE_DPAD_LEFT: // ASCII left arrow - u8_dec(text_.c_str(), &caret_); + MoveLeft(); break; case NKCODE_DPAD_RIGHT: // ASCII right arrow - u8_inc(text_.c_str(), &caret_); + MoveRight(); break; case NKCODE_MOVE_HOME: case NKCODE_PAGE_UP: @@ -1291,12 +1332,7 @@ bool TextEdit::Key(const KeyInput &input) { } break; case NKCODE_DEL: - if (caret_ > 0) { - int begCaret = caret_; - u8_dec(text_.c_str(), &begCaret); - undo_ = text_; - text_.erase(text_.begin() + begCaret, text_.begin() + caret_); - caret_--; + if (Backspace()) { textChanged = true; } break; diff --git a/Common/UI/View.h b/Common/UI/View.h index 20b9578174..793ac39e1d 100644 --- a/Common/UI/View.h +++ b/Common/UI/View.h @@ -1104,12 +1104,15 @@ public: bool Key(const KeyInput &key) override; bool Touch(const TouchInput &touch) override; + bool Backspace(); + void MoveLeft(); + void MoveRight(); + void InsertAtCaret(const char *text); + Event OnTextChange; Event OnEnter; private: - void InsertAtCaret(const char *text); - std::string text_; std::string title_; std::string undo_; @@ -1122,6 +1125,7 @@ private: bool ctrlDown_ = false; // TODO: Make some global mechanism for this. bool passwordMasking_ = false; int align_ = 0; + int selectAtX_ = -1; // on next draw, will select the character closest to this X coordinate. Used for touch selection. // TODO: Selections }; diff --git a/Common/UI/ViewGroup.cpp b/Common/UI/ViewGroup.cpp index 47fd84e208..2a3ddc91bc 100644 --- a/Common/UI/ViewGroup.cpp +++ b/Common/UI/ViewGroup.cpp @@ -160,6 +160,18 @@ void ViewGroup::DeviceRestored(Draw::DrawContext *draw) { } } +bool ViewGroup::ReplaceSubview(View *view, View *newView) { + for (int i = 0; i < (int)views_.size(); i++) { + if (views_[i] == view) { + views_[i] = newView; + delete view; + return true; + } + } + delete newView; + return false; +} + void ViewGroup::Draw(UIContext &dc) { if (hasDropShadow_) { // Darken things behind. diff --git a/Common/UI/ViewGroup.h b/Common/UI/ViewGroup.h index 54bd051086..11643030c8 100644 --- a/Common/UI/ViewGroup.h +++ b/Common/UI/ViewGroup.h @@ -51,6 +51,10 @@ public: return view; } + // Note: This deletes the old view, if found. Returns whether the view was found. + // If it fails, the newView is deleted. + bool ReplaceSubview(View *view, View *newView); + bool SetFocus() override; bool SubviewFocused(View *view) override; virtual void RemoveSubview(View *view); diff --git a/UI/UIAtlas.cpp b/UI/UIAtlas.cpp index 1225b9d613..e68c41cf1f 100644 --- a/UI/UIAtlas.cpp +++ b/UI/UIAtlas.cpp @@ -182,6 +182,8 @@ static const ImageMeta imageIDs[] = { {"I_UMD_VIDEO_ISO", false}, {"I_APP", false}, {"I_SHORTCUT", false}, + {"I_KEYBOARD", false}, + {"I_MOUSE", false}, }; static std::string PNGNameFromID(std::string_view id) { diff --git a/assets/ui_images/images.svg b/assets/ui_images/images.svg index f19c437852..80237be237 100644 --- a/assets/ui_images/images.svg +++ b/assets/ui_images/images.svg @@ -7,7 +7,7 @@ viewBox="0 0 158.74998 185.20831" version="1.1" id="svg1" - inkscape:version="1.4 (86a8ad7, 2024-10-11)" + inkscape:version="1.4.3 (0d15f75, 2025-12-25)" sodipodi:docname="images.svg" xml:space="preserve" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" @@ -24,13 +24,13 @@ inkscape:pagecheckerboard="true" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="1" - inkscape:cx="292.5" - inkscape:cy="350.5" - inkscape:window-width="2560" - inkscape:window-height="1377" - inkscape:window-x="-8" - inkscape:window-y="-8" + inkscape:zoom="2.8284271" + inkscape:cx="-1.2374369" + inkscape:cy="458.73552" + inkscape:window-width="3840" + inkscape:window-height="2071" + inkscape:window-x="-9" + inkscape:window-y="-9" inkscape:window-maximized="1" inkscape:current-layer="layer1" showgrid="false" @@ -4538,7 +4538,137 @@ id="I_GEAR_STAR" style="fill:#ececec;fill-opacity:1;stroke-width:0.0151106" class="st0" - d="m 7.0939218,41.027272 -0.40566,1.30483 -1.30483,0.40566 1.1157,0.7891 -0.0176,1.36632 1.09502,-0.817 1.2945,0.43873 -0.43874,-1.2945 0.81701,-1.09502 -1.36633,0.0176 z m -2.1611,3.17293 c -0.10207,0 -0.191,0.0703 -0.21446,0.1695 l -0.1912,0.81287 c -0.32129,0.0745 -0.62282,0.20047 -0.89452,0.37 l -0.71004,-0.43873 c -0.0868,-0.0536 -0.19933,-0.0404 -0.2713,0.0315 l -0.37569,0.37569 c -0.0723,0.0723 -0.0853,0.18449 -0.0315,0.2713 l 0.43873,0.71055 c -0.16954,0.27187 -0.29604,0.5727 -0.37052,0.894 l -0.81235,0.19172 c -0.0993,0.0236 -0.1695,0.11216 -0.1695,0.21394 v 0.53175 c 0,0.10219 0.0701,0.19049 0.1695,0.21394 l 0.81235,0.19224 c 0.0745,0.32131 0.201,0.62215 0.37052,0.894 l -0.43873,0.71004 c -0.0536,0.0868 -0.0404,0.19984 0.0315,0.27181 l 0.37517,0.37517 c 0.0723,0.0723 0.18486,0.0853 0.27182,0.0315 l 0.71004,-0.43874 c 0.27171,0.16955 0.57323,0.29604 0.89452,0.37052 l 0.1912,0.81236 c 0.0235,0.0994 0.11239,0.1695 0.21446,0.1695 h 0.53175 c 0.10193,0 0.19049,-0.0701 0.21394,-0.1695 l 0.19172,-0.81236 c 0.32116,-0.0745 0.62276,-0.20097 0.89451,-0.37052 l 0.71004,0.43874 c 0.087,0.0537 0.19919,0.0407 0.2713,-0.0315 l 0.37569,-0.37517 c 0.0721,-0.0721 0.0852,-0.18447 0.0315,-0.2713 l -0.43925,-0.71055 c 0.16953,-0.27173 0.29603,-0.57282 0.37052,-0.894 l 0.81235,-0.19224 c 0.0996,-0.0236 0.1695,-0.11186 0.1695,-0.21394 v -0.53123 c -2e-5,-0.10206 -0.0701,-0.19099 -0.1695,-0.21446 l -0.81235,-0.1912 c -0.0745,-0.32129 -0.20099,-0.62267 -0.37052,-0.89452 l 0.43925,-0.71055 c 0.0537,-0.0871 0.0407,-0.19906 -0.0315,-0.2713 l -0.37569,-0.37517 c -0.0721,-0.0721 -0.18435,-0.0858 -0.2713,-0.032 l -0.71004,0.43873 c -0.27187,-0.16952 -0.57322,-0.29552 -0.89451,-0.37 l -0.19172,-0.81287 c -0.0235,-0.0993 -0.11201,-0.1695 -0.21394,-0.1695 z m 0.26561,1.83451 c 1.1236,0 2.03399,0.91063 2.03399,2.03399 0,1.12334 -0.91039,2.03398 -2.03399,2.03398 -1.12322,0 -2.03398,-0.91064 -2.03398,-2.03398 0,-1.12334 0.91076,-2.03399 2.03398,-2.03399 z m 0,1.16324 a 0.87043075,0.87043075 0 0 0 -0.87075,0.87075 0.87043075,0.87043075 0 0 0 0.87075,0.87023 0.87043075,0.87043075 0 0 0 0.87075,-0.87023 0.87043075,0.87043075 0 0 0 -0.87075,-0.87075 z" />