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
|
||||
|
||||
#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()) {
|
||||
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,13 +407,17 @@ void action_refresh_all_channels(bool ask=true) {
|
|||
if(count)
|
||||
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, {
|
||||
{"{{updatedChannels}}", std::to_string(updated_channels)},
|
||||
{"{{newVideos}}", std::to_string(new_videos)}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void action_mark_video_watched() {
|
||||
Channel &ch = channels.at(selected_channel);
|
||||
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
#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_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',
|
||||
]
|
||||
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