637 lines
15 KiB
C++
637 lines
15 KiB
C++
#include "TabControl.h"
|
||
#include "SxLog.h"
|
||
#include "Window.h"
|
||
|
||
namespace
|
||
{
|
||
bool SxTabRectsIntersect(const RECT& a, const RECT& b)
|
||
{
|
||
return a.left < b.right && a.right > b.left &&
|
||
a.top < b.bottom && a.bottom > b.top;
|
||
}
|
||
|
||
RECT SxTabUnionRect(const RECT& a, const RECT& b)
|
||
{
|
||
RECT out{};
|
||
out.left = (std::min)(a.left, b.left);
|
||
out.top = (std::min)(a.top, b.top);
|
||
out.right = (std::max)(a.right, b.right);
|
||
out.bottom = (std::max)(a.bottom, b.bottom);
|
||
return out;
|
||
}
|
||
}
|
||
inline void TabControl::initTabBar()
|
||
{
|
||
if (controls.empty())return;
|
||
int butW = max(this->width / (int)controls.size(), BUTMINWIDTH);
|
||
int butH = max(this->height / (int)controls.size(), BUTMINHEIGHT);
|
||
|
||
if (this->tabPlacement == StellarX::TabPlacement::Top || this->tabPlacement == StellarX::TabPlacement::Bottom)
|
||
for (auto& c : controls)
|
||
{
|
||
c.first->setHeight(tabBarHeight);
|
||
c.first->setWidth(butW);
|
||
}
|
||
else if (this->tabPlacement == StellarX::TabPlacement::Left || this->tabPlacement == StellarX::TabPlacement::Right)
|
||
for (auto& c : controls)
|
||
{
|
||
c.first->setHeight(butH);
|
||
c.first->setWidth(tabBarHeight);
|
||
}
|
||
int i = 0;
|
||
switch (this->tabPlacement)
|
||
{
|
||
case StellarX::TabPlacement::Top:
|
||
for (auto& c : controls)
|
||
{
|
||
c.first->setX(this->x + i * butW);
|
||
c.first->setY(this->y);
|
||
i++;
|
||
}
|
||
break;
|
||
case StellarX::TabPlacement::Bottom:
|
||
for (auto& c : controls)
|
||
{
|
||
c.first->setX(this->x + i * butW);
|
||
c.first->setY(this->y + this->height - tabBarHeight);
|
||
i++;
|
||
}
|
||
break;
|
||
case StellarX::TabPlacement::Left:
|
||
for (auto& c : controls)
|
||
{
|
||
c.first->setX(this->x);
|
||
c.first->setY(this->y + i * butH);
|
||
i++;
|
||
}
|
||
break;
|
||
case StellarX::TabPlacement::Right:
|
||
for (auto& c : controls)
|
||
{
|
||
c.first->setX(this->x + this->width - tabBarHeight);
|
||
c.first->setY(this->y + i * butH);
|
||
i++;
|
||
}
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
inline void TabControl::initTabPage()
|
||
{
|
||
if (controls.empty())return;
|
||
// TabControl 内部页签页仍然保留专用布局:
|
||
// 这里负责把“当前选项卡容器矩形”拆分成页签栏和页面区,
|
||
// 不把这部分细节下放到通用布局解算器里。
|
||
// 但页面内容子树的世界坐标映射职责,继续归页面 Canvas 自己管理。
|
||
// 因此这里不再手工遍历页内子控件做 setX/setY,
|
||
// 只安置每个页面 Canvas 的矩形,让 Canvas 通过自身 relayoutManagedChildren() 收口。
|
||
switch (this->tabPlacement)
|
||
{
|
||
case StellarX::TabPlacement::Top:
|
||
for (auto& c : controls)
|
||
{
|
||
c.second->setX(this->x);
|
||
c.second->setY(this->y + tabBarHeight);
|
||
c.second->setWidth(this->width);
|
||
c.second->setHeight(this->height - tabBarHeight);
|
||
}
|
||
break;
|
||
case StellarX::TabPlacement::Bottom:
|
||
for (auto& c : controls)
|
||
{
|
||
c.second->setX(this->x);
|
||
c.second->setY(this->y);
|
||
c.second->setWidth(this->width);
|
||
c.second->setHeight(this->height - tabBarHeight);
|
||
}
|
||
break;
|
||
case StellarX::TabPlacement::Left:
|
||
for (auto& c : controls)
|
||
{
|
||
c.second->setX(this->x + tabBarHeight);
|
||
c.second->setY(this->y);
|
||
c.second->setWidth(this->width - tabBarHeight);
|
||
c.second->setHeight(this->height);
|
||
}
|
||
break;
|
||
case StellarX::TabPlacement::Right:
|
||
for (auto& c : controls)
|
||
{
|
||
c.second->setX(this->x);
|
||
c.second->setY(this->y);
|
||
c.second->setWidth(this->width - tabBarHeight);
|
||
c.second->setHeight(this->height);
|
||
}
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
void TabControl::refreshRuntimeLayout()
|
||
{
|
||
// 这是 TabControl 的内部专用布局入口:
|
||
// 外层先通过统一解算得到 TabControl 自身矩形,
|
||
// 再由这里继续安置页签按钮和页面区。
|
||
initTabBar();
|
||
initTabPage();
|
||
}
|
||
|
||
TabControl::TabControl() :Canvas()
|
||
{
|
||
this->id = "TabControl";
|
||
// TabControl 作为外层容器,当前阶段显式允许双轴 Stretch;
|
||
// 内部页签栏和页面区仍由自己的专用布局逻辑管理。
|
||
this->layoutCapability.allowStretchX = true;
|
||
this->layoutCapability.allowStretchY = true;
|
||
}
|
||
|
||
TabControl::TabControl(int x, int y, int width, int height)
|
||
: Canvas(x, y, width, height)
|
||
{
|
||
this->id = "TabControl";
|
||
this->layoutCapability.allowStretchX = true;
|
||
this->layoutCapability.allowStretchY = true;
|
||
}
|
||
|
||
TabControl::~TabControl()
|
||
{
|
||
}
|
||
|
||
void TabControl::setX(int x)
|
||
{
|
||
applyRuntimeRectDirect(x, y, width, height);
|
||
refreshRuntimeLayout();
|
||
onWindowResize();
|
||
}
|
||
|
||
void TabControl::setY(int y)
|
||
{
|
||
applyRuntimeRectDirect(x, y, width, height);
|
||
refreshRuntimeLayout();
|
||
onWindowResize();
|
||
}
|
||
|
||
void TabControl::setWidth(int width)
|
||
{
|
||
applyRuntimeRectDirect(x, y, width, height);
|
||
refreshRuntimeLayout();
|
||
onWindowResize();
|
||
}
|
||
|
||
void TabControl::setHeight(int height)
|
||
{
|
||
applyRuntimeRectDirect(x, y, width, height);
|
||
refreshRuntimeLayout();
|
||
onWindowResize();
|
||
}
|
||
|
||
void TabControl::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
|
||
{
|
||
// TabControl 作为外层控件接入统一解算;
|
||
// 但页签栏和页面区仍由自身专用逻辑继续排布。
|
||
applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height);
|
||
refreshRuntimeLayout();
|
||
}
|
||
|
||
void TabControl::draw()
|
||
{
|
||
if (!dirty || !show)return;
|
||
// 绘制画布背景和基本形状及其子画布控件
|
||
Canvas::draw();
|
||
for (auto& c : controls)
|
||
{
|
||
c.second->setDirty(true);
|
||
c.second->draw();
|
||
}
|
||
for (auto& c : controls)
|
||
{
|
||
c.first->setDirty(true);
|
||
c.first->draw();
|
||
}
|
||
|
||
// 首次绘制时处理默认激活页签
|
||
if (IsFirstDraw)
|
||
{
|
||
if (defaultActivation >= 0 && defaultActivation < (int)controls.size())
|
||
controls[defaultActivation].first->setButtonClick(true);
|
||
else if (defaultActivation >= (int)controls.size())//索引越界则激活最后一个
|
||
controls[controls.size() - 1].first->setButtonClick(true);
|
||
IsFirstDraw = false;//避免重复处理
|
||
}
|
||
dirty = false;
|
||
}
|
||
|
||
bool TabControl::handleEvent(const ExMessage& msg)
|
||
{
|
||
if (!show)return false;
|
||
resetEventVisualChanged();
|
||
bool consume = false;
|
||
bool anyVisualChanged = false;
|
||
|
||
// TabControl 的同层页签按钮/页面区事件分发顺序,
|
||
// 与 Window / Canvas 保持一致:按倒序处理,后加入者视为更靠上层。
|
||
if (msg.message == WM_MOUSEMOVE)
|
||
{
|
||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||
{
|
||
if (!consume)
|
||
{
|
||
if (it->first->handleEvent(msg))
|
||
{
|
||
consume = true;
|
||
}
|
||
if (it->first->didEventAffectVisual())
|
||
anyVisualChanged = true;
|
||
}
|
||
else if (it->first->clearTransientMouseState())
|
||
{
|
||
anyVisualChanged = true;
|
||
}
|
||
}
|
||
|
||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||
{
|
||
if (!it->second->IsVisible())
|
||
continue;
|
||
|
||
if (!consume)
|
||
{
|
||
if (it->second->handleEvent(msg))
|
||
{
|
||
consume = true;
|
||
}
|
||
if (it->second->didEventAffectVisual())
|
||
anyVisualChanged = true;
|
||
}
|
||
else if (it->second->clearTransientMouseState())
|
||
{
|
||
anyVisualChanged = true;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||
if (it->first->handleEvent(msg))
|
||
{
|
||
consume = true;
|
||
break;
|
||
}
|
||
if (!consume)
|
||
{
|
||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||
if (it->second->IsVisible())
|
||
if (it->second->handleEvent(msg))
|
||
{
|
||
consume = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (dirty)
|
||
requestRepaint(parent);
|
||
markEventVisualChanged(anyVisualChanged || dirty);
|
||
return consume;
|
||
}
|
||
|
||
bool TabControl::clearTransientMouseState()
|
||
{
|
||
bool changed = false;
|
||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||
{
|
||
if (it->first->IsVisible() && it->first->clearTransientMouseState())
|
||
changed = true;
|
||
|
||
if (it->second->IsVisible() && it->second->clearTransientMouseState())
|
||
changed = true;
|
||
}
|
||
return changed;
|
||
}
|
||
|
||
void TabControl::add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>&& control)
|
||
{
|
||
controls.push_back(std::move(control));
|
||
initTabBar();
|
||
initTabPage();
|
||
size_t idx = controls.size() - 1;
|
||
controls[idx].first->setParent(this);
|
||
controls[idx].first->enableTooltip(true);
|
||
controls[idx].first->setbuttonMode(StellarX::ButtonMode::TOGGLE);
|
||
|
||
controls[idx].first->setOnToggleOnListener([this, idx]()
|
||
{
|
||
int prevIdx = -1;
|
||
for (size_t i = 0; i < controls.size(); ++i)
|
||
{
|
||
if (controls[i].second->IsVisible())
|
||
{
|
||
prevIdx = (int)i;
|
||
break;
|
||
}
|
||
}
|
||
for (auto& tab : controls)
|
||
{
|
||
if (tab.first->getButtonText() != controls[idx].first->getButtonText() && tab.first->isClicked())
|
||
tab.first->setButtonClick(false);
|
||
}
|
||
|
||
|
||
SX_LOGI("Tab") << SX_T("激活选项卡:","activate tab: ") << prevIdx << "->" << (int)idx
|
||
<< " text=" << controls[idx].first->getButtonText();
|
||
controls[idx].second->onWindowResize();
|
||
controls[idx].second->setIsVisible(true);
|
||
dirty = true;
|
||
|
||
|
||
});
|
||
controls[idx].first->setOnToggleOffListener([this, idx]()
|
||
{
|
||
SX_LOGI("Tab") << SX_T("关闭选项卡:id=","deactivate tab: idx=") << (int)idx
|
||
<< " text=" << controls[idx].first->getButtonText();
|
||
|
||
controls[idx].second->setIsVisible(false);
|
||
dirty = true;
|
||
});
|
||
controls[idx].second->setParent(this);
|
||
controls[idx].second->setLinewidth(canvaslinewidth);
|
||
controls[idx].second->setIsVisible(false);
|
||
}
|
||
|
||
void TabControl::add(std::string tabText, std::unique_ptr<Control> control)
|
||
{
|
||
control->setDirty(true);
|
||
for (auto& tab : controls)
|
||
{
|
||
if (tab.first->getButtonText() == tabText)
|
||
{
|
||
control->setParent(tab.second.get());
|
||
control->setIsVisible(tab.second->IsVisible());
|
||
tab.second->addControl(std::move(control));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
void TabControl::setTabPlacement(StellarX::TabPlacement placement)
|
||
{
|
||
this->tabPlacement = placement;
|
||
setDirty(true);
|
||
initTabBar();
|
||
initTabPage();
|
||
}
|
||
|
||
void TabControl::setTabBarHeight(int height)
|
||
{
|
||
tabBarHeight = height;
|
||
setDirty(true);
|
||
initTabBar();
|
||
initTabPage();
|
||
}
|
||
|
||
void TabControl::setIsVisible(bool visible)
|
||
{
|
||
// 先让基类 Canvas 处理自己的回贴/丢快照逻辑
|
||
Canvas::setIsVisible(visible);
|
||
for (auto& tab : controls)
|
||
{
|
||
if(true == visible)
|
||
{
|
||
tab.first->setIsVisible(visible);
|
||
//页也要跟着关/开,否则它们会保留旧的 saveBkImage
|
||
if (tab.first->isClicked())
|
||
tab.second->setIsVisible(true);
|
||
else
|
||
tab.second->setIsVisible(false);
|
||
tab.second->setDirty(true);
|
||
}
|
||
else
|
||
{
|
||
tab.first->setIsVisible(visible);
|
||
tab.second->setIsVisible(visible);
|
||
}
|
||
}
|
||
}
|
||
|
||
void TabControl::onWindowResize()
|
||
{
|
||
// 本轮不再在 onWindowResize 中重做页签布局,
|
||
// 这里只负责失效快照、标脏,并把 resize 语义向页签按钮和页面传递。
|
||
Control::onWindowResize();
|
||
for (auto& c : controls)
|
||
{
|
||
c.first->onWindowResize();
|
||
c.second->onWindowResize();
|
||
}
|
||
dirty = true;
|
||
}
|
||
|
||
int TabControl::getActiveIndex() const
|
||
{
|
||
int idx = -1;
|
||
for (auto& c : controls)
|
||
{
|
||
idx++;
|
||
if (c.first->isClicked())
|
||
return idx;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
void TabControl::setActiveIndex(int idx)
|
||
{
|
||
if (IsFirstDraw)
|
||
defaultActivation = idx;
|
||
else
|
||
{
|
||
// 外部重复激活“已经处于激活状态”的页签时,不应再把整条 onToggleOn 链重跑一遍。
|
||
// 否则当前可见页面会重复执行 onWindowResize()/setIsVisible(true),
|
||
// 对页内像 Table 这种会越出页面边界绘制的控件,容易把快照链再次扰乱,留下残影。
|
||
if (idx >= 0 && idx < controls.size())
|
||
{
|
||
if (getActiveIndex() == idx)
|
||
return;
|
||
controls[idx].first->setButtonClick(true);
|
||
}
|
||
}
|
||
}
|
||
|
||
int TabControl::count() const
|
||
{
|
||
return (int)controls.size();
|
||
}
|
||
|
||
int TabControl::indexOf(const std::string& tabText) const
|
||
{
|
||
int idx = -1;
|
||
for (auto& c : controls)
|
||
{
|
||
idx++;
|
||
if (c.first->getButtonText() == tabText)
|
||
return idx;
|
||
}
|
||
|
||
return idx;
|
||
}
|
||
|
||
void TabControl::setDirty(bool dirty)
|
||
{
|
||
this->dirty = dirty;
|
||
for (auto& c : controls)
|
||
{
|
||
c.first->setDirty(dirty);
|
||
c.second->setDirty(dirty);
|
||
}
|
||
}
|
||
|
||
void TabControl::requestRepaint(Control* parent)
|
||
{
|
||
if (shouldDeferManagedRepaint())
|
||
{
|
||
// 托管路径:TabControl 作为“页签栏 + 当前页面”的统一重绘 root 登记到 Window。
|
||
if (auto* host = getHostWindow())
|
||
host->requestManagedRepaint(this);
|
||
return;
|
||
}
|
||
|
||
if (this == parent)
|
||
{
|
||
RECT coverage{};
|
||
bool hasCoverage = false;
|
||
auto commitTabUnit = [&](Control* unit, bool forceOverlayRedraw)
|
||
{
|
||
if (!unit || !unit->IsVisible())
|
||
return;
|
||
|
||
const bool directDirty = unit->isDirty();
|
||
const bool subtreeDirty = unit->hasManagedDirtySubtree();
|
||
|
||
if (forceOverlayRedraw)
|
||
{
|
||
// 下层单元已经写过像素,上层页签/页面作为 overlay 补画时,
|
||
// 必须先丢掉旧快照,重新抓取当前背景后再画,否则会把旧背景再贴回来。
|
||
unit->invalidateBackgroundSnapshot();
|
||
unit->setDirty(true);
|
||
unit->draw();
|
||
}
|
||
else if (directDirty)
|
||
{
|
||
unit->draw();
|
||
}
|
||
else if (subtreeDirty)
|
||
{
|
||
unit->commitManagedRepaint();
|
||
}
|
||
else
|
||
{
|
||
return;
|
||
}
|
||
|
||
const RECT rc = unit->getManagedRepaintCoverageRect();
|
||
if (!hasCoverage)
|
||
{
|
||
coverage = rc;
|
||
hasCoverage = true;
|
||
}
|
||
else
|
||
{
|
||
coverage = SxTabUnionRect(coverage, rc);
|
||
}
|
||
};
|
||
|
||
// 局部重绘必须和 draw() 维持同一套顺序:
|
||
// 先页面,再页签按钮。
|
||
// 否则页签按钮上的 Tooltip 会被后画的页面盖掉,
|
||
// 表现为“有页面打开时 Tooltip 看不到,所有页关闭时才正常”。
|
||
for (auto& control : controls)
|
||
{
|
||
Control* page = control.second.get();
|
||
if (!page->IsVisible())
|
||
continue;
|
||
|
||
if (page->hasManagedDirtySubtree())
|
||
{
|
||
commitTabUnit(page, false);
|
||
}
|
||
else if (hasCoverage && SxTabRectsIntersect(page->getManagedRepaintCoverageRect(), coverage))
|
||
{
|
||
commitTabUnit(page, true);
|
||
}
|
||
}
|
||
|
||
for (auto& control : controls)
|
||
{
|
||
Control* button = control.first.get();
|
||
if (!button->IsVisible())
|
||
continue;
|
||
|
||
if (button->hasManagedDirtySubtree())
|
||
{
|
||
commitTabUnit(button, false);
|
||
}
|
||
else if (hasCoverage && SxTabRectsIntersect(button->getManagedRepaintCoverageRect(), coverage))
|
||
{
|
||
commitTabUnit(button, true);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
onRequestRepaintAsRoot();
|
||
}
|
||
|
||
bool TabControl::hasManagedDirtySubtree() const
|
||
{
|
||
if (dirty)
|
||
return true;
|
||
|
||
for (const auto& control : controls)
|
||
{
|
||
if (control.first->IsVisible() && control.first->hasManagedDirtySubtree())
|
||
return true;
|
||
if (control.second->IsVisible() && control.second->hasManagedDirtySubtree())
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
RECT TabControl::getManagedRepaintCoverageRect() const
|
||
{
|
||
// TabControl 的 draw() 会写自身背景、全部页签按钮以及全部页面。
|
||
// 因此 coverage 也必须按“页签按钮 + 页面”递归并集,避免内部 Tooltip 等附加绘制区域被漏算。
|
||
RECT coverage = getBoundsRect();
|
||
for (const auto& control : controls)
|
||
{
|
||
if (control.first->IsVisible())
|
||
coverage = SxTabUnionRect(coverage, control.first->getManagedRepaintCoverageRect());
|
||
if (control.second->IsVisible())
|
||
coverage = SxTabUnionRect(coverage, control.second->getManagedRepaintCoverageRect());
|
||
}
|
||
return coverage;
|
||
}
|
||
|
||
bool TabControl::canCommitManagedPartialRepaint() const
|
||
{
|
||
// TabControl 只有在自己本体不脏且背景快照有效时,才允许只更新脏页签/脏页面。
|
||
return show && !dirty && hasValidBackgroundSnapshot();
|
||
}
|
||
|
||
void TabControl::commitManagedRepaint()
|
||
{
|
||
if (!show)
|
||
return;
|
||
|
||
if (canCommitManagedPartialRepaint())
|
||
{
|
||
// 页签栏和页面基线都还有效:沿用原有局部重绘逻辑。
|
||
requestRepaint(this);
|
||
return;
|
||
}
|
||
|
||
// 自身布局或背景已经变化:升级为整 TabControl 重画。
|
||
this->dirty = true;
|
||
onRequestRepaintAsRoot();
|
||
}
|