From 9b432f3fc9f7a3d0f38d2ca5d538ce986a3dd9bf Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Fri, 7 Nov 2025 17:08:13 +0100 Subject: [PATCH] Refactor timeline flag handling and enhance media player functionality - Removed the TimelineSlider function declaration from ImGuiToolkit.h. - Updated InputTime function signature in ImGuiToolkit.h to include max_time and validity check. - Modified MediaPlayer to manage flag statuses more effectively, avoiding repeated pauses during flagged sections. - Added flag_status_ to MediaPlayer.h to track the current flag status. - Enhanced SessionLoader to query and set flag types when loading media player sessions. - Updated SessionVisitor to include flag type attributes in XML serialization. - Extended Settings to save and load timeline flag configurations. - Adjusted SourceControlWindow to incorporate flag type management and UI updates for flag interactions. - Implemented new TimelineSlider function to handle timeline interactions with flags. - Enhanced Timeline class to support flag types, including methods for adding, retrieving, and setting flag types. - Updated TimeInterval structure to include a type field for flags. --- rsc/images/icons.dds | Bin 1638528 -> 1638528 bytes src/ImGuiToolkit.cpp | 189 ++++++---------------- src/ImGuiToolkit.h | 3 +- src/MediaPlayer.cpp | 14 +- src/MediaPlayer.h | 1 + src/SessionCreator.cpp | 4 +- src/SessionVisitor.cpp | 1 + src/Settings.cpp | 2 + src/Settings.h | 2 + src/SourceControlWindow.cpp | 302 +++++++++++++++++++++++++++++++----- src/Timeline.cpp | 48 +++++- src/Timeline.h | 17 +- 12 files changed, 387 insertions(+), 196 deletions(-) diff --git a/rsc/images/icons.dds b/rsc/images/icons.dds index 26d142a1c4bb524e76d5f5e9b938599b2ef57558..91ad4bbd1e3b24811437441312044eee904533f1 100644 GIT binary patch delta 10372 zcmcIKeSB2KwR3mxWW|pC@8_cCiq=> zbvFuzmS}dp&o{4X&C;k;YMbEN-?gGmuvV?8n@~Sm;JH9Qhsq6mEokm%d(RyF*)8 z=++*QPw#2nAZcw|aqcf}6d^-^Le@4+0crDTI`sh=MIO5n@d+G7Fw zq|<*HR?>^^wwKQgkv|Gyl(VleE?Gust$Rx_=U&1VvNU}-gnE7Mv%>}Q-+6of!ssXPSt!Z$D!F={0 ziPP;5LaJM;{}zPv}mTb;scK~u6`Z8@H7ORU=L93(^Va8;c+1A17Jo;e_w z$}vWM3kH}+Ri{(-&YGg~DnsQEU0mh}$X_Kahu%Hpx`eK+l!LLBa@x>uQ=qfbMBCSe zRBvj#uRF#EZ4C}u<60?5fAm;hRq%u{BeHbFPhQZ9>HS>E*HSsr83rEhXBcH=pJsaLY$tt*!Bq&rJIw zk80|pMH(Ztt|LeGh8Q`6{b6qq+*X!{*{rm^-o!+I3WBi&jwS5Fg}&krtTqZ4w{J#Y z@&Nl(d*RlNTvDY20zN*7gBXd(e|RBA-#y&fvPHr{i(eu85_`h$dbMqhk7p$A#z0l- z?a88xMGCXSa-58iNy1FFpic`uHA@P}!EqAO-gu_!k{;0|{D!;ivWe@Ih)VugXRbG+ z1}C%K(h$72M(qIS%oJ20w8~h6unJ+B`tFhoFYO433N#prs5}TE!XUHa5yBlXpclZf z3j9PQ>X5W;ZRa{$+QA~{B|5baA&fW1)gWhOB0lrXRJan%=WAf2z~ATrxif8ZbvZbC zv?55SqVIocD%?n==~B5gl32URE@|s`&BAa}3o0*bjt9(QTZz4r*o zihb?a0q1S3hFPdSq=j-Y}c@B;Z(NVQ1S5QfJ%q@aj^mtvJ!S_fv=m zZ=Y``_aD^scP{uG;zB{*gsia{3zdKgSx#6RedB9V^$gWQBdT)zxJh=FuD#S5ptrAb zmHu-%YtO&)HaE$`h0bZULL3I`k%Mj=qM^KfeN|MdO z&iL3mE+aO2$B^qwh9*Z)lNpKKhi6K$ZN=K*f?p?g{vi#T46md||HIWa0jJTD>RusF@~ zHZiAQ-9a}$D~E82<=pf(mxSyt)!&pQzl7ZMo$tuyk{(P2o|P!%pxg9Q)1(eJICozx z8qECAXqj|M9U^s8{FqH9YwVU^U#l&?yKYV&iB3ggZ0S-9iFL51rFOy-W4*1EOLL8W zaJmG3z@;gdP`MXSIryVTFO<$ndRq_>>5($q4fMo9X@DNClDelP&qg{8;$J1T9=!x# zn||~Xd(qD&ML%=Wel{>SMED~N^CiNjvs8-D4`WurqDXAAKHVz(ALvIH$Vq>=n{}t-O=OwiG@pdYP0b`CVC5PzQF$(H=>QhNN3@eyr}oL*e=bvs zvil??S`z0WSs0u^s*$0mRdLDZ94s3w3(X+F*3FtM(gm}KuU<*ew% zrmemHe0{bqzZFnm>Q5)>9J8ra+AqWF(53x35A=4nI&5&HaHC!SQ`GtRw9b|i8|>lF zm-MT4*qSB1@JZ*l@;h4=LFEMywCi@*zA5SU7n~2~C!#gQ65aKN^M7IUcVTt<9y?k4 z3(*N%u$u%Ld7kHICK+V)i^D(UpjlU-q&JBg zbeV03>?Hk?QG@Ww4++`*rUmbqt=!K23#zbPd1`7FJlVvIC)18%XN#^kCwwiEek$ec z0LdFs@&PWnFaIWx9G?s&NV~J>FLTpxPWZkLOa?6`vJ2J!dzd&~rX#k4H*kK&4 zx$tC0jIo|&_FI3CAoq>*4*wMgIb^_Bf9{U|&VaoAyz_qWy=?fKzh5S0r`wFxys?C^ z2N4H-4S&}jwAR5ItNAph$?IzdDAlY9^b20Jlj%qM&Q3BBq7c$z`l+W~9YAFzf$(K-jlJZ8*A1@+-duRS@aDlg4c>e` z_LB02-(0A?W-0)2dg}><48vpWW-m{LVI;4iOLo}a(O-MOS0U5UQ_ej9eV-vpx6##d z;VpVaDzI0a*+QyW06J z9@9G?^_?%rT1%YDM4SyXJVl#d?qOlZvm;AdTh``OS5twPGQG#zzF6IorIz+^<`ftO z2tFV}p}}aQouv$=1>;Kyo3D*5?a|-s^c`^1Ry{}gw=TghB`+clYlYhl2}Xol4iJz% zyDV*6mfa4k<&<{V@^`Zk?-B7GRBFvHq92smzNKqF^PRTGwyh`*l4i&_;-XKE%zAEv z-An1huGg)@5Tn5e=E$s!RFPWDkf0zu)hLx=mHa_8spk$bVF*O!tkRJ5XGq~^$Gj{( z7<++XZjjXuM&(xKT_tU7kuW6kL!E8pauXk6`fizRv;K44cf=hAP_TmJK!wL#UST<^ zWz~~aW;-E?;A8`WyemS!NYb*ki9^<4u6 zd-T{Vz5zQ-xixWukC|&$ll(R1A6A==d!}Qs`dE`;GE}h6dAjy&nAMA3M)<+%FoIml zCdkq1;X#-Fq5kXFeXrPJt-<+R4Ww5lx)~QKgAYF`c0t-a`n^$GyFN%`$Gw4qV=Oo+ z*uqJCE^B8@J3gmgr~7>;-LY*8!sFGWwS(cll)xgRV9>1mwOz5r$x+fP+OJoc9SrCY z@VP}n39Tq#a+ZuMStf4>x5|TaXR{rVp=4m1DzmNAWAA4y83Ff&M7h;`d{TvxbVHVwYjjL9VrhKQ|v6eaEAhuvbADohB504u%T~^8I3@2>H zoc0#;;4m$9k&B3Og`kH{74qKiI05qOXH#=74;+ryk{)3}K3`ylykBO!MUQ=K8Zt`y ztX)OwRd#LiTzg#fac%H}lk@y(GmrhaX#SseCavW83R zmG&J3L!N&gY_SB#q0dOkoGGb^cyXpQ(j%|A9rh8@OPAc5UrN{CSnwrHy-m^MgFeZV z9|iXsCFX3wn>Ohrq{u<13=Ufhp&dH?jvkvdR13q{36r3C+F`cTk}}gA?=j-f9=9(d z07VE&#>g<7Yu5=Fe?>ulIdHp^vO&*K5H_eS`dDd~2~$-uiF znZA0`cDW;cs1}^zgAMtW4;LylKA0oha}DnRZRUVBbo0{%7gd_0(*MhmpGOl5mD7Q+Bo{QZ zW^6O`@&F<{hF%Saa^%uz_8o2x1U-B*=v>Dt)UkKOD|;hZ7D5O1m60yI`0Nn)gzihY z1M(Q$w$kg5dAjRV*FX<<0u6+ zQB?@N%JQqKvC^}}XC(9b35KZ(RvFV9x&GA|{ZD%3QWcI+1V0eg%gR`_fMbzj>3&!$ zHmY&(#G|l8i58Z_WM)fcNdm)tC7MEMDsbcukv@S5mI(RWczox^g~u?oLRG!}A(&aimO;`HRs=CRDfk!%Ew(H%YUZkBRGs9B3g;V$q~{|# z7PKJ?54S%b-qJ)j{zU$Yu3hIpi5`B{7+flywpKVWSl9p07tQmu-62>y;#qkZsOMmp z)1tUkyFj(pt`V5yJbq?6?}9j0m36PlJ!p;J=b^s$;A;C1tihBM?N;H$3qYskLF)H# ztgVA3iMG7#RjaU4`7fSVqlyS_eUWqGDkCgIe8d`Thodw-wvJnPPSBif;CMp~lN0p< zi?MJ2SD@a_C5@u0>5az_&g89#=wAxcnqbKs^Qe}ehe^5ZFVU~HA z2hvq287*pqBm}2+5m6Q9nW8niXj?3z9ae|#Y{TV3Ryk=K$%PwX+GPZpiLDDHU=A6W>6$gd?EJ2-k({hY4^gB4gnO za3`f&;x-9lk&qq@`-?o+D7On7&U4*ClLoc5lPAmzh2JqFp%wV}JH~?N1dPF2>s}~d zRu85Tm2hI848rlHfLXdRfE@|=+3R_?CM?mF=h-jRweR~+!s4Rle?m^E9nJLu9R|O+ zdn!c-8JADzMw;l7by9gNhtu(C{2{76T%V;?`=j*u93Lbe+Q=6#URt;SX+(>}&A zjX!+UdYGpxE8&;8(3qZXqTO%G6?(ts?{zB?%=xV4=Tq%efm-ndXOqIe$jU|@lGwHP z%oZ?NNuPVvetyM|u)=T={$>0@ihA%+NC(jX-`N#^!Y_xLo76PKcU4djvhildZo1Qg z;r{cIk2wqEz@;VMx>B?UA<}oJ$IW!`9Z2#XyUQAa7r_{J{@8+Hfl8i8E8Hr;ZZT(Q z%L)as=|9_?8d)SdNX;pQ?XJaO>dw1BdY<|Y$$_O?CoCPfunD bQtQB8KfD2W3*aq;w+P;1ePHkO=+gfKF~me2 delta 6800 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 U@MOZ11y43SIkf+sTz|>`16V( ceil(g.FontSize / space_size.x) ); space_buf[space_num]='\0'; - char text_buf[256]; + char text_buf[512]; ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), "%s %s", space_buf, tooltip); ImVec2 draw_pos = ImGui::GetCursorScreenPos() + g.Style.FramePadding * 0.5; @@ -237,7 +237,9 @@ bool ImGuiToolkit::ButtonIcon(int i, int j, const char *tooltip, bool enabled, b ImVec2 uv0( static_cast(i) * 0.05, static_cast(j) * 0.05 ); ImVec2 uv1( uv0.x + 0.05, uv0.y + 0.05 ); - ImGui::PushID( i*20 + j); + char text_buf[512]; + ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), "ButtonIcon %d %d %s", i, j, tooltip); + ImGui::PushID( text_buf ); if (enabled) ret = ImGui::ImageButton((void*)(intptr_t)textureicons, ImVec2(g.FontSize, g.FontSize), @@ -297,7 +299,9 @@ bool ImGuiToolkit::IconButton(int i, int j, const char *tooltip, const char* sho if (window->SkipItems) return false; - ImGui::PushID( i * 20 + j + ( tooltip ? window->GetID(tooltip) : 0) ); + char text_buf[512]; + ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), "IconButton %d %d %s", i, j, tooltip); + ImGui::PushID( text_buf ); // duplicate of ImGui::InvisibleButton to handle ImGuiButtonFlags_Repeat const ImGuiID id = window->GetID("##iconijbutton"); @@ -338,7 +342,9 @@ bool ImGuiToolkit::IconButton(int i, int j, const char *tooltip, const char* sho bool ImGuiToolkit::IconButton(const char* icon, const char *tooltip, const char* shortcut) { bool ret = false; - ImGui::PushID( icon ); + char text_buf[512]; + ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), "IconButton %s %s", icon, tooltip); + ImGui::PushID( text_buf ); float frame_height = ImGui::GetFrameHeight(); ImVec2 draw_pos = ImGui::GetCursorScreenPos(); @@ -414,7 +420,9 @@ bool ImGuiToolkit::TextButton(const char* text, const char *tooltip, const char* bool ImGuiToolkit::IconToggle(int i, int j, int i_toggle, int j_toggle, bool* toggle, const char *tooltips[]) { bool ret = false; - ImGui::PushID( i * 20 + j + i_toggle * 20 + j_toggle); + char text_buf[512]; + ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), "IconToggle %d %d %d %d ", i, j, i_toggle, j_toggle); + ImGui::PushID( text_buf ); float frame_height = ImGui::GetFrameHeight(); float frame_width = frame_height; @@ -612,7 +620,9 @@ bool ImGuiToolkit::SelectableIcon(int i, int j, const char* label, bool selected char text_buf[256]; ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), " %s", label); - ImGui::PushID( i * 20 + j + ImGui::GetID(text_buf) ); + char id_buf[512]; + ImFormatString(id_buf, IM_ARRAYSIZE(id_buf), "SelectableIcon %d %d %s", i, j, label); + ImGui::PushID( id_buf ); // draw menu item bool ret = ImGui::Selectable(text_buf, selected, ImGuiSelectableFlags_None, size_arg); @@ -1098,107 +1108,6 @@ void ImGuiToolkit::RenderTimelineBPM (ImVec2 min_bbox, ImVec2 max_bbox, double t ImGui::PopStyleColor(1); } - -bool ImGuiToolkit::TimelineSlider (const char* label, guint64 *time, guint64 begin, guint64 first, - 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 - begin) / static_cast(end - begin) ); - - // - // 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){ - *time = static_cast ( 0.1 * static_cast(time_slider) * static_cast(end - begin) ); - if (first != GST_CLOCK_TIME_NONE) - *time -= first; - 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); - - // render the timeline - RenderTimeline(timeline_bbox.Min, timeline_bbox.Max, begin, 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; -} - - - bool ImGuiToolkit::InvisibleSliderInt (const char* label, uint *index, uint min, uint max, ImVec2 size) { // get window @@ -1680,12 +1589,11 @@ void word_wrap(std::string *str, unsigned per_line) -bool ImGuiToolkit::InputTime(const char *label, guint64 *time, ImGuiInputTextFlags flag) +bool ImGuiToolkit::InputTime(const char *label, guint64 *time, guint64 max_time, bool *valid, ImGuiInputTextFlags flag) { bool changed = false; // filtering for reading MM:SS.MS text entry - static bool valid = false; static std::regex RegExTime("([0-9]+\\:)?([0-9]+\\:)?([0-5][0-9]|[0-9])((\\.|\\,)[0-9]+)?"); struct TextFilters { @@ -1698,7 +1606,8 @@ bool ImGuiToolkit::InputTime(const char *label, guint64 *time, ImGuiInputTextFla }; // convert gst time to hh mm s.ms - guint64 ms = GST_TIME_AS_MSECONDS(*time); + guint64 tmp_time = *time; + guint64 ms = GST_TIME_AS_MSECONDS(tmp_time); guint64 hh = ms / 3600000; guint64 mm = (ms % 3600000) / 60000; ms -= (hh * 3600000 + mm * 60000); @@ -1713,7 +1622,7 @@ bool ImGuiToolkit::InputTime(const char *label, guint64 *time, ImGuiInputTextFla ImGui::PushStyleColor(ImGuiCol_Text, flag & ImGuiInputTextFlags_ReadOnly ? g.Style.Colors[ImGuiCol_TextDisabled] - : ImVec4(1.0f, valid ? 1.0f : 0.2f, valid ? 1.0f : 0.2f, 1.f)); + : ImVec4(1.0f, *valid ? 1.0f : 0.2f, *valid ? 1.0f : 0.2f, 1.f)); ImGui::PushStyleColor(ImGuiCol_FrameBg, flag & ImGuiInputTextFlags_ReadOnly ? g.Style.Colors[ImGuiCol_WindowBg] : g.Style.Colors[ImGuiCol_FrameBg]); @@ -1727,41 +1636,41 @@ bool ImGuiToolkit::InputTime(const char *label, guint64 *time, ImGuiInputTextFla ImGui::PopStyleColor(2); // test string format with regular expression - valid = std::regex_match(buf_time_input, RegExTime); + *valid = std::regex_match(buf_time_input, RegExTime); - if (ImGui::IsItemDeactivatedAfterEdit()) { - if (valid) { - ms = 0; - sec = 0.f; - // user confirmed the entry and the input is valid - // split the "HH:MM:SS.ms" string in HH MM SS.ms - std::string timing(buf_time_input); - std::size_t found = timing.find_last_of(':'); - // read the right part SS.ms as a value + if (valid) { + ms = 0; + sec = 0.f; + // split the "HH:MM:SS.ms" string in HH MM SS.ms + std::string timing(buf_time_input); + std::size_t found = timing.find_last_of(':'); + // read the right part SS.ms as a value + if (std::string::npos != found + && BaseToolkit::is_a_value(timing.substr(found + 1), &sec)) { + ms = (guint64) (sec * 1000.f); + // read right part MM as a number + timing = timing.substr(0, found); + found = timing.find_last_of(':'); + int min = 0; if (std::string::npos != found - && BaseToolkit::is_a_value(timing.substr(found + 1), &sec)) { - ms = (guint64) (sec * 1000.f); - // read right part MM as a number + && BaseToolkit::is_a_number(timing.substr(found + 1), &min)) { + ms += 60000 * (guint64) min; + // read right part HH as a number timing = timing.substr(0, found); - found = timing.find_last_of(':'); - int min = 0; - if (std::string::npos != found - && BaseToolkit::is_a_number(timing.substr(found + 1), &min)) { - ms += 60000 * (guint64) min; - // read right part HH as a number - timing = timing.substr(0, found); - int hour = 0; - if (std::string::npos != found && BaseToolkit::is_a_number(timing, &hour)) { - ms += 3600000 * (guint64) hour; - } + int hour = 0; + if (std::string::npos != found && BaseToolkit::is_a_number(timing, &hour)) { + ms += 3600000 * (guint64) hour; } } - // set time - *time = GST_MSECOND * ms; - changed = true; } - // force to test validity next frame - valid = false; + // set time + tmp_time = GST_MSECOND * ms; + *valid &= tmp_time < max_time; + } + + if (valid && ImGui::IsItemDeactivatedAfterEdit()) { + *time = tmp_time; + changed = true; } return changed; diff --git a/src/ImGuiToolkit.h b/src/ImGuiToolkit.h index dc06b3e..393868c 100644 --- a/src/ImGuiToolkit.h +++ b/src/ImGuiToolkit.h @@ -52,7 +52,6 @@ namespace ImGuiToolkit // sliders bool SliderTiming (const char* label, uint *ms, uint v_min, uint v_max, uint v_step, const char* text_max = nullptr); - bool TimelineSlider (const char* label, guint64 *time, guint64 begin, guint64 first, guint64 end, guint64 step, const float width); void RenderTimeline (ImVec2 min_bbox, ImVec2 max_bbox, guint64 begin, guint64 end, guint64 step, bool verticalflip = false); void RenderTimelineBPM (ImVec2 min_bbox, ImVec2 max_bbox, double tempo, double quantum, guint64 begin, guint64 end, guint64 step, bool verticalflip = false); bool InvisibleSliderInt(const char* label, uint *index, uint min, uint max, const ImVec2 size); @@ -75,7 +74,7 @@ namespace ImGuiToolkit void Spacing(); // text input - bool InputTime(const char *label, guint64 *time, ImGuiInputTextFlags flag = 0); + bool InputTime(const char *label, guint64 *time, guint64 max_time, bool *valid, ImGuiInputTextFlags flag = 0); bool InputText(const char* label, std::string* str, ImGuiInputTextFlags flag = ImGuiInputTextFlags_CharsNoBlank); bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0), int *numline = NULL); void TextMultiline(const char* label, const std::string &str, float width); diff --git a/src/MediaPlayer.cpp b/src/MediaPlayer.cpp index 70090fa..09e95f1 100644 --- a/src/MediaPlayer.cpp +++ b/src/MediaPlayer.cpp @@ -1401,16 +1401,16 @@ void MediaPlayer::update() } // test if position is flagged else if ( isPlaying() ) { + int t = timeline_.flagTypeAt(position_); + if ( t > 0 ) { // Avoid to pause repeatedly when inside a flagged section - static bool _in_flagged = false; - if (timeline_.isFlagged(position_)) { - // pause at this position - if (!_in_flagged) + if (flag_status_ == LoopStatus::LOOP_STATUS_DEFAULT) { + loop_status_ = flag_status_ = (LoopStatus) t; play(false); - _in_flagged = true; + } } - else - _in_flagged = false; + else + flag_status_ = LoopStatus::LOOP_STATUS_DEFAULT; } } diff --git a/src/MediaPlayer.h b/src/MediaPlayer.h index 7cda45e..81e4c63 100644 --- a/src/MediaPlayer.h +++ b/src/MediaPlayer.h @@ -328,6 +328,7 @@ private: GstClockTime position_; LoopMode loop_; LoopStatus loop_status_; + LoopStatus flag_status_; GstState desired_state_; GstElement *pipeline_; GstBus *bus_; diff --git a/src/SessionCreator.cpp b/src/SessionCreator.cpp index 285c485..65ab337 100644 --- a/src/SessionCreator.cpp +++ b/src/SessionCreator.cpp @@ -933,9 +933,11 @@ void SessionLoader::visit(MediaPlayer &n) { uint64_t a = GST_CLOCK_TIME_NONE; uint64_t b = GST_CLOCK_TIME_NONE; + int t = 0; flag->QueryUnsigned64Attribute("begin", &a); flag->QueryUnsigned64Attribute("end", &b); - tl.addFlag( TimeInterval( (GstClockTime) a, (GstClockTime) b ) ); + flag->QueryIntAttribute("type", &t); + tl.addFlag( TimeInterval( (GstClockTime) a, (GstClockTime) b ), t ); } } n.setTimeline(tl); diff --git a/src/SessionVisitor.cpp b/src/SessionVisitor.cpp index eb5c347..b69d6f4 100644 --- a/src/SessionVisitor.cpp +++ b/src/SessionVisitor.cpp @@ -477,6 +477,7 @@ void SessionVisitor::visit(MediaPlayer &n) XMLElement *f = xmlDoc_->NewElement("Interval"); f->SetAttribute("begin", (uint64_t) (*it).begin); f->SetAttribute("end", (uint64_t) (*it).end); + f->SetAttribute("type", (int) (*it).type); flagselement->InsertEndChild(f); } timelineelement->InsertEndChild(flagselement); diff --git a/src/Settings.cpp b/src/Settings.cpp index 1d1f277..10df361 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -183,6 +183,7 @@ void Settings::Save(uint64_t runtime, const std::string &filename) widgetsNode->SetAttribute("media_player", application.widget.media_player); widgetsNode->SetAttribute("media_player_view", application.widget.media_player_view); widgetsNode->SetAttribute("timeline_editmode", application.widget.media_player_timeline_editmode); + widgetsNode->SetAttribute("timeline_flag", application.widget.media_player_timeline_flag); widgetsNode->SetAttribute("media_player_slider", application.widget.media_player_slider); widgetsNode->SetAttribute("shader_editor", application.widget.shader_editor); widgetsNode->SetAttribute("shader_editor_view", application.widget.shader_editor_view); @@ -522,6 +523,7 @@ void Settings::Load(const std::string &filename) widgetsNode->QueryBoolAttribute("media_player", &application.widget.media_player); widgetsNode->QueryIntAttribute("media_player_view", &application.widget.media_player_view); widgetsNode->QueryIntAttribute("timeline_editmode", &application.widget.media_player_timeline_editmode); + widgetsNode->QueryIntAttribute("timeline_flag", &application.widget.media_player_timeline_flag); widgetsNode->QueryFloatAttribute("media_player_slider", &application.widget.media_player_slider); widgetsNode->QueryBoolAttribute("shader_editor", &application.widget.shader_editor); widgetsNode->QueryIntAttribute("shader_editor_view", &application.widget.shader_editor_view); diff --git a/src/Settings.h b/src/Settings.h index 525d976..4b0a7be 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -24,6 +24,7 @@ struct WidgetsConfig bool media_player; int media_player_view; int media_player_timeline_editmode; + int media_player_timeline_flag; float media_player_slider; bool timer; int timer_view; @@ -47,6 +48,7 @@ struct WidgetsConfig media_player = false; media_player_view = -1; media_player_timeline_editmode = 0; + media_player_timeline_flag = 1; media_player_slider = 0.f; toolbox = false; help = false; diff --git a/src/SourceControlWindow.cpp b/src/SourceControlWindow.cpp index eda0473..37fce84 100644 --- a/src/SourceControlWindow.cpp +++ b/src/SourceControlWindow.cpp @@ -579,7 +579,7 @@ bool EditTimeline(const char *label, const guint64 end = tl->end(); bool cursor_dot = edit_mode == 1; - bool cursor_flag = false; + int cursor_flag = -1; Timeline _tl; bool array_changed = false; float *lines_array = tl->fadingArray(); @@ -635,6 +635,7 @@ bool EditTimeline(const char *label, char cursor_text[64]; guint64 time = begin + (index * end) / static_cast(MAX_TIMELINE_ARRAY); static guint64 removed_flag_time = 0; + static int removed_flag_type = -1; // enter edit if widget is active if (ImGui::GetActiveID() == id) { @@ -676,17 +677,18 @@ bool EditTimeline(const char *label, } else if (edit_mode == 2) { if (!active) { - // remove flag on mouse press + // remove flag on mouse press if ( tl->isFlagged(time) ) { removed_flag_time = time; + removed_flag_type = tl->flagTypeAt(time); tl->removeFlagAt(time); } // add flag on mouse release else active = true; } - else if (! tl->isFlagged(time) ) { - cursor_flag = true; + else if ( !tl->isFlagged(time) ) { + cursor_flag = tl->flagTypeAt(time);; } } @@ -699,9 +701,13 @@ bool EditTimeline(const char *label, if (edit_mode == 2 && active) { // exception: if flag was removed at same time, do not add it back if ( removed_flag_time != time ) { - tl->addFlag(time); + if (removed_flag_type >= 0) + tl->addFlag(time, removed_flag_type); + else + tl->addFlag(time, Settings::application.widget.media_player_timeline_flag); } removed_flag_time = 0; + removed_flag_type = -1; } active = false; @@ -763,7 +769,7 @@ bool EditTimeline(const char *label, break; case TimelinePayload::FLAG_ADD: _tl = *tl; - _tl.addFlag(time); + _tl.addFlag(time, pl->argument); flags_array = _tl.flagsArray(); break; case TimelinePayload::FLAG_REMOVE: @@ -787,7 +793,7 @@ bool EditTimeline(const char *label, } else { if ( edit_mode == 2 && tl->isFlagged(time) ) { - cursor_flag = true; + cursor_flag = tl->flagTypeAt(time);; } } @@ -839,10 +845,10 @@ bool EditTimeline(const char *label, cursor_pos = cursor_pos + mouse_pos_in_canvas; window->DrawList->AddCircleFilled( cursor_pos, 3.f, cur_color, 8); } - else if (cursor_flag) { + else if (cursor_flag >= 0) { cursor_pos = cursor_pos + ImVec2(mouse_pos_in_canvas.x, 12.f); window->DrawList->AddLine( cursor_pos, cursor_pos + ImVec2(0.f, size.y - 8.f), cur_color); - _drawIcon(cursor_pos - ImVec2(2.f, 1.f), 12, 6, true, window); + _drawIcon(cursor_pos - ImVec2(2.f, 1.f), 11 + cursor_flag, 6, true, window); } else { cursor_pos = cursor_pos + ImVec2(mouse_pos_in_canvas.x, 4.f); @@ -863,6 +869,139 @@ bool EditTimeline(const char *label, return array_changed; } +bool TimelineSlider (const char* label, guint64 *time, Timeline *tl, 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); + + // + // PREPARE 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 - tl->begin()) / static_cast(tl->duration()) ); + + // 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 + ImGuiToolkit::RenderTimeline(timeline_bbox.Min, timeline_bbox.Max, tl->begin(), tl->end(), tl->step()); + + // + // FLAGS + // + bool flag_clicked = false; + const TimeIntervalSet flags = tl->flags(); + for (const auto &flag_Interval : flags) { + + GstClockTime flag_time = flag_Interval.midpoint(); + float flag_pos_ = static_cast ( static_cast(flag_time - tl->begin()) / static_cast(tl->duration()) ); + ImVec2 flag_pos = ImLerp(timeline_bbox.GetTL(), timeline_bbox.GetTR(), flag_pos_); + flag_pos -= ImVec2(2.f, -3.f); + + bool hovered = false, held = false; + ImRect bb(flag_pos, flag_pos + ImVec2(ImGui::GetTextLineHeightWithSpacing(), ImGui::GetTextLineHeightWithSpacing())); + + const ImGuiID fid = window->GetID((void*)(intptr_t)(flag_time)); + if ( ImGui::ButtonBehavior(bb, fid, &hovered, &held, ImGuiButtonFlags_PressedOnClick) ) { + *time = flag_time; + flag_clicked = true; + } + + // icon depends on flag type + _drawIcon(flag_pos, 11 + flag_Interval.type, 6, hovered, window); + // show time when hovering + if (hovered) + ImGui::SetTooltip(" %s ", GstToolkit::time_to_string(flag_time).c_str()); + } + + // + // GET SLIDER 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); + + // slider only if no flag clicked + if (!flag_clicked && !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){ + + *time = static_cast ( 0.1 * static_cast(time_slider) * static_cast(tl->duration()) ); + if (tl->first() != GST_CLOCK_TIME_NONE) + *time -= tl->first(); + grab_slider_color = ImGui::GetColorU32(ImGuiCol_SliderGrabActive); + + ImGui::MarkItemEdited(id); + } + + // + // RENDER CURSOR + // + + // 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 (flag_clicked || left_mouse_press); +} + + std::list< std::pair > DrawTimeline(const char* label, Timeline *timeline, guint64 time, double width_ratio, float height) { @@ -1893,8 +2032,6 @@ void SourceControlWindow::RenderSingleSource(Source *s) void DragButtonIcon(int i, int j, const char *tooltip, TimelinePayload payload) { - - ImGuiToolkit::ButtonIcon(i, j, tooltip); if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { // _payload.action = TimelinePayload::FADE_OUT_IN; @@ -1904,12 +2041,15 @@ void DragButtonIcon(int i, int j, const char *tooltip, TimelinePayload payload) ImGuiToolkit::Icon(i, j); ImGui::EndDragDropSource(); } + if (ImGui::IsItemHovered()) + ImGui::SetMouseCursor(7); // ImGuiMouseCursor_Hand } void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) { static bool show_overlay_info = false; static std::vector< std::pair > editmode_icon = { {8, 3}, {7, 4}, {12, 6} }; + editmode_icon[2] = { Settings::application.widget.media_player_timeline_flag + 11, 6 }; static std::vector< std::string > editmode_tooltip = { "Cutting tool", "Fading tool", "Flag tool" }; mediaplayer_active_ = ms->mediaplayer(); @@ -2022,6 +2162,11 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) /// /// media player timelines /// + static std::vector< std::pair > icons_loop = { {0, 15}, {1, 15}, {19, 14}, {18, 14} }; + static std::vector< std::string > tooltips_loop = { "Stop at end", "Loop to start", "Bounce (reverse speed)", "Stop and blackout at end" }; + static std::vector< std::pair > icons_flags = { {11, 6}, {12, 6}, {13, 6} }; + static std::vector< std::string > tooltips_flags = { "Bookmark", "Stop Flag", "Blackout Flag" }; + double current_play_speed = mediaplayer_active_->playSpeed(); static uint counter_menu_timeout = 0; const ImVec2 scrollwindow = ImVec2(ImGui::GetContentRegionAvail().x - slider_zoom_width - 3.0, @@ -2070,8 +2215,7 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) } // custom timeline slider // TODO : if (mediaplayer_active_->syncToMetronome() > Metronome::SYNC_NONE) - mediaplayer_slider_pressed_ = ImGuiToolkit::TimelineSlider("##timeline", &seek_t, tl->begin(), - tl->first(), tl->end(), tl->step(), size.x); + mediaplayer_slider_pressed_ = TimelineSlider("##timeline", &seek_t, tl, size.x); } } ImGui::EndChild(); @@ -2125,7 +2269,8 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) ImGui::SameLine(0, h_space_); ImGui::PushButtonRepeat(true); - if (ImGui::Button( mediaplayer_active_->playSpeed() < 0 ? ICON_FA_BACKWARD :ICON_FA_FORWARD)) + if (ImGui::Button( mediaplayer_active_->playSpeed() < 0 ? ICON_FA_BACKWARD :ICON_FA_FORWARD, + ImVec2(ImGui::GetFrameHeightWithSpacing(), 0)) ) mediaplayer_active_->jump (); ImGui::PopButtonRepeat(); } @@ -2138,25 +2283,53 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) ImGui::SameLine(0, h_space_); ImGui::PushButtonRepeat(true); - if (ImGui::Button( mediaplayer_active_->playSpeed() < 0 ? ICON_FA_STEP_BACKWARD : ICON_FA_STEP_FORWARD)) + if (ImGui::Button( mediaplayer_active_->playSpeed() < 0 ? ICON_FA_STEP_BACKWARD : ICON_FA_STEP_FORWARD, + ImVec2(ImGui::GetFrameHeightWithSpacing(), 0))) mediaplayer_active_->step(); ImGui::PopButtonRepeat(); } - // loop modes button - ImGui::SameLine(0, h_space_); - static int current_loop = 0; - static std::vector< std::pair > icons_loop = { {0, 15}, {1, 15}, {19, 14}, {18, 14} }; - static std::vector< std::string > tooltips_loop = { "Stop at end", "Loop to start", "Bounce (reverse speed)", "Stop and blackout at end" }; - current_loop = (int) mediaplayer_active_->loop(); - if ( ImGuiToolkit::IconMultistate(icons_loop, ¤t_loop, tooltips_loop) ) - mediaplayer_active_->setLoop( (MediaPlayer::LoopMode) current_loop ); + // flag buttons + if ( !mediaplayer_mode_ && mediaplayer_active_->timeline()->numFlags() > 0 ) { + + ImGui::SameLine(0, h_space_); + if( ImGuiToolkit::ButtonIcon(3, 0, "Go to next flag") ){ + // find next flag and go to its midpoint + TimeInterval next_flag = mediaplayer_active_->timeline()->getNextFlag( mediaplayer_active_->position() ); + if ( next_flag.is_valid() ) + mediaplayer_active_->go_to( next_flag.midpoint() ); + } + + // if stopped at a flag, show flag type editor + if (mediaplayer_active_->timeline()->isFlagged( mediaplayer_active_->position() )) { + static int current_flag = 0; + current_flag = mediaplayer_active_->timeline()->flagTypeAt( mediaplayer_active_->position() ); + ImGui::SameLine(0, h_space_); + if ( ImGuiToolkit::IconMultistate(icons_flags, ¤t_flag, tooltips_flags) ){ + mediaplayer_active_->timeline()->setFlagTypeAt( mediaplayer_active_->position(), current_flag ); + oss << ": Flag type changed"; + Action::manager().store(oss.str()); + } + } + } + else { + ImGui::SameLine(0, h_space_); + ImGuiToolkit::ButtonIcon(3, 0, nullptr, false); + } + + // right aligned buttons (if enough space) + if ( rendersize.x > min_width_ * 1.5f ) { + + // loop modes button + ImGui::SameLine(0, MAX(h_space_ , rendersize.x - min_width_ * 1.55f) ); + static int current_loop = 0; + current_loop = (int) mediaplayer_active_->loop(); + if ( ImGuiToolkit::IconMultistate(icons_loop, ¤t_loop, tooltips_loop) ) + mediaplayer_active_->setLoop( (MediaPlayer::LoopMode) current_loop ); - // speed slider (if enough space) - if ( rendersize.x > min_width_ * 1.2f ) { - ImGui::SameLine(0, MAX(h_space_ * 2.f, rendersize.x - min_width_ * 1.4f) ); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - buttons_height_ ); // speed slider + ImGui::SameLine(0, h_space_); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - buttons_height_ ); float s = fabs(static_cast(current_play_speed)); if (ImGui::DragFloat( "##Speed", &s, 0.01f, 0.1f, 10.f, UNICODE_MULTIPLY " %.2f")) mediaplayer_active_->setPlaySpeed( SIGN(current_play_speed) * static_cast(s) ); @@ -2274,7 +2447,8 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) // variables state of panel static int current_curve = 2; static uint d = UINT_MAX; - static guint64 target_time = 30000000000; + static guint64 target_time = 1000000000; + static bool target_time_valid = true; // timeline to edit Timeline *tl = mediaplayer_active_->timeline(); @@ -2331,19 +2505,18 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) ImGui::Text("|"); /// Enter time value for CUT - target_time = MIN(target_time, tl->duration()); ImGui::SameLine(0, 0); ImVec2 draw_pos = ImGui::GetCursorPos(); float w = gap_dialog_size.x - 4.f * ImGui::GetTextLineHeightWithSpacing() ; ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10, 11)); ImGui::SetNextItemWidth(w - draw_pos.x - IMGUI_SAME_LINE); // VARIABLE WIDTH - ImGuiToolkit::InputTime("##Time", &target_time); + ImGuiToolkit::InputTime("##TimeCut", &target_time, tl->duration(), &target_time_valid); ImGui::PopStyleVar(); /// CUT LEFT TIME ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::SetCursorPos( ImVec2(w, draw_pos.y)); - if (ImGuiToolkit::ButtonIcon(17, 3, "Cut left at given time")) { + if (ImGuiToolkit::ButtonIcon(17, 3, "Cut left at given time", target_time_valid)) { tl->cut(target_time, true); tl->refresh(); oss << ": Timeline cut"; @@ -2351,7 +2524,7 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) } /// CUT RIGHT TIME ImGui::SameLine(0, IMGUI_SAME_LINE); - if (ImGuiToolkit::ButtonIcon(18, 3, "Cut right at given time")){ + if (ImGuiToolkit::ButtonIcon(18, 3, "Cut right at given time", target_time_valid)){ tl->cut(target_time, false); tl->refresh(); oss << ": Timeline cut"; @@ -2542,15 +2715,72 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); /// - /// CUT LEFT OF CURSOR + /// DROP ICONS /// - DragButtonIcon(12, 6, "Drop in timeline to\nAdd flag", + DragButtonIcon(11, 6, "Drop in timeline to\nAdd a Bookmark", + TimelinePayload(TimelinePayload::FLAG_ADD, 0, 0) ); + + ImGui::SameLine(0, IMGUI_SAME_LINE); + DragButtonIcon(12, 6, "Drop in timeline to\nAdd a Stop Flag", TimelinePayload(TimelinePayload::FLAG_ADD, 0, 1) ); ImGui::SameLine(0, IMGUI_SAME_LINE); + DragButtonIcon(13, 6, "Drop in timeline to\nAdd a Blackout Flag", + TimelinePayload(TimelinePayload::FLAG_ADD, 0, 2) ); - DragButtonIcon(6, 0, "Drop in timeline to\nDelete flag", - TimelinePayload(TimelinePayload::FLAG_REMOVE, 0, 1) ); + ImGui::SameLine(0, IMGUI_SAME_LINE); + DragButtonIcon(2, 0, "Drop in timeline to\nErase a flag", + TimelinePayload(TimelinePayload::FLAG_REMOVE, 0, 0) ); + + /// + /// SECTION WITH BUTTONS + /// + ImGui::SameLine(0, 0); + ImGui::Text("|"); + ImGui::SameLine(0, 0); + + ImGuiToolkit::IconMultistate(icons_flags, + &Settings::application.widget.media_player_timeline_flag, tooltips_flags); + + /// Enter time value for Flag + ImGui::SameLine(0, IMGUI_SAME_LINE); + ImVec2 draw_pos = ImGui::GetCursorPos(); + float w = gap_dialog_size.x - 4.f * ImGui::GetTextLineHeightWithSpacing() ; + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10, 11)); + ImGui::SetNextItemWidth(w - draw_pos.x - IMGUI_SAME_LINE); // VARIABLE WIDTH + ImGuiToolkit::InputTime("##TimeFlag", &target_time, tl->duration(), &target_time_valid); + ImGui::PopStyleVar(); + + /// FLAG AT TIME + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::SetCursorPos( ImVec2(w, draw_pos.y)); + if (ImGuiToolkit::ButtonIcon(1, 0, "Add flag at given time", target_time_valid)) { + tl->removeFlagAt(target_time); + tl->addFlag(target_time, Settings::application.widget.media_player_timeline_flag); + tl->refresh(); + oss << ": Timeline flag add"; + Action::manager().store(oss.str()); + } + + /// Add + ImGui::SameLine(0, IMGUI_SAME_LINE); + if (ImGuiToolkit::ButtonIcon(0, 0, "Add flag at cursor position", !mediaplayer_active_->isPlaying()) ) { + tl->removeFlagAt(mediaplayer_active_->position()); + tl->addFlag(mediaplayer_active_->position(), Settings::application.widget.media_player_timeline_flag); + tl->refresh(); + oss << ": Timeline flag add"; + Action::manager().store(oss.str()); + } + + /// CLEAR + ImGui::SameLine(0, IMGUI_SAME_LINE); + if (ImGuiToolkit::ButtonIcon(11, 14, "Clear all flags")) { + tl->clearFlags(); + oss << ": Timeline flag clear"; + Action::manager().store(oss.str()); + } + + ImGui::PopStyleColor(); // end icons ImGui::PopFont(); diff --git a/src/Timeline.cpp b/src/Timeline.cpp index 1c37599..62463b8 100644 --- a/src/Timeline.cpp +++ b/src/Timeline.cpp @@ -957,17 +957,20 @@ float *Timeline::flagsArray() return flagsArray_; } -bool Timeline::addFlag(GstClockTime t) +bool Timeline::addFlag(GstClockTime t, int type) { if (t > timing_.begin + (step_ * 2) && t < timing_.end - (step_ * 2) && !isFlagged(t)) { // compute nearest frame time - GstClockTime t_frame = ( (t - timing_.begin) / step_ ) * step_ + timing_.begin + step_; + GstClockTime t_frame = ( (t - timing_.begin) / step_ ) * step_ + timing_.begin; + if (t - t_frame > (step_ / 2)) + t_frame += step_; // Flag interval centered on t_frame TimeInterval f(t_frame - step_ - FLAG_MARGIN, t_frame + step_ + FLAG_MARGIN); + f.type = type; flags_array_need_update_ = true; return flags_.insert(f).second; @@ -976,9 +979,10 @@ bool Timeline::addFlag(GstClockTime t) return false; } -bool Timeline::addFlag(TimeInterval s) +bool Timeline::addFlag(TimeInterval s, int type) { if ( s.is_valid() ) { + s.type = type; flags_array_need_update_ = true; return flags_.insert(s).second; } @@ -1008,16 +1012,46 @@ bool Timeline::isFlagged(GstClockTime t) const return ( f != flags_.end() ); } - -GstClockTime Timeline::getFlagAt(GstClockTime t) const +int Timeline::flagTypeAt(GstClockTime t) const { TimeIntervalSet::const_iterator f = std::find_if(flags_.begin(), flags_.end(), includesTime(t)); + if ( f != flags_.end() ) + return (*f).type; + else + return -1; +} + +void Timeline::setFlagTypeAt(GstClockTime t, int type) +{ + TimeIntervalSet::iterator f = std::find_if(flags_.begin(), flags_.end(), includesTime(t)); if ( f != flags_.end() ) { - return ( (*f).begin + step_ + FLAG_MARGIN); + TimeInterval i = (*f); + flags_.erase(f); + i.type = type; + flags_.insert(i); + } +} + +TimeInterval Timeline::getNextFlag(GstClockTime t) const +{ + if ( !flags_.empty() ) { + // loop over flags + auto f = flags_.begin(); + for (; f != flags_.end(); ++f) { + // gap before target? + if ( f->begin > t ) + // done + break; + } + + if ( f != flags_.end() ) + return (*f); + else + return *(flags_.begin()); } - return GST_CLOCK_TIME_NONE; + return TimeInterval(); } void Timeline::clearFlags() diff --git a/src/Timeline.h b/src/Timeline.h index cc7d5fe..86a863c 100644 --- a/src/Timeline.h +++ b/src/Timeline.h @@ -14,6 +14,8 @@ struct TimeInterval { GstClockTime begin; GstClockTime end; + int type; + TimeInterval() { reset(); @@ -22,23 +24,30 @@ struct TimeInterval { begin = b.begin; end = b.end; + type = b.type; } TimeInterval(GstClockTime a, GstClockTime b) : TimeInterval() { if ( a != GST_CLOCK_TIME_NONE && b != GST_CLOCK_TIME_NONE) { begin = MIN(a, b); end = MAX(a, b); + type = 0; } } inline void reset() { begin = GST_CLOCK_TIME_NONE; end = GST_CLOCK_TIME_NONE; + type = 0; } inline GstClockTime duration() const { return is_valid() ? (end - begin) : GST_CLOCK_TIME_NONE; } + inline GstClockTime midpoint() const + { + return is_valid() ? (end - begin) / 2 + begin : GST_CLOCK_TIME_NONE; + } inline bool is_valid() const { return begin != GST_CLOCK_TIME_NONE && end != GST_CLOCK_TIME_NONE && begin < end; @@ -141,11 +150,13 @@ public: inline TimeIntervalSet flags() const { return flags_; }; inline size_t numFlags() const { return flags_.size(); }; float *flagsArray(); - bool addFlag(GstClockTime t); - bool addFlag(TimeInterval s); + bool addFlag(GstClockTime t, int type = 0); + bool addFlag(TimeInterval s, int type = 0); bool removeFlagAt(GstClockTime t); bool isFlagged(GstClockTime t) const; - GstClockTime getFlagAt(GstClockTime t) const; + int flagTypeAt(GstClockTime t) const; + void setFlagTypeAt(GstClockTime t, int type); + TimeInterval getNextFlag(GstClockTime t) const; void clearFlags(); // inverse of gaps: sections of play areas