/* This file is part of the KDE project * SPDX-FileCopyrightText: 2018 Scott Petrovic * SPDX-FileCopyrightText: 2021 L. E. Segovia * * SPDX-License-Identifier: LGPL-2.0-or-later */ #include "KisWelcomePageWidget.h" #include "KisRecentDocumentsModelWrapper.h" #include #include #include #include #include #include #include #include #include #include "KisRemoteFileFetcher.h" #include "kactioncollection.h" #include "kis_action.h" #include "kis_action_manager.h" #include "dialogs/KisDlgCreateNewDocument.h" #include #include #include "KConfigGroup" #include "KSharedConfig" #include #include #include #include #include "kis_clipboard.h" #include "kis_icon_utils.h" #include #include "KoStore.h" #include "kis_config.h" #include "KisDocument.h" #include #include #include #include #include "KisMainWindow.h" #include #include #include #include #include #include "config-updaters.h" #ifdef ENABLE_UPDATERS #ifdef Q_OS_LINUX #include #endif #include #endif #include #include #include #include #include #include #include "opengl/kis_opengl.h" #ifdef Q_OS_WIN #include #endif #ifdef Q_OS_MACOS #include "libs/macosutils/KisMacosEntitlements.h" #endif #ifdef Q_OS_ANDROID #include "KisAndroidDonations.h" #include #include #include #include #include #endif // Used for triggering a QAction::setChecked signal from a QLabel::linkActivated signal void ShowNewsAction::enableFromLink(QString unused_url) { Q_UNUSED(unused_url); Q_EMIT setChecked(true); } // class to override item height for Breeze since qss seems to not work class RecentItemDelegate : public QStyledItemDelegate { int itemHeight = 0; public: RecentItemDelegate(QObject *parent = 0) : QStyledItemDelegate(parent) { } void setItemHeight(int itemHeight) { this->itemHeight = itemHeight; } QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &/*index*/) const override { return QSize(option.rect.width(), itemHeight); } }; KisWelcomePageWidget::KisWelcomePageWidget(QWidget *parent) : QWidget(parent) { setupUi(this); // URLs that go to web browser... devBuildIcon->setIcon(KisIconUtils::loadIcon("warning")); devBuildLabel->setVisible(false); updaterFrame->setVisible(false); versionNotificationLabel->setVisible(false); bnVersionUpdate->setVisible(false); bnErrorDetails->setVisible(false); // Recent docs... recentDocumentsListView->setDragEnabled(false); recentDocumentsListView->viewport()->setAutoFillBackground(false); recentDocumentsListView->setSpacing(2); recentDocumentsListView->installEventFilter(this); recentDocumentsListView->setViewMode(QListView::IconMode); recentDocumentsListView->setSelectionMode(QAbstractItemView::NoSelection); recentDocumentsListView->setVerticalScrollMode(QListView::ScrollPerPixel); recentDocumentsListView->verticalScrollBar()->setSingleStep(50); { QScroller* scroller = KisKineticScroller::createPreconfiguredScroller(recentDocumentsListView); if (scroller) { connect(scroller, SIGNAL(stateChanged(QScroller::State)), this, SLOT(slotScrollerStateChanged(QScroller::State))); } } recentDocumentsListView->setContextMenuPolicy(Qt::CustomContextMenu); connect(recentDocumentsListView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotRecentDocContextMenuRequest(QPoint))); recentFilesResized(); // News widget... QMenu *newsOptionsMenu = new QMenu(this); newsOptionsMenu->setToolTipsVisible(true); ShowNewsAction *showNewsAction = new ShowNewsAction(i18n("Enable news and check for new releases"), newsOptionsMenu); newsOptionsMenu->addAction(showNewsAction); showNewsAction->setToolTip(i18n("Show news about Krita: this needs internet to retrieve information from the krita.org website")); showNewsAction->setCheckable(true); newsOptionsMenu->addSection(i18n("Language")); QAction *newsInfoAction = newsOptionsMenu->addAction(i18n("English news is always up to date.")); newsInfoAction->setEnabled(false); setupNewsLangSelection(newsOptionsMenu); btnNewsOptions->setMenu(newsOptionsMenu); labelSupportText->setFont(largerFont()); connect(btnDismisAll, SIGNAL(clicked()), newsWidget, SLOT(dismissAll())); connect(newsWidget, SIGNAL(newsStateChanged(bool)), this, SLOT(newsStateChanged(bool))); newsStateChanged(false); connect(showNewsAction, SIGNAL(toggled(bool)), newsWidget, SLOT(setVisible(bool))); connect(showNewsAction, SIGNAL(toggled(bool)), labelNoFeed, SLOT(setHidden(bool))); connect(showNewsAction, SIGNAL(toggled(bool)), newsWidget, SLOT(toggleNews(bool))); connect(labelNoFeed, SIGNAL(linkActivated(QString)), showNewsAction, SLOT(enableFromLink(QString))); #ifdef ENABLE_UPDATERS connect(showNewsAction, SIGNAL(toggled(bool)), this, SLOT(slotToggleUpdateChecks(bool))); #endif supporterBadge->hide(); wdgAndroidSupportBanner->hide(); #ifdef Q_OS_ANDROID initDonations(); #endif // configure the News area KisConfig cfg(true); m_networkIsAllowed = cfg.readEntry("FetchNews", false); #ifdef ENABLE_UPDATERS #ifndef Q_OS_ANDROID // Setup version updater, but do not check for them, unless the user explicitly // wants to check for updates. // * No updater is created for Linux/Steam, Windows/Steam and Windows/Store distributions, // as those stores have their own updating mechanism. // * STEAMAPPID(Windows)/SteamAppId(Linux) environment variable is set when Krita is run from Steam. // The environment variables are not public API. // * MS Store version runs as a package (though we cannot know if it was // installed from the Store or manually with the .msix package) #if defined Q_OS_LINUX if (!qEnvironmentVariableIsSet("SteamAppId")) { // do not create updater for linux/steam if (qEnvironmentVariableIsSet("APPIMAGE")) { m_versionUpdater.reset(new KisAppimageUpdater()); } else { m_versionUpdater.reset(new KisManualUpdater()); } } #elif defined Q_OS_WIN if (!KisWindowsPackageUtils::isRunningInPackage() && !qEnvironmentVariableIsSet("STEAMAPPID")) { m_versionUpdater.reset(new KisManualUpdater()); KisUsageLogger::log("Non-store package - creating updater"); } else { KisUsageLogger::log("detected appx or steam package - not creating the updater"); } #else // always create updater for MacOS m_versionUpdater.reset(new KisManualUpdater()); #endif // Q_OS_* if (!m_versionUpdater.isNull()) { connect(bnVersionUpdate, SIGNAL(clicked()), this, SLOT(slotRunVersionUpdate())); connect(bnErrorDetails, SIGNAL(clicked()), this, SLOT(slotShowUpdaterErrorDetails())); connect(m_versionUpdater.data(), SIGNAL(sigUpdateCheckStateChange(KisUpdaterStatus)), this, SLOT(slotSetUpdateStatus(const KisUpdaterStatus&))); if (m_networkIsAllowed) { // only if the user wants them m_versionUpdater->checkForUpdate(); } } #endif // ifndef Q_OS_ANDROID #endif // ENABLE_UPDATERS showNewsAction->setChecked(m_networkIsAllowed); newsWidget->setVisible(m_networkIsAllowed); versionNotificationLabel->setEnabled(m_networkIsAllowed); // Drop area.. setAcceptDrops(true); } KisWelcomePageWidget::~KisWelcomePageWidget() { } void KisWelcomePageWidget::setMainWindow(KisMainWindow* mainWin) { if (mainWin) { m_mainWindow = mainWin; // set the shortcut links from actions (only if a shortcut exists) KisActionManager *actionManager = mainWin->viewManager()->actionManager(); updateShortcutLink(newFileLink, newFileLinkShortcut, actionManager->actionByName(QStringLiteral("file_new"))); updateShortcutLink(openFileLink, openFileShortcut, actionManager->actionByName(QStringLiteral("file_open"))); connect(recentDocumentsListView, SIGNAL(clicked(QModelIndex)), this, SLOT(recentDocumentClicked(QModelIndex))); // we need the view manager to actually call actions, so don't create the connections // until after the view manager is set connect(newFileLink, SIGNAL(clicked(bool)), this, SLOT(slotNewFileClicked())); connect(openFileLink, SIGNAL(clicked(bool)), this, SLOT(slotOpenFileClicked())); connect(clearRecentFilesLink, SIGNAL(clicked(bool)), mainWin, SLOT(clearRecentFiles())); KisAction *pasteAction = mainWin->viewManager()->actionManager()->actionByName("edit_paste"); connect(pasteAction, SIGNAL(triggered()), this, SLOT(slotPaste())); slotUpdateThemeColors(); // allows RSS news items to apply analytics tracking. Q_EMIT newsWidget->setAnalyticsTracking("?" + analyticsString); KisRecentDocumentsModelWrapper *recentFilesModel = KisRecentDocumentsModelWrapper::instance(); connect(recentFilesModel, SIGNAL(sigModelIsUpToDate()), this, SLOT(slotRecentFilesModelIsUpToDate())); recentDocumentsListView->setModel(&recentFilesModel->model()); slotRecentFilesModelIsUpToDate(); } } void KisWelcomePageWidget::showDropAreaIndicator(bool show) { if (!show) { QString dropFrameStyle = QStringLiteral("QFrame#dropAreaIndicator { border: 2px solid transparent }"); dropFrameBorder->setStyleSheet(dropFrameStyle); } else { QColor textColor = qApp->palette().color(QPalette::Text); QColor backgroundColor = qApp->palette().color(QPalette::Window); QColor blendedColor = KisPaintingTweaks::blendColors(textColor, backgroundColor, 0.8); // QColor.name() turns it into a hex/web format QString dropFrameStyle = QString("QFrame#dropAreaIndicator { border: 2px dotted ").append(blendedColor.name()).append(" }") ; dropFrameBorder->setStyleSheet(dropFrameStyle); } } void KisWelcomePageWidget::slotUpdateThemeColors() { textColor = qApp->palette().color(QPalette::Text); backgroundColor = qApp->palette().color(QPalette::Window); // make the welcome screen labels a subtle color so it doesn't clash with the main UI elements blendedColor = KisPaintingTweaks::blendColors(textColor, backgroundColor, 0.8); // only apply color to the widget itself, not to the tooltip or something blendedStyle = "QWidget{color: " + blendedColor.name() + "}"; // what labels to change the color... startTitleLabel->setStyleSheet(blendedStyle); recentDocumentsLabel->setStyleSheet(blendedStyle); helpTitleLabel->setStyleSheet(blendedStyle); newsTitleLabel->setStyleSheet(blendedStyle); newFileLinkShortcut->setStyleSheet(blendedStyle); openFileShortcut->setStyleSheet(blendedStyle); clearRecentFilesLink->setStyleSheet(blendedStyle); recentDocumentsListView->setStyleSheet(blendedStyle); newsWidget->setStyleSheet(blendedStyle); #ifdef Q_OS_ANDROID blendedStyle = blendedStyle + "\nQPushButton { padding: 10px }"; #endif newFileLink->setStyleSheet(blendedStyle); openFileLink->setStyleSheet(blendedStyle); // make drop area QFrame have a dotted line dropFrameBorder->setObjectName("dropAreaIndicator"); QString dropFrameStyle = QString("QFrame#dropAreaIndicator { border: 4px dotted ").append(blendedColor.name()).append("}"); dropFrameBorder->setStyleSheet(dropFrameStyle); // only show drop area when we have a document over the empty area showDropAreaIndicator(false); // add icons for new and open settings to make them stand out a bit more openFileLink->setIconSize(QSize(48, 48)); newFileLink->setIconSize(QSize(48, 48)); openFileLink->setIcon(KisIconUtils::loadIcon("document-open")); newFileLink->setIcon(KisIconUtils::loadIcon("document-new")); btnNewsOptions->setIcon(KisIconUtils::loadIcon("view-choose")); btnNewsOptions->setFlat(true); notifyIcon->setIcon(KisIconUtils::loadIcon("notification-dot")); supportKritaIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("support-krita"))); userManualIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("help-contents"))); gettingStartedIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("get_started"))); userCommunityIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("comunity"))); kritaWebsiteIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("website"))); sourceCodeIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("code"))); kdeIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("kde"))); // HTML links seem to be a bit more stubborn with theme changes... setting inline styles to help with color change userCommunityLink->setText(QString("") .append(i18n("User Community")).append("")); gettingStartedLink->setText(QString("") .append(i18n("Getting Started")).append("")); manualLink->setText(QString("") .append(i18n("User Manual")).append("")); supportKritaLink->setText(QString("") .append(i18n("Support Krita")).append("")); kritaWebsiteLink->setText(QString("") .append(i18n("Krita Website")).append("")); sourceCodeLink->setText(QString("") .append(i18n("Source Code")).append("")); poweredByKDELink->setText(QString("") .append(i18n("Powered by KDE")).append("")); QString translationNoFeed = i18n("You can enable news from krita.org in various languages with the menu above"); labelNoFeed->setText(translationNoFeed.replace("COLOR_PLACEHOLDER", blendedColor.name())); const QColor faintTextColor = KisPaintingTweaks::blendColors(textColor, backgroundColor, 0.4); const QString &faintTextStyle = "QWidget{color: " + faintTextColor.name() + "}"; labelNoRecentDocs->setStyleSheet(faintTextStyle); labelNoFeed->setStyleSheet(faintTextStyle); const QColor frameColor = KisPaintingTweaks::blendColors(textColor, backgroundColor, 0.1); const QString &frameQss = "{border: 1px solid " + frameColor.name() + "}"; recentDocsStackedWidget->setStyleSheet("QStackedWidget#recentDocsStackedWidget" + frameQss); newsFrame->setStyleSheet("QFrame#newsFrame" + frameQss); // show the dev version labels, if dev version is detected showDevVersionHighlight(); #ifdef ENABLE_UPDATERS updateVersionUpdaterFrame(); // updater frame #endif #ifdef Q_OS_MACOS // macOS store version should not contain external links containing donation buttons or forms if (KisMacosEntitlements().sandbox()) { supportKritaLink->hide(); supportKritaIcon->hide(); labelSupportText->hide(); kritaWebsiteLink->hide(); kritaWebsiteIcon->hide(); } #endif } void KisWelcomePageWidget::dragEnterEvent(QDragEnterEvent *event) { showDropAreaIndicator(true); if (event->mimeData()->hasUrls() || event->mimeData()->hasFormat("application/x-krita-node-internal-pointer") || event->mimeData()->hasFormat("application/x-qt-image")) { return event->accept(); } return event->ignore(); } void KisWelcomePageWidget::dropEvent(QDropEvent *event) { showDropAreaIndicator(false); if (event->mimeData()->hasUrls() && !event->mimeData()->urls().empty()) { Q_FOREACH (const QUrl &url, event->mimeData()->urls()) { if (url.toLocalFile().endsWith(".bundle", Qt::CaseInsensitive)) { bool r = m_mainWindow->installBundle(url.toLocalFile()); if (!r) { qWarning() << "Could not install bundle" << url.toLocalFile(); } } else if (!url.isLocalFile()) { QScopedPointer tmp(new QTemporaryFile()); tmp->setFileName(url.fileName()); KisRemoteFileFetcher fetcher; if (!fetcher.fetchFile(url, tmp.data())) { qWarning() << "Fetching" << url << "failed"; continue; } const auto localUrl = QUrl::fromLocalFile(tmp->fileName()); m_mainWindow->openDocument(localUrl.toLocalFile(), KisMainWindow::None); } else { m_mainWindow->openDocument(url.toLocalFile(), KisMainWindow::None); } } } } void KisWelcomePageWidget::dragMoveEvent(QDragMoveEvent *event) { m_mainWindow->dragMoveEvent(event); if (event->mimeData()->hasUrls() || event->mimeData()->hasFormat("application/x-krita-node-internal-pointer") || event->mimeData()->hasFormat("application/x-qt-image")) { return event->accept(); } return event->ignore(); } void KisWelcomePageWidget::dragLeaveEvent(QDragLeaveEvent */*event*/) { showDropAreaIndicator(false); m_mainWindow->dragLeave(); } void KisWelcomePageWidget::changeEvent(QEvent *event) { if (event->type() == QEvent::FontChange) { labelSupportText->setFont(largerFont()); } } bool KisWelcomePageWidget::eventFilter(QObject *watched, QEvent *event) { if (watched == recentDocumentsListView && event->type() == QEvent::Leave) { recentDocumentsListView->clearSelection(); }else if (watched == recentDocumentsListView && event->type() == QEvent::Resize) { recentFilesResized(); } return QWidget::eventFilter(watched, event); } namespace { QString getAutoNewsLang() { // Get current UI languages: const QStringList uiLangs = KLocalizedString::languages(); QString autoNewsLang = uiLangs.first(); if (autoNewsLang.isEmpty()) { // If nothing else, use English. autoNewsLang = QString("en"); } else if (autoNewsLang == "ja") { return QString("jp"); } else if (autoNewsLang == "zh_CN") { return QString("zh"); } else if (autoNewsLang == "zh_TW") { return QString("zh-tw"); } else if (autoNewsLang == "zh_HK") { return QString("zh-hk"); } else if (autoNewsLang == "en" || autoNewsLang == "en_US" || autoNewsLang == "en_GB") { return QString("en"); } return autoNewsLang; } } /* namespace */ void KisWelcomePageWidget::setupNewsLangSelection(QMenu *newsOptionsMenu) { // Hard-coded news language data: // These are languages in which the news items should be regularly // translated into as of 04-09-2024. // The language display names should not be translated. This reflects // the language selection box on the Krita website. struct Lang { const QString siteCode; const QString name; }; static const std::array newsLangs = {{ {QString("en"), QStringLiteral("English")}, {QString("jp"), QStringLiteral("日本語")}, {QString("zh"), QStringLiteral("中文 (简体)")}, {QString("zh-tw"), QStringLiteral("中文 (台灣正體)")}, {QString("zh-hk"), QStringLiteral("廣東話 (香港)")}, {QString("ca"), QStringLiteral("Català")}, {QString("ca@valencia"), QStringLiteral("Català de Valencia")}, {QString("cs"), QStringLiteral("Čeština")}, {QString("de"), QStringLiteral("Deutsch")}, {QString("eo"), QStringLiteral("Esperanto")}, {QString("es"), QStringLiteral("Español")}, {QString("eu"), QStringLiteral("Euskara")}, {QString("fr"), QStringLiteral("Français")}, {QString("it"), QStringLiteral("Italiano")}, {QString("lt"), QStringLiteral("lietuvių")}, {QString("nl"), QStringLiteral("Nederlands")}, {QString("pt"), QStringLiteral("Português")}, {QString("sk"), QStringLiteral("Slovenský")}, {QString("sl"), QStringLiteral("Slovenski")}, {QString("sv"), QStringLiteral("Svenska")}, {QString("tr"), QStringLiteral("Türkçe")}, {QString("uk"), QStringLiteral("Українська")} }}; static const QString newsLangConfigName = QStringLiteral("FetchNewsLanguages"); QSharedPointer> enabledNewsLangs = QSharedPointer>::create(); { // Initialize with the config. KisConfig cfg(true); auto languagesList = cfg.readList(newsLangConfigName); *enabledNewsLangs = QSet(languagesList.begin(), languagesList.end()); } // If no languages are selected in the config, use the automatic selection. if (enabledNewsLangs->isEmpty()) { enabledNewsLangs->insert(QString(getAutoNewsLang())); } for (const auto &lang : newsLangs) { QAction *langItem = newsOptionsMenu->addAction(lang.name); langItem->setCheckable(true); // We can copy `code` into the lambda because its backing string is a // static string literal. const QString code = lang.siteCode; connect(langItem, &QAction::toggled, newsWidget, [=](bool checked) { newsWidget->toggleNewsLanguage(code, checked); }); // Set the initial checked state. if (enabledNewsLangs->contains(code)) { langItem->setChecked(true); } // Connect this lambda after setting the initial checked state because // we don't want to overwrite the config when doing the initial setup. connect(langItem, &QAction::toggled, [=](bool checked) { KisConfig cfg(false); // It is safe to modify `enabledNewsLangs` here, because the slots // are called synchronously on the UI thread so there is no need // for explicit synchronization. if (checked) { enabledNewsLangs->insert(QString(code)); } else { enabledNewsLangs->remove(QString(code)); } cfg.writeList(newsLangConfigName, enabledNewsLangs->values()); }); } } void KisWelcomePageWidget::showDevVersionHighlight() { // always flag development version if (isDevelopmentBuild()) { QString devBuildLabelText = QString("") .append(i18n("DEV BUILD")).append(""); devBuildLabel->setText(devBuildLabelText); devBuildIcon->setVisible(true); devBuildLabel->setVisible(true); } else { devBuildIcon->setVisible(false); devBuildLabel->setVisible(false); } } void KisWelcomePageWidget::updateShortcutLink(QToolButton *button, QLabel *label, QAction *action) { if (action) { QString shortcutText = action->shortcut().toString(QKeySequence::NativeText); if (shortcutText.isEmpty()) { label->setText(QString()); } else { label->setText(QStringLiteral("(%1)").arg(shortcutText)); } button->show(); label->show(); } else { button->hide(); label->hide(); } } void KisWelcomePageWidget::recentDocumentClicked(QModelIndex index) { QString fileUrl = index.data(Qt::ToolTipRole).toString(); m_mainWindow->openDocument(fileUrl, KisMainWindow::None ); } void KisWelcomePageWidget::slotRecentDocContextMenuRequest(const QPoint &pos) { QMenu contextMenu; QModelIndex index = recentDocumentsListView->indexAt(pos); QAction *actionForget = 0; if (index.isValid()) { actionForget = new QAction(i18n("Forget \"%1\"", index.data(Qt::DisplayRole).toString()), &contextMenu); contextMenu.addAction(actionForget); } QAction *triggered = contextMenu.exec(recentDocumentsListView->mapToGlobal(pos)); if (index.isValid() && triggered == actionForget) { m_mainWindow->removeRecentFile(index.data(Qt::ToolTipRole).toString()); } } bool KisWelcomePageWidget::isDevelopmentBuild() { return KritaVersionWrapper::isDevelopersBuild(); } void KisWelcomePageWidget::slotNewFileClicked() { m_mainWindow->slotFileNew(); } void KisWelcomePageWidget::slotOpenFileClicked() { m_mainWindow->slotFileOpen(); } void KisWelcomePageWidget::slotPaste() { if (!this->isVisible()) return; // Don't do anything if there's no image in the clipboard if (!KisClipboard::instance()->hasImage()) return; KisDlgCreateNewDocument *dlg = new KisDlgCreateNewDocument(this); dlg->SelectPage(KisDlgCreateNewDocument::Page::CreateFromClipboard); dlg->exec(); dlg->deleteLater(); } void KisWelcomePageWidget::slotRecentFilesModelIsUpToDate() { KisRecentDocumentsModelWrapper *recentFilesModel = KisRecentDocumentsModelWrapper::instance(); const bool modelIsEmpty = recentFilesModel->model().rowCount() == 0; if (modelIsEmpty) { recentDocsStackedWidget->setCurrentWidget(labelNoRecentDocs); } else { recentDocsStackedWidget->setCurrentWidget(recentDocumentsListView); } clearRecentFilesLink->setVisible(!modelIsEmpty); } #ifdef ENABLE_UPDATERS void KisWelcomePageWidget::slotToggleUpdateChecks(bool state) { if (m_versionUpdater.isNull()) { return; } m_networkIsAllowed = state; if (m_networkIsAllowed) { m_versionUpdater->checkForUpdate(); } updateVersionUpdaterFrame(); } void KisWelcomePageWidget::slotRunVersionUpdate() { if (m_versionUpdater.isNull()) { return; } if (m_networkIsAllowed) { m_versionUpdater->doUpdate(); } } void KisWelcomePageWidget::slotSetUpdateStatus(KisUpdaterStatus updateStatus) { m_updaterStatus = updateStatus; updateVersionUpdaterFrame(); } void KisWelcomePageWidget::slotShowUpdaterErrorDetails() { QMessageBox::warning(qApp->activeWindow(), i18nc("@title:window", "Krita"), m_updaterStatus.updaterOutput()); } void KisWelcomePageWidget::updateVersionUpdaterFrame() { updaterFrame->setVisible(false); versionNotificationLabel->setVisible(false); bnVersionUpdate->setVisible(false); bnErrorDetails->setVisible(false); if (!m_networkIsAllowed || m_versionUpdater.isNull()) { return; } QString versionLabelText; if (m_updaterStatus.status() == UpdaterStatus::StatusID::UPDATE_AVAILABLE) { updaterFrame->setVisible(true); updaterFrame->setEnabled(true); versionLabelText = i18n("New version of Krita is available."); versionNotificationLabel->setVisible(true); updateIcon->setIcon(KisIconUtils::loadIcon("update-medium")); if (m_versionUpdater->hasUpdateCapability()) { bnVersionUpdate->setVisible(true); } else { // build URL for label QString downloadLink = QString(" Download Krita %4") .arg(blendedColor.name()) .arg(m_updaterStatus.downloadLink()) .arg(analyticsString + "version-update") .arg(m_updaterStatus.availableVersion()); versionLabelText.append(downloadLink); } } else if ( (m_updaterStatus.status() == UpdaterStatus::StatusID::UPTODATE) || (m_updaterStatus.status() == UpdaterStatus::StatusID::CHECK_ERROR) || (m_updaterStatus.status() == UpdaterStatus::StatusID::IN_PROGRESS) ){ // no notifications, if uptodate // also, stay silent on check error - we do not want to generate lots of user support issues // because of failing wifis and proxies over the world updaterFrame->setVisible(false); } else if (m_updaterStatus.status() == UpdaterStatus::StatusID::UPDATE_ERROR) { updaterFrame->setVisible(true); versionLabelText = i18n("An error occurred during the update"); versionNotificationLabel->setVisible(true); bnErrorDetails->setVisible(true); updateIcon->setIcon(KisIconUtils::loadIcon("warning")); } else if (m_updaterStatus.status() == UpdaterStatus::StatusID::RESTART_REQUIRED) { updaterFrame->setVisible(true); versionLabelText = QString("%1 %2").arg(i18n("Restart is required.")).arg(m_updaterStatus.details()); versionNotificationLabel->setVisible(true); updateIcon->setIcon(KisIconUtils::loadIcon("view-refresh")); } versionNotificationLabel->setText(versionLabelText); if (!blendedStyle.isNull()) { versionNotificationLabel->setStyleSheet(blendedStyle); } } #endif #ifdef Q_OS_ANDROID void KisWelcomePageWidget::initDonations() { KisAndroidDonations *androidDonations = KisAndroidDonations::instance(); if (!androidDonations) { qWarning("KisWelcomePage::initDonations: androidDonations is null"); return; } // Pick a random banner. Note that the second number is *exclusive*. int bannerIndex = QRandomGenerator::global()->bounded(1, 5); // Banners have space where we can place text, but this varies by banner. // These are the relative locations where that space is. qreal ry = 0.02; // Vertical offset, always the same. qreal rh = 1.0 - ry * 2.0; // Height, dito. qreal rwpad = 0.02; // Horizontal padding. qreal rx, rw; // Horizontal offset and width, vary by banner. switch (bannerIndex) { case 1: // Kiki winking on the left. case 3: // Stargazers on the left. rx = 0.4; rw = 1.0 - rx - rwpad; break; case 2: // Cat holding brush in its mouth on the right. case 4: // Person holding digital palette on the right. rx = rwpad; rw = 0.6 - rx; break; default: // Shouldn't happen, punt to using the entire width minus padding. qWarning("Unhandled banner index %d", bannerIndex); rx = rwpad; rw = 1.0 - rwpad * 2.0; break; } QString welcomeBannerPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("share/krita/donation/welcomebanner%1.jpg").arg(bannerIndex)); QPixmap welcomeBannerPixmap(welcomeBannerPath); if (welcomeBannerPixmap.isNull()) { qWarning("KisWelcomePage::initDonations: failed to load welcome banner from '%s'", qUtf8Printable(welcomeBannerPath)); // Leave the button alone, it will just say "Support Krita!" } else { QVector headlines = { i18n("Become a Supporter!"), i18n("Support Krita!"), }; QVector subtitles = { i18n("Supporters get brush packs and more."), i18n("Contributions keep development going."), }; QString headline = headlines[QRandomGenerator::global()->bounded(headlines.size())]; QString subtitle = subtitles[QRandomGenerator::global()->bounded(subtitles.size())]; // We want some text on the banner, which unfortunately requires some // manual layout. We're going to write a top line in a larger font and // a bottom line in a smaller font. They're going to be separated by a // few pixels, centered horizontally and fit into predefined bounds on // the banner depending on which image we're using. The text will be // white with a black outline, so we'll use QPainterPath. Math time. QPainter painter(&welcomeBannerPixmap); painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::TextAntialiasing); QPen pen(Qt::black, 12.0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin); QBrush brush(Qt::white); qreal verticalSeparation = 40.0; // Top line, the heading. QFont currentFont = font(); currentFont.setPointSize(100); currentFont.setBold(false); QPainterPath topPath; topPath.addText(0, 0, currentFont, headline); QRectF topBounds = topPath.boundingRect(); // Bottom line, smaller text for more words. currentFont.setPointSize(60); currentFont.setBold(true); QPainterPath bottomPath; bottomPath.addText(0, 0, currentFont, subtitle); QRectF bottomBounds = bottomPath.boundingRect(); // Stick those two lines into a combined path, horizontally centered, // separated a bit so that the text doesn't get glued together. qreal maxWidth = qMax(topBounds.width(), bottomBounds.width()); qreal topOffsetX = (maxWidth - topBounds.width()) / 2.0; qreal bottomOffsetX = (maxWidth - bottomBounds.width()) / 2.0; QTransform topTransform; topTransform.translate(topOffsetX - topBounds.left(), -topBounds.top()); topPath = topTransform.map(topPath); topPath.setFillRule(Qt::WindingFill); QTransform bottomTransform; bottomTransform.translate(bottomOffsetX - bottomBounds.left(), topBounds.height() + verticalSeparation - bottomBounds.top()); bottomPath = bottomTransform.map(bottomPath); bottomPath.setFillRule(Qt::WindingFill); QPainterPath combinedPath; combinedPath.addPath(topPath); combinedPath.addPath(bottomPath); // Determine the rectangle on the banner that we want to fit the text // into. Taking a detour through the paint device just in case high-DPI // scaling messes with the dimensions somehow. qreal w = painter.device()->width(); qreal h = painter.device()->height(); QRect rect(w * rx, h * ry, w * rw, h * rh); QRectF combinedBounds = combinedPath.boundingRect(); qreal scaleX = rect.width() / combinedBounds.width(); qreal scaleY = rect.height() / combinedBounds.height(); qreal scale = qMin(scaleX, scaleY); qreal combinedOffsetX = (rect.width() - (combinedBounds.width() * scale)) / 2.0; qreal combinedOffsetY = (rect.height() - (combinedBounds.height() * scale)) / 2.0; QTransform combinedTransform; combinedTransform.translate(rect.left() + combinedOffsetX, rect.top() + combinedOffsetY); combinedTransform.scale(scale, scale); combinedTransform.translate(-combinedBounds.left(), -combinedBounds.top()); combinedPath = combinedTransform.map(combinedPath); combinedPath.setFillRule(Qt::WindingFill); // And finally, draw the text onto the banner. First stroking the // outside, then filling the inside, otherwise there's weird effects // with strokes inside of letters overlapping with the fill. painter.setPen(pen); painter.setBrush(Qt::NoBrush); painter.drawPath(combinedPath); painter.setPen(Qt::NoPen); painter.setBrush(brush); painter.drawPath(combinedPath); QSize welcomeBannerSize = welcomeBannerPixmap.size().scaled(500, 100, Qt::KeepAspectRatio); bnAndroidSupport->setFlat(false); bnAndroidSupport->setText(QString()); bnAndroidSupport->setContentsMargins(0, 0, 0, 0); bnAndroidSupport->setIcon(QIcon(welcomeBannerPixmap)); bnAndroidSupport->setIconSize(welcomeBannerSize); bnAndroidSupport->setFixedSize(welcomeBannerSize); } connect(bnAndroidSupport, &QPushButton::clicked, androidDonations, &KisAndroidDonations::slotStartDonationFlow); connect(bnAndroidSupporterManage, &QPushButton::clicked, androidDonations, &KisAndroidDonations::sigShowDonationManagementDialogRequested); QString badgePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, "share/krita/donation/banner.png"); QPixmap badgePixmap(badgePath); if (badgePixmap.isNull()) { qWarning("KisWelcomePage::initDonations: failed to load badge from '%s'", qUtf8Printable(badgePath)); } else { supporterBadge->setPixmap(badgePixmap); } connect(androidDonations, SIGNAL(sigStateChanged()), this, SLOT(slotUpdateDonationState())); slotUpdateDonationState(); } void KisWelcomePageWidget::slotUpdateDonationState() { bool badgeVisible = false; QWidget *pageVisible = pgSupportMessage; KisAndroidDonations *androidDonations = KisAndroidDonations::instance(); if (androidDonations) { badgeVisible = androidDonations->shouldShowSupporterBadge(); switch (androidDonations->state()) { case KisAndroidDonations::State::Supporter: pageVisible = pgAndroidSupporter; break; case KisAndroidDonations::State::NoSupport: pageVisible = nullptr; break; default: break; } } else { qWarning("KisWelcomePageWidget::slotUpdateDonationState: android donations is null"); } if(pageVisible) { stkSupport->setCurrentWidget(pageVisible); wdgAndroidSupportBanner->hide(); stkSupport->show(); } else { stkSupport->hide(); wdgAndroidSupportBanner->show(); } supporterBadge->setVisible(badgeVisible); } #endif QFont KisWelcomePageWidget::largerFont() { QFont larger = font(); // Font size may be in pixels (on Android) or points (everywhere else.) qreal ratio = 1.1; if (larger.pixelSize() == -1) { larger.setPointSizeF(larger.pointSizeF() * ratio); } else { larger.setPixelSize(qRound(larger.pixelSize() * ratio)); } return larger; } void KisWelcomePageWidget::recentFilesResized() { int scrollSize = qApp->style()->pixelMetric(QStyle::PM_SliderThickness); int spacing = 8; int iconLength = KisRecentDocumentsModelWrapper::DEFALT_ICON_SIZE_LENGTH + spacing; int currentSize = recentDocumentsListView->size().width()-scrollSize-2; int iconAmount = floor(currentSize/iconLength); int length = floor(currentSize / iconAmount)-spacing; recentDocumentsListView->setIconSize(QSize(length,length)); } void KisWelcomePageWidget::newsStateChanged(bool hasNews) { btnDismisAll->setVisible(hasNews); notifyIcon->setVisible(hasNews); //colapse or expand notifyIcon and btnDismisAll QSizePolicy sizePolicy = notifyIcon->sizePolicy(); if (hasNews) { sizePolicy.setHorizontalPolicy(QSizePolicy::Preferred); } else { sizePolicy.setHorizontalPolicy(QSizePolicy::Ignored); } notifyIcon->setSizePolicy(sizePolicy); //notifyIcon and btnDismisAll should both have the same size policy btnDismisAll->setSizePolicy(sizePolicy); }