1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 2007 Urs Wolfer <uwolfer at kde.org> |
4 | SPDX-FileCopyrightText: 2008, 2019 Friedrich W. H. Kossebau <kossebau@kde.org> |
5 | SPDX-FileCopyrightText: 2010 Teo Mrnjavac <teo@kde.org> |
6 | |
7 | Parts of this class have been take from the KAboutApplication class, which was |
8 | SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org> |
9 | SPDX-FileCopyrightText: 2000 Espen Sand <espen@kde.org> |
10 | |
11 | SPDX-License-Identifier: LGPL-2.0-only |
12 | */ |
13 | |
14 | #include "kabstractaboutdialog_p.h" |
15 | |
16 | #include "klicensedialog_p.h" |
17 | #include <kaboutdata.h> |
18 | #include <kxmlgui_version.h> |
19 | // KF |
20 | #include <KAdjustingScrollArea> |
21 | #include <KLocalizedString> |
22 | #include <KSandbox> |
23 | #include <KSeparator> |
24 | #include <KTitleWidget> |
25 | // Qt |
26 | #include <QApplication> |
27 | #include <QCheckBox> |
28 | #include <QClipboard> |
29 | #include <QDesktopServices> |
30 | #include <QDialogButtonBox> |
31 | #include <QIcon> |
32 | #include <QLabel> |
33 | #include <QPushButton> |
34 | #include <QStyle> |
35 | #include <QToolButton> |
36 | #include <QVBoxLayout> |
37 | |
38 | using namespace Qt::StringLiterals; |
39 | |
40 | QWidget *KAbstractAboutDialogPrivate::createTitleWidget(const QIcon &icon, const QString &displayName, const QString &version, QWidget *parent) |
41 | { |
42 | KTitleWidget *titleWidget = new KTitleWidget(parent); |
43 | |
44 | titleWidget->setIconSize(QSize(48, 48)); |
45 | titleWidget->setIcon(icon, alignment: KTitleWidget::ImageLeft); |
46 | titleWidget->setText( |
47 | text: QLatin1String("<html><font size=\"5\">%1</font><br />%2</html>" ).arg(args: displayName, i18nc("Version version-number" , "Version %1" , version))); |
48 | return titleWidget; |
49 | } |
50 | |
51 | QWidget *KAbstractAboutDialogPrivate::createAboutWidget(const QString &shortDescription, |
52 | const QString &otherText, |
53 | const QString ©rightStatement, |
54 | const QString &homepage, |
55 | const QList<KAboutLicense> &licenses, |
56 | QWidget *parent) |
57 | { |
58 | auto wrapper = new KAdjustingScrollArea(parent); |
59 | auto aboutWidget = new QWidget(parent); |
60 | wrapper->setWidget(aboutWidget); |
61 | auto aboutLayout = new QVBoxLayout(aboutWidget); |
62 | |
63 | QString aboutPageText = shortDescription + QLatin1Char('\n'); |
64 | |
65 | if (!otherText.isEmpty()) { |
66 | aboutPageText += QLatin1Char('\n') + otherText + QLatin1Char('\n'); |
67 | } |
68 | |
69 | if (!copyrightStatement.isEmpty()) { |
70 | aboutPageText += QLatin1Char('\n') + copyrightStatement + QLatin1Char('\n'); |
71 | } |
72 | |
73 | if (!homepage.isEmpty()) { |
74 | aboutPageText += QLatin1Char('\n') + QStringLiteral("<a href=\"%1\">%1</a>" ).arg(a: homepage) + QLatin1Char('\n'); |
75 | } |
76 | aboutPageText = aboutPageText.trimmed(); |
77 | |
78 | QLabel *aboutLabel = new QLabel; |
79 | aboutLabel->setWordWrap(true); |
80 | aboutLabel->setOpenExternalLinks(true); |
81 | aboutLabel->setText(aboutPageText.replace(c: QLatin1Char('\n'), QStringLiteral("<br />" ))); |
82 | aboutLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); |
83 | |
84 | aboutLayout->addWidget(aboutLabel); |
85 | |
86 | const int licenseCount = licenses.count(); |
87 | for (int i = 0; i < licenseCount; ++i) { |
88 | const KAboutLicense &license = licenses.at(i); |
89 | |
90 | QLabel *showLicenseLabel = new QLabel; |
91 | showLicenseLabel->setText(QStringLiteral("<a href=\"%1\">%2</a>" ).arg(args: QString::number(i), i18n("License: %1" , license.name(KAboutLicense::FullName)))); |
92 | showLicenseLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); |
93 | QObject::connect(sender: showLicenseLabel, signal: &QLabel::linkActivated, context: parent, slot: [license, parent]() { |
94 | auto *dialog = new KLicenseDialog(license, parent); |
95 | dialog->show(); |
96 | }); |
97 | |
98 | aboutLayout->addWidget(showLicenseLabel); |
99 | } |
100 | |
101 | aboutLayout->addStretch(); |
102 | |
103 | return wrapper; |
104 | } |
105 | |
106 | QWidget *KAbstractAboutDialogPrivate::createComponentWidget(const QList<KAboutComponent> &components, QWidget *parent) |
107 | { |
108 | auto wrapper = new KAdjustingScrollArea(parent); |
109 | auto componentWidget = new QWidget; |
110 | auto componentLayout = new QVBoxLayout(componentWidget); |
111 | wrapper->setWidget(componentWidget); |
112 | |
113 | QList<KAboutComponent> allComponents = components; |
114 | QString platformName; |
115 | auto platform = QGuiApplication::platformName(); |
116 | platform.replace(i: 0, len: 1, after: platform[0].toUpper()); |
117 | if (platform == u"Wayland"_s || platform == u"Xcb"_s ) { |
118 | platform = i18nc("@info Platform name" , "%1 (%2)" , QSysInfo::prettyProductName(), platform); |
119 | } else { |
120 | platform = QSysInfo::prettyProductName(); |
121 | } |
122 | |
123 | allComponents.append(t: KAboutComponent(i18n("KDE Frameworks" ), |
124 | i18nc("@info" , "Collection of libraries created by the KDE Community to extend Qt." ), |
125 | QStringLiteral(KXMLGUI_VERSION_STRING), |
126 | QStringLiteral("https://develop.kde.org/products/frameworks/" ), |
127 | KAboutLicense::LGPL_V2_1)); |
128 | allComponents.append(t: KAboutComponent(i18n("Qt" ), |
129 | i18nc("@info" , "Cross-platform application development framework." ), |
130 | i18n("Using %1 and built against %2" , QString::fromLocal8Bit(qVersion()), QStringLiteral(QT_VERSION_STR)), |
131 | QStringLiteral("https://www.qt.io/" ), |
132 | KAboutLicense::LGPL_V3)); |
133 | |
134 | QString packageText; |
135 | if (KSandbox::isFlatpak()) { |
136 | packageText = i18nc("Linux packaging format" , "Flatpak" ); |
137 | } |
138 | if (KSandbox::isSnap()) { |
139 | packageText = i18nc("Linux packaging format" , "Snap" ); |
140 | } |
141 | if (qEnvironmentVariableIsSet(varName: "APPIMAGE" )) { |
142 | packageText = i18nc("Linux packaging format" , "AppImage" ); |
143 | } |
144 | if (!packageText.isEmpty()) { |
145 | allComponents.append(t: KAboutComponent(packageText, i18nc("@info" , "Distribution method." ))); |
146 | } |
147 | |
148 | allComponents.append(t: KAboutComponent(platform, i18nc("@info" , "Underlying platform." ))); |
149 | |
150 | for (qsizetype i = 0, count = allComponents.count(); i < count; i++) { |
151 | const auto &component = allComponents[i]; |
152 | |
153 | QVBoxLayout *col = nullptr; |
154 | QHBoxLayout *row = new QHBoxLayout; |
155 | |
156 | auto name = new QLabel(u"<span style='font-weight: 600'>"_s + component.name() + u"</span>"_s |
157 | + (!component.version().isEmpty() ? (u" (" + component.version() + u')') : QString{})); |
158 | if (!component.description().isEmpty()) { |
159 | col = new QVBoxLayout; |
160 | col->setSpacing(0); |
161 | auto description = new QLabel(component.description()); |
162 | auto palette = description->palette(); |
163 | auto foregroundColor = palette.color(cr: QPalette::WindowText); |
164 | foregroundColor.setAlphaF(0.85); |
165 | palette.setColor(acr: QPalette::WindowText, acolor: foregroundColor); |
166 | description->setPalette(palette); |
167 | col->addWidget(name); |
168 | col->addWidget(description); |
169 | row->addLayout(layout: col); |
170 | } else { |
171 | row->addWidget(name); |
172 | } |
173 | |
174 | if (!component.webAddress().isEmpty()) { |
175 | const auto url = QUrl(component.webAddress()); |
176 | auto webAction = new QAction(QIcon::fromTheme(name: u"internet-services-symbolic"_s ), i18nc("@action:button" , "Visit component's homepage" )); |
177 | webAction->setToolTip(i18nc("@info:tooltip" , "Visit components's homepage\n%1" , component.webAddress())); |
178 | QObject::connect(sender: webAction, signal: &QAction::triggered, context: webAction, slot: [url]() { |
179 | QDesktopServices::openUrl(url: QUrl(url)); |
180 | }); |
181 | auto web = new QToolButton; |
182 | web->setDefaultAction(webAction); |
183 | web->setToolButtonStyle(Qt::ToolButtonIconOnly); |
184 | web->setAutoRaise(true); |
185 | row->addWidget(web); |
186 | } |
187 | |
188 | componentLayout->addLayout(layout: row); |
189 | |
190 | if (i + 1 != count) { |
191 | auto separator = new KSeparator; |
192 | separator->setEnabled(false); |
193 | componentLayout->addWidget(separator); |
194 | } |
195 | } |
196 | |
197 | componentLayout->addSpacing(size: parent->style()->pixelMetric(metric: QStyle::PM_LayoutVerticalSpacing)); |
198 | |
199 | auto copyButton = new QPushButton(QIcon::fromTheme(name: u"edit-copy-symbolic"_s ), i18nc("@action:button" , "Copy to Clipboard" ), parent); |
200 | copyButton->setSizePolicy(hor: QSizePolicy::Maximum, ver: QSizePolicy::Preferred); |
201 | QObject::connect(sender: copyButton, signal: &QPushButton::clicked, context: copyButton, slot: [allComponents]() { |
202 | auto aboutData = KAboutData::applicationData(); |
203 | QString info = aboutData.displayName() + u": "_s + aboutData.version() + u'\n'; |
204 | |
205 | for (const auto &component : allComponents) { |
206 | info += component.name(); |
207 | if (!component.version().isEmpty()) { |
208 | info += u": "_s + component.version(); |
209 | } |
210 | info += u'\n'; |
211 | } |
212 | |
213 | info += u"Build ABI: "_s + QSysInfo::buildAbi() + u'\n'; |
214 | info += u"Kernel: "_s + QSysInfo::kernelType() + u' ' + QSysInfo::kernelVersion() + u'\n'; |
215 | |
216 | QClipboard *clipboard = QGuiApplication::clipboard(); |
217 | clipboard->setText(info); |
218 | }); |
219 | |
220 | componentLayout->addWidget(copyButton); |
221 | |
222 | componentLayout->addStretch(); |
223 | |
224 | return wrapper; |
225 | } |
226 | |
227 | static void createPersonLayout(QVBoxLayout *layout, const QList<KAboutPerson> &persons) |
228 | { |
229 | for (qsizetype i = 0, count = persons.count(); i < count; i++) { |
230 | const auto &person = persons[i]; |
231 | |
232 | QVBoxLayout *col = nullptr; |
233 | QHBoxLayout *row = new QHBoxLayout; |
234 | |
235 | auto name = new QLabel(person.name()); |
236 | auto font = name->font(); |
237 | font.setWeight(QFont::DemiBold); |
238 | name->setFont(font); |
239 | |
240 | if (!person.task().isEmpty()) { |
241 | col = new QVBoxLayout; |
242 | col->setSpacing(0); |
243 | auto task = new QLabel(person.task()); |
244 | auto palette = task->palette(); |
245 | auto foregroundColor = palette.color(cr: QPalette::WindowText); |
246 | foregroundColor.setAlphaF(0.85); |
247 | palette.setColor(acr: QPalette::WindowText, acolor: foregroundColor); |
248 | task->setPalette(palette); |
249 | |
250 | col->addWidget(name); |
251 | col->addWidget(task); |
252 | row->addLayout(layout: col); |
253 | } else { |
254 | row->addWidget(name); |
255 | } |
256 | |
257 | if (!person.webAddress().isEmpty()) { |
258 | const auto url = QUrl(person.webAddress()); |
259 | auto webAction = new QAction(QIcon::fromTheme(name: u"internet-services-symbolic"_s ), i18nc("@action:button" , "Visit author's homepage" )); |
260 | webAction->setToolTip(i18nc("@info:tooltip" , "Visit author's homepage\n%1" , person.webAddress())); |
261 | QObject::connect(sender: webAction, signal: &QAction::triggered, context: webAction, slot: [url]() { |
262 | QDesktopServices::openUrl(url); |
263 | }); |
264 | auto web = new QToolButton; |
265 | web->setDefaultAction(webAction); |
266 | web->setAutoRaise(true); |
267 | web->setToolButtonStyle(Qt::ToolButtonIconOnly); |
268 | row->addWidget(web); |
269 | } |
270 | |
271 | if (!person.emailAddress().isEmpty()) { |
272 | const auto url = person.emailAddress(); |
273 | auto webAction = |
274 | new QAction(QIcon::fromTheme(name: u"mail-send-symbolic"_s ), i18nc("@action:button Send an email to a contributor" , "Email contributor" )); |
275 | webAction->setToolTip(i18nc("@info:tooltip" , "Email contributor: %1" , person.emailAddress())); |
276 | QObject::connect(sender: webAction, signal: &QAction::triggered, context: webAction, slot: [url]() { |
277 | QDesktopServices::openUrl(url: QUrl(u"mailto:"_s + url)); |
278 | }); |
279 | auto web = new QToolButton; |
280 | web->setDefaultAction(webAction); |
281 | web->setToolButtonStyle(Qt::ToolButtonIconOnly); |
282 | web->setAutoRaise(true); |
283 | row->addWidget(web); |
284 | } |
285 | |
286 | layout->addLayout(layout: row); |
287 | if (i + 1 != count) { |
288 | auto separator = new KSeparator; |
289 | separator->setEnabled(false); |
290 | layout->addWidget(separator); |
291 | } |
292 | } |
293 | |
294 | layout->addStretch(); |
295 | } |
296 | |
297 | QWidget *KAbstractAboutDialogPrivate::createAuthorsWidget(const QList<KAboutPerson> &authors, |
298 | bool customAuthorTextEnabled, |
299 | const QString &customAuthorRichText, |
300 | const QString &bugAddress, |
301 | QWidget *parent) |
302 | { |
303 | auto wrapper = new KAdjustingScrollArea; |
304 | auto authorWidget = new QWidget(parent); |
305 | wrapper->setWidget(authorWidget); |
306 | auto authorLayout = new QVBoxLayout(authorWidget); |
307 | |
308 | if (!customAuthorTextEnabled || !customAuthorRichText.isEmpty()) { |
309 | QLabel *bugsLabel = new QLabel(authorWidget); |
310 | bugsLabel->setOpenExternalLinks(true); |
311 | if (!customAuthorTextEnabled) { |
312 | if (bugAddress.isEmpty() || bugAddress == QLatin1String("submit@bugs.kde.org" )) { |
313 | bugsLabel->setText(i18nc("Reference to website" , |
314 | "Please use %1 to report bugs.<br/>" |
315 | "If you have questions or need help, please visit %2.<br/>" , |
316 | QLatin1String("<a href=\"https://bugs.kde.org\">https://bugs.kde.org</a>" ), |
317 | QLatin1String("<a href=\"https://kde.org/support/\">https://kde.org/support/</a>" ))); |
318 | } else { |
319 | QUrl bugUrl(bugAddress); |
320 | if (bugUrl.scheme().isEmpty()) { |
321 | bugUrl.setScheme(QStringLiteral("mailto" )); |
322 | } |
323 | bugsLabel->setText(i18nc("Reference to email address" , |
324 | "Please report bugs to %1.\n" , |
325 | QLatin1String("<a href=\"%1\">%2</a>" ).arg(bugUrl.toString(), bugAddress))); |
326 | } |
327 | } else { |
328 | bugsLabel->setText(customAuthorRichText); |
329 | } |
330 | bugsLabel->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Minimum); |
331 | authorLayout->addWidget(bugsLabel); |
332 | } |
333 | |
334 | createPersonLayout(layout: authorLayout, persons: authors); |
335 | |
336 | return wrapper; |
337 | } |
338 | |
339 | QWidget *KAbstractAboutDialogPrivate::createCreditWidget(const QList<KAboutPerson> &credits, QWidget *parent) |
340 | { |
341 | auto wrapper = new KAdjustingScrollArea; |
342 | auto creditWidget = new QWidget(parent); |
343 | wrapper->setWidget(creditWidget); |
344 | auto creditLayout = new QVBoxLayout(creditWidget); |
345 | |
346 | createPersonLayout(layout: creditLayout, persons: credits); |
347 | |
348 | return wrapper; |
349 | } |
350 | |
351 | QWidget *KAbstractAboutDialogPrivate::createTranslatorsWidget(const QList<KAboutPerson> &translators, QWidget *parent) |
352 | { |
353 | auto wrapper = new KAdjustingScrollArea; |
354 | auto translatorWidget = new QWidget(parent); |
355 | wrapper->setWidget(translatorWidget); |
356 | auto translatorLayout = new QVBoxLayout(translatorWidget); |
357 | |
358 | createPersonLayout(layout: translatorLayout, persons: translators); |
359 | |
360 | QString aboutTranslationTeam = KAboutData::aboutTranslationTeam(); |
361 | if (!aboutTranslationTeam.isEmpty()) { |
362 | QLabel *translationTeamLabel = new QLabel(translatorWidget); |
363 | translationTeamLabel->setContentsMargins(left: 4, top: 2, right: 4, bottom: 4); |
364 | translationTeamLabel->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Minimum); |
365 | translationTeamLabel->setWordWrap(true); |
366 | translationTeamLabel->setText(aboutTranslationTeam); |
367 | translationTeamLabel->setOpenExternalLinks(true); |
368 | translatorLayout->addWidget(translationTeamLabel); |
369 | // TODO: this could be displayed as a view item to save space |
370 | } |
371 | |
372 | return wrapper; |
373 | } |
374 | |
375 | void KAbstractAboutDialogPrivate::createForm(QWidget *titleWidget, QWidget *tabWidget, QDialog *dialog) |
376 | { |
377 | QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); |
378 | buttonBox->setStandardButtons(QDialogButtonBox::Close); |
379 | QObject::connect(sender: buttonBox, signal: &QDialogButtonBox::accepted, context: dialog, slot: &QDialog::accept); |
380 | QObject::connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: dialog, slot: &QDialog::reject); |
381 | |
382 | // And we jam everything together in a layout... |
383 | QVBoxLayout *mainLayout = new QVBoxLayout(dialog); |
384 | mainLayout->addWidget(titleWidget); |
385 | mainLayout->addWidget(tabWidget); |
386 | mainLayout->addWidget(buttonBox); |
387 | } |
388 | |