diff --git a/src/Mod/Start/App/DisplayedFilesModel.cpp b/src/Mod/Start/App/DisplayedFilesModel.cpp index a791fb8868..85ef1ff3a6 100644 --- a/src/Mod/Start/App/DisplayedFilesModel.cpp +++ b/src/Mod/Start/App/DisplayedFilesModel.cpp @@ -88,9 +88,11 @@ QByteArray loadFCStdThumbnail(const std::string& pathToFCStdFile) if (proj.loadDocument()) { try { std::string thumbnailFile = proj.extractInputFile("thumbnails/Thumbnail.png"); - auto inputFile = QFile(QString::fromStdString(thumbnailFile)); - inputFile.open(QIODevice::OpenModeFlag::ReadOnly); - return inputFile.readAll(); + if (!thumbnailFile.empty()) { + auto inputFile = QFile(QString::fromStdString(thumbnailFile)); + inputFile.open(QIODevice::OpenModeFlag::ReadOnly); + return inputFile.readAll(); + } } catch (...) { } diff --git a/src/Mod/Start/Gui/CMakeLists.txt b/src/Mod/Start/Gui/CMakeLists.txt index 9f8141ad35..5d6559e9fd 100644 --- a/src/Mod/Start/Gui/CMakeLists.txt +++ b/src/Mod/Start/Gui/CMakeLists.txt @@ -53,6 +53,8 @@ SET(StartGui_SRCS FileCardDelegate.h FileCardView.cpp FileCardView.h + FlowLayout.cpp + FlowLayout.h Manipulator.cpp Manipulator.h PreCompiled.cpp diff --git a/src/Mod/Start/Gui/FileCardDelegate.cpp b/src/Mod/Start/Gui/FileCardDelegate.cpp index e2ba0f2d73..d8122dbf9b 100644 --- a/src/Mod/Start/Gui/FileCardDelegate.cpp +++ b/src/Mod/Start/Gui/FileCardDelegate.cpp @@ -32,12 +32,13 @@ #include #include #include -#include +#include #endif #include "FileCardDelegate.h" #include "../App/DisplayedFilesModel.h" #include "App/Application.h" +#include #include <3rdParty/GSL/include/gsl/pointers> using namespace Start; @@ -49,6 +50,32 @@ FileCardDelegate::FileCardDelegate(QObject* parent) "User parameter:BaseApp/Preferences/Mod/Start"); } +QColor FileCardDelegate::getBorderColor() const +{ + QColor color(98, 160, 234); // NOLINT + uint32_t packed = App::Color::asPackedRGB(color); + packed = _parameterGroup->GetUnsigned("FileThumbnailBorderColor", packed); + color = App::Color::fromPackedRGB(packed); + return color; +} + +QColor FileCardDelegate::getBackgroundColor() const +{ + QColor color(221, 221, 221); // NOLINT + uint32_t packed = App::Color::asPackedRGB(color); + packed = _parameterGroup->GetUnsigned("FileThumbnailBackgroundColor", packed); + color = App::Color::fromPackedRGB(packed); + return color; +} + +QColor FileCardDelegate::getSelectionColor() const +{ + QColor color(38, 162, 105); // NOLINT + uint32_t packed = App::Color::asPackedRGB(color); + packed = _parameterGroup->GetUnsigned("FileThumbnailSelectionColor", packed); + color = App::Color::fromPackedRGB(packed); + return color; +} void FileCardDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, @@ -63,6 +90,7 @@ void FileCardDelegate::paint(QPainter* painter, auto path = index.data(static_cast(DisplayedFilesModelRoles::path)).toString(); painter->save(); auto widget = gsl::owner(new QWidget()); + widget->setObjectName(QLatin1String("thumbnailWidget")); auto layout = gsl::owner(new QVBoxLayout()); widget->setLayout(layout); auto thumbnail = gsl::owner(new QLabel()); @@ -81,6 +109,47 @@ void FileCardDelegate::paint(QPainter* painter, } thumbnail->setFixedSize(thumbnailSize, thumbnailSize); thumbnail->setSizePolicy(QSizePolicy::Policy::Fixed, QSizePolicy::Policy::Fixed); + + widget->setProperty("state", QStringLiteral("")); + if (option.state & QStyle::State_Selected) { + widget->setProperty("state", QStringLiteral("pressed")); + if (qApp->styleSheet().isEmpty()) { + QColor color = getSelectionColor(); + widget->setStyleSheet(QString::fromLatin1("QWidget#thumbnailWidget {" + " border: 2px solid rgb(%1, %2, %3);" + " border-radius: 4px;" + " padding: 2px;" + "}") + .arg(color.red()) + .arg(color.green()) + .arg(color.blue())); + } + } + else if (option.state & QStyle::State_MouseOver) { + widget->setProperty("state", QStringLiteral("hovered")); + if (qApp->styleSheet().isEmpty()) { + QColor color = getBorderColor(); + widget->setStyleSheet(QString::fromLatin1("QWidget#thumbnailWidget {" + " border: 2px solid rgb(%1, %2, %3);" + " border-radius: 4px;" + " padding: 2px;" + "}") + .arg(color.red()) + .arg(color.green()) + .arg(color.blue())); + } + } + else if (qApp->styleSheet().isEmpty()) { + QColor color = getBackgroundColor(); + widget->setStyleSheet(QString::fromLatin1("QWidget#thumbnailWidget {" + " background-color: rgb(%1, %2, %3);" + " border-radius: 8px;" + "}") + .arg(color.red()) + .arg(color.green()) + .arg(color.blue())); + } + auto elided = painter->fontMetrics().elidedText(baseName, Qt::TextElideMode::ElideRight, cardWidth); auto name = gsl::owner(new QLabel(elided)); @@ -95,6 +164,7 @@ void FileCardDelegate::paint(QPainter* painter, widget->render(painter, QPoint(), QRegion(), QWidget::DrawChildren); painter->restore(); delete pixmap; + delete widget; } @@ -103,7 +173,7 @@ QSize FileCardDelegate::sizeHint(const QStyleOptionViewItem& option, const QMode Q_UNUSED(option) Q_UNUSED(index) auto thumbnailSize = _parameterGroup->GetInt("FileThumbnailIconsSize", 128); // NOLINT - auto cardSpacing = _parameterGroup->GetInt("FileCardSpacing", 20); // NOLINT + auto cardSpacing = _parameterGroup->GetInt("FileCardSpacing", 30); // NOLINT auto cardWidth = thumbnailSize + cardSpacing; auto font = QGuiApplication::font(); diff --git a/src/Mod/Start/Gui/FileCardDelegate.h b/src/Mod/Start/Gui/FileCardDelegate.h index 6ba177d93f..201dc7a933 100644 --- a/src/Mod/Start/Gui/FileCardDelegate.h +++ b/src/Mod/Start/Gui/FileCardDelegate.h @@ -44,6 +44,11 @@ public: protected: QPixmap generateThumbnail(const QString& path) const; +private: + QColor getBorderColor() const; + QColor getBackgroundColor() const; + QColor getSelectionColor() const; + private: Base::Reference _parameterGroup; }; diff --git a/src/Mod/Start/Gui/FileCardView.cpp b/src/Mod/Start/Gui/FileCardView.cpp index 3ea44a0a59..5b2ab5687d 100644 --- a/src/Mod/Start/Gui/FileCardView.cpp +++ b/src/Mod/Start/Gui/FileCardView.cpp @@ -44,6 +44,8 @@ FileCardView::FileCardView(QWidget* parent) setFlow(QListView::Flow::LeftToRight); setResizeMode(QListView::ResizeMode::Adjust); setUniformItemSizes(true); + setMouseTracking(true); + setSpacing(20); } int FileCardView::heightForWidth(int width) const @@ -83,5 +85,4 @@ QSize FileCardView::sizeHint() const cardSize.height() + 2 * cardSpacing}; } - } // namespace StartGui diff --git a/src/Mod/Start/Gui/FlowLayout.cpp b/src/Mod/Start/Gui/FlowLayout.cpp new file mode 100644 index 0000000000..0a92b07922 --- /dev/null +++ b/src/Mod/Start/Gui/FlowLayout.cpp @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/*************************************************************************** + * Copyright (c) 2024 Werner Mayer * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + +#include "PreCompiled.h" +#include +#include "FlowLayout.h" + +using namespace StartGui; + +FlowLayout::FlowLayout(QWidget* parent, int margin, int hSpacing, int vSpacing) + : QLayout(parent) + , hSpace {hSpacing} + , vSpace {vSpacing} +{ + setContentsMargins(margin, margin, margin, margin); +} + +FlowLayout::~FlowLayout() +{ + QLayoutItem* item {}; + while ((item = takeAt(0))) { + delete item; + } +} + +void FlowLayout::addItem(QLayoutItem* item) +{ + itemList.append(item); +} + +int FlowLayout::horizontalSpacing() const +{ + if (hSpace >= 0) { + return hSpace; + } + + return smartSpacing(QStyle::PM_LayoutHorizontalSpacing); +} + +int FlowLayout::verticalSpacing() const +{ + if (vSpace >= 0) { + return vSpace; + } + + return smartSpacing(QStyle::PM_LayoutVerticalSpacing); +} + +int FlowLayout::count() const +{ + return itemList.size(); +} + +QLayoutItem* FlowLayout::itemAt(int index) const +{ + if (index >= 0 && index < itemList.size()) { + return itemList[index]; + } + + return nullptr; +} + +QLayoutItem* FlowLayout::takeAt(int index) +{ + if (index >= 0 && index < itemList.size()) { + return itemList.takeAt(index); + } + + return nullptr; +} + +Qt::Orientations FlowLayout::expandingDirections() const +{ + return {}; +} + +bool FlowLayout::hasHeightForWidth() const +{ + return true; +} + +int FlowLayout::heightForWidth(int width) const +{ + int height = doLayout(QRect(0, 0, width, 0), true); + return height; +} + +void FlowLayout::setGeometry(const QRect& rect) +{ + QLayout::setGeometry(rect); + doLayout(rect, false); +} + +QSize FlowLayout::sizeHint() const +{ + return minimumSize(); +} + +QSize FlowLayout::minimumSize() const +{ + QSize size; + for (auto item : qAsConst(itemList)) { + size = size.expandedTo(item->minimumSize()); + } + + QMargins margins = contentsMargins(); + size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom()); + return size; +} + +int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const +{ + QObject* par = parent(); + if (!par) { + return -1; + } + + if (par->isWidgetType()) { + auto widget = qobject_cast(par); + return widget->style()->pixelMetric(pm, nullptr, widget); + } + + return static_cast(par)->spacing(); +} + +int FlowLayout::doLayout(const QRect& rect, bool testOnly) const +{ + int left {}; + int top {}; + int right {}; + int bottom {}; + getContentsMargins(&left, &top, &right, &bottom); + QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom); + int x = effectiveRect.x(); + int y = effectiveRect.y(); + int lineHeight = 0; + + for (auto item : qAsConst(itemList)) { + QWidget* wid = item->widget(); + int spaceX = horizontalSpacing(); + if (spaceX == -1) { + spaceX = wid->style()->layoutSpacing(QSizePolicy::PushButton, + QSizePolicy::PushButton, + Qt::Horizontal); + } + + int spaceY = verticalSpacing(); + if (spaceY == -1) { + spaceY = wid->style()->layoutSpacing(QSizePolicy::PushButton, + QSizePolicy::PushButton, + Qt::Vertical); + } + + int nextX = x + item->sizeHint().width() + spaceX; + if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) { + x = effectiveRect.x(); + y = y + lineHeight + spaceY; + nextX = x + item->sizeHint().width() + spaceX; + lineHeight = 0; + } + + if (!testOnly) { + item->setGeometry(QRect(QPoint(x, y), item->sizeHint())); + } + + x = nextX; + lineHeight = std::max(lineHeight, item->sizeHint().height()); + } + + return y + lineHeight - rect.y() + bottom; +} + +#include "moc_FlowLayout.cpp" diff --git a/src/Mod/Start/Gui/FlowLayout.h b/src/Mod/Start/Gui/FlowLayout.h new file mode 100644 index 0000000000..2518df809d --- /dev/null +++ b/src/Mod/Start/Gui/FlowLayout.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/*************************************************************************** + * Copyright (c) 2024 Werner Mayer * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + +#ifndef FREECAD_FLOWLAYOUT_H +#define FREECAD_FLOWLAYOUT_H + +#include +#include +#include + +namespace StartGui +{ + +/*! + * \brief The FlowLayout class + * Based on https://forum.qt.io/topic/109408/is-there-a-qt-layout-grid-that-can-dynamically- + * change-row-and-column-counts-to-best-fit-the-space + */ +class FlowLayout: public QLayout +{ + Q_OBJECT + +public: + explicit FlowLayout(QWidget* parent = nullptr, + int margin = -1, + int hSpacing = -1, + int vSpacing = -1); + ~FlowLayout() override; + + void addItem(QLayoutItem* item) override; + int count() const override; + QLayoutItem* itemAt(int index) const override; + QLayoutItem* takeAt(int index) override; + Qt::Orientations expandingDirections() const override; + bool hasHeightForWidth() const override; + int heightForWidth(int width) const override; + void setGeometry(const QRect& rect) override; + QSize sizeHint() const override; + QSize minimumSize() const override; + +private: + int horizontalSpacing() const; + int verticalSpacing() const; + int smartSpacing(QStyle::PixelMetric pm) const; + int doLayout(const QRect& rect, bool testOnly) const; + +private: + QList itemList; + int hSpace = -1; + int vSpace = -1; +}; + +} // namespace StartGui + +#endif // FREECAD_FLOWLAYOUT_H diff --git a/src/Mod/Start/Gui/Manipulator.cpp b/src/Mod/Start/Gui/Manipulator.cpp index 4b060ef6f3..ebf0e525a5 100644 --- a/src/Mod/Start/Gui/Manipulator.cpp +++ b/src/Mod/Start/Gui/Manipulator.cpp @@ -69,8 +69,11 @@ void CmdStart::activated(int iMsg) void StartGui::Manipulator::modifyMenuBar(Gui::MenuItem* menuBar) { Gui::CommandManager& rcCmdMgr = Gui::Application::Instance->commandManager(); - auto newCommand = gsl::owner(new CmdStart); - rcCmdMgr.addCommand(newCommand); // Transfer ownership + if (!rcCmdMgr.getCommandByName("Start_Start")) { + auto newCommand = gsl::owner(new CmdStart); + rcCmdMgr.addCommand(newCommand); // Transfer ownership + } + Gui::MenuItem* helpMenu = menuBar->findItem("&Help"); Gui::MenuItem* loadStart = new Gui::MenuItem(); loadStart->setCommand("Start_Start"); diff --git a/src/Mod/Start/Gui/PreCompiled.h b/src/Mod/Start/Gui/PreCompiled.h index e5c164a33c..11cc15a39d 100644 --- a/src/Mod/Start/Gui/PreCompiled.h +++ b/src/Mod/Start/Gui/PreCompiled.h @@ -43,8 +43,8 @@ #include // Qt +#include #include -#include #include #include #include diff --git a/src/Mod/Start/Gui/StartView.cpp b/src/Mod/Start/Gui/StartView.cpp index 678c1cb799..a655d11476 100644 --- a/src/Mod/Start/Gui/StartView.cpp +++ b/src/Mod/Start/Gui/StartView.cpp @@ -24,7 +24,7 @@ #include "PreCompiled.h" #ifndef _PreComp_ -#include +#include #include #include #include @@ -37,6 +37,7 @@ #include "StartView.h" #include "FileCardDelegate.h" #include "FileCardView.h" +#include "FlowLayout.h" #include "Gui/Workbench.h" #include #include @@ -64,8 +65,9 @@ gsl::owner createNewButton(const NewButton& newButton) { auto hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Start"); - const auto cardSpacing = static_cast(hGrp->GetInt("FileCardSpacing", 20)); // NOLINT - const auto newFileIconSize = static_cast(hGrp->GetInt("NewFileIconSize", 48)); // NOLINT + const auto cardSpacing = static_cast(hGrp->GetInt("FileCardSpacing", 20)); // NOLINT + const auto newFileIconSize = static_cast(hGrp->GetInt("NewFileIconSize", 48)); // NOLINT + const auto cardLabelWith = static_cast(hGrp->GetInt("FileCardLabelWith", 180)); // NOLINT auto button = gsl::owner(new QPushButton()); auto mainLayout = gsl::owner(new QHBoxLayout(button)); @@ -88,6 +90,7 @@ gsl::owner createNewButton(const NewButton& newButton) mainLayout->addStretch(); button->setMinimumHeight(newFileIconSize + cardSpacing); + button->setMinimumWidth(newFileIconSize + cardLabelWith); return button; } @@ -100,7 +103,7 @@ StartView::StartView(Gui::Document* pcDocument, QWidget* parent) setObjectName(QLatin1String("StartView")); auto hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Start"); - auto cardSpacing = hGrp->GetInt("FileCardSpacing", 20); // NOLINT + auto cardSpacing = hGrp->GetInt("FileCardSpacing", 30); // NOLINT auto scrolledWidget = gsl::owner(new QWidget(this)); _contents->setWidget(scrolledWidget); @@ -137,9 +140,9 @@ StartView::StartView(Gui::Document* pcDocument, QWidget* parent) auto newFileLabel = gsl::owner(new QLabel(h1Start + tr("New File") + h1End)); layout->addWidget(newFileLabel); - auto gridLayout = gsl::owner(new QGridLayout); - layout->addLayout(gridLayout); - configureNewFileButtons(gridLayout); + auto flowLayout = gsl::owner(new FlowLayout); + layout->addLayout(flowLayout); + configureNewFileButtons(flowLayout); auto recentFilesLabel = gsl::owner(new QLabel(h1Start + tr("Recent Files") + h1End)); layout->addWidget(recentFilesLabel); @@ -165,8 +168,7 @@ StartView::StartView(Gui::Document* pcDocument, QWidget* parent) configureRecentFilesListWidget(recentFilesListWidget, recentFilesLabel); } - -void StartView::configureNewFileButtons(QGridLayout* layout) const +void StartView::configureNewFileButtons(QLayout* layout) const { auto newEmptyFile = createNewButton({tr("Empty file"), tr("Create a new empty FreeCAD file"), @@ -187,14 +189,26 @@ void StartView::configureNewFileButtons(QGridLayout* layout) const tr("Create an architectural project"), QLatin1String(":/icons/ArchWorkbench.svg")}); + auto hGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Mod/Start"); + if (hGrp->GetBool("FileCardUseStyleSheet", true)) { + QString style = fileCardStyle(); + newEmptyFile->setStyleSheet(style); + openFile->setStyleSheet(style); + partDesign->setStyleSheet(style); + assembly->setStyleSheet(style); + draft->setStyleSheet(style); + arch->setStyleSheet(style); + } + // TODO: Ensure all of the required WBs are actually available // TODO: Make this layout more flexible (e.g. use a single line if possible) - layout->addWidget(partDesign, 0, 0); - layout->addWidget(assembly, 0, 1); - layout->addWidget(draft, 0, 2); - layout->addWidget(arch, 1, 0); - layout->addWidget(newEmptyFile, 1, 1); - layout->addWidget(openFile, 1, 2); + layout->addWidget(partDesign); + layout->addWidget(assembly); + layout->addWidget(draft); + layout->addWidget(arch); + layout->addWidget(newEmptyFile); + layout->addWidget(openFile); connect(newEmptyFile, &QPushButton::clicked, this, &StartView::newEmptyFile); connect(openFile, &QPushButton::clicked, this, &StartView::openExistingFile); @@ -204,14 +218,60 @@ void StartView::configureNewFileButtons(QGridLayout* layout) const connect(arch, &QPushButton::clicked, this, &StartView::newArchFile); } +QString StartView::fileCardStyle() const +{ + if (!qApp->styleSheet().isEmpty()) { + return {}; + } + + auto hGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Mod/Start"); + + auto getUserColor = [&hGrp](QColor color, const char* parameter) { + uint32_t packed = App::Color::asPackedRGB(color); + packed = hGrp->GetUnsigned(parameter, packed); + color = App::Color::fromPackedRGB(packed); + return color; + }; + + QColor background(221, 221, 221); // NOLINT + background = getUserColor(background, "FileCardBackgroundColor"); + + QColor hovered(98, 160, 234); // NOLINT + hovered = getUserColor(hovered, "FileCardBorderColor"); + + QColor pressed(38, 162, 105); // NOLINT + pressed = getUserColor(pressed, "FileCardSelectionColor"); + + return QString::fromLatin1("QPushButton {" + " background-color: rgb(%1, %2, %3);" + " border-radius: 8px;" + "}" + "QPushButton:hover {" + " border: 2px solid rgb(%4, %5, %6);" + "}" + "QPushButton:pressed {" + " border: 2px solid rgb(%7, %8, %9);" + "}") + .arg(background.red()) + .arg(background.green()) + .arg(background.blue()) + .arg(hovered.red()) + .arg(hovered.green()) + .arg(hovered.blue()) + .arg(pressed.red()) + .arg(pressed.green()) + .arg(pressed.blue()); +} + void StartView::configureFileCardWidget(QListView* fileCardWidget) { - auto delegate = gsl::owner(new FileCardDelegate); + auto delegate = gsl::owner(new FileCardDelegate(fileCardWidget)); fileCardWidget->setItemDelegate(delegate); fileCardWidget->setMinimumWidth(fileCardWidget->parentWidget()->width()); - fileCardWidget->setGridSize( - fileCardWidget->itemDelegate()->sizeHint(QStyleOptionViewItem(), - fileCardWidget->model()->index(0, 0))); + // fileCardWidget->setGridSize( + // fileCardWidget->itemDelegate()->sizeHint(QStyleOptionViewItem(), + // fileCardWidget->model()->index(0, 0))); } diff --git a/src/Mod/Start/Gui/StartView.h b/src/Mod/Start/Gui/StartView.h index f8aba3314c..8f9439c2ad 100644 --- a/src/Mod/Start/Gui/StartView.h +++ b/src/Mod/Start/Gui/StartView.h @@ -75,7 +75,7 @@ public: }; protected: - void configureNewFileButtons(QGridLayout* layout) const; + void configureNewFileButtons(QLayout* layout) const; static void configureFileCardWidget(QListView* fileCardWidget); void configureRecentFilesListWidget(QListView* recentFilesListWidget, QLabel* recentFilesLabel); void configureExamplesListWidget(QListView* examplesListWidget); @@ -85,6 +85,7 @@ protected: void fileCardSelected(const QModelIndex& index); void showOnStartupChanged(bool checked); + QString fileCardStyle() const; private: QScrollArea* _contents = nullptr;