From 99ea14fab033bffe318c0a741358377939044d06 Mon Sep 17 00:00:00 2001 From: Bruno Date: Sun, 13 Jun 2021 00:24:45 +0200 Subject: [PATCH] Player timeline for selection Selection of media sources now displays in a list with proportional timelines, showing actual play time and cursor on effective timeline with opacity curve. --- ImGuiToolkit.cpp | 555 ++++++++++++++++++--------------------- ImGuiToolkit.h | 28 +- Timeline.cpp | 42 +++ Timeline.h | 8 +- UserInterfaceManager.cpp | 412 +++++++++++++++++++---------- UserInterfaceManager.h | 5 +- rsc/images/icons.dds | Bin 1638528 -> 1638528 bytes 7 files changed, 590 insertions(+), 460 deletions(-) diff --git a/ImGuiToolkit.cpp b/ImGuiToolkit.cpp index 2e8a1dc..cee822a 100644 --- a/ImGuiToolkit.cpp +++ b/ImGuiToolkit.cpp @@ -435,240 +435,19 @@ void ImGuiToolkit::HelpIcon(const char* desc, int i, int j, const char* shortcut #define NUM_MARKS 10 #define LARGE_TICK_INCREMENT 1 #define LABEL_TICK_INCREMENT 3 -static guint64 optimal_tick_marks[NUM_MARKS + LABEL_TICK_INCREMENT] = { 100 * MILISECOND, 500 * MILISECOND, 1 * SECOND, 2 * SECOND, 5 * SECOND, 10 * SECOND, 20 * SECOND, 1 * MINUTE, 2 * MINUTE, 5 * MINUTE, 10 * MINUTE, 60 * MINUTE, 60 * MINUTE }; -bool ImGuiToolkit::TimelineSlider(const char* label, guint64 *time, guint64 start, guint64 end, guint64 step, const float width) +void ImGuiToolkit::RenderTimeline (ImGuiWindow* window, ImRect timeline_bbox, guint64 start, guint64 end, guint64 step, bool verticalflip) { - // get window - ImGuiWindow* window = ImGui::GetCurrentWindow(); - if (window->SkipItems) - return false; + static guint64 optimal_tick_marks[NUM_MARKS + LABEL_TICK_INCREMENT] = { 100 * MILISECOND, 500 * MILISECOND, 1 * SECOND, 2 * SECOND, 5 * SECOND, 10 * SECOND, 20 * SECOND, 1 * MINUTE, 2 * MINUTE, 5 * MINUTE, 10 * MINUTE, 60 * MINUTE, 60 * MINUTE }; - // get style & id const ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; const float fontsize = g.FontSize; - const ImGuiID id = window->GetID(label); + const ImU32 text_color = ImGui::GetColorU32(ImGuiCol_Text); - // - // FIRST PREPARE ALL data structures - // - - // widget bounding box - const float height = 2.f * (fontsize + style.FramePadding.y); - ImVec2 pos = window->DC.CursorPos; - ImVec2 size = ImVec2(width, height); - ImRect bbox(pos, pos + size); - ImGui::ItemSize(size, style.FramePadding.y); - if (!ImGui::ItemAdd(bbox, id)) - return false; - - // cursor size - const float cursor_scale = 1.f; - const float cursor_width = 0.5f * fontsize * cursor_scale; - - // TIMELINE is inside the bbox, in a slightly smaller bounding box - ImRect timeline_bbox(bbox); - timeline_bbox.Expand( ImVec2(-cursor_width, -style.FramePadding.y) ); - - // SLIDER is inside the timeline - ImRect slider_bbox( timeline_bbox.GetTL() + ImVec2(-cursor_width + 2.f, cursor_width + 4.f ), timeline_bbox.GetBR() + ImVec2( cursor_width - 2.f, 0.f ) ); - - // units conversion: from time to float (calculation made with higher precision first) guint64 duration = end - start; - float time_ = static_cast ( static_cast(*time - start) / static_cast(duration) ); float step_ = static_cast ( static_cast(step) / static_cast(duration) ); - // - // SECOND GET USER INPUT AND PERFORM CHANGES AND DECISIONS - // - - // read user input from system - bool left_mouse_press = false; - const bool hovered = ImGui::ItemHoverable(bbox, id); - bool temp_input_is_active = ImGui::TempInputIsActive(id); - if (!temp_input_is_active) - { - const bool focus_requested = ImGui::FocusableItemRegister(window, id); - left_mouse_press = hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left); - if (focus_requested || left_mouse_press || g.NavActivateId == id || g.NavInputId == id) - { - ImGui::SetActiveID(id, window); - ImGui::SetFocusID(id, window); - ImGui::FocusWindow(window); - } - } - - // time Slider behavior - ImRect grab_slider_bb; - ImU32 grab_slider_color = ImGui::GetColorU32(ImGuiCol_SliderGrab); - float time_slider = time_ * 10.f; // x 10 precision on grab - float time_zero = 0.f; - float time_end = 10.f; - bool value_changed = ImGui::SliderBehavior(slider_bbox, id, ImGuiDataType_Float, &time_slider, &time_zero, - &time_end, "%.2f", 1.f, ImGuiSliderFlags_None, &grab_slider_bb); - if (value_changed){ - // g_print("slider %f %ld \n", time_slider, static_cast ( static_cast(time_slider) * static_cast(duration) )); - *time = static_cast ( 0.1 * static_cast(time_slider) * static_cast(end) ) + start; - grab_slider_color = ImGui::GetColorU32(ImGuiCol_SliderGrabActive); - } - - // - // THIRD RENDER - // - - // Render the bounding box - const ImU32 frame_col = ImGui::GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : g.HoveredId == id ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); - ImGui::RenderFrame(bbox.Min, bbox.Max, frame_col, true, style.FrameRounding); - - // by default, put a tick mark at every frame step and a large mark every second - guint64 tick_step = step; - guint64 large_tick_step = optimal_tick_marks[1+LARGE_TICK_INCREMENT]; - guint64 label_tick_step = optimal_tick_marks[1+LABEL_TICK_INCREMENT]; - - // how many pixels to represent one frame step? - float tick_step_pixels = timeline_bbox.GetWidth() * step_; - - // tick at each step: add a label every 0 frames - if (tick_step_pixels > 5.f) - { - large_tick_step = 10 * step; - - // try to put a label every second - if ( 1000000000 % step < 1000) - label_tick_step = (1000000000 / step) * step; - // not a round framerate: probalby best to use 10 frames interval - else - label_tick_step = 10 * step; - } - else { - // while there is less than 5 pixels between two tick marks (or at last optimal tick mark) - for ( int i=0; i<10 && tick_step_pixels < 5.f; ++i ) - { - // try to use the optimal tick marks pre-defined - tick_step = optimal_tick_marks[i]; - large_tick_step = optimal_tick_marks[i+LARGE_TICK_INCREMENT]; - label_tick_step = optimal_tick_marks[i+LABEL_TICK_INCREMENT]; - tick_step_pixels = timeline_bbox.GetWidth() * static_cast ( static_cast(tick_step) / static_cast(duration) ); - } - } - - // render the tick marks along TIMELINE - ImU32 color = ImGui::GetColorU32( style.Colors[ImGuiCol_Text] ); - pos = timeline_bbox.GetTL(); - guint64 tick = 0; - char overlay_buf[24]; - - // render text duration - ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%s", - GstToolkit::time_to_string(end, GstToolkit::TIME_STRING_MINIMAL).c_str()); - ImVec2 overlay_size = ImGui::CalcTextSize(overlay_buf, NULL); - ImVec2 duration_label = bbox.GetBR() - overlay_size - ImVec2(3.f, 3.f); - if (overlay_size.x > 0.0f) - ImGui::RenderTextClipped( duration_label, bbox.Max, overlay_buf, NULL, &overlay_size); - - // render tick marks - while ( tick < duration ) - { - // large tick mark - float tick_length = !(tick%large_tick_step) ? fontsize - style.FramePadding.y : style.FramePadding.y; - - // label tick mark - if ( !(tick%label_tick_step) ) { - tick_length = fontsize; - guint64 ticklabel = 100 * (guint64) round( (double)( tick + start ) / 100.0); // round value to avoid '0.99' and alike - ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%s", - GstToolkit::time_to_string(ticklabel, GstToolkit::TIME_STRING_MINIMAL).c_str()); - overlay_size = ImGui::CalcTextSize(overlay_buf, NULL); - ImVec2 mini = ImVec2( pos.x - overlay_size.x / 2.f, pos.y + tick_length ); - ImVec2 maxi = ImVec2( pos.x + overlay_size.x / 2.f, pos.y + tick_length + overlay_size.y ); - // do not overlap with label for duration - if (maxi.x < duration_label.x) - ImGui::RenderTextClipped(mini, maxi, overlay_buf, NULL, &overlay_size); - } - - // draw the tick mark each step - window->DrawList->AddLine( pos, pos + ImVec2(0.f, tick_length), color); - - // next tick - tick += tick_step; - float tick_percent = static_cast ( static_cast(tick) / static_cast(duration) ); - pos = ImLerp(timeline_bbox.GetTL(), timeline_bbox.GetTR(), tick_percent); - } - - // tick EOF - window->DrawList->AddLine( timeline_bbox.GetTR(), timeline_bbox.GetTR() + ImVec2(0.f, fontsize), color); - -// disabled: render position -// ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%s", GstToolkit::time_to_string(*time).c_str()); -// overlay_size = ImGui::CalcTextSize(overlay_buf, NULL); -// overlay_size = ImVec2(3.f, -3.f - overlay_size.y); -// if (overlay_size.x > 0.0f) -// ImGui::RenderTextClipped( bbox.GetBL() + overlay_size, bbox.Max, overlay_buf, NULL, &overlay_size); - - // draw slider grab handle - if (grab_slider_bb.Max.x > grab_slider_bb.Min.x) { - window->DrawList->AddRectFilled(grab_slider_bb.Min, grab_slider_bb.Max, grab_slider_color, style.GrabRounding); - } - - // draw the cursor - color = ImGui::GetColorU32(style.Colors[ImGuiCol_SliderGrab]); - pos = ImLerp(timeline_bbox.GetTL(), timeline_bbox.GetTR(), time_) - ImVec2(cursor_width, 2.f); - ImGui::RenderArrow(window->DrawList, pos, color, ImGuiDir_Up, cursor_scale); - - return left_mouse_press; -} - -void ImGuiToolkit::Timeline (const char* label, guint64 time, guint64 start, guint64 end, guint64 step, const float width) -{ - // get window - ImGuiWindow* window = ImGui::GetCurrentWindow(); - if (window->SkipItems) - return; - - // get style & id - const ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; - const float fontsize = g.FontSize; - const ImGuiID id = window->GetID(label); - - // - // FIRST PREPARE ALL data structures - // - - // widget bounding box - const float height = 2.f * (fontsize + style.FramePadding.y); - ImVec2 pos = window->DC.CursorPos; - ImVec2 size = ImVec2(width, height); - ImRect bbox(pos, pos + size); - ImGui::ItemSize(size, style.FramePadding.y); - if (!ImGui::ItemAdd(bbox, id)) - return; - - // cursor size - const float cursor_scale = 1.f; - const float cursor_width = 0.5f * fontsize * cursor_scale; - - // TIMELINE is inside the bbox, in a slightly smaller bounding box - ImRect timeline_bbox(bbox); - timeline_bbox.Expand( ImVec2(-cursor_width, -style.FramePadding.y) ); - - // SLIDER is inside the timeline -// ImRect slider_bbox( timeline_bbox.GetTL() + ImVec2(-cursor_width + 2.f, cursor_width + 4.f ), timeline_bbox.GetBR() + ImVec2( cursor_width - 2.f, 0.f ) ); - - // units conversion: from time to float (calculation made with higher precision first) - guint64 duration = end - start; - float time_ = static_cast ( static_cast(time - start) / static_cast(duration) ); - float step_ = static_cast ( static_cast(step) / static_cast(duration) ); - - // - // THIRD RENDER - // - - // Render the bounding box - const ImU32 frame_col = ImGui::GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : g.HoveredId == id ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); - ImGui::RenderFrame(bbox.Min, bbox.Max, frame_col, true, style.FrameRounding); - // by default, put a tick mark at every frame step and a large mark every second guint64 tick_step = step; guint64 large_tick_step = optimal_tick_marks[1+LARGE_TICK_INCREMENT]; @@ -701,73 +480,239 @@ void ImGuiToolkit::Timeline (const char* label, guint64 time, guint64 start, gui } } - // render the tick marks along TIMELINE - ImU32 color = ImGui::GetColorU32( style.Colors[ImGuiCol_Text] ); - pos = timeline_bbox.GetTL(); - guint64 tick = 0; - char overlay_buf[24]; + // render tics and text + char text_buf[24]; - // render text duration - ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%s", + ImGuiToolkit::PushFont(ImGuiToolkit::FONT_BOLD); + + // render tick and text START + ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), "%s", + GstToolkit::time_to_string(start, GstToolkit::TIME_STRING_MINIMAL).c_str()); + ImVec2 beginning_label_size = ImGui::CalcTextSize(text_buf, NULL); + ImVec2 beginning_label_pos = timeline_bbox.GetTL() + ImVec2(3.f, fontsize); + if (verticalflip) + beginning_label_pos.y -= fontsize; + ImGui::RenderTextClipped( beginning_label_pos, beginning_label_pos + beginning_label_size, + text_buf, NULL, &beginning_label_size); + window->DrawList->AddLine( timeline_bbox.GetTL(), timeline_bbox.GetBL(), text_color, 1.5f); + + + // render tick and text END + ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), "%s", GstToolkit::time_to_string(end, GstToolkit::TIME_STRING_MINIMAL).c_str()); - ImVec2 overlay_size = ImGui::CalcTextSize(overlay_buf, NULL); - ImVec2 duration_label = bbox.GetBR() - overlay_size - ImVec2(3.f, 5.f); - if (overlay_size.x > 0.0f) - ImGui::RenderTextClipped( duration_label, bbox.Max, overlay_buf, NULL, &overlay_size); + ImVec2 duration_label_size = ImGui::CalcTextSize(text_buf, NULL); + ImVec2 duration_label_pos = timeline_bbox.GetTR() + ImVec2( -2.f -duration_label_size.x, fontsize); + if (verticalflip) + duration_label_pos.y -= fontsize; + ImGui::RenderTextClipped( duration_label_pos, duration_label_pos + duration_label_size, + text_buf, NULL, &duration_label_size); + window->DrawList->AddLine( timeline_bbox.GetTR(), timeline_bbox.GetBR(), text_color, 1.5f); - // render tick marks + ImGui::PopFont(); + + // render the tick marks along TIMELINE + ImGui::PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_Text] -ImVec4(0.f,0.f,0.f,0.4f)); + ImVec2 pos = verticalflip ? timeline_bbox.GetBL() : timeline_bbox.GetTL(); + guint64 tick = 0; while ( tick < duration ) { // large tick mark - float tick_length = !(tick%large_tick_step) ? fontsize - style.FramePadding.y : style.FramePadding.y; + float tick_length = (tick%large_tick_step) ? style.FramePadding.y : fontsize - style.FramePadding.y; // label tick mark if ( !(tick%label_tick_step) ) { tick_length = fontsize; guint64 ticklabel = 100 * (guint64) round( (double)( tick + start) / 100.0); // round value to avoid '0.99' and alike - ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%s", + ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), "%s", GstToolkit::time_to_string(ticklabel, GstToolkit::TIME_STRING_MINIMAL).c_str()); - overlay_size = ImGui::CalcTextSize(overlay_buf, NULL); - ImVec2 mini = ImVec2( pos.x - overlay_size.x / 2.f, pos.y + tick_length ); - ImVec2 maxi = ImVec2( pos.x + overlay_size.x / 2.f, pos.y + tick_length + overlay_size.y ); - // do not overlap with label for duration - if (maxi.x < duration_label.x) - ImGui::RenderTextClipped(mini, maxi, overlay_buf, NULL, &overlay_size); - } + ImVec2 label_size = ImGui::CalcTextSize(text_buf, NULL); + ImVec2 mini = ImVec2( pos.x - label_size.x / 2.f, pos.y); + ImVec2 maxi = ImVec2( pos.x + label_size.x / 2.f, pos.y); - // draw the tick mark each step - window->DrawList->AddLine( pos, pos + ImVec2(0.f, tick_length), color); + if (verticalflip) { + mini.y -= tick_length + label_size.y; + maxi.y -= tick_length; + } + else { + mini.y += tick_length; + maxi.y += tick_length + label_size.y; + } + + // do not overlap with labels for beginning and duration + if (mini.x - style.ItemSpacing.x > (beginning_label_pos.x + beginning_label_size.x) && maxi.x + style.ItemSpacing.x < duration_label_pos.x) + ImGui::RenderTextClipped(mini, maxi, text_buf, NULL, &label_size); + } // next tick tick += tick_step; float tick_percent = static_cast ( static_cast(tick) / static_cast(duration) ); - pos = ImLerp(timeline_bbox.GetTL(), timeline_bbox.GetTR(), tick_percent); + + // draw the tick mark each step + if (verticalflip) { + window->DrawList->AddLine( pos, pos - ImVec2(0.f, tick_length), text_color); + pos = ImLerp(timeline_bbox.GetBL(), timeline_bbox.GetBR(), tick_percent); + } + else { + window->DrawList->AddLine( pos, pos + ImVec2(0.f, tick_length), text_color); + pos = ImLerp(timeline_bbox.GetTL(), timeline_bbox.GetTR(), tick_percent); + } + + } + ImGui::PopStyleColor(1); + +} + +bool ImGuiToolkit::TimelineSlider(const char* label, guint64 *time, guint64 start, guint64 end, guint64 step, const float width) +{ + // get window + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + // get style & id + const ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float fontsize = g.FontSize; + const ImGuiID id = window->GetID(label); + + // + // FIRST PREPARE ALL data structures + // + + // widget bounding box + const float height = 2.f * (fontsize + style.FramePadding.y); + ImVec2 pos = window->DC.CursorPos; + ImVec2 size = ImVec2(width, height); + ImRect bbox(pos, pos + size); + ImGui::ItemSize(size, style.FramePadding.y); + if (!ImGui::ItemAdd(bbox, id)) + return false; + + // cursor size + const float cursor_width = 0.5f * fontsize; + + // TIMELINE is inside the bbox, in a slightly smaller bounding box + ImRect timeline_bbox(bbox); + timeline_bbox.Expand( ImVec2() - style.FramePadding ); + + // SLIDER is inside the timeline + ImRect slider_bbox( timeline_bbox.GetTL() + ImVec2(-cursor_width + 2.f, cursor_width + 4.f ), timeline_bbox.GetBR() + ImVec2( cursor_width - 2.f, 0.f ) ); + + // units conversion: from time to float (calculation made with higher precision first) + float time_ = static_cast ( static_cast(*time - start) / static_cast(end - start) ); + + // + // SECOND GET USER INPUT AND PERFORM CHANGES AND DECISIONS + // + + // read user input from system + bool left_mouse_press = false; + const bool hovered = ImGui::ItemHoverable(bbox, id); + bool temp_input_is_active = ImGui::TempInputIsActive(id); + if (!temp_input_is_active) + { + const bool focus_requested = ImGui::FocusableItemRegister(window, id); + left_mouse_press = hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left); + if (focus_requested || left_mouse_press || g.NavActivateId == id || g.NavInputId == id) + { + ImGui::SetActiveID(id, window); + ImGui::SetFocusID(id, window); + ImGui::FocusWindow(window); + } } - // tick EOF - window->DrawList->AddLine( timeline_bbox.GetTR(), timeline_bbox.GetTR() + ImVec2(0.f, fontsize), color); + // time Slider behavior + ImRect grab_slider_bb; + ImU32 grab_slider_color = ImGui::GetColorU32(ImGuiCol_SliderGrab); + float time_slider = time_ * 10.f; // x 10 precision on grab + float time_zero = 0.f; + float time_end = 10.f; + bool value_changed = ImGui::SliderBehavior(slider_bbox, id, ImGuiDataType_Float, &time_slider, &time_zero, + &time_end, "%.2f", 1.f, ImGuiSliderFlags_None, &grab_slider_bb); + if (value_changed){ + // g_print("slider %f %ld \n", time_slider, static_cast ( static_cast(time_slider) * static_cast(duration) )); + *time = static_cast ( 0.1 * static_cast(time_slider) * static_cast(end - start) ) + start; + grab_slider_color = ImGui::GetColorU32(ImGuiCol_SliderGrabActive); + } -// disabled: render position -// ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%s", GstToolkit::time_to_string(*time).c_str()); -// overlay_size = ImGui::CalcTextSize(overlay_buf, NULL); -// overlay_size = ImVec2(3.f, -3.f - overlay_size.y); -// if (overlay_size.x > 0.0f) -// ImGui::RenderTextClipped( bbox.GetBL() + overlay_size, bbox.Max, overlay_buf, NULL, &overlay_size); + // + // THIRD RENDER + // -// // draw slider grab handle -// if (grab_slider_bb.Max.x > grab_slider_bb.Min.x) { -// window->DrawList->AddRectFilled(grab_slider_bb.Min, grab_slider_bb.Max, grab_slider_color, style.GrabRounding); -// } + // Render the bounding box + const ImU32 frame_col = ImGui::GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : g.HoveredId == id ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + ImGui::RenderFrame(bbox.Min, bbox.Max, frame_col, true, style.FrameRounding); + + // render the timeline + RenderTimeline(window, timeline_bbox, start, end, step); + + // draw slider grab handle + if (grab_slider_bb.Max.x > grab_slider_bb.Min.x) { + window->DrawList->AddRectFilled(grab_slider_bb.Min, grab_slider_bb.Max, grab_slider_color, style.GrabRounding); + } + + // draw the cursor + pos = ImLerp(timeline_bbox.GetTL(), timeline_bbox.GetTR(), time_) - ImVec2(cursor_width, 2.f); + ImGui::RenderArrow(window->DrawList, pos, ImGui::GetColorU32(ImGuiCol_SliderGrab), ImGuiDir_Up); + + return left_mouse_press; +} + + +void ImGuiToolkit::Timeline (const char* label, guint64 time, guint64 start, guint64 end, guint64 step, const float width) +{ + // get window + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return; + + // get style & id + const ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float fontsize = g.FontSize; + const ImGuiID id = window->GetID(label); + + // + // FIRST PREPARE ALL data structures + // + + // widget bounding box + const float height = 2.f * (fontsize + style.FramePadding.y); + ImVec2 pos = window->DC.CursorPos; + ImVec2 size = ImVec2(width, height); + ImRect bbox(pos, pos + size); + ImGui::ItemSize(size, style.FramePadding.y); + if (!ImGui::ItemAdd(bbox, id)) + return; + + // cursor size + const float cursor_width = 0.5f * fontsize; + + // TIMELINE is inside the bbox, in a slightly smaller bounding box + ImRect timeline_bbox(bbox); + timeline_bbox.Expand( ImVec2() - style.FramePadding ); + + // units conversion: from time to float (calculation made with higher precision first) + guint64 duration = end - start; + float time_ = static_cast ( static_cast(time - start) / static_cast(duration) ); + + // + // THIRD RENDER + // + + // Render the bounding box + ImGui::RenderFrame(bbox.Min, bbox.Max, ImGui::GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + + // render the timeline + RenderTimeline(window, timeline_bbox, start, end, step); // draw the cursor if ( time_ > -FLT_EPSILON && time_ < 1.f ) { - color = ImGui::GetColorU32(style.Colors[ImGuiCol_SliderGrab]); pos = ImLerp(timeline_bbox.GetTL(), timeline_bbox.GetTR(), time_) - ImVec2(cursor_width, 2.f); - ImGui::RenderArrow(window->DrawList, pos, color, ImGuiDir_Up, cursor_scale); + ImGui::RenderArrow(window->DrawList, pos, ImGui::GetColorU32(ImGuiCol_SliderGrab), ImGuiDir_Up); } } -//bool ImGuiToolkit::InvisibleSliderInt(const char* label, guint64 *index, guint64 min, guint64 max, ImVec2 size) bool ImGuiToolkit::InvisibleSliderInt(const char* label, uint *index, uint min, uint max, ImVec2 size) { // get window @@ -908,31 +853,10 @@ bool ImGuiToolkit::EditPlotLines(const char* label, float *array, int values_cou return array_changed; } -// Function to create Gaussian filter -void FilterCreation(float GKernel[], int N) -{ - // intialising standard deviation to 1.0 - float sigma = N * 0.25f; - float s = 2.0 * sigma * sigma; - - // sum is for normalization - float max = 0.0; - - // generating 5x5 kernel - for (int x = 0 ; x < N; x++) { - float r = x; - GKernel[x] = (exp(-(r * r) / s)) / sqrt(M_PI * 2.0 * sigma); - max = MAX(max, GKernel[x]); - } - - // normalising the Kernel - for (int i = 0; i < N; ++i) - GKernel[i] /= max; -} - bool ImGuiToolkit::EditPlotHistoLines(const char* label, float *histogram_array, float *lines_array, - int values_count, float values_min, float values_max, bool edit_histogram, bool *released, const ImVec2 size) + int values_count, float values_min, float values_max, guint64 start, guint64 end, + bool edit_histogram, bool *released, const ImVec2 size) { bool array_changed = false; @@ -942,7 +866,7 @@ bool ImGuiToolkit::EditPlotHistoLines(const char* label, float *histogram_array, return false; // capture coordinates before any draw or action - ImVec2 canvas_pos = ImGui::GetCursorScreenPos(); + const ImVec2 canvas_pos = ImGui::GetCursorScreenPos(); ImVec2 mouse_pos_in_canvas = ImVec2(ImGui::GetIO().MousePos.x - canvas_pos.x, ImGui::GetIO().MousePos.y - canvas_pos.y); // get id @@ -976,9 +900,17 @@ bool ImGuiToolkit::EditPlotHistoLines(const char* label, float *histogram_array, const ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; - const float _h_space = g.Style.WindowPadding.x; + const float _h_space = style.WindowPadding.x; ImVec4 bg_color = hovered ? style.Colors[ImGuiCol_FrameBgHovered] : style.Colors[ImGuiCol_FrameBg]; + // prepare index + double x = (mouse_pos_in_canvas.x - _h_space) / (size.x - 2.f * _h_space); + size_t index = CLAMP( (int) floor(static_cast(values_count) * x), 0, values_count); + char cursor_text[64]; + guint64 time = start + (index * end) / static_cast(values_count); + ImFormatString(cursor_text, IM_ARRAYSIZE(cursor_text), "%s", + GstToolkit::time_to_string(time, GstToolkit::TIME_STRING_MINIMAL).c_str()); + // enter edit if widget is active if (ImGui::GetActiveID() == id) { @@ -989,12 +921,8 @@ bool ImGuiToolkit::EditPlotHistoLines(const char* label, float *histogram_array, static size_t previous_index = UINT32_MAX; if (mouse_press) { - - float x = (float) values_count * (mouse_pos_in_canvas.x - _h_space) / (size.x - 2.f * _h_space); - size_t index = CLAMP( (int) floor(x), 0, values_count); - - float y = mouse_pos_in_canvas.y / bbox.GetHeight(); - y = CLAMP( (y * (values_max-values_min)) + values_min, values_min, values_max); + float val = mouse_pos_in_canvas.y / bbox.GetHeight(); + val = CLAMP( (val * (values_max-values_min)) + values_min, values_min, values_max); if (previous_index == UINT32_MAX) previous_index = index; @@ -1015,7 +943,7 @@ bool ImGuiToolkit::EditPlotHistoLines(const char* label, float *histogram_array, histogram_array[i] = target_value; } else { - const float target_value = values_max - y; + const float target_value = values_max - val; for (size_t i = left; i < right; ++i) lines_array[i] = target_value; @@ -1038,9 +966,9 @@ bool ImGuiToolkit::EditPlotHistoLines(const char* label, float *histogram_array, // back to draw ImGui::SetCursorScreenPos(canvas_pos); - // plot transparent histogram + // plot histogram (with frame) ImGui::PushStyleColor(ImGuiCol_FrameBg, bg_color); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, style.Colors[ImGuiCol_TitleBg]); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, style.Colors[ImGuiCol_TitleBg]); // a dark color char buf[128]; sprintf(buf, "##Histo%s", label); ImGui::PlotHistogram(buf, histogram_array, values_count, 0, NULL, values_min, values_max, size); @@ -1048,21 +976,40 @@ bool ImGuiToolkit::EditPlotHistoLines(const char* label, float *histogram_array, ImGui::SetCursorScreenPos(canvas_pos); - // plot lines + // plot (transparent) lines ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0,0,0,0)); sprintf(buf, "##Lines%s", label); ImGui::PlotLines(buf, lines_array, values_count, 0, NULL, values_min, values_max, size); ImGui::PopStyleColor(1); - // draw the cursor bar + // draw the cursor if (hovered) { - const ImU32 cur_color = ImGui::GetColorU32( style.Colors[ImGuiCol_CheckMark] ); + // prepare color and text + const ImU32 cur_color = ImGui::GetColorU32(ImGuiCol_CheckMark); + ImGui::PushStyleColor(ImGuiCol_Text, cur_color); + ImVec2 label_size = ImGui::CalcTextSize(cursor_text, NULL); + + // render cursor depending on action mouse_pos_in_canvas.x = CLAMP(mouse_pos_in_canvas.x, _h_space, size.x - _h_space); - if (edit_histogram) - window->DrawList->AddLine( canvas_pos + ImVec2(mouse_pos_in_canvas.x, 4.f), canvas_pos + ImVec2(mouse_pos_in_canvas.x, size.y - 4.f), cur_color); - else { - window->DrawList->AddCircleFilled(canvas_pos + mouse_pos_in_canvas, 3.f, cur_color, 8); + ImVec2 cursor_pos = canvas_pos; + if (edit_histogram) { + cursor_pos = cursor_pos + ImVec2(mouse_pos_in_canvas.x, 4.f); + window->DrawList->AddLine( cursor_pos, cursor_pos + ImVec2(0.f, size.y - 8.f), cur_color); } + else { + cursor_pos = cursor_pos + mouse_pos_in_canvas; + window->DrawList->AddCircleFilled( cursor_pos, 3.f, cur_color, 8); + } + + // draw text + cursor_pos.y = canvas_pos.y + size.y - label_size.y - 1.f; + if (mouse_pos_in_canvas.x + label_size.x < size.x - 2.f * _h_space) + cursor_pos.x += _h_space; + else + cursor_pos.x -= label_size.x + _h_space; + ImGui::RenderTextClipped(cursor_pos, cursor_pos + label_size, cursor_text, NULL, &label_size); + + ImGui::PopStyleColor(1); } return array_changed; diff --git a/ImGuiToolkit.h b/ImGuiToolkit.h index 7d5a8cd..0757e7e 100644 --- a/ImGuiToolkit.h +++ b/ImGuiToolkit.h @@ -5,7 +5,7 @@ #include #include #include -#include + #include "rsc/fonts/IconsFontAwesome5.h" namespace ImGuiToolkit @@ -13,7 +13,7 @@ namespace ImGuiToolkit // Icons from resource icon.dds void Icon (int i, int j, bool enabled = true); bool IconButton (int i, int j, const char *tooltips = nullptr); - bool IconButton (const char* icon = ICON_FA_EXCLAMATION_CIRCLE, const char *tooltips = nullptr); + bool IconButton (const char* icon, const char *tooltips = nullptr); bool IconToggle (int i, int j, int i_toggle, int j_toggle, bool* toggle, const char *tooltips[] = nullptr); void ShowIconsWindow(bool* p_open); @@ -23,21 +23,23 @@ namespace ImGuiToolkit bool ButtonIconMultistate (std::vector > icons, int* state); bool ComboIcon (std::vector > icons, std::vector labels, int* state); - // utility buttons + // buttons bool ButtonToggle (const char* label, bool* toggle); bool ButtonSwitch (const char* label, bool* toggle , const char *help = nullptr); void ButtonOpenUrl (const char* label, const char* url, const ImVec2& size_arg = ImVec2(0,0)); + // tooltip and mouse over void ToolTip (const char* desc, const char* shortcut = nullptr); void HelpMarker (const char* desc, const char* icon = ICON_FA_QUESTION_CIRCLE, const char* shortcut = nullptr); void HelpIcon (const char* desc, int i = 19, int j = 5, const char* shortcut = nullptr); - // utility sliders + // sliders bool TimelineSlider (const char* label, guint64 *time, guint64 start, guint64 end, guint64 step, const float width); + void RenderTimeline (struct ImGuiWindow* window, struct ImRect timeline_bbox, guint64 start, guint64 end, guint64 step, bool verticalflip = false); void Timeline (const char* label, guint64 time, guint64 start, guint64 end, guint64 step, const float width); bool InvisibleSliderInt(const char* label, uint *index, uint min, uint max, const ImVec2 size); bool EditPlotLines(const char* label, float *array, int values_count, float values_min, float values_max, const ImVec2 size); - bool EditPlotHistoLines(const char* label, float *histogram_array, float *lines_array, int values_count, float values_min, float values_max, bool cut, bool *released, const ImVec2 size); + bool EditPlotHistoLines(const char* label, float *histogram_array, float *lines_array, int values_count, float values_min, float values_max, guint64 start, guint64 end, bool cut, bool *released, const ImVec2 size); void ShowPlotHistoLines(const char* label, float *histogram_array, float *lines_array, int values_count, float values_min, float values_max, const ImVec2 size); // fonts from ressources 'fonts/' @@ -51,12 +53,11 @@ namespace ImGuiToolkit void SetFont (font_style type, const std::string &ttf_font_name, int pointsize, int oversample = 2); void PushFont (font_style type); - void WindowText(const char* window_name, ImVec2 window_pos, const char* text); - bool WindowButton(const char* window_name, ImVec2 window_pos, const char* text); - void WindowDragFloat(const char* window_name, ImVec2 window_pos, float* v, float v_speed, float v_min, float v_max, const char* format); + // text input + bool InputText(const char* label, std::string* str); + bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0), int linesize = 0); - - // color of gui items + // accent color of UI typedef enum { ACCENT_BLUE =0, ACCENT_ORANGE, @@ -65,8 +66,11 @@ namespace ImGuiToolkit void SetAccentColor (accent_color color); struct ImVec4 HighlightColor (bool active = true); - bool InputText(const char* label, std::string* str); - bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0), int linesize = 0); + // varia + void WindowText(const char* window_name, ImVec2 window_pos, const char* text); + bool WindowButton(const char* window_name, ImVec2 window_pos, const char* text); + void WindowDragFloat(const char* window_name, ImVec2 window_pos, float* v, float v_speed, float v_min, float v_max, const char* format); + } #endif // __IMGUI_TOOLKIT_H_ diff --git a/Timeline.cpp b/Timeline.cpp index aeba084..ce4263b 100644 --- a/Timeline.cpp +++ b/Timeline.cpp @@ -142,6 +142,40 @@ bool Timeline::addGap(GstClockTime begin, GstClockTime end) return addGap( TimeInterval(begin, end) ); } +bool Timeline::cut(GstClockTime t) +{ + bool ret = false; + + if (timing_.includes(t)) + { + TimeIntervalSet::iterator g = std::find_if(gaps_.begin(), gaps_.end(), includesTime(t)); + // cut a gap + if ( g != gaps_.end() ) + { + GstClockTime b = g->begin; + gaps_.erase(g); + ret = addGap(b, t); + } + // create a gap + else { + TimeIntervalSet::iterator previous = gaps_.end(); + for (g = gaps_.begin(); g != gaps_.end(); previous = g++) { + if ( g->begin > t) + break; + } + if (previous == gaps_.end()) + ret = addGap( TimeInterval(timing_.begin, t) ); + else { + GstClockTime b = previous->begin; + gaps_.erase(previous); + ret = addGap(b, t); + } + } + } + + return ret; +} + bool Timeline::addGap(TimeInterval s) { if ( s.is_valid() ) { @@ -180,6 +214,7 @@ GstClockTime Timeline::sectionsDuration() const for (auto it = gaps_.begin(); it != gaps_.end(); ++it) d += (*it).end - (*it).begin; + // remove sum of gaps from actual duration return duration() - d; } @@ -304,6 +339,13 @@ float Timeline::fadingAt(const GstClockTime t) const return v; } +size_t Timeline::fadingIndexAt(const GstClockTime t) const +{ + double true_index = (static_cast(MAX_TIMELINE_ARRAY) * static_cast(t)) / static_cast(timing_.end); + double previous_index = floor(true_index); + return MINI( static_cast(previous_index), MAX_TIMELINE_ARRAY-1); +} + void Timeline::clearFading() { // fill static with 1 (only once) diff --git a/Timeline.h b/Timeline.h index d842b2b..5fd61e0 100644 --- a/Timeline.h +++ b/Timeline.h @@ -62,6 +62,10 @@ struct TimeInterval { return (is_valid() && t != GST_CLOCK_TIME_NONE && !(t < this->begin) && !(t > this->end) ); } + inline bool includes(const TimeInterval& b) const + { + return (is_valid() && b.is_valid() && includes(b.begin) && includes(b.end) ); + } }; @@ -69,7 +73,7 @@ struct order_comparator { inline bool operator () (const TimeInterval a, const TimeInterval b) const { - return (a < b); + return (a < b || a.begin < b.begin); } }; @@ -114,11 +118,13 @@ public: void setGaps(const TimeIntervalSet &g); bool addGap(TimeInterval s); bool addGap(GstClockTime begin, GstClockTime end); + bool cut(GstClockTime t); bool removeGaptAt(GstClockTime t); bool gapAt(const GstClockTime t, TimeInterval &gap) const; // Manipulation of Fading float fadingAt(const GstClockTime t) const; + size_t fadingIndexAt(const GstClockTime t) const; inline float *fadingArray() { return fadingArray_; } void clearFading(); diff --git a/UserInterfaceManager.cpp b/UserInterfaceManager.cpp index b0a6504..2f66945 100644 --- a/UserInterfaceManager.cpp +++ b/UserInterfaceManager.cpp @@ -2113,6 +2113,136 @@ void SourceController::Render() } +void DrawTimeScale(const char* label, guint64 duration, double width_ratio) +{ + // get window + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return; + + // get style & id + const ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + + const ImVec2 timeline_size( static_cast( static_cast(duration) * width_ratio ), 2.f * g.FontSize); + + const ImVec2 pos = window->DC.CursorPos; + const ImVec2 frame_size( timeline_size.x + 2.f * style.FramePadding.x, timeline_size.y + style.FramePadding.y); + const ImRect bbox(pos, pos + frame_size); + ImGui::ItemSize(frame_size, style.FramePadding.y); + if (!ImGui::ItemAdd(bbox, id)) + return; + + const ImVec2 timescale_pos = pos + ImVec2(style.FramePadding.x, 0.f); + const ImRect timescale_bbox( timescale_pos, timescale_pos + timeline_size); + + ImGuiToolkit::RenderTimeline(window, timescale_bbox, 0, duration, 1000, true); + +} + +void DrawTimeline(const char* label, Timeline *timeline, guint64 time, double width_ratio, float height) +{ + // get window + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return; + + // get style & id + const ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float fontsize = g.FontSize; + const ImGuiID id = window->GetID(label); + + // + // FIRST PREPARE ALL data structures + // + + // fixed elements of timeline + float *lines_array = timeline->fadingArray(); + const guint64 duration = timeline->sectionsDuration(); + TimeIntervalSet se = timeline->sections(); + const ImVec2 timeline_size( static_cast( static_cast(duration) * width_ratio ), 2.f * fontsize); + + // widget bounding box + const ImVec2 frame_pos = window->DC.CursorPos; + const ImVec2 frame_size( timeline_size.x + 2.f * style.FramePadding.x, height); + const ImRect bbox(frame_pos, frame_pos + frame_size); + ImGui::ItemSize(frame_size, style.FramePadding.y); + if (!ImGui::ItemAdd(bbox, id)) + return; + + // capture hover to avoid tooltip on plotlines + ImGui::ItemHoverable(bbox, id); + + // cursor size + const float cursor_width = 0.5f * fontsize; + + // TIMELINE is inside the bbox, at the bottom + const ImVec2 timeline_pos = frame_pos + ImVec2(style.FramePadding.x, frame_size.y - timeline_size.y -style.FramePadding.y); + const ImRect timeline_bbox( timeline_pos, timeline_pos + timeline_size); + + // PLOT of opacity is inside the bbox, at the top + const ImVec2 plot_pos = frame_pos + style.FramePadding; + const ImRect plot_bbox( plot_pos, plot_pos + ImVec2(timeline_size.x, frame_size.y - 2.f * style.FramePadding.y - timeline_size.y)); + + // + // THIRD RENDER + // + + // Render the bounding box frame + ImGui::RenderFrame(bbox.Min, bbox.Max, ImGui::GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + + // loop over sections of sources' timelines + guint64 d = 0; + guint64 e = 0; + ImVec2 section_bbox_min = timeline_bbox.Min; + for (auto section = se.begin(); section != se.end(); ++section) { + + // increment duration to adjust horizontal position + d += section->duration(); + e = section->end; + const float percent = static_cast(d) / static_cast(duration) ; + ImVec2 section_bbox_max = ImLerp(timeline_bbox.GetBL(), timeline_bbox.GetBR(), percent); + + // adjust bbox of section and render a timeline + ImRect section_bbox(section_bbox_min, section_bbox_max); + ImGuiToolkit::RenderTimeline(window, section_bbox, section->begin, section->end, timeline->step()); + + // draw the cursor + float time_ = static_cast ( static_cast(time - section->begin) / static_cast(section->duration()) ); + if ( time_ > -FLT_EPSILON && time_ < 1.f ) { + ImVec2 pos = ImLerp(section_bbox.GetTL(), section_bbox.GetTR(), time_) - ImVec2(cursor_width, 2.f); + ImGui::RenderArrow(window->DrawList, pos, ImGui::GetColorU32(ImGuiCol_SliderGrab), ImGuiDir_Up); + } + + // draw plot of lines + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0,0,0,0)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.f, 0.f)); + ImGui::SetCursorScreenPos(ImVec2(section_bbox_min.x, plot_bbox.Min.y)); + // find the index in timeline array of the section start time + size_t i = timeline->fadingIndexAt(section->begin); + // number of values is the index after end time of section (+1), minus the start index + size_t values_count = 1 + timeline->fadingIndexAt(section->end) - i; + ImGui::PlotLines("##linessection", lines_array + i, values_count, 0, NULL, 0.f, 1.f, ImVec2(section_bbox.GetWidth(), plot_bbox.GetHeight())); + ImGui::PopStyleColor(1); + ImGui::PopStyleVar(1); + + // detect if there was a gap before + if (i > 0) + window->DrawList->AddRectFilled(ImVec2(section_bbox_min.x -2.f, plot_bbox.Min.y), ImVec2(section_bbox_min.x + 2.f, plot_bbox.Max.y), ImGui::GetColorU32(ImGuiCol_TitleBg)); + + // iterate: next bbox of section starts at end of current + section_bbox_min.x = section_bbox_max.x; + } + + // detect if there is a gap after + if (e < timeline->duration()) + window->DrawList->AddRectFilled(ImVec2(section_bbox_min.x -2.f, plot_bbox.Min.y), ImVec2(section_bbox_min.x + 2.f, plot_bbox.Max.y), ImGui::GetColorU32(ImGuiCol_TitleBg)); + + +} + void SourceController::RenderSelection(size_t i) { ImVec2 top = ImGui::GetCursorScreenPos(); @@ -2121,7 +2251,6 @@ void SourceController::RenderSelection(size_t i) selection_ = Mixer::manager().session()->playGroup(i); int numsources = selection_.size(); - bool buttons_enabled = false; // no source selected if (numsources < 1) @@ -2140,27 +2269,38 @@ void SourceController::RenderSelection(size_t i) } else { /// - /// Sources grid + /// Sources LIST /// /// - // get max duration + // get max duration and max frame width GstClockTime maxduration = 0; + float maxframewidth = 0.f; for (auto source = selection_.begin(); source != selection_.end(); ++source) { MediaSource *ms = dynamic_cast(*source); if (ms != nullptr) { - GstClockTime d = ((double) ms->mediaplayer()->timeline()->sectionsDuration() / ms->mediaplayer()->playSpeed()); + GstClockTime d = (static_cast(ms->mediaplayer()->timeline()->sectionsDuration()) / ms->mediaplayer()->playSpeed()); if ( d > maxduration ) maxduration = d; } + float w = 1.5f * _timeline_height * (*source)->frame()->aspectRatio(); + if ( w > maxframewidth) + maxframewidth = w; } + // compute the ratio for timeline rendering : width (pixel) per time unit (ms) + const float w = rendersize.x -maxframewidth - 3.f * _h_space - _scrollbar; + const double width_ratio = static_cast(w) / static_cast(maxduration); - - // draw list - ImGui::BeginChild("##v_scroll2", rendersize, false, ImGuiWindowFlags_AlwaysVerticalScrollbar); + // draw list in a scroll area + ImGui::BeginChild("##v_scroll2", rendersize, false); { - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.f, _v_space)); + // draw play time scale if a duration is set + if (maxduration > 0) { + ImGui::SetCursorPos(ImGui::GetCursorPos() + ImVec2( maxframewidth + _h_space, 0)); + DrawTimeScale("##timescale", maxduration, width_ratio); + } + // loop over all sources for (auto source = selection_.begin(); source != selection_.end(); ++source) { ImVec2 framesize(1.5f * _timeline_height * (*source)->frame()->aspectRatio(), 1.5f * _timeline_height); @@ -2171,52 +2311,25 @@ void SourceController::RenderSelection(size_t i) UserInterface::manager().showSourceEditor(*source); // text below thumbnail to show status - const char *icon = ICON_FA_SNOWFLAKE; - if ((*source)->active()) { - icon = (*source)->playing() ? ICON_FA_PLAY : ICON_FA_PAUSE; - buttons_enabled = true; - } - ImGui::Text("%s %s", icon, GstToolkit::time_to_string((*source)->playtime()).c_str() ); + ImGui::Text(" %s %s", SourcePlayIcon(*source), GstToolkit::time_to_string((*source)->playtime()).c_str() ); + // get media source MediaSource *ms = dynamic_cast(*source); if (ms != nullptr) { - ImGui::SetCursorPos(image_top + ImVec2( framesize.x + _h_space, 0)); + // ok, get the media player of the media source MediaPlayer *mp = ms->mediaplayer(); - Timeline *tl = mp->timeline(); - // cut gaps -// // timeline of MediaSource to the right -// static float gaps[MAX_TIMELINE_ARRAY]; -// static float fade[MAX_TIMELINE_ARRAY]; -// size_t numsteps = tl->fillSectionsArrays(gaps, fade) - 1; + // start to draw timeline aligned at maximum frame width + horizontal space + ImGui::SetCursorPos(image_top + ImVec2( maxframewidth + _h_space, 0)); - ImVec2 size(rendersize.x - framesize.x - _h_space - _scrollbar, 1.5f * _timeline_height); + // draw the mediaplayer's timeline, with the indication of cursor position + // NB: use the same width/time ratio for all to ensure timing vertical correspondance + DrawTimeline("##timeline_mediaplayer", mp->timeline(), mp->position(), width_ratio / fabs(mp->playSpeed()), framesize.y); -// GstClockTime d = tl->sectionsDuration(); -// size.x = size.x * d / ( maxduration * mp->playSpeed() ); - -// ImGuiToolkit::ShowPlotHistoLines("##TimelineArray2", gaps, fade, numsteps, 0.f, 1.f, size); - - TimeIntervalSet se = tl->sections(); - GstClockTime playtime = tl->sectionsDuration(); - for (auto section = se.begin(); section != se.end(); ++section) { - - GstClockTime d = section->duration(); - float w = size.x * d / ( maxduration * mp->playSpeed() ); - ImGuiToolkit::Timeline("##timeline2", mp->position(), section->begin, section->end, tl->step(), w); - ImGui::SameLine(0,1); - - - } - -// ImGuiToolkit::Timeline("##timeline2", mp->position(), tl->begin(), tl->end(), tl->step(), size.x); - - // text below timeline to show info - ImGui::SetCursorPos(image_top + ImVec2( framesize.x + _h_space, 1.5f * _timeline_height + _v_space)); - GstClockTime t = (GstClockTime) ( (double) playtime / mp->playSpeed() ); - ImGui::Text("%d sections, %s play time / %.2f speed = %s (effective duration)", - se.size(), GstToolkit::time_to_string(playtime).c_str(), - mp->playSpeed(), GstToolkit::time_to_string(t).c_str()); + ImGui::SetCursorPos(image_top + ImVec2( maxframewidth + _h_space, framesize.y + _v_space)); + ImGui::Text("%s play time @ %.2f speed / %s (max duration)", + GstToolkit::time_to_string(mp->timeline()->sectionsDuration()).c_str(), + mp->playSpeed(), GstToolkit::time_to_string(maxduration).c_str()); } // next line position @@ -2224,52 +2337,15 @@ void SourceController::RenderSelection(size_t i) // ImGui::Spacing(); } - ImGui::PopStyleVar(); } ImGui::EndChild(); -// ImGui::BeginChild("##v_scroll", rendersize, false, ImGuiWindowFlags_AlwaysVerticalScrollbar); -// { -// ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.f, _v_space)); - -// // area horizontal pack -// int numcolumns = CLAMP( int(ceil(1.0f * rendersize.x / rendersize.y)), 1, numsources ); -// ImGui::Columns( numcolumns, "##selectiongrid", false); - -// for (auto source = selection_.begin(); source != selection_.end(); ++source) { - -// ImVec2 framesize(ImGui::GetColumnWidth(), ImGui::GetColumnWidth() / (*source)->frame()->aspectRatio()); -// framesize.x -= _h_space; -// ImVec2 image_top = ImGui::GetCursorPos(); - -// if (SourceButton(*source, framesize)) -// UserInterface::manager().showSourceEditor(*source); - -// // Play icon lower left corner -// ImGuiToolkit::PushFont(framesize.x > 350.f ? ImGuiToolkit::FONT_LARGE : ImGuiToolkit::FONT_MONO); -// float h = ImGui::GetTextLineHeightWithSpacing(); -// ImGui::SetCursorPos(image_top + ImVec2( _h_space, framesize.y - h)); -// if ((*source)->active()) -// ImGui::Text("%s %s", (*source)->playing() ? ICON_FA_PLAY : ICON_FA_PAUSE, GstToolkit::time_to_string((*source)->playtime()).c_str() ); -// else -// ImGui::Text("%s %s", ICON_FA_SNOWFLAKE, GstToolkit::time_to_string((*source)->playtime()).c_str() ); -// ImGui::PopFont(); - -// ImGui::Spacing(); -// ImGui::NextColumn(); -// } - -// ImGui::Columns(1); -// ImGui::PopStyleVar(); -// } -// ImGui::EndChild(); - } /// /// Play button bar /// - DrawButtonBar(bottom, rendersize.x, buttons_enabled); + DrawButtonBar(bottom, rendersize.x); /// /// Selection of sources @@ -2368,42 +2444,43 @@ void SourceController::RenderSelectedSources() ImGui::Text("Nothing to play"); ImGui::PopFont(); ImGui::PopStyleColor(1); + /// - /// Play button bar + /// Play button bar (automatically disabled) /// - DrawButtonBar(bottom, rendersize.x, false); + DrawButtonBar(bottom, rendersize.x); } // single source selected else if (numsources < 2) { /// - /// Sources display + /// Single Source display /// RenderSingleSource( selection_.front() ); } // Several sources selected else { - /// /// Sources grid /// - /// - bool buttons_enabled = false; - ImGui::BeginChild("##v_scroll", rendersize, false, ImGuiWindowFlags_AlwaysVerticalScrollbar); + ImGui::BeginChild("##v_scroll", rendersize, false); { - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.f, _v_space)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.f, 2.f * _v_space)); // area horizontal pack int numcolumns = CLAMP( int(ceil(1.0f * rendersize.x / rendersize.y)), 1, numsources ); ImGui::Columns( numcolumns, "##selectiongrid", false); + float widthcolumn = rendersize.x / static_cast(numcolumns); + widthcolumn -= _scrollbar; + // loop over sources in grid for (auto source = selection_.begin(); source != selection_.end(); ++source) { - - ImVec2 framesize(ImGui::GetColumnWidth(), ImGui::GetColumnWidth() / (*source)->frame()->aspectRatio()); - framesize.x -= _h_space; - + /// + /// Source Image Button + /// ImVec2 image_top = ImGui::GetCursorPos(); + ImVec2 framesize(widthcolumn, widthcolumn / (*source)->frame()->aspectRatio()); if (SourceButton(*source, framesize)) UserInterface::manager().showSourceEditor(*source); @@ -2411,12 +2488,7 @@ void SourceController::RenderSelectedSources() ImGuiToolkit::PushFont(framesize.x > 350.f ? ImGuiToolkit::FONT_LARGE : ImGuiToolkit::FONT_MONO); float h = ImGui::GetTextLineHeightWithSpacing(); ImGui::SetCursorPos(image_top + ImVec2( _h_space, framesize.y - h)); - if ((*source)->active()) { - ImGui::Text("%s %s", (*source)->playing() ? ICON_FA_PLAY : ICON_FA_PAUSE, GstToolkit::time_to_string((*source)->playtime()).c_str() ); - buttons_enabled = true; - } - else - ImGui::Text(ICON_FA_SNOWFLAKE " %s", GstToolkit::time_to_string((*source)->playtime()).c_str() ); + ImGui::Text("%s %s", SourcePlayIcon(*source), GstToolkit::time_to_string((*source)->playtime()).c_str() ); ImGui::PopFont(); ImGui::Spacing(); @@ -2431,10 +2503,10 @@ void SourceController::RenderSelectedSources() /// /// Play button bar /// - DrawButtonBar(bottom, rendersize.x, buttons_enabled); + DrawButtonBar(bottom, rendersize.x); /// - /// New Selection from active sources + /// Menu to store Selection from current sources /// ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.00f, 0.00f, 0.00f, 0.00f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.14f, 0.14f, 0.14f, 0.5f)); @@ -2491,10 +2563,16 @@ void SourceController::RenderSingleSource(Source *s) framesize.x = tmp.x; } + /// + /// Image + /// top += corner; ImGui::SetCursorScreenPos(top); ImGui::Image((void*)(uintptr_t) s->texture(), framesize); + /// + /// Info overlays + /// ImGui::SetCursorScreenPos(top + ImVec2(framesize.x - ImGui::GetTextLineHeightWithSpacing(), _v_space)); ImGui::Text(ICON_FA_INFO_CIRCLE); if (ImGui::IsItemHovered()){ @@ -2516,17 +2594,13 @@ void SourceController::RenderSingleSource(Source *s) // Play icon lower left corner ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); ImGui::SetCursorScreenPos(bottom + ImVec2(_h_space, -ImGui::GetTextLineHeightWithSpacing())); - if (s->active()) - ImGui::Text("%s %s", s->playing() ? ICON_FA_PLAY : ICON_FA_PAUSE, GstToolkit::time_to_string(s->playtime()).c_str() ); - else - ImGui::Text("%s %s", ICON_FA_SNOWFLAKE, GstToolkit::time_to_string(s->playtime()).c_str() ); + ImGui::Text("%s %s", SourcePlayIcon(s), GstToolkit::time_to_string(s->playtime()).c_str() ); ImGui::PopFont(); /// - /// Play source button bar + /// Play button bar /// - DrawButtonBar(bottom, rendersize.x, s->active()); - + DrawButtonBar(bottom, rendersize.x); } } @@ -2555,10 +2629,16 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) framesize.x = tmp.x; } + /// + /// Image + /// top += corner; ImGui::SetCursorScreenPos(top); ImGui::Image((void*)(uintptr_t) mp->texture(), framesize); + /// + /// Info overlays + /// ImGui::SetCursorScreenPos(top + ImVec2(framesize.x - ImGui::GetTextLineHeightWithSpacing(), _v_space)); ImGui::Text(ICON_FA_INFO_CIRCLE); if (ImGui::IsItemHovered()){ @@ -2588,11 +2668,11 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) if (mp->isEnabled()) ImGui::Text("%s %s", mp->isPlaying() ? ICON_FA_PLAY : ICON_FA_PAUSE, GstToolkit::time_to_string(mp->position()).c_str() ); else - ImGui::Text(ICON_FA_SNOWFLAKE " %s", GstToolkit::time_to_string(mp->position()).c_str() ); + ImGui::Text( ICON_FA_SNOWFLAKE " %s", GstToolkit::time_to_string(mp->position()).c_str() ); ImGui::PopFont(); /// - /// media player buttons bar + /// media player buttons bar (custom) /// draw_list->AddRectFilled(bottom, bottom + ImVec2(rendersize.x, _buttons_height), ImGui::GetColorU32(ImGuiCol_FrameBg), _h_space); @@ -2651,6 +2731,9 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) mp->setPlaySpeed( static_cast(speed) ); } + /// + /// Media Player context menu + /// ImGui::SameLine(); ImGui::SetCursorPosX(rendersize.x - _buttons_height / 1.5f); @@ -2659,11 +2742,11 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) ImGui::OpenPopup( "MenuTimeline" ); if (ImGui::BeginPopup( "MenuTimeline" )) { - if (ImGui::MenuItem(UNICODE_MULTIPLY " " ICON_FA_CARET_RIGHT " Reset Speed" )){ + if (ImGui::MenuItem(UNICODE_MULTIPLY " " ICON_FA_CARET_RIGHT " Reset speed" )){ speed = 1.f; mp->setPlaySpeed( static_cast(speed) ); } - if (ImGui::MenuItem(ICON_FA_WINDOW_CLOSE " Reset Timeline" )){ + if (ImGui::MenuItem(ICON_FA_WINDOW_CLOSE " Reset timeline" )){ timeline_zoom = 1.f; mp->timeline()->clearFading(); mp->timeline()->clearGaps(); @@ -2681,7 +2764,14 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) } ImGui::EndMenu(); } - if (Settings::application.render.gpu_decoding && ImGui::BeginMenu(ICON_FA_MICROCHIP " Hardware Decoding")) + if (ImGui::BeginMenu(ICON_FA_CUT " Auto cut" )){ + if (ImGui::MenuItem("Cut faded areas")) + if (mp->timeline()->autoCut()) + Action::manager().store("Timeline Auto cut"); + + ImGui::EndMenu(); + } + if (Settings::application.render.gpu_decoding && ImGui::BeginMenu(ICON_FA_MICROCHIP " Hardware decoding")) { bool hwdec = !mp->softwareDecodingForced(); if (ImGui::MenuItem("Auto", "", &hwdec )) @@ -2723,7 +2813,7 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) { bool released = false; if ( ImGuiToolkit::EditPlotHistoLines("##TimelineArray", tl->gapsArray(), tl->fadingArray(), - MAX_TIMELINE_ARRAY, 0.f, 1.f, + MAX_TIMELINE_ARRAY, 0.f, 1.f, tl->begin(), tl->end(), Settings::application.widget.timeline_editmode, &released, size) ) { tl->update(); } @@ -2745,15 +2835,15 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) bottom += ImVec2(scrollwindow.x + 2.f, 0.f); draw_list->AddRectFilled(bottom, bottom + ImVec2(slider_zoom_width, _timeline_height -1.f), ImGui::GetColorU32(ImGuiCol_FrameBg)); ImGui::SetCursorScreenPos(bottom + ImVec2(1.f, 0.f)); - const char *tooltip[2] = {"Draw opacity", "Cut sections"}; + const char *tooltip[2] = {"Draw opacity tool", "Cut tool"}; ImGuiToolkit::IconToggle(7,4,8,3, &Settings::application.widget.timeline_editmode, tooltip); ImGui::SetCursorScreenPos(bottom + ImVec2(1.f, 0.5f * _timeline_height)); if (Settings::application.widget.timeline_editmode) { // action auto cut - if (ImGuiToolkit::IconButton(14, 12, "Auto cut")) { - if (mp->timeline()->autoCut()) - Action::manager().store("Timeline Auto cut"); + if (ImGuiToolkit::IconButton(9, 3, "Cut at cursor")) { + if (mp->timeline()->cut(mp->position())) + Action::manager().store("Timeline Cut"); } } else { @@ -2780,9 +2870,9 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) ImGui::PopStyleVar(2); /// - /// media player actions - /// + /// media player timeline actions /// + // request seek (ASYNC) if ( slider_pressed_ && mp->go_to(seek_t) ) slider_pressed_ = false; @@ -2797,41 +2887,81 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) } } -void SourceController::DrawButtonBar(ImVec2 bottom, float width, bool enabled) +const char *SourceController::SourcePlayIcon(Source *s) { + if (s->active()) { + if ( s->playing() ) + return ICON_FA_PLAY; + else + return ICON_FA_PAUSE; + } + else + return ICON_FA_SNOWFLAKE; +} + +void SourceController::DrawButtonBar(ImVec2 bottom, float width) +{ + // draw box ImDrawList* draw_list = ImGui::GetWindowDrawList(); draw_list->AddRectFilled(bottom, bottom + ImVec2(width, _buttons_height), ImGui::GetColorU32(ImGuiCol_FrameBg), _h_space); - // buttons style: inactive if no source in selection - if (!enabled || selection_.empty()) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6, 0.6, 0.6, 0.5f)); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.00f, 0.00f, 0.00f, 0.00f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.00f, 0.00f, 0.00f, 0.00f)); + // prepare position to draw text + ImGui::SetCursorScreenPos(bottom + ImVec2(_h_space, _v_space) ); + + // play bar is enabled if only one source selected is enabled + bool enabled = false; + for (auto source = selection_.begin(); source != selection_.end(); ++source){ + if ( (*source)->active() ) { + enabled = true; + break; + } } - else { + + // buttons style for disabled / enabled bar + if (enabled) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.00f, 0.00f, 0.00f, 0.00f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.14f, 0.14f, 0.14f, 0.5f)); } + else { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6, 0.6, 0.6, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.00f, 0.00f, 0.00f, 0.00f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.00f, 0.00f, 0.00f, 0.00f)); + } - ImGui::SetCursorScreenPos(bottom + ImVec2(_h_space, _v_space) ); + // Always the rewind button if (ImGui::Button(ICON_FA_FAST_BACKWARD) && enabled) { for (auto source = selection_.begin(); source != selection_.end(); ++source) (*source)->replay(); } ImGui::SameLine(0, _h_space); - if (ImGui::Button(ICON_FA_PLAY) && enabled) { - for (auto source = selection_.begin(); source != selection_.end(); ++source) - (*source)->play(true); + + // unique play / pause button for single source + if (selection_.size() == 1) { + Source *s = selection_.front(); + if (s->playing()) { + if (ImGui::Button(ICON_FA_PAUSE) && enabled) + (s)->play(false); + } else { + if (ImGui::Button(ICON_FA_PLAY) && enabled) + (s)->play(true); + } } - ImGui::SameLine(0, _h_space); - if (ImGui::Button(ICON_FA_PAUSE) && enabled) { - for (auto source = selection_.begin(); source != selection_.end(); ++source) - (*source)->play(false); + // separate play & pause buttons for multiple sources (or none) + else { + if (ImGui::Button(ICON_FA_PLAY) && enabled) { + for (auto source = selection_.begin(); source != selection_.end(); ++source) + (*source)->play(true); + } + ImGui::SameLine(0, _h_space); + if (ImGui::Button(ICON_FA_PAUSE) && enabled) { + for (auto source = selection_.begin(); source != selection_.end(); ++source) + (*source)->play(false); + } } ImGui::SameLine(0, _h_space); - // restore + // restore style ImGui::PopStyleColor(3); } @@ -2958,7 +3088,7 @@ void Navigator::Render() ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImVec2 p1 = ImGui::GetCursorScreenPos() + ImVec2(icon_width, 0.5f * icon_width); ImVec2 p2 = ImVec2(p1.x + 2.f, p1.y + 2.f); - const ImU32 color = ImGui::GetColorU32( style.Colors[ImGuiCol_Text] ); + const ImU32 color = ImGui::GetColorU32(ImGuiCol_Text); if ((*iter)->mode() == Source::CURRENT) { p1 = ImGui::GetCursorScreenPos() + ImVec2(icon_width, 0); p2 = ImVec2(p1.x + 2.f, p1.y + icon_width); diff --git a/UserInterfaceManager.h b/UserInterfaceManager.h index c313c46..0599e02 100644 --- a/UserInterfaceManager.h +++ b/UserInterfaceManager.h @@ -118,9 +118,10 @@ class SourceController std::string active_label_; int active_selection_; InfoVisitor info_; - SourceList selection_; - void DrawButtonBar(ImVec2 bottom, float width, bool enabled = true); + + void DrawButtonBar(ImVec2 bottom, float width); + const char *SourcePlayIcon(Source *s); bool SourceButton(Source *s, ImVec2 framesize); void RenderSelectedSources(); diff --git a/rsc/images/icons.dds b/rsc/images/icons.dds index fa007ae379ace246d09953e8d7f2f44264c10e33..4e51eebfc7ab548cf68ad9856e6d35d6eec8d0db 100644 GIT binary patch delta 6890 zcmcIp33L=i8m{i1Tb%AWG85vF4tEI11XPf4Oh8Uq9ah1}Dkf1}*8_t*1rJCX$ilKH zX-6o;BZ=_LZcxG@7UFv(0X+AafFQ7oFtErWVkYQr2uWsYt9nvB6OJIhx7Ad7{;vAJ z|ERzIIw(>eEh7IGaUb%%ixFzJPUlhaV*XhN{QNGP-R1c6-!UnbqySPluyic6d@9sI z&v2XFoq6BcXkzmiSGW%5WVjy)9?Khuf~)ya&Kn6=LKnYuV3y932Wwy8Ji+s&U6Wtp z@l!ZVJ2GJ$=nATMm~-6lrd^RQsCv`dpyQz=cygLi%)k--Yj`4KX8>MM%HysKP%X-X z>QbSxLfOR|kaE8zl|yA8@;7!-mw9LS!A5X`^*q&%%4qVQS0!XfjQ(J*0Ho0${lw^1 zZD4cvO<%V$n!Z_~ej_@}#Pt}%@ejBkP?drA`9V4Op7x-v=P#BP!b6vHDL&Rdo(Iz8 zkI!g;wEtebSLdL#)21YdDx4*NJxj zsv&KV&PIMTyg$gP)1lCi!3j2aR3#TFy@XWan(W@0U>9hX^vDNb0vBFJS=k%}zzA*t zbP}KONRFNBeo1I^aNCCAku=7TGE|VAsxmS;5G}zwur?L7*+II8a20+Au392~qAnbs z{0ojfOavYodtxwm3j>_N#=P!5We@;mtj`5~!o<`f!E3{&kU-UHz9J%R{R>j_g$$>< zEUEWi&?L1;Eew;sS)mb7%FQIfWz|f*D(3UTNM{<@45Y;9h=!kgJdrlSS;wrHsJ9mv zVY!tG(?V?|nYNZ{Ein@HmJ0uIt~en)CG7)Rw6Si#3#Q#ug4B0}Mkm_XY#qs?n$3dS zqb}+W4-^O@{OqVDgid@V9CfP2{k)r`3rh5)W)GF|5Fk@{7_Nc`?bd&yV@F6YIBUG2 zn|+tB^{6M?z~jh&keI^lgga zeG);pcJYBtj5in79&!}G$%UM&iy6$+r-E8E8U=4L*j--gZvX;}fqH>vf4FrLzr+$U zQXk+jZ2rcUgUUu3I9K5=Fp6?828_qt*^$2U|4REYzBU1`a<_4b3aUlfsC=rS(Mc23 zB9|;}VW!YtkB*IEn6t{BPSO#oc*1aocywUWsJuh*&;Y;%UUK1O@N}uMf0hJf#-A76 z7iym0+LzIx$OFYxG^4|&NdME*PdvkDwP*@jF`sCn7H1W{9+6(Bvw<~YG6;(z(n3Zr zbK|$^H%0^C25%ei2RbbS8TfjB#OSt^)=6#c(MF?)=;43-%LbkQ)bzIg*u0$OU#^n) zr9yel;8K-9t~M*+4~^z2#i zEU{Z30*s^Kmi4V!tUtzwBR$pved96|)CGG6goC)k!;TAcV_LZ-wDw+>wGw4 zw`*9%p5dZ7l{(`ZIMrhRH3)hUsbj@~wj`glD=*5FSrFCMB@7RaFpf_S_aRxUy--!j z(11x?*h2?n71}*nJZB<_?>7ScwG+wml_WDgY4%a3iM?zodQ6hIS~Iy#bF=2x)8t5C zEAW;w17hCh0g2rtJP#(%5-QQpGsG>Xt3W)`BYd{Lm zm%40rG9KNvKbR>Q^z`HiUjMX|~FE71J6x#|V_P$&v?00-K@nuNrl2|BGw>L4h4 z=#_Pi>GdkZ#PjNnLqU8$nF;swHQv*?9T`YjAR270Y!IJEKWvWD{UX@|Pl$UWc+Q>b zeTNTc(Q^`$Nas-!n2+YXBHGQJVLZ&`LFH}HmDhq_)48vJrl&B94lbh@ldb_(;dDla zJP&dL+|t+hq-JyH8m|;>Hdwyb=rXK{N~b&83O8=Ku1A-wcl5);(=cz3Nrq|K5M5Lk_K6rqlLQ=hz>-eWUqG`; z)6L!y3C*q)Z?<{!G(;UTLf48jU_mgs<&O9`$`l^Q_GvWGa0oxRUg)E9c~!+%p>7e) zSA!cn)J1_P2F(zGHf=48WeD1E6&Yi%pe<$8^wPpQF&|6>^+r68wfoxWlc`Vpv|nM^ z093MFbl8$E+CJ>ZelizfuOIChx}Ur$@oDvReNy7I;#9?t)GtNJd?^r})H;ygR~$Y| z^`(W{6tf0*h8!4tiE^DMik!AO4OUxNXoPg8In3MbBD#M=X1rRQ(LN^)EqFmxXE)wZ zMb?ORg+rc6oRAQuYS9=}d|1pfU!K)<8CJScxP0%<1&yv)cv7^%Uyh1*r|u_5#RM%d zS?1RalY22O$wKegiODjbm7g$U&%JNV4JuE15IiT|VV0%e3R0q7&ir;ADwGz{Rzddi z98!!@z)a721Ne_bNwI2QNnW(bsJyG>*P47ha&(spwaw5ur1X~xw9nPT;tfM2QXQl^ zNp+zOL)`Z|d-}l+(8N5E-Sbc#{Y?uEH~_rhRW5dXE1m3%e+K!_{-m8(wQkbrue7T& z);?uS!x8$M77F78Uyo-eyBI77CUW6E=x@!|9|Xu*t&31`!2Pn_(@d^x_JUl-s3G78 zL=j6W?3Hi*14%CCopNhAxn`j$_tK?0nH&N`LJWAtF`*EZJnHt?Q>5n@WGu(cbu(Hm z3c!+;PJ;{V??kQ~W7D%7$yfn$#dn;cvp1Svd(L@<&jeDscL=fH=_kG3{*XW=^ENT3TZQbY>4%Vxo^{%Hzq5s&y-ICa2 za>GC304>3dPsp$~VoB1RYHE^NFAU&OLzK+k7ispt{6N88oON!L%~GPus)Mltzv=tIhf?g4i5333xg9{Bn1g)&=+Nq7d%YPI=# zw?`fOdV%CwxhatiTOc6i3!+<2yKar!aa|o0Jd0tqCIA z@MdyfOJ?qXbj_cXi}uE$H{!RN^TXbWuJ(eb-F^Cm+XJ0hR_Dd@adm7zOs%51kKZJe z)6P=Z7#2#o_;{&BxtX;%5CLoBW>CQI4tMLu04e_AW!>1FM`a19zK;-e+Lx5m94B`(VjH3t&1O?8HMm{=coZUgw zt;=K_6?nEs$9*x9Z-yRS8GthT-pTq^!H!|xc}(A!2~*p z$lx2F#1W0Zv3-L8G!r6Z|IfO^GI>R2ECzT_A))8DrytNFKh>93%TN2XYUSZ_Q+5~x zD|^?(yDV96d|}GLZ#3oHiSF=*d=Q#wh$p0gG&$Jws7v+~Qtxit+TIFTgd}L2vL_Nh zo1jNHdNMMscO^3tU0jDma!HNEdSRcoWPq-{El==|Rf3;j2Qon${>ImF1QYPwY7Pjv z`B`%W2{_so$f0#Jr1TQT9{M#>qN(_1tdoCp*fPSzyT}3(v$0$xK$SGKO`ccvL(Dk+ ztSLPqfNM&8B@8gwQw=ukLIsR%_>IipAHgy>0Pv^r;8CI`@r(^&tb>c-0v50)4!{?} zgvi82h5%!X5n1-=;osUMsV=fxw(F5;?tYTKYOCC*XBH|W3VE-h>kM$lSa{VxtUnr+ zW{m%t!KzS2jEh?)J2@q);6D;yxK>WW@29&+GbtwuV_zO-L&mDw<7dNE0!5 zKjQW9E_s+=Swy~4yS4yfjBQ@EZhbs@K9GIqGGs(1c>P_aECUk{!k*`jPkfpzB4NZ| zPYH7wcEe`y+0&Mc%)7w8WV_h!f!g#Q@`;A>owQ}0KXhFq9{fHd#*-krSQya>^Pl44 z1O8Y+RmkRC!O{W7^s+JmqPPh4{Mq_Zyg2GXTt9E@~ZgNCwV*)zfSrsrG1^+<$G;7ORQAkF1G(nLSvupz(UU}@}r)_^c)x8-HV2!pHMD1Zz-fVzZ2#ECPXE7YZx`=6_n2Bk8A#r2Tu~_ohII3 zk1{8Gpy7COqQ2aGz1nZf^($c*X$1>$V#LwS{cVAslgYy5=uYNk1$rK4wH6JGZ@^tm ztVGCbnqLCZWR4DH%s)#`k*^4M2*R8(@x|>!TO1^uEKG!M`t z`L=%kDzz)v0lDq87|+%A)=02*yI7IHu(paVU#7BtU^mM<~969R?`I)~#EYs4fs|;+y1psS5ZI)|O28|2_bi zhcULGzjO?<6kxr}Q>zA<45lHmek}gc^$2TA?)<;AVAl0508dP|4fR*mVo7p1S?MA* z)drv=-{tC?&gf-&X>Jg%l`BaP$OgTJx;c;Uop=vm-ELz)=m2KRIHgfwWHP{QRI$)mnkkXFZFg6qcwSHEQj~cQBoFd{8?7LRP zgSz$$TdDui;5J;r=#Y;t+2B{4{rppsXFAC2kFy8nr^3vbKQaZ5Z5ZX}{H7%{aFr%t zZ3&zlAXLCuVOYDlHX1CW40hkEGFHbiFtSiCFuD2s;LrcAd41UT+ox;4wSDIw0#*tO zE8>TY_UATP+kGtO-bCcZSYaWU#Ltm+k(IVgWeovOBz=*sPt|;DC3@F3`6YeFd$vHf zEi|Koe!{%d-~2HBef{*Owv+x6#XQ^Kl&of_*k#7ILq0`DF}NYo5HFEYq-h|WPM{%d z>edgQu(kPJ5%gTcd!ra30Nb*vCsTI@e);NSyOaqfUfEB&o7&2L(n`vb-at{UtgIE^ z$c=B&IDC%E584ONoDUr7Mf)GlveW?4{*gaQ5wz*$fDd#bGcpFWNJy`J$lm6M;EfQC zLWWApqXC74pTJmdU~H=YM)_X!ey)q`jf4C;DfwIGbw6ly;#2x2JF zwDAeq2F3dQmJM2Xz+;NJiy+S>7*~7Bq1kZMNK;6uMb5{QG9+aWoDEZ826^cU;~9gy zWC&E+^(3Z0`_h8Tt+i*;3XkNY zTlz|ww$m&eJrfw@&Vh1sxfGLW!L+nN<#FW-S)EL{=HlQn!$lSNjJLUV_(D~_t=rAj zmVh~~iA};c;%7z6Xt}=gLp$@kp%gy_(GiOFY9Hisvfk`=Hk-Z`b&f9WBrkUNnYvIvo`T+&RV9$Vt#<8lVfukh6 zx-enBTQul9{>`pr`pOM;da9CpX>FN1R=wk*9UccfPIz4KxZz2I#{*9fczVK* zF~Ac&?J)Gik~hyqFZA^Idd%#u3NBXz*GK3r9xqFap3Jq=Q#l@A^g`|*>473wR^90= zuj|%CnTA79J6E$mr38GFjqWP)E~I7K}mFz;d!Mqfc77UE05SJ-NL^hxh%*t4l}H7Bkydh$uHO3Sy&({wuAdxwWM zmwNgrF_e)^c3o5odBXJIDPM)&G7gNGSYawhGE~JETFT{3lWit!CCTYiv9O+#3}{@t zadw$b+vpjC4hbt)3#551j^TEc&xtUPYZJZ0J=%+XY0CqiRcK7PesC%n1Ccm~99DUP zMPn|TPPc9?Ss|L^BDCNwM`1pg(ri{pNDEsnH$@CY0<$L*n5yES=jDvH!Kn~X6l`E>+pcvZ?q zCDyzvN6FMw82_)aCLZL`jr2wVtH;q{Lm2I=lw!S-u=c`~I_-V0ON;b#409&Um%RNOD=`KHDa_1`2txXbkGwul2rYspIH7dds&*(Z3iwa>gT<4P7Gw8qvJJH8 zbD#=TOCj!4g>7k3n6B$n-pRrkH8gfafPYWMvmgRxnGlRC&!Amr+$`houXB}$rsx7g3l8X0J z7OO*qoWrKm29jetqdz-gsJuO5PB(|zmZQpunX|E*^|bC$UQ@SG_jT(#E%dSZv)g>K~y={39;?8)&bte?M}IotB3!K8CbkJlo>9_{=R?> zJcUPRWtq9v3CR-XsLh=K8X3s;Ds2EVRkgOjVlMa#ZSJMe?K>VQkq zef^cHw7fMe_)cn|GInlLDqK%nK5(nFWoVDt0kxg*v#(_iYduGQy2)Ls?OgiizZEe@|>d;#+S8py>;PJuZhbL3tTpW17Pyb_k`u^mObe&(T^hkLioN*n@ zf$n|v`ii-M>XCgnPx{48kzzVpr0;ACw55N)dXCj171cZKS@2}TlcVpn_iE_) EPs&*j?*IS*