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" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+