diff --git a/CMakeLists.txt b/CMakeLists.txt index cced477b8003fda0c1099bfa8423001ccc6a7662..3159722c543f7c58d882ba6f9f19b489847e9fb8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,10 +10,6 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -# C++20, at least we try -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - # Pthread ftw set(THREADS_PREFER_PTHREAD_FLAG ON) @@ -59,20 +55,38 @@ target_link_libraries(Vivy PRIVATE lua) # Headers related things target_include_directories(Vivy PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/inc) -target_precompile_headers(Vivy - PRIVATE - # Private Vivy headers - ${Vivy_INC} - - # Qt headers - <QString> - <QList> - <QMap> - <QWidget> - <QIcon> - - # STL headers - <memory> +target_precompile_headers(Vivy PRIVATE + # Private Vivy headers + ${Vivy_INC} + + # STL headers + <memory> + <vector> + + # Qt headers + <QString> + <QList> + <QVector> + <QMap> + <QWidget> + <QIcon> +) + +# Set Vivy's needed C++ features +target_compile_features(Vivy PRIVATE + cxx_std_20 + cxx_auto_type + cxx_deleted_functions + cxx_explicit_conversions + cxx_final + cxx_inline_namespaces + cxx_lambdas + cxx_noexcept + cxx_nonstatic_member_init + cxx_nullptr + cxx_override + cxx_range_for + cxx_strong_enums ) # More options and warnings @@ -90,7 +104,9 @@ if (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") -Wno-unused-private-field # Skip the unused private fields for now -fopenmp # We do OpenMP here ) - target_link_libraries(Vivy PRIVATE -fopenmp) + target_link_libraries(Vivy PRIVATE + -fopenmp + ) elseif (${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") target_compile_options(Vivy PRIVATE -fopenmp) target_link_libraries(Vivy PRIVATE -fopenmp) diff --git a/src/Lib/Ass/Ass.hh b/src/Lib/Ass/Ass.hh index 59eb171f92087ab6592d1e4d17737a95366733ab..acb14ff2f5c568a0db4277cb455a278526f210d0 100644 --- a/src/Lib/Ass/Ass.hh +++ b/src/Lib/Ass/Ass.hh @@ -1,21 +1,12 @@ #ifndef VIVY_ASS_ASS_H #define VIVY_ASS_ASS_H -#include "Char.hh" #include "Line.hh" #include "Syl.hh" #include "Style.hh" +#include "AssPrivate.hh" #include "AssFactory.hh" #include "StyleProperties.hh" #include <memory.h> -namespace Vivy::Ass -{ -using StylePtr = AssFactory::StylePtr; -using LinePtr = AssFactory::LinePtr; - -using StyleWeakPtr = AssFactory::StyleWeakPtr; -using LineWeakPtr = AssFactory::LineWeakPtr; -} - #endif // VIVY_ASS_ASS_H diff --git a/src/Lib/Ass/AssFactory.cc b/src/Lib/Ass/AssFactory.cc index 91a61f1c4a311b6feb78c1663c248417bd20f1e0..06bd5db2f487e370ad6d301cd98e96b59fbfae75 100644 --- a/src/Lib/Ass/AssFactory.cc +++ b/src/Lib/Ass/AssFactory.cc @@ -1,5 +1,6 @@ #include "AssFactory.hh" +#include <algorithm> #include <stdexcept> using namespace Vivy::Ass; @@ -10,17 +11,19 @@ AssFactory::initFromStorage() noexcept QTextStream in(&diskStorage); QString currentSection{}; quint64 lineIndex = 0; + QStringList stylesContent{}; + QStringList eventsContent{}; while (!in.atEnd()) { const QString line = in.readLine().trimmed(); - /* Dectect comment */ + // Dectect comment if (line.startsWith(";") || line.isEmpty()) { lineIndex++; continue; } - /* Dectect sections */ + // Dectect sections else if (line.startsWith("[") && line.endsWith("]")) { currentSection = line.mid(1, line.size() - 2); qDebug() << "Parsing section" << currentSection; @@ -30,31 +33,45 @@ AssFactory::initFromStorage() noexcept } } - /* Other lines */ + // Other lines else if (!currentSection.isEmpty()) { const int separatorIndex = line.indexOf(": "); const int baseValueIndex = separatorIndex + 2; - if (separatorIndex < 0) { + + // Easy way to see if the line was invalid + if (separatorIndex < 0) qWarning() << "Invalid line #" << lineIndex << ":" << line; - } else { - assContent[currentSection].insert(line.mid(0, separatorIndex), - line.mid(baseValueIndex)); + + // Script's info + else if (currentSection == sectionScriptInfo) { + assInfo.insert(line.mid(0, separatorIndex), line.mid(baseValueIndex)); qDebug() << "Got line #" << lineIndex << ":" << line; } + + // Skip the headers and the comment lines + else if ((currentSection == sectionStyles) && line.startsWith("Style: ")) { + stylesContent.append(line); + } else if ((currentSection == sectionEvents) && line.startsWith("Dialogue: ")) { + eventsContent.append(line); + } else if ((currentSection == sectionEvents) && line.startsWith("Comment: ")) { + eventsContent.append(line); + } } lineIndex++; } - /* Construct the styles and the lines */ - for (const auto &styleLine : assContent[sectionStyles]) - assStyles.push_back(std::make_shared<Style>(styleLine.toString())); - for (const auto &assLine : assContent[sectionEvents]) - assLines.push_back(std::make_shared<Line>(assLine.toString())); + // Construct the styles and the lines + try { + for (const auto &styleLine : stylesContent) + assStyles.push_back(std::make_shared<Style>(styleLine)); + for (const auto &assLine : eventsContent) + assLines.push_back(std::make_shared<Line>(this, assLine)); + } catch (const std::runtime_error &e) { + qCritical() << "Failed to create ASS style or events with error:" << e.what(); + } - /* Get back some memory */ - assContent.remove(sectionStyles); - assContent.remove(sectionEvents); + qDebug() << "Got" << assLines.size() << "ASS dialog lines"; return true; } @@ -62,27 +79,31 @@ AssFactory::initFromStorage() noexcept bool AssFactory::checkValidity() const noexcept { - if (assContent.isEmpty()) { + if (assInfo.isEmpty()) { + qCritical() << "Empty info section"; return false; } - const SectionContent &scriptInfo = assContent[sectionScriptInfo]; - - /* Check for fields that must be integers */ - bool ok = false; - SectionContent::const_iterator it = scriptInfo.begin(); - const SectionContent::const_iterator end = scriptInfo.end(); + // Check for fields that must be integers + SectionContent::const_iterator it = assInfo.begin(); + const SectionContent::const_iterator end = assInfo.end(); while (it != end) { - if (intTypeFields.contains(it.key()) && (it.value().toInt(&ok), !ok)) + bool ok = false; + if (intTypeFields.contains(it.key()) && (it.value().toInt(&ok), !ok)) { + qCritical() << it.key() << "is not an integer:" << it.value(); return false; + } ++it; } - /* Check for fixed values fields */ + // Check for fixed values fields for (const auto &fixedValues : checkedValues) { if (const auto &first = fixedValues.first; - scriptInfo.contains(first) && scriptInfo[first] != fixedValues.second) + assInfo.contains(first) && assInfo[first] != fixedValues.second) { + qCritical() << "Invalid" << first << "as it should be equal to" << fixedValues.second + << "but was" << assInfo[first]; return false; + } } return true; @@ -102,7 +123,7 @@ AssFactory::AssFactory(const QString &fileName) } void -AssFactory::getStyles(QVector<AssFactory::StylePtr> &ret) const noexcept +AssFactory::getStyles(QVector<StylePtr> &ret) const noexcept { ret.clear(); for (const auto &style : assStyles) @@ -110,7 +131,7 @@ AssFactory::getStyles(QVector<AssFactory::StylePtr> &ret) const noexcept } void -AssFactory::getLines(QVector<AssFactory::LinePtr> &ret) const noexcept +AssFactory::getLines(QVector<LinePtr> &ret) const noexcept { ret.clear(); for (const auto &line : assLines) @@ -120,5 +141,30 @@ AssFactory::getLines(QVector<AssFactory::LinePtr> &ret) const noexcept AssFactory::SectionContent AssFactory::getInfoSection() const noexcept { - return assContent[sectionScriptInfo]; + return assInfo; +} + +StyleWeakPtr +AssFactory::getStyle(const QString &name) const noexcept +{ + auto findByName = [&name](const StylePtr &style) noexcept -> bool { + return style->getElementName() == name; + }; + + auto styleIt = std::find_if(std::begin(assStyles), std::end(assStyles), findByName); + if (styleIt != std::end(assStyles)) + return StyleWeakPtr(*styleIt); + + // Will be unable to lock + return StyleWeakPtr(spareNullStylePtr); +} + +bool +AssFactory::hasStyle(const QString &name) const noexcept +{ + for (const auto &stylePtr : assStyles) { + if (stylePtr->getElementName() == name) + return true; + } + return false; } diff --git a/src/Lib/Ass/AssFactory.hh b/src/Lib/Ass/AssFactory.hh index 48f849b01cc6ef60afbaa5b4d18627b59d24f439..f21fddb15a6b33a60917b0eb0371b4cd33592599 100644 --- a/src/Lib/Ass/AssFactory.hh +++ b/src/Lib/Ass/AssFactory.hh @@ -4,16 +4,16 @@ #include "../Utils.hh" #include "Style.hh" #include "Line.hh" +#include "AssPrivate.hh" -#include <memory.h> +#include <memory> #include <QFile> #include <QMap> #include <QVariant> #include <QString> -/* An ASS file is basically an INI file, but some keys are present multiple - * times (V4+ Styles/Style, Events/Dialogue, Events/Comment). - */ +// An ASS file is basically an INI file, but some keys are present multiple +// times (V4+ Styles/Style, Events/Dialogue, Events/Comment). namespace Vivy::Ass { @@ -27,29 +27,27 @@ public: Events = 3, }; - using LinePtr = std::shared_ptr<Line>; - using LineWeakPtr = std::weak_ptr<Line>; - using StylePtr = std::shared_ptr<Style>; - using StyleWeakPtr = std::weak_ptr<Style>; using SectionContent = QMap<QString, QVariant>; private: QFile diskStorage; - QMap<QString, SectionContent> assContent{}; + SectionContent assInfo{}; QVector<LinePtr> assLines{}; QVector<StylePtr> assStyles{}; + StylePtr spareNullStylePtr{ nullptr }; + static inline const QString sectionScriptInfo = "Script Info"; static inline const QString sectionStyles = "V4+ Styles"; static inline const QString sectionEvents = "Events"; static inline const QStringList validSections = { sectionEvents, sectionScriptInfo, sectionStyles }; - static inline const QStringList intTypeFields = { "PlayResX", "PlayResY", "WrapStyle" }; - static inline const QList<QPair<QString, QString>> checkedValues = { + static inline const QStringList intTypeFields{ "PlayResX", "PlayResY", "WrapStyle" }; + static inline const QVector<QPair<QString, QString>> checkedValues{ { "ScriptType", "v4.00+" }, { "WrapStyle", "0" }, - { "YCbCr Matrix", "TV.601" }, + // { "YCbCr Matrix", "TV.601" }, { "ScaledBorderAndShadow", "yes" } }; @@ -60,6 +58,9 @@ public: explicit AssFactory(const QString &); ~AssFactory() noexcept = default; + bool hasStyle(const QString &) const noexcept; + StyleWeakPtr getStyle(const QString &) const noexcept; + SectionContent getInfoSection() const noexcept; void getStyles(QVector<StylePtr> &) const noexcept; void getLines(QVector<LinePtr> &) const noexcept; diff --git a/src/Lib/Ass/AssPrivate.hh b/src/Lib/Ass/AssPrivate.hh new file mode 100644 index 0000000000000000000000000000000000000000..71f9c7ee1b3fcfddb33e7961a01d7a96ab39bc1d --- /dev/null +++ b/src/Lib/Ass/AssPrivate.hh @@ -0,0 +1,15 @@ +#pragma once + +#include <memory> + +namespace Vivy::Ass +{ +class Style; +class Line; + +using StylePtr = std::shared_ptr<Style>; +using LinePtr = std::shared_ptr<Line>; + +using StyleWeakPtr = std::weak_ptr<Style>; +using LineWeakPtr = std::weak_ptr<Line>; +} diff --git a/src/Lib/Ass/Char.cc b/src/Lib/Ass/Char.cc deleted file mode 100644 index e768c6bbf4e3552c3aecdd0538871a1d6c586d96..0000000000000000000000000000000000000000 --- a/src/Lib/Ass/Char.cc +++ /dev/null @@ -1,10 +0,0 @@ -#include "Char.hh" -#include "Syl.hh" - -using namespace Vivy::Ass; - -Char::Char(Syl *const syl, const QChar /*unused*/) - : parentLine(syl->parentLine) - , parentSyl(syl) -{ -} diff --git a/src/Lib/Ass/Char.hh b/src/Lib/Ass/Char.hh deleted file mode 100644 index a632e341ef1c2b5dcbfaddad4fd537af7c4b2b91..0000000000000000000000000000000000000000 --- a/src/Lib/Ass/Char.hh +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef VIVY_ASS_CHAR_H -#define VIVY_ASS_CHAR_H - -#include <QChar> -#include <QtGlobal> -#include "StyleProperties.hh" - -namespace Vivy::Ass -{ -class Line; -class Syl; - -class Char { -private: - QChar content{}; - StyleProperties styleProperties; - quint64 dur{ 0 }; - -public: - Line *const parentLine; - Syl *const parentSyl; - -public: - // Copy constructor - explicit Char(const Char &) = default; - explicit Char(Syl *const, const QChar); - - Char &operator=(const Char &) = delete; - ~Char() noexcept = default; -}; -} - -#endif // VIVY_ASS_CHAR_H diff --git a/src/Lib/Ass/Line.cc b/src/Lib/Ass/Line.cc index 1b7b8c6a21de97616d506a86d87aff64a9832b0a..7fabdbace9fdd01450f41f49c1f295b91ff5e88b 100644 --- a/src/Lib/Ass/Line.cc +++ b/src/Lib/Ass/Line.cc @@ -1,9 +1,93 @@ #include "Line.hh" +#include "AssFactory.hh" + +#include <QRegularExpression> using namespace Vivy::Ass; -Line::Line(const QString & /*unused*/) +Line::Line(AssFactory *const factory, const QString &lineString) + : assFactory(factory) +{ + enum LineIndex : int { + Layer = 0, // int + Start = 1, // time + End = 2, // time + Style = 3, // text + Name = 4, // text + MarginL = 5, // int + MarginR = 6, // int + MarginV = 7, // int + Effect = 8, // text + Text = 9, // text + PastLastCode + + // NOTE: time is of the form: `h:mm:ss.cc` + }; + + static const QString lineHeader = "Dialogue: "; + isComment = !lineString.startsWith(lineHeader); + + const QString lineContent = lineString.mid(lineString.indexOf(": ") + 2 /* strlen ": " */); + QStringList contentList = lineContent.split(",", Qt::KeepEmptyParts, Qt::CaseInsensitive); + + if (contentList.size() < LineIndex::PastLastCode) + throw std::runtime_error(("invalid number of items " + QString::number(contentList.size()) + + " instead of something superiror or equal to " + + QString::number(PastLastCode) + " for line: " + lineContent) + .toStdString()); + + layer = Utils::decodeLineToInteger(contentList[LineIndex::Layer], "Layer is not an integer"); + effect = contentList[LineIndex::Effect]; + nameOrActor = contentList[LineIndex::Name]; + start = Utils::Time::fromString(contentList[LineIndex::Start]).toUInt(); + end = Utils::Time::fromString(contentList[LineIndex::End]).toUInt(); + + styleProperties.marginL = + Utils::decodeLineToInteger(contentList[LineIndex::MarginL], "MarginL is not an integer"); + styleProperties.marginR = + Utils::decodeLineToInteger(contentList[LineIndex::MarginR], "MarginR is not an integer"); + styleProperties.marginV = + Utils::decodeLineToInteger(contentList[LineIndex::MarginV], "MarginV is not an integer"); + + const QString style = contentList[LineIndex::Style]; + if (!assFactory->hasStyle(style)) + throw std::runtime_error(("Invalid or not declared style name: " + style).toStdString()); + lineStyle = assFactory->getStyle(style); + + // Pop all but the text, it may contains `,` characters + for (int i = 0; i < LineIndex::Text; ++i) + contentList.removeFirst(); + ___contentAsText = contentList.join(""); + initSylFromString(___contentAsText); +} + +void +Line::initSylFromString(const QString &line) noexcept { + // Matches syllabes like: `{\toto}{\alpha&HFF}content` + QRegularExpression re("((?:{[^}]*})+[^{]*)"); + if (!re.isValid()) + qFatal("The regex '%s' is not valid...", re.pattern().toStdString().c_str()); + + bool once = false; + try { + QRegularExpressionMatchIterator it = re.globalMatch(line); + + while (it.hasNext()) { + QRegularExpressionMatch match = it.next(); + content.append(Syl(this, match.captured(1))); + once |= true; + } + } catch (const std::runtime_error &e) { + qCritical() << "Failed to init syllabes with line:" << line; + qCritical() << "Fallback to all line is one syllabe"; + once = false; + } + + if (!once) { + content.clear(); + content.append(Syl(this, line, Syl::ConstructMode::Raw)); + } } void @@ -21,3 +105,33 @@ Line::setEnd(quint64 s) noexcept if (start > s) start = s; } + +quint64 +Line::getDuration() const noexcept +{ + return end - start; +} + +StyleProperties +Line::getStyleProperties() const noexcept +{ + return styleProperties; +} + +StyleWeakPtr +Line::getStyle() const noexcept +{ + return lineStyle; +} + +const QVector<Syl> & +Line::getContent() const noexcept +{ + return content; +} + +bool +Line::getIsComment() const noexcept +{ + return isComment; +} diff --git a/src/Lib/Ass/Line.hh b/src/Lib/Ass/Line.hh index 7f7bb0511588cc7bdb03c8c7684722ff2a9e536d..7463dad6692230fe9627ff83f21d4f32290f1862 100644 --- a/src/Lib/Ass/Line.hh +++ b/src/Lib/Ass/Line.hh @@ -5,27 +5,47 @@ #include <QtGlobal> #include "Syl.hh" #include "StyleProperties.hh" +#include "Style.hh" namespace Vivy::Ass { -class Line { +class AssFactory; + +class Line final { private: quint64 start{ 0 }; quint64 end{ 0 }; + int layer{ 0 }; + bool isComment{ true }; QVector<Syl> content{}; - StyleProperties styleProperties; + StyleProperties styleProperties{}; + QString effect{}; + QString nameOrActor{}; + StyleWeakPtr lineStyle; + + QString ___contentAsText; + + AssFactory *const assFactory; public: - explicit Line() = default; explicit Line(const Line &) = default; - explicit Line(const QString &); + explicit Line(AssFactory *const, const QString &); Line &operator=(const Line &) = delete; ~Line() noexcept = default; void setStart(quint64 s) noexcept; void setEnd(quint64 s) noexcept; + quint64 getDuration() const noexcept; + bool getIsComment() const noexcept; + + StyleProperties getStyleProperties() const noexcept; + StyleWeakPtr getStyle() const noexcept; + const QVector<Syl> &getContent() const noexcept; + +private: + void initSylFromString(const QString &) noexcept; }; } diff --git a/src/Lib/Ass/Style.cc b/src/Lib/Ass/Style.cc index 58d247a43074935f369c7c469fdb524255d052a7..80d5eb3183194fbd54272dff550a3aa12f8648f0 100644 --- a/src/Lib/Ass/Style.cc +++ b/src/Lib/Ass/Style.cc @@ -1,12 +1,14 @@ #include "Style.hh" #include <stdexcept> +#include <QJsonDocument> +#include <QJsonObject> using namespace Vivy::Ass; Style::Style(const QString &styleString) { - /* Some usefull defines only needed in that function */ + // Some usefull defines only needed in that function enum StyleIndex : int { Name = 0, @@ -34,111 +36,104 @@ Style::Style(const QString &styleString) Shadow = 17, Alignement = 18, - MarginL = 18, - MarginR = 19, - MarginV = 20, + MarginL = 19, + MarginR = 20, + MarginV = 21, - Encoding = 21, + Encoding = 22, PastLastCode }; - static const QString lineHeader = "Style: "; - static const QString headerSeparator = ": "; - static constexpr int styleContentListSize = StyleIndex::PastLastCode; + static const QString lineHeader = "Style: "; - /* Check line header and content items number */ + // Check line header and content items number if (!styleString.startsWith(lineHeader)) - throw std::runtime_error("invalid style line header"); + throw std::runtime_error(("invalid style line header: " + styleString).toStdString()); - const QString lineContent = styleString.section(headerSeparator, 2, 2); + const QString lineContent = styleString.mid(styleString.indexOf(": ") + 2 /* strlen ": " */); const QStringList content = lineContent.split(",", Qt::KeepEmptyParts, Qt::CaseInsensitive); - if (lineContent.size() != styleContentListSize) - throw std::runtime_error("invalid line content: invalid number of intems"); + if (content.size() != StyleIndex::PastLastCode) + throw std::runtime_error(("invalid number of items " + QString::number(content.size()) + + " instead of " + QString::number(PastLastCode) + + " for line: " + lineContent) + .toStdString()); - /* Unpack data from the line */ + // Unpack data from the line styleName = content[StyleIndex::Name]; fontName = content[StyleIndex::FontName]; - fontSize = decodeLineToInteger(content[StyleIndex::FontSize], "FontSize is not an integer"); + fontSize = + Utils::decodeLineToInteger(content[StyleIndex::FontSize], "FontSize is not an integer"); primaryColor = Color::fromString(content[StyleIndex::ColorPrimary]); secondaryColor = Color::fromString(content[StyleIndex::ColorSecondary]); outlineColor = Color::fromString(content[StyleIndex::ColorOutline]); backColor = Color::fromString(content[StyleIndex::ColorBack]); - bold = decodeLineToBoolean(content[StyleIndex::Bold], "Bold is not a boolean"); - italic = decodeLineToBoolean(content[StyleIndex::Italic], "Italic is not a boolean"); - underline = decodeLineToBoolean(content[StyleIndex::Underline], "Underline is not a boolean"); - strikeOut = decodeLineToBoolean(content[StyleIndex::StrikeOut], "StrikeOut is not a boolean"); - - scaleX = decodeLineToFloating(content[StyleIndex::ScaleX], "ScaleX is not a floating"); - scaleY = decodeLineToFloating(content[StyleIndex::ScaleY], "ScaleY is not a floating"); - spacing = decodeLineToFloating(content[StyleIndex::ScaleY], "Spacing is not a floating"); - angle = decodeLineToFloating(content[StyleIndex::Angle], "Angle is not a floating"); - borderStyle = - decodeLineToFloating(content[StyleIndex::BorderStyle], "BorderStyle is not a floating"); - outline = decodeLineToFloating(content[StyleIndex::Outline], "Outline is not a floating"); - shadow = decodeLineToFloating(content[StyleIndex::Shadow], "Shadow is not a floating"); - - alignment = decodeLineToInteger(content[StyleIndex::Shadow], "Alignement is not an integer"); + bold = Utils::decodeLineToBoolean(content[StyleIndex::Bold], "Bold is not a boolean"); + italic = Utils::decodeLineToBoolean(content[StyleIndex::Italic], "Italic is not a boolean"); + underline = + Utils::decodeLineToBoolean(content[StyleIndex::Underline], "Underline is not a boolean"); + strikeOut = + Utils::decodeLineToBoolean(content[StyleIndex::StrikeOut], "StrikeOut is not a boolean"); + + scaleX = Utils::decodeLineToFloating(content[StyleIndex::ScaleX], "ScaleX is not a floating"); + scaleY = Utils::decodeLineToFloating(content[StyleIndex::ScaleY], "ScaleY is not a floating"); + spacing = Utils::decodeLineToFloating(content[StyleIndex::ScaleY], "Spacing is not a floating"); + angle = Utils::decodeLineToFloating(content[StyleIndex::Angle], "Angle is not a floating"); + borderStyle = Utils::decodeLineToFloating(content[StyleIndex::BorderStyle], + "BorderStyle is not a floating"); + outline = + Utils::decodeLineToFloating(content[StyleIndex::Outline], "Outline is not a floating"); + shadow = Utils::decodeLineToFloating(content[StyleIndex::Shadow], "Shadow is not a floating"); + + alignment = + Utils::decodeLineToInteger(content[StyleIndex::Alignement], "Alignement is not an integer"); if (alignment < 1 || alignment > 9) throw std::runtime_error( - "invalid line content: Alignement must be between 1 and 9 included"); + ("invalid line content: Alignement must be between 1 and 9 included, it was " + + QString::number(alignment)) + .toStdString()); - marginL = decodeLineToInteger(content[StyleIndex::MarginL], "MarginL is not an integer"); - marginR = decodeLineToInteger(content[StyleIndex::MarginR], "MarginR is not an integer"); - marginV = decodeLineToInteger(content[StyleIndex::MarginV], "MarginV is not an integer"); - encoding = decodeLineToInteger(content[StyleIndex::Encoding], "Encoding is not an integer"); + marginL = Utils::decodeLineToInteger(content[StyleIndex::MarginL], "MarginL is not an integer"); + marginR = Utils::decodeLineToInteger(content[StyleIndex::MarginR], "MarginR is not an integer"); + marginV = Utils::decodeLineToInteger(content[StyleIndex::MarginV], "MarginV is not an integer"); + encoding = + Utils::decodeLineToInteger(content[StyleIndex::Encoding], "Encoding is not an integer"); if (encoding != 1) qWarning() << "Encoding is not '1' in the ASS Style"; } -bool -Style::decodeLineToBoolean(const QString &item, const QString &error) +QString +Color::toString(const QColor &c) noexcept { - return decodeLineToInteger(item, error) >= 1; -} - -int -Style::decodeLineToInteger(const QString &item, const QString &error) -{ - bool conversion = false; - int ret = item.toInt(&conversion); - if (!conversion) - throw std::runtime_error(("invalid line content: " + error).toStdString()); - return ret; -} - -float -Style::decodeLineToFloating(const QString &item, const QString &error) -{ - bool conversion = false; - float ret = item.toFloat(&conversion); - if (!conversion) - throw std::runtime_error(("invalid line content: " + error).toStdString()); - return ret; + return "&H" + QString::number(c.blue(), 16).toUpper() + + QString::number(c.green(), 16).toUpper() + QString::number(c.red(), 16).toUpper(); } QColor Color::fromString(const QString &colorString) noexcept { - /* Ignore the '&H' at the begeing of the color if needed */ + // Ignore the '&H' at the begeing of the color if needed int startIndex = 0; - if (colorString.startsWith("&")) + if (colorString[startIndex] == '&') startIndex++; - if (colorString.startsWith("H") || colorString.startsWith("h")) + if (colorString[startIndex] == 'H' || colorString[startIndex] == 'h') startIndex++; - /* In some cases, the color can begin by a '#' */ - if (colorString.startsWith("#")) + // In some cases, the color can begin by a '#' + if (colorString[startIndex] == '#') startIndex++; - /* A valid string color is like 'AARRGGBB' for now (skipped 'aH') */ + // A valid string color is like 'AARRGGBB' for now (skipped 'aH') if (colorString.size() - startIndex != 8) { + qCritical() + << "Invalid color string: size - index_start =" << (colorString.size() - startIndex) + << "| string =" << colorString.mid(startIndex); qCritical() << "Found an invalid color string:" << colorString; return Color::defaultValue; } @@ -159,3 +154,54 @@ Color::fromString(const QString &colorString) noexcept return QColor(red, green, blue, alpha); } + +QString +Style::getElementName() const noexcept +{ + return styleName; +} + +QJsonDocument +Style::getProperties() const noexcept +{ + QJsonDocument ret; + + QJsonObject styleFont{ + { "Font name", fontName }, { "Font size", fontSize }, { "Bold", bold }, + { "Italic", italic }, { "Underline", underline }, { "Strike Out", strikeOut }, + }; + + QJsonObject styleColors{ + { "Primary", Color::toString(primaryColor) }, + { "Secondary", Color::toString(secondaryColor) }, + { "Outline", Color::toString(outlineColor) }, + { "Back", Color::toString(backColor) }, + }; + + QJsonObject styleFontProperties{ + { "Scale X", static_cast<const double>(scaleX) }, + { "Scale Y", static_cast<const double>(scaleY) }, + { "Spacing", static_cast<const double>(spacing) }, + { "Angle", static_cast<const double>(angle) }, + { "Border Style", static_cast<const double>(borderStyle) }, + { "Outline", static_cast<const double>(outline) }, + { "Shadow", static_cast<const double>(shadow) }, + }; + + QJsonObject styleMargins{ + { "Left", marginL }, + { "Right", marginR }, + { "Vertical", marginV }, + }; + + QJsonObject object{ { "Name", styleName }, + { "Font", styleFont }, + { "Font properties", styleFontProperties }, + { "Colors", styleColors }, + { "Margin", styleMargins }, + { "Alignement", alignment }, + { "Encoding", encoding } }; + + ret.setObject(object); + return ret; +} diff --git a/src/Lib/Ass/Style.hh b/src/Lib/Ass/Style.hh index 21ca04d27a99099562cbfef7f923fc68d19c16e1..9b9bba686f6e61e10b8227a791c73ef288f6e2fc 100644 --- a/src/Lib/Ass/Style.hh +++ b/src/Lib/Ass/Style.hh @@ -5,12 +5,15 @@ #include <QVector> #include <QtGlobal> #include <QColor> +#include <QObject> +#include "AssPrivate.hh" namespace Vivy::Ass { namespace Color { QColor fromString(const QString &) noexcept; +QString toString(const QColor &) noexcept; static inline const QColor defaultValue = QColor(0, 0, 0, 0); }; @@ -41,10 +44,8 @@ public: Style &operator=(const Style &) = delete; ~Style() noexcept = default; -private: - static bool decodeLineToBoolean(const QString &item, const QString &error); - static int decodeLineToInteger(const QString &item, const QString &error); - static float decodeLineToFloating(const QString &item, const QString &error); + QString getElementName() const noexcept; + QJsonDocument getProperties() const noexcept; }; } diff --git a/src/Lib/Ass/StyleProperties.hh b/src/Lib/Ass/StyleProperties.hh index d6d5c722ff8871c5437ae17e3580b8e1bb85da1e..374d9e2afb3a42f4c5d195c33fafe5a1494f09c8 100644 --- a/src/Lib/Ass/StyleProperties.hh +++ b/src/Lib/Ass/StyleProperties.hh @@ -7,16 +7,15 @@ namespace Vivy::Ass { -/* Overrides some properties of the Style of an Ass::Line, Ass::Char, Ass::Syl - */ +// Overrides some properties of the Style of an Ass::Line, Ass::Char, Ass::Syl struct StyleProperties final { // Colors QColor primaryColor{ Color::defaultValue }, secondaryColor{ Color::defaultValue }, outlineColor{ Color::defaultValue }, backColor{ Color::defaultValue }; - QString fontName; // Can override the font's name - int fontSize{}; // The font size != the font scaling + QString fontName{}; // Can override the font's name + int fontSize{}; // The font size != the font scaling bool bold{ false }, italic{ false }, underline{ false }, strikeOut{ false }; diff --git a/src/Lib/Ass/Syl.cc b/src/Lib/Ass/Syl.cc index 5f1bb9612cadb8bd2cfa78a58439c0072e816784..bac0a4d0363e269ec1d984466a767881c53108a6 100644 --- a/src/Lib/Ass/Syl.cc +++ b/src/Lib/Ass/Syl.cc @@ -1,8 +1,42 @@ #include "Syl.hh" +#include "Line.hh" + +#include <QRegularExpression> using namespace Vivy::Ass; -Syl::Syl(Line *const line, const QString & /*unused*/) - : parentLine(line) +Syl::Syl(Line *const line, const QString &lineString, ConstructMode mode) noexcept + : content(lineString) + , styleProperties(line->getStyleProperties()) + , duration(line->getDuration()) + , parentLine(line) +{ + // Will override the `content`, but will be heavy anyway + if (mode == ConstructMode::ReadAssTags) { + const int textBegin = lineString.lastIndexOf('}') + 1; + content = (textBegin >= lineString.size()) ? "" : lineString.mid(textBegin); + duration = getDurationFromString(lineString); + } +} + +quint64 +Syl::getDurationFromString(const QString &line) noexcept +{ + QRegularExpression re("\\\\(?:k|K|ko|kf)(\\d+)"); + if (!re.isValid()) + qFatal("The regex '%s' is not valid...", re.pattern().toStdString().c_str()); + + quint64 duration = 0; + QRegularExpressionMatchIterator it = re.globalMatch(line); + + while (it.hasNext()) + duration += it.next().captured(1).toUInt(); + + return duration; +} + +QString +Syl::getContent() const noexcept { + return content; } diff --git a/src/Lib/Ass/Syl.hh b/src/Lib/Ass/Syl.hh index cf637f06b66240c89315344295868ded662822f5..79c7a9bc376c028ff9b283d7d12aca3ece0414c0 100644 --- a/src/Lib/Ass/Syl.hh +++ b/src/Lib/Ass/Syl.hh @@ -1,7 +1,6 @@ #ifndef VIVY_SYL_H #define VIVY_SYL_H -#include "Char.hh" #include "StyleProperties.hh" #include <QString> #include <QVector> @@ -11,21 +10,32 @@ namespace Vivy::Ass { class Line; -class Syl { +class Syl final { private: - QVector<Char> content; + QString content; StyleProperties styleProperties; - quint64 dur{ 0 }; + quint64 duration{ 0 }; public: Line *const parentLine; public: - explicit Syl(const Syl &) = default; - explicit Syl(Line *const, const QString &); + enum class ConstructMode { + Raw, // Don't read ASS tags + ReadAssTags, // Read ass tags + }; + + explicit Syl(const Syl &) noexcept = default; + explicit Syl(Line *const, const QString &, + ConstructMode mode = ConstructMode::ReadAssTags) noexcept; Syl &operator=(const Syl &) = delete; ~Syl() noexcept = default; + + QString getContent() const noexcept; + +private: + static quint64 getDurationFromString(const QString &) noexcept; }; } diff --git a/src/Lib/Audio.cc b/src/Lib/Audio.cc index ec20ea67aa8af5be2de9650007cabfa7976cf1e9..e0d7b4808b7be922ebbb34c5410cfd377716cd4c 100644 --- a/src/Lib/Audio.cc +++ b/src/Lib/Audio.cc @@ -34,7 +34,6 @@ AudioContext::AudioContext(const QString &path) AVCodecParameters *params = itFormat->codecpar; AVCodec *streamCodec = avcodec_find_decoder(params->codec_id); if (streamCodec && streamCodec->type == AVMEDIA_TYPE_AUDIO) { - audioStreamIndexes.push_back(i); audioStreams.insert(i, std::make_shared<Stream>(streamCodec, formatPtr, itFormat, i)); } } @@ -44,15 +43,12 @@ AudioContext::AudioContext(const QString &path) -1, // Let AV find one stream -1, // We don't want related streams nullptr, 0); + if (defaultStreamIndex < 0) { + qCritical() << "Could not find the best audio stream"; + } - qDebug() << "Opened audio context for" << path << "with duration" << formatPtr->duration; -} - -// Get the number of audio streams in the audio context -int -AudioContext::getStreamCount() const noexcept -{ - return audioStreamIndexes.size(); + qDebug() << "Opened audio context for" << path << "with duration" << formatPtr->duration + << "and default stream index" << defaultStreamIndex; } // Get a specific audio stream, try to lock the weak pointer. if the index was @@ -60,15 +56,14 @@ AudioContext::getStreamCount() const noexcept AudioContext::StreamWeakPtr AudioContext::getStream(int index) const noexcept { - if (index >= audioStreamIndexes.size()) + if (index < 0) return StreamWeakPtr{ spareNullSreamPtr }; - const uint streamIndex = audioStreamIndexes[index]; - const auto found = audioStreams.find(streamIndex); + uint unsignedIndex = static_cast<uint>(index); + const auto found = audioStreams.find(unsignedIndex); - if (found != audioStreams.end()) { + if (found != audioStreams.end()) return StreamWeakPtr{ *found }; - } return StreamWeakPtr{ spareNullSreamPtr }; } @@ -94,7 +89,6 @@ QJsonDocument AudioContext::getProperties() const noexcept { QJsonDocument ret; - QJsonObject self; QJsonArray streams; QFileInfo file(filePath); @@ -103,9 +97,7 @@ AudioContext::getProperties() const noexcept streams.append(audioStreamPtr->getProperties()); } - self.insert("Streams", streams); - self.insert("File path", filePath); - self.insert("Base name", file.baseName()); + QJsonObject self{ { "Streams", streams }, { "Base name", file.baseName() } }; ret.setObject(self); return ret; @@ -157,8 +149,11 @@ AudioContext::Stream::Stream(AVCodec *streamCodec, AVFormatContext *format, AVSt AudioContext::Stream::~Stream() noexcept { - if (dataPtr) + if (dataPtr) { + qDebug() << "Free data ptr"; free(dataPtr); + } + qDebug() << "Delete stream object"; } QJsonObject diff --git a/src/Lib/Audio.hh b/src/Lib/Audio.hh index 72b67d288ea24fe93cf160e9f244ee4b7f733a75..3a7ad817e1a6c65d76b36c7084b8098d1e6f63eb 100644 --- a/src/Lib/Audio.hh +++ b/src/Lib/Audio.hh @@ -127,7 +127,6 @@ public: // The stream non-owning view pointer using StreamWeakPtr = std::weak_ptr<Stream>; - int getStreamCount() const noexcept; StreamWeakPtr getStream(int) const noexcept; StreamWeakPtr getDefaultStream() const noexcept; @@ -145,7 +144,6 @@ private: AVFormatContextPtr format{ avformat_alloc_context(), avFormatContextDeleter }; QString filePath; // Usefull information - QVector<uint> audioStreamIndexes{}; // Index all the audio streams of the file QMap<uint, StreamPtr> audioStreams{}; // THe audio streams of the file int defaultStreamIndex{ -1 }; diff --git a/src/Lib/Document/AbstractDocument.hh b/src/Lib/Document/AbstractDocument.hh index e6295263f040572bc680b5c3a97f6244da07e11d..823a1a83a152f34ce73e4b96ceacdaccd9dc9b8c 100644 --- a/src/Lib/Document/AbstractDocument.hh +++ b/src/Lib/Document/AbstractDocument.hh @@ -5,9 +5,14 @@ #error "This is a C++ header" #endif +#include "../Utils.hh" + namespace Vivy { -class AbstractDocument { +class AbstractDocument : public QObject { + Q_OBJECT + VIVY_UNMOVABLE_OBJECT(AbstractDocument) + protected: AbstractDocument() = default; virtual ~AbstractDocument() noexcept = default; @@ -15,6 +20,9 @@ protected: public: virtual bool rename(const QString &) noexcept = 0; virtual QString getName() const noexcept = 0; + +signals: + void documentChanged(); }; } diff --git a/src/Lib/Document/CRTPSubDocument.cc b/src/Lib/Document/CRTPSubDocument.cc index d6ac414b607200268e9d6a2b7a9755fd6d59a6ce..ed788183a5f1acc939f1c28479d9cfeec9951e42 100644 --- a/src/Lib/Document/CRTPSubDocument.cc +++ b/src/Lib/Document/CRTPSubDocument.cc @@ -25,17 +25,11 @@ AudioSubDocument::getDefaultStream() const noexcept if (auto ptr = contextPtr->getDefaultStream().lock()) { return ptr; } else { + qCritical() << "Document deleted!"; return nullptr; } } -// Get the stream count, may be 0 -int -AudioSubDocument::getStreamCount() const noexcept -{ - return contextPtr->getStreamCount(); -} - // Get the stream asked for, nullptr if no stream or if the index is invalid AudioContext::StreamPtr AudioSubDocument::getStream(int index) const noexcept @@ -68,9 +62,10 @@ QJsonDocument AudioSubDocument::getProperties() const noexcept { QJsonDocument ret; - QJsonObject object; - const QJsonDocument contextDocument = contextPtr->getProperties(); - object.insert("Audio context", contextDocument.object()); + QJsonObject object{ + { "Audio context", contextPtr->getProperties().object() }, + { "File", filePath }, + }; ret.setObject(object); return ret; } @@ -108,17 +103,39 @@ AssSubDocument::initFromPath(const QString &path) factory.getStyles(styles); factory.getLines(lines); } + QString AssSubDocument::getElementName() const noexcept { return "AssSubDocument"; } +const QVector<Ass::LinePtr> & +AssSubDocument::getLines() const noexcept +{ + return lines; +} + +const QVector<Ass::StylePtr> & +AssSubDocument::getStyles() const noexcept +{ + return styles; +} + QJsonDocument AssSubDocument::getProperties() const noexcept { QJsonDocument ret; - QJsonObject object; + + QJsonObject styleObject; + for (const Ass::StylePtr &style : styles) { + styleObject.insert(style->getElementName(), style->getProperties().object()); + } + + QJsonObject object{ + { "Styles", styleObject }, + { "File", filePath }, + }; ret.setObject(object); return ret; } diff --git a/src/Lib/Document/CRTPSubDocument.hh b/src/Lib/Document/CRTPSubDocument.hh index f89b26f65e235f3c3b3fd72a22510c7afd7cebdd..715ec6bfbdd765cc8d7bffc1aaaf12dc083950c9 100644 --- a/src/Lib/Document/CRTPSubDocument.hh +++ b/src/Lib/Document/CRTPSubDocument.hh @@ -41,7 +41,7 @@ public: try { ret->initFromPath(path); // May throw } catch (const std::runtime_error &e) { - qDebug() << "Failed to init document from file" << path; + qDebug().nospace() << "Failed to init document from file " << path << ": " << e.what(); ret.reset(); } @@ -75,7 +75,6 @@ private: public: int getDefaultStreamIndex() const noexcept; AudioContext::StreamPtr getDefaultStream() const noexcept; - int getStreamCount() const noexcept; AudioContext::StreamPtr getStream(int index) const noexcept; QString getElementName() const noexcept; @@ -110,6 +109,9 @@ public: QString getElementName() const noexcept; QJsonDocument getProperties() const noexcept; + const QVector<Ass::LinePtr> &getLines() const noexcept; + const QVector<Ass::StylePtr> &getStyles() const noexcept; + private: QVector<Ass::StylePtr> styles; QVector<Ass::LinePtr> lines; diff --git a/src/Lib/Document/VivyDocument.cc b/src/Lib/Document/VivyDocument.cc index cc2f3aaeffc5847e4dd995be1dcb8233665a4964..ed8fbfc66a7cfeb545919b2f8ded803a823135d6 100644 --- a/src/Lib/Document/VivyDocument.cc +++ b/src/Lib/Document/VivyDocument.cc @@ -199,10 +199,8 @@ VivyDocument::setAudioSubDocument(const QString filename) noexcept } audioDocument = AudioSubDocument::fromFile(filename); - if (audioDocument) { - documentType |= Capabilities::AudioAble; - documentOptions &= ~Options::UntouchedByDefault; - } + if (audioDocument) + addDocumentType(AudioAble); } void @@ -218,10 +216,8 @@ VivyDocument::setVideoSubDocument(const QString filename) noexcept } videoDocument = VideoSubDocument::fromFile(filename); - if (videoDocument) { - documentType |= Capabilities::VideoAble; - documentOptions &= ~Options::UntouchedByDefault; - } + if (videoDocument) + addDocumentType(VideoAble); } void @@ -237,10 +233,16 @@ VivyDocument::setAssSubDocument(const QString filename) noexcept } assDocument = AssSubDocument::fromFile(filename); - if (assDocument) { - documentType |= Capabilities::AssAble; - documentOptions &= ~Options::UntouchedByDefault; - } + if (assDocument) + addDocumentType(AssAble); +} + +void +VivyDocument::addDocumentType(Capabilities newCap) noexcept +{ + documentType |= newCap; + documentOptions &= ~Options::UntouchedByDefault; + emit documentChanged(); } QString diff --git a/src/Lib/Document/VivyDocument.hh b/src/Lib/Document/VivyDocument.hh index 10a8aeb2a11ee319de000f4c20a0541a2e406405..9ecfbaa56c186cd73c677fb5ed0eb2e075a77495 100644 --- a/src/Lib/Document/VivyDocument.hh +++ b/src/Lib/Document/VivyDocument.hh @@ -18,6 +18,9 @@ namespace Vivy { class VivyDocument final : public AbstractDocument { + Q_OBJECT + VIVY_UNMOVABLE_OBJECT(VivyDocument) + public: enum Capabilities : quint64 { AudioAble = (1 << 1), @@ -63,6 +66,8 @@ private: static bool detectDocumentType(const QFileInfo &file, Capabilities *) noexcept; + void addDocumentType(Capabilities) noexcept; + public: // Create an empty document explicit VivyDocument(const QString &name, Options opt = NoOption); diff --git a/src/Lib/Utils.cc b/src/Lib/Utils.cc index b49fa9ee2ee3e2ce33226cba8797f5d028ca754a..2e1a1b5fa800e146cee8033a432fd7a68ec65a51 100644 --- a/src/Lib/Utils.cc +++ b/src/Lib/Utils.cc @@ -1,5 +1,6 @@ #include "Utils.hh" +#include <QRegExp> #include <QFileInfo> using namespace Vivy; @@ -24,3 +25,60 @@ Utils::detectDocumentType(const QFileInfo &file, DocumentType *type) return false; } + +bool +Utils::decodeLineToBoolean(const QString &item, const QString &error) +{ + return Utils::decodeLineToInteger(item, error) >= 1; +} + +int +Utils::decodeLineToInteger(const QString &item, const QString &error) +{ + bool conversion = false; + int ret = item.toInt(&conversion); + if (!conversion) + throw std::runtime_error(("invalid line content: " + error).toStdString()); + return ret; +} + +float +Utils::decodeLineToFloating(const QString &item, const QString &error) +{ + bool conversion = false; + float ret = item.toFloat(&conversion); + if (!conversion) + throw std::runtime_error(("invalid line content: " + error).toStdString()); + return ret; +} + +// The string must be of the form: `H:MM:SS.cs` +Utils::Time +Utils::Time::fromString(const QString &str) +{ + QRegExp re("^(\\d+):(\\d\\d):(\\d\\d)\\.(\\d\\d)$"); + + // Here the toUint is safe because the RegExp is OK + if (re.indexIn(str) != -1) + return { .hour = re.cap(1).toUInt(), + .minute = re.cap(2).toUInt(), + .second = re.cap(3).toUInt(), + .centisecond = re.cap(4).toUInt() }; + + else + throw std::runtime_error("The string is not of the format `H:MM:SS.cs`"); +} + +quint64 +Utils::Time::toUInt() const noexcept +{ + return ((hour * 3600) + (minute * 60) + second) * 100 + centisecond; +} + +// Get a string from a time +QString +Utils::Time::toString() const noexcept +{ + return QString::number(hour) + ":" + QString::number(minute) + ":" + QString::number(second) + + "." + QString::number(centisecond); +} diff --git a/src/Lib/Utils.hh b/src/Lib/Utils.hh index eeba9f84c41a63e6bf3eeb23d2db3b4d602f1a4d..ef38724c7c54862d236bb0fb7db4c36389f00483 100644 --- a/src/Lib/Utils.hh +++ b/src/Lib/Utils.hh @@ -84,7 +84,23 @@ toUnderlying(E e) noexcept return static_cast<std::underlying_type_t<E>>(e); } +// Structure used to represent the time as it is presented in an ASS file. +struct Time final { + quint64 hour; + quint64 minute; + quint64 second; + quint64 centisecond; + + static Time fromString(const QString &); + + QString toString() const noexcept; + quint64 toUInt() const noexcept; +}; + bool detectDocumentType(const QFileInfo &, DocumentType *); +bool decodeLineToBoolean(const QString &item, const QString &error); +int decodeLineToInteger(const QString &item, const QString &error); +float decodeLineToFloating(const QString &item, const QString &error); } namespace Vivy diff --git a/src/UI/AbstractDocumentView.hh b/src/UI/AbstractDocumentView.hh index 0ce9a6e03a4957ca3e9f8b5e3018bff4f3af9b87..e73814ba43fd51bd424095a6378e1d57265dd027 100644 --- a/src/UI/AbstractDocumentView.hh +++ b/src/UI/AbstractDocumentView.hh @@ -1,5 +1,4 @@ -#ifndef VIVY_DOCUMENT_VIEW_H -#define VIVY_DOCUMENT_VIEW_H +#pragma once #ifndef __cplusplus #error "This is a C++ header" @@ -56,5 +55,3 @@ private: const Type documentType; }; } - -#endif // VIVY_DOCUMENT_VIEW_H diff --git a/src/UI/DocumentViews/AssLinesModel.cc b/src/UI/DocumentViews/AssLinesModel.cc new file mode 100644 index 0000000000000000000000000000000000000000..99cbc42e322449684b824a3c7bedafeb09c92bc1 --- /dev/null +++ b/src/UI/DocumentViews/AssLinesModel.cc @@ -0,0 +1,155 @@ +#include "AssLinesModel.hh" + +using namespace Vivy; + +AssLinesModel::Item::Item(Ass::LineWeakPtr linePtr) noexcept + : line(linePtr) +{ +} + +quint64 +AssLinesModel::Item::getLineNumber() const noexcept +{ + return 0; +} + +QString +AssLinesModel::Item::getLineText() const noexcept +{ + QString ret; + if (auto ptr = line.lock()) { + for (const auto &syl : ptr->getContent()) { + ret.append(syl.getContent()); + ret.append("|"); + } + } + ret.remove(ret.size() - 1); + return ret; +} + +bool +AssLinesModel::Item::getIsComment() const noexcept +{ + if (auto ptr = line.lock()) + return ptr->getIsComment(); + return false; +} + +QString +AssLinesModel::Item::getLineStyle() const noexcept +{ + if (auto ptr = line.lock()) { + if (auto style = ptr->getStyle().lock()) { + return style->getElementName(); + } + } + return ""; +} + +AssLinesModel::AssLinesModel(const QVector<Ass::LinePtr> &lines) noexcept + : lineRealData(lines) +{ + childs.reserve(lines.count()); + for (const Ass::LinePtr &line : lines) + childs.append(new Item(Ass::LineWeakPtr{ line })); +} + +AssLinesModel::~AssLinesModel() noexcept +{ + qDeleteAll(childs); +} + +QVariant +AssLinesModel::data(const QModelIndex &index, int role) const noexcept +{ + if (!index.isValid() || index.column() >= Utils::toUnderlying(Item::Field::TotalFieldCount)) + return QVariant(); + + const Item *line = static_cast<const Item *>(index.internalPointer()); + const Item::Field column = static_cast<Item::Field>(index.column()); + + // Display role + if (Qt::DisplayRole == role) { + switch (column) { + case Item::Field::Text: + return line->getLineText(); + + // We want a warning when adding a field so don"t use "default" + case Item::Field::TotalFieldCount: + qFatal("Unreachable"); + } + } + + // Happily ignore the edit role for now + else if (Qt::EditRole == role) { + } + + return QVariant(); +} + +bool +AssLinesModel::setData(const QModelIndex &index, const QVariant & /* value */, int role) noexcept +{ + const int col = index.column(); + if (col >= Utils::toUnderlying(Item::Field::TotalFieldCount)) + return false; + + if (Qt::EditRole == role) { + emit dataChanged(index, index, { Qt::EditRole }); + return true; + } + + return false; +} + +QVariant +AssLinesModel::headerData(int section, Qt::Orientation oriantation, int role) const noexcept +{ + if (Qt::DisplayRole != role) + return QVariant(); + + else if (Qt::Horizontal == oriantation) + return headers.value(section); + + else if (Qt::Vertical == oriantation) + return section + 1; + + else + return QVariant(); +} + +QModelIndex +AssLinesModel::parent(const QModelIndex &) const noexcept +{ + return QModelIndex(); +} + +QModelIndex +AssLinesModel::index(int row, int column, const QModelIndex &parent) const noexcept +{ + if (!hasIndex(row, column, parent)) + return QModelIndex(); + else if (row >= childs.size()) + return QModelIndex(); + else + return createIndex(row, column, childs[row]); +} + +int +AssLinesModel::rowCount(const QModelIndex & /* parent */) const noexcept +{ + return childs.size(); +} + +int +AssLinesModel::columnCount(const QModelIndex & /* parent */) const noexcept +{ + return Utils::toUnderlying(Item::Field::TotalFieldCount); +} + +Qt::ItemFlags +AssLinesModel::flags(const QModelIndex &index) const noexcept +{ + [[maybe_unused]] const Item *item = static_cast<const Item *>(index.internalPointer()); + return Qt::ItemNeverHasChildren | Qt::ItemIsSelectable | QAbstractItemModel::flags(index); +} diff --git a/src/UI/DocumentViews/AssLinesModel.hh b/src/UI/DocumentViews/AssLinesModel.hh new file mode 100644 index 0000000000000000000000000000000000000000..2dfdbcd1c709a55ce9c02e902f628358109dc2ed --- /dev/null +++ b/src/UI/DocumentViews/AssLinesModel.hh @@ -0,0 +1,60 @@ +#pragma once + +#include "../../Lib/Utils.hh" +#include "../../Lib/Ass/Ass.hh" +#include <QAbstractItemModel> +#include <QStringList> + +namespace Vivy +{ +class AssLinesModel final : public QAbstractItemModel { + Q_OBJECT + VIVY_UNMOVABLE_OBJECT(AssLinesModel) + +private: + class Item final { + VIVY_UNMOVABLE_OBJECT(Item) + + public: + Item(Ass::LineWeakPtr linePtr) noexcept; + ~Item() noexcept = default; + + enum class Field : int { + Text, + + // Last, the count + TotalFieldCount, + }; + + bool getIsComment() const noexcept; + quint64 getLineNumber() const noexcept; + QString getLineText() const noexcept; + QString getLineStyle() const noexcept; + + private: + Ass::LineWeakPtr line; + }; + + static inline const QStringList headers{ "", "Style", "Text" }; + +public: + explicit AssLinesModel(const QVector<Ass::LinePtr> &) noexcept; + ~AssLinesModel() noexcept; + + QVariant data(const QModelIndex &, int role) const noexcept override; + bool setData(const QModelIndex &, const QVariant &v, int r = Qt::EditRole) noexcept override; + + QVariant headerData(int section, Qt::Orientation, int r) const noexcept override; + QModelIndex parent(const QModelIndex &) const noexcept override; + QModelIndex index(int, int, const QModelIndex &parent = QModelIndex()) const noexcept override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const noexcept override; + int columnCount(const QModelIndex &parent = QModelIndex()) const noexcept override; + + Qt::ItemFlags flags(const QModelIndex &) const noexcept override; + +private: + QVector<Item *> childs; + const QVector<Ass::LinePtr> &lineRealData; +}; +} diff --git a/src/UI/DocumentViews/AssLinesView.cc b/src/UI/DocumentViews/AssLinesView.cc new file mode 100644 index 0000000000000000000000000000000000000000..a29245be8ae0b58d1a78a0fb285f73df24bd0751 --- /dev/null +++ b/src/UI/DocumentViews/AssLinesView.cc @@ -0,0 +1,126 @@ +#include "AssLinesView.hh" +#include "../../VivyApplication.hh" +#include <QPaintEvent> +#include <QHeaderView> + +using namespace Vivy; + +AssLinesView::AssLinesView(QAbstractItemModel *model, QWidget *parent) noexcept + : QTableView(parent) + , delegate(new Delegate(this)) +{ + // Force the 9pt font size for now + { + QFont f = font(); + f.setPointSize(9); + setFont(f); + } + + setMouseTracking(true); + setItemDelegate(delegate); + connect(this, &AssLinesView::hoverIndexChanged, delegate, + &AssLinesView::Delegate::onItemEntered); + + horizontalHeader()->setHighlightSections(false); + horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + horizontalHeader()->setStretchLastSection(true); + horizontalHeader()->hide(); + verticalHeader()->setVisible(true); + setSelectionMode(QAbstractItemView::ExtendedSelection); + setSelectionBehavior(QAbstractItemView::SelectRows); + setShowGrid(false); + + // Also set the 9pt font size for the headers + { + QFont f = horizontalHeader()->font(); + f.setPointSize(9); + horizontalHeader()->setFont(f); + verticalHeader()->setFont(f); + } + + static const QString style = "QTableView::item:selected {" + " border-top:1px solid #1394B4;" + " border-bottom:1px solid #1394B4;" + " border-right:1px solid #1394B4;" + " border-left:1px solid #1394B4;" + " background-color: #002B36;" + "}" + "QTableView::item {" + " border: 1px solid #474747;" + "};" + "QTableView::item:last:selected {" + " border-top:1px solid #1394B4;" + " border-bottom:1px solid #1394B4;" + " border-right:1px solid #1394B4;" + " border-left:1px solid #1394B4;" + " background-color: #002B36;" + "}"; + setStyleSheet(style); + setModel(model); +} + +void +AssLinesView::paintEvent(QPaintEvent *e) noexcept +{ + QTableView::paintEvent(e); +} + +void +AssLinesView::mouseMoveEvent(QMouseEvent *event) noexcept +{ + QModelIndex index = indexAt(event->pos()); + emit hoverIndexChanged(index); +} + +AssLinesView::Delegate::Delegate(AssLinesView *view, QWidget *parent) noexcept + : QStyledItemDelegate(parent) + , tableView(view) +{ +} + +bool +AssLinesView::Delegate::isHoveredRow(const QStyleOptionViewItem &option, + const QModelIndex &index) const noexcept +{ + bool ret = false; + const int gridWidth = tableView->showGrid() ? 1 : 0; + + for (int i = 0; i < index.model()->columnCount(); ++i) { + QModelIndex idx = index.model()->index(index.row(), i); + if (const QAbstractItemView *view = + qobject_cast<const QAbstractItemView *>(option.widget)) { + QStyleOptionViewItem o; + o.initFrom(view); + o.rect = view->visualRect(idx).adjusted(0, 0, gridWidth, gridWidth); + if (o.rect.contains(view->viewport()->mapFromGlobal(QCursor::pos()))) { + ret = true; + } + } + } + + return ret; +} + +void +AssLinesView::Delegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const noexcept +{ + QStyleOptionViewItem opt = option; + if (index.row() == hoveredRow) { + opt.state |= QStyle::State_MouseOver; + } + QStyledItemDelegate::paint(painter, opt, index); +} + +void +AssLinesView::Delegate::onItemEntered(const QModelIndex &index) noexcept +{ + hoveredRow = index.row(); +} + +void +AssLinesView::Delegate::initStyleOption(QStyleOptionViewItem *option, + const QModelIndex &index) const noexcept +{ + QStyledItemDelegate::initStyleOption(option, index); +} diff --git a/src/UI/DocumentViews/AssLinesView.hh b/src/UI/DocumentViews/AssLinesView.hh new file mode 100644 index 0000000000000000000000000000000000000000..54c58c660067f55270af7fa92d3ede42074823ae --- /dev/null +++ b/src/UI/DocumentViews/AssLinesView.hh @@ -0,0 +1,51 @@ +#pragma once + +#include "../../Lib/Utils.hh" +#include <QAbstractItemModel> +#include <QTableView> +#include <QStyledItemDelegate> + +namespace Vivy +{ +class AssLinesView final : public QTableView { + Q_OBJECT + VIVY_UNMOVABLE_OBJECT(AssLinesView) + +private: + class Delegate : public QStyledItemDelegate { + VIVY_UNMOVABLE_OBJECT(Delegate) + + private: + bool isHoveredRow(const QStyleOptionViewItem &, const QModelIndex &) const noexcept; + + int hoveredRow{ 0 }; + AssLinesView *tableView{ nullptr }; + + public: + Delegate(AssLinesView *, QWidget *parent = nullptr) noexcept; + ~Delegate() noexcept = default; + + void initStyleOption(QStyleOptionViewItem *option, + const QModelIndex &index) const noexcept override; + + void paint(QPainter *, const QStyleOptionViewItem &, + const QModelIndex &) const noexcept override; + void onItemEntered(const QModelIndex &) noexcept; + }; + +private: + Delegate *delegate{ nullptr }; + +public: + AssLinesView(QAbstractItemModel *, QWidget *parent = nullptr) noexcept; + ~AssLinesView() noexcept = default; + + void paintEvent(QPaintEvent *) noexcept override; + +private: + void mouseMoveEvent(QMouseEvent *) noexcept override; + +signals: + void hoverIndexChanged(QModelIndex); +}; +} diff --git a/src/UI/PropertyModel.cc b/src/UI/PropertyModel.cc index b32144f0ed5a76a8dce19077f348a5527ad1ed59..47308fa476f236532eaa12078efce9ae338754ea 100644 --- a/src/UI/PropertyModel.cc +++ b/src/UI/PropertyModel.cc @@ -79,7 +79,7 @@ PropertyModel::Item::getType() const noexcept return type; } -const QList<PropertyModel::Item *> +const QVector<PropertyModel::Item *> PropertyModel::Item::getChilds() const noexcept { return childs; diff --git a/src/UI/PropertyModel.hh b/src/UI/PropertyModel.hh index c6cdca5eb16601a03c1a4d46c01c4ae8dba2d454..c79f0aa831199e4b8726d6adcf37d6c76c734992 100644 --- a/src/UI/PropertyModel.hh +++ b/src/UI/PropertyModel.hh @@ -37,7 +37,7 @@ private: void appendChild(Item *) noexcept; Item *getChild(int row) const noexcept; - const QList<Item *> getChilds() const noexcept; + const QVector<Item *> getChilds() const noexcept; Item *getParent() const noexcept; int getChildCount() const noexcept; int getRow() const noexcept; @@ -55,7 +55,7 @@ private: QString key{}; QString value{}; QJsonValue::Type type{ QJsonValue::Null }; - QList<Item *> childs{}; + QVector<Item *> childs{}; Item *parent{ nullptr }; }; @@ -77,8 +77,7 @@ public: void setEditable(const bool); QVariant data(const QModelIndex &, int role) const noexcept override; - bool setData(const QModelIndex &, const QVariant &value, - int role = Qt::EditRole) noexcept override; + bool setData(const QModelIndex &, const QVariant &v, int r = Qt::EditRole) noexcept override; QVariant headerData(int section, Qt::Orientation, int role) const noexcept override; QModelIndex parent(const QModelIndex &) const noexcept override; diff --git a/src/UI/VivyDocumentView.cc b/src/UI/VivyDocumentView.cc index 91362576fa42d62df995bace7ca902f11423f2bc..d8af3d93b9cb0e32374f1f788c95411748153106 100644 --- a/src/UI/VivyDocumentView.cc +++ b/src/UI/VivyDocumentView.cc @@ -1,12 +1,14 @@ #include "VivyDocumentView.hh" #include "PropertyModel.hh" #include "DocumentViews/AudioVisualizer.hh" +#include "DocumentViews/AssLinesView.hh" #include "../VivyApplication.hh" #include "../Lib/Document/VivyDocument.hh" #include <QHeaderView> #include <QTreeView> #include <QVBoxLayout> +#include <QTableView> using namespace Vivy; @@ -24,9 +26,13 @@ VivyDocumentView::VivyDocumentView(std::shared_ptr<VivyDocument> doc, QWidget *p setDockNestingEnabled(true); // Add some actions... - QAction *openProperties = new QAction("Open properties", this); - connect(openProperties, &QAction::triggered, this, &VivyDocumentView::openProperties); - viewsActions.append(openProperties); + QAction *openPropertiesAct = new QAction("Open properties", this); + connect(openPropertiesAct, &QAction::triggered, this, &VivyDocumentView::openProperties); + connect(document.get(), &AbstractDocument::documentChanged, this, [=, this]() noexcept -> void { + if (property) + openProperties(); + }); + viewsActions.append(openPropertiesAct); // The separator QAction *separator = new QAction(this); @@ -72,6 +78,18 @@ VivyDocumentView::loadVideoView() noexcept void VivyDocumentView::loadAssView() noexcept { + if (assLines) + delDockWidget(&assLines); + + if (document->checkDocumentCapabilities(VivyDocument::Capabilities::AssAble)) { + 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); + } } void diff --git a/src/UI/VivyDocumentView.hh b/src/UI/VivyDocumentView.hh index 493b75e1112f8d4e2e259ef3f173448a5b4aab5b..b30b27d14dcbc2290f6e0b4c082ab029607cede1 100644 --- a/src/UI/VivyDocumentView.hh +++ b/src/UI/VivyDocumentView.hh @@ -7,6 +7,7 @@ #include "../Lib/Document/VivyDocument.hh" #include "DocumentViews/AudioVisualizer.hh" +#include "DocumentViews/AssLinesModel.hh" #include "AbstractDocumentView.hh" #include "PropertyModel.hh" @@ -39,9 +40,11 @@ public slots: private: std::shared_ptr<VivyDocument> document; - std::unique_ptr<PropertyModel> propertyModel; + std::unique_ptr<PropertyModel> propertyModel{ nullptr }; + std::unique_ptr<AssLinesModel> assModel{ nullptr }; QDockWidget *visualizer{ nullptr }; QDockWidget *property{ nullptr }; + QDockWidget *assLines{ nullptr }; }; } diff --git a/src/VivyApplication.cc b/src/VivyApplication.cc index 5b23121370541f8427260fd27a3439c45f65a1bd..0b08c8017def22582ac61c7f35115599721d9835 100644 --- a/src/VivyApplication.cc +++ b/src/VivyApplication.cc @@ -11,7 +11,10 @@ VivyApplication::VivyApplication(int argc, char **argv) int VivyApplication::exec() noexcept { + // Show the main window MainWindow mainWindow; mainWindow.show(); + + // Main loop return QApplication::exec(); }