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 }