diff --git a/.gitignore b/.gitignore index b4e77fa32739c69ea56da53e8e768fb63951eab0..e227c36f7b7374bacba2c600686e210979634162 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ build.clang/* # Local configurations .vim/* +.vscode/* diff --git a/CMakeLists.txt b/CMakeLists.txt index a3e88ea5ce9a820890e1612082c2520272c68aed..d5ff07305cb4f88bf378c626121e1ce6c06dd07d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,14 +20,23 @@ set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED) +if(WIN32) + message("You are building on windows, pay attenion to the dependencies") + # Needed setup for Vivy to compile on Windows goes here +endif() + # Find others dependencies find_library(AVCODEC_LIBRARY avcodec 4.0 REQUIRED) find_library(AVUTIL_LIBRARY avutil 4.0 REQUIRED) find_library(SWRESAMPLE_LIBRARY swresample REQUIRED) find_library(AVFORMAT_LIBRARY avformat REQUIRED) +find_library(MPV_LIBRARY mpv REQUIRED) # Add the lua dependency -add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/vendor/lua-5.4.3" "${CMAKE_BINARY_DIR}/vendor/lua-5.4.3") +add_subdirectory( + "${CMAKE_CURRENT_SOURCE_DIR}/vendor/lua-5.4.3" + "${CMAKE_BINARY_DIR}/vendor/lua-5.4.3" +) # Grab all files file(GLOB_RECURSE Vivy_SRC CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cc") @@ -54,6 +63,7 @@ target_link_libraries(Vivy PRIVATE ${AVCODEC_LIBRARY}) target_link_libraries(Vivy PRIVATE ${AVUTIL_LIBRARY}) target_link_libraries(Vivy PRIVATE ${SWRESAMPLE_LIBRARY}) target_link_libraries(Vivy PRIVATE ${AVFORMAT_LIBRARY}) +target_link_libraries(Vivy PRIVATE ${MPV_LIBRARY}) target_link_libraries(Vivy PRIVATE lua) # Headers related things @@ -107,6 +117,9 @@ target_compile_options(Vivy PRIVATE target_link_libraries(Vivy PRIVATE -fopenmp) +# Prepare for Qt6 +target_compile_definitions(Vivy PRIVATE QT_DISABLE_DEPRECATED_BEFORE=0x050F00) + # Some compiler specific warnings and options if (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") target_compile_options(Vivy PRIVATE diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d4bae82a96bc372b6c64cd020d64a801e842c8c..a28934fb8dbc059e39cd1acd2f66e68515d05f29 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,9 +42,11 @@ the Vivy's lib and the types used in Vivy's UI. Some of the new features will be used: -- concepts (of course!) -- coroutines +- concepts (of course! at least we try) +- coroutines (not supported by compilers for now) - likely and unlikely attributes +- auto return types in functions and arguments, to pass objects like + lambda or other callable #### Unused features @@ -52,6 +54,6 @@ Some of the feature may not be used or not already be in use for multiple reasons: - the new C++20 modules feature is not used because I don't know how it -will play with `moc` + will play with `moc`. It's not supported by clangd for now anyway diff --git a/README.md b/README.md index 7afecb61aa645eeaa959dac99285630474412f57..ee2312464888bfaa77586c4d5e7b5c57191d8fc6 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,10 @@ Simply use cmake to build in another folder of the source folder: cmake -Bbuild -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang ``` -If you want to use the `compile_commands.json`, use the -`-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` option with cmake and copy the json -file into the root folder of the project. - To do a debug build, use the debug switch with cmake: -`-DCMAKE_BUILD_TYPE=Debug`. +`-DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON`. Note that +the last option is here to generate the `compile_commands.json`, you +should copy it at the root of the project. ### Dependencies @@ -24,9 +22,10 @@ way to add the av libraries to it. #### Ubuntu/debian -Vivy depends on Qt5, libav and more. On ubuntu install the following -packages: `qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools -libavutil-dev libavcodec-dev libavformat-dev`. +Vivy depends on Qt5 and libav. On ubuntu install the following packages: +`qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libavutil-dev +libavcodec-dev libavformat-dev libmpv-dev cmake clang clang-format +libomp-dev`. #### Arch/Manjaro diff --git a/rsc/VivyRessources.qrc b/rsc/VivyRessources.qrc index b27b8c63186b315b4fd7cb2fb9e8c355c97c22c7..542123ecd6cfc6901242cd5182f3821252f809ce 100644 --- a/rsc/VivyRessources.qrc +++ b/rsc/VivyRessources.qrc @@ -35,6 +35,9 @@ <file alias="folder.svg">icons/breeze-dark/folder.svg</file> <file alias="text-x-generic.svg">icons/breeze-dark/text-x-generic.svg</file> <file alias="help-about.svg">icons/breeze-dark/help-about.svg</file> + <file alias="media-pause.svg">icons/breeze-dark/media-playback-pause.svg</file> + <file alias="media-play.svg">icons/breeze-dark/media-playback-start.svg</file> + <file alias="media-stop.svg">icons/breeze-dark/media-playback-stop.svg</file> </qresource> <qresource prefix="icons/light"> <file alias="document-new.svg">icons/breeze-light/document-new.svg</file> @@ -44,6 +47,9 @@ <file alias="folder.svg">icons/breeze-light/folder.svg</file> <file alias="text-x-generic.svg">icons/breeze-light/text-x-generic.svg</file> <file alias="help-about.svg">icons/breeze-light/help-about.svg</file> + <file alias="media-pause.svg">icons/breeze-light/media-playback-pause.svg</file> + <file alias="media-play.svg">icons/breeze-light/media-playback-start.svg</file> + <file alias="media-stop.svg">icons/breeze-light/media-playback-stop.svg</file> </qresource> <!-- QDarkStyle style sheet, MIT Licence --> diff --git a/rsc/icons/breeze-dark/media-playback-pause.svg b/rsc/icons/breeze-dark/media-playback-pause.svg new file mode 100644 index 0000000000000000000000000000000000000000..6d62eb8de4e89f33c536636878600078a001e6e6 --- /dev/null +++ b/rsc/icons/breeze-dark/media-playback-pause.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#eff0f1; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="m 6 6 0 20 8 0 0 -20 z m 12 0 0 20 8 0 0 -20 z" + id="path8" + class="ColorScheme-Text" + /> +</svg> diff --git a/rsc/icons/breeze-dark/media-playback-start.svg b/rsc/icons/breeze-dark/media-playback-start.svg new file mode 100644 index 0000000000000000000000000000000000000000..2014d770a7d1ff5e449d68cefa64421b9e0e91f7 --- /dev/null +++ b/rsc/icons/breeze-dark/media-playback-start.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#eff0f1; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="m 6 6 0 20 20 -10 z" + id="path105" + class="ColorScheme-Text" + /> +</svg> diff --git a/rsc/icons/breeze-dark/media-playback-stop.svg b/rsc/icons/breeze-dark/media-playback-stop.svg new file mode 100644 index 0000000000000000000000000000000000000000..3660cf8baaad932135642fe472a347bc45e043ed --- /dev/null +++ b/rsc/icons/breeze-dark/media-playback-stop.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#eff0f1; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="m 6 6 0 20 20 0 0 -20 z" + id="path91" + class="ColorScheme-Text" + /> +</svg> diff --git a/rsc/icons/breeze-light/media-playback-pause.svg b/rsc/icons/breeze-light/media-playback-pause.svg new file mode 100644 index 0000000000000000000000000000000000000000..ece183885e0811d28bcd87beee0639f4fb63f155 --- /dev/null +++ b/rsc/icons/breeze-light/media-playback-pause.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="m 6 6 0 20 8 0 0 -20 z m 12 0 0 20 8 0 0 -20 z" + id="path8" + class="ColorScheme-Text" + /> +</svg> diff --git a/rsc/icons/breeze-light/media-playback-start.svg b/rsc/icons/breeze-light/media-playback-start.svg new file mode 100644 index 0000000000000000000000000000000000000000..28f7c9b3d05dd7ba88dfaf8ae5bfe456500835a5 --- /dev/null +++ b/rsc/icons/breeze-light/media-playback-start.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="m 6 6 0 20 20 -10 z" + id="path105" + class="ColorScheme-Text" + /> +</svg> diff --git a/rsc/icons/breeze-light/media-playback-stop.svg b/rsc/icons/breeze-light/media-playback-stop.svg new file mode 100644 index 0000000000000000000000000000000000000000..2aa152a2d0211d2e9492639b8ac469f657481075 --- /dev/null +++ b/rsc/icons/breeze-light/media-playback-stop.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="m 6 6 0 20 20 0 0 -20 z" + id="path91" + class="ColorScheme-Text" + /> +</svg> diff --git a/src/Lib/Utils.hh b/src/Lib/Utils.hh index 7e75014ad3010e694ee5170830f9ba958a8014b9..ac421be7dd692377f98eb204e1485634734c7efe 100644 --- a/src/Lib/Utils.hh +++ b/src/Lib/Utils.hh @@ -1,6 +1,10 @@ #ifndef VIVY_UTILS_H #define VIVY_UTILS_H +#ifndef __cplusplus +#error "This is a C++ header" +#endif + #include <QString> #include <QFileInfo> #include <QStringList> @@ -9,6 +13,10 @@ #include <QJsonDocument> #include <QtGlobal> #include <type_traits> +#include <chrono> + +// Use chrono instead of std::chrono... +namespace chrono = std::chrono; // Prety define for OpenMP's parallel for loop with indentation not fucked up // by clang-format. diff --git a/src/UI/DocumentViews/AudioVisualizer.cc b/src/UI/DocumentViews/AudioVisualizer.cc index 60a6279bde639662a6351f6211bd39504db4d3ac..b7813c425d8f0c39d88a0d10f96b5cece7dd2c62 100644 --- a/src/UI/DocumentViews/AudioVisualizer.cc +++ b/src/UI/DocumentViews/AudioVisualizer.cc @@ -37,9 +37,7 @@ AudioVisualizer::AudioVisualizer(AudioContext::StreamPtr stream, QWidget *parent RDFTContextPtr ctx(av_rdft_init((static_cast<int>(log2(static_cast<int>(height)))), DFT_R2C), rdftContextDeleter); - if (!pixels) { - throw std::runtime_error("out of memory"); - } else if (!(chunkData && ctx)) { + if (!(chunkData && ctx)) { delete[] pixels; throw std::runtime_error("out of memory"); } diff --git a/src/UI/DocumentViews/MpvContainer.cc b/src/UI/DocumentViews/MpvContainer.cc new file mode 100644 index 0000000000000000000000000000000000000000..375b9f949828eced98bcf7ae71cf75f5215951c8 --- /dev/null +++ b/src/UI/DocumentViews/MpvContainer.cc @@ -0,0 +1,326 @@ +#include "MpvContainer.hh" + +#include <mpv/client.h> + +using namespace Vivy; +using namespace std::string_literals; + +void +MpvContainer::mpvEventWakeUpCB(void *user) noexcept +{ + MpvContainer *container = reinterpret_cast<MpvContainer *>(user); + emit container->mpvEvent(); +} + +MpvContainer::MpvContainer(QWidget *parent) + : QWidget(parent) + , mpv(mpv_create()) +{ + if (mpv == nullptr) + throw std::runtime_error("Failed to create the MPV context"); + + setAttribute(Qt::WA_DontCreateNativeAncestors); + setAttribute(Qt::WA_NativeWindow); + + quint64 wid = winId(); + mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &wid); + mpv_set_option_string(mpv, "idle", "yes"); + mpv_set_option_string(mpv, "loop-file", "inf"); + mpv_set_option_string(mpv, "no-config", "yes"); + mpv_set_option_string(mpv, "sid", "no"); + mpv_set_option_string(mpv, "input-default-bindings", "no"); + mpv_set_option_string(mpv, "input-vo-keyboard", "no"); + mpv_request_log_messages(mpv, "info"); + mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG); + mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE); + + connect(this, &MpvContainer::mpvEvent, this, &MpvContainer::onMpvEvent, Qt::QueuedConnection); + mpv_set_wakeup_callback(mpv, &MpvContainer::mpvEventWakeUpCB, this); + + if (int rc = mpv_initialize(mpv); rc < 0) { + printMpvError(rc); + throw std::runtime_error("Failed to initialize the mpv context"); + } +} + +void +MpvContainer::registerMpvTimeCallback(std::function<void(double)> callback) noexcept +{ + if (mpvTimeCallback) + qWarning() << "Override a previous mpv callback!"; + mpvTimeCallback = callback; +} + +void +MpvContainer::registerMpvDurationCallback(std::function<void(double)> callback) noexcept +{ + if (mpvDurationCallback) + qWarning() << "Override a previous mpv callback!"; + mpvDurationCallback = callback; +} + +void +MpvContainer::closeMpv() noexcept +{ + if (mpv) { + qDebug() << "Closing the MPV context"; + asyncCommand(AsyncCmdType::None, { "quit", nullptr }); + registerMpvTimeCallback(nullptr); + registerMpvDurationCallback(nullptr); + mpv_handle *tmp_mpv = mpv; + mpv = nullptr; // Stop all other callbacks here + mpv_terminate_destroy(tmp_mpv); + } +} + +MpvContainer::~MpvContainer() noexcept +{ + closeMpv(); +} + +void +MpvContainer::handleMpvEvent(mpv_event *event) noexcept +{ + // Declare here variables that can be used in the switch-case statements + qint64 w, h; + double time; + QString msgText; + union { + mpv_event_log_message *msg; + mpv_event_property *prop; + }; + + auto checkProp = [](mpv_event_property *prop, const std::string &str, + int format) noexcept -> bool { + return (prop->name == str) && (prop->format == format); + }; + + switch (event->event_id) { + case MPV_EVENT_SHUTDOWN: + closeMpv(); + break; + + case MPV_EVENT_VIDEO_RECONFIG: + // TODO: Those are sync calls, prefer async calls + if (mpv_get_property(mpv, "dwidth", MPV_FORMAT_INT64, &w) >= 0 && + mpv_get_property(mpv, "dheight", MPV_FORMAT_INT64, &h) >= 0 && (w > 0 && h > 0)) { + qDebug() << "Reconfigure video to" << w << "x" << h; + } + break; + + case MPV_EVENT_LOG_MESSAGE: + msg = reinterpret_cast<mpv_event_log_message *>(event->data); + msgText = msg->text; + msgText.replace('\n', ""); + qDebug().nospace().noquote() + << "MPV - MSG [" << msg->prefix << "] " << msg->level << ": " << msgText; + break; + + case MPV_EVENT_PROPERTY_CHANGE: + prop = reinterpret_cast<mpv_event_property *>(event->data); + if (checkProp(prop, "time-pos"s, MPV_FORMAT_DOUBLE) && mpvTimeCallback) { + time = *reinterpret_cast<double *>(prop->data); + mpvTimeCallback(time); + } + + else if (checkProp(prop, "duration"s, MPV_FORMAT_DOUBLE) && mpvDurationCallback) { + time = *reinterpret_cast<double *>(prop->data); + mpvDurationCallback(time); + } + + else if (checkProp(prop, "pause"s, MPV_FORMAT_FLAG)) { + isPlaybackPaused = *reinterpret_cast<bool *>(prop->data); + emit mpvPlaybackToggled(!isPlaybackPaused); + qDebug() << "MPV -> set to" << (isPlaybackPaused ? "pause" : "play"); + } + + break; + + case MPV_EVENT_PAUSE: + isPlaybackPaused = true; + emit mpvPlaybackToggled(!isPlaybackPaused); + qDebug() << "MPV -> set to pause"; + break; + + case MPV_EVENT_UNPAUSE: + isPlaybackPaused = false; + emit mpvPlaybackToggled(!isPlaybackPaused); + qDebug() << "MPV -> set to play"; + break; + + case MPV_EVENT_START_FILE: + qDebug() << "MPV: Begin of file"; + break; + + case MPV_EVENT_END_FILE: + qDebug() << "MPV: Reached end of file!"; + break; + + case MPV_EVENT_COMMAND_REPLY: + qDebug() << "Got return of" << event->reply_userdata; + handleMpvEventCommandReply(static_cast<AsyncCmdType>(event->reply_userdata)); + break; + + // Explicitly ignored + case MPV_EVENT_NONE: + case MPV_EVENT_GET_PROPERTY_REPLY: + case MPV_EVENT_SET_PROPERTY_REPLY: + case MPV_EVENT_FILE_LOADED: + case MPV_EVENT_TRACKS_CHANGED: + case MPV_EVENT_TRACK_SWITCHED: + case MPV_EVENT_IDLE: + case MPV_EVENT_TICK: + case MPV_EVENT_SCRIPT_INPUT_DISPATCH: + case MPV_EVENT_CLIENT_MESSAGE: + case MPV_EVENT_AUDIO_RECONFIG: + case MPV_EVENT_METADATA_UPDATE: + case MPV_EVENT_SEEK: + case MPV_EVENT_PLAYBACK_RESTART: + case MPV_EVENT_CHAPTER_CHANGE: + case MPV_EVENT_QUEUE_OVERFLOW: + case MPV_EVENT_HOOK: + break; + } +} + +int +MpvContainer::getAssSid() const noexcept +{ + bool conversionOk = false; + const char *result = mpv_get_property_string(mpv, "sid"); + const int ret = QString(result).toInt(&conversionOk); + return (result == nullptr || !conversionOk) ? -1 : ret; +} + +void +MpvContainer::handleMpvEventCommandReply(const AsyncCmdType type) noexcept +{ + switch (type) { + case AsyncCmdType::None: + break; + + case AsyncCmdType::LoadAssFile: + case AsyncCmdType::ReloadAss: + sid = getAssSid(); + qDebug() << "Load ASS file with id:" << sid; + break; + + case AsyncCmdType::UnloadAss: + sid = getAssSid(); + qDebug().nospace() << "Unload Ass, rc = " << sid; + if (sid != -1) + unloadAssFile(); + break; + + case AsyncCmdType::LoadFile: + sid = getAssSid(); + qDebug() << "MPV - CMD: File loaded by mpv, sid =" << sid; + isPlaybackPaused = false; + mpvPause(); + unloadAssFile(); + break; + + case AsyncCmdType::SeekTime: + qDebug() << "MPV - CMD: Seeked playback"; + break; + + case AsyncCmdType::TogglePlayback: + qDebug() << "MPV - CMD: Playback was toggled"; + break; + } +} + +void +MpvContainer::onMpvEvent() noexcept +{ + while (mpv) { + mpv_event *event = mpv_wait_event(mpv, 0); + if (event == nullptr || event->event_id == MPV_EVENT_NONE) + break; + handleMpvEvent(event); + } + + if (mpv == nullptr) { + qDebug() << "MPV was closed while in the event loop!"; + } +} + +void +MpvContainer::loadFile(const QString &filename) noexcept +{ + if (filename.isEmpty()) + return; + + const QByteArray cFileName = filename.toUtf8(); + asyncCommand(AsyncCmdType::LoadFile, { "loadfile", cFileName.data(), nullptr }); +} + +void +MpvContainer::loadAssFile(const QString &ass) noexcept +{ + if (ass.isEmpty()) + return; + + const QByteArray cFileName = ass.toUtf8(); + asyncCommand(AsyncCmdType::LoadAssFile, { "sub-add", cFileName.data(), nullptr }); +} + +void +MpvContainer::reloadAssFile() noexcept +{ + asyncCommand(AsyncCmdType::ReloadAss, { "sub-reload", nullptr }); +} + +void +MpvContainer::printMpvError(int rc) const noexcept +{ + if (rc == MPV_ERROR_SUCCESS) + return; + + qCritical() << "MPV error:" << mpv_error_string(rc); +} + +void +MpvContainer::mpvPlay() noexcept +{ + if (isPlaybackPaused) + mpvTogglePlayback(); +} + +void +MpvContainer::mpvPause() noexcept +{ + if (!isPlaybackPaused) + mpvTogglePlayback(); +} + +void +MpvContainer::mpvTogglePlayback() noexcept +{ + qDebug() << "MPV: Toggling the playback"; + asyncCommand(AsyncCmdType::TogglePlayback, { "cycle", "pause", "up", nullptr }); +} + +void +MpvContainer::asyncCommand(const AsyncCmdType cmd, + std::initializer_list<const char *> args) noexcept +{ + // NOTE: const_cast here, we have faith in MPV to not change the value of + // the temporary pointers here. Should be OK anyway because the `args` init + // list is a temporary and should be discarded afer the method call. + printMpvError(mpv_command_async(mpv, cmd, const_cast<const char **>(std::data(args)))); +} + +void +MpvContainer::unloadAssFile() noexcept +{ + asyncCommand(AsyncCmdType::UnloadAss, { "sub-remove", nullptr }); +} + +void +MpvContainer::seekInFile(const chrono::seconds time) noexcept +{ + QByteArray seconds = QString::number(time.count()).toUtf8(); + asyncCommand(AsyncCmdType::SeekTime, { "seek", seconds.data(), "absolute", nullptr }); +} diff --git a/src/UI/DocumentViews/MpvContainer.hh b/src/UI/DocumentViews/MpvContainer.hh new file mode 100644 index 0000000000000000000000000000000000000000..8296cac69656bfd2b8d14b3eb2bac70722744884 --- /dev/null +++ b/src/UI/DocumentViews/MpvContainer.hh @@ -0,0 +1,79 @@ +#pragma once + +#ifndef __cplusplus +#error "This is a C++ header" +#endif + +#include "../../Lib/Utils.hh" + +#include <functional> +#include <initializer_list> + +extern "C" { +struct mpv_handle; +struct mpv_event; +} + +namespace Vivy +{ +class MpvContainer final : public QWidget { + Q_OBJECT + VIVY_UNMOVABLE_OBJECT(MpvContainer) + + enum AsyncCmdType : uint64_t { + None, + LoadFile, + LoadAssFile, + ReloadAss, + UnloadAss, + SeekTime, + TogglePlayback, + }; + +private: + bool isPlaybackPaused{ true }; + mpv_handle *mpv{ nullptr }; + qint64 sid{ -1 }; + std::function<void(double)> mpvTimeCallback{ nullptr }; + std::function<void(double)> mpvDurationCallback{ nullptr }; + +public: + explicit MpvContainer(QWidget *parent); + ~MpvContainer() noexcept override; + + void loadFile(const QString &) noexcept; + void loadAssFile(const QString &) noexcept; + void reloadAssFile() noexcept; + + void seekInFile(const chrono::seconds time) noexcept; + + // Register a callback for time change, don't use Qt's signals for that. + // Here the function will likely be moved if necessary (I hope...). + void registerMpvTimeCallback(std::function<void(double)>) noexcept; + void registerMpvDurationCallback(std::function<void(double)>) noexcept; + +private: + void handleMpvEvent(mpv_event *) noexcept; + void closeMpv() noexcept; + void printMpvError(int) const noexcept; + void asyncCommand(const AsyncCmdType, std::initializer_list<const char *>) noexcept; + void unloadAssFile() noexcept; + void handleMpvEventCommandReply(const AsyncCmdType) noexcept; + int getAssSid() const noexcept; + + // Must be static to be passed as a function ptr + static void mpvEventWakeUpCB(void *) noexcept; + +public slots: + void mpvPlay() noexcept; + void mpvPause() noexcept; + void mpvTogglePlayback() noexcept; + +private slots: + void onMpvEvent() noexcept; + +signals: + void mpvEvent(); + void mpvPlaybackToggled(bool isPlaying); +}; +} diff --git a/src/UI/DocumentViews/MpvControls.cc b/src/UI/DocumentViews/MpvControls.cc new file mode 100644 index 0000000000000000000000000000000000000000..5c490dca4cbb1b12228f4b81e61d7e31b14849cf --- /dev/null +++ b/src/UI/DocumentViews/MpvControls.cc @@ -0,0 +1,72 @@ +#include "MpvControls.hh" +#include "MpvContainer.hh" + +#include <QSlider> +#include <QHBoxLayout> +#include <QPushButton> + +using namespace Vivy; + +MpvControls::MpvControls(MpvContainer *passedContainer, QWidget *parent) noexcept + : QWidget(parent) + , mpv(passedContainer) +{ + auto *progressBar = new QSlider(this); + auto *togglePlaybackButton = new QPushButton(playIcon, "", this); // Be default MPV is paused + + progressBar->setTracking(false); + progressBar->setOrientation(Qt::Horizontal); + + mpv->registerMpvDurationCallback([progressBar, this](double time) noexcept -> void { + timePosition = chrono::seconds::zero(); + timeDuration = chrono::seconds(static_cast<long>(time)); + progressBar->setMaximum(static_cast<int>(timeDuration.count())); + progressBar->setValue(0); + }); + + mpv->registerMpvTimeCallback([progressBar, this](double time) noexcept -> void { + if (!progressBar->isSliderDown()) { + // The user is not pressing the slider + timePosition = chrono::seconds(static_cast<long>(time)); + progressBar->setValue(static_cast<int>(timePosition.count())); + } + }); + + connect(progressBar, &QAbstractSlider::sliderMoved, this, + [this](int value) noexcept -> void { askedSliderPosition = value; }); + + connect(progressBar, &QAbstractSlider::actionTriggered, this, + [progressBar](int action) noexcept -> void { + switch (static_cast<QAbstractSlider::SliderAction>(action)) { + case QAbstractSlider::SliderSingleStepAdd: + case QAbstractSlider::SliderSingleStepSub: + case QAbstractSlider::SliderPageStepAdd: + case QAbstractSlider::SliderPageStepSub: + progressBar->setSliderPosition(progressBar->value()); + break; + + case QAbstractSlider::SliderAction::SliderMove: + case QAbstractSlider::SliderNoAction: + case QAbstractSlider::SliderToMinimum: + case QAbstractSlider::SliderToMaximum: + break; + } + }); + + connect(progressBar, &QAbstractSlider::sliderReleased, this, [this]() noexcept -> void { + qDebug() << "Slider set to" << askedSliderPosition << "max was" << timeDuration.count(); + timePosition = chrono::seconds(askedSliderPosition); + mpv->seekInFile(timePosition); + }); + + connect(togglePlaybackButton, &QAbstractButton::clicked, mpv, &MpvContainer::mpvTogglePlayback); + connect(mpv, &MpvContainer::mpvPlaybackToggled, this, + [this, togglePlaybackButton](bool isPlay) noexcept -> void { + togglePlaybackButton->setIcon(isPlay ? pauseIcon : playIcon); + }); + + auto *centralLayout = new QHBoxLayout(this); + centralLayout->addWidget(togglePlaybackButton); + centralLayout->addWidget(progressBar, 1); + setLayout(centralLayout); +} diff --git a/src/UI/DocumentViews/MpvControls.hh b/src/UI/DocumentViews/MpvControls.hh new file mode 100644 index 0000000000000000000000000000000000000000..db00d5900753f8411c2c2f2896aab0d8458708dd --- /dev/null +++ b/src/UI/DocumentViews/MpvControls.hh @@ -0,0 +1,31 @@ +#pragma once + +#ifndef __cplusplus +#error "This is a C++ header" +#endif + +#include "../../Lib/Utils.hh" +#include "../../VivyApplication.hh" +#include <QIcon> + +namespace Vivy +{ +class MpvContainer; + +class MpvControls final : public QWidget { + Q_OBJECT + VIVY_UNMOVABLE_OBJECT(MpvControls) + +private: + MpvContainer *mpv{ nullptr }; + chrono::seconds timeDuration; + chrono::seconds timePosition; + int askedSliderPosition{ 0 }; + + const QIcon playIcon{ VIVY_ICON_PLAY }; + const QIcon pauseIcon{ VIVY_ICON_PAUSE }; + +public: + explicit MpvControls(MpvContainer *mpv, QWidget *parent) noexcept; +}; +} diff --git a/src/UI/DocumentViews/VideoView.cc b/src/UI/DocumentViews/VideoView.cc new file mode 100644 index 0000000000000000000000000000000000000000..bc73922e714944733f788251d866f26441bba4e0 --- /dev/null +++ b/src/UI/DocumentViews/VideoView.cc @@ -0,0 +1,31 @@ +#include "VideoView.hh" +#include "MpvContainer.hh" +#include "MpvControls.hh" + +#include <QVBoxLayout> + +using namespace Vivy; + +VideoView::VideoView(QWidget *parent) noexcept + : QWidget(parent) + , mpv(new MpvContainer(this)) +{ + auto *centralLayout = new QVBoxLayout(this); + + centralLayout->addWidget(mpv, 1); + centralLayout->addWidget(new MpvControls(mpv, this)); + + setLayout(centralLayout); +} + +void +VideoView::loadFile(const QString &filename) noexcept +{ + mpv->loadFile(filename); +} + +void +VideoView::forceStopPlayback() noexcept +{ + mpv->mpvPause(); +} diff --git a/src/UI/DocumentViews/VideoView.hh b/src/UI/DocumentViews/VideoView.hh new file mode 100644 index 0000000000000000000000000000000000000000..20a97b8d6c519cd28d0b1d96e670161303582a9c --- /dev/null +++ b/src/UI/DocumentViews/VideoView.hh @@ -0,0 +1,26 @@ +#pragma once + +#ifndef __cplusplus +#error "This is a C++ header" +#endif + +#include "../../Lib/Utils.hh" + +namespace Vivy +{ +class MpvContainer; + +class VideoView final : public QWidget { + Q_OBJECT + VIVY_UNMOVABLE_OBJECT(VideoView) + + MpvContainer *mpv{ nullptr }; + +public: + explicit VideoView(QWidget *parent) noexcept; + +public slots: + void loadFile(const QString &) noexcept; + void forceStopPlayback() noexcept; +}; +} diff --git a/src/UI/MainWindow.hh b/src/UI/MainWindow.hh index 17386985d089c75caaef8345ebd1984cd29f241b..05d2292d0255aabb1c079f3e21cf3f6bcc636482 100644 --- a/src/UI/MainWindow.hh +++ b/src/UI/MainWindow.hh @@ -23,7 +23,7 @@ class MainWindow final : public QMainWindow { QTabWidget *documents{ nullptr }; QMenu *viewMenu{ nullptr }; - QMutex aboutWindowMutex{ QMutex::NonRecursive }; + QMutex aboutWindowMutex; AboutWindow *aboutWindow{ nullptr }; public: diff --git a/src/UI/PropertyModel.cc b/src/UI/PropertyModel.cc index 5dd71eaaca81359c570771bf09db4c45356602b1..d80604ca44e28994fd8797e6afeba3ed5a1e59f4 100644 --- a/src/UI/PropertyModel.cc +++ b/src/UI/PropertyModel.cc @@ -261,22 +261,7 @@ PropertyModel::columnCount(const QModelIndex & /* parent */) const noexcept Qt::ItemFlags PropertyModel::flags(const QModelIndex &index) const noexcept { - // Handle the case where the model is not editable - if (!editableState) { - return /* Qt::ItemIsSelectable | */ QAbstractItemModel::flags(index); - } - - // Here the model is editable - const int col = index.column(); - const Item *item = static_cast<Item *>(index.internalPointer()); - const bool isArray = QJsonValue::Array == item->getType(); - const bool isObject = QJsonValue::Object == item->getType(); - - if ((col == 1) && !(isArray || isObject)) - return Qt::ItemIsSelectable | Qt::ItemIsEditable | QAbstractItemModel::flags(index); - - else - return QAbstractItemModel::flags(index); + return QAbstractItemModel::flags(index); } // Get the json stored inside the model diff --git a/src/UI/PropertyModel.hh b/src/UI/PropertyModel.hh index c79f0aa831199e4b8726d6adcf37d6c76c734992..d22ab6eccb28c96e34f3e00419b66a519d12f3b8 100644 --- a/src/UI/PropertyModel.hh +++ b/src/UI/PropertyModel.hh @@ -71,10 +71,7 @@ public: root->setKey(object.getElementName()); } - ~PropertyModel() noexcept = default; - void loadJson(const QJsonDocument &json); - void setEditable(const bool); QVariant data(const QModelIndex &, int role) const noexcept override; bool setData(const QModelIndex &, const QVariant &v, int r = Qt::EditRole) noexcept override; @@ -96,7 +93,6 @@ private: QJsonValue generateJson(Item *) const noexcept; std::unique_ptr<Item> root{ nullptr }; QStringList headers{}; - bool editableState{ false }; }; } diff --git a/src/UI/Utils.cc b/src/UI/Utils.cc new file mode 100644 index 0000000000000000000000000000000000000000..95f9269d01b74fab6579a1a68ae28878cea2c62d --- /dev/null +++ b/src/UI/Utils.cc @@ -0,0 +1,12 @@ +#include "Utils.hh" +#include <QWidget> + +using namespace Vivy; + +void +Utils::setTransparentBackgroundForWidget(QWidget *const w) noexcept +{ + w->setAttribute(Qt::WA_NoSystemBackground); + w->setAttribute(Qt::WA_TranslucentBackground); + w->setAttribute(Qt::WA_TransparentForMouseEvents); +} diff --git a/src/UI/Utils.hh b/src/UI/Utils.hh new file mode 100644 index 0000000000000000000000000000000000000000..34b57e2a119bcae629be2901e2332a26d7341836 --- /dev/null +++ b/src/UI/Utils.hh @@ -0,0 +1,12 @@ +#pragma once + +#ifndef __cplusplus +#error "This is a C++ header" +#endif + +class QWidget; + +namespace Vivy::Utils +{ +void setTransparentBackgroundForWidget(QWidget *const) noexcept; +} diff --git a/src/UI/VivyDocumentView.cc b/src/UI/VivyDocumentView.cc index 64213d65f39dd15677361015d4c99e6c8d5e7b50..a82725074114229ae36477d7aa9ef5a4a5f84f35 100644 --- a/src/UI/VivyDocumentView.cc +++ b/src/UI/VivyDocumentView.cc @@ -1,7 +1,9 @@ #include "VivyDocumentView.hh" #include "PropertyModel.hh" +#include "Utils.hh" #include "DocumentViews/AudioVisualizer.hh" #include "DocumentViews/AssLinesView.hh" +#include "DocumentViews/AssLinesModel.hh" #include "../VivyApplication.hh" #include "../Lib/Document/VivyDocument.hh" @@ -9,6 +11,8 @@ #include <QTreeView> #include <QVBoxLayout> #include <QTableView> +#include <QWidget> +#include <QDockWidget> using namespace Vivy; @@ -75,22 +79,43 @@ VivyDocumentView::getDocumentTabToolTip() const noexcept void VivyDocumentView::loadVideoView() noexcept { + if (document->checkDocumentCapabilities(VivyDocument::Capabilities::VideoAble)) { + if (!videoView) { + videoView = new QDockWidget("Video View", this); + videoView->setAllowedAreas(Qt::AllDockWidgetAreas); + videoView->setFeatures(QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable | + QDockWidget::DockWidgetClosable); + addDockWidget(Qt::BottomDockWidgetArea, videoView, Qt::Vertical); + videoView->setTitleBarWidget(new QWidget(this)); + Utils::setTransparentBackgroundForWidget(videoView->titleBarWidget()); + } + + // Kubat: because the dock is "closable", when closed the widget itself + // is not deleted, it will be hidden and the content will be deleted. + // TODO: Check if it works on more platforms. + videoView->setWidget(new VideoView(videoView)); + qobject_cast<VideoView *>(videoView->widget()) + ->loadFile(document->getVideoSubDocument()->getFilePath()); + } } void VivyDocumentView::loadAssView() noexcept { - if (assLines) - delDockWidget(&assLines); - if (document->checkDocumentCapabilities(VivyDocument::Capabilities::AssAble)) { + if (!assLines) { + assLines = new QDockWidget("ASS Lines", this); + assLines->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable); + assLines->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea | + Qt::BottomDockWidgetArea); + addDockWidget(Qt::BottomDockWidgetArea, assLines, Qt::Vertical); + assLines->setTitleBarWidget(new QWidget(this)); + Utils::setTransparentBackgroundForWidget(assLines->titleBarWidget()); + } + assModel.reset(new AssLinesModel(document->getAssSubDocument()->getLines())); - assLines = new QDockWidget("ASS Lines", this); assLines->setWidget(new AssLinesView(assModel.get(), assLines)); - assLines->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable); - assLines->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea | - Qt::BottomDockWidgetArea); - addDockWidget(Qt::BottomDockWidgetArea, assLines, Qt::Vertical); } } @@ -108,23 +133,24 @@ VivyDocumentView::loadAudioView() noexcept return; } - if (visualizer) - delDockWidget(&visualizer); - visualizer = new QDockWidget("Visualizer", this); - - AudioVisualizer *visualizerInner = new AudioVisualizer(stream, visualizer); - if (visualizerInner == nullptr) { - qCritical() << "Failed to create visualizer for" << audioDocument->getFilePath(); - return; + if (!visualizer) { + visualizer = new QDockWidget("Visualizer", this); + + visualizer->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum); + visualizer->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::TopDockWidgetArea | + Qt::BottomDockWidgetArea); + visualizer->setFeatures(QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetClosable); + addDockWidget(Qt::LeftDockWidgetArea, visualizer, Qt::Horizontal); + visualizer->setTitleBarWidget(new QWidget(this)); + Utils::setTransparentBackgroundForWidget(visualizer->titleBarWidget()); } - visualizer->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum); + // Kubat: don't check, may throw an error but don't think we can + // recover from it. + AudioVisualizer *visualizerInner = new AudioVisualizer(stream, visualizer); visualizer->setWidget(visualizerInner); - visualizer->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::TopDockWidgetArea | - Qt::BottomDockWidgetArea); - visualizer->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable); visualizer->layout()->setAlignment(visualizerInner, Qt::AlignTop); - addDockWidget(Qt::LeftDockWidgetArea, visualizer, Qt::Horizontal); } else { @@ -139,7 +165,7 @@ VivyDocumentView::closeDocument() noexcept << document.use_count() << ")"; vivyApp->documentStore.closeDocument(document->getUuid()); - // The visualizer pointer should have been deleted by the + // Kubat: the visualizer pointer should have been deleted by the // deleteAllContent() call if it was created. deleteAllContent(); visualizer = nullptr; @@ -155,16 +181,19 @@ VivyDocumentView::getDocumentTabIcon() const noexcept void VivyDocumentView::openProperties() noexcept { - if (property) - delDockWidget(&property); - propertyModel.reset(new PropertyModel(*document.get())); - property = new QDockWidget("Properties", this); QTreeView *view = new QTreeView(property); view->setModel(propertyModel.get()); view->header()->setSectionResizeMode(QHeaderView::ResizeToContents); view->expandAll(); + + if (!property) { + property = new QDockWidget("Properties", this); + property->setAllowedAreas(Qt::AllDockWidgetAreas); + addDockWidget(Qt::RightDockWidgetArea, property, Qt::Vertical); + property->setTitleBarWidget(new QWidget(this)); + Utils::setTransparentBackgroundForWidget(property->titleBarWidget()); + } + property->setWidget(view); - property->setAllowedAreas(Qt::AllDockWidgetAreas); - addDockWidget(Qt::RightDockWidgetArea, property, Qt::Vertical); } diff --git a/src/UI/VivyDocumentView.hh b/src/UI/VivyDocumentView.hh index 56c54a493826fe4bbe4447c65c5b75a05a34b11a..4cdcc8efc8d9cd6d1a68be9c0627b68f410f82ab 100644 --- a/src/UI/VivyDocumentView.hh +++ b/src/UI/VivyDocumentView.hh @@ -5,17 +5,18 @@ #error "This is a C++ header" #endif -#include "../Lib/Document/VivyDocument.hh" -#include "DocumentViews/AudioVisualizer.hh" -#include "DocumentViews/AssLinesModel.hh" #include "AbstractDocumentView.hh" -#include "PropertyModel.hh" -#include <QWidget> -#include <QDockWidget> +class QDockWidget; +class QWidget; namespace Vivy { +class PropertyModel; +class AssLinesModel; +class VivyDocument; +class VideoView; + class VivyDocumentView final : public AbstractDocumentView { Q_OBJECT VIVY_UNMOVABLE_OBJECT(VivyDocumentView) @@ -45,6 +46,7 @@ private: QDockWidget *visualizer{ nullptr }; QDockWidget *property{ nullptr }; QDockWidget *assLines{ nullptr }; + QDockWidget *videoView{ nullptr }; }; } diff --git a/src/VivyApplication.cc b/src/VivyApplication.cc index 86ef22ab58de0ca87bc7265a8e2b662aa4b19677..70c84396b458884121d7e91e561e57687ba8868f 100644 --- a/src/VivyApplication.cc +++ b/src/VivyApplication.cc @@ -4,6 +4,7 @@ #include <QtGlobal> #include <QIcon> #include <QFontDatabase> +#include <locale> using namespace Vivy; @@ -34,6 +35,9 @@ VivyApplication::setTheme(Theme theme) noexcept int VivyApplication::exec() noexcept { + // For MPV + std::setlocale(LC_NUMERIC, "C"); + // Add fonts fontIdMonospace = QFontDatabase::addApplicationFont(":/fonts/FiraCode-Regular.ttf"); fontIdMonospaceBold = QFontDatabase::addApplicationFont(":/fonts/FiraCode-Bold.ttf"); diff --git a/src/VivyApplication.hh b/src/VivyApplication.hh index 4b0b82b988e3b6bbf696b3fe0adbf098d95568fd..a13719cda18f38253f0181147f9559f3ff776f34 100644 --- a/src/VivyApplication.hh +++ b/src/VivyApplication.hh @@ -16,6 +16,9 @@ #define VIVY_ICON_ABOUT ":icons/dark/help-about.svg" #define VIVY_ICON_FILE ":icons/dark/text-x-generic.svg" #define VIVY_ICON_FOLDER ":icons/dark/folder.svg" +#define VIVY_ICON_PLAY ":icons/dark/media-play.svg" +#define VIVY_ICON_PAUSE ":icons/dark/media-pause.svg" +#define VIVY_ICON_STOP ":icons/dark/media-stop.svg" #include <QApplication> #include <QPixmap>