From f482d3c85742a25ede6cd8d45550e9ebef50167e Mon Sep 17 00:00:00 2001 From: Henrik Rydgard Date: Sat, 25 May 2013 12:40:57 +0200 Subject: [PATCH] NewUI: Add basic keyboard focus movement --- ui/view.cpp | 47 ++++++++++++++---- ui/view.h | 121 ++++++++++++++++++++++++++++++++++++++++------- ui/viewgroup.cpp | 80 +++++++++++++++++++++++++++---- ui/viewgroup.h | 23 ++++----- 4 files changed, 223 insertions(+), 48 deletions(-) diff --git a/ui/view.cpp b/ui/view.cpp index bd0cad34fb..39af395ff9 100644 --- a/ui/view.cpp +++ b/ui/view.cpp @@ -9,6 +9,7 @@ namespace UI { static View *focusedView; +static bool focusMovementEnabled; const float ITEM_HEIGHT = 48.f; @@ -20,6 +21,14 @@ void SetFocusedView(View *view) { focusedView = view; } +void EnableFocusMovement(bool enable) { + focusMovementEnabled = true; +} + +bool IsFocusMovementEnabled() { + return focusMovementEnabled; +} + void MeasureBySpec(Size sz, float contentWidth, MeasureSpec spec, float *measured) { *measured = sz; if (sz == WRAP_CONTENT) { @@ -64,7 +73,6 @@ void Event::Update() { } } - void View::Measure(const DrawContext &dc, MeasureSpec horiz, MeasureSpec vert) { float contentW = 0.0f, contentH = 0.0f; GetContentDimensions(dc, contentW, contentH); @@ -88,7 +96,8 @@ void Clickable::Click() { void Clickable::Touch(const TouchInput &input) { if (input.flags & (TOUCH_DOWN | TOUCH_MOVE)) { if (bounds_.Contains(input.x, input.y)) { - SetFocusedView(this); + if (IsFocusMovementEnabled()) + SetFocusedView(this); down_ = true; } else { down_ = false; @@ -102,10 +111,18 @@ void Clickable::Touch(const TouchInput &input) { } } -void Choice::GetContentDimensions(const DrawContext &dc, float &w, float &h) const { - // Will be sized to fill parent horizontally. - w = 0.0f; - h = ITEM_HEIGHT; +void Clickable::Update(const InputState &input_state) { + if (!HasFocus()) + return; + if (input_state.pad_buttons_down & PAD_BUTTON_A) { + down_ = true; + } else if (input_state.pad_buttons_up & PAD_BUTTON_A) { + if (down_) { + UI::EventParams e; + OnClick.Trigger(e); + } + down_ = false; + } } void Choice::Draw(DrawContext &dc) { @@ -115,15 +132,27 @@ void Choice::Draw(DrawContext &dc) { // dc.draw->DrawText(dc.theme->uiFontSmaller, text_.c_str(), paddingX, paddingY, 0xFFFFFFFF, ALIGN_TOPLEFT); } +void CheckBox::Draw(DrawContext &dc) { + int paddingX = 80; + int paddingY = 4; + dc.draw->DrawImage(dc.theme->checkOn, bounds_.x + 30, bounds_.centerY(), 0xFFFFFFFF, ALIGN_VCENTER); + dc.draw->DrawText(dc.theme->uiFont, text_.c_str(), bounds_.x + paddingX, bounds_.y + paddingY, 0xFFFFFFFF, ALIGN_TOPLEFT); + // dc.draw->DrawText(dc.theme->uiFontSmaller, text_.c_str(), paddingX, paddingY, 0xFFFFFFFF, ALIGN_TOPLEFT); +} + void Button::GetContentDimensions(const DrawContext &dc, float &w, float &h) const { dc.draw->MeasureText(dc.theme->uiFont, text_.c_str(), &w, &h); } void Button::Draw(DrawContext &dc) { int image = down_ ? dc.theme->buttonImage : dc.theme->buttonSelected; - - dc.draw->DrawImage4Grid(dc.theme->buttonImage, bounds_.x, bounds_.y, bounds_.x2(), bounds_.y2(), style_.bgColor); - dc.draw->DrawText(dc.theme->uiFont, text_.c_str(), bounds_.centerX(), bounds_.centerY(), style_.fgColor, ALIGN_CENTER); + + Style style = dc.theme->buttonStyle; + if (HasFocus()) style = dc.theme->buttonFocusedStyle; + if (down_) style = dc.theme->buttonDownStyle; + + dc.draw->DrawImage4Grid(dc.theme->buttonImage, bounds_.x, bounds_.y, bounds_.x2(), bounds_.y2(), style.bgColor); + dc.draw->DrawText(dc.theme->uiFont, text_.c_str(), bounds_.centerX(), bounds_.centerY(), style.fgColor, ALIGN_CENTER); } void ImageView::GetContentDimensions(const DrawContext &dc, float &w, float &h) const { diff --git a/ui/view.h b/ui/view.h index 50d0ea1d87..1944464876 100644 --- a/ui/view.h +++ b/ui/view.h @@ -16,6 +16,7 @@ #include "math/lin/matrix4x4.h" struct TouchInput; +struct InputState; class DrawBuffer; class DrawContext; @@ -23,6 +24,15 @@ class DrawContext; // I don't generally like namespaces but I think we do need one for UI, so many potentially-clashing names. namespace UI { +class View; + +// The ONLY global is the currently focused item. +// Can be and often is null. +void EnableFocusMovement(bool enable); +bool IsFocusMovementEnabled(); +View *GetFocusedView(); +void SetFocusedView(View *view); + enum DrawableType { DRAW_NOTHING, DRAW_SOLID_COLOR, @@ -52,7 +62,20 @@ struct Theme { int buttonSelected; int checkOn; int checkOff; + Style buttonStyle; + Style buttonFocusedStyle; + Style buttonDownStyle; +}; + +// The four cardinal directions should be enough, plus Prev/Next in "element order". +enum FocusDirection { + FOCUS_UP, + FOCUS_DOWN, + FOCUS_LEFT, + FOCUS_RIGHT, + FOCUS_NEXT, + FOCUS_PREV, }; enum { @@ -118,10 +141,8 @@ struct Bounds { float h; }; - void Fill(DrawContext &dc, const Bounds &bounds, const Drawable &drawable); - struct MeasureSpec { MeasureSpec(MeasureSpecType t, float s = 0.0f) : type(t), size(s) {} MeasureSpec() : type(UNSPECIFIED), size(0) {} @@ -232,6 +253,7 @@ public: // Can even be called on a different thread! This is to really minimize latency, and decouple // touch response from the frame rate. virtual void Touch(const TouchInput &input) = 0; + virtual void Update(const InputState &input_state) = 0; void Move(Bounds bounds) { bounds_ = bounds; @@ -253,9 +275,17 @@ public: virtual const LayoutParams *GetLayoutParams() const { return layoutParams_.get(); } const Bounds &GetBounds() const { return bounds_; } + virtual bool SetFocus() { + if (CanBeFocused()) { + SetFocusedView(this); + return true; + } + return false; + } + virtual void MoveFocus(FocusDirection direction) {} virtual bool CanBeFocused() const { return true; } - bool Focused() const { + bool HasFocus() const { return GetFocusedView() == this; } @@ -283,6 +313,8 @@ public: : View(layoutParams) {} virtual void Touch(const TouchInput &input) {} + virtual bool CanBeFocused() const { return false; } + virtual void Update(const InputState &input_state) {} }; @@ -290,9 +322,10 @@ public: class Clickable : public View { public: Clickable(LayoutParams *layoutParams) - : View(layoutParams) {} + : View(layoutParams), down_(false) {} virtual void Touch(const TouchInput &input); + virtual void Update(const InputState &input_state); Event OnClick; @@ -319,38 +352,93 @@ private: DISALLOW_COPY_AND_ASSIGN(Button); }; +class Item : public InertView { +public: + Item(LayoutParams *layoutParams) : InertView(layoutParams) { + layoutParams_->width = FILL_PARENT; + layoutParams_->height = 80; + } + + virtual void GetContentDimensions(const DrawContext &dc, float &w, float &h) const { + w = 0.0f; + h = 0.0f; + } +}; + +class ClickableItem : public Clickable { +public: + ClickableItem(LayoutParams *layoutParams) : Clickable(layoutParams) { + layoutParams_->width = FILL_PARENT; + layoutParams_->height = 80; + } + + virtual void GetContentDimensions(const DrawContext &dc, float &w, float &h) const { + w = 0.0f; + h = 0.0f; + } +}; + + + // The following classes are mostly suitable as items in ListView which // really is just a LinearLayout in a ScrollView, possibly with some special optimizations. // Use to trigger something or open a submenu screen. -class Choice : public Clickable { +class Choice : public ClickableItem { public: Choice(const std::string &text, const std::string &smallText = "", LayoutParams *layoutParams = 0) - : Clickable(layoutParams), text_(text), smallText_(smallText) {} + : ClickableItem(layoutParams), text_(text), smallText_(smallText) {} virtual void Draw(DrawContext &dc); - virtual void GetContentDimensions(const DrawContext &dc, float &w, float &h) const; private: std::string text_; std::string smallText_; }; +class InfoItem : public Item { +public: + InfoItem(const std::string &text, const std::string &rightText, LayoutParams *layoutParams = 0) + : Item(layoutParams), text_(text), rightText_(rightText) {} + + virtual void Draw(DrawContext &dc); + +private: + std::string text_; + std::string rightText_; +}; + class CheckBox : public Clickable { public: - CheckBox(const std::string &text, const std::string &smallText = "", LayoutParams *layoutParams = 0) - : Clickable(layoutParams), text_(text), smallText_(smallText) {} + CheckBox(bool *toggle, const std::string &text, const std::string &smallText = "", LayoutParams *layoutParams = 0) + : Clickable(layoutParams), text_(text), smallText_(smallText) { + OnClick.Add(std::bind(&CheckBox::OnClicked, this, std::placeholders::_1)); + } - virtual void GetContentDimensions(const DrawContext &dc, float &w, float &h) const; virtual void Draw(DrawContext &dc); + EventReturn OnClicked(const EventParams &e) { + if (toggle_) + *toggle_ = !(*toggle_); + return EVENT_DONE; + } private: + bool *toggle_; std::string text_; std::string smallText_; }; -enum ImageSizeMode { +// These are for generic use. + +class Spacer : public InertView { +public: + Spacer(LayoutParams *layoutParams = 0) + : InertView(layoutParams) {} + virtual void GetContentDimensions(const DrawContext &dc, float &w, float &h) { + w = 0.0f; h = 0.0f; + } + virtual void Draw(DrawContext &dc) {} }; class TextView : public InertView { @@ -360,13 +448,16 @@ public: virtual void GetContentDimensions(const DrawContext &dc, float &w, float &h) const; virtual void Draw(DrawContext &dc); - virtual bool CanBeFocused() const { return false; } private: std::string text_; int font_; }; +enum ImageSizeMode { + IS_DEFAULT, +}; + class ImageView : public InertView { public: ImageView(int atlasImage, ImageSizeMode sizeMode, LayoutParams *layoutParams = 0) @@ -374,7 +465,6 @@ public: virtual void GetContentDimensions(const DrawContext &dc, float &w, float &h) const; virtual void Draw(DrawContext &dc); - virtual bool CanBeFocused() const { return false; } private: int atlasImage_; @@ -406,11 +496,6 @@ private: std::vector tabs_; };*/ -// The ONLY global is the currently focused item. -// Can be and often is null. - -View *GetFocusedView(); -void SetFocusedView(View *view); void MeasureBySpec(Size sz, float contentWidth, MeasureSpec spec, float *measured); } // namespace \ No newline at end of file diff --git a/ui/viewgroup.cpp b/ui/viewgroup.cpp index 466ebfe15a..26c5501c30 100644 --- a/ui/viewgroup.cpp +++ b/ui/viewgroup.cpp @@ -42,6 +42,39 @@ void ViewGroup::Draw(DrawContext &dc) { } } +void ViewGroup::Update(const InputState &input_state) { + for (auto iter = views_.begin(); iter != views_.end(); ++iter) { + // TODO: If there is a transformation active, transform input coordinates accordingly. + (*iter)->Update(input_state); + } +} + +bool ViewGroup::SetFocus() { + if (!CanBeFocused() && !views_.empty()) { + for (size_t i = 0; i < views_.size(); i++) { + if (views_[i]->SetFocus()) + return true; + } + } + return false; +} + +void ViewGroup::MoveFocus(FocusDirection direction) { + if (!GetFocusedView()) { + SetFocus(); + return; + } + + View *neighbor = FindNeighbor(GetFocusedView(), direction); + if (neighbor) { + neighbor->SetFocus(); + } else { + for (auto iter = views_.begin(); iter != views_.end(); ++iter) { + (*iter)->MoveFocus(direction); + } + } +} + View *ViewGroup::FindNeighbor(View *view, FocusDirection direction) { // First, find the position of the view in the list. size_t num = -1; @@ -74,15 +107,9 @@ View *ViewGroup::FindNeighbor(View *view, FocusDirection direction) { } } -void FrameLayout::Measure(const DrawContext &dc, MeasureSpec horiz, MeasureSpec vert) { - if (views_.empty()) { - ELOG("A FrameLayout must have a child view"); - return; - } -} void LinearLayout::Measure(const DrawContext &dc, MeasureSpec horiz, MeasureSpec vert) { - if (!views_.size()) { + if (views_.empty()) { MeasureBySpec(layoutParams_->width, 0.0f, horiz, &measuredWidth_); MeasureBySpec(layoutParams_->height, 0.0f, vert, &measuredHeight_); return; @@ -215,8 +242,26 @@ void LinearLayout::Layout() { } } +void FrameLayout::Measure(const DrawContext &dc, MeasureSpec horiz, MeasureSpec vert) { + if (views_.empty()) { + MeasureBySpec(layoutParams_->width, 0.0f, horiz, &measuredWidth_); + MeasureBySpec(layoutParams_->height, 0.0f, vert, &measuredHeight_); + return; + } + + for (size_t i = 0; i < views_.size(); i++) { + views_[i]->Measure(dc, horiz, vert); + } +} + +void FrameLayout::Layout() { + +} + void ScrollView::Measure(const DrawContext &dc, MeasureSpec horiz, MeasureSpec vert) { - + // The scroll view itself simply obeys its parent. + MeasureBySpec(layoutParams_->width, 0.0f, horiz, &measuredWidth_); + MeasureBySpec(layoutParams_->height, 0.0f, vert, &measuredHeight_); } void ScrollView::Layout() { @@ -246,6 +291,7 @@ void ScrollView::Touch(const TouchInput &input) { } } + void GridLayout::Layout() { } @@ -271,4 +317,22 @@ void LayoutViewHierarchy(const DrawContext &dc, ViewGroup *root) { root->Layout(); } +void UpdateViewHierarchy(const InputState &input_state, ViewGroup *root) { + if (input_state.pad_buttons_down & (PAD_BUTTON_LEFT | PAD_BUTTON_RIGHT | PAD_BUTTON_UP | PAD_BUTTON_DOWN)) + EnableFocusMovement(true); + + if (input_state.pad_last_buttons == 0) { + if (input_state.pad_buttons_down & PAD_BUTTON_RIGHT) + root->MoveFocus(FOCUS_RIGHT); + if (input_state.pad_buttons_down & PAD_BUTTON_UP) + root->MoveFocus(FOCUS_UP); + if (input_state.pad_buttons_down & PAD_BUTTON_LEFT) + root->MoveFocus(FOCUS_LEFT); + if (input_state.pad_buttons_down & PAD_BUTTON_DOWN) + root->MoveFocus(FOCUS_DOWN); + } + + root->Update(input_state); +} + } // namespace UI \ No newline at end of file diff --git a/ui/viewgroup.h b/ui/viewgroup.h index cbd43aabd4..68d75d1c43 100644 --- a/ui/viewgroup.h +++ b/ui/viewgroup.h @@ -5,16 +5,6 @@ namespace UI { -// The four cardinal directions should be enough, plus Prev/Next in "element order". -enum FocusDirection { - FOCUS_UP, - FOCUS_DOWN, - FOCUS_LEFT, - FOCUS_RIGHT, - FOCUS_NEXT, - FOCUS_PREV, -}; - class ViewGroup : public View { public: ViewGroup(LayoutParams *layoutParams = 0) : View(layoutParams) {} @@ -26,6 +16,7 @@ public: // By default, a container will layout to its own bounds. virtual void Measure(const DrawContext &dc, MeasureSpec horiz, MeasureSpec vert) = 0; virtual void Layout() = 0; + virtual void Update(const InputState &input_state); virtual void Draw(DrawContext &dc); @@ -36,9 +27,14 @@ public: // Takes ownership! DO NOT add a view to multiple parents! void Add(View *view) { views_.push_back(view); } + virtual bool SetFocus(); + virtual void MoveFocus(FocusDirection direction); + // Assumes that layout has taken place. View *FindNeighbor(View *view, FocusDirection direction); + virtual bool CanBeFocused() const { return false; } + protected: std::vector views_; }; @@ -72,15 +68,15 @@ private: class GridLayout : public ViewGroup { public: - GridLayout(Orientation orientation, int colsOrRows) : - orientation_(orientation), colsOrRows_(colsOrRows) {} + GridLayout(Orientation orientation, int numPerLine) : + orientation_(orientation), numPerLine_(numPerLine) {} void Measure(const DrawContext &dc, MeasureSpec horiz, MeasureSpec vert); void Layout(); private: Orientation orientation_; - int colsOrRows_; + int numPerLine_; }; // A scrollview usually contains just a single child - a linear layout or similar. @@ -106,5 +102,6 @@ public: }; void LayoutViewHierarchy(const DrawContext &dc, ViewGroup *root); +void UpdateViewHierarchy(const InputState &input_state, ViewGroup *root); } // namespace UI \ No newline at end of file