diff --git a/BaseToolkit.cpp b/BaseToolkit.cpp index 0547c62..8234193 100644 --- a/BaseToolkit.cpp +++ b/BaseToolkit.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -115,3 +116,129 @@ std::string BaseToolkit::trunc_string(const std::string& path, int N) } return trunc; } + + +std::string BaseToolkit::common_prefix( const std::list & allStrings ) +{ + if (allStrings.empty()) + return std::string(); + + const std::string &s0 = allStrings.front(); + auto _end = s0.cend(); + for (auto it=std::next(allStrings.cbegin()); it != allStrings.cend(); ++it) + { + auto _loc = std::mismatch(s0.cbegin(), s0.cend(), it->cbegin(), it->cend()); + if (std::distance(_loc.first, _end) > 0) + _end = _loc.first; + } + + return std::string(s0.cbegin(), _end); +} + + +std::string BaseToolkit::common_suffix(const std::list & allStrings) +{ + if (allStrings.empty()) + return std::string(); + + const std::string &s0 = allStrings.front(); + auto r_end = s0.crend(); + for (auto it=std::next(allStrings.cbegin()); it != allStrings.cend(); ++it) + { + auto r_loc = std::mismatch(s0.crbegin(), s0.crend(), it->crbegin(), it->crend()); + if (std::distance(r_loc.first, r_end) > 0) + r_end = r_loc.first; + } + + std::string suffix = std::string(s0.crbegin(), r_end); + std::reverse(suffix.begin(), suffix.end()); + return suffix; +} + + +std::string BaseToolkit::common_pattern(const std::list &allStrings) +{ + if (allStrings.empty()) + return std::string(); + + // find common prefix and suffix + const std::string &s0 = allStrings.front(); + auto _end = s0.cend(); + auto r_end = s0.crend(); + for (auto it=std::next(allStrings.cbegin()); it != allStrings.cend(); ++it) + { + auto _loc = std::mismatch(s0.cbegin(), s0.cend(), it->cbegin(), it->cend()); + if (std::distance(_loc.first, _end) > 0) + _end = _loc.first; + + auto r_loc = std::mismatch(s0.crbegin(), s0.crend(), it->crbegin(), it->crend()); + if (std::distance(r_loc.first, r_end) > 0) + r_end = r_loc.first; + } + + std::string suffix = std::string(s0.crbegin(), r_end); + std::reverse(suffix.begin(), suffix.end()); + + return std::string(s0.cbegin(), _end) + "*" + suffix; +} + +std::string BaseToolkit::common_numbered_pattern(const std::list &allStrings, int *min, int *max) +{ + if (allStrings.empty()) + return std::string(); + + // find common prefix and suffix + const std::string &s0 = allStrings.front(); + auto _end = s0.cend(); + auto r_end = s0.crend(); + for (auto it=std::next(allStrings.cbegin()); it != allStrings.cend(); ++it) + { + auto _loc = std::mismatch(s0.cbegin(), s0.cend(), it->cbegin(), it->cend()); + if (std::distance(_loc.first, _end) > 0) + _end = _loc.first; + + auto r_loc = std::mismatch(s0.crbegin(), s0.crend(), it->crbegin(), it->crend()); + if (std::distance(r_loc.first, r_end) > 0) + r_end = r_loc.first; + } + + // range of middle string, after prefix and before suffix + size_t pos_prefix = std::distance(s0.cbegin(), _end); + size_t pos_suffix = s0.size() - pos_prefix - std::distance(s0.crbegin(), r_end); + + int n = -1; + *max = 0; + *min = INT_MAX; + // loop over all strings to verify there are numbers between prefix and suffix + for (auto it = allStrings.cbegin(); it != allStrings.cend(); ++it) + { + // get middle string, after prefix and before suffix + std::string s = it->substr(pos_prefix, pos_suffix); + // is this central string ONLY made of digits? + if (s.end() == std::find_if(s.begin(), s.end(), [](unsigned char c)->bool { return !isdigit(c); })) { + // yes, validate + *max = std::max(*max, std::atoi(s.c_str()) ); + *min = std::min(*min, std::atoi(s.c_str()) ); + if (n < 0) + n = s.size(); + else if ( n != s.size() ) { + n = 0; + break; + } + } + else { + n = 0; + break; + } + } + + if ( n < 1 ) + return std::string(); + + std::string suffix = std::string(s0.crbegin(), r_end); + std::reverse(suffix.begin(), suffix.end()); + std::string pattern = std::string(s0.cbegin(), _end); + pattern += "%0" + std::to_string(n) + "d"; + pattern += suffix; + return pattern; +} diff --git a/BaseToolkit.h b/BaseToolkit.h index 7992fb8..9f97435 100644 --- a/BaseToolkit.h +++ b/BaseToolkit.h @@ -25,6 +25,16 @@ std::string bits_to_string(long b); // Truncate a string to display the right most N characters (e.g. ./home/me/toto.mpg -> ...ome/me/toto.mpg) std::string trunc_string(const std::string& path, int N); +// find common parts in a list of strings +std::string common_prefix(const std::list &allStrings); +std::string common_suffix(const std::list &allStrings); + +// form a pattern "prefix*suffix" (e.g. file list) +std::string common_pattern(const std::list &allStrings); + +// form a pattern "prefix%03dsuffix" (e.g. numbered file list) +std::string common_numbered_pattern(const std::list &allStrings, int *min, int *max); + } diff --git a/CMakeLists.txt b/CMakeLists.txt index c431145..40e0bf8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -309,6 +309,7 @@ set(VMIX_SRCS PatternSource.cpp DeviceSource.cpp NetworkSource.cpp + MultiFileSource.cpp FrameBuffer.cpp RenderingManager.cpp UserInterfaceManager.cpp @@ -326,6 +327,7 @@ set(VMIX_SRCS NetworkToolkit.cpp Connection.cpp ActionManager.cpp + Overlay.cpp ) @@ -428,6 +430,7 @@ set(VMIX_RSC_FILES ./rsc/mesh/icon_eye_slash.ply ./rsc/mesh/icon_vector_square_slash.ply ./rsc/mesh/icon_cube.ply + ./rsc/mesh/icon_sequence.ply ./rsc/mesh/h_line.ply ./rsc/mesh/h_mark.ply ) diff --git a/Decorations.cpp b/Decorations.cpp index 7fe27cf..1572631 100644 --- a/Decorations.cpp +++ b/Decorations.cpp @@ -404,6 +404,8 @@ Symbol::Symbol(Type t, glm::vec3 pos) : Node(), type_(t) shadows[SQUARE_POINT] = nullptr; icons[IMAGE] = new Mesh("mesh/icon_image.ply"); shadows[IMAGE] = shadow; + icons[SEQUENCE] = new Mesh("mesh/icon_sequence.ply"); + shadows[SEQUENCE]= shadow; icons[VIDEO] = new Mesh("mesh/icon_video.ply"); shadows[VIDEO] = shadow; icons[SESSION] = new Mesh("mesh/icon_vimix.ply"); diff --git a/Decorations.h b/Decorations.h index 8fb9016..ee6d130 100644 --- a/Decorations.h +++ b/Decorations.h @@ -60,9 +60,9 @@ protected: class Symbol : public Node { public: - typedef enum { CIRCLE_POINT = 0, SQUARE_POINT, IMAGE, VIDEO, SESSION, CLONE, RENDER, GROUP, PATTERN, CAMERA, CUBE, SHARE, + typedef enum { CIRCLE_POINT = 0, SQUARE_POINT, IMAGE, SEQUENCE, VIDEO, SESSION, CLONE, RENDER, GROUP, PATTERN, CAMERA, CUBE, SHARE, DOTS, BUSY, LOCK, UNLOCK, EYE, EYESLASH, VECTORSLASH, ARROWS, ROTATION, CROP, CIRCLE, SQUARE, CLOCK, CLOCK_H, GRID, CROSS, EMPTY } Type; - Symbol(Type t = CIRCLE_POINT, glm::vec3 pos = glm::vec3(0.f)); + Symbol(Type t, glm::vec3 pos = glm::vec3(0.f)); ~Symbol(); void draw (glm::mat4 modelview, glm::mat4 projection) override; diff --git a/DialogToolkit.cpp b/DialogToolkit.cpp index bd6079e..74bb065 100644 --- a/DialogToolkit.cpp +++ b/DialogToolkit.cpp @@ -178,7 +178,7 @@ std::string DialogToolkit::openSessionFileDialog(const std::string &path) } -std::string DialogToolkit::ImportFileDialog(const std::string &path) +std::string DialogToolkit::openMediaFileDialog(const std::string &path) { std::string filename = ""; std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path(); @@ -190,7 +190,7 @@ std::string DialogToolkit::ImportFileDialog(const std::string &path) "*.gif", "*.tif", "*.svg" }; #if USE_TINYFILEDIALOG char const * open_file_name; - open_file_name = tinyfd_openFileDialog( "Import a file", startpath.c_str(), 18, open_pattern, "All supported formats", 0); + open_file_name = tinyfd_openFileDialog( "Open Media File", startpath.c_str(), 18, open_pattern, "All supported formats", 0); if (open_file_name) filename = std::string(open_file_name); @@ -201,7 +201,7 @@ std::string DialogToolkit::ImportFileDialog(const std::string &path) return filename; } - GtkWidget *dialog = gtk_file_chooser_dialog_new( "Import Media File", NULL, + GtkWidget *dialog = gtk_file_chooser_dialog_new( "Open Media File", NULL, GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL ); @@ -238,7 +238,7 @@ std::string DialogToolkit::ImportFileDialog(const std::string &path) return filename; } -std::string DialogToolkit::FolderDialog(const std::string &path) +std::string DialogToolkit::openFolderDialog(const std::string &path) { std::string foldername = ""; std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path(); @@ -290,6 +290,73 @@ std::string DialogToolkit::FolderDialog(const std::string &path) } +std::list DialogToolkit::selectImagesFileDialog(const std::string &path) +{ + std::list files; + + std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path(); + char const * open_pattern[3] = { "*.tif", "*.jpg", "*.png" }; + +#if USE_TINYFILEDIALOG + char const * open_file_names; + open_file_names = tinyfd_openFileDialog( "Select images", startpath.c_str(), 3, open_pattern, "Images", 1); + + if (open_file_names) +// filename = std::string(open_file_name); + // TODO +#else + + if (!gtk_init()) { + ErrorDialog("Could not initialize GTK+ for dialog"); + return files; + } + + GtkWidget *dialog = gtk_file_chooser_dialog_new( "Select images", NULL, + GTK_FILE_CHOOSER_ACTION_OPEN, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Open", GTK_RESPONSE_ACCEPT, NULL ); + + // set file filters + add_filter_file_dialog(dialog, 3, open_pattern, "All supported formats"); + add_filter_any_file_dialog(dialog); + + // multiple files + gtk_file_chooser_set_select_multiple( GTK_FILE_CHOOSER(dialog), true ); + + // Set the default path + gtk_file_chooser_set_current_folder( GTK_FILE_CHOOSER(dialog), startpath.c_str() ); + + // ensure front and centered + gtk_window_set_keep_above( GTK_WINDOW(dialog), TRUE ); + if (window_x > 0 && window_y > 0) + gtk_window_move( GTK_WINDOW(dialog), window_x, window_y); + + // display and get filename + if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_ACCEPT ) { + + GSList *open_file_names = gtk_file_chooser_get_filenames( GTK_FILE_CHOOSER(dialog) ); + + while (open_file_names) { + files.push_back( (char *) open_file_names->data ); + open_file_names = open_file_names->next; +// g_free( open_file_names->data ); + } + + g_slist_free( open_file_names ); + } + + // remember position + gtk_window_get_position( GTK_WINDOW(dialog), &window_x, &window_y); + + // done + gtk_widget_destroy(dialog); + wait_for_event(); +#endif + + + return files; +} + void DialogToolkit::ErrorDialog(const char* message) { #if USE_TINYFILEDIALOG diff --git a/DialogToolkit.h b/DialogToolkit.h index bb2b6a0..d486d1c 100644 --- a/DialogToolkit.h +++ b/DialogToolkit.h @@ -2,6 +2,7 @@ #define DIALOGTOOLKIT_H #include +#include namespace DialogToolkit @@ -11,9 +12,11 @@ std::string saveSessionFileDialog(const std::string &path); std::string openSessionFileDialog(const std::string &path); -std::string ImportFileDialog(const std::string &path); +std::string openMediaFileDialog(const std::string &path); -std::string FolderDialog(const std::string &path); +std::string openFolderDialog(const std::string &path); + +std::list selectImagesFileDialog(const std::string &path); void ErrorDialog(const char* message); diff --git a/GeometryView.cpp b/GeometryView.cpp index fd27279..1cc200e 100644 --- a/GeometryView.cpp +++ b/GeometryView.cpp @@ -516,7 +516,7 @@ std::pair GeometryView::pick(glm::vec2 P) bool GeometryView::canSelect(Source *s) { - return ( View::canSelect(s) && s->active() && s->workspace() == Settings::application.current_workspace); + return ( View::canSelect(s) && s->ready() && s->active() && s->workspace() == Settings::application.current_workspace); } diff --git a/ImGuiVisitor.cpp b/ImGuiVisitor.cpp index a7c7ad4..b30ca9f 100644 --- a/ImGuiVisitor.cpp +++ b/ImGuiVisitor.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "tinyxml2Toolkit.h" @@ -24,6 +25,7 @@ #include "PatternSource.h" #include "DeviceSource.h" #include "NetworkSource.h" +#include "MultiFileSource.h" #include "SessionCreator.h" #include "SessionVisitor.h" #include "Settings.h" @@ -593,7 +595,6 @@ void ImGuiVisitor::visit (SessionFileSource& s) std::string label = BaseToolkit::trunc_string(path, 25); label = BaseToolkit::transliterate(label); ImGuiToolkit::ButtonOpenUrl( label.c_str(), path.c_str(), ImVec2(IMGUI_RIGHT_ALIGN, 0) ); - ImGui::SameLine(); ImGui::Text("Folder"); @@ -711,3 +712,47 @@ void ImGuiVisitor::visit (NetworkSource& s) } + +void ImGuiVisitor::visit (MultiFileSource& s) +{ + ImGuiToolkit::Icon(s.icon().x, s.icon().y); + ImGui::SameLine(0, 10); + ImGui::Text("Images sequence"); + + // information text + std::ostringstream msg; + msg << "Sequence of " << s.sequence().max - s.sequence().min << " "; + msg << s.sequence().codec << " images"; +// msg << "named " << SystemToolkit::base_filename( s.sequence().location ); +// msg << " in range [" << s.sequence().min << " - " << s.sequence().max << "]"; + ImGui::Text("%s", msg.str().c_str()); + + // change range + glm::ivec2 range = s.range(); + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + if ( ImGui::DragIntRange2("Range", &range.x, &range.y, 1, s.sequence().min, s.sequence().max) ){ + s.setRange( range ); + } + + // change framerate + int _fps = s.framerate(); + static int _fps_changed = -1; + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + if ( ImGui::SliderInt("Framerate", &_fps, 1, 30, "%d fps") ) { + _fps_changed = _fps; + } + // only call for setFramerate after mouse release to avoid repeating call to re-open the stream + else if (_fps_changed > 0 && ImGui::IsMouseReleased(ImGuiMouseButton_Left)){ + s.setFramerate(_fps_changed); + _fps_changed = -1; + } + + // offer to open file browser at location + std::string path = SystemToolkit::path_filename(s.sequence().location); + std::string label = BaseToolkit::trunc_string(path, 25); + label = BaseToolkit::transliterate(label); + ImGuiToolkit::ButtonOpenUrl( label.c_str(), path.c_str(), ImVec2(IMGUI_RIGHT_ALIGN, 0) ); + ImGui::SameLine(); + ImGui::Text("Folder"); + +} diff --git a/ImGuiVisitor.h b/ImGuiVisitor.h index 06f2a76..2fa46d2 100644 --- a/ImGuiVisitor.h +++ b/ImGuiVisitor.h @@ -30,6 +30,7 @@ public: void visit (PatternSource& s) override; void visit (DeviceSource& s) override; void visit (NetworkSource& s) override; + void visit (MultiFileSource& s) override; }; #endif // IMGUIVISITOR_H diff --git a/Log.cpp b/Log.cpp index 2948684..d27e6cb 100644 --- a/Log.cpp +++ b/Log.cpp @@ -57,86 +57,90 @@ struct AppLog ImGui::SetNextWindowPos(ImVec2(430, 660), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(1150, 220), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSizeConstraints(ImVec2(600, 180), ImVec2(FLT_MAX, FLT_MAX)); - if (ImGui::Begin(title, p_open)) + if ( !ImGui::Begin(title, p_open)) { - // window - ImGui::SameLine(0, 0); - static bool numbering = true; - ImGuiToolkit::ButtonToggle( ICON_FA_SORT_NUMERIC_DOWN, &numbering ); - ImGui::SameLine(); - bool clear = ImGui::Button( ICON_FA_BACKSPACE " Clear"); - ImGui::SameLine(); - bool copy = ImGui::Button( ICON_FA_COPY " Copy"); - ImGui::SameLine(); - Filter.Draw("Filter", -60.0f); - - ImGui::Separator(); - if ( ImGui::BeginChild("scrolling", ImVec2(0,0), false, ImGuiWindowFlags_AlwaysHorizontalScrollbar) ) - { - if (clear) - Clear(); - if (copy) - ImGui::LogToClipboard(); - - ImGuiToolkit::PushFont(ImGuiToolkit::FONT_MONO); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); - - mtx.lock(); - - const char* buf = Buf.begin(); - const char* buf_end = Buf.end(); - if (Filter.IsActive()) - { - // In this example we don't use the clipper when Filter is enabled. - // This is because we don't have a random access on the result on our filter. - // A real application processing logs with ten of thousands of entries may want to store the result of search/filter. - // especially if the filtering function is not trivial (e.g. reg-exp). - for (int line_no = 0; line_no < LineOffsets.Size; line_no++) - { - const char* line_start = buf + LineOffsets[line_no]; - const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end; - if (Filter.PassFilter(line_start, line_end)) - ImGui::TextUnformatted(line_start, line_end); - } - } - else - { - // The simplest and easy way to display the entire buffer: - // ImGui::TextUnformatted(buf_begin, buf_end); - // And it'll just work. TextUnformatted() has specialization for large blob of text and will fast-forward to skip non-visible lines. - // Here we instead demonstrate using the clipper to only process lines that are within the visible area. - // If you have tens of thousands of items and their processing cost is non-negligible, coarse clipping them on your side is recommended. - // Using ImGuiListClipper requires A) random access into your data, and B) items all being the same height, - // both of which we can handle since we an array pointing to the beginning of each line of text. - // When using the filter (in the block of code above) we don't have random access into the data to display anymore, which is why we don't use the clipper. - // Storing or skimming through the search result would make it possible (and would be recommended if you want to search through tens of thousands of entries) - ImGuiListClipper clipper; - clipper.Begin(LineOffsets.Size); - while (clipper.Step()) - { - for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++) - { - const char* line_start = buf + LineOffsets[line_no] + (numbering?0:6); - const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end; - ImGui::TextUnformatted(line_start, line_end); - } - } - clipper.End(); - } - - mtx.unlock(); - - ImGui::PopStyleVar(); - ImGui::PopFont(); - - // Auto scroll - if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) - ImGui::SetScrollHereY(1.0f); - - ImGui::EndChild(); - } ImGui::End(); + return; } + + // window + ImGui::SameLine(0, 0); + static bool numbering = true; + ImGuiToolkit::ButtonToggle( ICON_FA_SORT_NUMERIC_DOWN, &numbering ); + ImGui::SameLine(); + bool clear = ImGui::Button( ICON_FA_BACKSPACE " Clear"); + ImGui::SameLine(); + bool copy = ImGui::Button( ICON_FA_COPY " Copy"); + ImGui::SameLine(); + Filter.Draw("Filter", -60.0f); + + ImGui::Separator(); + if ( ImGui::BeginChild("scrolling", ImVec2(0,0), false, ImGuiWindowFlags_AlwaysHorizontalScrollbar) ) + { + if (clear) + Clear(); + if (copy) + ImGui::LogToClipboard(); + + ImGuiToolkit::PushFont(ImGuiToolkit::FONT_MONO); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + + mtx.lock(); + + const char* buf = Buf.begin(); + const char* buf_end = Buf.end(); + if (Filter.IsActive()) + { + // In this example we don't use the clipper when Filter is enabled. + // This is because we don't have a random access on the result on our filter. + // A real application processing logs with ten of thousands of entries may want to store the result of search/filter. + // especially if the filtering function is not trivial (e.g. reg-exp). + for (int line_no = 0; line_no < LineOffsets.Size; line_no++) + { + const char* line_start = buf + LineOffsets[line_no]; + const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end; + if (Filter.PassFilter(line_start, line_end)) + ImGui::TextUnformatted(line_start, line_end); + } + } + else + { + // The simplest and easy way to display the entire buffer: + // ImGui::TextUnformatted(buf_begin, buf_end); + // And it'll just work. TextUnformatted() has specialization for large blob of text and will fast-forward to skip non-visible lines. + // Here we instead demonstrate using the clipper to only process lines that are within the visible area. + // If you have tens of thousands of items and their processing cost is non-negligible, coarse clipping them on your side is recommended. + // Using ImGuiListClipper requires A) random access into your data, and B) items all being the same height, + // both of which we can handle since we an array pointing to the beginning of each line of text. + // When using the filter (in the block of code above) we don't have random access into the data to display anymore, which is why we don't use the clipper. + // Storing or skimming through the search result would make it possible (and would be recommended if you want to search through tens of thousands of entries) + ImGuiListClipper clipper; + clipper.Begin(LineOffsets.Size); + while (clipper.Step()) + { + for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++) + { + const char* line_start = buf + LineOffsets[line_no] + (numbering?0:6); + const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end; + ImGui::TextUnformatted(line_start, line_end); + } + } + clipper.End(); + } + + mtx.unlock(); + + ImGui::PopStyleVar(); + ImGui::PopFont(); + + // Auto scroll + if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) + ImGui::SetScrollHereY(1.0f); + + ImGui::EndChild(); + } + + ImGui::End(); } }; diff --git a/MediaPlayer.cpp b/MediaPlayer.cpp index 0a54053..c82ea38 100644 --- a/MediaPlayer.cpp +++ b/MediaPlayer.cpp @@ -84,7 +84,7 @@ guint MediaPlayer::texture() const #define LIMIT_DISCOVERER -static MediaInfo UriDiscoverer_(std::string uri) +MediaInfo MediaPlayer::UriDiscoverer(const std::string &uri) { #ifdef MEDIA_PLAYER_DEBUG Log::Info("Checking file '%s'", uri.c_str()); @@ -220,24 +220,27 @@ static MediaInfo UriDiscoverer_(std::string uri) return video_stream_info; } -void MediaPlayer::open(string path) +void MediaPlayer::open (const std::string & filename, const string &uri) { // set path - filename_ = BaseToolkit::transliterate( path ); + filename_ = BaseToolkit::transliterate( filename ); // set uri to open - uri_ = GstToolkit::filename_to_uri(path); + if (uri.empty()) + uri_ = GstToolkit::filename_to_uri( filename ); + else + uri_ = uri; // close before re-openning if (isOpen()) close(); // start URI discovering thread: - discoverer_ = std::async( UriDiscoverer_, uri_); + discoverer_ = std::async( MediaPlayer::UriDiscoverer, uri_); // wait for discoverer to finish in the future (test in update) // // debug without thread -// media_ = UriDiscoverer_(uri_); +// media_ = MediaPlayer::UriDiscoverer(uri_); // if (media_.valid) { // timeline_.setEnd( media_.end ); // timeline_.setStep( media_.dt ); diff --git a/MediaPlayer.h b/MediaPlayer.h index 7c9e70f..f20c81e 100644 --- a/MediaPlayer.h +++ b/MediaPlayer.h @@ -91,8 +91,8 @@ public: /** * Open a media using gstreamer URI * */ - void open( std::string path); - void reopen(); + void open ( const std::string &filename, const std::string &uri = ""); + void reopen (); /** * Get name of the media * */ @@ -265,6 +265,8 @@ public: static std::list::const_iterator begin() { return registered_.cbegin(); } static std::list::const_iterator end() { return registered_.cend(); } + static MediaInfo UriDiscoverer(const std::string &uri); + private: // video player description diff --git a/Mixer.cpp b/Mixer.cpp index 1899699..ae797b9 100644 --- a/Mixer.cpp +++ b/Mixer.cpp @@ -26,6 +26,7 @@ #include "MediaSource.h" #include "PatternSource.h" #include "DeviceSource.h" +#include "MultiFileSource.h" #include "StreamSource.h" #include "NetworkSource.h" #include "ActionManager.h" @@ -245,6 +246,34 @@ Source * Mixer::createSourceFile(const std::string &path) return s; } +Source * Mixer::createSourceMultifile(const std::list &list_files, uint fps) +{ + // ready to create a source + Source *s = nullptr; + + if ( list_files.size() >0 ) { + + // validate the creation of a sequence from the list + MultiFileSequence sequence(list_files); + + if ( sequence.valid() ) { + + // try to create a sequence + MultiFileSource *mfs = new MultiFileSource; + mfs->setSequence(sequence, fps); + s = mfs; + + // remember in recent media + Settings::application.recentImport.path = SystemToolkit::path_filename(list_files.front()); + + // propose a new name + s->setName( SystemToolkit::base_filename( BaseToolkit::common_prefix(list_files) ) ); + } + } + + return s; +} + Source * Mixer::createSourceRender() { // ready to create a source @@ -679,7 +708,7 @@ void Mixer::setCurrentSource(SourceList::iterator it) unsetCurrentSource(); // change current if 'it' is valid - if ( it != session_->end() ) { + if ( it != session_->end() /*&& (*it)->mode() > Source::UNINITIALIZED */) { current_source_ = it; current_source_index_ = session_->index(current_source_); diff --git a/Mixer.h b/Mixer.h index 1d8346c..302207f 100644 --- a/Mixer.h +++ b/Mixer.h @@ -48,6 +48,7 @@ public: // creation of sources Source * createSourceFile (const std::string &path); + Source * createSourceMultifile(const std::list &list_files, uint fps); Source * createSourceClone (const std::string &namesource = ""); Source * createSourceRender (); Source * createSourceStream (const std::string &gstreamerpipeline); diff --git a/MultiFileSource.cpp b/MultiFileSource.cpp new file mode 100644 index 0000000..f441e1d --- /dev/null +++ b/MultiFileSource.cpp @@ -0,0 +1,203 @@ +#include +#include + +#include "defines.h" +#include "ImageShader.h" +#include "Resource.h" +#include "Decorations.h" +#include "Stream.h" +#include "Visitor.h" +#include "GstToolkit.h" +#include "BaseToolkit.h" +#include "SystemToolkit.h" +#include "MediaPlayer.h" +#include "Log.h" + +#include "MultiFileSource.h" + +// example test gstreamer pipelines +// +// multifile : sequence of numbered images +// gst-launch-1.0 multifilesrc location="/home/bhbn/Images/sequence/frames%03d.png" caps="image/png,framerate=\(fraction\)12/1" loop=1 ! decodebin ! videoconvert ! autovideosink +// +// imagesequencesrc : sequence of numbered images (cannot loop) +// gst-launch-1.0 imagesequencesrc location=frames%03d.png start-index=1 framerate=24/1 ! decodebin ! videoconvert ! autovideosink +// +// splitfilesrc +// gst-launch-1.0 splitfilesrc location="/home/bhbn/Videos/MOV01*.MOD" ! decodebin ! videoconvert ! autovideosink + +MultiFileSequence::MultiFileSequence() : width(0), height(0), min(0), max(0), loop(1) +{ +} + +MultiFileSequence::MultiFileSequence(const std::list &list_files) : loop(1) +{ + location = BaseToolkit::common_numbered_pattern(list_files, &min, &max); + + // sanity check: the location pattern looks like a filename and seems consecutive numbered + if ( SystemToolkit::extension_filename(location).empty() || + SystemToolkit::path_filename(location) != SystemToolkit::path_filename(list_files.front()) || + list_files.size() != max - min + 1 ) { + location.clear(); + } + + if ( !location.empty() ) { + MediaInfo media = MediaPlayer::UriDiscoverer( GstToolkit::filename_to_uri( list_files.front() ) ); + if (media.valid && media.isimage) { + codec.resize(media.codec_name.size()); + std::transform(media.codec_name.begin(), media.codec_name.end(), codec.begin(), ::tolower); + width = media.width; + height = media.height; + } + } +} + +bool MultiFileSequence::valid() const +{ + return !( location.empty() || codec.empty() || width < 1 || height < 1 || max == min); +} + +inline MultiFileSequence& MultiFileSequence::operator = (const MultiFileSequence& b) +{ + if (this != &b) { + this->width = b.width; + this->height = b.height; + this->min = b.min; + this->max = b.max; + this->loop = b.loop; + this->location = b.location; + this->codec = b.codec; + } + return *this; +} + + +bool MultiFileSequence::operator != (const MultiFileSequence& b) +{ + return ( location != b.location || codec != b.codec || width != b.width || + height != b.height || min != b.min || max != b.max || loop != b.loop ); +} + +MultiFile::MultiFile() : Stream(), src_(nullptr) +{ + +} + +void MultiFile::open(const MultiFileSequence &sequence, uint framerate ) +{ + if (sequence.location.empty()) + return; + + std::ostringstream gstreamer_pipeline; + gstreamer_pipeline << "multifilesrc name=src location=\""; + gstreamer_pipeline << sequence.location; + gstreamer_pipeline << "\" caps=\"image/"; + gstreamer_pipeline << sequence.codec; + gstreamer_pipeline << ",framerate=(fraction)"; + gstreamer_pipeline << framerate; + gstreamer_pipeline << "/1\" loop="; + gstreamer_pipeline << sequence.loop; + gstreamer_pipeline << " start-index="; + gstreamer_pipeline << sequence.min; + gstreamer_pipeline << " stop-index="; + gstreamer_pipeline << sequence.max; + gstreamer_pipeline << " ! decodebin ! videoconvert"; + + // (private) open stream + Stream::open(gstreamer_pipeline.str(), sequence.width, sequence.height); + + src_ = gst_bin_get_by_name (GST_BIN (pipeline_), "src"); + +} + +void MultiFile::setLoop (int on) +{ + if (src_) { + g_object_set (src_, "loop", on, NULL); + } +} + +void MultiFile::setRange (int begin, int end) +{ + if (src_) { + g_object_set (src_, "start-index", MAX(begin, 0), NULL); + g_object_set (src_, "stop-index", MAX(end, 0), NULL); + } +} + + + +MultiFileSource::MultiFileSource(uint64_t id) : StreamSource(id), framerate_(0), range_(glm::ivec2(0)) +{ + // create stream + stream_ = static_cast( new MultiFile ); + + // set symbol + symbol_ = new Symbol(Symbol::SEQUENCE, glm::vec3(0.75f, 0.75f, 0.01f)); + symbol_->scale_.y = 1.5f; +} + + +void MultiFileSource::setFiles (const std::list &list_files, uint framerate) +{ + setSequence(MultiFileSequence(list_files), framerate); +} + +void MultiFileSource::setSequence (const MultiFileSequence &sequence, uint framerate) +{ + framerate_ = CLAMP( framerate, 1, 30); + sequence_ = sequence; + + if (sequence_.valid()) + { + // open gstreamer + multifile()->open( sequence_, framerate_ ); + stream_->play(true); + + // init range + range_ = glm::ivec2(sequence_.min, sequence_.max); + + // will be ready after init and one frame rendered + ready_ = false; + } +} + +void MultiFileSource::setFramerate (uint framerate) +{ + if (multifile()) { + setSequence(sequence_, framerate); + } +} + +void MultiFileSource::setLoop (bool on) +{ + if (multifile()) { + sequence_.loop = on ? 1 : 0; + multifile()->setLoop( sequence_.loop ); + } +} + +void MultiFileSource::setRange (glm::ivec2 range) +{ + range_.x = glm::clamp( range.x, sequence_.min, sequence_.max ); + range_.y = glm::clamp( range.y, sequence_.min, sequence_.max ); + range_.x = glm::min( range_.x, range_.y ); + range_.y = glm::max( range_.x, range_.y ); + + if (multifile()) + multifile()->setRange( range_.x, range_.y ); +} + +void MultiFileSource::accept(Visitor& v) +{ + Source::accept(v); + if (!failed()) + v.visit(*this); +} + +MultiFile *MultiFileSource::multifile() const +{ + return dynamic_cast(stream_); +} + + diff --git a/MultiFileSource.h b/MultiFileSource.h new file mode 100644 index 0000000..52d27c0 --- /dev/null +++ b/MultiFileSource.h @@ -0,0 +1,74 @@ +#ifndef MULTIFILESOURCE_H +#define MULTIFILESOURCE_H + +#include +#include + +#include "StreamSource.h" + +struct MultiFileSequence { + std::string location; + std::string codec; + uint width; + uint height; + int min; + int max; + int loop; + + MultiFileSequence (); + MultiFileSequence (const std::list &list_files); + bool valid () const; + MultiFileSequence& operator = (const MultiFileSequence& b); + bool operator != (const MultiFileSequence& b); +}; + +class MultiFile : public Stream +{ +public: + MultiFile (); + void open (const MultiFileSequence &sequence, uint framerate = 30); + + void setRange(int begin, int end); + void setLoop (int on); + +protected: + GstElement *src_ ; +}; + +class MultiFileSource : public StreamSource +{ +public: + MultiFileSource (uint64_t id = 0); + + // Source interface + void accept (Visitor& v) override; + + // StreamSource interface + Stream *stream () const override { return stream_; } + glm::ivec2 icon () const override { return glm::ivec2(3, 9); } + + // specific interface + void setFiles (const std::list &list_files, uint framerate); + + void setSequence (const MultiFileSequence &sequence, uint framerate); + inline MultiFileSequence sequence () const { return sequence_; } + + void setFramerate (uint fps); + inline uint framerate () const { return framerate_; } + + void setLoop (bool on); + inline bool loop () const { return sequence_.loop; } + + void setRange (glm::ivec2 range); + glm::ivec2 range () const { return range_; } + + MultiFile *multifile () const; + +private: + MultiFileSequence sequence_; + uint framerate_; + glm::ivec2 range_; +}; + + +#endif // MULTIFILESOURCE_H diff --git a/Overlay.cpp b/Overlay.cpp new file mode 100644 index 0000000..ccef13b --- /dev/null +++ b/Overlay.cpp @@ -0,0 +1,23 @@ +#include "Overlay.h" + +Overlay::Overlay() +{ + +} + +SnapshotOverlay::SnapshotOverlay() : Overlay() +{ + +} + +void SnapshotOverlay::draw() +{ + +} + + +void SnapshotOverlay::update (float dt) +{ + + +} diff --git a/Overlay.h b/Overlay.h new file mode 100644 index 0000000..81dc87c --- /dev/null +++ b/Overlay.h @@ -0,0 +1,38 @@ +#ifndef OVERLAY_H +#define OVERLAY_H + +#include "View.h" + +class Overlay +{ +public: + Overlay(); + + virtual void update (float dt) = 0; + virtual void draw () = 0; + + virtual std::pair pick(glm::vec2) { + return { nullptr, glm::vec2(0.f) }; + } + + virtual View::Cursor grab (Source*, glm::vec2, glm::vec2, std::pair) { + return View::Cursor (); + } + + virtual View::Cursor over (glm::vec2) { + return View::Cursor (); + } +}; + + +class SnapshotOverlay: public Overlay +{ +public: + SnapshotOverlay(); + + void draw () override; + void update (float dt) override; +}; + + +#endif // OVERLAY_H diff --git a/RenderingManager.cpp b/RenderingManager.cpp index 7ba0a5e..d2d6b57 100644 --- a/RenderingManager.cpp +++ b/RenderingManager.cpp @@ -424,14 +424,15 @@ glm::vec2 Rendering::project(glm::vec3 scene_coordinate, glm::mat4 modelview, bo void Rendering::FileDropped(GLFWwindow *, int path_count, const char* paths[]) { - for (int i = 0; i < path_count; ++i) { + int i = 0; + for (; i < path_count; ++i) { std::string filename(paths[i]); if (filename.empty()) break; // try to create a source Mixer::manager().addSource ( Mixer::manager().createSourceFile( filename ) ); } - if (path_count>0) { + if (i>0) { UserInterface::manager().showPannel(); Rendering::manager().mainWindow().show(); } diff --git a/SessionCreator.cpp b/SessionCreator.cpp index a9d80a7..083c5a6 100644 --- a/SessionCreator.cpp +++ b/SessionCreator.cpp @@ -12,6 +12,7 @@ #include "PatternSource.h" #include "DeviceSource.h" #include "NetworkSource.h" +#include "MultiFileSource.h" #include "Session.h" #include "ImageShader.h" #include "ImageProcessingShader.h" @@ -267,6 +268,9 @@ void SessionLoader::load(XMLElement *sessionNode) else if ( std::string(pType) == "NetworkSource") { load_source = new NetworkSource(id_xml_); } + else if ( std::string(pType) == "MultiFileSource") { + load_source = new MultiFileSource(id_xml_); + } // skip failed (including clones) if (!load_source) @@ -384,6 +388,9 @@ Source *SessionLoader::createSource(tinyxml2::XMLElement *sourceNode, Mode mode) else if ( std::string(pType) == "NetworkSource") { load_source = new NetworkSource(id__); } + else if ( std::string(pType) == "MultiFileSource") { + load_source = new MultiFileSource(id__); + } else if ( std::string(pType) == "CloneSource") { // clone from given origin XMLElement* originNode = xmlCurrent_->FirstChildElement("origin"); @@ -880,3 +887,37 @@ void SessionLoader::visit (NetworkSource& s) } +void SessionLoader::visit (MultiFileSource& s) +{ + XMLElement* seq = xmlCurrent_->FirstChildElement("Sequence"); + + if (seq) { + + MultiFileSequence sequence; + sequence.location = std::string ( seq->GetText() ); + seq->QueryIntAttribute("min", &sequence.min); + seq->QueryIntAttribute("max", &sequence.max); + seq->QueryIntAttribute("loop", &sequence.loop); + seq->QueryUnsignedAttribute("width", &sequence.width); + seq->QueryUnsignedAttribute("height", &sequence.height); + const char *codec = seq->Attribute("codec"); + if (codec) + sequence.codec = std::string(codec); + uint fps = 0; + seq->QueryUnsignedAttribute("fps", &fps); + + // different sequence + if ( sequence != s.sequence() ) { + s.setSequence( sequence, fps); + } + // same sequence, different framerate + else if ( fps != s.framerate() ) { + s.setFramerate( fps ); + } + + + } + +} + + diff --git a/SessionCreator.h b/SessionCreator.h index 95e242c..321c1ba 100644 --- a/SessionCreator.h +++ b/SessionCreator.h @@ -58,6 +58,7 @@ public: void visit (PatternSource& s) override; void visit (DeviceSource& s) override; void visit (NetworkSource& s) override; + void visit (MultiFileSource& s) override; static void XMLToNode(const tinyxml2::XMLElement *xml, Node &n); static void XMLToSourcecore(tinyxml2::XMLElement *xml, SourceCore &s); diff --git a/SessionVisitor.cpp b/SessionVisitor.cpp index 56fcd81..2361595 100644 --- a/SessionVisitor.cpp +++ b/SessionVisitor.cpp @@ -11,6 +11,7 @@ #include "PatternSource.h" #include "DeviceSource.h" #include "NetworkSource.h" +#include "MultiFileSource.h" #include "ImageShader.h" #include "ImageProcessingShader.h" #include "MediaPlayer.h" @@ -593,6 +594,36 @@ void SessionVisitor::visit (MixingGroup& g) } } +void SessionVisitor::visit (MultiFileSource& s) +{ + xmlCurrent_->SetAttribute("type", "MultiFileSource"); + + XMLElement *sequence = xmlDoc_->NewElement("Sequence"); + sequence->SetAttribute("fps", s.framerate()); + sequence->SetAttribute("min", s.sequence().min); + sequence->SetAttribute("max", s.sequence().max); + sequence->SetAttribute("loop", s.sequence().loop); + sequence->SetAttribute("width", s.sequence().width); + sequence->SetAttribute("height", s.sequence().height); + sequence->SetAttribute("codec", s.sequence().codec.c_str()); + XMLText *location = xmlDoc_->NewText( s.sequence().location.c_str() ); + sequence->InsertEndChild( location ); + + xmlCurrent_->InsertEndChild(sequence); + +// XMLElement *sequence = xmlDoc_->NewElement("Sequence"); +// sequence->SetAttribute("fps", s.fps()); + +// std::list list = s.sequence (); +// for (auto it = list.cbegin(); it != list.cend(); ++it) { +// XMLElement *f = xmlDoc_->NewElement("file"); +// XMLText *filename = xmlDoc_->NewText( it->c_str() ); +// f->InsertEndChild( filename ); +// sequence->InsertEndChild(f); +// } +// xmlCurrent_->InsertEndChild(sequence); +} + std::string SessionVisitor::getClipboard(const SourceList &list) { std::string x = ""; diff --git a/SessionVisitor.h b/SessionVisitor.h index 49226e5..35961e0 100644 --- a/SessionVisitor.h +++ b/SessionVisitor.h @@ -28,26 +28,26 @@ public: static std::string getClipboard(ImageProcessingShader * const s); // Elements of Scene - void visit(Scene& n) override; - void visit(Node& n) override; - void visit(Group& n) override; - void visit(Switch& n) override; - void visit(Primitive& n) override; - void visit(Surface&) override; - void visit(ImageSurface& n) override; - void visit(MediaSurface& n) override; - void visit(FrameBufferSurface&) override; - void visit(LineStrip& n) override; - void visit(LineSquare&) override; - void visit(Mesh& n) override; - void visit(Frame& n) override; + void visit (Scene& n) override; + void visit (Node& n) override; + void visit (Group& n) override; + void visit (Switch& n) override; + void visit (Primitive& n) override; + void visit (Surface&) override; + void visit (ImageSurface& n) override; + void visit (MediaSurface& n) override; + void visit (FrameBufferSurface&) override; + void visit (LineStrip& n) override; + void visit (LineSquare&) override; + void visit (Mesh& n) override; + void visit (Frame& n) override; // Elements with attributes - void visit(MediaPlayer& n) override; - void visit(Shader& n) override; - void visit(ImageShader& n) override; - void visit(MaskShader& n) override; - void visit(ImageProcessingShader& n) override; + void visit (MediaPlayer& n) override; + void visit (Shader& n) override; + void visit (ImageShader& n) override; + void visit (MaskShader& n) override; + void visit (ImageProcessingShader& n) override; // Sources void visit (Source& s) override; @@ -60,6 +60,7 @@ public: void visit (DeviceSource& s) override; void visit (NetworkSource& s) override; void visit (MixingGroup& s) override; + void visit (MultiFileSource& s) override; static tinyxml2::XMLElement *NodeToXML(const Node &n, tinyxml2::XMLDocument *doc); static tinyxml2::XMLElement *ImageToXML(const FrameBufferImage *img, tinyxml2::XMLDocument *doc); diff --git a/Stream.cpp b/Stream.cpp index e05a611..cb879b8 100644 --- a/Stream.cpp +++ b/Stream.cpp @@ -78,7 +78,7 @@ guint Stream::texture() const } -void Stream::open(const std::string &gstreamer_description, int w, int h) +void Stream::open(const std::string &gstreamer_description, guint w, guint h) { // set gstreamer pipeline source description_ = gstreamer_description; diff --git a/Stream.h b/Stream.h index e879dc2..0708b07 100644 --- a/Stream.h +++ b/Stream.h @@ -33,7 +33,7 @@ public: /** * Open a media using gstreamer pipeline keyword * */ - void open(const std::string &gstreamer_description, int w = 1024, int h = 576); + void open(const std::string &gstreamer_description, guint w = 1024, guint h = 576); /** * Get description string * */ diff --git a/SystemToolkit.cpp b/SystemToolkit.cpp index 74bbf67..c021f5c 100644 --- a/SystemToolkit.cpp +++ b/SystemToolkit.cpp @@ -134,7 +134,10 @@ string SystemToolkit::path_filename(const string& path) string SystemToolkit::extension_filename(const string& filename) { - string ext = filename.substr(filename.find_last_of(".") + 1); + string ext; + auto loc = filename.find_last_of("."); + if (loc != string::npos) + ext = filename.substr( loc + 1 ); return ext; } @@ -274,7 +277,7 @@ std::string SystemToolkit::path_directory(const std::string& path) return directorypath; } -list SystemToolkit::list_directory(const string& path, const string& filter) +list SystemToolkit::list_directory(const string& path, const list& extensions) { list ls; @@ -286,7 +289,8 @@ list SystemToolkit::list_directory(const string& path, const string& fil if ( ent->d_type == DT_REG) { string filename = string(ent->d_name); - if ( extension_filename(filename) == filter) + string ext = extension_filename(filename); + if ( extensions.empty() || find(extensions.cbegin(), extensions.cend(), ext) != extensions.cend()) ls.push_back( full_filename(path, filename) ); } } diff --git a/SystemToolkit.h b/SystemToolkit.h index 0bf3809..1b6f08e 100644 --- a/SystemToolkit.h +++ b/SystemToolkit.h @@ -49,7 +49,7 @@ namespace SystemToolkit std::string path_directory(const std::string& path); // list all files of a directory mathing the given filter extension (if any) - std::list list_directory(const std::string& path, const std::string& filter = ""); + std::list list_directory(const std::string& path, const std::list &extensions); // true of file exists bool file_exists(const std::string& path); diff --git a/UserInterfaceManager.cpp b/UserInterfaceManager.cpp index dcc97df..ce70f14 100644 --- a/UserInterfaceManager.cpp +++ b/UserInterfaceManager.cpp @@ -1100,7 +1100,7 @@ void UserInterface::RenderPreview() ImGui::Combo("Path", &selected_path, name_path, 4); if (selected_path > 2) { if (recordFolderFileDialogs.empty()) { - recordFolderFileDialogs.emplace_back( std::async(std::launch::async, DialogToolkit::FolderDialog, Settings::application.record.path) ); + recordFolderFileDialogs.emplace_back( std::async(std::launch::async, DialogToolkit::openFolderDialog, Settings::application.record.path) ); fileDialogPending_ = true; } } @@ -1413,67 +1413,70 @@ void UserInterface::RenderShaderEditor() { static bool show_statusbar = true; - if ( ImGui::Begin(IMGUI_TITLE_SHADEREDITOR, &Settings::application.widget.shader_editor, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_MenuBar) ) + if ( !ImGui::Begin(IMGUI_TITLE_SHADEREDITOR, &Settings::application.widget.shader_editor, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_MenuBar) ) { - ImGui::SetWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver); - if (ImGui::BeginMenuBar()) - { - if (ImGui::BeginMenu("Edit")) - { - bool ro = editor.IsReadOnly(); - if (ImGui::MenuItem("Read-only mode", nullptr, &ro)) - editor.SetReadOnly(ro); - ImGui::Separator(); - - if (ImGui::MenuItem( ICON_FA_UNDO " Undo", CTRL_MOD "Z", nullptr, !ro && editor.CanUndo())) - editor.Undo(); - if (ImGui::MenuItem( ICON_FA_REDO " Redo", CTRL_MOD "Y", nullptr, !ro && editor.CanRedo())) - editor.Redo(); - - ImGui::Separator(); - - if (ImGui::MenuItem( ICON_FA_COPY " Copy", CTRL_MOD "C", nullptr, editor.HasSelection())) - editor.Copy(); - if (ImGui::MenuItem( ICON_FA_CUT " Cut", CTRL_MOD "X", nullptr, !ro && editor.HasSelection())) - editor.Cut(); - if (ImGui::MenuItem( ICON_FA_ERASER " Delete", "Del", nullptr, !ro && editor.HasSelection())) - editor.Delete(); - if (ImGui::MenuItem( ICON_FA_PASTE " Paste", CTRL_MOD "V", nullptr, !ro && ImGui::GetClipboardText() != nullptr)) - editor.Paste(); - - ImGui::Separator(); - - if (ImGui::MenuItem( "Select all", nullptr, nullptr)) - editor.SetSelection(TextEditor::Coordinates(), TextEditor::Coordinates(editor.GetTotalLines(), 0)); - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("View")) - { - bool ws = editor.IsShowingWhitespaces(); - if (ImGui::MenuItem( ICON_FA_LONG_ARROW_ALT_RIGHT " Whitespace", nullptr, &ws)) - editor.SetShowWhitespaces(ws); - ImGui::MenuItem( ICON_FA_WINDOW_MAXIMIZE " Statusbar", nullptr, &show_statusbar); - ImGui::EndMenu(); - } - ImGui::EndMenuBar(); - } - - if (show_statusbar) { - auto cpos = editor.GetCursorPosition(); - ImGui::Text("%6d/%-6d %6d lines | %s | %s | %s ", cpos.mLine + 1, cpos.mColumn + 1, editor.GetTotalLines(), - editor.IsOverwrite() ? "Ovr" : "Ins", - editor.CanUndo() ? "*" : " ", - editor.GetLanguageDefinition().mName.c_str()); - } - - ImGuiToolkit::PushFont(ImGuiToolkit::FONT_MONO); - editor.Render("ShaderEditor"); - ImGui::PopFont(); - ImGui::End(); + return; } + + ImGui::SetWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver); + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Edit")) + { + bool ro = editor.IsReadOnly(); + if (ImGui::MenuItem("Read-only mode", nullptr, &ro)) + editor.SetReadOnly(ro); + ImGui::Separator(); + + if (ImGui::MenuItem( ICON_FA_UNDO " Undo", CTRL_MOD "Z", nullptr, !ro && editor.CanUndo())) + editor.Undo(); + if (ImGui::MenuItem( ICON_FA_REDO " Redo", CTRL_MOD "Y", nullptr, !ro && editor.CanRedo())) + editor.Redo(); + + ImGui::Separator(); + + if (ImGui::MenuItem( ICON_FA_COPY " Copy", CTRL_MOD "C", nullptr, editor.HasSelection())) + editor.Copy(); + if (ImGui::MenuItem( ICON_FA_CUT " Cut", CTRL_MOD "X", nullptr, !ro && editor.HasSelection())) + editor.Cut(); + if (ImGui::MenuItem( ICON_FA_ERASER " Delete", "Del", nullptr, !ro && editor.HasSelection())) + editor.Delete(); + if (ImGui::MenuItem( ICON_FA_PASTE " Paste", CTRL_MOD "V", nullptr, !ro && ImGui::GetClipboardText() != nullptr)) + editor.Paste(); + + ImGui::Separator(); + + if (ImGui::MenuItem( "Select all", nullptr, nullptr)) + editor.SetSelection(TextEditor::Coordinates(), TextEditor::Coordinates(editor.GetTotalLines(), 0)); + + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("View")) + { + bool ws = editor.IsShowingWhitespaces(); + if (ImGui::MenuItem( ICON_FA_LONG_ARROW_ALT_RIGHT " Whitespace", nullptr, &ws)) + editor.SetShowWhitespaces(ws); + ImGui::MenuItem( ICON_FA_WINDOW_MAXIMIZE " Statusbar", nullptr, &show_statusbar); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + if (show_statusbar) { + auto cpos = editor.GetCursorPosition(); + ImGui::Text("%6d/%-6d %6d lines | %s | %s | %s ", cpos.mLine + 1, cpos.mColumn + 1, editor.GetTotalLines(), + editor.IsOverwrite() ? "Ovr" : "Ins", + editor.CanUndo() ? "*" : " ", + editor.GetLanguageDefinition().mName.c_str()); + } + + ImGuiToolkit::PushFont(ImGuiToolkit::FONT_MONO); + editor.Render("ShaderEditor"); + ImGui::PopFont(); + + ImGui::End(); } void UserInterface::RenderMetrics(bool *p_open, int* p_corner, int *p_mode) @@ -2330,6 +2333,7 @@ void Navigator::clearButtonSelection() // clear new source pannel new_source_preview_.setSource(); pattern_type = -1; + _selectedFiles.clear(); } void Navigator::showPannelSource(int index) @@ -2653,18 +2657,11 @@ void Navigator::RenderNewPannel() ImGui::SetCursorPosY(width_); ImGui::Text("Source"); -// ImGui::SameLine(); -// ImGui::SetCursorPosX(pannel_width_ IMGUI_RIGHT_ALIGN); -// if (ImGui::BeginMenu("Edit")) -// { -// UserInterface::manager().showMenuEdit(); -// ImGui::EndMenu(); -// } - // // News Source selection pannel // - static const char* origin_names[4] = { ICON_FA_PHOTO_VIDEO " File", + static const char* origin_names[5] = { ICON_FA_PHOTO_VIDEO " File", + ICON_FA_SORT_NUMERIC_DOWN " Sequence", ICON_FA_SYNC " Internal", ICON_FA_COG " Generated", ICON_FA_PLUG " Connected" @@ -2673,16 +2670,16 @@ void Navigator::RenderNewPannel() if (ImGui::Combo("##Origin", &Settings::application.source.new_type, origin_names, IM_ARRAYSIZE(origin_names)) ) new_source_preview_.setSource(); + ImGui::SetCursorPosY(2.f * width_); + // File Source creation if (Settings::application.source.new_type == 0) { - ImGui::SetCursorPosY(2.f * width_); - // clic button to load file if ( ImGui::Button( ICON_FA_FILE_EXPORT " Open file", ImVec2(ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN, 0)) ) { // launch async call to file dialog and get its future. if (fileImportFileDialogs.empty()) { - fileImportFileDialogs.emplace_back( std::async(std::launch::async, DialogToolkit::ImportFileDialog, Settings::application.recentImport.path) ); + fileImportFileDialogs.emplace_back( std::async(std::launch::async, DialogToolkit::openMediaFileDialog, Settings::application.recentImport.path) ); fileDialogPending_ = true; } } @@ -2730,10 +2727,74 @@ void Navigator::RenderNewPannel() ImGui::EndCombo(); } } - // Internal Source creator + // Folder Source creator else if (Settings::application.source.new_type == 1){ - ImGui::SetCursorPosY(2.f * width_); + bool update_new_source = false; + static std::vector< std::future< std::list > > _selectedImagesFileDialogs; + + // clic button to load file + if ( ImGui::Button( ICON_FA_IMAGES " Open images", ImVec2(ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN, 0)) ) { + _selectedFiles.clear(); + if (_selectedImagesFileDialogs.empty()) { + _selectedImagesFileDialogs.emplace_back( std::async(std::launch::async, DialogToolkit::selectImagesFileDialog, Settings::application.recentImport.path) ); + fileDialogPending_ = true; + } + } + + // Indication + ImGui::SameLine(); + ImGuiToolkit::HelpMarker("Create a source from a sequence of numbered images."); + + // return from thread for folder openning + if ( !_selectedImagesFileDialogs.empty() ) { + // check that file dialog thread finished + if (_selectedImagesFileDialogs.back().wait_for(timeout) == std::future_status::ready ) { + // get the filenames from this file dialog + _selectedFiles = _selectedImagesFileDialogs.back().get(); + // done with this file dialog + _selectedImagesFileDialogs.pop_back(); + fileDialogPending_ = false; + // ask to reload the preview + update_new_source = true; + } + } + + // a selection was made + if (!_selectedFiles.empty()) { + + // set framerate + static int _fps = 30; + static bool _fps_changed = false; + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + if ( ImGui::SliderInt("Framerate", &_fps, 1, 30, "%d fps") ) { + _fps_changed = true; + } + // only call for new source after mouse release to avoid repeating call to re-open the stream + else if (_fps_changed && ImGui::IsMouseReleased(ImGuiMouseButton_Left)){ + update_new_source = true; + _fps_changed = false; + } + + if (update_new_source) { + // multiple files selected + if (_selectedFiles.size() > 1) { + std::string label = BaseToolkit::transliterate( _selectedFiles.front() ); + label = BaseToolkit::trunc_string(label, 35); + new_source_preview_.setSource( Mixer::manager().createSourceMultifile(_selectedFiles, _fps), label); + } + // single file selected + else { + std::string label = BaseToolkit::transliterate( _selectedFiles.front() ); + label = BaseToolkit::trunc_string(label, 35); + new_source_preview_.setSource( Mixer::manager().createSourceFile(_selectedFiles.front()), label); + } + } + } + + } + // Internal Source creator + else if (Settings::application.source.new_type == 2){ // fill new_source_preview with a new source ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); @@ -2760,9 +2821,7 @@ void Navigator::RenderNewPannel() ImGuiToolkit::HelpMarker("Create a source replicating internal vimix objects."); } // Generated Source creator - else if (Settings::application.source.new_type == 2){ - - ImGui::SetCursorPosY(2.f * width_); + else if (Settings::application.source.new_type == 3){ bool update_new_source = false; @@ -2804,9 +2863,7 @@ void Navigator::RenderNewPannel() } } // External source creator - else if (Settings::application.source.new_type == 3){ - - ImGui::SetCursorPosY(2.f * width_); + else if (Settings::application.source.new_type == 4){ ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); if (ImGui::BeginCombo("##External", "Select device")) @@ -2838,7 +2895,7 @@ void Navigator::RenderNewPannel() // if a new source was added if (new_source_preview_.filled()) { // show preview - new_source_preview_.Render(ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN, Settings::application.source.new_type != 1); + new_source_preview_.Render(ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN, Settings::application.source.new_type != 2); // ask to import the source in the mixer ImGui::NewLine(); if (new_source_preview_.ready() && ImGui::Button( ICON_FA_CHECK " Create", ImVec2(pannel_width_ - padding_width_, 0)) ) { @@ -2876,44 +2933,6 @@ void Navigator::RenderMainPannelVimix() ImGui::SetCursorPosY(width_); -// // -// // Buttons to show WINDOWS -// // -// ImGui::Spacing(); -// ImGui::Text("Windows"); -// ImGui::Spacing(); - -// ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); -// std::string tooltip_ = ""; - -// if ( ImGuiToolkit::IconButton( Rendering::manager().mainWindow().isFullscreen() ? ICON_FA_COMPRESS_ALT : ICON_FA_EXPAND_ALT ) ) -// Rendering::manager().mainWindow().toggleFullscreen(); -// if (ImGui::IsItemHovered()) -// tooltip_ = "Fullscreen " CTRL_MOD "Shift+F"; - -// ImGui::SameLine(0, 40); -// if ( ImGuiToolkit::IconButton( ICON_FA_STICKY_NOTE ) ) -// Mixer::manager().session()->addNote(); -// if (ImGui::IsItemHovered()) -// tooltip_ = "New note " CTRL_MOD "Shift+N"; - -// ImGui::SameLine(0, 40); -// if ( ImGuiToolkit::IconButton( ICON_FA_FILM ) ) -// Settings::application.widget.media_player = true; -// if (ImGui::IsItemHovered()) -// tooltip_ = "Player " CTRL_MOD "P"; - -// ImGui::SameLine(0, 40); -// if ( ImGuiToolkit::IconButton( ICON_FA_DESKTOP ) ) -// Settings::application.widget.preview = true; -// if (ImGui::IsItemHovered()) -// tooltip_ = "Output " CTRL_MOD "D"; - -// ImGui::PopFont(); -// if (!tooltip_.empty()) { -// ImGuiToolkit::ToolTip(tooltip_.substr(0, tooltip_.size()-12).c_str(), tooltip_.substr(tooltip_.size()-12, 12).c_str()); -// } - // // SESSION panel // @@ -2947,7 +2966,7 @@ void Navigator::RenderMainPannelVimix() // Option 2 : add a folder if (ImGui::Selectable( ICON_FA_FOLDER_PLUS " Add Folder") ){ if (recentFolderFileDialogs.empty()) { - recentFolderFileDialogs.emplace_back( std::async(std::launch::async, DialogToolkit::FolderDialog, Settings::application.recentFolders.path) ); + recentFolderFileDialogs.emplace_back( std::async(std::launch::async, DialogToolkit::openFolderDialog, Settings::application.recentFolders.path) ); fileDialogPending_ = true; } } @@ -3016,7 +3035,7 @@ void Navigator::RenderMainPannelVimix() // selection MODE 1 : LIST FOLDER else if ( selection_session_mode == 1) { // show list of vimix files in folder - sessions_list = SystemToolkit::list_directory( Settings::application.recentFolders.path, "mix"); + sessions_list = SystemToolkit::list_directory( Settings::application.recentFolders.path, {"mix", "MIX"}); } // indicate the list changed (do not change at every frame) selection_session_mode_changed = false; @@ -3119,8 +3138,8 @@ void Navigator::RenderMainPannelVimix() ImGui::SetCursorPos( ImVec2( pannel_width_ IMGUI_RIGHT_ALIGN, pos_bot.y - 2.f * ImGui::GetFrameHeightWithSpacing())); ImGuiToolkit::HelpMarker("Select the history of recently\n" - "opened files or a folder, and\n" - "double-clic a filename to open it.\n\n" + "opened files or a folder.\n" + "Double-clic a filename to open.\n\n" ICON_FA_ARROW_CIRCLE_RIGHT " Enable smooth transition to\n" "perform smooth cross fading."); // toggle button for smooth transition @@ -3349,16 +3368,22 @@ void Navigator::RenderMainPannelVimix() if (ImGui::IsItemHovered()) ImGuiToolkit::ToolTip("Take Snapshot ", CTRL_MOD "Y"); - ImGui::SetCursorPos( ImVec2( pannel_width_ IMGUI_RIGHT_ALIGN, pos_bot.y - 2.f * ImGui::GetFrameHeightWithSpacing())); - ImGuiToolkit::HelpMarker("Snapshots capture the state of the session.\n" - "Double-clic on a snapshot to restore it.\n\n" - ICON_FA_ROUTE " Enable interpolation to animate\n" - "from current state to snapshot's state."); - // toggle button for smooth interpolation - ImGui::SetCursorPos( ImVec2( pannel_width_ IMGUI_RIGHT_ALIGN, pos_bot.y - ImGui::GetFrameHeightWithSpacing()) ); - ImGuiToolkit::ButtonToggle(ICON_FA_ROUTE, &Settings::application.smooth_snapshot); - if (ImGui::IsItemHovered()) - ImGuiToolkit::ToolTip("Snapshot interpolation"); + ImGui::SetCursorPos( ImVec2( pannel_width_ IMGUI_RIGHT_ALIGN, pos_bot.y - ImGui::GetFrameHeightWithSpacing())); + ImGuiToolkit::HelpMarker("Snapshots keeps a list of favorite\n" + "status of the current session.\n" + "Clic an item to preview or edit.\n" + "Double-clic to restore immediately.\n"); + +// ImGui::SetCursorPos( ImVec2( pannel_width_ IMGUI_RIGHT_ALIGN, pos_bot.y - 2.f * ImGui::GetFrameHeightWithSpacing())); +// ImGuiToolkit::HelpMarker("Snapshots capture the state of the session.\n" +// "Double-clic on a snapshot to restore it.\n\n" +// ICON_FA_ROUTE " Enable interpolation to animate\n" +// "from current state to snapshot's state."); +// // toggle button for smooth interpolation +// ImGui::SetCursorPos( ImVec2( pannel_width_ IMGUI_RIGHT_ALIGN, pos_bot.y - ImGui::GetFrameHeightWithSpacing()) ); +// ImGuiToolkit::ButtonToggle(ICON_FA_ROUTE, &Settings::application.smooth_snapshot); +// if (ImGui::IsItemHovered()) +// ImGuiToolkit::ToolTip("Snapshot interpolation"); // if (Action::manager().currentSnapshot() > 0) { // ImGui::SetCursorPos( pos_bot ); @@ -3368,6 +3393,8 @@ void Navigator::RenderMainPannelVimix() // Action::manager().interpolate( static_cast ( interpolation ) * 0.01f ); // } + + ImGui::SetCursorPos( pos_bot ); } // @@ -3442,7 +3469,7 @@ void Navigator::RenderMainPannelSettings() #ifndef NDEBUG ImGui::Text("Expert"); - ImGuiToolkit::ButtonSwitch( IMGUI_TITLE_HISTORY, &Settings::application.widget.history); +// ImGuiToolkit::ButtonSwitch( IMGUI_TITLE_HISTORY, &Settings::application.widget.history); ImGuiToolkit::ButtonSwitch( IMGUI_TITLE_SHADEREDITOR, &Settings::application.widget.shader_editor, CTRL_MOD "E"); ImGuiToolkit::ButtonSwitch( IMGUI_TITLE_TOOLBOX, &Settings::application.widget.toolbox, CTRL_MOD "T"); ImGuiToolkit::ButtonSwitch( IMGUI_TITLE_LOGS, &Settings::application.widget.logs, CTRL_MOD "L"); diff --git a/UserInterfaceManager.h b/UserInterfaceManager.h index 42f4cc2..ba10fd2 100644 --- a/UserInterfaceManager.h +++ b/UserInterfaceManager.h @@ -60,6 +60,7 @@ class Navigator bool view_pannel_visible; bool selected_button[NAV_COUNT]; int pattern_type; + std::list _selectedFiles; void clearButtonSelection(); void applyButtonSelection(int index); diff --git a/View.h b/View.h index b62c5dd..c8da86e 100644 --- a/View.h +++ b/View.h @@ -80,7 +80,7 @@ public: return Cursor (); } - //TODO: test mouse over provided a point in screen coordinates + // test mouse over provided a point in screen coordinates virtual Cursor over (glm::vec2) { return Cursor (); } diff --git a/Visitor.h b/Visitor.h index 2842cdd..b6e0262 100644 --- a/Visitor.h +++ b/Visitor.h @@ -38,6 +38,7 @@ class RenderSource; class CloneSource; class NetworkSource; class MixingGroup; +class MultiFileSource; // Declares the interface for the visitors class Visitor { @@ -81,6 +82,7 @@ public: virtual void visit (SessionGroupSource&) {} virtual void visit (RenderSource&) {} virtual void visit (CloneSource&) {} + virtual void visit (MultiFileSource&) {} }; diff --git a/rsc/mesh/icon_sequence.ply b/rsc/mesh/icon_sequence.ply new file mode 100644 index 0000000..5396220 --- /dev/null +++ b/rsc/mesh/icon_sequence.ply @@ -0,0 +1,144 @@ +ply +format ascii 1.0 +comment Created by Blender 2.92.0 - www.blender.org +element vertex 73 +property float x +property float y +property float z +element face 61 +property list uchar uint vertex_indices +end_header +-0.073338 -0.096730 0.001250 +0.088477 -0.052608 0.001250 +-0.088049 -0.052608 0.001250 +0.073767 -0.096730 0.001250 +-0.080068 -0.037900 0.001250 +-0.071205 0.006222 0.001250 +-0.088049 0.020929 0.001250 +0.088477 0.020929 0.001250 +0.071568 0.006222 0.001250 +0.080254 -0.037900 0.001250 +-0.065218 -0.037900 0.001250 +0.065397 -0.037900 0.001250 +-0.038191 -0.001199 0.001250 +-0.035936 -0.001199 0.001250 +-0.037069 -0.001132 0.001250 +-0.034843 -0.001395 0.001250 +-0.039256 -0.001396 0.001250 +-0.033799 -0.001711 0.001250 +-0.040255 -0.001712 0.001250 +-0.032816 -0.002139 0.001250 +-0.041178 -0.002141 0.001250 +-0.031903 -0.002669 0.001250 +-0.042017 -0.002672 0.001250 +-0.031070 -0.003293 0.001250 +-0.042761 -0.003296 0.001250 +-0.030329 -0.004002 0.001250 +-0.043403 -0.004006 0.001250 +-0.029690 -0.004787 0.001250 +-0.043932 -0.004792 0.001250 +-0.029162 -0.005639 0.001250 +-0.044340 -0.005646 0.001250 +-0.028756 -0.006551 0.001250 +-0.044617 -0.006558 0.001250 +-0.028484 -0.007512 0.001250 +-0.044753 -0.007519 0.001250 +-0.002441 -0.026583 0.001250 +0.036991 -0.037900 0.001250 +0.010975 -0.006875 0.001250 +-0.028353 -0.008515 0.001250 +-0.044741 -0.008522 0.001250 +-0.028373 -0.009518 0.001250 +-0.044580 -0.009525 0.001250 +-0.028536 -0.010479 0.001250 +-0.044282 -0.010486 0.001250 +-0.028833 -0.011391 0.001250 +-0.043857 -0.011398 0.001250 +-0.029256 -0.012245 0.001250 +-0.043315 -0.012250 0.001250 +-0.029794 -0.013031 0.001250 +-0.042667 -0.013035 0.001250 +-0.030439 -0.013740 0.001250 +-0.041923 -0.013744 0.001250 +-0.031181 -0.014365 0.001250 +-0.041094 -0.014368 0.001250 +-0.032011 -0.014896 0.001250 +-0.040189 -0.014898 0.001250 +-0.032921 -0.015324 0.001250 +-0.039221 -0.015326 0.001250 +-0.033901 -0.015641 0.001250 +-0.038198 -0.015642 0.001250 +-0.034941 -0.015838 0.001250 +-0.037132 -0.015838 0.001250 +-0.036032 -0.015905 0.001250 +-0.036562 -0.037900 0.001250 +-0.015776 -0.016935 0.001250 +-0.074375 0.035637 0.001250 +0.078371 0.050344 0.001250 +-0.077972 0.050344 0.001250 +0.074760 0.035637 0.001250 +-0.062747 0.065052 0.001250 +0.066375 0.079759 0.001250 +-0.065976 0.079759 0.001250 +0.063146 0.065052 0.001250 +3 0 1 2 +3 0 3 1 +3 4 5 6 +3 5 7 6 +3 5 8 7 +3 8 9 7 +3 4 10 5 +3 11 9 8 +3 12 13 14 +3 12 15 13 +3 16 15 12 +3 16 17 15 +3 18 17 16 +3 18 19 17 +3 20 19 18 +3 20 21 19 +3 22 21 20 +3 22 23 21 +3 24 23 22 +3 24 25 23 +3 26 25 24 +3 26 27 25 +3 28 27 26 +3 28 29 27 +3 30 29 28 +3 30 31 29 +3 32 31 30 +3 32 33 31 +3 34 33 32 +3 35 36 37 +3 34 38 33 +3 39 38 34 +3 39 40 38 +3 41 40 39 +3 41 42 40 +3 43 42 41 +3 43 44 42 +3 45 44 43 +3 45 46 44 +3 47 46 45 +3 47 48 46 +3 49 48 47 +3 49 50 48 +3 51 50 49 +3 51 52 50 +3 53 52 51 +3 53 54 52 +3 55 54 53 +3 55 56 54 +3 57 56 55 +3 57 58 56 +3 59 58 57 +3 59 60 58 +3 61 60 59 +3 61 62 60 +3 63 35 64 +3 63 36 35 +3 65 66 67 +3 65 68 66 +3 69 70 71 +3 69 72 70 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 032028c..980ac06 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: vimix base: core18 -version: '0.5' +version: '0.6' summary: Live video mixing title: vimix description: |