diff --git a/app.cpp b/app.cpp index c6e4c0d..1fbf335 100644 --- a/app.cpp +++ b/app.cpp @@ -519,6 +519,10 @@ void LoadFile(std::string path) { return; } + // Force inspector to reload marker and effect lists + appState.active_tab->marker_filter_state.reload = true; + appState.active_tab->effect_filter_state.reload = true; + appState.active_tab->file_path = path; auto end = std::chrono::high_resolution_clock::now(); @@ -622,7 +626,16 @@ bool IconButton(const char* label, const ImVec2 size = ImVec2(0, 0)) { return result; } -void AppUpdate() { } +void AppUpdate() { + // If something has happend that changed the active tabs state + // then handle any redraw/recalculation flags here + if (appState.active_tab && appState.active_tab->state_change) { + appState.active_tab->marker_filter_state.reload = true; + appState.active_tab->effect_filter_state.reload = true; + + appState.active_tab->state_change = false; + } +} void DrawRoot(otio::SerializableObjectWithMetadata* root) { if (!root){ diff --git a/app.h b/app.h index c4275d8..7d3234c 100644 --- a/app.h +++ b/app.h @@ -7,6 +7,7 @@ #include "imgui.h" #include "imgui_internal.h" +#include #include #include namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; @@ -77,6 +78,36 @@ struct AppTheme { ImU32 colors[AppThemeCol_COUNT]; }; + +typedef std::pair, + otio::SerializableObject::Retainer> marker_parent_pair; + +// Store the state of the marker filter to save regenerating the list every frame +// if the filter options haven't changed +struct MarkerFilterState { + bool color_change; // Has the color combo box changed? + std::string filter_text = ""; // Text in filter box + bool name_check = true; // State of filter by Name checkbox + bool item_check = false; // State of filter by Item checkbox + std::vector pairs; // List of Markers the passed filtering + bool reload = false; // Trigger from loading a new file or state change + std::string filter_marker_color; // Stores the selected color in the combo box +}; + +typedef std::pair, + otio::SerializableObject::Retainer> effect_parent_pair; + +// Store the state of the effect filter to save regenerating the list every frame +// if the filter options haven't changed +struct EffectFilterState { + std::string filter_text = ""; // Text in filter box + bool reload = false; // Trigger from loading a new file or state change + bool name_check = true; // State of filter by Name checkbox + bool item_check = false; // State of filter by Item checkbox + bool effect_check = true; // State of filter by Effect checkbox + std::vector pairs; //List of Effects that passed filtering +}; + // Struct that holds data specific to individual tabs. struct TabData { // This holds the main Schema object. Pretty much everything drills into @@ -93,6 +124,17 @@ struct TabData { bool first_frame = true; // The timeline drawing code has to be drawn across // two frames so we keep track of that here + + // Filter state + MarkerFilterState marker_filter_state; // Persistant state of Marker filtering + EffectFilterState effect_filter_state; // Persistant state of Effect filtering + + // This should be set to true whenever something happens that changes to state + // of the tab. Then on the next draw loop we can check this and update things + // as required. See the Effects Inspector for an example. If set to true + // things are handled in AppUpdate() in app.c + // TODO: Could use to add a "file changed" indicator to the tab headers + bool state_change = false; }; // Struct that holds the application's state diff --git a/editing.cpp b/editing.cpp index 6987c87..19b544d 100644 --- a/editing.cpp +++ b/editing.cpp @@ -12,6 +12,15 @@ void DeleteSelectedObject() { return; } + // Flag general state change + appState.active_tab->state_change = true; + + // This function is called from DrawMenu() which is called before + // DrawInspector() in the main loop. THerefore we have to force update + // Marker and Filter redrawing here + appState.active_tab->marker_filter_state.reload = true; + appState.active_tab->effect_filter_state.reload = true; + if (appState.selected_object == GetActiveRoot()) { CloseTab(appState.active_tab); return; @@ -179,6 +188,9 @@ void AddMarkerAtPlayhead(otio::Item* item, std::string name, std::string color) otio::SerializableObject::Retainer marker = new otio::Marker(name, marked_range, color); item->markers().push_back(marker); + + // Force Marker inpsector to redraw + appState.active_tab->marker_filter_state.reload = true; } void AddTrack(std::string kind) { diff --git a/inspector.cpp b/inspector.cpp index 5b4eadc..df516d7 100644 --- a/inspector.cpp +++ b/inspector.cpp @@ -209,6 +209,7 @@ void DrawJSONApplyEditButtons() { SelectObject(replacement_object); UpdateJSONInspector(); Message("Edits applied."); + appState.active_tab->state_change = true; } } } @@ -218,6 +219,7 @@ void DrawJSONApplyEditButtons() { if (ImGui::Button("Revert")) { UpdateJSONInspector(); Message("Edits reverted."); + appState.active_tab->state_change = true; } } @@ -803,7 +805,7 @@ void DrawInspector() { } // Set the active media ref key based on user selection - if (ImGui::Combo("", &appState.selected_reference_index, reference_names.data(), num_references)) { + if (ImGui::Combo("##", &appState.selected_reference_index, reference_names.data(), num_references)) { if (appState.selected_reference_index >= 0 && appState.selected_reference_index < num_references) { clip->set_active_media_reference_key(reference_names[appState.selected_reference_index]); } @@ -887,13 +889,116 @@ void DrawInspector() { } } +bool MarkerFilterTest(ImGuiTextFilter* filter, std::string marker_name, bool name_check, std::string marker_item, bool item_check) { + // If we are not filtering by anything return all values + if (!name_check && !item_check) { + return true; + } + + // When filtering values out (-), if a header is checked and it's corresponding + // filter fails, immediately skip. When filtering in, if a header is checked and + // its corresponding filter passes, immediately pass. + if (filter->InputBuf[0] == '-') { + if (name_check && !filter->PassFilter(marker_name.c_str())) { + return false; + } + if (item_check && !filter->PassFilter(marker_item.c_str())) { + return false; + } + + return true; + } else { + if (name_check && filter->PassFilter(marker_name.c_str())) { + return true; + } + if (item_check && filter->PassFilter(marker_item.c_str())) { + return true; + } + + return false; + } +} + void DrawMarkersInspector() { - // This temporary variable is used only for a moment to convert - // between the datatypes that OTIO uses vs the one that ImGui widget uses. - char tmp_str[1000]; + if (!GetActiveRoot()) { + ImGui::Text("No file loaded."); + return; + } + + MarkerFilterState* active_tab_filter_state = &appState.active_tab->marker_filter_state; + + // Clear color selction button + if (ImGui::Button("X##color")){ + active_tab_filter_state->filter_marker_color = ""; + active_tab_filter_state->color_change = true; + } + + // Draw color selection combo box + ImGui::SameLine(); + + const char** color_choices = marker_color_names; + int num_color_choices = IM_ARRAYSIZE(marker_color_names); + + int current_index = -1; + for (int i = 0; i < num_color_choices; i++) { + if (active_tab_filter_state->filter_marker_color == color_choices[i]) { + current_index = i; + break; + } + } + if (ImGui::Combo("Color", ¤t_index, color_choices, num_color_choices)) { + if (current_index >= 0 && current_index < num_color_choices) { + active_tab_filter_state->filter_marker_color = color_choices[current_index]; + active_tab_filter_state->color_change = true; + } + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_::ImGuiHoveredFlags_DelayNormal)) { + ImGui::SetTooltip("Select Marker Color\nDefault is all colours selected"); + } + + // Show selected marker color + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, UIColorFromName(active_tab_filter_state->filter_marker_color)); + ImGui::TextUnformatted("\xef\x80\xab"); + ImGui::PopStyleColor(); + + // Filter box + static ImGuiTextFilter marker_filter; + strncpy(marker_filter.InputBuf, active_tab_filter_state->filter_text.c_str(), 256); // InputBuf is hardcoded as 256 chars + + // Clear filter button + if (ImGui::Button("X##filter")) { + marker_filter.Clear(); + } + + ImGui::SameLine(); + marker_filter.Draw("Filter"); + + // A TextFilter is not a normal widget so we cannot append a tooltip directly too it. + // Instead we add a (?) symbol and add the tooltip to that. + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_::ImGuiHoveredFlags_DelayNormal)){ + if (ImGui::BeginTooltip()) { + ImGui::TextUnformatted("Type to filter by Marker or Item name"); + ImGui::TextUnformatted("To exclude values, use the \"-\" symbol"); + ImGui::TextUnformatted("e.g. -special_marker"); + ImGui::TextUnformatted("To filter multiple values use a comma (,)"); + ImGui::TextUnformatted("e.g. marker1,marker2"); + ImGui::EndTooltip(); + } + } + + // "Filter By" selection + ImGui::TextUnformatted("Filter By:"); + ImGui::SameLine(); + + bool name_check = active_tab_filter_state->name_check; + ImGui::Checkbox("Name##filter", &name_check); + ImGui::SameLine(); - typedef std::pair, otio::SerializableObject::Retainer> marker_parent_pair; - std::vector pairs; + bool item_check = active_tab_filter_state->item_check; + ImGui::Checkbox("Item##filter", &item_check); auto root = new otio::Stack(); auto global_start = otio::RationalTime(0.0); @@ -902,22 +1007,59 @@ void DrawMarkersInspector() { root = timeline->tracks(); global_start = timeline->global_start_time().value_or(otio::RationalTime()); - for (const auto& marker : root->markers()) { - pairs.push_back(marker_parent_pair(marker, root)); - } + // Only rebuild list if the filter state or the overall tab state + // has changed + if (active_tab_filter_state->color_change || + active_tab_filter_state->filter_text != marker_filter.InputBuf || + active_tab_filter_state->name_check != name_check || + active_tab_filter_state->item_check != item_check || + active_tab_filter_state->reload){ + + std::vector pairs; + + for (const auto& marker : root->markers()) { + if (active_tab_filter_state->filter_marker_color != "") { + if (marker->color() != active_tab_filter_state->filter_marker_color) { + continue; + } + } + if (MarkerFilterTest(&marker_filter, marker->name(), name_check, root->name(), item_check)) { + pairs.push_back(marker_parent_pair(marker, root)); + } + } - for (const auto& child : - timeline->tracks()->find_children()) - { - if (const auto& item = dynamic_cast(&*child)) + for (const auto& child : + root->find_children()) { - for (const auto& marker : item->markers()) { - pairs.push_back(marker_parent_pair(marker, item)); + if (const auto& item = dynamic_cast(&*child)) + { + for (const auto& marker : item->markers()) { + if (active_tab_filter_state->filter_marker_color != "") { + if (marker->color() != active_tab_filter_state->filter_marker_color) { + continue; + } + } + if (MarkerFilterTest(&marker_filter, marker->name(), name_check, item->name(), item_check)) { + pairs.push_back(marker_parent_pair(marker, item)); + } + } } } + + // Update state + active_tab_filter_state->color_change = false; + active_tab_filter_state->filter_text = marker_filter.InputBuf; + active_tab_filter_state->name_check = name_check; + active_tab_filter_state->item_check = item_check; + active_tab_filter_state->pairs = pairs; + active_tab_filter_state->reload = false; } } + // Count of filtered items + ImGui::Text("Count: %d", active_tab_filter_state->pairs.size()); + + // Draw list auto selectable_flags = ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap; if (ImGui::BeginTable("Markers", @@ -941,13 +1083,13 @@ void DrawMarkersInspector() { ImGuiListClipper marker_clipper; - marker_clipper.Begin(pairs.size()); + marker_clipper.Begin(active_tab_filter_state->pairs.size()); while(marker_clipper.Step()) { for (int row = marker_clipper.DisplayStart; row < marker_clipper.DisplayEnd; row++) { - auto pair = pairs.at(row); + auto pair = active_tab_filter_state->pairs.at(row); auto marker = pair.first; auto parent = pair.second; @@ -1005,10 +1147,93 @@ void DrawMarkersInspector() { ImGui::EndTable(); } +bool EffectsFilterTest(ImGuiTextFilter* filter, std::string effect_name, bool name_check, std::string effect_effect, bool effect_check, std::string effect_item, bool item_check) { + // If we are not filtering by anything return all values + if (!name_check && !effect_check && !item_check) { + return true; + } + + // When filtering values out (-), if a header is checked and it's corresponding + // filter fails, immediately skip. When filtering in, if a header is checked and + // its corresponding filter passes, immediately pass. + if (filter->InputBuf[0] == '-') { + if (name_check && !filter->PassFilter(effect_name.c_str())) { + return false; + } + if (effect_check && !filter->PassFilter(effect_effect.c_str())) { + return false; + } + if (item_check && !filter->PassFilter(effect_item.c_str())) { + return false; + } + + return true; + } else { + if (name_check && filter->PassFilter(effect_name.c_str())) { + return true; + } + if (effect_check && filter->PassFilter(effect_effect.c_str())) { + return true; + } + if (item_check && filter->PassFilter(effect_item.c_str())) { + return true; + } + + return false; + } +} + void DrawEffectsInspector() { - typedef std::pair, otio::SerializableObject::Retainer> effect_parent_pair; - std::vector pairs; + if (!GetActiveRoot()) { + ImGui::Text("No file loaded."); + return; + } + + EffectFilterState* active_tab_filter_state = &appState.active_tab->effect_filter_state; + + // Filter box + static ImGuiTextFilter effect_filter; + strncpy(effect_filter.InputBuf, active_tab_filter_state->filter_text.c_str(), 256); // InputBuf is hardcoded as 256 chars + + // Clear filter button + if (ImGui::Button("X##filter")) { + effect_filter.Clear(); + } + + ImGui::SameLine(); + effect_filter.Draw("Filter"); + + // A TextFilter is not a normal widget so we cannot append a tooltip directly too it. + // Instead we add a (?) symbol and add the tooltip to that. + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_::ImGuiHoveredFlags_DelayNormal)) { + if (ImGui::BeginTooltip()) { + ImGui::TextUnformatted("Type to filter by Name, Effect type or Item name"); + ImGui::TextUnformatted("To exclude values, use the \"-\" symbol"); + ImGui::TextUnformatted("e.g. -special_effect"); + ImGui::TextUnformatted("To filter multiple values use a comma (,)"); + ImGui::TextUnformatted("e.g. effect1,effect2"); + ImGui::EndTooltip(); + } + } + // "Filter By" selection + ImGui::TextUnformatted("Filter By:"); + ImGui::SameLine(); + + bool name_check = active_tab_filter_state->name_check; + ImGui::Checkbox("Name##filter", &name_check); + ImGui::SameLine(); + + bool effect_check = active_tab_filter_state->effect_check; + ImGui::Checkbox("Effect##filter", &effect_check); + ImGui::SameLine(); + + bool item_check = active_tab_filter_state->item_check; + ImGui::Checkbox("Item##filter", &item_check); + + // Build list of filtered effects auto root = new otio::Stack(); auto global_start = otio::RationalTime(0.0); @@ -1016,22 +1241,50 @@ void DrawEffectsInspector() { root = timeline->tracks(); global_start = timeline->global_start_time().value_or(otio::RationalTime()); - for (const auto& effect : root->effects()) { - pairs.push_back(effect_parent_pair(effect, root)); - } + // Only rebuild list if the filter state or the overall tab state + // has changed + if (active_tab_filter_state->filter_text != effect_filter.InputBuf || + active_tab_filter_state->effect_check != effect_check || + active_tab_filter_state->item_check != item_check || + active_tab_filter_state->name_check != name_check || + active_tab_filter_state->reload) { + + std::vector pairs; + + for (const auto& effect : root->effects()) { + if (EffectsFilterTest(&effect_filter, + effect->name(), name_check, + effect->effect_name(), effect_check, + root->name(), item_check)) { + pairs.push_back(effect_parent_pair(effect, root)); + } + } - for (const auto& child : - timeline->tracks()->find_children()) - { - if (const auto& item = dynamic_cast(&*child)) - { - for (const auto& effect : item->effects()) { - pairs.push_back(effect_parent_pair(effect, item)); + for (const auto& child : + root->find_children()) { + if (const auto& item = dynamic_cast(&*child)) { + for (const auto& effect : item->effects()) { + if (EffectsFilterTest(&effect_filter, + effect->name(), name_check, + effect->effect_name(), effect_check, + item->name(), item_check)) { + pairs.push_back(effect_parent_pair(effect, item)); + } + } } } + active_tab_filter_state->filter_text = effect_filter.InputBuf; + active_tab_filter_state->effect_check = effect_check; + active_tab_filter_state->item_check = item_check; + active_tab_filter_state->name_check = name_check; + active_tab_filter_state->pairs = pairs; + active_tab_filter_state->reload = false; } } + // Count of filtered items + ImGui::Text("Count: %d", active_tab_filter_state->pairs.size()); + auto selectable_flags = ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap; if (ImGui::BeginTable("Effects", @@ -1054,13 +1307,13 @@ void DrawEffectsInspector() { ImGuiListClipper effects_clipper; - effects_clipper.Begin(pairs.size()); + effects_clipper.Begin(active_tab_filter_state->pairs.size()); while (effects_clipper.Step()) { for (int row = effects_clipper.DisplayStart; row < effects_clipper.DisplayEnd; row++) { - auto pair = pairs.at(row); + auto pair = active_tab_filter_state->pairs.at(row); auto effect = pair.first; auto parent = pair.second;