Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e9e1528
Add basic search and color select for markers
ThomasWilshaw Mar 4, 2025
2f1c0fb
Add 'Filter By' selction and cleanup
ThomasWilshaw Mar 5, 2025
2021d55
Add Effect filtering
ThomasWilshaw Mar 5, 2025
d053029
Add state information to the marker filter to improve performance
ThomasWilshaw Mar 5, 2025
ae92002
Merge branch 'main' into marker_search
ThomasWilshaw Mar 14, 2025
0c3ce05
Fix merge
ThomasWilshaw Mar 14, 2025
2b5c3e3
Redraw list when marker colour selection is cleared
ThomasWilshaw Mar 14, 2025
cf6a058
Improve comemnting
ThomasWilshaw Mar 14, 2025
769e07d
Merge branch 'main' into marker_search
ThomasWilshaw Sep 25, 2025
ae00360
Move marker filter state to tab data structure
ThomasWilshaw Sep 25, 2025
2cdd4ff
Add usage box and tooltip to the marker filter UI
ThomasWilshaw Sep 25, 2025
6d191ca
Add place holder text when no file is loaded
ThomasWilshaw Sep 26, 2025
4a990f8
Move effect state to tab data and fix marker checkboxes
ThomasWilshaw Sep 26, 2025
edc97cf
Remove Usage box and move text to help icon
ThomasWilshaw Sep 27, 2025
02a6234
Tidy up code
ThomasWilshaw Sep 27, 2025
1a7bd5a
Fix marker and effect filter logic
ThomasWilshaw Sep 30, 2025
e5a9c6f
Add a state change variable to TabData that can be used to check if w…
ThomasWilshaw Sep 30, 2025
922f1a8
Move state change code to AppUpdate function and piggy back existing …
ThomasWilshaw Sep 30, 2025
ec24ae3
Fix crash that may be unrelated. Empty strings for labels are not all…
ThomasWilshaw Sep 30, 2025
4b05d51
Fix typos
ThomasWilshaw Sep 30, 2025
12d618b
Fix crash when deleting object with marker
ThomasWilshaw Dec 3, 2025
41f302a
Ensure JSON edits trigger a Marker/Filter redraw
ThomasWilshaw Dec 3, 2025
48e6ca8
Ensure adding a marker causes a redraw
ThomasWilshaw Dec 3, 2025
e4f8dc2
Merge branch 'main' into marker_search
timlehr Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,9 @@ void LoadFile(std::string path) {
return;
}

// Force inspector to relaod marker list
appState.active_tab->marker_filter_state.reload = true;

appState.active_tab->file_path = path;

auto end = std::chrono::high_resolution_clock::now();
Expand Down
20 changes: 20 additions & 0 deletions app.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "imgui.h"
#include "imgui_internal.h"

#include <opentimelineio/marker.h>
#include <opentimelineio/timeline.h>
#include <opentimelineio/serializableObjectWithMetadata.h>
namespace otio = opentimelineio::OPENTIMELINEIO_VERSION;
Expand Down Expand Up @@ -77,6 +78,22 @@ struct AppTheme {
ImU32 colors[AppThemeCol_COUNT];
};


typedef std::pair<otio::SerializableObject::Retainer<otio::Marker>,
otio::SerializableObject::Retainer<otio::Item>> marker_parent_pair;

// Store the state of the marker filter to save regenerating the list every frame
// even 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; // State of filter by Name checkbox
bool item_check; // State of filter by Item checkbox
std::vector<marker_parent_pair> pairs; // List of Markers the passed filtering
bool reload = false; // Trigger from loading a new file
std::string filter_marker_color; // Stores the selected color in the combo box
};

// Struct that holds data specific to individual tabs.
struct TabData {
// This holds the main Schema object. Pretty much everything drills into
Expand All @@ -93,6 +110,9 @@ 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
MarkerFilterState marker_filter_state; // Persistant state of Marker filtering
};

// Struct that holds the application's state
Expand Down
190 changes: 171 additions & 19 deletions inspector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -888,12 +888,84 @@ void DrawInspector() {
}

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];

typedef std::pair<otio::SerializableObject::Retainer<otio::Marker>, otio::SerializableObject::Retainer<otio::Item>> marker_parent_pair;
std::vector<marker_parent_pair> pairs;
if (!GetActiveRoot()) {
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", &current_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);

// Clear filter button
if (ImGui::Button("X##filter")) {
marker_filter.Clear();
}

ImGui::SameLine();
marker_filter.Draw("Filter");

// "Filter By" selection
ImGui::TextUnformatted("Filter By:");
ImGui::SameLine();

static bool name_check = true;
ImGui::Checkbox("Name##filter", &name_check);
ImGui::SameLine();

static bool item_check = false;
ImGui::Checkbox("Item##filter", &item_check);

// ImGuiTextFilter is not a widget in its own right so tooltips don't work
// so we have to put the usage instructions in a collapsable header.

if (ImGui::CollapsingHeader("Filter Usage", ImGuiTreeNodeFlags_None)) {
ImGui::Separator();
ImGui::TextUnformatted("Filter Usage:");
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::Separator();
}


auto root = new otio::Stack();
auto global_start = otio::RationalTime(0.0);
Expand All @@ -902,22 +974,62 @@ 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 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){

for (const auto& child :
timeline->tracks()->find_children())
{
if (const auto& item = dynamic_cast<otio::Item*>(&*child))
std::vector<marker_parent_pair> 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 ((marker_filter.PassFilter(marker->name().c_str()) && name_check) ||
(marker_filter.PassFilter(root->name().c_str()) && item_check) ||
(!name_check && ! item_check)) {
pairs.push_back(marker_parent_pair(marker, root));
}
}

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<otio::Item*>(&*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 ((marker_filter.PassFilter(marker->name().c_str()) && name_check) ||
(marker_filter.PassFilter(item->name().c_str()) && item_check) ||
(!name_check && ! 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",
Expand All @@ -941,13 +1053,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;

Expand Down Expand Up @@ -1009,6 +1121,33 @@ void DrawEffectsInspector() {
typedef std::pair<otio::SerializableObject::Retainer<otio::Effect>, otio::SerializableObject::Retainer<otio::Item>> effect_parent_pair;
std::vector<effect_parent_pair> pairs;

// Filter box
static ImGuiTextFilter effect_filter;

// lear filter button
if (ImGui::Button("X##filter")) {
effect_filter.Clear();
}

ImGui::SameLine();
effect_filter.Draw("Filter (inc,-exc)");

// "Filter By" selection
ImGui::TextUnformatted("Filter By:");
ImGui::SameLine();

static bool name_check = true;
ImGui::Checkbox("Name##filter", &name_check);
ImGui::SameLine();

static bool effect_check = true;
ImGui::Checkbox("Effect##filter", &effect_check);
ImGui::SameLine();

static bool item_check = false;
ImGui::Checkbox("Item##filter", &item_check);

// Build list of filtered effects
auto root = new otio::Stack();
auto global_start = otio::RationalTime(0.0);

Expand All @@ -1017,21 +1156,34 @@ void DrawEffectsInspector() {
global_start = timeline->global_start_time().value_or(otio::RationalTime());

for (const auto& effect : root->effects()) {
pairs.push_back(effect_parent_pair(effect, root));
if ((!name_check && !effect_check && !item_check) ||
(effect_filter.PassFilter(effect->name().c_str()) && name_check) ||
(effect_filter.PassFilter(effect->effect_name().c_str()) && effect_check) ||
(effect_filter.PassFilter(root->name().c_str()) && item_check)) {
pairs.push_back(effect_parent_pair(effect, root));
}
}

for (const auto& child :
timeline->tracks()->find_children())
root->find_children())
{
if (const auto& item = dynamic_cast<otio::Item*>(&*child))
{
for (const auto& effect : item->effects()) {
pairs.push_back(effect_parent_pair(effect, item));
if ((!name_check && !effect_check && !item_check) ||
(effect_filter.PassFilter(effect->name().c_str()) && name_check) ||
(effect_filter.PassFilter(effect->effect_name().c_str()) && effect_check) ||
(effect_filter.PassFilter(item->name().c_str()) && item_check)){
pairs.push_back(effect_parent_pair(effect, item));
}
}
}
}
}

// Count of filtered items
ImGui::Text("Count: %d", pairs.size());

auto selectable_flags = ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap;

if (ImGui::BeginTable("Effects",
Expand Down
Loading