// For license of this file, see /LICENSE.md. #include "miscellaneous/application.h" #include "3rd-party/boolinq/boolinq.h" #include "dynamic-shortcuts/dynamicshortcuts.h" #include "exceptions/applicationexception.h" #include "gui/dialogs/formabout.h" #include "gui/dialogs/formlog.h" #include "gui/dialogs/formmain.h" #include "gui/feedmessageviewer.h" #include "gui/feedsview.h" #include "gui/messagebox.h" #include "gui/toolbars/statusbar.h" #include "gui/webviewers/qtextbrowser/textbrowserviewer.h" #include "miscellaneous/feedreader.h" #include "miscellaneous/iconfactory.h" #include "miscellaneous/iofactory.h" #include "miscellaneous/mutex.h" #include "miscellaneous/notificationfactory.h" #include "network-web/adblock/adblockicon.h" #include "network-web/adblock/adblockmanager.h" #include "network-web/webfactory.h" #include "services/abstract/serviceroot.h" #include "services/owncloud/owncloudserviceentrypoint.h" #include "services/standard/standardserviceentrypoint.h" #include "services/standard/standardserviceroot.h" #include "services/tt-rss/ttrssserviceentrypoint.h" #include #include #include #include #include #include #include #include #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) #include #include #endif #if defined(USE_WEBENGINE) #include "gui/webviewers/webengine/webengineviewer.h" // WebEngine-based web browsing. #include "network-web/webengine/networkurlinterceptor.h" #if QT_VERSION_MAJOR == 6 #include #else #include #endif #endif #if defined(Q_OS_WIN) #include #if QT_VERSION_MAJOR == 5 #include #endif #endif Application::Application(const QString& id, int& argc, char** argv, const QStringList& raw_cli_args) : SingleApplication(id, argc, argv), m_rawCliArgs(raw_cli_args), m_updateFeedsLock(new Mutex()) { QString custom_ua; parseCmdArgumentsFromMyInstance(raw_cli_args, custom_ua); qInstallMessageHandler(performLogging); m_feedReader = nullptr; m_quitLogicDone = false; m_mainForm = nullptr; m_logForm = nullptr; m_trayIcon = nullptr; m_settings = Settings::setupSettings(this); #if defined(USE_WEBENGINE) if (!m_forcedNoWebEngine && qgetenv("QTWEBENGINE_CHROMIUM_FLAGS").isEmpty()) { qputenv("QTWEBENGINE_CHROMIUM_FLAGS", settings()->value(GROUP(Browser), SETTING(Browser::WebEngineChromiumFlags)).toString().toLocal8Bit()); } #endif m_nodejs = new NodeJs(m_settings, this); m_webFactory = new WebFactory(this); m_system = new SystemFactory(this); m_skins = new SkinFactory(this); m_localization = new Localization(this); m_icons = new IconFactory(this); m_database = new DatabaseFactory(this); m_downloadManager = nullptr; m_notifications = new NotificationFactory(this); m_shouldRestart = false; #if defined(Q_OS_WIN) m_windowsTaskBar = nullptr; const GUID qIID_ITaskbarList4 = {0xc43dc798, 0x95d1, 0x4bea, {0x90, 0x30, 0xbb, 0x99, 0xe2, 0x98, 0x3a, 0x1a}}; HRESULT task_result = CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, qIID_ITaskbarList4, reinterpret_cast(&m_windowsTaskBar)); if (FAILED(task_result)) { qCriticalNN << LOGSEC_CORE << "Taskbar integration for Windows failed to initialize with HRESULT:" << QUOTE_W_SPACE_DOT(task_result); m_windowsTaskBar = nullptr; } else if (FAILED(m_windowsTaskBar->HrInit())) { qCriticalNN << LOGSEC_CORE << "Taskbar integration for Windows failed to initialize with inner HRESULT:" << QUOTE_W_SPACE_DOT(m_windowsTaskBar->HrInit()); m_windowsTaskBar->Release(); m_windowsTaskBar = nullptr; } #endif determineFirstRuns(); //: Abbreviation of language, e.g. en. //: Use ISO 639-1 code here combined with ISO 3166-1 (alpha-2) code. //: Examples: "cs", "en", "it", "cs_CZ", "en_GB", "en_US". QObject::tr("LANG_ABBREV"); //: Name of translator - optional. QObject::tr("LANG_AUTHOR"); // Add an extra path for non-system icon themes and set current icon theme // and skin. m_icons->setupSearchPaths(); m_icons->loadCurrentIconTheme(); m_skins->loadCurrentSkin(); connect(this, &Application::aboutToQuit, this, &Application::onAboutToQuit); connect(this, &Application::commitDataRequest, this, &Application::onCommitData); connect(this, &Application::saveStateRequest, this, &Application::onSaveState); connect(m_nodejs, &NodeJs::packageError, this, &Application::onNodeJsPackageUpdateError); connect(m_nodejs, &NodeJs::packageInstalledUpdated, this, &Application::onNodeJsPackageInstalled); #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) QString app_dir = QString::fromLocal8Bit(qgetenv("APPDIR")); if (!app_dir.isEmpty()) { bool success = qputenv("GST_PLUGIN_SYSTEM_PATH_1_0", QSL("%1/usr/lib/gstreamer-1.0:%2") .arg(app_dir, QString::fromLocal8Bit(qgetenv("GST_PLUGIN_SYSTEM_PATH_1_0"))) .toLocal8Bit()); success = qputenv("GST_PLUGIN_SCANNER_1_0", QSL("%1/usr/lib/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner").arg(app_dir).toLocal8Bit()) && success; if (!success) { qWarningNN << LOGSEC_CORE << "Unable to set up GStreamer environment."; } } #endif m_webFactory->setCustomUserAgent(custom_ua); #if defined(USE_WEBENGINE) m_webFactory->urlIinterceptor()->load(); const QString web_data_root = userDataFolder() + QDir::separator() + QSL("web"); m_webFactory->engineProfile()->setCachePath(web_data_root + QDir::separator() + QSL("cache")); m_webFactory->engineProfile()->setHttpCacheType(QWebEngineProfile::HttpCacheType::DiskHttpCache); m_webFactory->engineProfile()->setPersistentStoragePath(web_data_root + QDir::separator() + QSL("storage")); if (custom_ua.isEmpty()) { m_webFactory->engineProfile()->setHttpUserAgent(QString(HTTP_COMPLETE_USERAGENT)); } else { m_webFactory->engineProfile()->setHttpUserAgent(custom_ua); } qDebugNN << LOGSEC_NETWORK << "Persistent web data storage path:" << QUOTE_W_SPACE_DOT(m_webFactory->engineProfile()->persistentStoragePath()); connect(m_webFactory->engineProfile(), &QWebEngineProfile::downloadRequested, this, &Application::downloadRequested); #endif connect(m_webFactory->adBlock(), &AdBlockManager::processTerminated, this, &Application::onAdBlockFailure); QTimer::singleShot(3000, this, [=]() { try { m_webFactory->adBlock() ->setEnabled(qApp->settings()->value(GROUP(AdBlock), SETTING(AdBlock::AdBlockEnabled)).toBool()); } catch (...) { onAdBlockFailure(); } }); m_webFactory->updateProxy(); if (isFirstRun()) { m_notifications->save({Notification(Notification::Event::GeneralEvent, true), Notification(Notification::Event::NewUnreadArticlesFetched, true, QSL("%1/notify.wav").arg(SOUNDS_BUILTIN_DIRECTORY)), Notification(Notification::Event::NewAppVersionAvailable, true), Notification(Notification::Event::LoginFailure, true), Notification(Notification::Event::NodePackageUpdated, true), Notification(Notification::Event::NodePackageFailedToUpdate, true)}, settings()); } else { m_notifications->load(settings()); } QTimer::singleShot(1000, system(), &SystemFactory::checkForUpdatesOnStartup); qDebugNN << LOGSEC_CORE << "OpenSSL version:" << QUOTE_W_SPACE_DOT(QSslSocket::sslLibraryVersionString()); qDebugNN << LOGSEC_CORE << "OpenSSL supported:" << QUOTE_W_SPACE_DOT(QSslSocket::supportsSsl()); } Application::~Application() { #if defined(Q_OS_WIN) if (m_windowsTaskBar != nullptr) { m_windowsTaskBar->Release(); } #endif // Disable logging into the form. m_logForm = nullptr; qDebugNN << LOGSEC_CORE << "Destroying Application instance."; } QString s_customLogFile = QString(); bool s_disableDebug = false; void Application::performLogging(QtMsgType type, const QMessageLogContext& context, const QString& msg) { #ifndef QT_NO_DEBUG_OUTPUT QString console_message = qFormatLogMessage(type, context, msg); if (!s_disableDebug) { std::cerr << console_message.toStdString() << std::endl; } if (!s_customLogFile.isEmpty()) { QFile log_file(s_customLogFile); if (log_file.open(QFile::OpenModeFlag::Append | QFile::OpenModeFlag::Unbuffered)) { log_file.write(console_message.toUtf8()); log_file.write(QSL("\r\n").toUtf8()); log_file.close(); } } if (qApp != nullptr) { qApp->displayLogMessageInDialog(console_message); } if (type == QtMsgType::QtFatalMsg) { qApp->exit(EXIT_FAILURE); } #else Q_UNUSED(type) Q_UNUSED(context) Q_UNUSED(msg) #endif } void Application::reactOnForeignNotifications() { connect(this, &Application::messageReceived, this, &Application::parseCmdArgumentsFromOtherInstance); } void Application::hideOrShowMainForm() { // Display main window. if (qApp->settings()->value(GROUP(GUI), SETTING(GUI::MainWindowStartsHidden)).toBool() && SystemTrayIcon::isSystemTrayDesired() && SystemTrayIcon::isSystemTrayAreaAvailable()) { qDebugNN << LOGSEC_CORE << "Hiding the main window when the application is starting."; mainForm()->switchVisibility(true); } else { qDebugNN << LOGSEC_CORE << "Showing the main window when the application is starting."; mainForm()->show(); } } void Application::loadDynamicShortcuts() { DynamicShortcuts::load(userActions()); } void Application::showPolls() const { /* if(isFirstRunCurrentVersion()) { qApp->showGuiMessage(Notification::Event::GeneralEvent, { QSL("%1 survey").arg(QSL(APP_NAME)), QSL("Please, fill the survey."), QSystemTrayIcon::MessageIcon::Warning }, { false, true, false }); qApp->web()->openUrlInExternalBrowser(QSL("https://forms.gle/9GgSa38Awqr37xLV8")); } */ } void Application::offerChanges() const { if (isFirstRunCurrentVersion()) { qApp->showGuiMessage(Notification::Event::GeneralEvent, {tr("Welcome"), tr("Welcome to %1.\n\nPlease, check NEW stuff included in this\n" "version by clicking this popup notification.") .arg(QSL(APP_LONG_NAME)), QSystemTrayIcon::MessageIcon::NoIcon}, {}, {tr("Go to changelog"), [] { FormAbout(qApp->mainForm()).exec(); }}); } } bool Application::isAlreadyRunning() { return m_allowMultipleInstances ? false : sendMessage((QStringList() << QSL("-%1").arg(QSL(CLI_IS_RUNNING)) << Application::arguments().mid(1)) .join(QSL(ARGUMENTS_LIST_SEPARATOR))); } QStringList Application::builtinSounds() const { auto builtin_sounds = QDir(QSL(SOUNDS_BUILTIN_DIRECTORY)).entryInfoList(QDir::Filter::Files, QDir::SortFlag::Name); auto iter = boolinq::from(builtin_sounds) .select([](const QFileInfo& i) { return i.absoluteFilePath(); }) .toStdList(); auto descs = FROM_STD_LIST(QStringList, iter); return descs; } FeedReader* Application::feedReader() { return m_feedReader; } QList Application::userActions() { if (m_mainForm != nullptr && m_userActions.isEmpty()) { m_userActions = m_mainForm->allActions(); m_userActions.append(m_webFactory->adBlock()->adBlockIcon()); } return m_userActions; } bool Application::isFirstRun() const { return m_firstRunEver; } bool Application::isFirstRunCurrentVersion() const { return m_firstRunCurrentVersion; } QCommandLineParser* Application::cmdParser() { return &m_cmdParser; } WebFactory* Application::web() const { return m_webFactory; } SystemFactory* Application::system() { return m_system; } SkinFactory* Application::skins() { return m_skins; } Localization* Application::localization() { return m_localization; } DatabaseFactory* Application::database() { return m_database; } void Application::eliminateFirstRuns() { settings()->setValue(GROUP(General), General::FirstRun, false); settings()->setValue(GROUP(General), QString(General::FirstRun) + QL1C('_') + APP_VERSION, false); } void Application::displayLogMessageInDialog(const QString& message) { if (m_logForm != nullptr && m_logForm->isVisible()) { emit sendLogToDialog(message); } } int Application::customAdblockPort() const { return m_customAdblockPort; } QStringList Application::rawCliArgs() const { return m_rawCliArgs; } #if defined(USE_WEBENGINE) bool Application::forcedNoWebEngine() const { return m_forcedNoWebEngine; } #endif NodeJs* Application::nodejs() const { return m_nodejs; } NotificationFactory* Application::notifications() const { return m_notifications; } void Application::setFeedReader(FeedReader* feed_reader) { m_feedReader = feed_reader; connect(m_feedReader, &FeedReader::feedUpdatesStarted, this, &Application::onFeedUpdatesStarted); connect(m_feedReader, &FeedReader::feedUpdatesProgress, this, &Application::onFeedUpdatesProgress); connect(m_feedReader, &FeedReader::feedUpdatesFinished, this, &Application::onFeedUpdatesFinished); connect(m_feedReader->feedsModel(), &FeedsModel::messageCountsChanged, this, &Application::showMessagesNumber); } IconFactory* Application::icons() { return m_icons; } DownloadManager* Application::downloadManager() { if (m_downloadManager == nullptr) { m_downloadManager = new DownloadManager(); connect(m_downloadManager, &DownloadManager::downloadFinished, mainForm()->statusBar(), &StatusBar::clearProgressDownload); connect(m_downloadManager, &DownloadManager::downloadProgressed, mainForm()->statusBar(), &StatusBar::showProgressDownload); } return m_downloadManager; } Settings* Application::settings() const { return m_settings; } Mutex* Application::feedUpdateLock() { return m_updateFeedsLock.data(); } FormMain* Application::mainForm() { return m_mainForm; } QWidget* Application::mainFormWidget() { return m_mainForm; } void Application::setMainForm(FormMain* main_form) { m_mainForm = main_form; } QString Application::configFolder() const { return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::GenericConfigLocation); } QString Application::userDataAppFolder() const { // In "app" folder, we would like to separate all user data into own subfolder, // therefore stick to "data" folder in this mode. return applicationDirPath() + QDir::separator() + QSL("data4"); } QString Application::userDataFolder() { if (settings()->type() == SettingsProperties::SettingsType::Custom) { return customDataFolder(); } else if (settings()->type() == SettingsProperties::SettingsType::Portable) { return userDataAppFolder(); } else { return userDataHomeFolder(); } } QString Application::replaceDataUserDataFolderPlaceholder(QString text) const { auto user_data_folder = qApp->userDataFolder(); return text.replace(QSL(USER_DATA_PLACEHOLDER), user_data_folder); } QStringList Application::replaceDataUserDataFolderPlaceholder(QStringList texts) const { auto user_data_folder = qApp->userDataFolder(); return texts.replaceInStrings(QSL(USER_DATA_PLACEHOLDER), user_data_folder); } QString Application::userDataHomeFolder() const { #if defined(Q_OS_ANDROID) return IOFactory::getSystemFolder(QStandardPaths::GenericDataLocation) + QDir::separator() + QSL(APP_NAME) + QSL(" 4"); #else return configFolder() + QDir::separator() + QSL(APP_NAME) + QSL(" 4"); #endif } QString Application::tempFolder() const { return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::TempLocation); } QString Application::documentsFolder() const { return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::DocumentsLocation); } QString Application::homeFolder() const { #if defined(Q_OS_ANDROID) return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::GenericDataLocation); #else return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::HomeLocation); #endif } void Application::backupDatabaseSettings(bool backup_database, bool backup_settings, const QString& target_path, const QString& backup_name) { if (!QFileInfo(target_path).isWritable()) { throw ApplicationException(tr("Output directory is not writable.")); } if (backup_settings) { settings()->sync(); if (!IOFactory::copyFile(settings()->fileName(), target_path + QDir::separator() + backup_name + BACKUP_SUFFIX_SETTINGS)) { throw ApplicationException(tr("Settings file not copied to output directory successfully.")); } } if (backup_database) { // We need to save the database first. database()->driver()->saveDatabase(); database()->driver()->backupDatabase(target_path, backup_name); } } void Application::restoreDatabaseSettings(bool restore_database, bool restore_settings, const QString& source_database_file_path, const QString& source_settings_file_path) { if (restore_database) { if (!qApp->database()->driver()->initiateRestoration(source_database_file_path)) { throw ApplicationException(tr("Database restoration was not initiated. Make sure that output directory is " "writable.")); } } if (restore_settings) { if (!qApp->settings()->initiateRestoration(source_settings_file_path)) { throw ApplicationException(tr("Settings restoration was not initiated. Make sure that output directory is " "writable.")); } } } SystemTrayIcon* Application::trayIcon() { if (m_trayIcon == nullptr) { if (qApp->settings()->value(GROUP(GUI), SETTING(GUI::MonochromeTrayIcon)).toBool()) { m_trayIcon = new SystemTrayIcon(APP_ICON_MONO_PATH, APP_ICON_MONO_PLAIN_PATH, m_mainForm); } else { m_trayIcon = new SystemTrayIcon(APP_ICON_PATH, APP_ICON_PLAIN_PATH, m_mainForm); } connect(m_trayIcon, &SystemTrayIcon::shown, m_feedReader->feedsModel(), &FeedsModel::notifyWithCounts); } return m_trayIcon; } QIcon Application::desktopAwareIcon() const { auto from_theme = m_icons->fromTheme(QSL(APP_LOW_NAME)); if (!from_theme.isNull()) { return from_theme; } else { return QIcon(APP_ICON_PATH); } } void Application::showTrayIcon() { // Display tray icon if it is enabled and available. if (SystemTrayIcon::isSystemTrayDesired()) { qDebugNN << LOGSEC_GUI << "User wants to have tray icon."; #if defined(Q_OS_WIN) if (SystemTrayIcon::isSystemTrayAreaAvailable()) { qDebugNN << LOGSEC_GUI << "Tray icon is available, showing now."; trayIcon()->show(); } else { m_feedReader->feedsModel()->notifyWithCounts(); } #else // Delay avoids race conditions and tray icon is properly displayed. qWarningNN << LOGSEC_GUI << "Showing tray icon with 3000 ms delay."; QTimer::singleShot(3000, this, [=]() { if (SystemTrayIcon::isSystemTrayAreaAvailable()) { qWarningNN << LOGSEC_GUI << "Tray icon is available, showing now."; trayIcon()->show(); } else { m_feedReader->feedsModel()->notifyWithCounts(); } }); #endif } else { m_feedReader->feedsModel()->notifyWithCounts(); } } void Application::deleteTrayIcon() { if (m_trayIcon != nullptr) { qDebugNN << LOGSEC_CORE << "Disabling tray icon, deleting it and raising main application window."; m_mainForm->display(); delete m_trayIcon; m_trayIcon = nullptr; // Make sure that application quits when last window is closed. setQuitOnLastWindowClosed(true); } } void Application::showGuiMessage(Notification::Event event, const GuiMessage& msg, const GuiMessageDestination& dest, const GuiAction& action, QWidget* parent) { if (SystemTrayIcon::areNotificationsEnabled()) { auto notification = m_notifications->notificationForEvent(event); notification.playSound(this); if (SystemTrayIcon::isSystemTrayDesired() && SystemTrayIcon::isSystemTrayAreaAvailable() && notification.balloonEnabled() && dest.m_tray) { trayIcon()->showMessage(msg.m_title.simplified().isEmpty() ? Notification::nameForEvent(notification.event()) : msg.m_title, msg.m_message, msg.m_type, TRAY_ICON_BUBBLE_TIMEOUT, std::move(action.m_action)); return; } } if (dest.m_messageBox || msg.m_type == QSystemTrayIcon::MessageIcon::Critical) { // Tray icon or OSD is not available, display simple text box. MsgBox::show(parent == nullptr ? mainFormWidget() : parent, QMessageBox::Icon(msg.m_type), msg.m_title, msg.m_message, {}, {}, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok, {}, action.m_title, action.m_action); } else if (dest.m_statusBar && mainForm()->statusBar() != nullptr && mainForm()->statusBar()->isVisible()) { mainForm()->statusBar()->showMessage(msg.m_message, 3000); } else { qDebugNN << LOGSEC_CORE << "Silencing GUI message:" << QUOTE_W_SPACE_DOT(msg.m_message); } } WebViewer* Application::createWebView() { #if !defined(USE_WEBENGINE) return new TextBrowserViewer(); #else if (forcedNoWebEngine()) { return new TextBrowserViewer(); } else { return new WebEngineViewer(); } #endif } void Application::onCommitData(QSessionManager& manager) { qDebugNN << LOGSEC_CORE << "OS asked application to commit its data."; onAboutToQuit(); manager.setRestartHint(QSessionManager::RestartHint::RestartNever); manager.release(); } void Application::onSaveState(QSessionManager& manager) { qDebugNN << LOGSEC_CORE << "OS asked application to save its state."; manager.setRestartHint(QSessionManager::RestartHint::RestartNever); manager.release(); } void Application::onAboutToQuit() { if (m_quitLogicDone) { qWarningNN << LOGSEC_CORE << "On-close logic is already done."; return; } m_quitLogicDone = true; // Make sure that we obtain close lock BEFORE even trying to quit the application. const bool locked_safely = feedUpdateLock()->tryLock(4 * CLOSE_LOCK_TIMEOUT); processEvents(); qDebugNN << LOGSEC_CORE << "Cleaning up resources and saving application state."; if (locked_safely) { // Application obtained permission to close in a safe way. qDebugNN << LOGSEC_CORE << "Close lock was obtained safely."; // We locked the lock to exit peacefully, unlock it to avoid warnings. feedUpdateLock()->unlock(); } else { // Request for write lock timed-out. This means // that some critical action can be processed right now. qWarningNN << LOGSEC_CORE << "Close lock timed-out."; } qApp->feedReader()->quit(); database()->driver()->saveDatabase(); if (mainForm() != nullptr) { mainForm()->saveSize(); } settings()->sync(); // Now, we can check if application should just quit or restart itself. if (m_shouldRestart) { finish(); qDebugNN << LOGSEC_CORE << "Killing local peer connection to allow another instance to start."; if (QProcess::startDetached(QDir::toNativeSeparators(applicationFilePath()), arguments().mid(1))) { qDebugNN << LOGSEC_CORE << "New application instance was started."; } else { qCriticalNN << LOGSEC_CORE << "New application instance was not started successfully."; } } } void Application::showMessagesNumber(int unread_messages, bool any_feed_has_new_unread_messages) { if (m_trayIcon != nullptr) { m_trayIcon->setNumber(unread_messages, any_feed_has_new_unread_messages); } // Set task bar overlay with number of unread articles. #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) // Use D-Bus "LauncherEntry" service on Linux. bool task_bar_count_enabled = settings()->value(GROUP(GUI), SETTING(GUI::UnreadNumbersOnTaskBar)).toBool(); QDBusMessage signal = QDBusMessage::createSignal(QSL("/"), QSL("com.canonical.Unity.LauncherEntry"), QSL("Update")); signal << QSL("application://%1.desktop").arg(APP_REVERSE_NAME); QVariantMap setProperty; setProperty.insert("count", qint64(unread_messages)); setProperty.insert("count-visible", task_bar_count_enabled && unread_messages > 0); signal << setProperty; QDBusConnection::sessionBus().send(signal); #elif defined(Q_OS_WIN) // Use SetOverlayIcon Windows API method on Windows. bool task_bar_count_enabled = settings()->value(GROUP(GUI), SETTING(GUI::UnreadNumbersOnTaskBar)).toBool(); if (m_mainForm != nullptr) { QImage overlay_icon = generateOverlayIcon(unread_messages); #if QT_VERSION_MAJOR == 5 HICON overlay_hicon = QtWin::toHICON(QPixmap::fromImage(overlay_icon)); #else HICON overlay_hicon = overlay_icon.toHICON(); #endif HRESULT overlay_result = m_windowsTaskBar->SetOverlayIcon(reinterpret_cast(m_mainForm->winId()), (task_bar_count_enabled && unread_messages > 0) ? overlay_hicon : nullptr, nullptr); DestroyIcon(overlay_hicon); if (FAILED(overlay_result)) { qCriticalNN << LOGSEC_CORE << "Failed to set overlay icon with HRESULT:" << QUOTE_W_SPACE_DOT(overlay_result); } } #endif if (m_mainForm != nullptr) { m_mainForm->setWindowTitle(unread_messages > 0 ? QSL("[%2] %1").arg(QSL(APP_LONG_NAME), QString::number(unread_messages)) : QSL(APP_LONG_NAME)); } } #if defined(Q_OS_WIN) QImage Application::generateOverlayIcon(int number) const { QImage img(128, 128, QImage::Format::Format_ARGB32); QPainter p; QString num_txt; if (number < 1000) { num_txt = QString::number(number); } else if (number < 100000) { num_txt = QSL("%1k").arg(int(number / 1000)); } else { num_txt = QChar(8734); } QPainterPath rounded_rectangle; rounded_rectangle.addRoundedRect(QRectF(img.rect()), 15, 15); QFont fon = font(); if (num_txt.size() == 3) { fon.setPixelSize(img.width() * 0.52); } else if (num_txt.size() == 2) { fon.setPixelSize(img.width() * 0.68); } else { fon.setPixelSize(img.width() * 0.79); } p.begin(&img); p.setFont(fon); p.setRenderHint(QPainter::RenderHint::SmoothPixmapTransform, true); p.setRenderHint(QPainter::RenderHint::TextAntialiasing, true); img.fill(Qt::GlobalColor::transparent); p.fillPath(rounded_rectangle, Qt::GlobalColor::white); p.setPen(Qt::GlobalColor::black); p.drawPath(rounded_rectangle); p.drawText(img.rect().marginsRemoved(QMargins(0, 0, 0, img.height() * 0.05)), num_txt, QTextOption(Qt::AlignmentFlag::AlignCenter)); p.end(); return img; } #endif void Application::restart() { m_shouldRestart = true; quit(); } #if defined(USE_WEBENGINE) #if QT_VERSION_MAJOR == 6 void Application::downloadRequested(QWebEngineDownloadRequest* download_item) { #else void Application::downloadRequested(QWebEngineDownloadItem* download_item) { #endif downloadManager()->download(download_item->url()); download_item->cancel(); download_item->deleteLater(); } #endif void Application::onFeedUpdatesStarted() { #if defined(Q_OS_WIN) // Use SetOverlayIcon Windows API method on Windows. bool task_bar_count_enabled = settings()->value(GROUP(GUI), SETTING(GUI::UnreadNumbersOnTaskBar)).toBool(); if (task_bar_count_enabled && m_mainForm != nullptr && m_windowsTaskBar != nullptr) { m_windowsTaskBar->SetProgressValue(reinterpret_cast(m_mainForm->winId()), 1ul, 100ul); } #endif } void Application::onFeedUpdatesProgress(const Feed* feed, int current, int total) { #if defined(Q_OS_WIN) // Use SetOverlayIcon Windows API method on Windows. bool task_bar_count_enabled = settings()->value(GROUP(GUI), SETTING(GUI::UnreadNumbersOnTaskBar)).toBool(); if (task_bar_count_enabled && m_mainForm != nullptr && m_windowsTaskBar != nullptr) { m_windowsTaskBar->SetProgressValue(reinterpret_cast(m_mainForm->winId()), current, total); } #endif } void Application::onFeedUpdatesFinished(const FeedDownloadResults& results) { auto fds = results.updatedFeeds(); bool some_unquiet_feed = boolinq::from(fds).any([](const QPair& fd) { return !fd.first->isQuiet(); }); if (some_unquiet_feed) { // Now, inform about results via GUI message/notification. qApp->showGuiMessage(Notification::Event::NewUnreadArticlesFetched, {tr("Unread articles fetched"), results.overview(10), QSystemTrayIcon::MessageIcon::NoIcon}); } #if defined(Q_OS_WIN) // Use SetOverlayIcon Windows API method on Windows. bool task_bar_count_enabled = settings()->value(GROUP(GUI), SETTING(GUI::UnreadNumbersOnTaskBar)).toBool(); if (task_bar_count_enabled && m_mainForm != nullptr && m_windowsTaskBar != nullptr) { m_windowsTaskBar->SetProgressState(reinterpret_cast(m_mainForm->winId()), TBPFLAG::TBPF_NOPROGRESS); } #endif } void Application::setupCustomDataFolder(const QString& data_folder) { if (!QDir().mkpath(data_folder)) { qCriticalNN << LOGSEC_CORE << "Failed to create custom data path" << QUOTE_W_SPACE(data_folder) << "thus falling back to standard setup."; m_customDataFolder = QString(); return; } // Disable single instance mode. m_allowMultipleInstances = true; // Save custom data folder. m_customDataFolder = data_folder; } void Application::onAdBlockFailure() { qApp->showGuiMessage(Notification::Event::GeneralEvent, {tr("AdBlock needs to be configured"), tr("AdBlock is not configured properly. Go to \"Settings\" -> \"Node.js\" and check " "if your Node.js is properly configured."), QSystemTrayIcon::MessageIcon::Critical}, {true, true, false}); qApp->settings()->setValue(GROUP(AdBlock), AdBlock::AdBlockEnabled, false); } void Application::determineFirstRuns() { m_firstRunEver = settings()->value(GROUP(General), SETTING(General::FirstRun)).toBool(); m_firstRunCurrentVersion = settings()->value(GROUP(General), QString(General::FirstRun) + QL1C('_') + APP_VERSION, true).toBool(); eliminateFirstRuns(); } void Application::parseCmdArgumentsFromOtherInstance(const QString& message) { if (message.isEmpty()) { qDebugNN << LOGSEC_CORE << "No execution message received from other app instances."; return; } qDebugNN << LOGSEC_CORE << "Received" << QUOTE_W_SPACE(message) << "execution message."; #if QT_VERSION >= 0x050F00 // Qt >= 5.15.0 QStringList messages = message.split(QSL(ARGUMENTS_LIST_SEPARATOR), Qt::SplitBehaviorFlags::SkipEmptyParts); #else QStringList messages = message.split(QSL(ARGUMENTS_LIST_SEPARATOR), QString::SplitBehavior::SkipEmptyParts); #endif QCommandLineParser cmd_parser; messages.prepend(qApp->applicationFilePath()); cmd_parser.addOption(QCommandLineOption({QSL(CLI_QUIT_INSTANCE)})); cmd_parser.addOption(QCommandLineOption({QSL(CLI_IS_RUNNING)})); fillCmdArgumentsParser(cmd_parser); if (!cmd_parser.parse(messages)) { qCriticalNN << LOGSEC_CORE << cmd_parser.errorText(); } if (cmd_parser.isSet(QSL(CLI_QUIT_INSTANCE))) { quit(); return; } else if (cmd_parser.isSet(QSL(CLI_IS_RUNNING))) { showGuiMessage(Notification::Event::GeneralEvent, {tr("Already running"), tr("Application is already running."), QSystemTrayIcon::MessageIcon::Information}); mainForm()->display(); } messages = cmd_parser.positionalArguments(); for (const QString& msg : qAsConst(messages)) { // Application was running, and someone wants to add new feed. ServiceRoot* rt = boolinq::from(feedReader()->feedsModel()->serviceRoots()).firstOrDefault([](ServiceRoot* root) { return root->supportsFeedAdding(); }); if (rt != nullptr) { rt->addNewFeed(nullptr, msg); } else { showGuiMessage(Notification::Event::GeneralEvent, {tr("Cannot add feed"), tr("Feed cannot be added because there is no active account which can add feeds."), QSystemTrayIcon::MessageIcon::Warning}); } } } void Application::parseCmdArgumentsFromMyInstance(const QStringList& raw_cli_args, QString& custom_ua) { fillCmdArgumentsParser(m_cmdParser); m_cmdParser.setApplicationDescription(QSL(APP_NAME)); m_cmdParser.setSingleDashWordOptionMode(QCommandLineParser::SingleDashWordOptionMode::ParseAsLongOptions); if (!m_cmdParser.parse(raw_cli_args)) { qCriticalNN << LOGSEC_CORE << m_cmdParser.errorText(); } s_customLogFile = m_cmdParser.value(QSL(CLI_LOG_SHORT)); if (s_customLogFile.startsWith('\'')) { s_customLogFile = s_customLogFile.mid(1); } if (s_customLogFile.endsWith('\'')) { s_customLogFile.chop(1); } if (m_cmdParser.isSet(QSL(CLI_NDEBUG_SHORT))) { QLoggingCategory::setFilterRules(QSL("*.debug=false")); } if (!m_cmdParser.value(QSL(CLI_DAT_SHORT)).isEmpty()) { auto data_folder = QDir::toNativeSeparators(m_cmdParser.value(QSL(CLI_DAT_SHORT))); qDebugNN << LOGSEC_CORE << "User wants to use custom directory for user data (and disable single instance mode):" << QUOTE_W_SPACE_DOT(data_folder); setupCustomDataFolder(data_folder); } else { m_allowMultipleInstances = false; } if (m_cmdParser.isSet(QSL(CLI_HELP_SHORT))) { m_cmdParser.showHelp(); } else if (m_cmdParser.isSet(QSL(CLI_VER_SHORT))) { m_cmdParser.showVersion(); } #if defined(USE_WEBENGINE) m_forcedNoWebEngine = m_cmdParser.isSet(QSL(CLI_FORCE_NOWEBENGINE_SHORT)); if (m_forcedNoWebEngine) { qDebugNN << LOGSEC_CORE << "Forcing no-web-engine."; } #endif if (m_cmdParser.isSet(QSL(CLI_SIN_SHORT))) { m_allowMultipleInstances = true; qDebugNN << LOGSEC_CORE << "Explicitly allowing this instance to run."; } if (m_cmdParser.isSet(QSL(CLI_NSTDOUTERR_SHORT))) { s_disableDebug = true; qDebugNN << LOGSEC_CORE << "Disabling any stdout/stderr outputs."; } if (!m_cmdParser.value(QSL(CLI_ADBLOCKPORT_SHORT)).isEmpty()) { m_customAdblockPort = m_cmdParser.value(QSL(CLI_ADBLOCKPORT_SHORT)).toInt(); qDebugNN << LOGSEC_ADBLOCK << "Setting custom server port."; } else { m_customAdblockPort = 0; } custom_ua = m_cmdParser.value(QSL(CLI_USERAGENT_SHORT)); } void Application::displayLog() { if (m_logForm == nullptr) { m_logForm = new FormLog(m_mainForm); connect(this, &Application::sendLogToDialog, m_logForm, &FormLog::appendLogMessage, Qt::ConnectionType::QueuedConnection); } m_logForm->show(); } void Application::fillCmdArgumentsParser(QCommandLineParser& parser) { QCommandLineOption help({QSL(CLI_HELP_SHORT), QSL(CLI_HELP_LONG)}, QSL("Displays overview of CLI.")); QCommandLineOption version({QSL(CLI_VER_SHORT), QSL(CLI_VER_LONG)}, QSL("Displays version of the application.")); QCommandLineOption log_file({QSL(CLI_LOG_SHORT), QSL(CLI_LOG_LONG)}, QSL("Write application debug log to file. Note that logging to file may slow application down."), QSL("log-file")); QCommandLineOption custom_data_folder({QSL(CLI_DAT_SHORT), QSL(CLI_DAT_LONG)}, QSL("Use custom folder for user data and disable single instance application mode."), QSL("user-data-folder")); QCommandLineOption disable_singleinstance({QSL(CLI_SIN_SHORT), QSL(CLI_SIN_LONG)}, QSL("Allow running of multiple application instances.")); #if defined(USE_WEBENGINE) QCommandLineOption force_nowebengine({QSL(CLI_FORCE_NOWEBENGINE_SHORT), QSL(CLI_FORCE_NOWEBENGINE_LONG)}, QSL("Force usage of simpler text-based embedded web browser.")); #endif QCommandLineOption disable_only_debug({QSL(CLI_NDEBUG_SHORT), QSL(CLI_NDEBUG_LONG)}, QSL("Disable just \"debug\" output.")); QCommandLineOption disable_debug({QSL(CLI_NSTDOUTERR_SHORT), QSL(CLI_NSTDOUTERR_LONG)}, QSL("Completely disable stdout/stderr outputs.")); QCommandLineOption forced_style({QSL(CLI_STYLE_SHORT), QSL(CLI_STYLE_LONG)}, QSL("Force some application style."), QSL("style-name")); QCommandLineOption custom_ua({QSL(CLI_USERAGENT_SHORT), QSL(CLI_USERAGENT_LONG)}, QSL("User custom User-Agent HTTP header for all network requests."), QSL("user-agent")); QCommandLineOption adblock_port({QSL(CLI_ADBLOCKPORT_SHORT), QSL(CLI_ADBLOCKPORT_LONG)}, QSL("Use custom port for AdBlock server. It is highly recommended to use values higher than 1024."), QSL("port")); parser.addOptions({ help, version, log_file, custom_data_folder, disable_singleinstance, disable_only_debug, disable_debug, #if defined(USE_WEBENGINE) force_nowebengine, #endif forced_style, adblock_port, custom_ua }); parser.addPositionalArgument(QSL("urls"), QSL("List of URL addresses pointing to individual online feeds which should be added."), QSL("[url-1 ... url-n]")); } void Application::onNodeJsPackageUpdateError(const QList& pkgs, const QString& error) { qApp->showGuiMessage(Notification::Event::NodePackageFailedToUpdate, {{}, tr("Packages %1 were NOT updated because of error: %2.") .arg(NodeJs::packagesToString(pkgs), error), QSystemTrayIcon::MessageIcon::Critical}); } void Application::onNodeJsPackageInstalled(const QList& pkgs, bool already_up_to_date) { if (!already_up_to_date) { qApp->showGuiMessage(Notification::Event::NodePackageUpdated, {{}, tr("Packages %1 were updated.").arg(NodeJs::packagesToString(pkgs)), QSystemTrayIcon::MessageIcon::Information}); } } QString Application::customDataFolder() const { return m_customDataFolder; }