Add keyboard navigation and menu list support
This commit is contained in:
1 parent
0b8063299a
commit
a1d4df897e
10 files changed
+602
-29
No files matched your search
@@ -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`
|
||||
|
||||
## 构建
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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 §ion : sections_)
|
||||
{
|
||||
if (section.expanded && section.content)
|
||||
{
|
||||
section.content->AppendFocusableVisuals(focusables);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Accordion::HitTest(const PointF &point) const
|
||||
{
|
||||
return PointInRect(bounds_, point);
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user