diff --git a/.clang-format b/.clang-format
index f24513acc97d596d6d78f3dbf9b8e62d579f41b4..57f14efdde6e9fa0a063289f44768397e65cce3f 100644
--- a/.clang-format
+++ b/.clang-format
@@ -63,6 +63,7 @@ ForEachMacros:
   - 'FOR_EVER_IF'
   - 'FOR_EVER_UNTIL'
   - 'FOR_EACH_FLAT_LIST_ITEM'
+  - 'parallel_for'
 
 IncludeCategories:
   - Regex: '.*'
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bc73970b37abf6bfa8ceca76ddf5616be52aa301..161c798e44b9effe7934150ca97fb1dda939f146 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,49 +1,36 @@
 cmake_minimum_required(VERSION 3.5)
 
+# Alpho Vivy, CXX only
 project(Vivy VERSION 0.1 LANGUAGES CXX)
-
-set(CMAKE_INCLUDE_CURRENT_DIR ON)
 cmake_policy(SET CMP0100 NEW) # Let cmake use moc and uic for .hh files
+cmake_policy(SET CMP0009 NEW) # Do not follow symlinks with GLOB_RECURSE
 
+# For Qt
 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)
 
-# Find all dependencies
-find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED)
+# Find Qt dependencies
+find_package(QT NAMES Qt6 Qt5      COMPONENTS Widgets REQUIRED)
 find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED)
-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)
 
-set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fopenmp")
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fopenmp")
+# 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)
 
 # Grab all files
-file(GLOB Vivy_SRC
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cc"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Document/*.cc"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Ass/*.cc"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Lib/*.cc"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/UI/*.cc"
-)
-file(GLOB Vivy_INC
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/*.hh"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Document/*.hh"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Ass/*.hh"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Lib/*.hh"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/UI/*.hh"
-)
-set(PROJECT_SOURCES
-    ${Vivy_SRC}
-    ${Vivy_INC}
-)
+file(GLOB_RECURSE Vivy_SRC CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cc")
+file(GLOB_RECURSE Vivy_INC CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.hh")
+set(PROJECT_SOURCES ${Vivy_SRC} ${Vivy_INC})
 
 # Add the Vivy executable
 if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
@@ -69,41 +56,36 @@ target_link_libraries(Vivy PRIVATE ${AVFORMAT_LIBRARY})
 # Headers related things
 target_include_directories(Vivy PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/inc)
 target_precompile_headers(Vivy PRIVATE
-    # ASS headers
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Ass/Ass.hh"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Ass/Line.hh"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Ass/Syl.hh"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Ass/Style.hh"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Ass/AssFactory.hh"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Ass/Char.hh"
-
-    # Libs
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Lib/Utils.hh"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Lib/Audio.hh"
+    # Private Vivy headers
+    ${Vivy_INC}
 
-    # Document headers
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Document/VivyDocument.hh"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Document/VivyDocumentStore.hh"
-    "${CMAKE_CURRENT_SOURCE_DIR}/src/Document/CRTPSubDocument.hh"
+    # Qt headers
+    <QString>
+    <QList>
+    <QMap>
+    <QWidget>
+    <QIcon>
 )
 
 # More options and warnings
 target_compile_options(Vivy PRIVATE
     -Wall -Wextra -Wshadow -pedantic
-    -Wcast-align -Wconversion -Wsign-conversion
+    -Wcast-align -Wconversion -Wsign-conversion -Wunused-variable
     -Wmisleading-indentation -Wnull-dereference -Wdouble-promotion
     -Wformat=2
-    $<$<COMPILE_LANGUAGE:CXX>:
-        -Woverloaded-virtual
-        -Wnon-virtual-dtor
-    >
+    -Woverloaded-virtual -Wnon-virtual-dtor
 )
 
+# Some compiler specific warnings and options
 if (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")
     target_compile_options(Vivy PRIVATE
-        -Wno-unused-private-field # Skip the unused private fields for now
+        -Wno-unused-private-field   # Skip the unused private fields for now
+        -fopenmp                    # We do OpenMP here
     )
+    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)
 endif()
 
 set_target_properties(Vivy PROPERTIES
diff --git a/src/Ass/Ass.hh b/src/Lib/Ass/Ass.hh
similarity index 100%
rename from src/Ass/Ass.hh
rename to src/Lib/Ass/Ass.hh
diff --git a/src/Ass/AssFactory.cc b/src/Lib/Ass/AssFactory.cc
similarity index 100%
rename from src/Ass/AssFactory.cc
rename to src/Lib/Ass/AssFactory.cc
diff --git a/src/Ass/AssFactory.hh b/src/Lib/Ass/AssFactory.hh
similarity index 98%
rename from src/Ass/AssFactory.hh
rename to src/Lib/Ass/AssFactory.hh
index e5343ec97deae93cbd864fa1844ca2fb72512b4a..48f849b01cc6ef60afbaa5b4d18627b59d24f439 100644
--- a/src/Ass/AssFactory.hh
+++ b/src/Lib/Ass/AssFactory.hh
@@ -1,7 +1,7 @@
 #ifndef VIVY_ASS_FACTORY_H
 #define VIVY_ASS_FACTORY_H
 
-#include "../Lib/Utils.hh"
+#include "../Utils.hh"
 #include "Style.hh"
 #include "Line.hh"
 
diff --git a/src/Ass/Char.cc b/src/Lib/Ass/Char.cc
similarity index 100%
rename from src/Ass/Char.cc
rename to src/Lib/Ass/Char.cc
diff --git a/src/Ass/Char.hh b/src/Lib/Ass/Char.hh
similarity index 100%
rename from src/Ass/Char.hh
rename to src/Lib/Ass/Char.hh
diff --git a/src/Ass/Line.cc b/src/Lib/Ass/Line.cc
similarity index 100%
rename from src/Ass/Line.cc
rename to src/Lib/Ass/Line.cc
diff --git a/src/Ass/Line.hh b/src/Lib/Ass/Line.hh
similarity index 100%
rename from src/Ass/Line.hh
rename to src/Lib/Ass/Line.hh
diff --git a/src/Ass/Style.cc b/src/Lib/Ass/Style.cc
similarity index 100%
rename from src/Ass/Style.cc
rename to src/Lib/Ass/Style.cc
diff --git a/src/Ass/Style.hh b/src/Lib/Ass/Style.hh
similarity index 100%
rename from src/Ass/Style.hh
rename to src/Lib/Ass/Style.hh
diff --git a/src/Ass/StyleProperties.hh b/src/Lib/Ass/StyleProperties.hh
similarity index 100%
rename from src/Ass/StyleProperties.hh
rename to src/Lib/Ass/StyleProperties.hh
diff --git a/src/Ass/Syl.cc b/src/Lib/Ass/Syl.cc
similarity index 100%
rename from src/Ass/Syl.cc
rename to src/Lib/Ass/Syl.cc
diff --git a/src/Ass/Syl.hh b/src/Lib/Ass/Syl.hh
similarity index 100%
rename from src/Ass/Syl.hh
rename to src/Lib/Ass/Syl.hh
diff --git a/src/Lib/Audio.cc b/src/Lib/Audio.cc
index 65c1cdc856d01a6d6a8023dc87ea1925fd751c78..ec20ea67aa8af5be2de9650007cabfa7976cf1e9 100644
--- a/src/Lib/Audio.cc
+++ b/src/Lib/Audio.cc
@@ -97,12 +97,15 @@ AudioContext::getProperties() const noexcept
     QJsonObject self;
     QJsonArray streams;
 
+    QFileInfo file(filePath);
+
     for (const auto &audioStreamPtr : audioStreams) {
         streams.append(audioStreamPtr->getProperties());
     }
 
-    self.insert("File path", filePath);
     self.insert("Streams", streams);
+    self.insert("File path", filePath);
+    self.insert("Base name", file.baseName());
 
     ret.setObject(self);
     return ret;
diff --git a/src/Lib/Document/AbstractDocument.hh b/src/Lib/Document/AbstractDocument.hh
new file mode 100644
index 0000000000000000000000000000000000000000..e6295263f040572bc680b5c3a97f6244da07e11d
--- /dev/null
+++ b/src/Lib/Document/AbstractDocument.hh
@@ -0,0 +1,21 @@
+#ifndef VIVY_ABSTRACT_DOCUMENT_H
+#define VIVY_ABSTRACT_DOCUMENT_H
+
+#ifndef __cplusplus
+#error "This is a C++ header"
+#endif
+
+namespace Vivy
+{
+class AbstractDocument {
+protected:
+    AbstractDocument()                   = default;
+    virtual ~AbstractDocument() noexcept = default;
+
+public:
+    virtual bool rename(const QString &) noexcept = 0;
+    virtual QString getName() const noexcept      = 0;
+};
+}
+
+#endif // VIVY_ABSTRACT_DOCUMENT_H
diff --git a/src/Document/CRTPSubDocument.cc b/src/Lib/Document/CRTPSubDocument.cc
similarity index 62%
rename from src/Document/CRTPSubDocument.cc
rename to src/Lib/Document/CRTPSubDocument.cc
index 85983b5e5389a04d152ba1fe50efd10d09e387a7..d6ac414b607200268e9d6a2b7a9755fd6d59a6ce 100644
--- a/src/Document/CRTPSubDocument.cc
+++ b/src/Lib/Document/CRTPSubDocument.cc
@@ -1,5 +1,8 @@
 #include "CRTPSubDocument.hh"
 
+#include <QJsonObject>
+#include <QJsonDocument>
+
 using namespace Vivy;
 
 // AudioSubDocument implementation
@@ -55,6 +58,23 @@ AudioSubDocument::initFromPath(const QString &path)
     qDebug() << "Audio OK for" << path;
 }
 
+QString
+AudioSubDocument::getElementName() const noexcept
+{
+    return "AudioSubDocument";
+}
+
+QJsonDocument
+AudioSubDocument::getProperties() const noexcept
+{
+    QJsonDocument ret;
+    QJsonObject object;
+    const QJsonDocument contextDocument = contextPtr->getProperties();
+    object.insert("Audio context", contextDocument.object());
+    ret.setObject(object);
+    return ret;
+}
+
 // VideoSubDocument implementation
 
 // Init a video sub-document from a file
@@ -63,7 +83,24 @@ VideoSubDocument::initFromPath(const QString &)
 {
 }
 
+QString
+VideoSubDocument::getElementName() const noexcept
+{
+    return "VideoSubDocument";
+}
+
+QJsonDocument
+VideoSubDocument::getProperties() const noexcept
+{
+    QJsonDocument ret;
+    QJsonObject object;
+    ret.setObject(object);
+    return ret;
+}
+
 // AssSubDocument implementation
+
+// Init a ASS sub-document from a file
 void
 AssSubDocument::initFromPath(const QString &path)
 {
@@ -71,3 +108,17 @@ AssSubDocument::initFromPath(const QString &path)
     factory.getStyles(styles);
     factory.getLines(lines);
 }
+QString
+AssSubDocument::getElementName() const noexcept
+{
+    return "AssSubDocument";
+}
+
+QJsonDocument
+AssSubDocument::getProperties() const noexcept
+{
+    QJsonDocument ret;
+    QJsonObject object;
+    ret.setObject(object);
+    return ret;
+}
diff --git a/src/Document/CRTPSubDocument.hh b/src/Lib/Document/CRTPSubDocument.hh
similarity index 88%
rename from src/Document/CRTPSubDocument.hh
rename to src/Lib/Document/CRTPSubDocument.hh
index 29b1c620eceb0e0c442b238a2d43ac37dd67cd13..f89b26f65e235f3c3b3fd72a22510c7afd7cebdd 100644
--- a/src/Document/CRTPSubDocument.hh
+++ b/src/Lib/Document/CRTPSubDocument.hh
@@ -5,8 +5,8 @@
 #error "This is a C++ header"
 #endif
 
-#include "../Lib/Utils.hh"
-#include "../Lib/Audio.hh"
+#include "../Utils.hh"
+#include "../Audio.hh"
 #include "../Ass/Ass.hh"
 #include <QString>
 #include <memory>
@@ -77,6 +77,9 @@ public:
     AudioContext::StreamPtr getDefaultStream() const noexcept;
     int getStreamCount() const noexcept;
     AudioContext::StreamPtr getStream(int index) const noexcept;
+
+    QString getElementName() const noexcept;
+    QJsonDocument getProperties() const noexcept;
 };
 
 // Video document
@@ -88,6 +91,10 @@ private:
 
     explicit VideoSubDocument() noexcept = default;
     friend CRTPSubDocument<VideoDocumentType, VideoSubDocument>;
+
+public:
+    QString getElementName() const noexcept;
+    QJsonDocument getProperties() const noexcept;
 };
 
 // ASS document
@@ -99,6 +106,10 @@ class AssSubDocument final : public CRTPSubDocument<AssDocumentType, AssSubDocum
     explicit AssSubDocument() noexcept = default;
     friend CRTPSubDocument<AssDocumentType, AssSubDocument>;
 
+public:
+    QString getElementName() const noexcept;
+    QJsonDocument getProperties() const noexcept;
+
 private:
     QVector<Ass::StylePtr> styles;
     QVector<Ass::LinePtr> lines;
diff --git a/src/Document/VivyDocument.cc b/src/Lib/Document/VivyDocument.cc
similarity index 80%
rename from src/Document/VivyDocument.cc
rename to src/Lib/Document/VivyDocument.cc
index 53dd53d970cfc5c0a9deb365aa9e8380753d957b..cc2f3aaeffc5847e4dd995be1dcb8233665a4964 100644
--- a/src/Document/VivyDocument.cc
+++ b/src/Lib/Document/VivyDocument.cc
@@ -1,9 +1,11 @@
 #include "VivyDocument.hh"
-#include "../Lib/Utils.hh"
+#include "../Utils.hh"
 
 #include <QFileInfo>
 #include <QString>
 #include <QStringList>
+#include <QJsonArray>
+#include <QJsonObject>
 
 using namespace Vivy;
 
@@ -23,10 +25,10 @@ bool
 VivyDocument::loadSubDocument(const QString &name) noexcept
 {
     QFileInfo file(name);
-    Capabilities type;
+    Capabilities ableType;
 
-    if (detectDocumentType(file, &type)) {
-        switch (type) {
+    if (detectDocumentType(file, &ableType)) {
+        switch (ableType) {
         case Capabilities::AudioAble:
             qDebug() << "Auto-detect audio document for" << name;
             setAudioSubDocument(file.absoluteFilePath());
@@ -52,25 +54,25 @@ bool
 VivyDocument::loadSubDocument(const QString &name, VivyDocument::Capabilities asType) noexcept
 {
     QFileInfo file(name);
-    Capabilities type;
-    if (!detectDocumentType(file, &type)) {
+    Capabilities ableType;
+    if (!detectDocumentType(file, &ableType)) {
         qCritical() << "Failed to detect type for file " << name;
         return false;
     }
 
-    if (type == Capabilities::AssAble && asType == Capabilities::AssAble) {
+    if (ableType == Capabilities::AssAble && asType == Capabilities::AssAble) {
         qDebug() << "Create an ass subDocument from " << name;
         setAssSubDocument(file.absoluteFilePath());
     }
 
-    else if (type == Capabilities::VideoAble && asType == Capabilities::VideoAble) {
+    else if (ableType == Capabilities::VideoAble && asType == Capabilities::VideoAble) {
         qDebug() << "Create a video subDocument from " << name;
         setVideoSubDocument(file.absoluteFilePath());
     }
 
     else if (const bool requestAudio = (asType == Capabilities::AudioAble);
-             (type == Capabilities::VideoAble && requestAudio) ||
-             (type == Capabilities::AudioAble && requestAudio)) {
+             (ableType == Capabilities::VideoAble && requestAudio) ||
+             (ableType == Capabilities::AudioAble && requestAudio)) {
         qDebug() << "Create an audio subDocument from " << name;
         setAudioSubDocument(file.absoluteFilePath());
     }
@@ -79,20 +81,20 @@ VivyDocument::loadSubDocument(const QString &name, VivyDocument::Capabilities as
 }
 
 bool
-VivyDocument::detectDocumentType(const QFileInfo &file, Capabilities *type) noexcept
+VivyDocument::detectDocumentType(const QFileInfo &file, Capabilities *ableType) noexcept
 {
     Vivy::Utils::DocumentType docType;
     bool rc = Vivy::Utils::detectDocumentType(file, &docType);
 
     switch (docType) {
     case Vivy::Utils::DocumentType::Video:
-        *type = Capabilities::VideoAble;
+        *ableType = Capabilities::VideoAble;
         break;
     case Vivy::Utils::DocumentType::Audio:
-        *type = Capabilities::AudioAble;
+        *ableType = Capabilities::AudioAble;
         break;
     case Vivy::Utils::DocumentType::ASS:
-        *type = Capabilities::AssAble;
+        *ableType = Capabilities::AssAble;
         break;
     default:
         return false;
@@ -240,3 +242,30 @@ VivyDocument::setAssSubDocument(const QString filename) noexcept
         documentOptions &= ~Options::UntouchedByDefault;
     }
 }
+
+QString
+VivyDocument::getElementName() const noexcept
+{
+    return getName();
+}
+QJsonDocument
+VivyDocument::getProperties() const noexcept
+{
+    QJsonDocument ret;
+    QJsonObject object;
+    QJsonObject subDocObject;
+
+    subDocObject.insert("Audio sub-document",
+                        audioDocument ? audioDocument->getProperties().object() : QJsonValue());
+    subDocObject.insert("Video sub-document",
+                        videoDocument ? videoDocument->getProperties().object() : QJsonValue());
+    subDocObject.insert("ASS sub-document",
+                        assDocument ? assDocument->getProperties().object() : QJsonValue());
+
+    object.insert("Uuid", getUuid().toString());
+    object.insert("Name", getName());
+    object.insert("Sub documents", subDocObject);
+
+    ret.setObject(object);
+    return ret;
+}
diff --git a/src/Document/VivyDocument.hh b/src/Lib/Document/VivyDocument.hh
similarity index 75%
rename from src/Document/VivyDocument.hh
rename to src/Lib/Document/VivyDocument.hh
index 4d7fa9e4031e5eb3375068aae948eca0557e95e6..10a8aeb2a11ee319de000f4c20a0541a2e406405 100644
--- a/src/Document/VivyDocument.hh
+++ b/src/Lib/Document/VivyDocument.hh
@@ -5,17 +5,19 @@
 #error "This is a C++ header"
 #endif
 
-#include "../Lib/Utils.hh"
+#include "../Utils.hh"
 #include "CRTPSubDocument.hh"
+#include "AbstractDocument.hh"
 
 #include <memory>
 #include <QString>
 #include <QDir>
 #include <QUuid>
+#include <QJsonDocument>
 
 namespace Vivy
 {
-class VivyDocument {
+class VivyDocument final : public AbstractDocument {
 public:
     enum Capabilities : quint64 {
         AudioAble = (1 << 1),
@@ -34,9 +36,15 @@ public:
             : QUuid(QUuid::createUuid())
         {
         }
+
+        QString toString() const noexcept
+        {
+            return QUuid::toString(Uuid::WithoutBraces);
+        };
     };
 
     static inline const QString filePrefix{ "vivy" };
+    static inline constexpr Utils::DocumentType type = Utils::DocumentType::Vivy;
 
 private:
     /* The document name */
@@ -53,32 +61,37 @@ private:
     std::shared_ptr<VideoSubDocument> videoDocument{};
     std::shared_ptr<AssSubDocument> assDocument{};
 
-    static bool detectDocumentType(const QFileInfo &file, Capabilities *type) noexcept;
+    static bool detectDocumentType(const QFileInfo &file, Capabilities *) noexcept;
 
 public:
-    /* Create an empty document */
+    // Create an empty document
     explicit VivyDocument(const QString &name, Options opt = NoOption);
+    ~VivyDocument() noexcept = default;
 
-    bool rename(const QString &) noexcept;
+    bool rename(const QString &) noexcept override;
     bool loadSubDocument(const QString &) noexcept;
     bool loadSubDocument(const QString &, Capabilities) noexcept;
 
-    /* Getters */
+    // Getters
     std::shared_ptr<AudioSubDocument> getAudioSubDocument() const noexcept;
     std::shared_ptr<VideoSubDocument> getVideoSubDocument() const noexcept;
     std::shared_ptr<AssSubDocument> getAssSubDocument() const noexcept;
 
-    QString getName() const noexcept;
+    QString getName() const noexcept override;
     Uuid getUuid() const noexcept;
     QString getDocumentCapabilitiesString() const noexcept;
 
     bool checkDocumentCapabilities(Capabilities) const noexcept;
     bool checkDocumentOption(Options) const noexcept;
 
-    /* Setters */
+    // Setters
     void setAudioSubDocument(const QString) noexcept;
     void setVideoSubDocument(const QString) noexcept;
     void setAssSubDocument(const QString) noexcept;
+
+    // PropertyConstViewable
+    QString getElementName() const noexcept;
+    QJsonDocument getProperties() const noexcept;
 };
 
 }
diff --git a/src/Document/VivyDocumentStore.cc b/src/Lib/Document/VivyDocumentStore.cc
similarity index 100%
rename from src/Document/VivyDocumentStore.cc
rename to src/Lib/Document/VivyDocumentStore.cc
diff --git a/src/Document/VivyDocumentStore.hh b/src/Lib/Document/VivyDocumentStore.hh
similarity index 97%
rename from src/Document/VivyDocumentStore.hh
rename to src/Lib/Document/VivyDocumentStore.hh
index 047a49ab29172b076b88a43af606f681fde0d5fa..f25fd3f7d374840596fa81ee0df07a38ddc4db66 100644
--- a/src/Document/VivyDocumentStore.hh
+++ b/src/Lib/Document/VivyDocumentStore.hh
@@ -2,7 +2,7 @@
 #define VIVY_DOCUMENTSTORE_H
 
 #include "VivyDocument.hh"
-#include "../Lib/Utils.hh"
+#include "../Utils.hh"
 
 #include <QMap>
 #include <QString>
diff --git a/src/Lib/Utils.hh b/src/Lib/Utils.hh
index 95f8e7488117d1242f52cf8f5e7b6d8bde94d147..eeba9f84c41a63e6bf3eeb23d2db3b4d602f1a4d 100644
--- a/src/Lib/Utils.hh
+++ b/src/Lib/Utils.hh
@@ -10,8 +10,13 @@
 #include <QtGlobal>
 #include <type_traits>
 
+// Prety define for OpenMP's parallel for loop with indentation not fucked up
+// by clang-format.
+#define parallel_for                                                                               \
+    _Pragma("omp parallel for") for
+
+// Don't move this object, create it in one place and never move it again.
 #define VIVY_UNMOVABLE_OBJECT(classname)                                                           \
-    /* Don't move this object around */                                                            \
     classname(const classname &) = delete;            /* Copy */                                   \
     classname(classname &&)      = delete;            /* Move */                                   \
     classname &operator=(const classname &) = delete; /* Copy assign */                            \
@@ -32,8 +37,9 @@ concept PropertyConstViewable = requires(T element)
 
 namespace Vivy::Utils
 {
-static const QStringList audioFileSuffix = { "wave", "wav", "ogg", "mp3", "m4a" };
-static const QStringList videoFileSuffix = { "mkv", "mp4" };
+static const QStringList audioFileSuffix = { "wave", "wav", "ogg",  "mp3",  "m4a",
+                                             "opus", "mp2", "aiff", "flac", "alac" };
+static const QStringList videoFileSuffix = { "mkv", "mp4", "mov", "avi", "av1", "m4v", "flv" };
 static const QStringList assFileSuffix   = { "ass" };
 static const QStringList vivyFileSuffix  = { "vivy" };
 
@@ -48,19 +54,32 @@ enum class DocumentType : quint64 {
     OGG  = (1 << 4),
     MP3  = (1 << 5),
     M4A  = (1 << 6),
+    OPUS = (1 << 7),
+    MP2  = (1 << 8),
+    AIFF = (1 << 9),
+    FLAC = (1 << 10),
+    ALAC = (1 << 11),
 
     /* Video */
-    MKV = (1 << 7),
-    MP4 = (1 << 8),
+    MKV = (1 << 12),
+    MP4 = (1 << 13),
+    MOV = (1 << 14),
+    AVI = (1 << 14),
+    AV1 = (1 << 15),
+    M4V = (1 << 16),
+    FLV = (1 << 17),
+
+    /* Vivy script */
+    VivyScript = (1 << 18),
 
     /* Meta-types */
-    Audio = (WAVE | OGG | MP3),
-    Video = (MKV | MP4),
+    Audio = (WAVE | OGG | MP3 | M4A | OPUS | MP2 | AIFF | FLAC | ALAC),
+    Video = (MKV | MP4 | MOV | AVI | AV1 | M4V | FLV),
 };
 
 template <typename E>
 constexpr auto
-to_underlying(E e) noexcept
+toUnderlying(E e) noexcept
 {
     return static_cast<std::underlying_type_t<E>>(e);
 }
@@ -72,20 +91,30 @@ namespace Vivy
 {
 // Audio document types
 enum class AudioDocumentType : quint64 {
-    MP3 = Vivy::Utils::to_underlying(Vivy::Utils::DocumentType::MP3),
-    OGG = Vivy::Utils::to_underlying(Vivy::Utils::DocumentType::OGG),
-    M4A = Vivy::Utils::to_underlying(Vivy::Utils::DocumentType::M4A),
+    MP3  = Utils::toUnderlying(Utils::DocumentType::MP3),
+    OGG  = Utils::toUnderlying(Utils::DocumentType::OGG),
+    M4A  = Utils::toUnderlying(Utils::DocumentType::M4A),
+    OPUS = Utils::toUnderlying(Utils::DocumentType::OPUS),
+    MP2  = Utils::toUnderlying(Utils::DocumentType::MP2),
+    AIFF = Utils::toUnderlying(Utils::DocumentType::AIFF),
+    FLAC = Utils::toUnderlying(Utils::DocumentType::FLAC),
+    ALAC = Utils::toUnderlying(Utils::DocumentType::ALAC),
 };
 
 // Video document types
 enum class VideoDocumentType : quint64 {
-    MKV = Vivy::Utils::to_underlying(Vivy::Utils::DocumentType::MKV),
-    MP4 = Vivy::Utils::to_underlying(Vivy::Utils::DocumentType::MP4),
+    MKV = Utils::toUnderlying(Utils::DocumentType::MKV),
+    MP4 = Utils::toUnderlying(Utils::DocumentType::MP4),
+    MOV = Utils::toUnderlying(Utils::DocumentType::MOV),
+    AVI = Utils::toUnderlying(Utils::DocumentType::AVI),
+    AV1 = Utils::toUnderlying(Utils::DocumentType::AV1),
+    M4V = Utils::toUnderlying(Utils::DocumentType::M4V),
+    FLV = Utils::toUnderlying(Utils::DocumentType::FLV),
 };
 
 // Ass document types
 enum class AssDocumentType : quint64 {
-    ASS = Vivy::Utils::to_underlying(Vivy::Utils::DocumentType::ASS),
+    ASS = Utils::toUnderlying(Utils::DocumentType::ASS),
 };
 }
 
diff --git a/src/UI/AbstractDocumentView.cc b/src/UI/AbstractDocumentView.cc
new file mode 100644
index 0000000000000000000000000000000000000000..e4b1d3867c0424f2fa5ac98773fd87ea35c49e7c
--- /dev/null
+++ b/src/UI/AbstractDocumentView.cc
@@ -0,0 +1,85 @@
+#include "AbstractDocumentView.hh"
+
+#include <QtGlobal>
+#include <QDockWidget>
+#include <QLayoutItem>
+#include <QLayout>
+#include <QWidget>
+#include <functional>
+
+using namespace Vivy;
+
+AbstractDocumentView::AbstractDocumentView(AbstractDocumentView::Type type,
+                                           QWidget *parent) noexcept
+    : QMainWindow(parent)
+    , documentType(type)
+{
+    setStatusBar(nullptr);
+    setMenuBar(nullptr);
+    setTabPosition(Qt::TopDockWidgetArea, QTabWidget::North);
+    setTabPosition(Qt::BottomDockWidgetArea, QTabWidget::South);
+    setTabPosition(Qt::LeftDockWidgetArea, QTabWidget::West);
+    setTabPosition(Qt::RightDockWidgetArea, QTabWidget::East);
+}
+
+// A utility function to delete all the child widgets
+void
+AbstractDocumentView::deleteAllContent() noexcept
+{
+    // Delete all widgets
+    if (layout() != NULL) {
+        QLayoutItem *item;
+        while ((item = layout()->takeAt(0)) != NULL) {
+            delete item->widget();
+            delete item;
+        }
+        delete layout();
+    }
+}
+
+// Returns actions that must be placed in the view actions to show/hide some of
+// the document's widgets
+const QList<QAction *> &
+AbstractDocumentView::getViewsActions() const noexcept
+{
+    return viewsActions;
+}
+
+AbstractDocumentView::Type
+AbstractDocumentView::getType() const noexcept
+{
+    return documentType;
+}
+
+void
+AbstractDocumentView::delDockWidget(QDockWidget **dock) noexcept
+{
+    // Remove the toggle view action
+    QAction *act = (*dock)->toggleViewAction();
+    if (viewsActions.contains(act))
+        viewsActions.removeAll(act);
+
+    // Remove the widget
+    removeDockWidget(*dock);
+    delete *dock;
+    *dock = nullptr;
+
+    // Emit the signal
+    emit viewActionsChanged();
+}
+void
+AbstractDocumentView::addDockWidget(Qt::DockWidgetArea area, QDockWidget *dock,
+                                    Qt::Orientation orientation) noexcept
+{
+    QMainWindow::addDockWidget(area, dock, orientation);
+    QAction *act = dock->toggleViewAction();
+    if (!viewsActions.contains(act))
+        viewsActions.prepend(act);
+    emit viewActionsChanged();
+}
+
+bool
+AbstractDocumentView::isType(const Utils::DocumentType t) const noexcept
+{
+    return Utils::toUnderlying(documentType) == Utils::toUnderlying(t);
+}
diff --git a/src/UI/AbstractDocumentView.hh b/src/UI/AbstractDocumentView.hh
new file mode 100644
index 0000000000000000000000000000000000000000..0ce9a6e03a4957ca3e9f8b5e3018bff4f3af9b87
--- /dev/null
+++ b/src/UI/AbstractDocumentView.hh
@@ -0,0 +1,60 @@
+#ifndef VIVY_DOCUMENT_VIEW_H
+#define VIVY_DOCUMENT_VIEW_H
+
+#ifndef __cplusplus
+#error "This is a C++ header"
+#endif
+
+#include "../Lib/Utils.hh"
+#include "../Lib/Document/AbstractDocument.hh"
+#include <QMainWindow>
+#include <QDockWidget>
+#include <QAction>
+
+namespace Vivy
+{
+// Any document added to the tabs in the main window must inherit from the
+// AbstractDocumentView widget. This widget will permit to query the type of
+// the document in the tab.
+class AbstractDocumentView : public QMainWindow {
+    Q_OBJECT
+    VIVY_UNMOVABLE_OBJECT(AbstractDocumentView)
+
+public:
+    enum class Type : quint64 {
+        Vivy   = Utils::toUnderlying(Utils::DocumentType::Vivy),
+        Script = Utils::toUnderlying(Utils::DocumentType::VivyScript),
+    };
+
+public:
+    explicit AbstractDocumentView(Type, QWidget *parent = nullptr) noexcept;
+    virtual ~AbstractDocumentView() noexcept = default;
+
+    virtual void closeDocument() noexcept  = 0;
+    virtual void openProperties() noexcept = 0;
+
+    virtual QString getDocumentTabName() const noexcept    = 0;
+    virtual QString getDocumentTabToolTip() const noexcept = 0;
+    virtual QIcon getDocumentTabIcon() const noexcept      = 0;
+    virtual AbstractDocument *getDocument() const noexcept = 0;
+
+    Type getType() const noexcept;
+    bool isType(const Utils::DocumentType t) const noexcept;
+    const QList<QAction *> &getViewsActions() const noexcept;
+
+signals:
+    void viewActionsChanged();
+
+protected:
+    void deleteAllContent() noexcept;
+
+    void delDockWidget(QDockWidget **) noexcept;
+    void addDockWidget(Qt::DockWidgetArea, QDockWidget *, Qt::Orientation) noexcept;
+    QList<QAction *> viewsActions{};
+
+private:
+    const Type documentType;
+};
+}
+
+#endif // VIVY_DOCUMENT_VIEW_H
diff --git a/src/UI/DialogHelp.cc b/src/UI/DialogHelp.cc
index 1eabd5c73d68bd6c479fec5db26206813c92f41a..bd0abab31efb949dabe7df5a357e9eedb98b9953 100644
--- a/src/UI/DialogHelp.cc
+++ b/src/UI/DialogHelp.cc
@@ -8,6 +8,21 @@
 
 using namespace Vivy;
 
+// See https://doc.qt.io/qt-5/richtext-html-subset.html for the supported
+// rich text subset.
+static const char *aboutContentHeader =
+    "<body>"
+    "<h1>About Vivy</h1>"
+    "<p>Vivy is a replacement for Aegisub, writen in Qt5+and with less segfaults - hopefully.</p>"
+    "<p>The following libraries where used:</p>"
+    "<ul>"
+    "  <li>Qt5</li>"
+    "  <li>libavutils</li>"
+    "  <li>libavcodec</li>"
+    "  <li>libavformat</li>"
+    "</ul>";
+static const char *aboutContentFooter = "</body>";
+
 DialogHelp::DialogHelp(QWidget *parent) noexcept
     : QMessageBox(parent)
 {
@@ -18,8 +33,22 @@ DialogHelp::DialogHelp(QWidget *parent) noexcept
     setSizePolicy(sizePolicy);
 
     setTextFormat(Qt::RichText);
-    setText(aboutContent);
+    setText(getAboutContent());
     setTextInteractionFlags(Qt::NoTextInteraction);
 
     adjustSize();
 }
+
+QString
+DialogHelp::getAboutContent() const noexcept
+{
+    QString ret;
+    ret = aboutContentHeader;
+
+    ret.append("<p>Vivy will handle the following most of the video and audio formats.");
+    ret.append("The audio formats are: " + Utils::audioFileSuffix.join(", "));
+    ret.append("The video formats are: " + Utils::videoFileSuffix.join(", ") + "</p>");
+
+    ret.append(aboutContentFooter);
+    return ret;
+}
diff --git a/src/UI/DialogHelp.hh b/src/UI/DialogHelp.hh
index 1ab9c7d9f3eff84d837438671f09b3b9a9ba917c..0bb5d9e09810ba60765cc462ae16237ccca8814e 100644
--- a/src/UI/DialogHelp.hh
+++ b/src/UI/DialogHelp.hh
@@ -18,22 +18,7 @@ public:
     ~DialogHelp() noexcept = default;
 
 private:
-    /* See https://doc.qt.io/qt-5/richtext-html-subset.html for the supported
-     * rich text subset. */
-    // clang-format off
-    static inline const char *aboutContent =
-        "<body>"
-        "<h1>About Vivy</h1>"
-        "<p>Vivy is a replacement for Aegisub, writen in Qt5+and with less segfaults - hopefully.</p>"
-        "<p>The following libraries where used:</p>"
-        "<ul>"
-        "  <li>Qt5</li>"
-        "  <li>libavutils</li>"
-        "  <li>libavcodec</li>"
-        "  <li>libavformat</li>"
-        "</ul>"
-        "</body>";
-    // clang-format on
+    QString getAboutContent() const noexcept;
 };
 
 }
diff --git a/src/UI/AudioVisualizer.cc b/src/UI/DocumentViews/AudioVisualizer.cc
similarity index 72%
rename from src/UI/AudioVisualizer.cc
rename to src/UI/DocumentViews/AudioVisualizer.cc
index a85ac12664f3ba222972bba19f0b99e4aca84f28..fa8d02c1314b267a7ffb04687bb6c56070372e5c 100644
--- a/src/UI/AudioVisualizer.cc
+++ b/src/UI/DocumentViews/AudioVisualizer.cc
@@ -1,6 +1,7 @@
 #include "AudioVisualizer.hh"
-#include "../Lib/Audio.hh"
+#include "../../Lib/Audio.hh"
 
+#include <algorithm>
 #include <QGraphicsPixmapItem>
 #include <QLabel>
 #include <QMessageBox>
@@ -12,15 +13,6 @@ using namespace Vivy;
 
 #define MAXPIXVALUE 7 // Some magix AV magic stuff
 
-#define CAP_VALUE(_value, _lower, _upper)                                                          \
-    {                                                                                              \
-        if (_value > _upper) {                                                                     \
-            _value = _upper;                                                                       \
-        } else if (_value < _lower) {                                                              \
-            _value = _lower;                                                                       \
-        }                                                                                          \
-    }
-
 AudioVisualizer::AudioVisualizer(AudioContext::StreamPtr stream, QWidget *parent)
     : QWidget(parent)
 {
@@ -55,20 +47,18 @@ AudioVisualizer::AudioVisualizer(AudioContext::StreamPtr stream, QWidget *parent
     /* Compute the image data */
 
     for (size_t x = 0, i = 0; i < size - height; i += decalage, ++x) {
-#pragma omp parallel for
-        for (size_t j = 0; j < height; j++) {
+        parallel_for (size_t j = 0; j < height; j++) {
             const double curr_dat = decodedData[i + j];
             const double window_modifier =
                 (1 - cos(2 * M_PI * static_cast<double>(j) / static_cast<double>(height - 1))) / 2;
-            float value = static_cast<float>(window_modifier * curr_dat);
-            CAP_VALUE(value, -1.0f, 1.0f);
+            const float value =
+                std::clamp(static_cast<float>(window_modifier * curr_dat), -1.0f, 1.0f);
             chunkData[j] = value;
         }
 
         av_rdft_calc(ctx.get(), chunkData.get());
 
-#pragma omp parallel for
-        for (size_t j = 0; j < height / 2; j++) {
+        parallel_for (size_t j = 0; j < height / 2; j++) {
             const float im     = chunkData[j * 2];
             const float re     = chunkData[j * 2 + 1];
             const float mag    = sqrtf(im * im + re * re);
diff --git a/src/UI/AudioVisualizer.hh b/src/UI/DocumentViews/AudioVisualizer.hh
similarity index 97%
rename from src/UI/AudioVisualizer.hh
rename to src/UI/DocumentViews/AudioVisualizer.hh
index 3273d285bba08de8865e08be657cf62ba414a483..451d50800979bca9f137b3fd2dfcd3842715629b 100644
--- a/src/UI/AudioVisualizer.hh
+++ b/src/UI/DocumentViews/AudioVisualizer.hh
@@ -6,7 +6,7 @@
 #endif
 
 #include "TimingView.hh"
-#include "../Lib/Audio.hh"
+#include "../../Lib/Audio.hh"
 #include <QWidget>
 #include <QString>
 
diff --git a/src/UI/TimingBar.cc b/src/UI/DocumentViews/TimingBar.cc
similarity index 100%
rename from src/UI/TimingBar.cc
rename to src/UI/DocumentViews/TimingBar.cc
diff --git a/src/UI/TimingBar.hh b/src/UI/DocumentViews/TimingBar.hh
similarity index 97%
rename from src/UI/TimingBar.hh
rename to src/UI/DocumentViews/TimingBar.hh
index 3dcd5fd0819682a7f81a768266b809ffecdd1e83..6edf64eede549c9284a1a332d05aaca6b8bfdc6a 100644
--- a/src/UI/TimingBar.hh
+++ b/src/UI/DocumentViews/TimingBar.hh
@@ -5,7 +5,6 @@
 #error "This is a C++ header"
 #endif
 
-#include <QWidget>
 #include <QGraphicsItem>
 #include <QGraphicsLineItem>
 
diff --git a/src/UI/TimingScene.cc b/src/UI/DocumentViews/TimingScene.cc
similarity index 98%
rename from src/UI/TimingScene.cc
rename to src/UI/DocumentViews/TimingScene.cc
index 7d2e21156582e7c6c77de94958b53785c057cb1a..3c916fd0868d3364b69b0822c233357dd9667f94 100644
--- a/src/UI/TimingScene.cc
+++ b/src/UI/DocumentViews/TimingScene.cc
@@ -1,5 +1,5 @@
 #include "TimingScene.hh"
-#include "../Lib/Utils.hh"
+#include "../../Lib/Utils.hh"
 
 #include <QGraphicsLineItem>
 #include <QGraphicsPixmapItem>
diff --git a/src/UI/TimingScene.hh b/src/UI/DocumentViews/TimingScene.hh
similarity index 95%
rename from src/UI/TimingScene.hh
rename to src/UI/DocumentViews/TimingScene.hh
index 860acecbc35628983b137e517385f82da36cf1ef..901de0fa63c48079797dd50018e5ce51cf85e018 100644
--- a/src/UI/TimingScene.hh
+++ b/src/UI/DocumentViews/TimingScene.hh
@@ -1,8 +1,8 @@
 #ifndef VIVY_TIMING_SCENE_H
 #define VIVY_TIMING_SCENE_H
 
-#include "../Lib/Utils.hh"
-#include "../Ass/Ass.hh"
+#include "../../Lib/Utils.hh"
+#include "../../Lib/Ass/Ass.hh"
 #include "TimingBar.hh"
 
 #include <QWidget>
diff --git a/src/UI/TimingView.cc b/src/UI/DocumentViews/TimingView.cc
similarity index 82%
rename from src/UI/TimingView.cc
rename to src/UI/DocumentViews/TimingView.cc
index 25f859d3dda9af16b58ebc910f9ee4ebe1a276bf..9614cf18ae6008657248d5e89a91c4c84a348d2e 100644
--- a/src/UI/TimingView.cc
+++ b/src/UI/DocumentViews/TimingView.cc
@@ -24,6 +24,9 @@ TimingView::TimingView(QImage img, quint64 soundLength, QWidget *parent) noexcep
     setFixedHeight(img.height());
     setMaximumHeight(img.height() + horizontalScrollBar()->height() - TO_ADD_TO_IMAGE_HEIGHT);
     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+    QObject::connect(verticalScrollBar(), &QScrollBar::rangeChanged, this,
+                     &TimingView::moveScrollBarToBottom);
     setScene(scene);
 }
 
@@ -43,3 +46,9 @@ TimingView::mousePressEvent(QMouseEvent *event) noexcept
 
     QGraphicsView::mousePressEvent(event);
 }
+
+void
+TimingView::moveScrollBarToBottom(int, int max) noexcept
+{
+    verticalScrollBar()->setValue(max);
+}
diff --git a/src/UI/TimingView.hh b/src/UI/DocumentViews/TimingView.hh
similarity index 90%
rename from src/UI/TimingView.hh
rename to src/UI/DocumentViews/TimingView.hh
index b2f11f792576e7d66b1e2628ae2cefb12bb51e67..ff5319fe5a19f4892d796b31cf827723adbab7fa 100644
--- a/src/UI/TimingView.hh
+++ b/src/UI/DocumentViews/TimingView.hh
@@ -5,7 +5,7 @@
 #error "This is a C++ header"
 #endif
 
-#include "../Lib/Utils.hh"
+#include "../../Lib/Utils.hh"
 #include "TimingBar.hh"
 #include "TimingScene.hh"
 
@@ -31,6 +31,7 @@ private:
 
 public slots:
     void mousePressEvent(QMouseEvent *event) noexcept;
+    void moveScrollBarToBottom(int, int) noexcept;
 };
 
 }
diff --git a/src/UI/MainWindow.cc b/src/UI/MainWindow.cc
index 212abaf9f3312c909a0d2f02a33de2bde7e46c2c..a362c375b6364c810f0530debb0d414d8ab2efde 100644
--- a/src/UI/MainWindow.cc
+++ b/src/UI/MainWindow.cc
@@ -1,11 +1,15 @@
 #include "MainWindow.hh"
 #include "DialogHelp.hh"
+#include "PropertyModel.hh"
 #include "VivyDocumentView.hh"
 #include "../Lib/Utils.hh"
 #include "../VivyApplication.hh"
 
+#include <algorithm>
 #include <functional>
+#include <QWindow>
 #include <optional>
+#include <QTreeView>
 #include <QFileInfo>
 #include <QStandardPaths>
 #include <QPixmap>
@@ -41,13 +45,14 @@ using namespace Vivy;
 MainWindow::MainWindow(QWidget *parent) noexcept
     : QMainWindow(parent)
 {
-    /* Setup the main window other properties */
+    // Setup the main window other properties
     setWindowIcon(QIcon(":/icons/vivy.png"));
 
     /* Some declarations */
     DCL_MENU(file, "&File");
     DCL_MENU(edit, "&Edit");
-    DCL_MENU(view, "&View");
+    DCL_MENU(viewTmp, "&View");
+    viewMenu = viewTmpMenu; // Save the view menu
     DCL_MENU(simulate, "&Simulate");
     DCL_MENU(help, "&Help");
     DCL_TOOLBAR(file, "File");
@@ -80,7 +85,7 @@ MainWindow::MainWindow(QWidget *parent) noexcept
     TOOLBAR_ADD_ACTION(file, saveFileAs);
     TOOLBAR_ADD_ACTION(other, openDialogHelp);
 
-    /* Setup the tabs to display the documents */
+    // Setup the tabs to display the documents
     documents = new QTabWidget(this);
     documents->setMovable(true);
     documents->setTabsClosable(true);
@@ -88,49 +93,71 @@ MainWindow::MainWindow(QWidget *parent) noexcept
     documents->setUsesScrollButtons(true);
     documents->setDocumentMode(true);
     connect(documents, &QTabWidget::tabCloseRequested, this, &MainWindow::closeDocument);
+    connect(documents, &QTabWidget::tabBarDoubleClicked, this, &MainWindow::openProperties);
     setCentralWidget(documents);
 
-    /* Enable/disable actions depending on the context */
+    // Enable/disable actions depending on the context
     saveFileAct->setEnabled(false);
     saveFileAsAct->setEnabled(false);
     loadSubDocumentAssAct->setEnabled(false);
     loadSubDocumentVideoAct->setEnabled(false);
     loadSubDocumentAudioAct->setEnabled(false);
 
-    auto enableOnDocument = [](auto *widget, int index) noexcept -> void {
-        widget->setEnabled(index >= 0);
+    // Enable actions if the document is save-able
+    auto enableSaveOnDocument = [this](auto *widget, int index) noexcept -> void {
+        if (index >= 0) {
+            auto type = static_cast<AbstractDocumentView *>(documents->widget(index))->getType();
+            widget->setEnabled(type == AbstractDocumentView::Type::Vivy ||
+                               type == AbstractDocumentView::Type::Script);
+        } else {
+            widget->setEnabled(false);
+        }
+    };
+
+    // Enable load sub-document on sub-document able objects
+    auto enableLoadSubOnDocument = [this](auto *widget, int index) noexcept -> void {
+        if (index >= 0) {
+            auto type = static_cast<AbstractDocumentView *>(documents->widget(index))->getType();
+            widget->setEnabled(type == AbstractDocumentView::Type::Vivy);
+        } else {
+            widget->setEnabled(false);
+        }
     };
 
+    connect(documents, &QTabWidget::currentChanged, this,
+            [this](int) noexcept -> void { documentViewActionsChanged(); });
     connect(documents, &QTabWidget::currentChanged, saveFileAct,
-            std::bind_front(enableOnDocument, saveFileAct));
+            std::bind_front(enableSaveOnDocument, saveFileAct));
     connect(documents, &QTabWidget::currentChanged, saveFileAsAct,
-            std::bind_front(enableOnDocument, saveFileAsAct));
+            std::bind_front(enableSaveOnDocument, saveFileAsAct));
 
     connect(documents, &QTabWidget::currentChanged, loadSubDocumentAudioAct,
-            std::bind_front(enableOnDocument, loadSubDocumentAudioAct));
+            std::bind_front(enableLoadSubOnDocument, loadSubDocumentAudioAct));
     connect(documents, &QTabWidget::currentChanged, loadSubDocumentVideoAct,
-            std::bind_front(enableOnDocument, loadSubDocumentVideoAct));
+            std::bind_front(enableLoadSubOnDocument, loadSubDocumentVideoAct));
     connect(documents, &QTabWidget::currentChanged, loadSubDocumentAssAct,
-            std::bind_front(enableOnDocument, loadSubDocumentAssAct));
+            std::bind_front(enableLoadSubOnDocument, loadSubDocumentAssAct));
 
-    /* Add a new empty document that will will be replaced if nothing is added
-     * to it. */
+    // Add a new empty document that will will be replaced if nothing is added
+    // to it.
     newDocument();
 
-    /* Main window has finished its construction */
+    // Main window has finished its construction
     statusBar()->showMessage("QSimulate has started");
 }
 
-std::weak_ptr<VivyDocument>
-MainWindow::getCurrentDocument() const
+void
+MainWindow::openProperties(int index) noexcept
 {
-    if (VivyDocumentView *currentView = getCurrentDocumentView()) {
-        return currentView->getDocument();
+    if (index < 0) {
+        // TODO: May may want to do something when the user is clicking where
+        // no tab bar is openned (like open document?)
+        return;
     }
 
-    else {
-        throw std::runtime_error("no current document");
-    }
+    qDebug().nospace() << "Tab n°" << index << " was double clicked";
+    AbstractDocumentView *current = getTab(index);
+    current->openProperties();
 }
 
 void
@@ -140,11 +167,22 @@ MainWindow::openDialogHelp() noexcept
     help_holder->exec();
 }
 
+AbstractDocument *
+MainWindow::getCurrentDocument() const noexcept
+{
+    try {
+        return getCurrentDocumentView()->getDocument();
+    } catch (const std::runtime_error &e) {
+        qCritical() << "No current view in the main window";
+        return nullptr;
+    }
+}
+
 void
 MainWindow::saveFile() noexcept
 {
     try {
-        if (auto document = getCurrentDocument().lock()) {
+        if (auto document = getCurrentDocument()) {
             qDebug() << "Request to save the document" << document->getName();
         }
     } catch (const std::runtime_error &e) {
@@ -156,7 +194,7 @@ void
 MainWindow::saveFileAs() noexcept
 {
     try {
-        if (auto document = getCurrentDocument().lock()) {
+        if (auto document = getCurrentDocument()) {
             qDebug() << "Request to save the document" << document->getName();
         }
     } catch (const std::runtime_error &e) {
@@ -167,27 +205,17 @@ MainWindow::saveFileAs() noexcept
 void
 MainWindow::closeDocument(int index) noexcept
 {
-    const VivyDocumentView *documentToClose =
-        reinterpret_cast<const VivyDocumentView *>(documents->widget(index));
-
-    if (documentToClose == nullptr) {
-        qCritical() << "Can't close a null document pointer";
+    if (index < 0)
         return;
-    }
-
-    std::optional<VivyDocument::Uuid> uuid;
-    if (auto doc = documentToClose->getDocument().lock()) {
-        uuid = doc->getUuid();
-        qDebug() << "Closing the document:" << doc->getName() << "( ref count is" << doc.use_count()
-                 << ")";
-    }
 
-    QWidget *tab = documents->widget(index);
+    auto *documentToClose = static_cast<AbstractDocumentView *>(documents->widget(index));
     documents->removeTab(index);
-    delete tab;
+    disconnect(documentToClose, &AbstractDocumentView::viewActionsChanged, this,
+               &MainWindow::documentViewActionsChanged);
 
-    if (uuid) {
-        vivyApp->documentStore.closeDocument(*uuid);
+    if (documentToClose) {
+        documentToClose->closeDocument();
+        delete documentToClose;
     }
 }
 
@@ -230,46 +258,45 @@ MainWindow::openDocument() noexcept
 void
 MainWindow::loadSubDocumentAss() noexcept
 {
-    QString filename = QFileDialog::getOpenFileName(this, "Select an ASS document to load");
-
-    if (filename.isEmpty()) {
-        qWarning() << "Found an empty filename, don't open a file";
-        return;
-    }
-
-    if (auto currDoc = getCurrentDocument().lock()) {
-        currDoc->setAssSubDocument(filename);
-    }
+    withOpenFileNameDialog<VivyDocumentView, VivyDocument>(
+        "Select an ASS document to load",
+        [](VivyDocumentView *view, VivyDocument *doc, const QString &filename) noexcept -> void {
+            doc->setAssSubDocument(filename);
+            view->loadAssView();
+        });
 }
 
 void
 MainWindow::loadSubDocumentVideo() noexcept
 {
-    QString filename = QFileDialog::getOpenFileName(this, "Select a video document to load");
-
-    if (filename.isEmpty()) {
-        qWarning() << "Found an empty filename, don't open a file";
-        return;
-    }
-
-    if (auto currDoc = getCurrentDocument().lock()) {
-        currDoc->setVideoSubDocument(filename);
-    }
+    withOpenFileNameDialog<VivyDocumentView, VivyDocument>(
+        "Select a video document to load",
+        [](VivyDocumentView *view, VivyDocument *doc, const QString &filename) noexcept -> void {
+            doc->setVideoSubDocument(filename);
+            view->loadVideoView();
+        });
 }
 
 void
 MainWindow::loadSubDocumentAudio() noexcept
 {
-    QString filename = QFileDialog::getOpenFileName(this, "Select an audio document to load");
-
-    if (filename.isEmpty()) {
-        qWarning() << "Found an empty filename, don't open a file";
-        return;
-    }
+    withOpenFileNameDialog<VivyDocumentView, VivyDocument>(
+        "Select an audio document to load",
+        [](VivyDocumentView *view, VivyDocument *doc, const QString &filename) noexcept -> void {
+            doc->setAudioSubDocument(filename);
+            view->loadAudioView();
+        });
+}
 
-    if (auto currDoc = getCurrentDocument().lock()) {
-        currDoc->setAudioSubDocument(filename);
-    }
+void
+MainWindow::addTab(AbstractDocumentView *tab)
+{
+    const int index = documents->addTab(tab, tab->getDocumentTabIcon(), tab->getDocumentTabName());
+    documents->setTabToolTip(index, tab->getDocumentTabToolTip());
+    documents->setCurrentIndex(index);
+    connect(tab, &AbstractDocumentView::viewActionsChanged, this,
+            &MainWindow::documentViewActionsChanged);
+    documentViewActionsChanged();
 }
 
 void
@@ -278,21 +305,25 @@ MainWindow::addTab(VivyDocumentView *tab)
     int index = -1;
     if (const int untouched_index = findFirstUntouchedDocument(); untouched_index >= 0) {
         closeDocument(untouched_index);
-        index = documents->insertTab(untouched_index, tab, QIcon(":/icons/vivy.png"),
+        index = documents->insertTab(untouched_index, tab, tab->getDocumentTabIcon(),
                                      tab->getDocumentTabName());
     } else {
-        index = documents->addTab(tab, QIcon(":/icons/vivy.png"), tab->getDocumentTabName());
+        index = documents->addTab(tab, tab->getDocumentTabIcon(), tab->getDocumentTabName());
     }
 
     documents->setTabToolTip(index, tab->getDocumentTabToolTip());
+    documents->setCurrentIndex(index);
+    connect(tab, &AbstractDocumentView::viewActionsChanged, this,
+            &MainWindow::documentViewActionsChanged);
+    documentViewActionsChanged();
     qDebug() << "View constructed successfully";
 }
 
-VivyDocumentView *
+AbstractDocumentView *
 MainWindow::getCurrentDocumentView() const
 {
-    if (VivyDocumentView *currentView =
-            reinterpret_cast<VivyDocumentView *>(documents->currentWidget())) {
+    if (AbstractDocumentView *currentView =
+            static_cast<AbstractDocumentView *>(documents->currentWidget())) {
         return currentView;
     }
 
@@ -301,12 +332,12 @@ MainWindow::getCurrentDocumentView() const
     }
 }
 
-VivyDocumentView *
+AbstractDocumentView *
 MainWindow::getTab(const int index) const noexcept
 {
     if (index > documents->count())
         return nullptr;
-    return reinterpret_cast<VivyDocumentView *>(documents->widget(index));
+    return static_cast<AbstractDocumentView *>(documents->widget(index));
 }
 
 int
@@ -314,10 +345,27 @@ MainWindow::findFirstUntouchedDocument() const noexcept
 {
     const int count = documents->count();
     for (int index = 0; index < count; ++index) {
-        if (getTab(index)->getDocument().lock()->checkDocumentOption(
-                VivyDocument::UntouchedByDefault))
+        AbstractDocumentView *documentView = getTab(index);
+        if (documentView && (documentView->getType() == AbstractDocumentView::Type::Vivy) &&
+            static_cast<VivyDocumentView *>(documentView)
+                ->getDocument()
+                ->checkDocumentOption(VivyDocument::UntouchedByDefault))
             return index;
     }
 
     return -1;
 }
+
+void
+MainWindow::documentViewActionsChanged() noexcept
+{
+    qInfo() << "Document view action changed";
+    viewMenu->clear();
+
+    // Change document view menu if we have a document
+    try {
+        viewMenu->addActions(getCurrentDocumentView()->getViewsActions());
+    } catch (const std::runtime_error &e) {
+        qInfo() << "No view to display:" << e.what();
+    }
+}
diff --git a/src/UI/MainWindow.hh b/src/UI/MainWindow.hh
index fc3e60ea739a7be2ca75da3e4132855c9afd72af..d42f057cb204720379445e45bc78bae675243f07 100644
--- a/src/UI/MainWindow.hh
+++ b/src/UI/MainWindow.hh
@@ -6,10 +6,13 @@
 #endif
 
 #include "../Lib/Utils.hh"
-#include "AudioVisualizer.hh"
-#include "../Document/VivyDocumentStore.hh"
+#include "../Lib/Document/AbstractDocument.hh"
+#include "../Lib/Document/VivyDocumentStore.hh"
+#include "DocumentViews/AudioVisualizer.hh"
 #include "VivyDocumentView.hh"
 #include <QMainWindow>
+#include <QMenu>
+#include <QFileDialog>
 
 namespace Vivy
 {
@@ -17,24 +20,62 @@ class MainWindow final : public QMainWindow {
     Q_OBJECT
 
     QTabWidget *documents{ nullptr };
+    QMenu *viewMenu{ nullptr };
 
 public:
     explicit MainWindow(QWidget *parent = nullptr) noexcept;
     ~MainWindow() noexcept = default;
 
-    std::weak_ptr<VivyDocument> getCurrentDocument() const;
+    AbstractDocument *getCurrentDocument() const noexcept;
+    template <class Document> Document *getCurrentDocument() const noexcept
+    {
+        if (AbstractDocumentView *currentView = getCurrentDocumentView();
+            currentView->getType() == Document::type) {
+            return currentView->getDocument();
+        } else {
+            return nullptr;
+        }
+    }
 
 private:
+    void addTab(AbstractDocumentView *);
     void addTab(VivyDocumentView *);
-    VivyDocumentView *getTab(const int) const noexcept;
-    VivyDocumentView *getCurrentDocumentView() const;
+    AbstractDocumentView *getTab(const int) const noexcept;
+    AbstractDocumentView *getCurrentDocumentView() const;
+
     int findFirstUntouchedDocument() const noexcept;
 
+    // Do an action with the selected filename. The 'call' variable must be
+    // callable like this: call(DocumentView, Document, QString)
+    template <typename DV, typename D>
+    void withOpenFileNameDialog(const QString &, auto call) noexcept
+    {
+        QString filename = QFileDialog::getOpenFileName(this, "Select an ASS document to load");
+
+        if (filename.isEmpty()) {
+            qWarning() << "Found an empty filename, don't open a file";
+            return;
+        }
+
+        try {
+            if (AbstractDocumentView *currView = getCurrentDocumentView();
+                currView->isType(D::type)) {
+                DV *view = static_cast<VivyDocumentView *>(currView);
+                D *doc   = view->getDocument();
+                call(view, doc, filename);
+            }
+        } catch (const std::runtime_error &e) {
+            qCritical() << "Operation failed with:" << e.what();
+        }
+    }
+
 private slots:
     void newDocument() noexcept;
     void openDocument() noexcept;
     void closeDocument(int index) noexcept;
 
+    void openProperties(int index) noexcept;
+
     void loadSubDocumentAss() noexcept;
     void loadSubDocumentVideo() noexcept;
     void loadSubDocumentAudio() noexcept;
@@ -43,6 +84,8 @@ private slots:
     void saveFileAs() noexcept;
 
     void openDialogHelp() noexcept;
+
+    void documentViewActionsChanged() noexcept;
 };
 
 }
diff --git a/src/UI/PropertyModel.cc b/src/UI/PropertyModel.cc
new file mode 100644
index 0000000000000000000000000000000000000000..b32144f0ed5a76a8dce19077f348a5527ad1ed59
--- /dev/null
+++ b/src/UI/PropertyModel.cc
@@ -0,0 +1,319 @@
+#include "PropertyModel.hh"
+#include <QDebug>
+
+using namespace Vivy;
+
+PropertyModel::Item::Item(Item *theParent) noexcept
+    : parent(theParent)
+{
+}
+
+PropertyModel::Item::~Item() noexcept
+{
+    qDeleteAll(childs);
+}
+
+void
+PropertyModel::Item::appendChild(PropertyModel::Item *item) noexcept
+{
+    childs.append(item);
+}
+
+PropertyModel::Item *
+PropertyModel::Item::getChild(int row) const noexcept
+{
+    return childs.value(row);
+}
+
+PropertyModel::Item *
+PropertyModel::Item::getParent() const noexcept
+{
+    return parent;
+}
+
+int
+PropertyModel::Item::getChildCount() const noexcept
+{
+    return childs.count();
+}
+
+int
+PropertyModel::Item::getRow() const noexcept
+{
+    return parent ? parent->childs.indexOf(const_cast<Item *>(this)) : 0;
+}
+
+void
+PropertyModel::Item::setKey(const QString &k) noexcept
+{
+    key = k;
+}
+
+void
+PropertyModel::Item::setValue(const QString &v) noexcept
+{
+    value = v;
+}
+
+void
+PropertyModel::Item::setType(const QJsonValue::Type &t) noexcept
+{
+    type = t;
+}
+
+QString
+PropertyModel::Item::getKey() const noexcept
+{
+    return key;
+}
+
+QString
+PropertyModel::Item::getValue() const noexcept
+{
+    return value;
+}
+
+QJsonValue::Type
+PropertyModel::Item::getType() const noexcept
+{
+    return type;
+}
+
+const QList<PropertyModel::Item *>
+PropertyModel::Item::getChilds() const noexcept
+{
+    return childs;
+}
+
+PropertyModel::Item *
+PropertyModel::Item::fromJson(const QJsonValue &value, PropertyModel::Item *parent)
+{
+    Item *root = new Item(parent);
+    root->setKey("root");
+
+    if (value.isObject()) {
+        for (const QString &key : value.toObject().keys()) {
+            QJsonValue v = value.toObject().value(key);
+            Item *child  = fromJson(v, root);
+            child->setKey(key);
+            child->setType(v.type());
+            root->appendChild(child);
+        }
+
+    }
+
+    else if (value.isArray()) {
+        int index = 0;
+        for (const QJsonValue &v : value.toArray()) {
+            Item *child = fromJson(v, root);
+            child->setKey(QString::number(index));
+            child->setType(v.type());
+            root->appendChild(child);
+            ++index;
+        }
+    }
+
+    else {
+        root->setValue(value.toVariant().toString());
+        root->setType(value.type());
+    }
+
+    return root;
+}
+
+PropertyModel::PropertyModel(QObject *parent) noexcept
+    : QAbstractItemModel(parent)
+{
+    headers.append("key");
+    headers.append("value");
+}
+
+PropertyModel::PropertyModel(const QJsonDocument &properties, QObject *parent) noexcept
+    : PropertyModel(parent)
+{
+    try {
+        loadJson(properties);
+    } catch (const std::runtime_error &e) {
+        qCritical() << "Failed to create the property model object:" << e.what();
+    }
+}
+
+void
+PropertyModel::loadJson(const QJsonDocument &jdoc)
+{
+    if (!jdoc.isNull()) {
+        beginResetModel();
+        root.reset();
+
+        if (jdoc.isArray()) {
+            root.reset(Item::fromJson(QJsonValue(jdoc.array())));
+            root->setType(QJsonValue::Array);
+
+        } else {
+            root.reset(Item::fromJson(QJsonValue(jdoc.object())));
+            root->setType(QJsonValue::Object);
+        }
+
+        endResetModel();
+        return;
+    }
+
+    throw std::runtime_error("failed to load json");
+}
+
+QVariant
+PropertyModel::data(const QModelIndex &index, int role) const noexcept
+{
+    if (!index.isValid())
+        return QVariant();
+
+    const Item *item = static_cast<Item *>(index.internalPointer());
+
+    if (role == Qt::DisplayRole) {
+        if (index.column() == 0)
+            return QString("%1").arg(item->getKey());
+
+        if (index.column() == 1)
+            return QString("%1").arg(item->getValue());
+    }
+
+    else if (Qt::EditRole == role) {
+        if (index.column() == 1) {
+            return QString("%1").arg(item->getValue());
+        }
+    }
+
+    return QVariant();
+}
+
+bool
+PropertyModel::setData(const QModelIndex &index, const QVariant &value, int role) noexcept
+{
+    const int col = index.column();
+    if (Qt::EditRole == role) {
+        if (col == 1) {
+            Item *item = static_cast<Item *>(index.internalPointer());
+            item->setValue(value.toString());
+            emit dataChanged(index, index, { Qt::EditRole });
+            return true;
+        }
+    }
+
+    return false;
+}
+
+QVariant
+PropertyModel::headerData(int section, Qt::Orientation orientation, int role) const noexcept
+{
+    if (role != Qt::DisplayRole)
+        return QVariant();
+
+    if (orientation == Qt::Horizontal)
+        return headers.value(section);
+
+    else
+        return QVariant();
+}
+
+QModelIndex
+PropertyModel::index(int row, int column, const QModelIndex &parent) const noexcept
+{
+    if (!hasIndex(row, column, parent))
+        return QModelIndex();
+
+    const Item *parentItem =
+        parent.isValid() ? static_cast<Item *>(parent.internalPointer()) : root.get();
+    Item *childItem = parentItem->getChild(row);
+    return childItem ? createIndex(row, column, childItem) : QModelIndex();
+}
+
+QModelIndex
+PropertyModel::parent(const QModelIndex &index) const noexcept
+{
+    if (!index.isValid())
+        return QModelIndex();
+
+    const Item *childItem = static_cast<Item *>(index.internalPointer());
+    Item *parentItem      = childItem->getParent();
+
+    if (parentItem == root.get())
+        return QModelIndex();
+
+    return createIndex(parentItem->getRow(), 0, parentItem);
+}
+
+int
+PropertyModel::rowCount(const QModelIndex &parent) const noexcept
+{
+    if (parent.column() > 0)
+        return 0;
+
+    const Item *parentItem =
+        parent.isValid() ? static_cast<Item *>(parent.internalPointer()) : root.get();
+    return parentItem->getChildCount();
+}
+
+int
+PropertyModel::columnCount(const QModelIndex & /* parent */) const noexcept
+{
+    return 2;
+}
+
+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);
+}
+
+// Get the json stored inside the model
+QJsonDocument
+PropertyModel::getJson() const noexcept
+{
+    QJsonValue v = generateJson(root.get());
+    return v.isObject() ? QJsonDocument(v.toObject()) : QJsonDocument(v.toArray());
+}
+
+// Get the json stored inside the model by calculating it
+QJsonValue
+PropertyModel::generateJson(PropertyModel::Item *item) const noexcept
+{
+    if (QJsonValue::Type type = item->getType(); QJsonValue::Object == type) {
+        QJsonObject jo;
+        for (const auto &child : item->getChilds())
+            jo.insert(child->getKey(), generateJson(child));
+        return jo;
+    }
+
+    else if (QJsonValue::Array == type) {
+        QJsonArray arr;
+        for (const auto &child : item->getChilds())
+            arr.append(generateJson(child));
+        return arr;
+    }
+
+    else {
+        QJsonValue va(item->getValue());
+        return va;
+    }
+}
+
+QString
+PropertyModel::getName() const noexcept
+{
+    return root ? "Property for " + root->getKey() : "Property view";
+}
diff --git a/src/UI/PropertyModel.hh b/src/UI/PropertyModel.hh
new file mode 100644
index 0000000000000000000000000000000000000000..c6cdca5eb16601a03c1a4d46c01c4ae8dba2d454
--- /dev/null
+++ b/src/UI/PropertyModel.hh
@@ -0,0 +1,105 @@
+#ifndef VIVY_UI_PROPERTY_VIEW_H
+#define VIVY_UI_PROPERTY_VIEW_H
+
+#include "../Lib/Utils.hh"
+
+#include <QAbstractItemModel>
+#include <QJsonDocument>
+#include <QJsonValue>
+#include <QJsonArray>
+#include <QJsonObject>
+
+//  auto model          = new PropertyModel(doc);
+//  QVBoxLayout *layout = new QVBoxLayout(this);
+//  QTreeView *view     = new QTreeView(this);
+//  view->setModel(model);
+//  view->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+//  view->expandAll();
+//  layout->addWidget(view);
+//  setLayout(layout);
+
+namespace Vivy
+{
+// PropertyModel is used to store a json document and be able to render it
+// through a QTreeView
+class PropertyModel final : public QAbstractItemModel {
+    Q_OBJECT
+    VIVY_UNMOVABLE_OBJECT(PropertyModel)
+
+private:
+    // Simple property item to be stored inside the PropertyModel
+    class Item final {
+        VIVY_UNMOVABLE_OBJECT(Item)
+
+    public:
+        Item(Item *parent = nullptr) noexcept;
+        ~Item() noexcept;
+
+        void appendChild(Item *) noexcept;
+        Item *getChild(int row) const noexcept;
+        const QList<Item *> getChilds() const noexcept;
+        Item *getParent() const noexcept;
+        int getChildCount() const noexcept;
+        int getRow() const noexcept;
+        void setKey(const QString &) noexcept;
+        void setValue(const QString &) noexcept;
+        void setType(const QJsonValue::Type &) noexcept;
+        QString getKey() const noexcept;
+        QString getValue() const noexcept;
+        QJsonValue::Type getType() const noexcept;
+
+        static Item *fromJson(const QJsonValue &, Item *parent = nullptr);
+
+    protected:
+    private:
+        QString key{};
+        QString value{};
+        QJsonValue::Type type{ QJsonValue::Null };
+        QList<Item *> childs{};
+        Item *parent{ nullptr };
+    };
+
+public:
+    explicit PropertyModel(QObject *parent = nullptr) noexcept;
+    explicit PropertyModel(const QJsonDocument &, QObject *parent = nullptr) noexcept;
+    explicit PropertyModel(const PropertyConstViewable auto &object,
+                           QObject *parent = nullptr) noexcept
+        : PropertyModel(object.getProperties(), parent)
+    {
+        // With concepts it seems that the function must be implemented where
+        // it is declared.
+        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 &value,
+                 int role = Qt::EditRole) noexcept override;
+
+    QVariant headerData(int section, Qt::Orientation, int role) const noexcept override;
+    QModelIndex parent(const QModelIndex &) const noexcept override;
+    QModelIndex index(int row, int column,
+                      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;
+    QJsonDocument getJson() const noexcept;
+
+    QString getName() const noexcept;
+
+private:
+    QJsonValue generateJson(Item *) const noexcept;
+    std::unique_ptr<Item> root{ nullptr };
+    QStringList headers{};
+    bool editableState{ false };
+};
+
+}
+
+#endif // VIVY_UI_PROPERTY_VIEW_H
diff --git a/src/UI/ScriptDocumentView.cc b/src/UI/ScriptDocumentView.cc
new file mode 100644
index 0000000000000000000000000000000000000000..661a1fbe5e23e8e193161cad68c3515426012511
--- /dev/null
+++ b/src/UI/ScriptDocumentView.cc
@@ -0,0 +1,36 @@
+#include "ScriptDocumentView.hh"
+
+using namespace Vivy;
+
+ScriptDocumentView::ScriptDocumentView(QWidget *parent) noexcept
+    : AbstractDocumentView(AbstractDocumentView::Type::Script, parent)
+{
+}
+
+void
+ScriptDocumentView::closeDocument() noexcept
+{
+}
+
+QString
+ScriptDocumentView::getDocumentTabName() const noexcept
+{
+    return "VS File";
+}
+
+QString
+ScriptDocumentView::getDocumentTabToolTip() const noexcept
+{
+    return "VS File";
+}
+
+QIcon
+ScriptDocumentView::getDocumentTabIcon() const noexcept
+{
+    return QIcon::fromTheme("text-x-script");
+}
+
+void
+ScriptDocumentView::openProperties() noexcept
+{
+}
diff --git a/src/UI/ScriptDocumentView.hh b/src/UI/ScriptDocumentView.hh
new file mode 100644
index 0000000000000000000000000000000000000000..4d93378aab9a173786c621c91cb2f7d66dfa5c04
--- /dev/null
+++ b/src/UI/ScriptDocumentView.hh
@@ -0,0 +1,38 @@
+#ifndef VIVY_SCRIPT_DOCUMENT_VIEW_H
+#define VIVY_SCRIPT_DOCUMENT_VIEW_H
+
+#ifndef __cplusplus
+#error "This is a C++ header"
+#endif
+
+#include "../Lib/Utils.hh"
+#include "AbstractDocumentView.hh"
+#include <QWidget>
+#include <QString>
+#include <memory>
+
+namespace Vivy
+{
+class ScriptDocumentView final : public AbstractDocumentView {
+    Q_OBJECT
+    VIVY_UNMOVABLE_OBJECT(ScriptDocumentView)
+
+public:
+    explicit ScriptDocumentView(QWidget *parent = nullptr) noexcept;
+    virtual ~ScriptDocumentView() noexcept = default;
+
+    void closeDocument() noexcept override;
+    void openProperties() noexcept override;
+
+    QString getDocumentTabName() const noexcept override;
+    QString getDocumentTabToolTip() const noexcept override;
+    QIcon getDocumentTabIcon() const noexcept override;
+
+    AbstractDocument *getDocument() const noexcept override
+    {
+        return nullptr;
+    };
+};
+}
+
+#endif // VIVY_PROPERTY_DOCUMENT_VIEW_H
diff --git a/src/UI/VivyDocumentView.cc b/src/UI/VivyDocumentView.cc
index f686215d6aa2f4e3983d4afb1ff571dce2642071..91362576fa42d62df995bace7ca902f11423f2bc 100644
--- a/src/UI/VivyDocumentView.cc
+++ b/src/UI/VivyDocumentView.cc
@@ -1,18 +1,37 @@
 #include "VivyDocumentView.hh"
-#include "AudioVisualizer.hh"
-#include "../Document/VivyDocument.hh"
+#include "PropertyModel.hh"
+#include "DocumentViews/AudioVisualizer.hh"
+#include "../VivyApplication.hh"
+#include "../Lib/Document/VivyDocument.hh"
 
+#include <QHeaderView>
+#include <QTreeView>
 #include <QVBoxLayout>
 
 using namespace Vivy;
 
 VivyDocumentView::VivyDocumentView(std::shared_ptr<VivyDocument> doc, QWidget *parent) noexcept
-    : QWidget(parent)
+    : AbstractDocumentView(AbstractDocumentView::Type::Vivy, parent)
     , document(doc)
 {
     qDebug() << "Create view for document" << doc->getName() << "with capabilities"
              << doc->getDocumentCapabilitiesString();
+
     loadAudioView();
+    loadVideoView();
+    loadAssView();
+
+    setDockNestingEnabled(true);
+
+    // Add some actions...
+    QAction *openProperties = new QAction("Open properties", this);
+    connect(openProperties, &QAction::triggered, this, &VivyDocumentView::openProperties);
+    viewsActions.append(openProperties);
+
+    // The separator
+    QAction *separator = new QAction(this);
+    separator->setSeparator(true);
+    viewsActions.append(separator);
 }
 
 VivyDocumentView::~VivyDocumentView() noexcept
@@ -20,14 +39,13 @@ VivyDocumentView::~VivyDocumentView() noexcept
     qDebug()
         << "Deleting the document view: ref count on document" << document->getName() << "is"
         << document.use_count() << "and" << (visualizer ? "has visualizer" : "without visualizer");
-    if (visualizer)
-        delete visualizer;
+    closeDocument();
 }
 
-std::weak_ptr<VivyDocument>
+VivyDocument *
 VivyDocumentView::getDocument() const noexcept
 {
-    return document;
+    return document.get();
 }
 
 QString
@@ -46,6 +64,16 @@ VivyDocumentView::getDocumentTabToolTip() const noexcept
                                                                             : "");
 }
 
+void
+VivyDocumentView::loadVideoView() noexcept
+{
+}
+
+void
+VivyDocumentView::loadAssView() noexcept
+{
+}
+
 void
 VivyDocumentView::loadAudioView() noexcept
 {
@@ -60,19 +88,61 @@ VivyDocumentView::loadAudioView() noexcept
             return;
         }
 
-        visualizer = new AudioVisualizer(stream);
-        if (visualizer == nullptr) {
+        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;
         }
 
-        QVBoxLayout *layout = new QVBoxLayout(this);
-        layout->addWidget(visualizer);
-
-        setLayout(layout);
+        visualizer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding);
+        visualizer->setWidget(visualizerInner);
+        visualizer->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::TopDockWidgetArea);
+        visualizer->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable);
+        addDockWidget(Qt::LeftDockWidgetArea, visualizer, Qt::Horizontal);
     }
 
     else {
         qDebug() << "The document" << document->getName() << "is not AudioAble";
     }
 }
+
+void
+VivyDocumentView::closeDocument() noexcept
+{
+    qDebug() << "Closing the document:" << document->getName() << "( ref count is"
+             << document.use_count() << ")";
+    vivyApp->documentStore.closeDocument(document->getUuid());
+
+    // The visualizer pointer should have been deleted by the
+    // deleteAllContent() call if it was created.
+    deleteAllContent();
+    visualizer = nullptr;
+    property   = nullptr;
+}
+
+QIcon
+VivyDocumentView::getDocumentTabIcon() const noexcept
+{
+    return QIcon(":/icons/vivy.png");
+}
+
+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();
+    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 89788e56e748ea8d02f4ed2146eeda27f119a749..493b75e1112f8d4e2e259ef3f173448a5b4aab5b 100644
--- a/src/UI/VivyDocumentView.hh
+++ b/src/UI/VivyDocumentView.hh
@@ -5,29 +5,43 @@
 #error "This is a C++ header"
 #endif
 
-#include "../Document/VivyDocument.hh"
-#include "AudioVisualizer.hh"
+#include "../Lib/Document/VivyDocument.hh"
+#include "DocumentViews/AudioVisualizer.hh"
+#include "AbstractDocumentView.hh"
+#include "PropertyModel.hh"
 
 #include <QWidget>
+#include <QDockWidget>
 
 namespace Vivy
 {
-class VivyDocumentView final : public QWidget {
+class VivyDocumentView final : public AbstractDocumentView {
     Q_OBJECT
+    VIVY_UNMOVABLE_OBJECT(VivyDocumentView)
 
 public:
     explicit VivyDocumentView(std::shared_ptr<VivyDocument>, QWidget *parent = nullptr) noexcept;
     ~VivyDocumentView() noexcept;
 
-    std::weak_ptr<VivyDocument> getDocument() const noexcept;
-    QString getDocumentTabName() const noexcept;
-    QString getDocumentTabToolTip() const noexcept;
+    void closeDocument() noexcept override;
+    void openProperties() noexcept override;
 
-private:
-    std::shared_ptr<VivyDocument> document;
-    AudioVisualizer *visualizer{ nullptr };
+    QString getDocumentTabName() const noexcept override;
+    QString getDocumentTabToolTip() const noexcept override;
+    QIcon getDocumentTabIcon() const noexcept override;
+
+    VivyDocument *getDocument() const noexcept override;
 
+public slots:
     void loadAudioView() noexcept;
+    void loadVideoView() noexcept;
+    void loadAssView() noexcept;
+
+private:
+    std::shared_ptr<VivyDocument> document;
+    std::unique_ptr<PropertyModel> propertyModel;
+    QDockWidget *visualizer{ nullptr };
+    QDockWidget *property{ nullptr };
 };
 
 }
diff --git a/src/VivyApplication.hh b/src/VivyApplication.hh
index 4492f05473899d8e764ae4c2598cdd3520f8e4c3..579f9b78c273d7ba936d3e98b0960d702f529c66 100644
--- a/src/VivyApplication.hh
+++ b/src/VivyApplication.hh
@@ -9,7 +9,7 @@
 
 #include <QApplication>
 #include <QPixmap>
-#include "Document/VivyDocumentStore.hh"
+#include "Lib/Document/VivyDocumentStore.hh"
 #include <memory>
 
 namespace Vivy