#include "SARibbonCategoryLayout.h" #include #include "SARibbonPannel.h" #include "SARibbonElementManager.h" #include "SARibbonSeparatorWidget.h" #include #include #ifndef SARibbonCategoryLayout_DEBUG_PRINT #define SARibbonCategoryLayout_DEBUG_PRINT 1 #endif /** * @brief The SARibbonCategoryLayoutPrivate class */ class SARibbonCategoryLayout::PrivateData { SA_RIBBON_DECLARE_PUBLIC(SARibbonCategoryLayout) public: PrivateData(SARibbonCategoryLayout* p); // 计算所有元素的sizehint总宽度 int totalSizeHintWidth() const; public: bool mDirty { true }; bool mIsRightScrollBtnShow { false }; ///< 标记右滚动按钮是否需要显示 bool mIsLeftScrollBtnShow { false }; ///< 标记左滚动按钮是否需要显示 SARibbonCategoryScrollButton* mLeftScrollBtn { nullptr }; ///< 在区域无法显示时显示的按钮 SARibbonCategoryScrollButton* mRightScrollBtn { nullptr }; ///< 在区域无法显示时显示的按钮 int mTotalWidth { 0 }; int mXBase { 0 }; QSize mSizeHint; QSize mMinSizeHint; QList< SARibbonCategoryLayoutItem* > mItemList; SARibbonAlignment mCategoryAlignment { SARibbonAlignment::AlignLeft }; ///< 对齐方式 }; //============================================================= // SARibbonCategoryLayoutPrivate //============================================================= SARibbonCategoryLayout::PrivateData::PrivateData(SARibbonCategoryLayout* p) : q_ptr(p) { } /** * @brief 计算所有元素的SizeHint宽度总和 * @return */ int SARibbonCategoryLayout::PrivateData::totalSizeHintWidth() const { int total = 0; QMargins mag = q_ptr->contentsMargins(); #if SA_DEBUG_PRINT_SIZE_HINT int debug_i__ = 0; QString debug_totalSizeHintWidth__; #endif if (!mag.isNull()) { total += (mag.left() + mag.right()); } // 先计算总长 for (SARibbonCategoryLayoutItem* item : qAsConst(mItemList)) { if (item->isEmpty()) { // 如果是hide就直接跳过 #if SARibbonCategoryLayout_DEBUG_PRINT && SA_DEBUG_PRINT_SIZE_HINT ++debug_i__; debug_totalSizeHintWidth__ += QString(" [%1](%2)is empty skip\n") .arg(debug_i__) .arg(item->toPannelWidget()->pannelName()); #endif continue; } // 这里要使用widget()->sizeHint(),因为pannel的标题会影总体布局,此处需要修改 // TODO QSize pannelSize = item->widget()->sizeHint(); QSize SeparatorSize(0, 0); if (item->separatorWidget) { SeparatorSize = item->separatorWidget->sizeHint(); } total += pannelSize.width(); total += SeparatorSize.width(); #if SARibbonCategoryLayout_DEBUG_PRINT && SA_DEBUG_PRINT_SIZE_HINT ++debug_i__; debug_totalSizeHintWidth__ += QString("|-[%1]pannelSize=(%2,%3),SeparatorSize=(%4,%5),name=(%6) \n") .arg(debug_i__) .arg(pannelSize.width()) .arg(pannelSize.height()) .arg(SeparatorSize.width()) .arg(SeparatorSize.height()) .arg(item->toPannelWidget()->pannelName()); #endif } #if SARibbonCategoryLayout_DEBUG_PRINT && SA_DEBUG_PRINT_SIZE_HINT qDebug() << "SARibbonCategoryLayout.totalSizeHintWidth=" << total; qDebug().noquote() << debug_totalSizeHintWidth__; #endif return (total); } //============================================================= // SARibbonCategoryLayout //============================================================= SARibbonCategoryLayout::SARibbonCategoryLayout(SARibbonCategory* parent) : QLayout(parent), d_ptr(new SARibbonCategoryLayout::PrivateData(this)) { setContentsMargins(1, 1, 1, 1); d_ptr->mLeftScrollBtn = new SARibbonCategoryScrollButton(Qt::LeftArrow, parent); d_ptr->mRightScrollBtn = new SARibbonCategoryScrollButton(Qt::RightArrow, parent); d_ptr->mLeftScrollBtn->setVisible(false); d_ptr->mRightScrollBtn->setVisible(false); connect(d_ptr->mLeftScrollBtn, &QToolButton::clicked, this, &SARibbonCategoryLayout::onLeftScrollButtonClicked); connect(d_ptr->mRightScrollBtn, &QToolButton::clicked, this, &SARibbonCategoryLayout::onRightScrollButtonClicked); } SARibbonCategoryLayout::~SARibbonCategoryLayout() { while (auto item = takePannelItem(0)) { delete item; } } SARibbonCategory* SARibbonCategoryLayout::ribbonCategory() const { return (qobject_cast< SARibbonCategory* >(parentWidget())); } void SARibbonCategoryLayout::addItem(QLayoutItem* item) { Q_UNUSED(item); qWarning() << tr("in SARibbonCategoryLayout cannot addItem,use addPannel instead"); invalidate(); } /** * @brief 返回pannel的layout * @param index 索引 * @return * @note 注意,pannel是和分割线一起的,但这个只返回一个pannel对应的layout */ QLayoutItem* SARibbonCategoryLayout::itemAt(int index) const { SARibbonCategoryLayoutItem* item = d_ptr->mItemList.value(index, nullptr); return (item); } /** * @brief 提取layout * @param index * @return */ QLayoutItem* SARibbonCategoryLayout::takeAt(int index) { QLayoutItem* r = takePannelItem(index); invalidate(); return r; } SARibbonCategoryLayoutItem* SARibbonCategoryLayout::takePannelItem(int index) { if ((index >= 0) && (index < d_ptr->mItemList.size())) { SARibbonCategoryLayoutItem* item = d_ptr->mItemList.takeAt(index); if (item->widget()) { item->widget()->hide(); } if (item->separatorWidget) { item->separatorWidget->hide(); } return (item); } return (nullptr); } SARibbonCategoryLayoutItem* SARibbonCategoryLayout::takePannelItem(SARibbonPannel* pannel) { for (int i = 0; i < d_ptr->mItemList.size(); ++i) { SARibbonCategoryLayoutItem* item = d_ptr->mItemList[ i ]; if (item->widget() == pannel) { return (takePannelItem(i)); } } return (nullptr); } /** * @brief 移除pannel,对应的分割线也会删除 * @param pannel * @return */ bool SARibbonCategoryLayout::takePannel(SARibbonPannel* pannel) { SARibbonCategoryLayoutItem* i = takePannelItem(pannel); if (i) { SARibbonSeparatorWidget* sp = i->separatorWidget; if (sp) { sp->deleteLater(); } delete i; invalidate(); return true; } return false; } int SARibbonCategoryLayout::count() const { return (d_ptr->mItemList.size()); } void SARibbonCategoryLayout::addPannel(SARibbonPannel* pannel) { insertPannel(d_ptr->mItemList.count(), pannel); } /** * @brief 插入一个pannel * @param index 索引 * @param pannel * @return 返回对应的分割线SARibbonSeparatorWidget * @note 在SARibbonCategoryLayout的布局中,一个pannel会携带一个分割线 */ void SARibbonCategoryLayout::insertPannel(int index, SARibbonPannel* pannel) { index = qMax(0, index); index = qMin(d_ptr->mItemList.count(), index); SARibbonCategoryLayoutItem* item = new SARibbonCategoryLayoutItem(pannel); // 分割线 item->separatorWidget = RibbonSubElementFactory->createRibbonSeparatorWidget(parentWidget()); // 插入list中 d_ptr->mItemList.insert(index, item); // 标记需要重新计算尺寸 invalidate(); } QSize SARibbonCategoryLayout::sizeHint() const { if (d_ptr->mSizeHint.isNull()) { SARibbonCategoryLayout* that = const_cast< SARibbonCategoryLayout* >(this); that->updateGeometryArr(); } return (d_ptr->mSizeHint); } QSize SARibbonCategoryLayout::minimumSize() const { if (d_ptr->mMinSizeHint.isNull()) { SARibbonCategoryLayout* that = const_cast< SARibbonCategoryLayout* >(this); that->updateGeometryArr(); } return (d_ptr->mMinSizeHint); } /** * @brief SARibbonCategory充满整个stacked widget * @return */ Qt::Orientations SARibbonCategoryLayout::expandingDirections() const { return (Qt::Horizontal | Qt::Vertical); } void SARibbonCategoryLayout::invalidate() { d_ptr->mDirty = true; QLayout::invalidate(); } /** * @brief category的内容尺寸(把margins减去) * @return */ QSize SARibbonCategoryLayout::categoryContentSize() const { SARibbonCategory* category = ribbonCategory(); QSize s = category->size(); QMargins mag = contentsMargins(); if (!mag.isNull()) { s.rheight() -= (mag.top() + mag.bottom()); s.rwidth() -= (mag.right() + mag.left()); } return (s); } /** * @brief 更新尺寸 */ void SARibbonCategoryLayout::updateGeometryArr() { SARibbonCategory* category = ribbonCategory(); if (nullptr == category) { return; } int categoryWidth = category->width(); QMargins mag = contentsMargins(); int height = category->height(); int y = 0; if (!mag.isNull()) { y = mag.top(); height -= (mag.top() + mag.bottom()); categoryWidth -= (mag.right() + mag.left()); } // total 是总宽,不是x坐标系,x才是坐标系 int total = d_ptr->totalSizeHintWidth(); // 扩展的宽度 int expandWidth = 0; // 如果total < categoryWidth,m_d->mXBase可以设置为0 // 判断是否超过总长度 #if SARibbonCategoryLayout_DEBUG_PRINT && SA_DEBUG_PRINT_SIZE_HINT qDebug() << "SARibbonCategoryLayout::updateGeometryArr" << "\n|-category name=" << category->categoryName() // << "\n|-category height=" << height // << "\n|-totalSizeHintWidth=" << total // << "\n|-y=" << y // << "\n|-expandWidth:" << expandWidth // << "\n|-mag=" << mag; #endif if (total > categoryWidth) { // 超过总长度,需要显示滚动按钮 if (0 == d_ptr->mXBase) { // 已经移动到最左,需要可以向右移动 d_ptr->mIsRightScrollBtnShow = true; d_ptr->mIsLeftScrollBtnShow = false; } else if (d_ptr->mXBase <= (categoryWidth - total)) { // 已经移动到最右,需要可以向左移动 d_ptr->mIsRightScrollBtnShow = false; d_ptr->mIsLeftScrollBtnShow = true; } else { // 移动到中间两边都可以动 d_ptr->mIsRightScrollBtnShow = true; d_ptr->mIsLeftScrollBtnShow = true; } } else { // 说明total 小于 categoryWidth // 记录可以扩展的数量 int canExpandingCount = 0; d_ptr->mIsRightScrollBtnShow = false; d_ptr->mIsLeftScrollBtnShow = false; // 这个是避免一开始totalWidth > categorySize.width(),通过滚动按钮调整了m_d->mBaseX // 随之调整了窗体尺寸,调整后totalWidth < categorySize.width()导致category在原来位置 // 无法显示,必须这里把mBaseX设置为0 d_ptr->mXBase = 0; // for (SARibbonCategoryLayoutItem* item : qAsConst(d_ptr->mItemList)) { if (SARibbonPannel* p = qobject_cast< SARibbonPannel* >(item->widget())) { if (p->isExpanding()) { // pannel可扩展 ++canExpandingCount; } } } // 计算可扩展的宽度 if (canExpandingCount > 0) { expandWidth = (categoryWidth - total) / canExpandingCount; } else { expandWidth = 0; } } int x = d_ptr->mXBase; if ((categoryAlignment() == SARibbonAlignment::AlignCenter) && (total < categoryWidth) && (0 == expandWidth)) { // 如果是居中对齐,同时没有伸缩的pannel,同时总宽度没有超过category的宽度 x = (categoryWidth - total) / 2; } total = 0; // total重新计算 // 先按照sizeHint设置所有的尺寸 for (SARibbonCategoryLayoutItem* item : qAsConst(d_ptr->mItemList)) { if (item->isEmpty()) { // 如果是hide就直接跳过 if (item->separatorWidget) { // pannel hide分割线也要hide item->separatorWidget->hide(); } item->mWillSetGeometry = QRect(0, 0, 0, 0); item->mWillSetSeparatorGeometry = QRect(0, 0, 0, 0); continue; } SARibbonPannel* p = item->toPannelWidget(); if (nullptr == p) { qDebug() << "unknow widget in SARibbonCategoryLayout"; continue; } // p->layout()->update(); QSize pannelSize = p->sizeHint(); QSize SeparatorSize(0, 0); if (item->separatorWidget) { SeparatorSize = item->separatorWidget->sizeHint(); } if (p->isExpanding()) { // 可扩展,就把pannel扩展到最大 pannelSize.setWidth(pannelSize.width() + expandWidth); } int w = pannelSize.width(); item->mWillSetGeometry = QRect(x, y, w, height); x += w; total += w; w = SeparatorSize.width(); item->mWillSetSeparatorGeometry = QRect(x, y, w, height); x += w; total += w; } d_ptr->mTotalWidth = total; d_ptr->mSizeHint = QSize(d_ptr->mTotalWidth, height); d_ptr->mMinSizeHint = QSize(categoryWidth, height); #if SARibbonCategoryLayout_DEBUG_PRINT && SA_DEBUG_PRINT_SIZE_HINT qDebug() << "SARibbonCategoryLayout updateGeometryArr,SizeHint=" << d_ptr->mSizeHint << ",Category name=" << category->categoryName(); #endif } /** * @brief 执行layout调整 */ void SARibbonCategoryLayout::doLayout() { if (d_ptr->mDirty) { updateGeometryArr(); } SARibbonCategory* category = ribbonCategory(); // 两个滚动按钮的位置永远不变 d_ptr->mLeftScrollBtn->setGeometry(0, 0, 12, category->height()); d_ptr->mRightScrollBtn->setGeometry(category->width() - 12, 0, 12, category->height()); QList< QWidget* > showWidgets, hideWidgets; #if SARibbonCategoryLayout_DEBUG_PRINT && SA_DEBUG_PRINT_SIZE_HINT int debug_i__(0); qDebug() << "SARibbonCategoryLayout::doLayout(),name=" << category->categoryName(); #endif for (SARibbonCategoryLayoutItem* item : qAsConst(d_ptr->mItemList)) { if (item->isEmpty()) { hideWidgets << item->widget(); if (item->separatorWidget) { hideWidgets << item->separatorWidget; } #if SARibbonCategoryLayout_DEBUG_PRINT && SA_DEBUG_PRINT_SIZE_HINT qDebug() << "|-[" << debug_i__ << "]pannelName(" << item->toPannelWidget()->pannelName() << ",will hide"; ++debug_i__; #endif } else { //! 这里不能用item->setGeometry(item->mWillSetGeometry);这样会得到一个很奇怪的显示效果 //! 就是窗口的最左边不会移出去,而是把最右边压缩, item->widget()->setGeometry(item->mWillSetGeometry); // item->widget()->setFixedSize(item->mWillSetGeometry.size()); // item->widget()->move(item->mWillSetGeometry.topLeft()); // item->setGeometry(item->mWillSetGeometry); showWidgets << item->widget(); if (item->separatorWidget) { item->separatorWidget->setGeometry(item->mWillSetSeparatorGeometry); showWidgets << item->separatorWidget; } #if SARibbonCategoryLayout_DEBUG_PRINT && SA_DEBUG_PRINT_SIZE_HINT qDebug() << "|-[" << debug_i__ << "]pannelName(" << item->toPannelWidget()->pannelName() << "),willSetGeometry:" << item->mWillSetGeometry << ",WillSetSeparatorGeometry:" << item->mWillSetSeparatorGeometry; ++debug_i__; #endif } } d_ptr->mRightScrollBtn->setVisible(d_ptr->mIsRightScrollBtnShow); d_ptr->mLeftScrollBtn->setVisible(d_ptr->mIsLeftScrollBtnShow); if (d_ptr->mIsRightScrollBtnShow) { d_ptr->mRightScrollBtn->raise(); } if (d_ptr->mIsLeftScrollBtnShow) { d_ptr->mLeftScrollBtn->raise(); } // 不在上面那里进行show和hide因为这会触发SARibbonPannelLayout的重绘,导致循环绘制,非常影响效率 for (QWidget* w : qAsConst(showWidgets)) { if (!w->isVisible()) { w->show(); } } for (QWidget* w : qAsConst(hideWidgets)) { if (w->isVisible()) { w->hide(); } } } /** * @brief 返回所有pannels * @return */ QList< SARibbonPannel* > SARibbonCategoryLayout::pannels() const { QList< SARibbonPannel* > res; for (SARibbonCategoryLayoutItem* item : qAsConst(d_ptr->mItemList)) { SARibbonPannel* p = item->toPannelWidget(); res.append(p); } return (res); } /** * @brief 通过ObjectName查找pannel * @param objname * @return */ SARibbonPannel* SARibbonCategoryLayout::pannelByObjectName(const QString& objname) const { for (SARibbonCategoryLayoutItem* item : d_ptr->mItemList) { if (SARibbonPannel* pannel = item->toPannelWidget()) { if (pannel->objectName() == objname) { return pannel; } } } return nullptr; } /** * @brief 通过名字查找pannel * @param title * @return 如果有重名,只会返回第一个符合条件的 */ SARibbonPannel* SARibbonCategoryLayout::pannelByName(const QString& pannelname) const { for (SARibbonCategoryLayoutItem* item : qAsConst(d_ptr->mItemList)) { if (SARibbonPannel* pannel = item->toPannelWidget()) { if (pannel->pannelName() == pannelname) { return (pannel); } } } return (nullptr); } /** * @brief 通过索引找到pannel,如果超过索引范围,会返回nullptr * @param i * @return */ SARibbonPannel* SARibbonCategoryLayout::pannelByIndex(int i) const { if (i >= 0 && i < d_ptr->mItemList.size()) { return d_ptr->mItemList[ i ]->toPannelWidget(); } return nullptr; } /** * @brief 移动pannel * @param from * @param to */ void SARibbonCategoryLayout::movePannel(int from, int to) { d_ptr->mItemList.move(from, to); doLayout(); } /** * @brief 返回pannel的个数 * @return */ int SARibbonCategoryLayout::pannelCount() const { return d_ptr->mItemList.size(); } /** * @brief 查找pannel对应的索引 * @param p * @return 如果找不到,返回-1 */ int SARibbonCategoryLayout::pannelIndex(SARibbonPannel* p) const { int c = pannelCount(); for (int i = 0; i < c; ++i) { if (d_ptr->mItemList[ i ]->toPannelWidget() == p) { return (i); } } return (-1); } /** * @brief 获取所有的pannel * @return */ QList< SARibbonPannel* > SARibbonCategoryLayout::pannelList() const { QList< SARibbonPannel* > res; for (SARibbonCategoryLayoutItem* i : qAsConst(d_ptr->mItemList)) { if (SARibbonPannel* p = i->toPannelWidget()) { res.append(p); } } return (res); } /** * @brief 执行滚动 * @param px */ void SARibbonCategoryLayout::scroll(int px) { QSize contentSize = categoryContentSize(); d_ptr->mXBase += px; if (d_ptr->mXBase > 0) { d_ptr->mXBase = 0; } else if ((d_ptr->mXBase + d_ptr->mTotalWidth) < contentSize.width()) { d_ptr->mXBase = contentSize.width() - d_ptr->mTotalWidth; } invalidate(); } /** * @brief 判断是否滚动过 * @return */ bool SARibbonCategoryLayout::isScrolled() const { return (d_ptr->mXBase != 0); } /** * @brief 这个宽度是实际内容的宽度,有可能大于size.width,也有可能小于 * @return */ int SARibbonCategoryLayout::categoryTotalWidth() const { return d_ptr->mTotalWidth; } /** @brief 设置Category的对齐方式 居中对齐会让pannel以居中进行对齐 @param al */ void SARibbonCategoryLayout::setCategoryAlignment(SARibbonAlignment al) { d_ptr->mCategoryAlignment = al; } /** @brief Category的对齐方式 @return */ SARibbonAlignment SARibbonCategoryLayout::categoryAlignment() const { return d_ptr->mCategoryAlignment; } void SARibbonCategoryLayout::onLeftScrollButtonClicked() { SARibbonCategory* category = qobject_cast< SARibbonCategory* >(parentWidget()); int width = category->width(); // 求总宽 int totalWidth = d_ptr->mTotalWidth; if (totalWidth > width) { int tmp = d_ptr->mXBase + width; if (tmp > 0) { tmp = 0; } d_ptr->mXBase = tmp; } else { d_ptr->mXBase = 0; } invalidate(); } void SARibbonCategoryLayout::onRightScrollButtonClicked() { SARibbonCategory* category = qobject_cast< SARibbonCategory* >(parentWidget()); int width = category->width(); // 求总宽 int totalWidth = d_ptr->mTotalWidth; if (totalWidth > width) { int tmp = d_ptr->mXBase - width; if (tmp < (width - totalWidth)) { tmp = width - totalWidth; } d_ptr->mXBase = tmp; } else { d_ptr->mXBase = 0; } invalidate(); } void SARibbonCategoryLayout::setGeometry(const QRect& rect) { QRect old = geometry(); if (old == rect) { return; } #if SARibbonCategoryLayout_DEBUG_PRINT && SA_DEBUG_PRINT_SIZE_HINT qDebug() << "===========SARibbonCategoryLayout.setGeometry(" << rect << "(" << ribbonCategory()->categoryName() << ")======="; #endif QLayout::setGeometry(rect); d_ptr->mDirty = false; updateGeometryArr(); doLayout(); } //============================================================= // SARibbonCategoryLayoutItem //============================================================= SARibbonCategoryLayoutItem::SARibbonCategoryLayoutItem(SARibbonPannel* w) : QWidgetItem(w) { separatorWidget = nullptr; } SARibbonPannel* SARibbonCategoryLayoutItem::toPannelWidget() { return qobject_cast< SARibbonPannel* >(widget()); }