Add keyboard navigation and menu list support

This commit is contained in:
fanlusky committed 2026-06-14 19:54:34 +08:00
1 parent 0b8063299a
commit a1d4df897e
10 files changed
+602 -29

No files matched your search

+2
View File
@@ -21,6 +21,7 @@
- `TextBlock` / `Button` / `CheckBox` / `ProgressBar` / `Slider` / `Separator` / `ListView` / `TreeView` / `TabControl` / `Accordion`
- `ComboBox`
- `ContextMenuHost`
- `MenuList`
- `Card`
- `TextBox` 输入控件
@@ -54,6 +55,7 @@
- 新增第一版 `PopupHost / Popup` 弹出层系统:`Scene` 现可管理 overlay popup,支持高于主视觉树的渲染顺序、命中测试、滚轮路由、窗口重排后的重新定位,以及点击空白区域自动关闭;demo 已加入一个快速操作弹出层示例
- 新增第一版 `ComboBox`:基于新的 overlay popup 系统实现下拉选择,支持展开/收起、点击项选中、当前值回写,以及点击空白区域自动关闭;demo 已加入可交互示例
- 新增第一版 `ContextMenuHost`:窗口现可分发右键上下文菜单事件,控件可在鼠标位置弹出 scene overlay 菜单,并支持点击菜单项执行动作、点击空白区域自动关闭;demo 已加入右键菜单示例
- 新增第一版键盘导航能力:窗口现支持 `Tab / Shift+Tab` 在可聚焦控件间切换焦点,`Button` / `CheckBox` 支持 `Enter / Space` 触发,`ComboBox` 支持方向键切换,新增 `MenuList` 并用于 demo 右键菜单,支持 `Up / Down / Enter / Space`
## 构建
+19 -23
View File
@@ -442,15 +442,11 @@ std::unique_ptr<Scene> CreateDemoScene()
auto contextMenuPopupStack = std::make_shared<StackPanel>(8.0f);
contextMenuPopupStack->AddChild(std::make_shared<TextBlock>(L"Canvas Actions", 15.0f, D2D1::ColorF(0x1F2937), true));
auto contextMenuRefresh = std::make_shared<Button>(L"Refresh Preview", 38.0f);
auto contextMenuInspect = std::make_shared<Button>(L"Inspect Layout", 38.0f);
auto contextMenuPin = std::make_shared<Button>(L"Pin as Favorite", 38.0f);
contextMenuRefresh->SetWidth(180.0f);
contextMenuInspect->SetWidth(180.0f);
contextMenuPin->SetWidth(180.0f);
contextMenuPopupStack->AddChild(contextMenuRefresh);
contextMenuPopupStack->AddChild(contextMenuInspect);
contextMenuPopupStack->AddChild(contextMenuPin);
auto contextMenuList = std::make_shared<MenuList>(38.0f);
contextMenuList->AddItem(L"Refresh Preview");
contextMenuList->AddItem(L"Inspect Layout");
contextMenuList->AddItem(L"Pin as Favorite");
contextMenuPopupStack->AddChild(contextMenuList);
auto contextMenuPopup = std::make_shared<Popup>(contextMenuPopupStack);
contextMenuPopup->SetMatchAnchorWidth(false);
@@ -459,22 +455,22 @@ std::unique_ptr<Scene> CreateDemoScene()
auto contextMenuHost = std::make_shared<ContextMenuHost>(contextMenuPreview, contextMenuPopup);
std::weak_ptr<ContextMenuHost> weakContextMenuHost = contextMenuHost;
contextMenuRefresh->SetOnClick([contextMenuStatus, weakContextMenuHost]() {
contextMenuStatus->SetText(L"Last action: refresh preview");
if (auto host = weakContextMenuHost.lock())
contextMenuList->SetOnItemInvoked([contextMenuStatus, weakContextMenuHost](size_t selectedIndex, const std::wstring &value) {
(void)value;
switch (selectedIndex)
{
host->ClosePopup();
case 0:
contextMenuStatus->SetText(L"Last action: refresh preview");
break;
case 1:
contextMenuStatus->SetText(L"Last action: inspect layout");
break;
case 2:
contextMenuStatus->SetText(L"Last action: pin as favorite");
break;
default:
break;
}
});
contextMenuInspect->SetOnClick([contextMenuStatus, weakContextMenuHost]() {
contextMenuStatus->SetText(L"Last action: inspect layout");
if (auto host = weakContextMenuHost.lock())
{
host->ClosePopup();
}
});
contextMenuPin->SetOnClick([contextMenuStatus, weakContextMenuHost]() {
contextMenuStatus->SetText(L"Last action: pin as favorite");
if (auto host = weakContextMenuHost.lock())
{
host->ClosePopup();
+46
View File
@@ -72,6 +72,7 @@ class Button : public Visual
void OnFocusChanged(bool focused) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
bool OnKeyDown(WPARAM key, LPARAM lParam) override;
HCURSOR GetCursor() const override;
protected:
@@ -104,6 +105,7 @@ class Popup : public Visual
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
void AppendFocusableVisuals(std::vector<Visual *> &focusables) override;
bool HitTest(const PointF &point) const override;
void LayoutOverlay(const SizeF &viewportSize) override;
@@ -128,6 +130,7 @@ class PopupHost : public Visual
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
void AppendFocusableVisuals(std::vector<Visual *> &focusables) override;
bool HitTest(const PointF &point) const override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
@@ -158,6 +161,7 @@ class ContextMenuHost : public Visual
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
void AppendFocusableVisuals(std::vector<Visual *> &focusables) override;
bool HitTest(const PointF &point) const override;
bool OnContextMenu(const POINT &point, WPARAM keyState) override;
HCURSOR GetCursor() const override;
@@ -196,6 +200,7 @@ class ComboBox : public Visual
void OnFocusChanged(bool focused) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
bool OnKeyDown(WPARAM key, LPARAM lParam) override;
HCURSOR GetCursor() const override;
bool KeepsPopupsOpenOnClick() const override;
@@ -217,6 +222,44 @@ class ComboBox : public Visual
std::shared_ptr<Popup> popup_;
};
class MenuList : public Visual
{
public:
using ItemInvokedHandler = std::function<void(size_t index, const std::wstring &value)>;
explicit MenuList(float itemHeight = 38.0f);
void AddItem(std::wstring item);
void ClearItems();
void SetOnItemInvoked(ItemInvokedHandler handler);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
bool HitTest(const PointF &point) const override;
bool IsFocusable() const override;
void OnFocusChanged(bool focused) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
bool OnMouseMove(const POINT &point, WPARAM keyState) override;
bool OnKeyDown(WPARAM key, LPARAM lParam) override;
HCURSOR GetCursor() const override;
private:
size_t HitTestItem(const PointF &point) const;
RectF ItemRect(size_t index) const;
void MoveSelection(int delta);
void InvokeSelected();
std::vector<std::wstring> items_;
float itemHeight_ = 38.0f;
bool focused_ = false;
size_t selectedIndex_ = 0;
size_t hoveredIndex_ = static_cast<size_t>(-1);
size_t pressedIndex_ = static_cast<size_t>(-1);
ItemInvokedHandler onItemInvoked_;
};
class CheckBox : public Visual
{
public:
@@ -236,6 +279,7 @@ class CheckBox : public Visual
void OnFocusChanged(bool focused) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
bool OnKeyDown(WPARAM key, LPARAM lParam) override;
HCURSOR GetCursor() const override;
private:
@@ -435,6 +479,7 @@ class TabControl : public Visual
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
void AppendFocusableVisuals(std::vector<Visual *> &focusables) override;
bool HitTest(const PointF &point) const override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
@@ -475,6 +520,7 @@ class Accordion : public Visual
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
void AppendFocusableVisuals(std::vector<Visual *> &focusables) override;
bool HitTest(const PointF &point) const override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
+5
View File
@@ -71,6 +71,7 @@ class Visual
virtual HCURSOR GetCursor() const;
virtual void LayoutOverlay(const SizeF &viewportSize);
virtual bool KeepsPopupsOpenOnClick() const;
virtual void AppendFocusableVisuals(std::vector<Visual *> &focusables);
void InvalidateMeasure();
void InvalidateArrange();
void InvalidateVisual();
@@ -148,6 +149,7 @@ class Panel : public Visual
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
void AppendFocusableVisuals(std::vector<Visual *> &focusables) override;
protected:
std::vector<std::shared_ptr<Visual>> children_;
@@ -228,6 +230,7 @@ class ScrollViewer : public Visual
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
void AppendFocusableVisuals(std::vector<Visual *> &focusables) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
bool OnMouseMove(const POINT &point, WPARAM keyState) override;
@@ -271,6 +274,7 @@ class Grid : public Visual
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
void AppendFocusableVisuals(std::vector<Visual *> &focusables) override;
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
@@ -305,6 +309,7 @@ class Container : public Visual
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
void AppendFocusableVisuals(std::vector<Visual *> &focusables) override;
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
+2
View File
@@ -33,6 +33,8 @@ class Scene
void Render(DeviceResources &deviceResources);
Visual *FindVisualAt(const PointF &point);
Visual *FindFocusableAt(const PointF &point);
Visual *FindNextFocusable(Visual *current, bool reverse);
bool HasPopups() const;
bool OnMouseWheel(const POINT &point, short delta, WPARAM keyState);
private:
+1
View File
@@ -32,6 +32,7 @@ class Window
void InvalidateArrange();
void InvalidateArrange(Visual *source);
void Relayout();
void FocusVisual(Visual *visual);
void SetScene(std::unique_ptr<Scene> scene);
+403 -6
View File
@@ -358,6 +358,39 @@ class ComboBoxPopupContent : public Visual
return hovered != static_cast<size_t>(-1);
}
bool OnKeyDown(WPARAM key, LPARAM lParam) override
{
(void)lParam;
if (items_.empty())
{
return false;
}
if (selectedIndex_ == static_cast<size_t>(-1))
{
selectedIndex_ = 0;
}
if (key == VK_DOWN || key == VK_UP)
{
const int count = static_cast<int>(items_.size());
int index = static_cast<int>(selectedIndex_);
index = (index + (key == VK_DOWN ? 1 : -1) + count) % count;
selectedIndex_ = static_cast<size_t>(index);
hoveredIndex_ = selectedIndex_;
InvalidateVisual();
return true;
}
if ((key == VK_RETURN || key == VK_SPACE) && onSelect_)
{
onSelect_(selectedIndex_);
return true;
}
return false;
}
HCURSOR GetCursor() const override
{
return LoadCursor(nullptr, IDC_HAND);
@@ -505,6 +538,14 @@ Visual *Popup::FindFirstFocusableDescendant()
return child_ ? child_->FindFirstFocusableDescendant() : nullptr;
}
void Popup::AppendFocusableVisuals(std::vector<Visual *> &focusables)
{
if (child_)
{
child_->AppendFocusableVisuals(focusables);
}
}
bool Popup::HitTest(const PointF &point) const
{
return PointInRect(bounds_, point);
@@ -594,13 +635,20 @@ Visual *PopupHost::FindVisualAt(const PointF &point)
Visual *PopupHost::FindFocusableAt(const PointF &point)
{
(void)point;
return nullptr;
return trigger_ ? trigger_->FindFocusableAt(point) : nullptr;
}
Visual *PopupHost::FindFirstFocusableDescendant()
{
return nullptr;
return trigger_ ? trigger_->FindFirstFocusableDescendant() : nullptr;
}
void PopupHost::AppendFocusableVisuals(std::vector<Visual *> &focusables)
{
if (trigger_)
{
trigger_->AppendFocusableVisuals(focusables);
}
}
bool PopupHost::HitTest(const PointF &point) const
@@ -741,13 +789,20 @@ Visual *ContextMenuHost::FindVisualAt(const PointF &point)
Visual *ContextMenuHost::FindFocusableAt(const PointF &point)
{
(void)point;
return nullptr;
return trigger_ ? trigger_->FindFocusableAt(point) : nullptr;
}
Visual *ContextMenuHost::FindFirstFocusableDescendant()
{
return nullptr;
return trigger_ ? trigger_->FindFirstFocusableDescendant() : nullptr;
}
void ContextMenuHost::AppendFocusableVisuals(std::vector<Visual *> &focusables)
{
if (trigger_)
{
trigger_->AppendFocusableVisuals(focusables);
}
}
bool ContextMenuHost::HitTest(const PointF &point) const
@@ -804,6 +859,10 @@ void ContextMenuHost::OpenPopupAt(const PointF &anchorPoint)
InvalidateVisual();
});
open_ = true;
if (Visual *focusable = popup_->FindFirstFocusableDescendant())
{
window_->FocusVisual(focusable);
}
InvalidateVisual();
}
}
@@ -1008,6 +1067,49 @@ bool ComboBox::OnMouseUp(const POINT &point, WPARAM keyState)
return true;
}
bool ComboBox::OnKeyDown(WPARAM key, LPARAM lParam)
{
(void)lParam;
if (items_.empty())
{
return false;
}
if (key == VK_RETURN || key == VK_SPACE)
{
TogglePopup();
return true;
}
if (key == VK_ESCAPE && open_)
{
ClosePopup();
return true;
}
const int delta = key == VK_DOWN ? 1 : (key == VK_UP ? -1 : 0);
if (delta == 0)
{
return false;
}
if (selectedIndex_ == static_cast<size_t>(-1))
{
selectedIndex_ = 0;
}
else
{
const int count = static_cast<int>(items_.size());
int index = static_cast<int>(selectedIndex_);
index = (index + delta + count) % count;
selectedIndex_ = static_cast<size_t>(index);
}
SyncPopupState();
NotifySelectionChanged();
InvalidateVisual();
return true;
}
HCURSOR ComboBox::GetCursor() const
{
return LoadCursor(nullptr, IDC_HAND);
@@ -1081,6 +1183,254 @@ void ComboBox::SyncPopupState()
}
}
MenuList::MenuList(float itemHeight) : itemHeight_(itemHeight)
{
}
void MenuList::AddItem(std::wstring item)
{
items_.push_back(std::move(item));
if (selectedIndex_ >= items_.size())
{
selectedIndex_ = items_.empty() ? static_cast<size_t>(-1) : 0;
}
InvalidateMeasure();
InvalidateVisual();
}
void MenuList::ClearItems()
{
items_.clear();
selectedIndex_ = static_cast<size_t>(-1);
hoveredIndex_ = static_cast<size_t>(-1);
pressedIndex_ = static_cast<size_t>(-1);
InvalidateMeasure();
InvalidateVisual();
}
void MenuList::SetOnItemInvoked(ItemInvokedHandler handler)
{
onItemInvoked_ = std::move(handler);
}
SizeF MenuList::Measure(const SizeF &availableSize)
{
float maxWidth = 180.0f;
for (const auto &item : items_)
{
const SizeF measured =
MeasureText(GetSharedDWriteFactory(), item, 15.0f, false, std::max(availableSize.width - 28.0f, 1.0f));
maxWidth = std::max(maxWidth, measured.width + 28.0f);
}
return {std::min(availableSize.width, maxWidth), itemHeight_ * static_cast<float>(items_.size())};
}
void MenuList::Arrange(const RectF &finalRect)
{
bounds_ = finalRect;
}
void MenuList::Render(DeviceResources &deviceResources)
{
ID2D1HwndRenderTarget *target = deviceResources.GetRenderTarget();
if (!target)
{
return;
}
const Theme &theme = ThemeManager::GetCurrent();
for (size_t index = 0; index < items_.size(); ++index)
{
const RectF row = ItemRect(index);
const bool selected = index == selectedIndex_;
const bool hovered = index == hoveredIndex_;
const bool pressed = index == pressedIndex_;
if (selected || hovered || pressed)
{
FillRoundedRect(target, row, 10.0f,
pressed ? theme.primarySoftPressed : (selected ? theme.primarySoft : theme.surfaceMuted),
focused_ && selected ? theme.primaryFocusStrong : theme.border,
focused_ && selected ? 1.5f : 1.0f);
}
DrawLabel(deviceResources, items_[index], 15.0f, selected, theme.textPrimary,
{row.x + 14.0f, row.y, std::max(row.width - 28.0f, 0.0f), row.height},
DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
}
}
bool MenuList::HitTest(const PointF &point) const
{
return PointInRect(bounds_, point);
}
bool MenuList::IsFocusable() const
{
return true;
}
void MenuList::OnFocusChanged(bool focused)
{
focused_ = focused;
InvalidateVisual();
}
bool MenuList::OnMouseDown(const POINT &point, WPARAM keyState)
{
(void)keyState;
if (!window_)
{
return false;
}
pressedIndex_ = HitTestItem(window_->ClientPixelsToDips(point));
if (pressedIndex_ != static_cast<size_t>(-1))
{
selectedIndex_ = pressedIndex_;
hoveredIndex_ = pressedIndex_;
InvalidateVisual();
return true;
}
return false;
}
bool MenuList::OnMouseUp(const POINT &point, WPARAM keyState)
{
(void)keyState;
if (!window_)
{
return false;
}
const size_t pressedIndex = pressedIndex_;
pressedIndex_ = static_cast<size_t>(-1);
hoveredIndex_ = HitTestItem(window_->ClientPixelsToDips(point));
InvalidateVisual();
if (pressedIndex == static_cast<size_t>(-1) || pressedIndex != hoveredIndex_)
{
return pressedIndex != static_cast<size_t>(-1);
}
selectedIndex_ = pressedIndex;
InvokeSelected();
return true;
}
bool MenuList::OnMouseMove(const POINT &point, WPARAM keyState)
{
(void)keyState;
if (!window_)
{
return false;
}
const size_t hovered = HitTestItem(window_->ClientPixelsToDips(point));
if (hovered != hoveredIndex_)
{
hoveredIndex_ = hovered;
if (hovered != static_cast<size_t>(-1))
{
selectedIndex_ = hovered;
}
InvalidateVisual();
}
return hovered != static_cast<size_t>(-1);
}
bool MenuList::OnKeyDown(WPARAM key, LPARAM lParam)
{
(void)lParam;
if (items_.empty())
{
return false;
}
if (key == VK_DOWN)
{
MoveSelection(1);
return true;
}
if (key == VK_UP)
{
MoveSelection(-1);
return true;
}
if (key == VK_HOME)
{
selectedIndex_ = 0;
InvalidateVisual();
return true;
}
if (key == VK_END)
{
selectedIndex_ = items_.size() - 1;
InvalidateVisual();
return true;
}
if (key == VK_RETURN || key == VK_SPACE)
{
InvokeSelected();
return true;
}
return false;
}
HCURSOR MenuList::GetCursor() const
{
return LoadCursor(nullptr, IDC_HAND);
}
size_t MenuList::HitTestItem(const PointF &point) const
{
if (!HitTest(point) || items_.empty())
{
return static_cast<size_t>(-1);
}
const float localY = point.y - bounds_.y;
const size_t index = static_cast<size_t>(localY / itemHeight_);
return index < items_.size() ? index : static_cast<size_t>(-1);
}
RectF MenuList::ItemRect(size_t index) const
{
return {bounds_.x, bounds_.y + itemHeight_ * static_cast<float>(index), bounds_.width, itemHeight_};
}
void MenuList::MoveSelection(int delta)
{
if (items_.empty())
{
return;
}
if (selectedIndex_ == static_cast<size_t>(-1))
{
selectedIndex_ = 0;
}
else
{
const int count = static_cast<int>(items_.size());
int index = static_cast<int>(selectedIndex_);
index = (index + delta + count) % count;
selectedIndex_ = static_cast<size_t>(index);
}
hoveredIndex_ = selectedIndex_;
InvalidateVisual();
}
void MenuList::InvokeSelected()
{
if (onItemInvoked_ && selectedIndex_ < items_.size())
{
onItemInvoked_(selectedIndex_, items_[selectedIndex_]);
}
}
SizeF Button::Measure(const SizeF &availableSize)
{
const float textWidth = std::max(availableSize.width - kControlPaddingX * 2.0f, 1.0f);
@@ -1153,6 +1503,17 @@ bool Button::OnMouseUp(const POINT &point, WPARAM keyState)
return true;
}
bool Button::OnKeyDown(WPARAM key, LPARAM lParam)
{
(void)lParam;
if (key == VK_RETURN || key == VK_SPACE)
{
OnClick();
return true;
}
return false;
}
HCURSOR Button::GetCursor() const
{
return LoadCursor(nullptr, IDC_HAND);
@@ -1301,6 +1662,23 @@ bool CheckBox::OnMouseUp(const POINT &point, WPARAM keyState)
return true;
}
bool CheckBox::OnKeyDown(WPARAM key, LPARAM lParam)
{
(void)lParam;
if (key != VK_RETURN && key != VK_SPACE)
{
return false;
}
checked_ = !checked_;
if (onChanged_)
{
onChanged_(checked_);
}
InvalidateVisual();
return true;
}
HCURSOR CheckBox::GetCursor() const
{
return LoadCursor(nullptr, IDC_HAND);
@@ -2129,6 +2507,14 @@ Visual *TabControl::FindFirstFocusableDescendant()
return nullptr;
}
void TabControl::AppendFocusableVisuals(std::vector<Visual *> &focusables)
{
if (!tabs_.empty() && tabs_[std::min(selectedIndex_, tabs_.size() - 1)].content)
{
tabs_[std::min(selectedIndex_, tabs_.size() - 1)].content->AppendFocusableVisuals(focusables);
}
}
bool TabControl::HitTest(const PointF &point) const
{
return PointInRect(bounds_, point);
@@ -2405,6 +2791,17 @@ Visual *Accordion::FindFirstFocusableDescendant()
return nullptr;
}
void Accordion::AppendFocusableVisuals(std::vector<Visual *> &focusables)
{
for (auto &section : sections_)
{
if (section.expanded && section.content)
{
section.content->AppendFocusableVisuals(focusables);
}
}
}
bool Accordion::HitTest(const PointF &point) const
{
return PointInRect(bounds_, point);
+62
View File
@@ -209,6 +209,14 @@ bool Visual::KeepsPopupsOpenOnClick() const
return false;
}
void Visual::AppendFocusableVisuals(std::vector<Visual *> &focusables)
{
if (IsFocusable())
{
focusables.push_back(this);
}
}
void Visual::InvalidateMeasure()
{
BubbleMeasureInvalidation(this);
@@ -955,11 +963,38 @@ Visual *ScrollViewer::FindFocusableAt(const PointF &point)
return nullptr;
}
void Panel::AppendFocusableVisuals(std::vector<Visual *> &focusables)
{
for (const auto &child : children_)
{
if (child)
{
child->AppendFocusableVisuals(focusables);
}
}
if (IsFocusable())
{
focusables.push_back(this);
}
}
Visual *ScrollViewer::FindFirstFocusableDescendant()
{
return content_ ? content_->FindFirstFocusableDescendant() : nullptr;
}
void ScrollViewer::AppendFocusableVisuals(std::vector<Visual *> &focusables)
{
if (content_)
{
content_->AppendFocusableVisuals(focusables);
}
else if (IsFocusable())
{
focusables.push_back(this);
}
}
bool ScrollViewer::OnMouseDown(const POINT &point, WPARAM keyState)
{
(void)keyState;
@@ -1208,6 +1243,21 @@ Visual *Grid::FindFirstFocusableDescendant()
return nullptr;
}
void Grid::AppendFocusableVisuals(std::vector<Visual *> &focusables)
{
for (auto &child : children_)
{
if (child.visual)
{
child.visual->AppendFocusableVisuals(focusables);
}
}
if (IsFocusable())
{
focusables.push_back(this);
}
}
SizeF Grid::Measure(const SizeF &availableSize)
{
if (rowDefinitions_.empty())
@@ -1418,6 +1468,18 @@ Visual *Container::FindFirstFocusableDescendant()
return child_ ? child_->FindFirstFocusableDescendant() : nullptr;
}
void Container::AppendFocusableVisuals(std::vector<Visual *> &focusables)
{
if (child_)
{
child_->AppendFocusableVisuals(focusables);
}
else if (IsFocusable())
{
focusables.push_back(this);
}
}
SizeF Container::Measure(const SizeF &availableSize)
{
const SizeF inner = DeflateSize(availableSize, padding_);
+40
View File
@@ -374,6 +374,46 @@ Visual *Scene::FindFocusableAt(const PointF &point)
return root_ ? root_->FindFocusableAt(point) : nullptr;
}
Visual *Scene::FindNextFocusable(Visual *current, bool reverse)
{
std::vector<Visual *> focusables;
if (!popups_.empty())
{
if (popups_.back().visual)
{
popups_.back().visual->AppendFocusableVisuals(focusables);
}
}
else if (root_)
{
root_->AppendFocusableVisuals(focusables);
}
if (focusables.empty())
{
return nullptr;
}
auto it = std::find(focusables.begin(), focusables.end(), current);
if (it == focusables.end())
{
return reverse ? focusables.back() : focusables.front();
}
const ptrdiff_t index = std::distance(focusables.begin(), it);
if (reverse)
{
return focusables[(index + static_cast<ptrdiff_t>(focusables.size()) - 1) % static_cast<ptrdiff_t>(focusables.size())];
}
return focusables[(index + 1) % focusables.size()];
}
bool Scene::HasPopups() const
{
return !popups_.empty();
}
bool Scene::OnMouseWheel(const POINT &point, short delta, WPARAM keyState)
{
for (auto it = popups_.rbegin(); it != popups_.rend(); ++it)
+22
View File
@@ -189,6 +189,15 @@ void Window::Relayout()
InvalidateRect(hwnd_, nullptr, FALSE);
}
void Window::FocusVisual(Visual *visual)
{
SetFocusedVisual(visual);
if (hwnd_)
{
SetFocus(hwnd_);
}
}
void Window::SetScene(std::unique_ptr<Scene> scene)
{
scene_ = std::move(scene);
@@ -312,8 +321,21 @@ LRESULT Window::HandleMessage(UINT message, WPARAM wParam, LPARAM lParam)
break;
case WM_KEYDOWN:
if (wParam == VK_TAB && scene_)
{
const bool reverse = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
if (Visual *next = scene_->FindNextFocusable(focusedVisual_, reverse))
{
FocusVisual(next);
return 0;
}
}
if (wParam == VK_ESCAPE && focusedVisual_)
{
if (scene_ && scene_->HasPopups())
{
scene_->ClearPopups(true);
}
SetFocusedVisual(nullptr);
return 0;
}