File indexing completed on 2024-05-19 05:42:28
0001 // ct_lvtqtw_toolbox.cpp -*-C++-*- 0002 0003 /* 0004 SPDX-FileCopyrightText: 2007 Vladimir Kuznetsov <ks.vladimir@gmail.com> 0005 // Copyright 2023 Codethink Ltd <codethink@codethink.co.uk> 0006 // SPDX-License-Identifier: Apache-2.0 0007 // 0008 // Licensed under the Apache License, Version 2.0 (the "License"); 0009 // you may not use this file except in compliance with the License. 0010 // You may obtain a copy of the License at 0011 // 0012 // http://www.apache.org/licenses/LICENSE-2.0 0013 // 0014 // Unless required by applicable law or agreed to in writing, software 0015 // distributed under the License is distributed on an "AS IS" BASIS, 0016 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 0017 // See the License for the specific language governing permissions and 0018 // limitations under the License. 0019 */ 0020 0021 #include <ct_lvtqtw_toolbox.h> 0022 0023 #include <ct_lvtqtc_itool.h> 0024 0025 #include <preferences.h> 0026 0027 #include <QActionGroup> 0028 #include <QApplication> 0029 #include <QButtonGroup> 0030 #include <QDebug> 0031 #include <QEvent> 0032 #include <QIcon> 0033 #include <QPaintEvent> 0034 #include <QPainter> 0035 #include <QScrollArea> 0036 #include <QScrollBar> 0037 #include <QStyleOption> 0038 #include <QStylePainter> 0039 #include <QToolButton> 0040 #include <QVBoxLayout> 0041 0042 class QPaintEvent; 0043 0044 using namespace Codethink::lvtqtw; 0045 0046 namespace { 0047 0048 void applyStyleSheetHack(QToolButton *btn) 0049 { 0050 #if defined __APPLE__ 0051 static const QString buttonCss = R"( 0052 QToolButton { 0053 border: none; 0054 } 0055 0056 QToolButton:pressed { 0057 border: none; 0058 background-color: gray; 0059 } 0060 0061 QToolButton:checked { 0062 border: none; 0063 background-color: gray; 0064 } 0065 0066 QToolButton:hover { 0067 border: none; 0068 background-color: lightgray; 0069 } 0070 )"; 0071 btn->setStyleSheet(buttonCss); 0072 #else 0073 Q_UNUSED(btn); 0074 #endif 0075 } 0076 0077 class Separator : public QWidget { 0078 public: 0079 explicit Separator(QString text, QWidget *parent): QWidget(parent), d_text(std::move(text)) 0080 { 0081 setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); 0082 setProperty("isSeparator", true); 0083 } 0084 0085 [[nodiscard]] QSize sizeHint() const override 0086 { 0087 QStyleOption opt; 0088 opt.initFrom(this); 0089 const int extent = style()->pixelMetric(QStyle::PM_ToolBarSeparatorExtent, &opt, parentWidget()); 0090 0091 QFontMetrics fm(font()); 0092 const int textHeight = fm.height(); 0093 0094 return {extent, textHeight}; 0095 } 0096 0097 void paintEvent(QPaintEvent *ev) override 0098 { 0099 Q_UNUSED(ev); 0100 QPainter p(this); 0101 QStyleOption opt; 0102 opt.initFrom(this); 0103 0104 QFontMetrics fm(font()); 0105 const int textWidth = fm.horizontalAdvance(d_text); 0106 constexpr int SPACER = 5; 0107 0108 const int line1X = rect().x(); 0109 const int lineWidth = (rect().width() / 2) - (textWidth / 2) - SPACER; 0110 const int line2X = rect().x() + lineWidth + textWidth + (SPACER * 2); 0111 0112 QRect origRect = opt.rect; 0113 // Too small to write the name of the separator, just draw the line. 0114 if (d_text.isEmpty()) { 0115 // noop. don't draw anything. 0116 } else if (origRect.width() < textWidth + (SPACER * 2)) { 0117 style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &opt, &p, parentWidget()); 0118 } 0119 // Draw two smaller lines on the side, and the name of the separator in 0120 // the middle. 0121 else { 0122 const QBrush textColor = qApp->palette().text(); 0123 opt.rect = QRect(line1X, rect().y(), lineWidth, rect().height()); 0124 style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &opt, &p, parentWidget()); 0125 0126 p.setPen(QPen(textColor, 1)); 0127 p.setBrush(textColor); 0128 p.drawText(rect(), Qt::AlignCenter, d_text); 0129 0130 opt.rect = QRect(line2X, rect().y(), lineWidth, rect().height()); 0131 style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &opt, &p, parentWidget()); 0132 } 0133 } 0134 0135 private: 0136 QString d_text; 0137 }; 0138 0139 class ToolBoxLayout : public QLayout { 0140 public: 0141 explicit ToolBoxLayout(QWidget *parent, int margin = 0, int spacing = -1): QLayout(parent) 0142 { 0143 setContentsMargins(margin, margin, margin, margin); 0144 setSpacing(spacing); 0145 resetCache(); 0146 } 0147 0148 ~ToolBoxLayout() override 0149 { 0150 qDeleteAll(d_itemList); 0151 } 0152 0153 void addItem(QLayoutItem *item) override 0154 { 0155 d_itemList.append(item); 0156 resetCache(); 0157 } 0158 0159 int count() const override 0160 { 0161 return d_itemList.size(); 0162 } 0163 0164 QLayoutItem *itemAt(int index) const override 0165 { 0166 return d_itemList.value(index); 0167 } 0168 0169 QLayoutItem *takeAt(int index) override 0170 { 0171 resetCache(); 0172 if (index >= 0 && index < d_itemList.size()) { 0173 return d_itemList.takeAt(index); 0174 } 0175 return nullptr; 0176 } 0177 0178 Qt::Orientations expandingDirections() const override 0179 { 0180 return Qt::Vertical | Qt::Horizontal; 0181 } 0182 0183 bool hasHeightForWidth() const override 0184 { 0185 return true; 0186 } 0187 0188 int heightForWidth(int width) const override 0189 { 0190 if (d_isCachedHeightForWidth && d_cachedHeightForWidth.width() == width) { 0191 return d_cachedHeightForWidth.height(); 0192 } 0193 d_cachedHeightForWidth.setWidth(width); 0194 d_cachedHeightForWidth.setHeight(doLayout(QRect(0, 0, width, 0), true)); 0195 d_isCachedHeightForWidth = true; 0196 return d_cachedHeightForWidth.height(); 0197 } 0198 0199 void setGeometry(const QRect& rect) override 0200 { 0201 resetCache(); 0202 QLayout::setGeometry(rect); 0203 doLayout(rect, false); 0204 } 0205 0206 QSize sizeHint() const override 0207 { 0208 return minimumSize(); 0209 } 0210 0211 QSize minimumSize() const override 0212 { 0213 if (d_isCachedMinimumSize) { 0214 return d_cachedMinimumSize; 0215 } 0216 0217 d_cachedMinimumSize = QSize(); 0218 for (QLayoutItem *item : d_itemList) { 0219 d_cachedMinimumSize = d_cachedMinimumSize.expandedTo(item->minimumSize()); 0220 } 0221 d_isCachedMinimumSize = true; 0222 return d_cachedMinimumSize; 0223 } 0224 0225 void setOneLine(bool b) 0226 { 0227 d_oneLine = b; 0228 invalidate(); 0229 } 0230 0231 bool isOneLine() const 0232 { 0233 return d_oneLine; 0234 } 0235 0236 void invalidate() override 0237 { 0238 resetCache(); 0239 QLayout::invalidate(); 0240 } 0241 0242 protected: 0243 void resetCache() 0244 { 0245 d_isCachedMinimumSize = false; 0246 d_isCachedHeightForWidth = false; 0247 } 0248 0249 int doLayout(const QRect& rect, bool testOnly) const 0250 { 0251 int x = rect.x(); 0252 int y = rect.y(); 0253 int lineHeight = 0; 0254 0255 if (d_oneLine) { 0256 for (QLayoutItem *item : d_itemList) { 0257 y = y + lineHeight + spacing(); 0258 lineHeight = item->sizeHint().height(); 0259 if (!testOnly) { 0260 item->setGeometry(QRect(rect.x(), y, rect.width(), lineHeight)); 0261 } 0262 } 0263 } else { 0264 for (QLayoutItem *item : d_itemList) { 0265 int w = item->sizeHint().width(); 0266 int h = item->sizeHint().height(); 0267 int nextX = x + item->sizeHint().width() + spacing(); 0268 if (item->widget() && item->widget()->property("isSeparator").toBool()) { 0269 x = rect.x(); 0270 y = y + lineHeight + spacing(); 0271 nextX = x + rect.width(); 0272 w = rect.width(); 0273 lineHeight = 0; 0274 } else if (nextX - spacing() > rect.right() && lineHeight > 0) { 0275 x = rect.x(); 0276 y = y + lineHeight + spacing(); 0277 nextX = x + w + spacing(); 0278 lineHeight = 0; 0279 } 0280 0281 if (!testOnly) { 0282 item->setGeometry(QRect(x, y, w, h)); 0283 } 0284 0285 x = nextX; 0286 lineHeight = qMax(lineHeight, h); 0287 } 0288 } 0289 return y + lineHeight - rect.y(); 0290 } 0291 0292 private: 0293 QList<QLayoutItem *> d_itemList; 0294 bool d_oneLine = false; 0295 0296 mutable bool d_isCachedMinimumSize = false; 0297 mutable bool d_isCachedHeightForWidth = true; 0298 mutable QSize d_cachedMinimumSize; 0299 mutable QSize d_cachedHeightForWidth; 0300 }; 0301 0302 class LayoutToolButton : public QToolButton { 0303 public: 0304 LayoutToolButton(QWidget *parent = nullptr): QToolButton(parent) 0305 { 0306 } 0307 0308 protected: 0309 void paintEvent(QPaintEvent *event) override 0310 { 0311 if (toolButtonStyle() == Qt::ToolButtonIconOnly) { 0312 QToolButton::paintEvent(event); 0313 return; 0314 } 0315 0316 QStylePainter sp(this); 0317 QStyleOptionToolButton opt; 0318 initStyleOption(&opt); 0319 const QString strText = opt.text; 0320 const QIcon icn = opt.icon; 0321 0322 // draw background 0323 opt.text.clear(); 0324 opt.icon = QIcon(); 0325 sp.drawComplexControl(QStyle::CC_ToolButton, opt); 0326 0327 // draw content 0328 const int nSizeHintWidth = minimumSizeHint().width(); 0329 0330 opt.text = strText; 0331 opt.icon = icn; 0332 opt.rect.setWidth(nSizeHintWidth); 0333 0334 // Those flags are drawn on the previous call to 0335 // drawn the background. Here we need to clear them 0336 // if we don't, we have artefacts. 0337 opt.state.setFlag(QStyle::State_MouseOver, false); 0338 opt.state.setFlag(QStyle::State_Selected, false); 0339 opt.state.setFlag(QStyle::State_Off, false); 0340 opt.state.setFlag(QStyle::State_On, false); 0341 opt.state.setFlag(QStyle::State_Sunken, false); 0342 sp.drawComplexControl(QStyle::CC_ToolButton, opt); 0343 } 0344 }; 0345 0346 class ToolBoxScrollArea : public QScrollArea { 0347 public: 0348 explicit ToolBoxScrollArea(QWidget *parent): QScrollArea(parent) 0349 { 0350 } 0351 0352 void recalculateWidgetSize() 0353 { 0354 if (widget() && widget()->layout()) { 0355 QSize size(maximumViewportSize().width(), 0356 widget()->layout()->heightForWidth(maximumViewportSize().width())); 0357 0358 if (size.height() > maximumViewportSize().height()) { 0359 const int ext = style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); 0360 size.setWidth(maximumViewportSize().width() - verticalScrollBar()->sizeHint().width() - ext); 0361 0362 size.setHeight(widget()->layout()->heightForWidth(size.width())); 0363 } 0364 0365 widget()->resize(size); 0366 } 0367 } 0368 0369 protected: 0370 void resizeEvent(QResizeEvent *event) override 0371 { 0372 QScrollArea::resizeEvent(event); 0373 recalculateWidgetSize(); 0374 } 0375 }; 0376 0377 } // namespace 0378 0379 struct ToolBox::Private { 0380 ToolBoxScrollArea *scrollArea = nullptr; 0381 QWidget *widget = nullptr; 0382 ToolBoxLayout *layout = nullptr; 0383 QAction *toggleInformativeViewAction = nullptr; 0384 QActionGroup *actionGroup = nullptr; 0385 QMap<QString, QList<QWidget *>> buttonsInCategory; 0386 QToolButton *invisibleButton = nullptr; 0387 QButtonGroup toolsGroup; 0388 QList<QToolButton *> toolButtons; 0389 QList<QString> categories; 0390 }; 0391 0392 ToolBox::ToolBox(QWidget *parent): QWidget(parent), d(std::make_unique<ToolBox::Private>()) 0393 { 0394 // "invisibleButton" is necessary to allow the button group to not have a default selected item. 0395 d->invisibleButton = new QToolButton(); 0396 d->invisibleButton->hide(); 0397 d->invisibleButton->setCheckable(true); 0398 d->invisibleButton->setChecked(true); 0399 d->toolsGroup.setExclusive(true); 0400 d->toolsGroup.addButton(d->invisibleButton); 0401 0402 d->scrollArea = new ToolBoxScrollArea(this); 0403 d->scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0404 d->scrollArea->setFrameShape(QFrame::NoFrame); 0405 0406 d->widget = new QWidget(d->scrollArea); 0407 d->layout = new ToolBoxLayout(d->widget); 0408 0409 d->layout->setSpacing(0); 0410 d->layout->setOneLine(Preferences::showText()); 0411 0412 d->actionGroup = new QActionGroup(d->widget); 0413 d->actionGroup->setExclusive(true); 0414 0415 d->scrollArea->setWidget(d->widget); 0416 d->scrollArea->setMinimumWidth(d->widget->minimumSizeHint().width()); 0417 0418 auto *topLayout = new QVBoxLayout(); 0419 topLayout->addWidget(d->scrollArea); 0420 setLayout(topLayout); 0421 0422 d->toggleInformativeViewAction = new QAction(tr("Informative View"), this); 0423 d->toggleInformativeViewAction->setCheckable(true); 0424 d->toggleInformativeViewAction->setChecked(Preferences::showText()); 0425 QObject::connect(d->toggleInformativeViewAction, &QAction::toggled, this, &ToolBox::setInformativeView); 0426 addAction(d->toggleInformativeViewAction); 0427 setContextMenuPolicy(Qt::ActionsContextMenu); 0428 } 0429 0430 ToolBox::~ToolBox() = default; 0431 0432 QToolButton *ToolBox::createToolButton(const QString& category, QAction *action) 0433 { 0434 auto *button = new LayoutToolButton(this); 0435 if (Preferences::showText()) { 0436 button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); 0437 } else { 0438 button->setToolButtonStyle(Qt::ToolButtonIconOnly); 0439 } 0440 0441 button->setAutoRaise(true); 0442 button->setIconSize(QSize(22, 22)); 0443 button->setDefaultAction(action); 0444 button->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); 0445 button->setObjectName(action->text()); 0446 0447 d->toolButtons.append(button); 0448 d->layout->addWidget(button); 0449 d->buttonsInCategory[category].append(button); 0450 applyStyleSheetHack(button); 0451 return button; 0452 } 0453 0454 QToolButton *ToolBox::createToolButton(const QString& category, lvtqtc::ITool *tool) 0455 { 0456 auto *btn = createToolButton(category, tool->action()); 0457 connect(tool, &lvtqtc::ITool::deactivated, this, [category, this] { 0458 d->invisibleButton->setChecked(true); 0459 }); 0460 d->toolsGroup.addButton(btn); 0461 return btn; 0462 } 0463 0464 void ToolBox::createGroup(const QString& category) 0465 { 0466 auto *action = new QAction(this); 0467 action->setSeparator(true); 0468 action->setObjectName(category); 0469 0470 QWidget *sep = new Separator(category, this); 0471 d->actionGroup->addAction(action); 0472 d->layout->addWidget(sep); 0473 d->buttonsInCategory[category].append(sep); 0474 } 0475 0476 void ToolBox::setInformativeView(bool isActive) 0477 { 0478 Preferences::setShowText(isActive); 0479 for (QToolButton *button : qAsConst(d->toolButtons)) { 0480 button->setToolButtonStyle(isActive ? Qt::ToolButtonTextBesideIcon : Qt::ToolButtonIconOnly); 0481 } 0482 d->layout->setOneLine(isActive); 0483 d->scrollArea->recalculateWidgetSize(); 0484 } 0485 0486 void ToolBox::hideElements(const QString& category) 0487 { 0488 for (auto *btn : qAsConst(d->buttonsInCategory[category])) { 0489 btn->setVisible(false); 0490 } 0491 d->scrollArea->recalculateWidgetSize(); 0492 } 0493 0494 void ToolBox::showElements(const QString& category) 0495 { 0496 for (auto *btn : qAsConst(d->buttonsInCategory[category])) { 0497 btn->setVisible(true); 0498 } 0499 d->scrollArea->recalculateWidgetSize(); 0500 } 0501 0502 QToolButton *ToolBox::getButtonNamed(const std::string& title) const 0503 { 0504 auto it = std::find_if(d->toolButtons.begin(), d->toolButtons.end(), [&title](QToolButton *btn) { 0505 auto txt = btn->objectName().toStdString(); 0506 return txt == title; 0507 }); 0508 0509 if (it == d->toolButtons.end()) { 0510 return nullptr; 0511 } 0512 return *it; 0513 }