Krita Source Code Documentation
Loading...
Searching...
No Matches
KisWelcomePageWidget.cpp
Go to the documentation of this file.
1
2/* This file is part of the KDE project
3 * SPDX-FileCopyrightText: 2018 Scott Petrovic <scottpetrovic@gmail.com>
4 * SPDX-FileCopyrightText: 2021 L. E. Segovia <amy@amyspark.me>
5 *
6 * SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8
11#include <QDesktopServices>
12#include <QMimeData>
13#include <QPixmap>
14#include <QMessageBox>
15#include <QTemporaryFile>
16#include <QBuffer>
17#include <QNetworkAccessManager>
18#include <QEventLoop>
19#include <QDomDocument>
20
22#include "kactioncollection.h"
23#include "kis_action.h"
24#include "kis_action_manager.h"
26#include <KisMimeDatabase.h>
27#include <KisApplication.h>
28
29#include "KConfigGroup"
30#include "KSharedConfig"
31
32#include <QListWidget>
33#include <QListWidgetItem>
34#include <QMenu>
35#include <QScrollBar>
36
37#include "kis_clipboard.h"
38#include "kis_icon_utils.h"
39#include <kis_painting_tweaks.h>
40#include "KoStore.h"
41#include "kis_config.h"
42#include "KisDocument.h"
43#include <kis_image.h>
44#include <kis_paint_device.h>
45#include <KisPart.h>
46#include <KisKineticScroller.h>
47#include "KisMainWindow.h"
48
50
51#include <QCoreApplication>
52#include <kis_debug.h>
53#include <QDir>
54
55#include <array>
56
57#include "config-updaters.h"
58
59#ifdef ENABLE_UPDATERS
60#ifdef Q_OS_LINUX
62#endif
63
65#endif
66
67#include <klocalizedstring.h>
68#include <KritaVersionWrapper.h>
69
70#include <KisUsageLogger.h>
71#include <QSysInfo>
72#include <kis_config.h>
73#include <kis_image_config.h>
74#include "opengl/kis_opengl.h"
75
76#ifdef Q_OS_WIN
78#endif
79
80#ifdef Q_OS_MACOS
82#endif
83
84#ifdef Q_OS_ANDROID
85#include "KisAndroidDonations.h"
86#include <QFontMetrics>
87#include <QPaintDevice>
88#include <QPainter>
89#include <QPainterPath>
90#include <QRandomGenerator>
91#endif
92
93// Used for triggering a QAction::setChecked signal from a QLabel::linkActivated signal
94void ShowNewsAction::enableFromLink(QString unused_url)
95{
96 Q_UNUSED(unused_url);
97 Q_EMIT setChecked(true);
98}
99
100
101// class to override item height for Breeze since qss seems to not work
102class RecentItemDelegate : public QStyledItemDelegate
103{
104 int itemHeight = 0;
105public:
106 RecentItemDelegate(QObject *parent = 0)
107 : QStyledItemDelegate(parent)
108 {
109 }
110
112 {
113 this->itemHeight = itemHeight;
114 }
115
116 QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &/*index*/) const override
117 {
118 return QSize(option.rect.width(), itemHeight);
119 }
120};
121
122
124 : QWidget(parent)
125{
126 setupUi(this);
127
128 // URLs that go to web browser...
129 devBuildIcon->setIcon(KisIconUtils::loadIcon("warning"));
130 devBuildLabel->setVisible(false);
131 updaterFrame->setVisible(false);
132 versionNotificationLabel->setVisible(false);
133 bnVersionUpdate->setVisible(false);
134 bnErrorDetails->setVisible(false);
135
136 // Recent docs...
137 recentDocumentsListView->setDragEnabled(false);
138 recentDocumentsListView->viewport()->setAutoFillBackground(false);
139 recentDocumentsListView->setSpacing(2);
140 recentDocumentsListView->installEventFilter(this);
141 recentDocumentsListView->setViewMode(QListView::IconMode);
142 recentDocumentsListView->setSelectionMode(QAbstractItemView::NoSelection);
143
144// m_recentItemDelegate.reset(new RecentItemDelegate(this));
145// m_recentItemDelegate->setItemHeight(KisRecentDocumentsModelWrapper::ICON_SIZE_LENGTH);
146// recentDocumentsListView->setItemDelegate(m_recentItemDelegate.data());
148 recentDocumentsListView->setVerticalScrollMode(QListView::ScrollPerPixel);
149 recentDocumentsListView->verticalScrollBar()->setSingleStep(50);
150 {
151 QScroller* scroller = KisKineticScroller::createPreconfiguredScroller(recentDocumentsListView);
152 if (scroller) {
153 connect(scroller, SIGNAL(stateChanged(QScroller::State)), this, SLOT(slotScrollerStateChanged(QScroller::State)));
154 }
155 }
156 recentDocumentsListView->setContextMenuPolicy(Qt::CustomContextMenu);
157 connect(recentDocumentsListView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotRecentDocContextMenuRequest(QPoint)));
158
159 // News widget...
160 QMenu *newsOptionsMenu = new QMenu(this);
161 newsOptionsMenu->setToolTipsVisible(true);
162 ShowNewsAction *showNewsAction = new ShowNewsAction(i18n("Enable news and check for new releases"), newsOptionsMenu);
163 newsOptionsMenu->addAction(showNewsAction);
164 showNewsAction->setToolTip(i18n("Show news about Krita: this needs internet to retrieve information from the krita.org website"));
165 showNewsAction->setCheckable(true);
166
167 newsOptionsMenu->addSection(i18n("Language"));
168 QAction *newsInfoAction = newsOptionsMenu->addAction(i18n("English news is always up to date."));
169 newsInfoAction->setEnabled(false);
170
171 setupNewsLangSelection(newsOptionsMenu);
172 btnNewsOptions->setMenu(newsOptionsMenu);
173
174 labelSupportText->setFont(largerFont());
175
176 connect(showNewsAction, SIGNAL(toggled(bool)), newsWidget, SLOT(setVisible(bool)));
177 connect(showNewsAction, SIGNAL(toggled(bool)), labelNoFeed, SLOT(setHidden(bool)));
178 connect(showNewsAction, SIGNAL(toggled(bool)), newsWidget, SLOT(toggleNews(bool)));
179 connect(labelNoFeed, SIGNAL(linkActivated(QString)), showNewsAction, SLOT(enableFromLink(QString)));
180
181#ifdef ENABLE_UPDATERS
182 connect(showNewsAction, SIGNAL(toggled(bool)), this, SLOT(slotToggleUpdateChecks(bool)));
183#endif
184
185 supporterBadge->hide();
186 wdgAndroidSupportBanner->hide();
187#ifdef Q_OS_ANDROID
188 initDonations();
189#endif
190
191 // configure the News area
192 KisConfig cfg(true);
193 m_networkIsAllowed = cfg.readEntry<bool>("FetchNews", false);
194
195
196#ifdef ENABLE_UPDATERS
197#ifndef Q_OS_ANDROID
198 // Setup version updater, but do not check for them, unless the user explicitly
199 // wants to check for updates.
200 // * No updater is created for Linux/Steam, Windows/Steam and Windows/Store distributions,
201 // as those stores have their own updating mechanism.
202 // * STEAMAPPID(Windows)/SteamAppId(Linux) environment variable is set when Krita is run from Steam.
203 // The environment variables are not public API.
204 // * MS Store version runs as a package (though we cannot know if it was
205 // installed from the Store or manually with the .msix package)
206#if defined Q_OS_LINUX
207 if (!qEnvironmentVariableIsSet("SteamAppId")) { // do not create updater for linux/steam
208 if (qEnvironmentVariableIsSet("APPIMAGE")) {
209 m_versionUpdater.reset(new KisAppimageUpdater());
210 } else {
211 m_versionUpdater.reset(new KisManualUpdater());
212 }
213 }
214#elif defined Q_OS_WIN
215 if (!KisWindowsPackageUtils::isRunningInPackage() && !qEnvironmentVariableIsSet("STEAMAPPID")) {
216 m_versionUpdater.reset(new KisManualUpdater());
217 KisUsageLogger::log("Non-store package - creating updater");
218 } else {
219 KisUsageLogger::log("detected appx or steam package - not creating the updater");
220 }
221#else
222 // always create updater for MacOS
223 m_versionUpdater.reset(new KisManualUpdater());
224#endif // Q_OS_*
225 if (!m_versionUpdater.isNull()) {
226 connect(bnVersionUpdate, SIGNAL(clicked()), this, SLOT(slotRunVersionUpdate()));
227 connect(bnErrorDetails, SIGNAL(clicked()), this, SLOT(slotShowUpdaterErrorDetails()));
228 connect(m_versionUpdater.data(), SIGNAL(sigUpdateCheckStateChange(KisUpdaterStatus)),
229 this, SLOT(slotSetUpdateStatus(const KisUpdaterStatus&)));
230
231 if (m_networkIsAllowed) { // only if the user wants them
232 m_versionUpdater->checkForUpdate();
233 }
234 }
235#endif // ifndef Q_OS_ANDROID
236#endif // ENABLE_UPDATERS
237
238
239 showNewsAction->setChecked(m_networkIsAllowed);
240 newsWidget->setVisible(m_networkIsAllowed);
241 versionNotificationLabel->setEnabled(m_networkIsAllowed);
242
243 // Drop area..
244 setAcceptDrops(true);
245}
246
250
252{
253 if (mainWin) {
254 m_mainWindow = mainWin;
255
256 // set the shortcut links from actions (only if a shortcut exists)
257 KisActionManager *actionManager = mainWin->viewManager()->actionManager();
258 updateShortcutLink(newFileLink, newFileLinkShortcut, actionManager->actionByName(QStringLiteral("file_new")));
259 updateShortcutLink(openFileLink, openFileShortcut, actionManager->actionByName(QStringLiteral("file_open")));
260 connect(recentDocumentsListView, SIGNAL(clicked(QModelIndex)), this, SLOT(recentDocumentClicked(QModelIndex)));
261 // we need the view manager to actually call actions, so don't create the connections
262 // until after the view manager is set
263 connect(newFileLink, SIGNAL(clicked(bool)), this, SLOT(slotNewFileClicked()));
264 connect(openFileLink, SIGNAL(clicked(bool)), this, SLOT(slotOpenFileClicked()));
265 connect(clearRecentFilesLink, SIGNAL(clicked(bool)), mainWin, SLOT(clearRecentFiles()));
266
267 KisAction *pasteAction = mainWin->viewManager()->actionManager()->actionByName("edit_paste");
268 connect(pasteAction, SIGNAL(triggered()), this, SLOT(slotPaste()));
269
271
272 // allows RSS news items to apply analytics tracking.
273 newsWidget->setAnalyticsTracking("?" + analyticsString);
274
276 connect(recentFilesModel, SIGNAL(sigModelIsUpToDate()), this, SLOT(slotRecentFilesModelIsUpToDate()));
277 recentDocumentsListView->setModel(&recentFilesModel->model());
279 }
280}
281
282
284{
285 if (!show) {
286 QString dropFrameStyle = QStringLiteral("QFrame#dropAreaIndicator { border: 2px solid transparent }");
287 dropFrameBorder->setStyleSheet(dropFrameStyle);
288 } else {
289 QColor textColor = qApp->palette().color(QPalette::Text);
290 QColor backgroundColor = qApp->palette().color(QPalette::Window);
292
293 // QColor.name() turns it into a hex/web format
294 QString dropFrameStyle = QString("QFrame#dropAreaIndicator { border: 2px dotted ").append(blendedColor.name()).append(" }") ;
295 dropFrameBorder->setStyleSheet(dropFrameStyle);
296 }
297}
298
300{
301 textColor = qApp->palette().color(QPalette::Text);
302 backgroundColor = qApp->palette().color(QPalette::Window);
303
304 // make the welcome screen labels a subtle color so it doesn't clash with the main UI elements
306 // only apply color to the widget itself, not to the tooltip or something
307 blendedStyle = "QWidget{color: " + blendedColor.name() + "}";
308
309 // what labels to change the color...
310 startTitleLabel->setStyleSheet(blendedStyle);
311 recentDocumentsLabel->setStyleSheet(blendedStyle);
312 helpTitleLabel->setStyleSheet(blendedStyle);
313 newsTitleLabel->setStyleSheet(blendedStyle);
314 newFileLinkShortcut->setStyleSheet(blendedStyle);
315 openFileShortcut->setStyleSheet(blendedStyle);
316 clearRecentFilesLink->setStyleSheet(blendedStyle);
317 recentDocumentsListView->setStyleSheet(blendedStyle);
318 newsWidget->setStyleSheet(blendedStyle);
319
320#ifdef Q_OS_ANDROID
321 blendedStyle = blendedStyle + "\nQPushButton { padding: 10px }";
322#endif
323
324 newFileLink->setStyleSheet(blendedStyle);
325 openFileLink->setStyleSheet(blendedStyle);
326
327 // make drop area QFrame have a dotted line
328 dropFrameBorder->setObjectName("dropAreaIndicator");
329 QString dropFrameStyle = QString("QFrame#dropAreaIndicator { border: 4px dotted ").append(blendedColor.name()).append("}");
330 dropFrameBorder->setStyleSheet(dropFrameStyle);
331
332 // only show drop area when we have a document over the empty area
334
335 // add icons for new and open settings to make them stand out a bit more
336 openFileLink->setIconSize(QSize(48, 48));
337 newFileLink->setIconSize(QSize(48, 48));
338
339 openFileLink->setIcon(KisIconUtils::loadIcon("document-open"));
340 newFileLink->setIcon(KisIconUtils::loadIcon("document-new"));
341
342 btnNewsOptions->setIcon(KisIconUtils::loadIcon("view-choose"));
343 btnNewsOptions->setFlat(true);
344
345 supportKritaIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("support-krita")));
346 userManualIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("bookmarks")));
347 gettingStartedIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("get_started")));
348 userCommunityIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("comunity")));
349 kritaWebsiteIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("website")));
350 sourceCodeIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("code")));
351 kdeIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("kde")));
352
353 // HTML links seem to be a bit more stubborn with theme changes... setting inline styles to help with color change
354 userCommunityLink->setText(QString("<a style=\"color: " + blendedColor.name() + " \" href=\"https://krita-artists.org\">")
355 .append(i18n("User Community")).append("</a>"));
356
357 gettingStartedLink->setText(QString("<a style=\"color: " + blendedColor.name() + " \" href=\"https://docs.krita.org/user_manual/getting_started.html\">")
358 .append(i18n("Getting Started")).append("</a>"));
359
360 manualLink->setText(QString("<a style=\"color: " + blendedColor.name() + " \" href=\"https://docs.krita.org\">")
361 .append(i18n("User Manual")).append("</a>"));
362
363 supportKritaLink->setText(QString("<a style=\"color: " + blendedColor.name() + " \" href=\"https://krita.org/support-us/donations?" + analyticsString + "donations" + "\">")
364 .append(i18n("Support Krita")).append("</a>"));
365
366 kritaWebsiteLink->setText(QString("<a style=\"color: " + blendedColor.name() + " \" href=\"https://www.krita.org?" + analyticsString + "marketing-site" + "\">")
367 .append(i18n("Krita Website")).append("</a>"));
368
369 sourceCodeLink->setText(QString("<a style=\"color: " + blendedColor.name() + " \" href=\"https://invent.kde.org/graphics/krita\">")
370 .append(i18n("Source Code")).append("</a>"));
371
372 poweredByKDELink->setText(QString("<a style=\"color: " + blendedColor.name() + " \" href=\"https://userbase.kde.org/What_is_KDE\">")
373 .append(i18n("Powered by KDE")).append("</a>"));
374
375 QString translationNoFeed = i18n("You can <a href=\"ignored\" style=\"color: COLOR_PLACEHOLDER; text-decoration: underline;\">enable news</a> from krita.org in various languages with the menu above");
376 labelNoFeed->setText(translationNoFeed.replace("COLOR_PLACEHOLDER", blendedColor.name()));
377
378 const QColor faintTextColor = KisPaintingTweaks::blendColors(textColor, backgroundColor, 0.4);
379 const QString &faintTextStyle = "QWidget{color: " + faintTextColor.name() + "}";
380 labelNoRecentDocs->setStyleSheet(faintTextStyle);
381 labelNoFeed->setStyleSheet(faintTextStyle);
382
383 const QColor frameColor = KisPaintingTweaks::blendColors(textColor, backgroundColor, 0.1);
384 const QString &frameQss = "{border: 1px solid " + frameColor.name() + "}";
385 recentDocsStackedWidget->setStyleSheet("QStackedWidget#recentDocsStackedWidget" + frameQss);
386 newsFrame->setStyleSheet("QFrame#newsFrame" + frameQss);
387
388 // show the dev version labels, if dev version is detected
390
391#ifdef ENABLE_UPDATERS
392 updateVersionUpdaterFrame(); // updater frame
393#endif
394
395#ifdef Q_OS_MACOS
396 // macOS store version should not contain external links containing donation buttons or forms
397 if (KisMacosEntitlements().sandbox()) {
398 supportKritaLink->hide();
399 supportKritaIcon->hide();
400 labelSupportText->hide();
401 kritaWebsiteLink->hide();
402 kritaWebsiteIcon->hide();
403 }
404#endif
405}
406
407void KisWelcomePageWidget::dragEnterEvent(QDragEnterEvent *event)
408{
410 if (event->mimeData()->hasUrls() ||
411 event->mimeData()->hasFormat("application/x-krita-node-internal-pointer") ||
412 event->mimeData()->hasFormat("application/x-qt-image")) {
413 return event->accept();
414 }
415
416 return event->ignore();
417}
418
419void KisWelcomePageWidget::dropEvent(QDropEvent *event)
420{
422
423 if (event->mimeData()->hasUrls() && !event->mimeData()->urls().empty()) {
424 Q_FOREACH (const QUrl &url, event->mimeData()->urls()) {
425 if (url.toLocalFile().endsWith(".bundle", Qt::CaseInsensitive)) {
426 bool r = m_mainWindow->installBundle(url.toLocalFile());
427 if (!r) {
428 qWarning() << "Could not install bundle" << url.toLocalFile();
429 }
430 } else if (!url.isLocalFile()) {
431 QScopedPointer<QTemporaryFile> tmp(new QTemporaryFile());
432 tmp->setFileName(url.fileName());
433
434 KisRemoteFileFetcher fetcher;
435
436 if (!fetcher.fetchFile(url, tmp.data())) {
437 qWarning() << "Fetching" << url << "failed";
438 continue;
439 }
440 const auto localUrl = QUrl::fromLocalFile(tmp->fileName());
441
442 m_mainWindow->openDocument(localUrl.toLocalFile(), KisMainWindow::None);
443 } else {
444 m_mainWindow->openDocument(url.toLocalFile(), KisMainWindow::None);
445 }
446 }
447 }
448}
449
450void KisWelcomePageWidget::dragMoveEvent(QDragMoveEvent *event)
451{
453
454 if (event->mimeData()->hasUrls() ||
455 event->mimeData()->hasFormat("application/x-krita-node-internal-pointer") ||
456 event->mimeData()->hasFormat("application/x-qt-image")) {
457 return event->accept();
458 }
459
460 return event->ignore();
461}
462
463void KisWelcomePageWidget::dragLeaveEvent(QDragLeaveEvent */*event*/)
464{
467}
468
470{
471 if (event->type() == QEvent::FontChange) {
472 labelSupportText->setFont(largerFont());
473 }
474}
475
476bool KisWelcomePageWidget::eventFilter(QObject *watched, QEvent *event)
477{
478 if (watched == recentDocumentsListView && event->type() == QEvent::Leave) {
479 recentDocumentsListView->clearSelection();
480 }
481 return QWidget::eventFilter(watched, event);
482}
483
484namespace {
485
486QString getAutoNewsLang()
487{
488 // Get current UI languages:
489 const QStringList uiLangs = KLocalizedString::languages();
490 QString autoNewsLang = uiLangs.first();
491 if (autoNewsLang.isEmpty()) {
492 // If nothing else, use English.
493 autoNewsLang = QString("en");
494 } else if (autoNewsLang == "ja") {
495 return QString("jp");
496 } else if (autoNewsLang == "zh_CN") {
497 return QString("zh");
498 } else if (autoNewsLang == "zh_TW") {
499 return QString("zh-tw");
500 } else if (autoNewsLang == "zh_HK") {
501 return QString("zh-hk");
502 } else if (autoNewsLang == "en" || autoNewsLang == "en_US" || autoNewsLang == "en_GB") {
503 return QString("en");
504 }
505
506 return autoNewsLang;
507}
508
509} /* namespace */
510
512{
513 // Hard-coded news language data:
514 // These are languages in which the news items should be regularly
515 // translated into as of 04-09-2024.
516 // The language display names should not be translated. This reflects
517 // the language selection box on the Krita website.
518 struct Lang {
519 const QString siteCode;
520 const QString name;
521 };
522 static const std::array<Lang, 22> newsLangs = {{
523 {QString("en"), QStringLiteral("English")},
524 {QString("jp"), QStringLiteral("日本語")},
525 {QString("zh"), QStringLiteral("中文 (简体)")},
526 {QString("zh-tw"), QStringLiteral("中文 (台灣正體)")},
527 {QString("zh-hk"), QStringLiteral("廣東話 (香港)")},
528 {QString("ca"), QStringLiteral("Català")},
529 {QString("ca@valencia"), QStringLiteral("Català de Valencia")},
530 {QString("cs"), QStringLiteral("Čeština")},
531 {QString("de"), QStringLiteral("Deutsch")},
532 {QString("eo"), QStringLiteral("Esperanto")},
533 {QString("es"), QStringLiteral("Español")},
534 {QString("eu"), QStringLiteral("Euskara")},
535 {QString("fr"), QStringLiteral("Français")},
536 {QString("it"), QStringLiteral("Italiano")},
537 {QString("lt"), QStringLiteral("lietuvių")},
538 {QString("nl"), QStringLiteral("Nederlands")},
539 {QString("pt"), QStringLiteral("Português")},
540 {QString("sk"), QStringLiteral("Slovenský")},
541 {QString("sl"), QStringLiteral("Slovenski")},
542 {QString("sv"), QStringLiteral("Svenska")},
543 {QString("tr"), QStringLiteral("Türkçe")},
544 {QString("uk"), QStringLiteral("Українська")}
545 }};
546
547 static const QString newsLangConfigName = QStringLiteral("FetchNewsLanguages");
548
549 QSharedPointer<QSet<QString>> enabledNewsLangs = QSharedPointer<QSet<QString>>::create();
550 {
551 // Initialize with the config.
552 KisConfig cfg(true);
553 auto languagesList = cfg.readList<QString>(newsLangConfigName);
554 *enabledNewsLangs = QSet(languagesList.begin(), languagesList.end());
555 }
556
557 // If no languages are selected in the config, use the automatic selection.
558 if (enabledNewsLangs->isEmpty()) {
559 enabledNewsLangs->insert(QString(getAutoNewsLang()));
560 }
561
562 for (const auto &lang : newsLangs) {
563 QAction *langItem = newsOptionsMenu->addAction(lang.name);
564 langItem->setCheckable(true);
565 // We can copy `code` into the lambda because its backing string is a
566 // static string literal.
567 const QString code = lang.siteCode;
568 connect(langItem, &QAction::toggled, newsWidget, [=](bool checked) {
569 newsWidget->toggleNewsLanguage(code, checked);
570 });
571
572 // Set the initial checked state.
573 if (enabledNewsLangs->contains(code)) {
574 langItem->setChecked(true);
575 }
576
577 // Connect this lambda after setting the initial checked state because
578 // we don't want to overwrite the config when doing the initial setup.
579 connect(langItem, &QAction::toggled, [=](bool checked) {
580 KisConfig cfg(false);
581 // It is safe to modify `enabledNewsLangs` here, because the slots
582 // are called synchronously on the UI thread so there is no need
583 // for explicit synchronization.
584 if (checked) {
585 enabledNewsLangs->insert(QString(code));
586 } else {
587 enabledNewsLangs->remove(QString(code));
588 }
589 cfg.writeList(newsLangConfigName, enabledNewsLangs->values());
590 });
591 }
592}
593
595{
596 // always flag development version
597 if (isDevelopmentBuild()) {
598 QString devBuildLabelText = QString("<a style=\"color: " +
599 blendedColor.name() +
600 " \" href=\"https://docs.krita.org/en/untranslatable_pages/triaging_bugs.html?"
601 + analyticsString + "dev-build" + "\">")
602 .append(i18n("DEV BUILD")).append("</a>");
603
604 devBuildLabel->setText(devBuildLabelText);
605 devBuildIcon->setVisible(true);
606 devBuildLabel->setVisible(true);
607 } else {
608 devBuildIcon->setVisible(false);
609 devBuildLabel->setVisible(false);
610 }
611}
612
613void KisWelcomePageWidget::updateShortcutLink(QToolButton *button, QLabel *label, QAction *action)
614{
615 if (action) {
616 QString shortcutText = action->shortcut().toString(QKeySequence::NativeText);
617 if (shortcutText.isEmpty()) {
618 label->setText(QString());
619 } else {
620 label->setText(QStringLiteral("(%1)").arg(shortcutText));
621 }
622 button->show();
623 label->show();
624 } else {
625 button->hide();
626 label->hide();
627 }
628}
629
631{
632 QString fileUrl = index.data(Qt::ToolTipRole).toString();
634}
635
637{
638 QMenu contextMenu;
639 QModelIndex index = recentDocumentsListView->indexAt(pos);
640 QAction *actionForget = 0;
641 if (index.isValid()) {
642 actionForget = new QAction(i18n("Forget \"%1\"", index.data(Qt::DisplayRole).toString()), &contextMenu);
643 contextMenu.addAction(actionForget);
644 }
645 QAction *triggered = contextMenu.exec(recentDocumentsListView->mapToGlobal(pos));
646
647 if (index.isValid() && triggered == actionForget) {
648 m_mainWindow->removeRecentFile(index.data(Qt::ToolTipRole).toString());
649 }
650}
651
656
661
666
668{
669 if (!this->isVisible())
670 return;
671
672 // Don't do anything if there's no image in the clipboard
673 if (!KisClipboard::instance()->hasImage())
674 return;
675
677
679 dlg->exec();
680 dlg->deleteLater();
681}
682
684{
686 const bool modelIsEmpty = recentFilesModel->model().rowCount() == 0;
687
688 if (modelIsEmpty) {
689 recentDocsStackedWidget->setCurrentWidget(labelNoRecentDocs);
690 } else {
691 recentDocsStackedWidget->setCurrentWidget(recentDocumentsListView);
692 }
693 clearRecentFilesLink->setVisible(!modelIsEmpty);
694}
695
696#ifdef ENABLE_UPDATERS
697void KisWelcomePageWidget::slotToggleUpdateChecks(bool state)
698{
699 if (m_versionUpdater.isNull()) {
700 return;
701 }
702
703 m_networkIsAllowed = state;
704
705 if (m_networkIsAllowed) {
706 m_versionUpdater->checkForUpdate();
707 }
708
709 updateVersionUpdaterFrame();
710}
711void KisWelcomePageWidget::slotRunVersionUpdate()
712{
713 if (m_versionUpdater.isNull()) {
714 return;
715 }
716
717 if (m_networkIsAllowed) {
718 m_versionUpdater->doUpdate();
719 }
720}
721
722void KisWelcomePageWidget::slotSetUpdateStatus(KisUpdaterStatus updateStatus)
723{
724 m_updaterStatus = updateStatus;
725 updateVersionUpdaterFrame();
726}
727
728void KisWelcomePageWidget::slotShowUpdaterErrorDetails()
729{
730 QMessageBox::warning(qApp->activeWindow(), i18nc("@title:window", "Krita"), m_updaterStatus.updaterOutput());
731}
732
733void KisWelcomePageWidget::updateVersionUpdaterFrame()
734{
735 updaterFrame->setVisible(false);
736 versionNotificationLabel->setVisible(false);
737 bnVersionUpdate->setVisible(false);
738 bnErrorDetails->setVisible(false);
739
740 if (!m_networkIsAllowed || m_versionUpdater.isNull()) {
741 return;
742 }
743
744 QString versionLabelText;
745
746 if (m_updaterStatus.status() == UpdaterStatus::StatusID::UPDATE_AVAILABLE) {
747 updaterFrame->setVisible(true);
748 updaterFrame->setEnabled(true);
749 versionLabelText = i18n("New version of Krita is available.");
750 versionNotificationLabel->setVisible(true);
751 updateIcon->setIcon(KisIconUtils::loadIcon("update-medium"));
752
753 if (m_versionUpdater->hasUpdateCapability()) {
754 bnVersionUpdate->setVisible(true);
755 } else {
756 // build URL for label
757 QString downloadLink = QString(" <a style=\"color: %1; text-decoration: underline\" href=\"%2?%3\">Download Krita %4</a>")
758 .arg(blendedColor.name())
759 .arg(m_updaterStatus.downloadLink())
760 .arg(analyticsString + "version-update")
761 .arg(m_updaterStatus.availableVersion());
762
763 versionLabelText.append(downloadLink);
764 }
765
766 } else if (
767 (m_updaterStatus.status() == UpdaterStatus::StatusID::UPTODATE)
768 || (m_updaterStatus.status() == UpdaterStatus::StatusID::CHECK_ERROR)
769 || (m_updaterStatus.status() == UpdaterStatus::StatusID::IN_PROGRESS)
770 ){
771 // no notifications, if uptodate
772 // also, stay silent on check error - we do not want to generate lots of user support issues
773 // because of failing wifis and proxies over the world
774 updaterFrame->setVisible(false);
775
776 } else if (m_updaterStatus.status() == UpdaterStatus::StatusID::UPDATE_ERROR) {
777 updaterFrame->setVisible(true);
778 versionLabelText = i18n("An error occurred during the update");
779 versionNotificationLabel->setVisible(true);
780 bnErrorDetails->setVisible(true);
781 updateIcon->setIcon(KisIconUtils::loadIcon("warning"));
782 } else if (m_updaterStatus.status() == UpdaterStatus::StatusID::RESTART_REQUIRED) {
783 updaterFrame->setVisible(true);
784 versionLabelText = QString("<b>%1</b> %2").arg(i18n("Restart is required.")).arg(m_updaterStatus.details());
785 versionNotificationLabel->setVisible(true);
786 updateIcon->setIcon(KisIconUtils::loadIcon("view-refresh"));
787 }
788
789 versionNotificationLabel->setText(versionLabelText);
790 if (!blendedStyle.isNull()) {
791 versionNotificationLabel->setStyleSheet(blendedStyle);
792 }
793}
794#endif
795
796#ifdef Q_OS_ANDROID
797void KisWelcomePageWidget::initDonations()
798{
800 if (!androidDonations) {
801 qWarning("KisWelcomePage::initDonations: androidDonations is null");
802 return;
803 }
804
805 // Pick a random banner. Note that the second number is *exclusive*.
806 int bannerIndex = QRandomGenerator::global()->bounded(1, 5);
807
808 // Banners have space where we can place text, but this varies by banner.
809 // These are the relative locations where that space is.
810 qreal ry = 0.02; // Vertical offset, always the same.
811 qreal rh = 1.0 - ry * 2.0; // Height, dito.
812 qreal rwpad = 0.02; // Horizontal padding.
813 qreal rx, rw; // Horizontal offset and width, vary by banner.
814 switch (bannerIndex) {
815 case 1: // Kiki winking on the left.
816 case 3: // Stargazers on the left.
817 rx = 0.4;
818 rw = 1.0 - rx - rwpad;
819 break;
820 case 2: // Cat holding brush in its mouth on the right.
821 case 4: // Person holding digital palette on the right.
822 rx = rwpad;
823 rw = 0.6 - rx;
824 break;
825 default: // Shouldn't happen, punt to using the entire width minus padding.
826 qWarning("Unhandled banner index %d", bannerIndex);
827 rx = rwpad;
828 rw = 1.0 - rwpad * 2.0;
829 break;
830 }
831
832 QString welcomeBannerPath =
833 QStandardPaths::locate(QStandardPaths::AppDataLocation,
834 QStringLiteral("share/krita/donation/welcomebanner%1.jpg").arg(bannerIndex));
835 QPixmap welcomeBannerPixmap(welcomeBannerPath);
836
837 if (welcomeBannerPixmap.isNull()) {
838 qWarning("KisWelcomePage::initDonations: failed to load welcome banner from '%s'",
839 qUtf8Printable(welcomeBannerPath));
840 // Leave the button alone, it will just say "Support Krita!"
841 } else {
842 QVector<QString> headlines = {
843 i18n("Become a Supporter!"),
844 i18n("Support Krita!"),
845 };
846 QVector<QString> subtitles = {
847 i18n("Supporters get brush packs and more."),
848 i18n("Contributions keep development going."),
849 };
850 QString headline = headlines[QRandomGenerator::global()->bounded(headlines.size())];
851 QString subtitle = subtitles[QRandomGenerator::global()->bounded(subtitles.size())];
852
853 // We want some text on the banner, which unfortunately requires some
854 // manual layout. We're going to write a top line in a larger font and
855 // a bottom line in a smaller font. They're going to be separated by a
856 // few pixels, centered horizontally and fit into predefined bounds on
857 // the banner depending on which image we're using. The text will be
858 // white with a black outline, so we'll use QPainterPath. Math time.
859 QPainter painter(&welcomeBannerPixmap);
860 painter.setRenderHint(QPainter::Antialiasing);
861 painter.setRenderHint(QPainter::TextAntialiasing);
862
863 QPen pen(Qt::black, 12.0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
864 QBrush brush(Qt::white);
865
866 qreal verticalSeparation = 40.0;
867
868 // Top line, the heading.
869 QFont currentFont = font();
870 currentFont.setPointSize(100);
871 currentFont.setBold(false);
872 QPainterPath topPath;
873 topPath.addText(0, 0, currentFont, headline);
874 QRectF topBounds = topPath.boundingRect();
875
876 // Bottom line, smaller text for more words.
877 currentFont.setPointSize(60);
878 currentFont.setBold(true);
879 QPainterPath bottomPath;
880 bottomPath.addText(0, 0, currentFont, subtitle);
881 QRectF bottomBounds = bottomPath.boundingRect();
882
883 // Stick those two lines into a combined path, horizontally centered,
884 // separated a bit so that the text doesn't get glued together.
885 qreal maxWidth = qMax(topBounds.width(), bottomBounds.width());
886 qreal topOffsetX = (maxWidth - topBounds.width()) / 2.0;
887 qreal bottomOffsetX = (maxWidth - bottomBounds.width()) / 2.0;
888
889 QTransform topTransform;
890 topTransform.translate(topOffsetX - topBounds.left(), -topBounds.top());
891 topPath = topTransform.map(topPath);
892 topPath.setFillRule(Qt::WindingFill);
893
894 QTransform bottomTransform;
895 bottomTransform.translate(bottomOffsetX - bottomBounds.left(),
896 topBounds.height() + verticalSeparation - bottomBounds.top());
897 bottomPath = bottomTransform.map(bottomPath);
898 bottomPath.setFillRule(Qt::WindingFill);
899
900 QPainterPath combinedPath;
901 combinedPath.addPath(topPath);
902 combinedPath.addPath(bottomPath);
903
904 // Determine the rectangle on the banner that we want to fit the text
905 // into. Taking a detour through the paint device just in case high-DPI
906 // scaling messes with the dimensions somehow.
907 qreal w = painter.device()->width();
908 qreal h = painter.device()->height();
909 QRect rect(w * rx, h * ry, w * rw, h * rh);
910
911 QRectF combinedBounds = combinedPath.boundingRect();
912 qreal scaleX = rect.width() / combinedBounds.width();
913 qreal scaleY = rect.height() / combinedBounds.height();
914 qreal scale = qMin(scaleX, scaleY);
915 qreal combinedOffsetX = (rect.width() - (combinedBounds.width() * scale)) / 2.0;
916 qreal combinedOffsetY = (rect.height() - (combinedBounds.height() * scale)) / 2.0;
917
918 QTransform combinedTransform;
919 combinedTransform.translate(rect.left() + combinedOffsetX, rect.top() + combinedOffsetY);
920 combinedTransform.scale(scale, scale);
921 combinedTransform.translate(-combinedBounds.left(), -combinedBounds.top());
922 combinedPath = combinedTransform.map(combinedPath);
923 combinedPath.setFillRule(Qt::WindingFill);
924
925 // And finally, draw the text onto the banner. First stroking the
926 // outside, then filling the inside, otherwise there's weird effects
927 // with strokes inside of letters overlapping with the fill.
928 painter.setPen(pen);
929 painter.setBrush(Qt::NoBrush);
930 painter.drawPath(combinedPath);
931
932 painter.setPen(Qt::NoPen);
933 painter.setBrush(brush);
934 painter.drawPath(combinedPath);
935
936 QSize welcomeBannerSize = welcomeBannerPixmap.size().scaled(500, 100, Qt::KeepAspectRatio);
937 bnAndroidSupport->setFlat(false);
938 bnAndroidSupport->setText(QString());
939 bnAndroidSupport->setContentsMargins(0, 0, 0, 0);
940 bnAndroidSupport->setIcon(QIcon(welcomeBannerPixmap));
941 bnAndroidSupport->setIconSize(welcomeBannerSize);
942 bnAndroidSupport->setFixedSize(welcomeBannerSize);
943 }
944
945 connect(bnAndroidSupport, &QPushButton::clicked, androidDonations, &KisAndroidDonations::slotStartDonationFlow);
946 connect(bnAndroidSupporterManage,
947 &QPushButton::clicked,
948 androidDonations,
950
951 QString badgePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, "share/krita/donation/banner.png");
952 QPixmap badgePixmap(badgePath);
953 if (badgePixmap.isNull()) {
954 qWarning("KisWelcomePage::initDonations: failed to load badge from '%s'", qUtf8Printable(badgePath));
955 } else {
956 supporterBadge->setPixmap(badgePixmap);
957 }
958
959 connect(androidDonations, SIGNAL(sigStateChanged()), this, SLOT(slotUpdateDonationState()));
960 slotUpdateDonationState();
961}
962
963void KisWelcomePageWidget::slotUpdateDonationState()
964{
965 bool badgeVisible = false;
966 QWidget *pageVisible = pgSupportMessage;
967
969 if (androidDonations) {
970 badgeVisible = androidDonations->shouldShowSupporterBadge();
971 switch (androidDonations->state()) {
973 pageVisible = pgAndroidSupporter;
974 break;
976 pageVisible = nullptr;
977 break;
978 default:
979 break;
980 }
981 } else {
982 qWarning("KisWelcomePageWidget::slotUpdateDonationState: android donations is null");
983 }
984
985 if(pageVisible) {
986 stkSupport->setCurrentWidget(pageVisible);
987 wdgAndroidSupportBanner->hide();
988 stkSupport->show();
989 } else {
990 stkSupport->hide();
991 wdgAndroidSupportBanner->show();
992 }
993
994 supporterBadge->setVisible(badgeVisible);
995}
996#endif
997
999{
1000 QFont larger = font();
1001 // Font size may be in pixels (on Android) or points (everywhere else.)
1002 qreal ratio = 1.1;
1003 if (larger.pixelSize() == -1) {
1004 larger.setPointSizeF(larger.pointSizeF() * ratio);
1005 } else {
1006 larger.setPixelSize(qRound(larger.pixelSize() * ratio));
1007 }
1008 return larger;
1009}
A KisActionManager class keeps track of KisActions. These actions are always associated with the GUI....
KisAction * actionByName(const QString &name) const
bool shouldShowSupporterBadge() const
static KisAndroidDonations * instance()
void sigShowDonationManagementDialogRequested()
static KisClipboard * instance()
void writeList(const QString &name, const QList< T > &value)
Definition kis_config.h:870
QList< T > readList(const QString &name, const QList< T > &defaultValue=QList< T >())
Definition kis_config.h:880
T readEntry(const QString &name, const T &defaultValue=T())
Definition kis_config.h:875
Main window for Krita.
bool installBundle(const QString &fileName) const
Copy the given file into the bundle directory.
bool openDocument(const QString &path, OpenFlags flags)
KisViewManager * viewManager
void slotFileOpen(bool isImporting=false)
void removeRecentFile(QString url)
void dragMoveEvent(QDragMoveEvent *event) override
static KisRecentDocumentsModelWrapper * instance()
The KisRemoteFileFetcher class can fetch a remote file and blocks until the file is downloaded.
bool fetchFile(const QUrl &remote, QIODevice *io)
fetch the image. Shows a progress dialog
static void log(const QString &message)
Logs with date/time.
KisActionManager * actionManager() const
void recentDocumentClicked(QModelIndex index)
KisWelcomePageWidget(QWidget *parent)
void dragEnterEvent(QDragEnterEvent *event) override
void setupNewsLangSelection(QMenu *newsOptionMenu)
bool eventFilter(QObject *watched, QEvent *event) override
static void updateShortcutLink(QToolButton *button, QLabel *label, QAction *action)
void dropEvent(QDropEvent *event) override
void slotScrollerStateChanged(QScroller::State state)
void dragLeaveEvent(QDragLeaveEvent *event) override
void changeEvent(QEvent *event) override
void setMainWindow(KisMainWindow *m_mainWindow)
void slotRecentDocContextMenuRequest(const QPoint &pos)
void showDropAreaIndicator(bool show)
void dragMoveEvent(QDragMoveEvent *event) override
RecentItemDelegate(QObject *parent=0)
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &) const override
void setItemHeight(int itemHeight)
void enableFromLink(QString unused_url)
QString button(const QWheelEvent &ev)
QIcon loadIcon(const QString &name)
void updateIcon(QAbstractButton *button)
KRITAWIDGETUTILS_EXPORT QScroller * createPreconfiguredScroller(QAbstractScrollArea *target)
QColor blendColors(const QColor &c1, const QColor &c2, qreal r1)
KRITAVERSION_EXPORT bool isDevelopersBuild()