From b5ded694228d10df7c91d8246b0dbc4b25e835c4 Mon Sep 17 00:00:00 2001
From: Tristan Walter <twalter@orn.mpg.de>
Date: Tue, 27 Oct 2020 17:21:06 +0100
Subject: [PATCH] * adding RAW video options to TRex opening dialog * find
 sphinx, add docs target

---
 Application/CMakeLists.txt                    |  24 +-
 Application/cmake/FindSphinx.cmake            |  14 +
 .../src/commons/common/cpputils/stringutils.h |   8 -
 Application/src/commons/common/file/Path.cpp  |  14 +-
 Application/src/commons/common/file/Path.h    |   2 +
 .../src/commons/common/gui/DrawStructure.cpp  |   5 +
 .../src/commons/common/gui/DrawStructure.h    |   5 +-
 .../src/commons/common/gui/FileChooser.cpp    | 231 +++++++++---
 .../src/commons/common/gui/FileChooser.h      |  49 ++-
 .../src/commons/common/gui/IMGUIBase.cpp      |   3 +
 .../src/commons/common/gui/IMGUIBase.h        |   8 +-
 .../commons/common/gui/types/Entangled.cpp    |   8 +-
 .../src/commons/common/video/Video.cpp        |   3 +-
 Application/src/commons/common/video/Video.h  |   3 +
 .../src/commons/common/video/VideoSource.cpp  |  67 +++-
 .../src/commons/common/video/VideoSource.h    |  12 +-
 Application/src/grabber/grabber.cpp           |  48 +--
 Application/src/grabber/main.cpp              |   2 +-
 Application/src/tracker/CMakeLists.txt        |   2 +
 Application/src/tracker/VideoOpener.cpp       | 338 +++++++++++++++++-
 Application/src/tracker/VideoOpener.h         |  69 +++-
 Application/src/tracker/gui/gui.cpp           |  32 +-
 Application/src/tracker/main.cpp              |  34 +-
 docs/parameters_trex.rst                      |   2 +-
 24 files changed, 808 insertions(+), 175 deletions(-)
 create mode 100644 Application/cmake/FindSphinx.cmake

diff --git a/Application/CMakeLists.txt b/Application/CMakeLists.txt
index 6bf4daa..2e10e98 100644
--- a/Application/CMakeLists.txt
+++ b/Application/CMakeLists.txt
@@ -25,6 +25,7 @@ set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "")
 
 #set(CMAKE_CXX_VISIBILITY_PRESET hidden)
 
+option(TREX_BUILD_DOCS OFF)
 option(TREX_WITH_TESTS OFF)
 option(TREX_BUILD_GLFW OFF)
 option(TREX_BUILD_PNG OFF)
@@ -963,6 +964,25 @@ foreach(dir ${dirs})
   message(STATUS "dir='${dir}'")
 endforeach()
 
-
-
 add_subdirectory(src)
+
+if(NOT WIN32 AND ${TREX_BUILD_DOCS})
+find_package(Sphinx)
+if(NOT Sphinx_FOUND)
+message(ERROR "Cannot find Sphinx executable")
+else()
+message(STATUS "Found sphinx.")
+
+add_custom_target(
+  doc ALL
+  COMMAND $<TARGET_FILE:TRex> -d "${CMAKE_SOURCE_DIR}/../docs" -h rst
+  COMMAND $<TARGET_FILE:TGrabs> -d "${CMAKE_SOURCE_DIR}/../docs" -h rst
+  COMMAND make html
+  DEPENDS
+    TRex
+  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/../docs
+  COMMENT "Generating API documentation with Doxygen"
+  VERBATIM
+)
+endif()
+endif()
diff --git a/Application/cmake/FindSphinx.cmake b/Application/cmake/FindSphinx.cmake
new file mode 100644
index 0000000..a2561fb
--- /dev/null
+++ b/Application/cmake/FindSphinx.cmake
@@ -0,0 +1,14 @@
+find_program(SPHINX_EXECUTABLE NAMES sphinx-build
+    HINTS
+    $ENV{SPHINX_DIR}
+    PATH_SUFFIXES bin
+    DOC "Sphinx documentation generator"
+)
+ 
+include(FindPackageHandleStandardArgs)
+ 
+find_package_handle_standard_args(Sphinx DEFAULT_MSG
+    SPHINX_EXECUTABLE
+)
+ 
+mark_as_advanced(SPHINX_EXECUTABLE)
diff --git a/Application/src/commons/common/cpputils/stringutils.h b/Application/src/commons/common/cpputils/stringutils.h
index 732c264..d8c32aa 100644
--- a/Application/src/commons/common/cpputils/stringutils.h
+++ b/Application/src/commons/common/cpputils/stringutils.h
@@ -1,14 +1,6 @@
 #ifndef _STRINGUTILS_H
 #define _STRINGUTILS_H
 
-#ifdef _WIN32
-#include <direct.h>
-#define GetCurrentDir _getcwd
-#else
-#include <unistd.h>
-#define GetCurrentDir getcwd
-#endif
-
 #include <string>
 #include <vector>
 #include <cctype>
diff --git a/Application/src/commons/common/file/Path.cpp b/Application/src/commons/common/file/Path.cpp
index 7468646..7cb43e2 100644
--- a/Application/src/commons/common/file/Path.cpp
+++ b/Application/src/commons/common/file/Path.cpp
@@ -234,6 +234,18 @@ namespace file {
         
         return rmdir(str().c_str()) == 0;
     }
+        
+        bool valid_extension(const file::Path& path, const std::string& filter_extension) {
+            if(filter_extension.empty())
+                return true;
+            
+            auto extensions = utils::split(utils::lowercase(filter_extension), ';');
+            if(path.has_extension()) {
+                return contains(extensions, path.extension().to_string());
+            }
+            
+            return false;
+        }
     
     std::set<Path> Path::find_files(const std::string& filter_extension) const {
         if(!is_folder())
@@ -250,7 +262,7 @@ namespace file {
                 if(file == "." || file == "..")
                     continue;
                 
-                if(ent->d_type & DT_DIR || filter_extension.empty() || utils::endsWith(utils::lowercase(ent->d_name), filter_extension))
+                if(ent->d_type & DT_DIR || valid_extension(file::Path(ent->d_name), filter_extension))
                     result.insert(*this / ent->d_name);
             }
             closedir (dir);
diff --git a/Application/src/commons/common/file/Path.h b/Application/src/commons/common/file/Path.h
index 0b1d04d..c488d03 100644
--- a/Application/src/commons/common/file/Path.h
+++ b/Application/src/commons/common/file/Path.h
@@ -87,6 +87,8 @@ namespace file {
     bool operator!=(const Path& lhs, const Path& rhs);
     
     std::string exec(const char* cmd);
+
+    bool valid_extension(const file::Path&, const std::string& filter_extension);
 }
 
 std::ostream& operator<<(std::ostream& os, const file::Path& p);
diff --git a/Application/src/commons/common/gui/DrawStructure.cpp b/Application/src/commons/common/gui/DrawStructure.cpp
index 16590c7..8f8b0ab 100644
--- a/Application/src/commons/common/gui/DrawStructure.cpp
+++ b/Application/src/commons/common/gui/DrawStructure.cpp
@@ -719,4 +719,9 @@ namespace gui {
         else
             _root.insert_cache(base, std::make_shared<CacheObject>());
     }
+
+    void DrawStructure::set_size(const Size2& size) {
+        _width = size.width;
+        _height = size.height;
+    }
 }
diff --git a/Application/src/commons/common/gui/DrawStructure.h b/Application/src/commons/common/gui/DrawStructure.h
index 4032326..0695188 100644
--- a/Application/src/commons/common/gui/DrawStructure.h
+++ b/Application/src/commons/common/gui/DrawStructure.h
@@ -88,8 +88,8 @@ namespace gui {
         GETTER_PTR(Drawable*, hovered_object)
         GETTER_PTR(Drawable*, selected_object)
         
-        GETTER_CONST(const uint16_t, width)
-        GETTER_CONST(const uint16_t, height)
+        GETTER(uint16_t, width)
+        GETTER(uint16_t, height)
         GETTER(Vec2, scale)
         GETTER(std::atomic_bool, changed)
         GETTER_SETTER(Vec2, dialog_window_size)
@@ -123,6 +123,7 @@ namespace gui {
         }
         
         void draw_log_messages();
+        void set_size(const Size2& size);
         
         std::vector<Drawable*> collect();
         bool is_key_pressed(Codes code) const;
diff --git a/Application/src/commons/common/gui/FileChooser.cpp b/Application/src/commons/common/gui/FileChooser.cpp
index 787e185..4508f86 100644
--- a/Application/src/commons/common/gui/FileChooser.cpp
+++ b/Application/src/commons/common/gui/FileChooser.cpp
@@ -4,18 +4,15 @@
 
 namespace gui {
 
-FileChooser::FileChooser(const file::Path& start,
-                         const std::string& filter_extension,
-                         std::function<void(const file::Path&)> callback,
-                         std::function<void(const file::Path&)> on_select_callback,
-                         derived_ptr<Entangled> extra)
+FileChooser::FileChooser(const file::Path& start, const std::string& extension,
+                         std::function<void(const file::Path&, std::string)> callback,
+                         std::function<void(const file::Path&, std::string)> on_select_callback)
 :
-    _graph(1300, 750),
+    _graph(std::make_unique<DrawStructure>(1300, 820)),
     _description(std::make_shared<Text>("Please choose a file in order to continue.", Vec2(10, 10), White, Font(0.75))),
-    _extra(extra),
     _columns(std::make_shared<HorizontalLayout>()),
     _overall(std::make_shared<VerticalLayout>()),
-    _base("Choose file", _graph, [this](){
+    _base("Choose file", *_graph, [this](){
         using namespace gui;
         
         {
@@ -29,25 +26,36 @@ FileChooser::FileChooser(const file::Path& start,
         if(!_list)
             return false;
         
-        //_graph.wrap_object(*_textfield);
-        //_graph.wrap_object(*_list);
-        _graph.wrap_object(*_overall);
+        //_graph->wrap_object(*_textfield);
+        //_graph->wrap_object(*_list);
+        _graph->wrap_object(*_overall);
+        if(_on_update)
+            _on_update(*_graph);
         
         if(!_selected_file.empty()) {
 
-            //auto text = _graph.text("Selected: "+_selected_file.str(), _list->bounds().pos() + Vec2(0, _list->bounds().height + 10), White, Font(0.6));
+            //auto text = _graph->text("Selected: "+_selected_file.str(), _list->bounds().pos() + Vec2(0, _list->bounds().height + 10), White, Font(0.6));
             //_button->set_pos(text->pos() + Vec2(0, text->height() + 10));
-            //_graph.wrap_object(*_button);
+            //_graph->wrap_object(*_button);
         }
         if (SETTING(terminate))
             _running = false;
 
         return _running;
-    }, [](gui::Event e) {
+    }, [this](gui::Event e) {
         // --
+        if(e.type == gui::WINDOW_RESIZED) {
+            using namespace gui;
+            {
+                std::lock_guard guard(_graph->lock());
+                Size2 size(e.size.width, e.size.height);
+                auto scale = _base.dpi_scale();
+                _graph->set_size(size.div(scale));
+            }
+            update_size();
+        }
     }),
     _path(start),
-    _filter(filter_extension),
     _running(true),
     _files([](const file::Path& A, const file::Path& B) -> bool {
         return (A.is_folder() && !B.is_folder()) || (A.is_folder() == B.is_folder() && A.str() < B.str()); //A.str() == ".." || (A.str() != ".." && ((A.is_folder() && !B.is_folder()) || (A.is_folder() == B.is_folder() && A.str() < B.str())));
@@ -55,16 +63,20 @@ FileChooser::FileChooser(const file::Path& start,
     _callback(callback),
     _on_select_callback(on_select_callback)
 {
+    _default_tab.extension = extension;
+    set_tab("");
+    
     _base.set_open_files_fn([this](const std::vector<file::Path>& paths) -> bool{
         if(paths.size() != 1)
             return false;
         
         auto path = paths.front();
-        if(path.exists() || path.str() == "/" || path.add_extension("pv").exists()) {
+        if(!_validity || _validity(path)) //path.exists() || path.str() == "/" || path.add_extension("pv").exists())
+        {
             file_selected(0, path.str());
             return true;
         } else {
-            Error("Path '%S' cannot be found.", &path.str());
+            Error("Path '%S' cannot be opened.", &path.str());
         }
         return false;
     });
@@ -74,12 +86,7 @@ FileChooser::FileChooser(const file::Path& start,
     _overall->set_policy(VerticalLayout::CENTER);
     //_overall->set_background(Transparent, Blue);
     
-    if(_extra) {
-        _extra->auto_size(Margin{0,0});
-        _extra->set_name("Extra");
-    }
-    
-    _list = std::make_shared<ScrollableList<FileItem>>(Bounds(0, 0, _graph.width() - 20 - (_extra ? _extra->width() + 5 : 0), _graph.height() - 70 - 10 - 70));
+    _list = std::make_shared<ScrollableList<FileItem>>(Bounds(0, 0, _graph->width() - 20 - (_current_tab.content ? _current_tab.content->width() + 5 : 0), _graph->height() - 70 - 10 - 70));
     _list->set_stays_toggled(true);
     //if(_extra)
     //    _extra->set_background(Transparent, Green);
@@ -93,10 +100,12 @@ FileChooser::FileChooser(const file::Path& start,
     _textfield->on_enter([&](){
         auto path = file::Path(_textfield->text());
         
-        if(path.exists() || path.str() == "/" || path.add_extension("pv").exists()) {
+        //if(path.exists() || path.str() == "/" || path.add_extension("pv").exists())
+        if(!_validity || _validity(path))
+        {
             file_selected(0, path.str());
         } else {
-            Error("Path '%S' cannot be found.", &path.str());
+            Error("Path '%S' cannot be opened.", &path.str());
         }
         
     });
@@ -109,8 +118,8 @@ FileChooser::FileChooser(const file::Path& start,
     _columns->set_name("Columns");
     _rows->set_name("Rows");
     
-    if(_extra)
-        _columns->set_children({_rows, _extra});
+    if(_current_tab.content)
+        _columns->set_children({_rows, _current_tab.content});
     else
         _columns->set_children({_rows});
     
@@ -120,21 +129,29 @@ FileChooser::FileChooser(const file::Path& start,
     
     if(!_path.exists())
         _path = ".";
-    auto files = _path.find_files(_filter);
-    _files.clear();
-    _files.insert(files.begin(), files.end());
-    _files.insert("..");
+    
+    try {
+        auto files = _path.find_files(_current_tab.extension);
+        _files.clear();
+        _files.insert(files.begin(), files.end());
+        _files.insert("..");
+        
+    } catch(const UtilsException& ex) {
+        Error("Cannot list folder '%S'.", &_path);
+    }
     
     update_names();
     _textfield->set_text(_path.str());
     
-    _graph.set_scale(_base.dpi_scale() * gui::interface_scale());
+    _graph->set_scale(_base.dpi_scale() * gui::interface_scale());
     _list->on_select([this](auto i, auto&path){ file_selected(i, path.path()); });
     
     _button->set_font(gui::Font(0.6, Align::Center));
     _button->on_click([this](auto){
         _running = false;
         _confirmed_file = _selected_file;
+        if(_on_open)
+            _on_open(_confirmed_file);
     });
     
     _list->set_font(gui::Font(0.6, gui::Align::Left));
@@ -146,6 +163,87 @@ FileChooser::FileChooser(const file::Path& start,
     });
 }
 
+void FileChooser::set_on_update(std::function<void (DrawStructure &)> && fn) {
+    _on_update = std::move(fn);
+}
+
+void FileChooser::set_tabs(const std::vector<Settings>& tabs) {
+    _tabs.clear();
+    tabs_elements.clear();
+    
+    for(auto tab : tabs) {
+        if(tab.extension == "")
+            tab.extension = _default_tab.extension;
+        _tabs[tab.name] = tab;
+        
+        auto button = new Button(tab.name, Bounds(0, 0, Base::default_text_bounds(tab.name).width + 20, 33));
+        button->set_toggleable(true);
+        button->on_click([this, button](auto e){
+            if(button->toggled()) {
+                set_tab(button->txt());
+            }
+        });
+        auto ptr = std::shared_ptr<Drawable>(button);
+        tabs_elements.push_back(ptr);
+    }
+    
+    if(_tabs.size() > 1) {
+        _tabs_bar = std::make_shared<HorizontalLayout>(tabs_elements);
+    } else {
+        _tabs_bar = nullptr;
+    }
+    
+    if(_tabs_bar) {
+        _rows->set_children(std::vector<Layout::Ptr>{
+            _description, _textfield, _list
+        });
+    } else {
+        _rows->set_children(std::vector<Layout::Ptr>{
+            _description, _textfield, _list
+        });
+    }
+    
+    if(!_tabs.empty())
+        set_tab(tabs.front().name);
+    else
+        set_tab("");
+}
+
+void FileChooser::set_tab(std::string tab) {
+    if(tab != _current_tab.name) {
+        
+    } else
+        return;
+    
+    if(tab.empty()) {
+        _current_tab = _default_tab;
+    } else if(!_tabs.count(tab)) {
+        auto str = Meta::toStr(_tabs);
+        Except("FileChooser %S does not contain tab '%S'.", &str, &tab);
+    } else {
+        _current_tab = _tabs.at(tab);
+    }
+    
+    for(auto &ptr : tabs_elements) {
+        if(static_cast<Button*>(ptr.get())->txt() != tab) {
+            static_cast<Button*>(ptr.get())->set_toggle(false);
+            static_cast<Button*>(ptr.get())->set_clickable(true);
+        } else
+            static_cast<Button*>(ptr.get())->set_clickable(false);
+    }
+    
+    change_folder(_path);
+    if(!_selected_file.empty())
+        file_selected(0, _selected_file);
+    
+    if(_current_tab.content) {
+        _current_tab.content->auto_size(Margin{0,0});
+        _current_tab.content->set_name("Extra");
+    }
+    
+    _graph->set_dirty(&_base);
+}
+
 void FileChooser::update_names() {
     _names.clear();
     for(auto &f : _files) {
@@ -174,16 +272,18 @@ Color FileChooser::FileItem::text_color() const {
 
 void FileChooser::open() {
     _base.loop();
-    _callback(_confirmed_file);
+    if(_callback)
+        _callback(_confirmed_file, _current_tab.name);
 }
 
-void FileChooser::file_selected(size_t, file::Path p) {
+void FileChooser::change_folder(const file::Path& p) {
     auto org = _path;
+    auto copy = _files;
     
     if(p.str() == "..") {
         try {
             _path = _path.remove_filename();
-            auto files = _path.find_files(_filter);
+            auto files = _path.find_files(_current_tab.extension);
             _files.clear();
             _files.insert(files.begin(), files.end());
             _files.insert("..");
@@ -193,16 +293,14 @@ void FileChooser::file_selected(size_t, file::Path p) {
             
         } catch(const UtilsException&e) {
             _path = org;
-            auto files = _path.find_files(_filter);
-            _files.clear();
-            _files.insert(files.begin(), files.end());
-            _files.insert("..");
+            _files = copy;
         }
+        update_names();
         
     } else if(p.is_folder()) {
         try {
             _path = p;
-            auto files = _path.find_files(_filter);
+            auto files = _path.find_files(_current_tab.extension);
             _files.clear();
             _files.insert(files.begin(), files.end());
             _files.insert("..");
@@ -212,29 +310,40 @@ void FileChooser::file_selected(size_t, file::Path p) {
             
         } catch(const UtilsException&e) {
             _path = org;
-            auto files = _path.find_files(_filter);
-            _files.clear();
-            _files.insert(files.begin(), files.end());
-            _files.insert("..");
+            _files = copy;
         }
-        
+        update_names();
     }
-    else {
-        _selected_file = p.remove_extension();
+}
+
+void FileChooser::file_selected(size_t, file::Path p) {
+    if(p.str() == ".." || p.is_folder()) {
+        change_folder(p);
+    } else {
+        _selected_file = p;
         if(!_selected_text)
             _selected_text = std::make_shared<StaticText>("Selected: "+_selected_file.str(), Vec2(), Vec2(700, 0), Font(0.6));
         else
             _selected_text->set_txt("Selected: "+_selected_file.str());
         
-        _overall->set_children({
-            _columns,
-            _selected_text,
-            _button
-        });
+        if(_tabs_bar)
+            _overall->set_children({
+                _tabs_bar,
+                _columns,
+                _selected_text,
+                _button
+            });
+        else
+            _overall->set_children({
+                _columns,
+                _selected_text,
+                _button
+            });
         _overall->update_layout();
         //update_size();
         
-        _on_select_callback(_selected_file);
+        if(_on_select_callback)
+            _on_select_callback(_selected_file, _current_tab.extension);
         update_size();
     }
     
@@ -242,12 +351,15 @@ void FileChooser::file_selected(size_t, file::Path p) {
 }
 
 void FileChooser::update_size() {
-    float left_column_width = _graph.width() - 20 - (_extra && _extra->width() > 20 ? _extra->width() + 10 : 0) - 10;
+    float left_column_width = _graph->width() - 20 - (_current_tab.content && _current_tab.content->width() > 20 ? _current_tab.content->width() + 10 : 0) - 10;
     if(_selected_text) {
-        _selected_text->set_max_size(Size2(left_column_width));
+        _selected_text->set_max_size(Size2(_graph->width() - 20));
     }
     
-    float left_column_height = _graph.height() - 70 - 10 - (_overall->children().size() > 1 ? _button->height() + 10 : 0);
+    if(_tabs_bar) _tabs_bar->auto_size(Margin{0,0});
+    if(_tabs_bar) _tabs_bar->update_layout();
+    
+    float left_column_height = _graph->height() - 70 - 10 - (_overall->children().size() > 1 ? _button->height() + 10 : 0) - (_tabs_bar ? _tabs_bar->height() + 10 : 0) - (_selected_text && !_selected_file.empty() ? _selected_text->height() : 0);
     _list->set_bounds(Bounds(0, 0, left_column_width, left_column_height - 70));
     
     _textfield->set_bounds(Bounds(0, 0, left_column_width, 30));
@@ -256,7 +368,12 @@ void FileChooser::update_size() {
     if(_rows) _rows->auto_size(Margin{0,0});
     if(_rows) _rows->update_layout();
     
-    if(_extra) _extra->auto_size(Margin{0,0});
+    if(_current_tab.content)
+        _columns->set_children({_rows, _current_tab.content});
+    else
+        _columns->set_children({_rows});
+    
+    if(_current_tab.content) _current_tab.content->auto_size(Margin{0,0});
     _columns->auto_size(Margin{0,0});
     _columns->update_layout();
     
diff --git a/Application/src/commons/common/gui/FileChooser.h b/Application/src/commons/common/gui/FileChooser.h
index 9b2b976..94ac55f 100644
--- a/Application/src/commons/common/gui/FileChooser.h
+++ b/Application/src/commons/common/gui/FileChooser.h
@@ -22,39 +22,74 @@ class FileChooser {
         operator std::string() const override;
     };
     
-    DrawStructure _graph;
+public:
+    struct Settings {
+        std::string name;
+        std::string extension;
+        derived_ptr<Entangled> content;
+        
+        bool is_valid_extension(const file::Path& path) const {
+            return file::valid_extension(path, extension);
+        }
+        
+        std::string toStr() const {
+            return name;
+        }
+        
+        static std::string class_name() {
+            return "FileChooser::Settings";
+        }
+    };
+    
+protected:
+    GETTER_NCONST(std::unique_ptr<DrawStructure>, graph)
     derived_ptr<Text> _description;
     derived_ptr<StaticText> _selected_text;
-    derived_ptr<Entangled> _extra;
     derived_ptr<ScrollableList<FileItem>> _list;
-    derived_ptr <Button> _button;
-    derived_ptr <Textfield> _textfield;
+    derived_ptr<Button> _button;
+    derived_ptr<Textfield> _textfield;
     derived_ptr<VerticalLayout> _rows;
     derived_ptr<HorizontalLayout> _columns;
     derived_ptr<VerticalLayout> _overall;
+    derived_ptr<HorizontalLayout> _tabs_bar;
+    std::vector<Layout::Ptr> tabs_elements;
     IMGUIBase _base;
     std::vector<FileItem> _names;
     
     file::Path _path;
-    std::string _filter;
     bool _running;
     
     std::set<file::Path, std::function<bool(const file::Path&, const file::Path&)>> _files;
     file::Path _selected_file, _confirmed_file;
-    std::function<void(const file::Path&)> _callback, _on_select_callback;
+    std::function<void(const file::Path&, std::string)> _callback, _on_select_callback;
+    std::function<void(DrawStructure&)> _on_update;
+    std::function<bool(file::Path)> _validity;
+    std::function<void(file::Path)> _on_open;
     std::queue<std::function<void()>> _execute;
     std::mutex _execute_mutex;
+    std::map<std::string, Settings> _tabs;
+    GETTER(Settings, current_tab)
+    Settings _default_tab;
     
 public:
-    FileChooser(const file::Path& start, const std::string& filter_extension, std::function<void(const file::Path&)> callback, std::function<void(const file::Path&)> on_select_callback= [](auto&){}, derived_ptr<Entangled> extra = nullptr);
+    FileChooser(const file::Path& start, const std::string& extension,
+                std::function<void(const file::Path&, std::string)> callback,
+                std::function<void(const file::Path&, std::string)> on_select_callback = nullptr);
     
+    void set_tabs(const std::vector<Settings>&);
+    void set_tab(std::string);
     void open();
     void execute(std::function<void()>&&);
     void update_size();
+    void set_on_update(std::function<void(DrawStructure&)>&&);
+    void set_on_open(std::function<void(file::Path)>&& fn) { _on_open = std::move(fn); }
+    void set_validity_check(std::function<bool(file::Path)>&& fn) { _validity = std::move(fn); }
     
 private:
     void file_selected(size_t i, file::Path path);
     void update_names();
+    void update_tabs();
+    void change_folder(const file::Path&);
 };
 
 }
diff --git a/Application/src/commons/common/gui/IMGUIBase.cpp b/Application/src/commons/common/gui/IMGUIBase.cpp
index 8af45b0..07825d3 100644
--- a/Application/src/commons/common/gui/IMGUIBase.cpp
+++ b/Application/src/commons/common/gui/IMGUIBase.cpp
@@ -1007,6 +1007,9 @@ void IMGUIBase::draw_element(const DrawOrder& order) {
                 //_all_textures[this].insert(tex_cache);
             }
             
+            if(!static_cast<ExternalImage*>(o)->source())
+                break;
+            
             auto image_size = static_cast<ExternalImage*>(o)->source()->bounds().size();
             if(image_size.empty())
                 break;
diff --git a/Application/src/commons/common/gui/IMGUIBase.h b/Application/src/commons/common/gui/IMGUIBase.h
index 3f6353e..c577051 100644
--- a/Application/src/commons/common/gui/IMGUIBase.h
+++ b/Application/src/commons/common/gui/IMGUIBase.h
@@ -66,8 +66,10 @@ namespace gui {
         
     public:
         template<typename impl_t = default_impl_t>
-        IMGUIBase(std::string title, DrawStructure& base, CrossPlatform::custom_function_t custom_loop, std::function<void(const gui::Event&)> event_fn) : _graph(&base), _custom_loop(custom_loop), _event_fn(event_fn)
+        IMGUIBase(std::string title, DrawStructure& base, CrossPlatform::custom_function_t custom_loop, std::function<void(const gui::Event&)> event_fn) : _custom_loop(custom_loop), _event_fn(event_fn)
         {
+            set_graph(base);
+            
             auto ptr = new impl_t([this](){
                 if(_graph == NULL)
                     return;
@@ -97,6 +99,10 @@ namespace gui {
             init(title);
         }
         
+        void set_graph(DrawStructure& base) {
+            _graph = &base;
+            
+        }
         void init(const std::string& title);
         ~IMGUIBase();
         
diff --git a/Application/src/commons/common/gui/types/Entangled.cpp b/Application/src/commons/common/gui/types/Entangled.cpp
index 8cf5fbb..0c85584 100644
--- a/Application/src/commons/common/gui/types/Entangled.cpp
+++ b/Application/src/commons/common/gui/types/Entangled.cpp
@@ -188,8 +188,8 @@ namespace gui {
         
         if(c) {
 #ifndef NDEBUG
-            if(!Drawable::name().empty())
-                Debug("Changed '%S' content (%d children, %f width).", &Drawable::name(), _children.size(), width());
+//            if(!Drawable::name().empty())
+//                Debug("Changed '%S' content (%d children, %f width).", &Drawable::name(), _children.size(), width());
 #endif
             /*SectionInterface* p = this;
             while((p = p->parent()) != nullptr) {
@@ -212,10 +212,6 @@ namespace gui {
     
     void Entangled::before_draw() {
         _content_changed_while_updating = false;
-#ifndef NDEBUG
-        if(!Drawable::name().empty() && _content_changed)
-            Debug("Calling before draw: '%S' (%d).", &Drawable::name(), _content_changed ? 1 : 0);
-#endif
         update();
         
         for(auto c : _children) {
diff --git a/Application/src/commons/common/video/Video.cpp b/Application/src/commons/common/video/Video.cpp
index 28d322f..9943a80 100644
--- a/Application/src/commons/common/video/Video.cpp
+++ b/Application/src/commons/common/video/Video.cpp
@@ -169,7 +169,9 @@ void Video::frame(long_t index, cv::Mat& frame, bool lazy) {
         //Warning("Have to reset video index from %d to %d (%S)", _last_index, index, &_filename);
         _cap->set(cv::CAP_PROP_POS_FRAMES, index);
     } else if(index > _last_index+1) {
+#ifndef NDEBUG
         Warning("Have to skip from video index from %d to %d (%S)", _last_index, index-1, &_filename);
+#endif
         for(; _last_index+1 < index; _last_index++)
             _cap->grab();
     }
@@ -177,7 +179,6 @@ void Video::frame(long_t index, cv::Mat& frame, bool lazy) {
     _last_index = index;
     
     // Read requested frame
-    static cv::Mat read;
     if(!_cap->read(read))
         U_EXCEPTION("Cannot read frame %d of video '%S'.", index, &_filename);
 #endif
diff --git a/Application/src/commons/common/video/Video.h b/Application/src/commons/common/video/Video.h
index 91a00c5..7611f06 100644
--- a/Application/src/commons/common/video/Video.h
+++ b/Application/src/commons/common/video/Video.h
@@ -206,6 +206,9 @@ private:
      */
     cv::Size _size;
     
+    //! Temporary read cache
+    cv::Mat read;
+    
     /**
      * Calculates maps using OpenCVs initUndistortRectifyMap method, which are later
      * used to undistort frames of this video.
diff --git a/Application/src/commons/common/video/VideoSource.cpp b/Application/src/commons/common/video/VideoSource.cpp
index 1a4fad1..062da96 100644
--- a/Application/src/commons/common/video/VideoSource.cpp
+++ b/Application/src/commons/common/video/VideoSource.cpp
@@ -1,6 +1,6 @@
 #include "VideoSource.h"
 #include "Video.h"
-#include <iomanip>
+#include <regex>
 #include <file/Path.h>
 #include <misc/GlobalSettings.h>
 #include <misc/ThreadPool.h>
@@ -207,7 +207,54 @@ VideoSource::~VideoSource() {
     }
 }
 
-VideoSource::VideoSource(const std::vector<file::Path>& files) : _last_file(NULL), _length(0), _has_timestamps(false), _framerate(-1)
+VideoSource::VideoSource(const std::string& source) {
+    std::smatch m;
+    std::regex rplaceholder ("%[0-9]+(\\.[0-9]+(.[1-9][0-9]*)?)?d$"), rext(".*(\\..+)$");
+    
+    long_t number_length = -1, start_number = 0, end_number = VIDEO_SEQUENCE_UNSPECIFIED_VALUE;
+    
+    std::string base_name, extension;
+    if(std::regex_search(source,m,rext)) {
+        auto x = m[1];
+        extension = x.str().substr(1);
+        base_name = source.substr(0, m.position(1));
+        
+        Debug("Extension '%S' basename '%S'", &extension, &base_name);
+        
+    } else {
+        U_EXCEPTION("File extension not found in '%S'", &source);
+    }
+    
+    if(std::regex_search (base_name,m,rplaceholder)) {
+        auto x = m[0];
+        
+        std::string s = x.str();
+        auto p = m.position();
+        
+        s = s.substr(1, s.length()-2);
+        auto split = utils::split(s, '.');
+        
+        if(split.size()>1) {
+            start_number = std::stoi(split[1]);
+        }
+        if(split.size()>2) {
+            end_number = std::stoi(split[2]);
+        }
+        
+        number_length = std::stoi(split[0]);
+        base_name = base_name.substr(0, p);
+        Debug("match '%S' at %d with nr %d", &s, p, number_length);
+    }
+    
+    if(number_length != -1) {
+        // no placeholders found, just load file.
+        open(base_name, extension, start_number, end_number, number_length);
+    } else {
+        open(base_name, extension);
+    }
+}
+
+VideoSource::VideoSource(const std::vector<file::Path>& files)
 {
     for(auto &path : files) {
         auto extension = path.extension().to_string();
@@ -229,8 +276,9 @@ VideoSource::VideoSource(const std::vector<file::Path>& files) : _last_file(NULL
     _has_timestamps = _files_in_seq.front()->has_timestamps();
 }
 
-VideoSource::VideoSource(const std::string& basename, const std::string& extension, int seq_start, int seq_end, int padding) : _last_file(NULL), _length(0), _has_timestamps(false), _framerate(-1) {
-    
+VideoSource::VideoSource() {}
+void VideoSource::open(const std::string& basename, const std::string& extension, int seq_start, int seq_end, int padding)
+{
     if (seq_start == VIDEO_SEQUENCE_INVALID_VALUE || seq_end == VIDEO_SEQUENCE_INVALID_VALUE) {
         File *f = File::open(0, basename, extension);
 
@@ -288,8 +336,8 @@ VideoSource::VideoSource(const std::string& basename, const std::string& extensi
             ss << basename << std::setfill('0') << std::setw(padding) << i;
             
             File *f = File::open(i-seq_start, ss.str(), extension, i != seq_start);
-			if(!f)
-				U_EXCEPTION("Cannot find file '%s.%S' in sequence %d-%d.", ss.str().c_str(), &extension, seq_start, seq_end);
+            if(!f)
+                U_EXCEPTION("Cannot find file '%s.%S' in sequence %d-%d.", ss.str().c_str(), &extension, seq_start, seq_end);
             _files_in_seq.push_back(f);
             
             _length += f->length();
@@ -305,9 +353,9 @@ VideoSource::VideoSource(const std::string& basename, const std::string& extensi
             U_EXCEPTION("Provided an empty video sequence for video source '%S'.", &basename);
     }
     
-	if(_files_in_seq.empty())
-		U_EXCEPTION("Cannot load video sequence '%S' (it is empty).", &basename);
-	
+    if(_files_in_seq.empty())
+        U_EXCEPTION("Cannot load video sequence '%S' (it is empty).", &basename);
+    
     _size = _files_in_seq.at(0)->resolution();
     _has_timestamps = _files_in_seq.front()->has_timestamps();
     
@@ -336,6 +384,7 @@ VideoSource::VideoSource(const std::string& basename, const std::string& extensi
 }
 
 void VideoSource::frame(uint64_t globalIndex, gpuMat& output) {
+    U_EXCEPTION("Using empty function.");
 }
 
 void VideoSource::frame(uint64_t globalIndex, cv::Mat& output) {
diff --git a/Application/src/commons/common/video/VideoSource.h b/Application/src/commons/common/video/VideoSource.h
index 680adb5..bfbd442 100644
--- a/Application/src/commons/common/video/VideoSource.h
+++ b/Application/src/commons/common/video/VideoSource.h
@@ -67,21 +67,23 @@ private:
      */
     std::vector<File*> _files_in_seq;
     
-    File* _last_file;
+    File* _last_file = nullptr;
     cv::Size _size;
-    uint64_t _length;
+    uint64_t _length = 0;
     cv::Mat _average;
     cv::Mat _mask;
-    bool _has_timestamps;
-    short _framerate;
+    bool _has_timestamps = false;
+    short _framerate = -1;
     
 public:
     /**
      * Automatically load a range of files with a certain extension called
      * {basename}{seq_start<=number<=seq_end}.{extension}
      */
-    VideoSource(const std::string& basename, const std::string& extension, int seq_start = VIDEO_SEQUENCE_INVALID_VALUE, int seq_end = VIDEO_SEQUENCE_INVALID_VALUE, int padding = 4);
+    VideoSource();
+    VideoSource(const std::string& source);
     VideoSource(const std::vector<file::Path>& files);
+    void open(const std::string& basename, const std::string& extension, int seq_start = VIDEO_SEQUENCE_INVALID_VALUE, int seq_end = VIDEO_SEQUENCE_INVALID_VALUE, int padding = 4);
     
     ~VideoSource();
     
diff --git a/Application/src/grabber/grabber.cpp b/Application/src/grabber/grabber.cpp
index 84377f7..12ec77c 100644
--- a/Application/src/grabber/grabber.cpp
+++ b/Application/src/grabber/grabber.cpp
@@ -1,4 +1,4 @@
-#include <regex>
+
 #include <cstdio>
 
 #include "grabber.h"
@@ -701,51 +701,7 @@ void FrameGrabber::initialize_video() {
     }
     
     if(filenames.size() == 1) {
-        std::string source = filenames.front().str();
-        std::smatch m;
-        std::regex rplaceholder ("%[0-9]+(\\.[0-9]+(.[1-9][0-9]*)?)?d$"), rext(".*(\\..+)$");
-        
-        long_t number_length = -1, start_number = 0, end_number = VIDEO_SEQUENCE_UNSPECIFIED_VALUE;
-        
-        std::string base_name, extension;
-        if(std::regex_search(source,m,rext)) {
-            auto x = m[1];
-            extension = x.str().substr(1);
-            base_name = source.substr(0, m.position(1));
-            
-            Debug("Extension '%S' basename '%S'", &extension, &base_name);
-            
-        } else {
-            U_EXCEPTION("File extension not found in '%S'", &source);
-        }
-        
-        if(std::regex_search (base_name,m,rplaceholder)) {
-            auto x = m[0];
-            
-            std::string s = x.str();
-            auto p = m.position();
-            
-            s = s.substr(1, s.length()-2);
-            auto split = utils::split(s, '.');
-            
-            if(split.size()>1) {
-                start_number = std::stoi(split[1]);
-            }
-            if(split.size()>2) {
-                end_number = std::stoi(split[2]);
-            }
-            
-            number_length = std::stoi(split[0]);
-            base_name = base_name.substr(0, p);
-            Debug("match '%S' at %d with nr %d", &s, p, number_length);
-        }
-        
-        if(number_length != -1) {
-            // no placeholders found, just load file.
-            _video = new VideoSource(base_name, extension, start_number, end_number, number_length);
-        } else {
-            _video = new VideoSource(base_name, extension);
-        }
+        _video = new VideoSource(filenames.front());
         
     } else {
         _video = new VideoSource(filenames);
diff --git a/Application/src/grabber/main.cpp b/Application/src/grabber/main.cpp
index ed681fe..d15b35d 100644
--- a/Application/src/grabber/main.cpp
+++ b/Application/src/grabber/main.cpp
@@ -452,7 +452,7 @@ int main(int argc, char** argv)
                             fwrite(rst.data(), sizeof(char), rst.length(), f);
                             fclose(f);
                             
-                            printf("%s\n", rst.c_str());
+                            //printf("%s\n", rst.c_str());
                             Debug("Saved at '%S'.", &path.str());
                             
                             exit(0);
diff --git a/Application/src/tracker/CMakeLists.txt b/Application/src/tracker/CMakeLists.txt
index 432e05f..46d74d8 100644
--- a/Application/src/tracker/CMakeLists.txt
+++ b/Application/src/tracker/CMakeLists.txt
@@ -20,6 +20,8 @@ file(GLOB SRCS
     main.cpp
     VideoOpener.cpp
     VideoOpener.h
+    ../grabber/default_config.cpp
+    ../grabber/default_config.h
     gfx/TRexIcon16.png
     gfx/TRexIcon32.png
     gfx/TRexIcon64.png
diff --git a/Application/src/tracker/VideoOpener.cpp b/Application/src/tracker/VideoOpener.cpp
index ef2a732..b050c2b 100644
--- a/Application/src/tracker/VideoOpener.cpp
+++ b/Application/src/tracker/VideoOpener.cpp
@@ -6,10 +6,15 @@
 #include <tracker/misc/Output.h>
 #include <misc/default_settings.h>
 #include <gui/types/StaticText.h>
+#include <processing/RawProcessing.h>
+#include <grabber/default_config.h>
 
 namespace gui {
 
 VideoOpener::VideoOpener() {
+    grab::default_config::get(GlobalSettings::map(), GlobalSettings::docs(), &GlobalSettings::set_access_level);
+    ::default_config::get(GlobalSettings::map(), GlobalSettings::docs(), &GlobalSettings::set_access_level);
+    
     _horizontal = std::make_shared<gui::HorizontalLayout>();
     _extra = std::make_shared<gui::VerticalLayout>();
     _infos = std::make_shared<gui::VerticalLayout>();
@@ -20,6 +25,54 @@ VideoOpener::VideoOpener() {
     
     _horizontal->set_children({_infos, _extra});
     
+    SETTING(output_name) = file::Path("video");
+    
+    _horizontal_raw = std::make_shared<gui::HorizontalLayout>();
+    _raw_settings = std::make_shared<gui::VerticalLayout>();
+    _raw_info = std::make_shared<gui::VerticalLayout>();
+    _raw_info->set_policy(gui::VerticalLayout::LEFT);
+    _screenshot = std::make_shared<gui::ExternalImage>();
+    _text_fields.clear();
+    _text_fields["output_name"] = LabeledField("output name");
+    _text_fields["output_name"]._text_field->set_text(SETTING(output_name).get().valueString());
+    _text_fields["output_name"]._text_field->on_text_changed([this](){
+        file::Path number = _text_fields["output_name"]._text_field->text();
+        SETTING(output_name) = number;
+    });
+    
+    _text_fields["threshold"] = LabeledField("threshold");
+    _text_fields["threshold"]._text_field->set_text(SETTING(threshold).get().valueString());
+    _text_fields["threshold"]._text_field->on_text_changed([this](){
+        auto number = Meta::fromStr<int>(_text_fields["threshold"]._text_field->text());
+        SETTING(threshold) = number;
+        
+        if(_buffer) {
+            _buffer->_threshold = number;
+        }
+    });
+    _text_fields["average_samples"] = LabeledField("average_samples");
+    _text_fields["average_samples"]._text_field->set_text(SETTING(average_samples).get().valueString());
+    _text_fields["average_samples"]._text_field->on_text_changed([this](){
+        auto number = Meta::fromStr<int>(_text_fields["average_samples"]._text_field->text());
+        SETTING(average_samples) = number;
+        
+        if(_buffer) {
+            _buffer->restart_background();
+        }
+    });
+    
+    std::vector<Layout::Ptr> objects{};
+    for(auto &[key, ptr] : _text_fields) {
+        objects.push_back(ptr._joint);
+    }
+    
+    _raw_settings->set_children(objects);
+    
+    _raw_description = std::make_shared<gui::StaticText>("Info", Vec2(), Size2(500, -1));
+    _raw_info->set_children({_screenshot, _raw_description});
+    _horizontal_raw->set_children({_raw_settings, _raw_info});
+    _horizontal_raw->set_policy(gui::HorizontalLayout::TOP);
+    
     _settings_to_show = {
         "track_max_individuals",
         "blob_size_ranges",
@@ -35,7 +88,10 @@ VideoOpener::VideoOpener() {
     
     _output_prefix = SETTING(output_prefix).value<std::string>();
     
-    _file_chooser = std::make_shared<gui::FileChooser>(SETTING(output_dir).value<file::Path>(), ".pv", [this](const file::Path& path)
+    _file_chooser = std::make_shared<gui::FileChooser>(
+        SETTING(output_dir).value<file::Path>(),
+        "pv",
+        [this](const file::Path& path, std::string tab) mutable
     {
         if(!path.empty()) {
             auto tmp = path;
@@ -88,6 +144,14 @@ VideoOpener::VideoOpener() {
             
             if(!first)
                 _result.extra_command_lines = str;
+            _result.tab = _file_chooser->current_tab();
+            _result.tab.content = nullptr;
+            
+            if(_result.tab.extension == "pv") {
+                // PV file, no need to add cmd
+            } else {
+                _result.cmd = "-i '" + path.str() + "' " + "-o '"+SETTING(output_name).value<file::Path>().str()+"' -threshold "+SETTING(threshold).get().valueString()+" -average_samples "+SETTING(average_samples).get().valueString()+ " -reset_average";
+            }
             
             if(_load_results_checkbox && _load_results_checkbox->checked()) {
                 _result.load_results = true;
@@ -96,18 +160,274 @@ VideoOpener::VideoOpener() {
             
         }
         
-    }, [this](auto& path){ select_file(path); }, _horizontal);
+    }, [this](auto& path, std::string tab){ select_file(path); });
+    
+    _file_chooser->set_tabs({
+        FileChooser::Settings{std::string("Pre-processed (PV)"), std::string("pv"), _horizontal},
+        FileChooser::Settings{std::string("Convert (RAW)"), std::string("mp4;avi;mov;flv;m4v;webm"), _horizontal_raw}
+    });
+    
+    _file_chooser->set_on_update([this](auto&) mutable {
+        std::lock_guard guard(_video_mutex);
+        if(_buffer) {
+            auto image = _buffer->next();
+            if(image) {
+                _screenshot->set_source(std::move(image));
+                
+                if(_screenshot->size().max() != _screenshot_previous_size) {
+                    _screenshot_previous_size = _screenshot->size().max();
+                    
+                    const double max_width = 500;
+                    auto ratio = max_width / _screenshot_previous_size;
+                    Debug("%f (%f / %f)", ratio, max_width, _screenshot_previous_size);
+                    _screenshot->set_scale(Vec2(ratio));
+                    
+                    _raw_info->auto_size(Margin{0, 0});
+                    _raw_settings->auto_size(Margin{0, 0});
+                    _horizontal_raw->auto_size(Margin{0, 0});
+                }
+            }
+        }
+    });
+    
+    _file_chooser->set_validity_check([this](file::Path path) {
+        if((path.exists() && path.is_folder())
+           || _file_chooser->current_tab().is_valid_extension(path))
+            return true;
+        return false;
+    });
     
     _file_chooser->open();
 }
 
+VideoOpener::BufferedVideo::BufferedVideo(const file::Path& path) : _path(path) {
+}
+
+VideoOpener::BufferedVideo::~BufferedVideo() {
+    _terminate = true;
+    _terminate_background = true;
+    
+    if(_update_thread)
+        _update_thread->join();
+    if(_background_thread)
+        _background_thread->join();
+    
+    _background_video = nullptr;
+}
+
+void VideoOpener::BufferedVideo::restart_background() {
+    _terminate_background = true;
+    if(_background_thread)
+        _background_thread->join();
+    
+    _terminate_background = false;
+    
+    std::lock_guard guard(_frame_mutex);
+    cv::Mat img;
+    _background_video->frame(0, img);
+    if(max(img.cols, img.rows) > 500)
+        resize_image(img, 500 / double(max(img.cols, img.rows)));
+    
+    img.convertTo(_background_image, CV_32FC1);
+    _background_image.copyTo(_accumulator);
+    
+    _background_samples = 1;
+    _background_video_index = 0;
+    //_accumulator = cv::Mat::zeros(img.rows, img.cols, CV_32FC1);
+    
+    _background_thread = std::make_unique<std::thread>([this](){
+        int step = max(1, int(_background_video->length() / max(2.0, double(SETTING(average_samples).value<int>()))));
+        Debug("Start calculating background in %d steps", step);
+        cv::Mat flt, img;
+        
+        while(!_terminate_background && _background_video_index+1+step < _background_video->length()) {
+            _background_video_index += step;
+            
+            _background_video->frame(_background_video_index, img);
+            if(max(img.cols, img.rows) > 500)
+                resize_image(img, 500 / double(max(img.cols, img.rows)));
+            
+            img.convertTo(flt, CV_32FC1);
+            if(!_accumulator.empty())
+                cv::add(_accumulator, flt, _accumulator);
+            else
+                flt.copyTo(_accumulator);
+            ++_background_samples;
+            Debug("%d/%d (%d)", _background_video_index, _background_video->length(), step);
+            
+            std::lock_guard guard(_frame_mutex);
+            cv::divide(_accumulator, cv::Scalar(_background_samples), _background_copy);
+            _set_copy_background = true;
+        }
+        
+        Debug("Done calculating background");
+    });
+}
+
+void VideoOpener::BufferedVideo::open() {
+    std::lock_guard guard(_video_mutex);
+    _video = std::make_unique<VideoSource>(_path.str());
+    
+    _video->frame(0, _local);
+    _local.copyTo(_img);
+    
+    _background_video = std::make_unique<VideoSource>(_path.str());
+    _cached_frame = std::make_unique<Image>(_local);
+    
+    _playback_index = 0;
+    _video_timer.reset();
+    
+    restart_background();
+    
+    // playback at 2x speed
+    _seconds_between_frames = 1 / double(_video->framerate());
+
+    _update_thread = std::make_unique<std::thread>([this](){
+        while(!_terminate) {
+            std::lock_guard guard(_video_mutex);
+            auto dt = _video_timer.elapsed();
+            if(dt < _seconds_between_frames)
+                continue;
+            
+            _playback_index = _playback_index + 1; // loading is too slow...
+            
+            if((uint32_t)_playback_index.load() % 100 == 0)
+                Debug("Playback %.2fms / %.2fms ...", dt * 1000, _seconds_between_frames * 1000);
+            
+            if(dt > _seconds_between_frames) {
+                
+            } //else
+                //_playback_index = _playback_index + dt / _seconds_between_frames;
+            _video_timer.reset();
+            
+            if(_playback_index+1 >= _video->length())
+                _playback_index = 0;
+            
+            update_loop();
+        }
+    });
+}
+
+void VideoOpener::BufferedVideo::update_loop() {
+    try {
+        _video->frame((size_t)_playback_index, _local);
+        _local.copyTo(_img);
+        if(max(_img.cols, _img.rows) > 500)
+            resize_image(_img, 500 / double(max(_img.cols, _img.rows)));
+        _img.convertTo(_flt, CV_32FC1);
+
+        if(_alpha.empty()) {
+            _alpha = gpuMat(_img.rows, _img.cols, CV_8UC1);
+            _alpha.setTo(cv::Scalar(255));
+        }
+        
+        {
+            std::lock_guard frame_guard(_frame_mutex);
+            if(_set_copy_background) {
+                _set_copy_background = false;
+                _background_copy.copyTo(_background_image);
+            }
+            cv::absdiff(_background_image, _flt, _diff);
+        }
+        
+        cv::inRange(_diff, _threshold.load(), 255, _mask);
+        cv::merge(std::vector<gpuMat>{_mask, _img, _img, _alpha}, _output);
+        _output.copyTo(_local);
+        
+        std::lock_guard frame_guard(_frame_mutex);
+        _cached_frame = std::make_unique<Image>(_local);
+        
+    } catch(const std::exception& e) {
+        Except("Caught exception while updating '%s'", e.what());
+    }
+}
+
+Size2 VideoOpener::BufferedVideo::size() {
+    std::lock_guard guard(_video_mutex);
+    return Size2(_video->size());
+}
+
+std::unique_ptr<Image> VideoOpener::BufferedVideo::next() {
+    std::lock_guard guard(_frame_mutex);
+    return std::move(_cached_frame);
+}
+
 void VideoOpener::select_file(const file::Path &p) {
+    const double max_width = 500;
+    
+    if(!p.empty() && (!p.has_extension() || p.extension() != "pv")) {
+        try {
+            Debug("Opening '%S'", &p.str());
+            
+            std::lock_guard guard(_video_mutex);
+            {
+                SETTING(output_name) = file::Path("video");
+                auto filename = p;
+                
+                if(p.has_extension())
+                    filename = filename.remove_extension();
+                
+                if(utils::contains(p.filename().to_string(), '%')) {
+                    filename = filename.remove_filename();
+                }
+                
+                filename = filename.filename();
+                
+                SETTING(output_name) = filename;
+                if(SETTING(output_name).value<file::Path>().empty()) {
+                    Warning("No output filename given. Defaulting to 'video'.");
+                } else
+                    Warning("Given empty filename, the program will default to using input basename '%S'.", &filename.str());
+                
+                _text_fields["output_name"]._text_field->set_text(filename.str());
+            }
+            
+            _buffer = std::make_unique<BufferedVideo>(p);
+            _buffer->open();
+            _screenshot->set_source(std::move(_buffer->next()));
+            _screenshot_previous_size = 0;
+            
+            try {
+                auto number = _text_fields["threshold"]._text_field->text();
+                if(!number.empty())
+                    _buffer->_threshold = Meta::fromStr<uint32_t>(number);
+                
+            } catch(const std::exception &e) {
+                Except("Converting number: '%s'", e.what());
+            }
+            
+            auto ratio = max_width / _screenshot->size().max();
+            Debug("%f (%f / %f)", ratio, max_width, _screenshot->size().max());
+            _screenshot->set_scale(Vec2(ratio));
+            
+            _raw_info->auto_size(Margin{0, 0});
+            _raw_settings->auto_size(Margin{0, 0});
+            _horizontal_raw->auto_size(Margin{0, 0});
+            
+        } catch(const std::exception& e) {
+            Except("Cannot open file '%S' (%s)", &p.str(), e.what());
+            
+            cv::Mat img = cv::Mat::zeros(max_width, max_width, CV_8UC1);
+            cv::putText(img, "Cannot open video.", Vec2(50, 220), cv::FONT_HERSHEY_PLAIN, 1, White);
+            _screenshot->set_source(std::make_unique<Image>(img));
+            _screenshot->set_scale(Vec2(1));
+            _buffer = nullptr;
+        }
+        return;
+        
+    } else {
+        std::lock_guard guard(_video_mutex);
+        if(_buffer) {
+            _buffer = nullptr;
+        }
+    }
+    
     using namespace gui;
     using namespace file;
     
     GlobalSettings::map().dont_print("filename");
-    _selected = p;
-    SETTING(filename) = p;
+    _selected = p.remove_extension();
+    SETTING(filename) = p.remove_extension();
     
     Path settings_file = pv::DataLocation::parse("settings");
     sprite::Map tmp;
@@ -148,10 +468,14 @@ void VideoOpener::select_file(const file::Path &p) {
         } else if(name == "output_prefix") {
             std::vector<std::string> folders;
             for(auto &p : _selected.remove_filename().find_files()) {
-                if(p.is_folder() && p.filename() != "data" && p.filename() != "..") {
-                    if(!p.find_files().empty()) {
-                        folders.push_back(p.filename().to_string());
+                try {
+                    if(p.is_folder() && p.filename() != "data" && p.filename() != "..") {
+                        if(!p.find_files().empty()) {
+                            folders.push_back(p.filename().to_string());
+                        }
                     }
+                } catch(const UtilsException& ex) {
+                    continue; // cannot read folder
                 }
             }
             
diff --git a/Application/src/tracker/VideoOpener.h b/Application/src/tracker/VideoOpener.h
index 9d72a24..0b5050b 100644
--- a/Application/src/tracker/VideoOpener.h
+++ b/Application/src/tracker/VideoOpener.h
@@ -5,6 +5,7 @@
 #include <gui/types/Layout.h>
 #include <gui/types/Checkbox.h>
 #include <file/Path.h>
+#include <video/VideoSource.h>
 
 namespace gui {
 
@@ -13,18 +14,82 @@ public:
     struct Result {
         std::string extra_command_lines;
         std::string load_results_from;
+        std::string cmd;
+        FileChooser::Settings tab;
         bool load_results;
         
         Result() : load_results(false) {}
         
     } _result;
     
+    struct BufferedVideo {
+        file::Path _path;
+        std::unique_ptr<VideoSource> _video;
+        std::unique_ptr<VideoSource> _background_video;
+        gpuMat _background_image;
+        cv::Mat _local;
+        gpuMat _flt, _img, _mask, _diff, _alpha, _output;
+        cv::Mat _accumulator, _background_copy;
+        bool _set_copy_background;
+        uint64_t _background_samples = 0;
+        uint64_t _background_video_index = 0;
+        
+        std::mutex _frame_mutex;
+        std::mutex _video_mutex;
+        
+        std::unique_ptr<Image> _cached_frame;
+        std::atomic<bool> _terminate = false, _terminate_background = false;
+        std::atomic<double> _playback_index = 0;
+        Timer _video_timer;
+        double _seconds_between_frames;
+        
+        std::atomic<uint32_t> _threshold = 0;
+        
+        std::unique_ptr<std::thread> _update_thread;
+        std::unique_ptr<std::thread> _background_thread;
+        
+        BufferedVideo() {}
+        BufferedVideo(const file::Path& path);
+        ~BufferedVideo();
+        
+        std::unique_ptr<Image> next();
+        void open();
+        Size2 size();
+        
+        void restart_background();
+        void update_loop();
+    };
+    
+    std::mutex _video_mutex;
+    std::unique_ptr<BufferedVideo> _buffer;
+    
     std::shared_ptr<FileChooser> _file_chooser;
     std::map<std::string, gui::Drawable*> pointers;
     std::map<std::string, std::string> start_values;
     
-    gui::derived_ptr<gui::VerticalLayout> _extra, _infos;
-    gui::derived_ptr<gui::HorizontalLayout> _horizontal;
+    gui::derived_ptr<gui::VerticalLayout> _extra, _infos, _raw_info, _raw_settings;
+    gui::derived_ptr<gui::HorizontalLayout> _horizontal, _horizontal_raw;
+    gui::derived_ptr<gui::ExternalImage> _screenshot;
+    gui::derived_ptr<gui::StaticText> _raw_description;
+    double _screenshot_previous_size;
+    
+    struct LabeledField {
+        gui::derived_ptr<gui::Text> _text;
+        gui::derived_ptr<gui::Textfield> _text_field;
+        gui::derived_ptr<gui::HorizontalLayout> _joint;
+        
+        LabeledField(const std::string& name = "")
+            : _text(std::make_shared<gui::Text>(name)),
+              _text_field(std::make_shared<gui::Textfield>("", Bounds(0, 0, 500 - _text->width() - 80, 33))),
+              _joint(std::make_shared<gui::HorizontalLayout>(std::vector<Layout::Ptr>{_text, _text_field}))
+        {
+            _text->set_font(Font(0.75, Style::Bold));
+            _text->set_color(White);
+            _text_field->set_placeholder(name);
+        }
+    };
+    std::map<std::string, LabeledField> _text_fields;
+    
     gui::Checkbox *_load_results_checkbox = nullptr;
     std::string _output_prefix;
     std::vector<std::string> _settings_to_show;
diff --git a/Application/src/tracker/gui/gui.cpp b/Application/src/tracker/gui/gui.cpp
index 569e7a4..9f95515 100644
--- a/Application/src/tracker/gui/gui.cpp
+++ b/Application/src/tracker/gui/gui.cpp
@@ -882,23 +882,29 @@ void GUI::start_recording() {
         }
         
         size_t max_number = 0;
-        for(auto &file : frames.find_files()) {
-            auto name = file.filename().to_string();
-            if(utils::beginsWith(name, "clip")) {
-                try {
-                    if(utils::endsWith(name, ".avi"))
-                        name = name.substr(0, name.length() - 4);
-                    auto number = Meta::fromStr<size_t>(name.substr(std::string("clip").length()));
-                    if(number > max_number)
-                        max_number = number;
-                    
-                } catch(const std::exception& e) {
-                    Except("%S not a number ('%s').", &name, e.what());
+        try {
+            for(auto &file : frames.find_files()) {
+                auto name = file.filename().to_string();
+                if(utils::beginsWith(name, "clip")) {
+                    try {
+                        if(utils::endsWith(name, ".avi"))
+                            name = name.substr(0, name.length() - 4);
+                        auto number = Meta::fromStr<size_t>(name.substr(std::string("clip").length()));
+                        if(number > max_number)
+                            max_number = number;
+                        
+                    } catch(const std::exception& e) {
+                        Except("%S not a number ('%s').", &name, e.what());
+                    }
                 }
             }
+            
+            ++max_number;
+            
+        } catch(const UtilsException& ex) {
+            Warning("Cannot iterate on folder '%S'. Defaulting to index 0.", &frames.str());
         }
         
-        ++max_number;
         Debug("Clip index is %d. Starting at frame %d.", max_number, frame());
         
         frames = frames / ("clip" + Meta::toStr(max_number));
diff --git a/Application/src/tracker/main.cpp b/Application/src/tracker/main.cpp
index f1ff779..acc4f26 100644
--- a/Application/src/tracker/main.cpp
+++ b/Application/src/tracker/main.cpp
@@ -78,6 +78,14 @@
 
 #include <opencv2/core/utils/logger.hpp>
 
+#ifdef _WIN32
+#include <direct.h>
+#define GetCurrentDir _getcwd
+#else
+#include <unistd.h>
+#define GetCurrentDir getcwd
+#endif
+
 //-Functions-------------------------------------------------------------------
 
 using namespace track;
@@ -432,7 +440,7 @@ int main(int argc, char** argv)
                         fwrite(rst.data(), sizeof(char), rst.length(), f);
                         fclose(f);
                         
-                        printf("%s\n", rst.c_str());
+                        //printf("%s\n", rst.c_str());
                         Debug("Saved at '%S'.", &path.str());
                         
                         exit(0);
@@ -518,11 +526,25 @@ int main(int argc, char** argv)
         if((GlobalSettings::map().has("nowindow") ? SETTING(nowindow).value<bool>() : false) == false) {
             gui::VideoOpener opener;
             opening_result = opener._result;
-
-            if(opening_result.load_results)
-                load_results = true;
-            if(!opening_result.load_results_from.empty())
-                load_results_from = opening_result.load_results_from;
+            
+            if(opening_result.tab.extension == "pv") {
+                if(opening_result.load_results)
+                    load_results = true;
+                if(!opening_result.load_results_from.empty())
+                    load_results_from = opening_result.load_results_from;
+            } else {
+                auto wd = SETTING(wd).value<file::Path>();
+                Debug("Opening a video file: '%S', '%S'", &opening_result.tab.name, &wd.str());
+#if defined(__APPLE__)
+                wd = wd / ".." / ".." / ".." / "TGrabs.app" / "Contents" / "MacOS" / "TGrabs";
+#else
+                wd = wd / "tgrabs";
+#endif
+                auto exec = wd.str() + " " + opening_result.cmd;
+                Debug("Executing '%S'", &exec);
+                file::exec(exec.c_str());
+                exit(0);
+            }
         }
         
         if(SETTING(filename).value<Path>().empty()) {
diff --git a/docs/parameters_trex.rst b/docs/parameters_trex.rst
index a98d9a9..6926e85 100644
--- a/docs/parameters_trex.rst
+++ b/docs/parameters_trex.rst
@@ -184,7 +184,7 @@ TRex parameters
 
 .. function:: build_is_debug(string)
 
-	**default value:** "debug"
+	**default value:** "release"
 
 
 	If built in debug mode, this will show 'debug'.
-- 
GitLab