diff --git a/kaffeine/src/player-parts/libmpv-part/CMakeLists.txt b/kaffeine/src/player-parts/libmpv-part/CMakeLists.txt index bfc0f0f..4a903ae 100644 --- a/kaffeine/src/player-parts/libmpv-part/CMakeLists.txt +++ b/kaffeine/src/player-parts/libmpv-part/CMakeLists.txt @@ -22,8 +22,7 @@ link_directories( tde_add_kpart( libmpvpart AUTOMOC - SOURCES - libmpv_part.cpp libmpv_event.cpp + SOURCES libmpv_part.cpp libmpv_event.cpp libmpv_widget.cpp LINK tdecore-shared tdeui-shared diff --git a/kaffeine/src/player-parts/libmpv-part/libmpv_event.cpp b/kaffeine/src/player-parts/libmpv-part/libmpv_event.cpp index f6edbec..f328eac 100644 --- a/kaffeine/src/player-parts/libmpv-part/libmpv_event.cpp +++ b/kaffeine/src/player-parts/libmpv-part/libmpv_event.cpp @@ -22,6 +22,8 @@ // TQt #include +#include +#include // TDE #include @@ -75,10 +77,7 @@ void MpvEventThread::processEvent(mpv_event *event) { << msg->text << endl; m_part->setStatusBarText(TQString("MPV %1 (%2): %3").arg(msg->prefix, msg->level, msg->text)); - - if (TQString(msg->level) == "error") { - processErrorMessage(msg->prefix, msg->level, msg->text); - } + processMessage(msg->prefix, msg->level, msg->text); break; } @@ -96,8 +95,9 @@ void MpvEventThread::processEvent(mpv_event *event) { } } -void MpvEventThread::processErrorMessage(TQString prefix, TQString level, TQString text) { - if (prefix == "recorder") { +void MpvEventThread::processMessage(TQString prefix, TQString level, TQString text) { + // Stream recorder errors + if (level == "error" && prefix == "recorder") { kdDebug() << "recorder error:" << text << endl; MpvErrorEvent *ee = new MpvErrorEvent( i18n("An error occurred in the stream recorder."), @@ -106,4 +106,42 @@ void MpvEventThread::processErrorMessage(TQString prefix, TQString level, TQStri TQApplication::postEvent(m_part, ee); m_part->stopRecording(); } + + // HACK: click-through for the context menu + // MPV does not yet natively support click-through and thus eats up all mouse + // events on its embedded window (https://github.com/mpv-player/mpv/issues/8938). + // Moreover, there seems to be no way to natively listen for keyboard/mouse events. + // This is a nasty workaround. + else if (level == "warn" && prefix == "input" && text.contains("MBTN_")) { + int button = TQt::NoButton; + if (text.contains("MBTN_LEFT")) { + button = TQt::LeftButton; + } + else if (text.contains("MBTN_MID")) { + button = TQt::MidButton; + } + else if (text.contains("MBTN_RIGHT")) { + button = TQt::RightButton; + } + + // Emulate mouse events + TQMouseEvent *me = new TQMouseEvent(TQEvent::MouseButtonPress, TQCursor::pos(), button, button); + TQApplication::postEvent(m_part->parent(), me); + me = new TQMouseEvent(TQEvent::MouseButtonRelease, TQCursor::pos(), button, button); + TQApplication::postEvent(m_part->parent(), me); + + // Emulate doubleclicks + if (text.contains("DBL")) { + TQMouseEvent *me = new TQMouseEvent(TQEvent::MouseButtonDblClick, TQCursor::pos(), button, button); + TQApplication::postEvent(m_part->parent(), me); + } + + // Context menu emulation + m_part->hideContextMenu(); + TQContextMenuEvent *cme; + if (button == TQt::RightButton) { + cme = new TQContextMenuEvent(TQContextMenuEvent::Mouse, TQCursor::pos(), TQt::RightButton); + TQApplication::postEvent(m_part->m_player, cme); + } + } } \ No newline at end of file diff --git a/kaffeine/src/player-parts/libmpv-part/libmpv_event.h b/kaffeine/src/player-parts/libmpv-part/libmpv_event.h index 477f191..1df5629 100644 --- a/kaffeine/src/player-parts/libmpv-part/libmpv_event.h +++ b/kaffeine/src/player-parts/libmpv-part/libmpv_event.h @@ -20,6 +20,9 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ +#ifndef __LIBMPVEVENT_H__ +#define __LIBMPVEVENT_H__ + // TQt #include #include @@ -90,8 +93,10 @@ class MpvEventThread : public TQThread { private: void initPropertyObservers(); void processEvent(mpv_event *event); - void processErrorMessage(TQString prefix, TQString level, TQString text); + void processMessage(TQString prefix, TQString level, TQString text); private: MpvPart *m_part; -}; \ No newline at end of file +}; + +#endif // __LIBMPVEVENT_H__ \ No newline at end of file diff --git a/kaffeine/src/player-parts/libmpv-part/libmpv_part.cpp b/kaffeine/src/player-parts/libmpv-part/libmpv_part.cpp index 27ae01a..5e89108 100644 --- a/kaffeine/src/player-parts/libmpv-part/libmpv_part.cpp +++ b/kaffeine/src/player-parts/libmpv-part/libmpv_part.cpp @@ -26,6 +26,9 @@ #include #include #include +#include +#include +#include // TDE #include @@ -46,6 +49,7 @@ // Part #include "libmpv_part.h" #include "libmpv_event.h" +#include "libmpv_widget.h" typedef KParts::GenericFactory MpvPartFactory; K_EXPORT_COMPONENT_FACTORY (libmpvpart, MpvPartFactory); @@ -54,27 +58,27 @@ MpvPart::MpvPart(TQWidget* parentWidget, const char* widgetName, TQObject* paren : KaffeinePart(parent, name ? name : "MpvPart"), m_current(0), m_seeking(false), - m_recordFilePath() + m_recordFilePath(), + m_context(nullptr) { - // Create an instance of this class - setInstance(MpvPartFactory::instance()); - - // Create container widget - m_player = new TQWidget(parentWidget); - m_player->setBackgroundColor(TQt::black); - m_player->setFocusPolicy(TQ_ClickFocus); - setWidget(m_player); - - // Initialize and embed mpv - if (!initMpv()) { - kdError() << "libmpvpart: initialization of mpv failed!" << endl; - emit canceled(i18n("MPV initialization failed!")); - return; - } + // Create an instance of this class + setInstance(MpvPartFactory::instance()); // Initialize GUI setXMLFile("libmpv_part.rc"); initActions(); + + // Create container widget + m_player = new MpvContainerWidget(this, parentWidget); + setWidget(m_player); + + // Initialize and embed mpv + if (!initMpv()) { + kdError() << "libmpvpart: initialization of mpv failed!" << endl; + emit canceled(i18n("MPV initialization failed!")); + return; + } + emit setStatusBarText(i18n("Ready")); } @@ -166,6 +170,9 @@ void MpvPart::initActions() new KWidgetAction(m_recordFile, i18n("Recording file"), 0, 0, 0, actionCollection(), "record_file"); updateRecordFileLabel(); + /*** Context menu ***/ + new TDEAction(i18n("Add subtitles"), "text-x-generic", 0, this, SLOT(slotAddSubtitles()), actionCollection(), "subtitles_add"); + resetTime(); stateChanged("disable_all"); } @@ -325,13 +332,7 @@ void MpvPart::slotPlay() { const char *args[] = {"loadfile", curMRL.url().local8Bit(), nullptr}; mpv_command_async(m_mpv, 0, args); - // Add subtitles - if ((!curMRL.subtitleFiles().isEmpty()) && (curMRL.currentSubtitle() > -1)) { - kdDebug() << "adding subs" << endl; - const char *args[] = {"sub-add", curMRL.subtitleFiles()[curMRL.currentSubtitle()].local8Bit(), nullptr}; - mpv_command_async(m_mpv, 0, args); - } - + slotReloadSubtitles(); } else { emit signalRequestCurrentTrack(); @@ -467,6 +468,23 @@ void MpvPart::slotMute() { mpv_set_property(m_mpv, "ao-mute", MPV_FORMAT_FLAG, &value); } +void MpvPart::slotReloadSubtitles() { + TQStringList subtitles = m_mrl.subtitleFiles(); + int subId = m_mrl.currentSubtitle(); + + // Remove existing subtitles synchronously + const char *rm_args[] = {"sub-remove", nullptr}; + mpv_command(m_mpv, rm_args); + + if ((!subtitles.isEmpty()) && (subId > -1)) { + kdDebug() << "adding subtitle file " << subtitles[subId] << endl; + + // Add requested subtitles + const char *add_args[] = {"sub-add", subtitles[subId].local8Bit(), nullptr}; + mpv_command_async(m_mpv, 0, add_args); + } +} + void MpvPart::slotToggleRecording() { if (m_recordAction->isChecked()) { startRecording(); @@ -527,4 +545,98 @@ void MpvPart::updateRecordFileLabel() { ); } +void MpvPart::slotAddSubtitles() { + TQString openURL = m_mrl.url(); + TQString subtitleURL = KFileDialog::getOpenURL(openURL, + i18n("*.smi *.srt *.sub *.txt *.ssa *.asc|Subtitle Files\n*.*|All Files"), + 0, i18n("Select Subtitle File")).path(); + + kdDebug()<<"slotAddSubtitles: "<itemParameter(choice); + if (subId == choice) { + // If itemParameter() returns the menu item id, then we know that + // the item has no item parameter, hence it is not an option that + // we handle through this slot. + return; + } + + kdDebug() << "switching to subtitle: " << subId + << " (" << m_subs->text(choice) << ")" << endl; + m_mrl.setCurrentSubtitle(subId); + emit signalNewMeta(m_mrl); + slotReloadSubtitles(); +} + + +void MpvPart::showContextMenu() { + /*** Context menu ***/ + m_context = new TQPopupMenu(0); + actionCollection()->action("player_play")->plug(m_context); + actionCollection()->action("player_pause")->plug(m_context); + actionCollection()->action("player_stop")->plug(m_context); + m_context->insertSeparator(); + actionCollection()->action("player_previous")->plug(m_context); + actionCollection()->action("player_next")->plug(m_context); + m_context->insertSeparator(); + + /*** Subtitles submenu ***/ + m_subs = new TQPopupMenu(0); + actionCollection()->action("subtitles_add")->plug(m_subs); + TQStringList subtitles = m_mrl.subtitleFiles(); + + if (m_mrl.isEmpty()) { + m_subs->setEnabled(false); + } + + m_subs->insertSeparator(); + int noSubsItem = m_subs->insertItem(i18n("None")); + m_subs->setItemParameter(noSubsItem, -1); + if (!subtitles.empty()) { + int index; + for (TQStringList::Iterator it = subtitles.begin(); it != subtitles.end(); ++it) { + int id = m_subs->insertItem((*it)); + m_subs->setItemParameter(id, index); + + // Comparing an index is cheaper than doing a string comparison + if (index == m_mrl.currentSubtitle()) { + m_subs->setItemChecked(id, true); + } + index++; + } + connect(m_subs, SIGNAL(activated(int)), this, SLOT(slotContextSetSubtitles(int))); + } + + if (subtitles.empty() || m_mrl.currentSubtitle() < 0) { + m_subs->setItemChecked(noSubsItem, true); + } + + m_context->insertItem(i18n("Sub&titles"), m_subs); + m_context->exec(TQCursor::pos()); + + hideContextMenu(); +} + +void MpvPart::hideContextMenu() { + if (m_context) { + delete m_context; + m_context = nullptr; + } + + if (!m_subs) { + delete m_subs; + m_subs = nullptr; + } +} + #include "libmpv_part.moc" \ No newline at end of file diff --git a/kaffeine/src/player-parts/libmpv-part/libmpv_part.h b/kaffeine/src/player-parts/libmpv-part/libmpv_part.h index ecd6641..0b94f36 100644 --- a/kaffeine/src/player-parts/libmpv-part/libmpv_part.h +++ b/kaffeine/src/player-parts/libmpv-part/libmpv_part.h @@ -37,9 +37,11 @@ class TQLabel; class TQSlider; +class TQPopupMenu; class TQPushButton; class TDEToggleAction; class MpvEventThread; +class MpvContainerWidget; /** * MpvPart -- A KaffeinePart which uses libmpv as backend @@ -52,6 +54,7 @@ class MpvPart : public KaffeinePart TQ_OBJECT friend class MpvEventThread; + friend class MpvContainerWidget; public: MpvPart(TQWidget*, const char*, TQObject*, const char*, const TQStringList&); @@ -81,27 +84,38 @@ class MpvPart : public KaffeinePart void slotPlay(); void slotPause(bool pause); void slotTogglePause(); - void slotSetVolume(uint); /* percent */ - void slotSetPosition(uint); /* percent */ void slotStop(); + void slotMute(); + void slotSetVolume(uint); /* percent */ + void slotPrevious(); void slotNext(); + void slotSetPosition(uint); /* percent */ + void slotToggleRecording(); void startRecording(); void stopRecording(); void slotSetRecordingFile(); + void slotAddSubtitles(); + void slotReloadSubtitles(); + + void showContextMenu(); + void hideContextMenu(); + signals: void mpvEvents(); protected: mpv_handle *m_mpv; + MRL m_mrl; private slots: void slotStartSeeking(); void slotStopSeeking(); void slotSetSeekingPos(int pos); + void slotContextSetSubtitles(int choice); // Overloaded method with "int" argument // Used in connection with m_volume::sliderMoved(int) @@ -122,8 +136,9 @@ class MpvPart : public KaffeinePart TQSlider *m_volume; TQPushButton *m_playtime; TQLabel *m_recordFile; + TQPopupMenu *m_context; + TQPopupMenu *m_subs; - MRL m_mrl; TQValueList m_playlist; uint m_current; diff --git a/kaffeine/src/player-parts/libmpv-part/libmpv_widget.cpp b/kaffeine/src/player-parts/libmpv-part/libmpv_widget.cpp new file mode 100644 index 0000000..b5bfbf6 --- /dev/null +++ b/kaffeine/src/player-parts/libmpv-part/libmpv_widget.cpp @@ -0,0 +1,36 @@ +/* + * Kaffeine libmpv part + * Copyright (C) 2023 Mavridis Philippe + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +// Part +#include "libmpv_widget.h" +#include "libmpv_part.h" + +MpvContainerWidget::MpvContainerWidget(MpvPart *part, TQWidget* parentWidget) + : TQWidget(parentWidget), m_part(part) +{ + setBackgroundColor(TQt::black); + setFocusPolicy(TQ_ClickFocus); +} + +MpvContainerWidget::~MpvContainerWidget() { +} + +void MpvContainerWidget::contextMenuEvent(TQContextMenuEvent *) { + m_part->showContextMenu(); +} \ No newline at end of file diff --git a/kaffeine/src/player-parts/libmpv-part/libmpv_widget.h b/kaffeine/src/player-parts/libmpv-part/libmpv_widget.h new file mode 100644 index 0000000..520574d --- /dev/null +++ b/kaffeine/src/player-parts/libmpv-part/libmpv_widget.h @@ -0,0 +1,39 @@ +/* + * Kaffeine libmpv part + * Copyright (C) 2023 Mavridis Philippe + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef __LIBMPVWIDGET_H__ +#define __LIBMPVWIDGET_H__ + +#include + +class MpvPart; + +class MpvContainerWidget : public TQWidget { + public: + MpvContainerWidget(MpvPart *part, TQWidget *parentWidget); + ~MpvContainerWidget(); + + private: + void contextMenuEvent(TQContextMenuEvent *); + + private: + MpvPart *m_part; +}; + +#endif // __LIBMPVWIDGET_H__ \ No newline at end of file