/*************************************************************************** * Copyright (c) 2004 Werner Mayer * * * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #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& 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(maxSize.width()) / static_cast(boxWidth); int newFontSize = static_cast(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(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::const_iterator tc = App::Application::Config().find("SplashInfoColor"); std::map::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::const_iterator te = App::Application::Config().find("SplashInfoExeName"); std::map::const_iterator tv = App::Application::Config().find("SplashInfoVersion"); std::map::const_iterator tp = App::Application::Config().find("SplashInfoPosition"); std::map::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; }