Files
create/src/Gui/SplashScreen.cpp
Markus Reitböck a72a0d6405 Gui: use CMake to generate precompiled headers on all platforms
"Professional CMake" book suggest the following:

"Targets should build successfully with or without compiler support for precompiled headers. It
should be considered an optimization, not a requirement. In particular, do not explicitly include a
precompile header (e.g. stdafx.h) in the source code, let CMake force-include an automatically
generated precompile header on the compiler command line instead. This is more portable across
the major compilers and is likely to be easier to maintain. It will also avoid warnings being
generated from certain code checking tools like iwyu (include what you use)."

Therefore, removed the "#include <PreCompiled.h>" from sources, also
there is no need for the "#ifdef _PreComp_" anymore
2025-09-14 09:47:03 +02:00

404 lines
15 KiB
C++

/***************************************************************************
* Copyright (c) 2004 Werner Mayer <wmayer[at]users.sourceforge.net> *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library 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 Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#include <cstdlib>
#include <QApplication>
#include <QClipboard>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QMutex>
#include <QPainter>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QWaitCondition>
#include <App/Application.h>
#include <App/Metadata.h>
#include <Base/Console.h>
#include "BitmapFactory.h"
#include "SplashScreen.h"
#include "Tools.h"
using namespace Gui;
namespace Gui
{
/** Displays all messages at startup inside the splash screen.
* \author Werner Mayer
*/
class SplashObserver: public Base::ILogger
{
public:
SplashObserver(const SplashObserver&) = delete;
SplashObserver(SplashObserver&&) = delete;
SplashObserver& operator=(const SplashObserver&) = delete;
SplashObserver& operator=(SplashObserver&&) = delete;
explicit SplashObserver(QSplashScreen* splasher = nullptr)
: splash(splasher)
, alignment(Qt::AlignBottom | Qt::AlignLeft)
, textColor(Qt::black)
{
Base::Console().attachObserver(this);
// allow to customize text position and color
const std::map<std::string, std::string>& cfg = App::Application::Config();
auto al = cfg.find("SplashAlignment");
if (al != cfg.end()) {
QString alt = QString::fromLatin1(al->second.c_str());
int align = 0;
if (alt.startsWith(QLatin1String("VCenter"))) {
align = Qt::AlignVCenter;
}
else if (alt.startsWith(QLatin1String("Top"))) {
align = Qt::AlignTop;
}
else {
align = Qt::AlignBottom;
}
if (alt.endsWith(QLatin1String("HCenter"))) {
align += Qt::AlignHCenter;
}
else if (alt.endsWith(QLatin1String("Right"))) {
align += Qt::AlignRight;
}
else {
align += Qt::AlignLeft;
}
alignment = align;
}
// choose text color
auto tc = cfg.find("SplashTextColor");
if (tc != cfg.end()) {
QColor col(QString::fromStdString(tc->second));
if (col.isValid()) {
textColor = col;
}
}
}
~SplashObserver() override
{
Base::Console().detachObserver(this);
}
const char* name() override
{
return "SplashObserver";
}
void sendLog(const std::string& notifiername,
const std::string& msg,
Base::LogStyle level,
Base::IntendedRecipient recipient,
Base::ContentType content) override
{
Q_UNUSED(notifiername)
Q_UNUSED(recipient)
Q_UNUSED(content)
#ifdef FC_DEBUG
Q_UNUSED(level)
Log(msg);
#else
if (level == Base::LogStyle::Log) {
Log(msg);
}
#endif
}
void Log(const std::string& text)
{
QString msg(QString::fromStdString(text));
QRegularExpression rx;
// ignore 'Init:' and 'Mod:' prefixes
rx.setPattern(QLatin1String("^\\s*(Init:|Mod:)\\s*"));
auto match = rx.match(msg);
if (match.hasMatch()) {
msg = msg.mid(match.capturedLength());
}
else {
// ignore activation of commands
rx.setPattern(QLatin1String(R"(^\s*(\+App::|Create|CmdC:|CmdG:|Act:)\s*)"));
match = rx.match(msg);
if (match.hasMatch() && match.capturedStart() == 0) {
return;
}
}
splash->showMessage(msg.replace(QLatin1String("\n"), QString()), alignment, textColor);
QMutex mutex;
QMutexLocker ml(&mutex);
QWaitCondition().wait(&mutex, 50);
}
private:
QSplashScreen* splash;
int alignment;
QColor textColor;
};
/**
* Displays a warning about this being a developer build. Designed for display in the Splashscreen.
* \param painter The painter to draw the warning into
* \param startPosition The painter-space coordinates to start the warning box at.
* \param maxSize The maximum extents for the box that is drawn. If the text exceeds this size it
* will be scaled down to fit.
* \note The text string is translatable, so its length is somewhat unpredictable. It is always
* displayed as two lines, regardless of the length of the text (e.g. no wrapping is done). Only the
* width is considered, the height simply follows from the font size.
*/
static void renderDevBuildWarning(QPainter& painter,
const QPoint startPosition,
const QSize maxSize,
QColor color)
{
// Create a background box that fades out the artwork for better legibility
QColor fader(Qt::white);
constexpr float halfDensity(0.65F);
fader.setAlphaF(halfDensity);
QBrush fillBrush(fader, Qt::BrushStyle::SolidPattern);
painter.setBrush(fillBrush);
// Construct the lines of text and figure out how much space they need
const auto devWarningLine1 = QObject::tr("WARNING: This is a development version.");
const auto devWarningLine2 = QObject::tr("Do not use it in a production environment.");
QFontMetrics fontMetrics(painter.font()); // Try to use the existing font
int padding = QtTools::horizontalAdvance(fontMetrics, QLatin1String("M")); // Arbitrary
int line1Width = QtTools::horizontalAdvance(fontMetrics, devWarningLine1);
int line2Width = QtTools::horizontalAdvance(fontMetrics, devWarningLine2);
int boxWidth = std::max(line1Width, line2Width) + 2 * padding;
int lineHeight = fontMetrics.lineSpacing();
if (boxWidth > maxSize.width()) {
// Especially if the text was translated, there is a chance that using the existing font
// will exceed the width of the Splashscreen graphic. Resize down so that it fits, no matter
// how long the text strings are.
float reductionFactor = static_cast<float>(maxSize.width()) / static_cast<float>(boxWidth);
int newFontSize = static_cast<int>(painter.font().pointSize() * reductionFactor);
padding *= reductionFactor;
QFont newFont = painter.font();
newFont.setPointSize(newFontSize);
painter.setFont(newFont);
lineHeight = painter.fontMetrics().lineSpacing();
boxWidth = maxSize.width();
}
constexpr float lineExpansionFactor(2.3F);
int boxHeight = static_cast<int>(lineHeight * lineExpansionFactor);
// Draw the background rectangle and the text
painter.setPen(color);
painter.drawRect(startPosition.x(), startPosition.y(), boxWidth, boxHeight);
painter.drawText(startPosition.x() + padding, startPosition.y() + lineHeight, devWarningLine1);
painter.drawText(startPosition.x() + padding,
startPosition.y() + 2 * lineHeight,
devWarningLine2);
}
} // namespace Gui
// ------------------------------------------------------------------------------
/**
* Constructs a splash screen that will display the pixmap.
*/
SplashScreen::SplashScreen(const QPixmap& pixmap, Qt::WindowFlags f)
: QSplashScreen(pixmap, f)
{
// write the messages to splasher
messages = new SplashObserver(this);
}
/** Destruction. */
SplashScreen::~SplashScreen()
{
delete messages;
}
/**
* Draws the contents of the splash screen using painter \a painter. The default
* implementation draws the message passed by message().
*/
void SplashScreen::drawContents(QPainter* painter)
{
QSplashScreen::drawContents(painter);
}
void SplashScreen::setShowMessages(bool on)
{
messages->bErr = on;
messages->bMsg = on;
messages->bLog = on;
messages->bWrn = on;
}
QPixmap SplashScreen::splashImage()
{
// search in the UserAppData dir as very first
QPixmap splash_image;
QFileInfo fi(QStringLiteral("images:splash_image.png"));
if (fi.isFile() && fi.exists()) {
splash_image.load(fi.filePath(), "PNG");
}
// if no image was found try the config
std::string splash_path = App::Application::Config()["SplashScreen"];
if (splash_image.isNull()) {
QString path = QString::fromStdString(splash_path);
if (QDir(path).isRelative()) {
QString home = QString::fromStdString(App::Application::getHomePath());
path = QFileInfo(QDir(home), path).absoluteFilePath();
}
splash_image.load(path);
}
// now try the icon paths
float pixelRatio(1.0);
if (splash_image.isNull()) {
// determine the count of splashes
QStringList pixmaps =
Gui::BitmapFactory().findIconFiles().filter(QString::fromStdString(splash_path));
// divide by 2 since there's two sets (normal and 2x)
// minus 1 to ignore the default splash that isn't numbered
int splash_count = pixmaps.count() / 2 - 1;
// set a random splash path
if (splash_count > 0) {
int random = rand() % splash_count;
splash_path += std::to_string(random);
}
if (qApp->devicePixelRatio() > 1.0) {
// For HiDPI screens, we have a double-resolution version of the splash image
splash_path += "_2x";
splash_image = Gui::BitmapFactory().pixmap(splash_path.c_str());
splash_image.setDevicePixelRatio(2.0);
pixelRatio = 2.0;
}
else {
splash_image = Gui::BitmapFactory().pixmap(splash_path.c_str());
}
}
// include application name and version number
std::map<std::string, std::string>::const_iterator tc =
App::Application::Config().find("SplashInfoColor");
std::map<std::string, std::string>::const_iterator wc =
App::Application::Config().find("SplashWarningColor");
if (tc != App::Application::Config().end() && wc != App::Application::Config().end()) {
QString title = qApp->applicationName();
QString major = QString::fromStdString(App::Application::Config()["BuildVersionMajor"]);
QString minor = QString::fromStdString(App::Application::Config()["BuildVersionMinor"]);
QString point = QString::fromStdString(App::Application::Config()["BuildVersionPoint"]);
QString suffix = QString::fromStdString(App::Application::Config()["BuildVersionSuffix"]);
QString version = QStringLiteral("%1.%2.%3%4").arg(major, minor, point, suffix);
QString position, fontFamily;
std::map<std::string, std::string>::const_iterator te =
App::Application::Config().find("SplashInfoExeName");
std::map<std::string, std::string>::const_iterator tv =
App::Application::Config().find("SplashInfoVersion");
std::map<std::string, std::string>::const_iterator tp =
App::Application::Config().find("SplashInfoPosition");
std::map<std::string, std::string>::const_iterator tf =
App::Application::Config().find("SplashInfoFont");
if (te != App::Application::Config().end()) {
title = QString::fromStdString(te->second);
}
if (tv != App::Application::Config().end()) {
version = QString::fromStdString(tv->second);
}
if (tp != App::Application::Config().end()) {
position = QString::fromStdString(tp->second);
}
if (tf != App::Application::Config().end()) {
fontFamily = QString::fromStdString(tf->second);
}
QPainter painter;
painter.begin(&splash_image);
if (!fontFamily.isEmpty()) {
QFont font = painter.font();
if (font.fromString(fontFamily)) {
painter.setFont(font);
}
}
QFont fontExe = painter.font();
fontExe.setPointSizeF(20.0);
QFontMetrics metricExe(fontExe);
int l = QtTools::horizontalAdvance(metricExe, title);
if (title == QLatin1String("FreeCAD")) {
l = 0.0; // "FreeCAD" text is already part of the splashscreen, version goes below it
}
int w = splash_image.width();
int h = splash_image.height();
QFont fontVer = painter.font();
fontVer.setPointSizeF(11.0);
QFontMetrics metricVer(fontVer);
int v = QtTools::horizontalAdvance(metricVer, version);
int x = -1, y = -1;
QRegularExpression rx(QLatin1String("(\\d+).(\\d+)"));
auto match = rx.match(position);
if (match.hasMatch()) {
x = match.captured(1).toInt();
y = match.captured(2).toInt();
}
else {
x = w - (l + v + 10);
y = h - 20;
}
QColor color(QString::fromStdString(tc->second));
if (color.isValid()) {
painter.setPen(color);
painter.setFont(fontExe);
if (title != QLatin1String("FreeCAD")) {
// FreeCAD's Splashscreen already contains the EXE name, no need to draw it
painter.drawText(x, y, title);
}
painter.setFont(fontVer);
painter.drawText(x + (l + 235), y - 7, version);
QColor warningColor(QString::fromStdString(wc->second));
if (suffix == QLatin1String("dev") && warningColor.isValid()) {
fontVer.setPointSizeF(14.0);
painter.setFont(fontVer);
const int lineHeight = metricVer.lineSpacing();
const int padding {45}; // Distance from the edge of the graphic's bounding box
QPoint startPosition(padding, y + lineHeight * 2);
QSize maxSize(w / pixelRatio - 2 * padding, lineHeight * 3);
renderDevBuildWarning(painter, startPosition, maxSize, warningColor);
}
painter.end();
}
}
return splash_image;
}