Add Qt5/KDE based "application host" (systray icon, native notifications)
This commit is contained in:
parent
b8bc284c8d
commit
00281a1a0d
|
@ -1,4 +1,7 @@
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "application.h"
|
||||||
|
|
||||||
#define _X_OPEN_SOURCE
|
#define _X_OPEN_SOURCE
|
||||||
|
|
||||||
#include "tui.h"
|
#include "tui.h"
|
||||||
|
@ -35,6 +38,8 @@ size_t title_offset = 0;
|
||||||
bool any_title_in_next_half = false;
|
bool any_title_in_next_half = false;
|
||||||
bool clear_channels_on_change = false;
|
bool clear_channels_on_change = false;
|
||||||
|
|
||||||
|
static application_host *host = nullptr;
|
||||||
|
|
||||||
static termpaint_attr* get_attr(const AttributeSetType type, const bool highlight=false)
|
static termpaint_attr* get_attr(const AttributeSetType type, const bool highlight=false)
|
||||||
{
|
{
|
||||||
return highlight ? attributes[type].highlight : attributes[type].normal;
|
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));
|
const int new_videos = fetch_videos_for_channel(channels.at(selected_channel));
|
||||||
if(new_videos == 0 || ch.is_virtual)
|
if(new_videos == 0 || ch.is_virtual)
|
||||||
return;
|
return;
|
||||||
if(new_videos == 1 && !notify_channel_new_video_command.empty()) {
|
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, {
|
run_command(notify_channel_new_video_command, {
|
||||||
{"{{channelName}}", ch.name},
|
{"{{channelName}}", ch.name},
|
||||||
{"{{videoTitle}}", videos[ch.id].front().title},
|
{"{{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()) {
|
} else if(notify_channel_new_videos_command.size()) {
|
||||||
run_command(notify_channel_new_videos_command, {
|
run_command(notify_channel_new_videos_command, {
|
||||||
{"{{channelName}}", ch.name},
|
{"{{channelName}}", ch.name},
|
||||||
{"{{newVideos}}", std::to_string(new_videos)}
|
{"{{newVideos}}", std::to_string(new_videos)}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void action_refresh_all_channels(bool ask=true) {
|
void action_refresh_all_channels(bool ask=true) {
|
||||||
|
@ -395,12 +407,16 @@ void action_refresh_all_channels(bool ask=true) {
|
||||||
if(count)
|
if(count)
|
||||||
updated_channels++;
|
updated_channels++;
|
||||||
}
|
}
|
||||||
if(updated_channels && new_videos && !notify_channels_new_videos_command.empty()) {
|
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, {
|
run_command(notify_channels_new_videos_command, {
|
||||||
{"{{updatedChannels}}", std::to_string(updated_channels)},
|
{"{{updatedChannels}}", std::to_string(updated_channels)},
|
||||||
{"{{newVideos}}", std::to_string(new_videos)}
|
{"{{newVideos}}", std::to_string(new_videos)}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void action_mark_video_watched() {
|
void action_mark_video_watched() {
|
||||||
|
@ -645,6 +661,10 @@ static void run()
|
||||||
|
|
||||||
bool draw = true;
|
bool draw = true;
|
||||||
do {
|
do {
|
||||||
|
if(host && host->quit && host->quit()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if(draw) {
|
if(draw) {
|
||||||
Channel &channel = channels.at(selected_channel);
|
Channel &channel = channels.at(selected_channel);
|
||||||
termpaint_surface_clear(surface, TERMPAINT_DEFAULT_COLOR, TERMPAINT_DEFAULT_COLOR);
|
termpaint_surface_clear(surface, TERMPAINT_DEFAULT_COLOR, TERMPAINT_DEFAULT_COLOR);
|
||||||
|
@ -654,7 +674,7 @@ static void run()
|
||||||
}
|
}
|
||||||
draw = true;
|
draw = true;
|
||||||
|
|
||||||
auto event = tp_wait_for_event(1000);
|
auto event = tp_wait_for_event(500);
|
||||||
if(!event)
|
if(!event)
|
||||||
abort();
|
abort();
|
||||||
|
|
||||||
|
@ -683,9 +703,10 @@ void run_standalone()
|
||||||
tp_shutdown();
|
tp_shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
void run_embedded(int pty_fd)
|
void run_embedded(int pty_fd, application_host *_host)
|
||||||
{
|
{
|
||||||
tp_init_from_fd(pty_fd);
|
tp_init_from_fd(pty_fd);
|
||||||
|
host = _host;
|
||||||
run();
|
run();
|
||||||
tp_shutdown();
|
tp_shutdown();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,14 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct application_host {
|
||||||
|
std::function<bool()> quit = nullptr;
|
||||||
|
std::function<void(const std::string &channel, const std::string &title)> notify_channel_single_video = nullptr;
|
||||||
|
std::function<void(const std::string &channel, const int count)> notify_channel_multiple_videos = nullptr;
|
||||||
|
std::function<void(const int channels, const int count)> notify_channels_multiple_videos = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
void run_standalone();
|
void run_standalone();
|
||||||
void run_embedded(int pty_fd);
|
void run_embedded(int pty_fd, application_host *host);
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
|
@ -0,0 +1,96 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="256"
|
||||||
|
height="256"
|
||||||
|
viewBox="0 0 67.733332 67.733335"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
||||||
|
sodipodi:docname="icon_0.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="3.959798"
|
||||||
|
inkscape:cx="96.726796"
|
||||||
|
inkscape:cy="133.29632"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1135"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
style="fill:#808080;fill-opacity:1;stroke:#7f7f7f;stroke-width:2.11666667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;stroke-linejoin:miter;opacity:0.5"
|
||||||
|
id="rect889"
|
||||||
|
width="63.500004"
|
||||||
|
height="46.302082"
|
||||||
|
x="2.1166673"
|
||||||
|
y="10.715626" />
|
||||||
|
<rect
|
||||||
|
style="fill:#1a1a1a;fill-opacity:1;stroke-width:0.330729"
|
||||||
|
id="rect10"
|
||||||
|
width="63.5"
|
||||||
|
height="42.333332"
|
||||||
|
x="2.1166673"
|
||||||
|
y="14.684376" />
|
||||||
|
<rect
|
||||||
|
style="fill:#cccccc;fill-opacity:1;stroke-width:0.0980494"
|
||||||
|
id="rect10-3"
|
||||||
|
width="59.53125"
|
||||||
|
height="3.96875"
|
||||||
|
x="2.1166673"
|
||||||
|
y="10.715626" />
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-weight:normal;font-size:29.1042px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
|
||||||
|
x="4.7997642"
|
||||||
|
y="43.432621"
|
||||||
|
id="text856"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan854"
|
||||||
|
x="4.7997642"
|
||||||
|
y="43.432621"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:29.1042px;font-family:Hack;-inkscape-font-specification:Hack;fill:#00ff00;stroke-width:0.264583">>yt</tspan></text>
|
||||||
|
<rect
|
||||||
|
style="fill:#666666;fill-opacity:1;stroke-width:0.0253162"
|
||||||
|
id="rect869"
|
||||||
|
width="3.9687486"
|
||||||
|
height="3.96875"
|
||||||
|
x="61.647919"
|
||||||
|
y="10.715626" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
20
meson.build
20
meson.build
|
@ -30,3 +30,23 @@ tui_files = [
|
||||||
'yttui.cpp',
|
'yttui.cpp',
|
||||||
]
|
]
|
||||||
executable('yttui', tui_files, link_with: [application], install: true)
|
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
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
#include <QApplication>
|
||||||
|
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QMainWindow>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QMetaEnum>
|
||||||
|
#include <QMetaObject>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QString>
|
||||||
|
#include <QSystemTrayIcon>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#include <KParts/Part>
|
||||||
|
#include <KParts/ReadOnlyPart>
|
||||||
|
#include <KPluginFactory>
|
||||||
|
#include <KPluginLoader>
|
||||||
|
#include <KService>
|
||||||
|
|
||||||
|
#include <kde_terminal_interface.h>
|
||||||
|
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <pty.h>
|
||||||
|
|
||||||
|
#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<QMouseEvent*>(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<KParts::ReadOnlyPart>(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"
|
|
@ -0,0 +1,5 @@
|
||||||
|
<RCC>
|
||||||
|
<qresource prefix="/">
|
||||||
|
<file>icons/icon_0.png</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
Loading…
Reference in New Issue