| 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 | |