From 00281a1a0d0ffd31d90e91562d94554a7da5aa8e Mon Sep 17 00:00:00 2001 From: trilader Date: Mon, 21 Dec 2020 00:00:01 +0100 Subject: [PATCH] Add Qt5/KDE based "application host" (systray icon, native notifications) --- application.cpp | 57 +++++++++----- application.h | 12 ++- icons/icon_0.png | Bin 0 -> 3051 bytes icons/icon_0.svg | 96 +++++++++++++++++++++++ meson.build | 20 +++++ yttui-qt5.cpp | 194 +++++++++++++++++++++++++++++++++++++++++++++++ yttui-qt5.qrc | 5 ++ 7 files changed, 365 insertions(+), 19 deletions(-) create mode 100644 icons/icon_0.png create mode 100644 icons/icon_0.svg create mode 100644 yttui-qt5.cpp create mode 100644 yttui-qt5.qrc diff --git a/application.cpp b/application.cpp index d80bda5..410f3c2 100644 --- a/application.cpp +++ b/application.cpp @@ -1,4 +1,7 @@ // SPDX-License-Identifier: MIT + +#include "application.h" + #define _X_OPEN_SOURCE #include "tui.h" @@ -35,6 +38,8 @@ size_t title_offset = 0; bool any_title_in_next_half = false; bool clear_channels_on_change = false; +static application_host *host = nullptr; + static termpaint_attr* get_attr(const AttributeSetType type, const bool highlight=false) { return highlight ? attributes[type].highlight : attributes[type].normal; @@ -368,18 +373,25 @@ void action_refresh_channel() { const int new_videos = fetch_videos_for_channel(channels.at(selected_channel)); if(new_videos == 0 || ch.is_virtual) return; - if(new_videos == 1 && !notify_channel_new_video_command.empty()) { - run_command(notify_channel_new_video_command, { - {"{{channelName}}", ch.name}, - {"{{videoTitle}}", videos[ch.id].front().title}, - }); - } else if(notify_channel_new_videos_command.size()) { - run_command(notify_channel_new_videos_command, { - {"{{channelName}}", ch.name}, - {"{{newVideos}}", std::to_string(new_videos)} - }); + if(new_videos == 1) { + if(host && host->notify_channel_single_video) { + host->notify_channel_single_video(ch.name, videos[ch.id].front().title); + } else if(!notify_channel_new_video_command.empty()) { + run_command(notify_channel_new_video_command, { + {"{{channelName}}", ch.name}, + {"{{videoTitle}}", videos[ch.id].front().title}, + }); + } + } else { + if(host && host->notify_channel_multiple_videos) { + host->notify_channel_multiple_videos(ch.name, new_videos); + } else if(notify_channel_new_videos_command.size()) { + run_command(notify_channel_new_videos_command, { + {"{{channelName}}", ch.name}, + {"{{newVideos}}", std::to_string(new_videos)} + }); + } } - } void action_refresh_all_channels(bool ask=true) { @@ -395,11 +407,15 @@ void action_refresh_all_channels(bool ask=true) { if(count) updated_channels++; } - if(updated_channels && new_videos && !notify_channels_new_videos_command.empty()) { - run_command(notify_channels_new_videos_command, { - {"{{updatedChannels}}", std::to_string(updated_channels)}, - {"{{newVideos}}", std::to_string(new_videos)} - }); + if(updated_channels && new_videos) { + if(host && host->notify_channels_multiple_videos) { + host->notify_channels_multiple_videos(updated_channels, new_videos); + } else if(!notify_channels_new_videos_command.empty()) { + run_command(notify_channels_new_videos_command, { + {"{{updatedChannels}}", std::to_string(updated_channels)}, + {"{{newVideos}}", std::to_string(new_videos)} + }); + } } } @@ -645,6 +661,10 @@ static void run() bool draw = true; do { + if(host && host->quit && host->quit()) { + break; + } + if(draw) { Channel &channel = channels.at(selected_channel); termpaint_surface_clear(surface, TERMPAINT_DEFAULT_COLOR, TERMPAINT_DEFAULT_COLOR); @@ -654,7 +674,7 @@ static void run() } draw = true; - auto event = tp_wait_for_event(1000); + auto event = tp_wait_for_event(500); if(!event) abort(); @@ -683,9 +703,10 @@ void run_standalone() tp_shutdown(); } -void run_embedded(int pty_fd) +void run_embedded(int pty_fd, application_host *_host) { tp_init_from_fd(pty_fd); + host = _host; run(); tp_shutdown(); } diff --git a/application.h b/application.h index 816a080..e261ca4 100644 --- a/application.h +++ b/application.h @@ -1,4 +1,14 @@ #pragma once +#include +#include + +struct application_host { + std::function quit = nullptr; + std::function notify_channel_single_video = nullptr; + std::function notify_channel_multiple_videos = nullptr; + std::function notify_channels_multiple_videos = nullptr; +}; + void run_standalone(); -void run_embedded(int pty_fd); +void run_embedded(int pty_fd, application_host *host); diff --git a/icons/icon_0.png b/icons/icon_0.png new file mode 100644 index 0000000000000000000000000000000000000000..155a0b6e6316e9a4d7d1710d697903fe04528bb4 GIT binary patch literal 3051 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 + + + + + + + image/svg+xml + + + + + + + + + + >yt + + + diff --git a/meson.build b/meson.build index 3c87c1d..e21bb17 100644 --- a/meson.build +++ b/meson.build @@ -30,3 +30,23 @@ tui_files = [ 'yttui.cpp', ] executable('yttui', tui_files, link_with: [application], install: true) + +qt5 = import('qt5') +qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Widgets'], required: false) +if qt5_dep.found() + kf5service_dep = dependency('KF5Service', required: false) + kf5core_dep = dependency('KF5CoreAddons', required: false) + kf5parts_dep = dependency('KF5Parts', required: false) + + cppc = meson.get_compiler('cpp') + libutil_dep = cppc.find_library('util') + + moc_files = qt5.preprocess(moc_sources: ['yttui-qt5.cpp'], qresources: ['yttui-qt5.qrc']) + + executable('yttui-qt5', + ['yttui-qt5.cpp', moc_files], + link_with: [application], + dependencies: [libutil_dep, qt5_dep, kf5service_dep, kf5core_dep, kf5parts_dep], + install: true + ) +endif diff --git a/yttui-qt5.cpp b/yttui-qt5.cpp new file mode 100644 index 0000000..c5288bf --- /dev/null +++ b/yttui-qt5.cpp @@ -0,0 +1,194 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "application.h" + +application_host *host = nullptr; + +class AppThread: public QThread +{ + Q_OBJECT + int app_fd; +public: + AppThread(int fd): QThread(), app_fd(fd){} + ~AppThread() = default; + + friend bool app_quit(void*); +protected: + void run() override + { + run_embedded(app_fd, host); + } +}; + +class RightClickFilter: public QObject +{ + Q_OBJECT +protected: + bool eventFilter(QObject *obj, QEvent *event) + { + if((event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease)) { + QMouseEvent *mouseEvent = static_cast(event); + if(mouseEvent->buttons() & Qt::RightButton) { + return true; + } + } + else if(event->type() == QEvent::ContextMenu) { + return true; + } + return QObject::eventFilter(obj, event); + } +}; + +QObject* find_obj_by_classname(QObject *in, const char* name) +{ + if(strcmp(name, in->metaObject()->className()) == 0) + return in; + + for(QObject *child: in->children()) + { + QObject *maybe = find_obj_by_classname(child, name); + if(maybe) + return maybe; + } + return nullptr; +} + +class ApplicationWindow: public QMainWindow +{ + Q_OBJECT + + QIcon icon; + QSystemTrayIcon *systray = nullptr; + KParts::ReadOnlyPart *terminal = nullptr; + QWidget *konsole = nullptr; + +public: + ApplicationWindow(int fd); + + void showMessage(const QString &title, const QString &message); + + // QWidget interface +protected: + void closeEvent(QCloseEvent *event) override; +}; + +ApplicationWindow::ApplicationWindow(int fd): icon(":/icons/icon_0.png"), systray(new QSystemTrayIcon(icon, this)) +{ + setWindowTitle("yttui-qt5"); + setWindowIcon(icon); + + KService::Ptr service = KService::serviceByDesktopName(QStringLiteral("konsolepart")); + Q_ASSERT(service); + KPluginFactory* factory = KPluginLoader(service->library()).factory(); + Q_ASSERT(factory); + + terminal = factory->create(this); + if(!QMetaObject::invokeMethod(terminal, "openTeletype", Qt::AutoConnection, Q_ARG(int, fd), Q_ARG(bool, false))) { + fputs("Failed to set KonsolePart PTY\n", stderr); + abort(); + } + + konsole = terminal->widget(); + setCentralWidget(konsole); + + QObject *td = find_obj_by_classname(konsole, "Konsole::TerminalDisplay"); + if(td) { + td->installEventFilter(new RightClickFilter()); + } + + QMenu *systrayContextMenu = new QMenu(this); + systrayContextMenu->addAction(QIcon::fromTheme("application-exit"), "Exit", QApplication::instance(), &QApplication::quit); + + systray->setContextMenu(systrayContextMenu); + connect(systray, &QSystemTrayIcon::activated, [this](QSystemTrayIcon::ActivationReason reason) { + if(reason == QSystemTrayIcon::Trigger) { + if(isVisible()) { + hide(); + } else { + show(); + } + } + }); + systray->show(); +} + +void ApplicationWindow::showMessage(const QString &title, const QString &message) +{ + systray->showMessage(title, message, icon); +} + +void ApplicationWindow::closeEvent(QCloseEvent *event) +{ + QMainWindow::closeEvent(event); + systray->hide(); +} + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + int term_fd = -1, app_fd = -1; + if(openpty(&term_fd, &app_fd, 0, 0, 0)) { + perror("openpty"); + } + + ApplicationWindow window(term_fd); + window.show(); + + bool app_quit = false; + + AppThread appthread(app_fd); + QObject::connect(&appthread, &AppThread::finished, [&] { + window.close(); + }); + QObject::connect(&app, &QApplication::aboutToQuit, [&] { + app_quit = true; + appthread.wait(); + }); + + host = new application_host; + host->quit = [&]{ return app_quit; }; + host->notify_channel_single_video = [&](const std::string &channel, const std::string &title) { + window.showMessage(QStringLiteral("New video from %1").arg(QString::fromStdString(channel)), QString::fromStdString(title)); + }; + host->notify_channel_multiple_videos = [&](const std::string &channel, const int videos) { + window.showMessage(QStringLiteral("New videos from %1").arg(QString::fromStdString(channel)), + QStringLiteral("There are %1 new videos.").arg(QString::number(videos))); + }; + host->notify_channels_multiple_videos = [&](const int channels, const int videos) { + window.showMessage("New videos from multiple channels", + QStringLiteral("There are %2 new videos from %1 channels.").arg(QString::number(channels), QString::number(videos))); + }; + + appthread.start(); + int rc = app.exec(); + close(term_fd); + close(app_fd); + + delete host; + + return rc; +} + +#include "yttui-qt5.moc" diff --git a/yttui-qt5.qrc b/yttui-qt5.qrc new file mode 100644 index 0000000..a62f325 --- /dev/null +++ b/yttui-qt5.qrc @@ -0,0 +1,5 @@ + + + icons/icon_0.png + +