diff --git a/ImGuiToolkit.cpp b/ImGuiToolkit.cpp index cee822a..7c499e1 100644 --- a/ImGuiToolkit.cpp +++ b/ImGuiToolkit.cpp @@ -145,7 +145,10 @@ bool ImGuiToolkit::ButtonIcon(int i, int j, const char *tooltip) ImVec2 uv1( uv0.x + 0.05, uv0.y + 0.05 ); ImGui::PushID( i*20 + j); - bool ret = ImGui::ImageButton((void*)(intptr_t)textureicons, ImVec2(ImGui::GetTextLineHeightWithSpacing(),ImGui::GetTextLineHeightWithSpacing()), uv0, uv1, 3); + ImGuiContext& g = *GImGui; + bool ret = ImGui::ImageButton((void*)(intptr_t)textureicons, + ImVec2(g.FontSize, g.FontSize), + uv0, uv1, g.Style.FramePadding.y); ImGui::PopID(); if (tooltip != nullptr && ImGui::IsItemHovered()) @@ -353,6 +356,23 @@ bool ImGuiToolkit::ComboIcon (std::vector > icons, std::vect return ret; } +bool ImGuiToolkit::MenuItemIcon (int i, int j, const char* label, bool selected, bool enabled) +{ + ImVec2 draw_pos = ImGui::GetCursorScreenPos(); + + // make some space + char text_buf[256]; + ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), " %s", label); + + // draw menu item + bool ret = ImGui::MenuItem(text_buf, NULL, selected, enabled); + + // draw icon + ImGui::SetCursorScreenPos(draw_pos); + Icon(i, j, enabled); + + return ret; +} void ImGuiToolkit::ShowIconsWindow(bool* p_open) { diff --git a/ImGuiToolkit.h b/ImGuiToolkit.h index 0757e7e..c0f691a 100644 --- a/ImGuiToolkit.h +++ b/ImGuiToolkit.h @@ -17,11 +17,12 @@ namespace ImGuiToolkit bool IconToggle (int i, int j, int i_toggle, int j_toggle, bool* toggle, const char *tooltips[] = nullptr); void ShowIconsWindow(bool* p_open); - // icon buttons + // buttons and gui items with icon bool ButtonIcon (int i, int j, const char* tooltip = nullptr); bool ButtonIconToggle (int i, int j, int i_toggle, int j_toggle, bool* toggle); bool ButtonIconMultistate (std::vector > icons, int* state); bool ComboIcon (std::vector > icons, std::vector labels, int* state); + bool MenuItemIcon (int i, int j, const char* label, bool selected = false, bool enabled = true); // buttons bool ButtonToggle (const char* label, bool* toggle); diff --git a/MediaPlayer.cpp b/MediaPlayer.cpp index 5abe87d..412dc54 100644 --- a/MediaPlayer.cpp +++ b/MediaPlayer.cpp @@ -676,7 +676,7 @@ bool MediaPlayer::go_to(GstClockTime pos) GstClockTime jumpPts = pos; - if (timeline_.gapAt(pos, gap)) { + if (timeline_.getGapAt(pos, gap)) { // if in a gap, find closest seek target if (gap.is_valid()) { // jump in one or the other direction @@ -922,7 +922,7 @@ void MediaPlayer::update() else { // manage timeline: test if position falls into a gap TimeInterval gap; - if (position_ != GST_CLOCK_TIME_NONE && timeline_.gapAt(position_, gap)) { + if (position_ != GST_CLOCK_TIME_NONE && timeline_.getGapAt(position_, gap)) { // if in a gap, seek to next section if (gap.is_valid()) { // jump in one or the other direction diff --git a/Timeline.cpp b/Timeline.cpp index ce4263b..b7c61ec 100644 --- a/Timeline.cpp +++ b/Timeline.cpp @@ -90,7 +90,7 @@ GstClockTime Timeline::next(GstClockTime time) const GstClockTime next_time = time; TimeInterval gap; - if (gapAt(time, gap) && gap.is_valid()) + if (getGapAt(time, gap) && gap.is_valid()) next_time = gap.end; return next_time; @@ -100,7 +100,7 @@ GstClockTime Timeline::previous(GstClockTime time) const { GstClockTime prev_time = time; TimeInterval gap; - if (gapAt(time, gap) && gap.is_valid()) + if (getGapAt(time, gap) && gap.is_valid()) prev_time = gap.begin; return prev_time; @@ -125,7 +125,13 @@ void Timeline::refresh() fillArrayFromGaps(gapsArray_, MAX_TIMELINE_ARRAY); } -bool Timeline::gapAt(const GstClockTime t, TimeInterval &gap) const +bool Timeline::gapAt(const GstClockTime t) const +{ + TimeIntervalSet::const_iterator g = std::find_if(gaps_.begin(), gaps_.end(), includesTime(t)); + return ( g != gaps_.end() ); +} + +bool Timeline::getGapAt(const GstClockTime t, TimeInterval &gap) const { TimeIntervalSet::const_iterator g = std::find_if(gaps_.begin(), gaps_.end(), includesTime(t)); @@ -142,34 +148,78 @@ bool Timeline::addGap(GstClockTime begin, GstClockTime end) return addGap( TimeInterval(begin, end) ); } -bool Timeline::cut(GstClockTime t) +bool Timeline::cut(GstClockTime t, bool left, bool join_extremity) { 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); + TimeIntervalSet::iterator gap = std::find_if(gaps_.begin(), gaps_.end(), includesTime(t)); + + // cut left part + if (left) { + // cut a gap + if ( gap != gaps_.end() ) + { + GstClockTime b = gap->begin; + gaps_.erase(gap); ret = addGap(b, t); } + // create a gap + else { + auto previous = gaps_.end(); + for (auto g = gaps_.begin(); g != gaps_.end(); previous = g++) { + if ( g->begin > t) + break; + } + if (join_extremity) { + //TODO + } + else { + if (previous == gaps_.end()) + ret = addGap( TimeInterval(timing_.begin, t) ); + else { + GstClockTime b = previous->begin; + gaps_.erase(previous); + ret = addGap( TimeInterval(b, t) ); + } + } + } + } + // cut right part + else { + // cut a gap + if ( gap != gaps_.end() ) + { + GstClockTime e = gap->end; + gaps_.erase(gap); + ret = addGap(t, e); + } + // create a gap + else { + auto suivant = gaps_.rend(); + for (auto g = gaps_.rbegin(); g != gaps_.rend(); suivant = g++) { + if ( g->end < t) + break; + } + if (join_extremity) { + if (suivant != gaps_.rend()) { + for (auto g = gaps_.find(*suivant); g != gaps_.end(); ) { + g = gaps_.erase(g); + } + } + ret = addGap( TimeInterval(t, timing_.end) ); + } + else { + if (suivant == gaps_.rend()) + ret = addGap( TimeInterval(t, timing_.end) ); + else { + GstClockTime e = suivant->end; + gaps_.erase( gaps_.find(*suivant)); + ret = addGap( TimeInterval(t, e) ); + } + } + } } } diff --git a/Timeline.h b/Timeline.h index 5fd61e0..d376ea6 100644 --- a/Timeline.h +++ b/Timeline.h @@ -118,9 +118,11 @@ public: void setGaps(const TimeIntervalSet &g); bool addGap(TimeInterval s); bool addGap(GstClockTime begin, GstClockTime end); - bool cut(GstClockTime t); + bool cut(GstClockTime t, bool left, bool join_extremity); bool removeGaptAt(GstClockTime t); - bool gapAt(const GstClockTime t, TimeInterval &gap) const; + + bool gapAt(const GstClockTime t) const; + bool getGapAt(const GstClockTime t, TimeInterval &gap) const; // Manipulation of Fading float fadingAt(const GstClockTime t) const; diff --git a/UserInterfaceManager.cpp b/UserInterfaceManager.cpp index 2f66945..b96790e 100644 --- a/UserInterfaceManager.cpp +++ b/UserInterfaceManager.cpp @@ -1986,6 +1986,7 @@ void ToolBox::Render() SourceController::SourceController() : _min_width(0.f), _h_space(0.f), _v_space(0.f), _buttons_height(0.f), _timeline_height(0.f), _scrollbar(0.f), _mediaplayer_height(0.f), _buttons_width(0.f), active_label_(LABEL_AUTO_MEDIA_PLAYER), active_selection_(-1), + _selection_context_menu(false), _selection_mediaplayer(nullptr), _selection_target_slower(0), _selection_target_faster(0), media_playing_mode_(false), slider_pressed_(false) { info_.setExtendedStringMode(); @@ -2275,18 +2276,24 @@ void SourceController::RenderSelection(size_t i) // get max duration and max frame width GstClockTime maxduration = 0; + std::list durations; float maxframewidth = 0.f; for (auto source = selection_.begin(); source != selection_.end(); ++source) { + // collect durations of all media sources MediaSource *ms = dynamic_cast(*source); - if (ms != nullptr) { - GstClockTime d = (static_cast(ms->mediaplayer()->timeline()->sectionsDuration()) / ms->mediaplayer()->playSpeed()); - if ( d > maxduration ) - maxduration = d; - } + if (ms != nullptr) + durations.push_back(static_cast(static_cast(ms->mediaplayer()->timeline()->sectionsDuration()) / ms->mediaplayer()->playSpeed())); + // compute the displayed width of frames of this source, and keep the max to align afterwards float w = 1.5f * _timeline_height * (*source)->frame()->aspectRatio(); if ( w > maxframewidth) maxframewidth = w; } + if (durations.size()>0) { + durations.sort(); + durations.unique(); + maxduration = durations.back(); + } + // 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); @@ -2294,6 +2301,7 @@ void SourceController::RenderSelection(size_t i) // draw list in a scroll area ImGui::BeginChild("##v_scroll2", rendersize, false); { + // draw play time scale if a duration is set if (maxduration > 0) { ImGui::SetCursorPos(ImGui::GetCursorPos() + ImVec2( maxframewidth + _h_space, 0)); @@ -2311,7 +2319,9 @@ void SourceController::RenderSelection(size_t i) UserInterface::manager().showSourceEditor(*source); // text below thumbnail to show status - ImGui::Text(" %s %s", SourcePlayIcon(*source), GstToolkit::time_to_string((*source)->playtime()).c_str() ); + ImGuiToolkit::PushFont(ImGuiToolkit::FONT_MONO); + ImGui::Text("%s %s", SourcePlayIcon(*source), GstToolkit::time_to_string((*source)->playtime()).c_str() ); + ImGui::PopFont(); // get media source MediaSource *ms = dynamic_cast(*source); @@ -2320,21 +2330,124 @@ void SourceController::RenderSelection(size_t i) MediaPlayer *mp = ms->mediaplayer(); // start to draw timeline aligned at maximum frame width + horizontal space - ImGui::SetCursorPos(image_top + ImVec2( maxframewidth + _h_space, 0)); + ImVec2 pos = image_top + ImVec2( maxframewidth + _h_space, 0); + ImGui::SetCursorPos(pos); // 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); - 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 icon buttons are small + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.f, 3.f)); + + // next buttons sub id + ImGui::PushID( static_cast(mp->id())); + + // display play speed + ImGui::SetCursorPos(pos + ImVec2( 0.f, framesize.y + _v_space)); + ImGui::Text(UNICODE_MULTIPLY " %.2f", mp->playSpeed()); + // if not 1x speed, offer to reset + if ( fabs( fabs(mp->playSpeed()) - 1.0 ) > EPSILON ) { + ImGui::SameLine(0,_h_space); + if (ImGuiToolkit::ButtonIcon(19, 15, "Reset speed")) + mp->setPlaySpeed( 1.0 ); + } + + // if more than one duration of media players, add buttons to adjust + if (durations.size() > 1) + { + + for (auto d = durations.crbegin(); d != durations.crend(); ++d) { + + ImGui::PushID( static_cast(*d)); + + // calculate position of icons + double x = static_cast(*d) * width_ratio; + ImGui::SetCursorPos(pos + ImVec2( static_cast(x) - 2.f, framesize.y + _v_space) ); + // depending on position relative to media play duration, offer corresponding action + double secdur = static_cast(mp->timeline()->sectionsDuration()); + guint64 playdur = static_cast( secdur / fabs(mp->playSpeed()) ); + // last icon in the timeline + if ( playdur == (*d) ) { + // not the minimum duration : + if (playdur > durations.front() ) { + // offer to speed up or slow down [<>] + if (playdur < durations.back() ) { + if ( ImGuiToolkit::ButtonIcon(0, 12, "Adjust duration") ) { + auto prev = d; + prev--; + _selection_target_slower = SIGN(mp->playSpeed()) * secdur / static_cast(*prev); + auto next = d; + next++; + _selection_target_faster = SIGN(mp->playSpeed()) * secdur / static_cast(*next); + _selection_mediaplayer = mp; + _selection_context_menu = true; + } + } + // offer to speed up [< ] + else if ( ImGuiToolkit::ButtonIcon(8, 12, "Adjust duration") ) { + auto next = d; + next++; + _selection_target_faster = SIGN(mp->playSpeed()) * secdur / static_cast(*next); + _selection_target_slower = 0.0; + _selection_mediaplayer = mp; + _selection_context_menu = true; + } + } + // minimum duration : offer to slow down [ >] + else if ( ImGuiToolkit::ButtonIcon(9, 12, "Adjust duration") ) { + _selection_target_faster = 0.0; + auto prev = d; + prev--; + _selection_target_slower = SIGN(mp->playSpeed()) * secdur / static_cast(*prev); + _selection_mediaplayer = mp; + _selection_context_menu = true; + } + } + // middle buttons : offer to cut at this position + else if ( playdur > (*d) ) { + char text_buf[256]; + GstClockTime cutposition = (*d) * fabs(mp->playSpeed()); + ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), "Cut at %s", + GstToolkit::time_to_string(cutposition, GstToolkit::TIME_STRING_MINIMAL).c_str()); + + if ( ImGuiToolkit::ButtonIcon(9, 3, text_buf) ) { + if ( mp->timeline()->cut(cutposition, false, true) ) { + std::ostringstream info; + info << SystemToolkit::base_filename( mp->filename() ) << ": Timeline " < 0) { + + // calculate position of icon + double x = static_cast(durations.front()) * width_ratio; + ImGui::SetCursorPos(pos + ImVec2( static_cast(x) - 2.f, framesize.y + _v_space) ); + + // offer only to adjust size by removing ending gap + if ( mp->timeline()->gapAt( mp->timeline()->end() ) ) { + if ( ImGuiToolkit::ButtonIcon(7, 0, "Remove end gap" )){ + if ( mp->timeline()->removeGaptAt(mp->timeline()->end()) ) { + std::ostringstream info; + info << SystemToolkit::base_filename( mp->filename() ) << ": Timeline Remove end gap"; + Action::manager().store(info.str()); + } + } + } + } + + ImGui::PopStyleVar(); + ImGui::PopID(); } // next line position - ImGui::SetCursorPos(image_top + ImVec2(0, 2.0f * _timeline_height + _v_space)); -// ImGui::Spacing(); + ImGui::SetCursorPos(image_top + ImVec2(0, 2.0f * _timeline_height + 2.f * _v_space)); } } @@ -2342,6 +2455,11 @@ void SourceController::RenderSelection(size_t i) } + /// + /// context menu from actions above + /// + RenderSelectionContextMenu(); + /// /// Play button bar /// @@ -2391,6 +2509,45 @@ void SourceController::RenderSelection(size_t i) ImGui::PopStyleColor(4); } +void SourceController::RenderSelectionContextMenu() +{ + if (_selection_mediaplayer == nullptr) + return; + + if (_selection_context_menu) { + ImGui::OpenPopup("_speedchange_context_menu"); + _selection_context_menu = false; + } + if (ImGui::BeginPopup("_speedchange_context_menu")) + { + std::ostringstream info; + info << SystemToolkit::base_filename( _selection_mediaplayer->filename() ); + + if ( ImGuiToolkit::MenuItemIcon(14, 16, ICON_FA_CARET_LEFT " Accelerate", false, fabs(_selection_target_faster) > 0 )){ + _selection_mediaplayer->setPlaySpeed( _selection_target_faster ); + info << ": Speed x" << std::setprecision(3) << _selection_target_faster; + Action::manager().store(info.str()); + } + if ( ImGuiToolkit::MenuItemIcon(15, 16, ICON_FA_CARET_RIGHT " Slow down", false, fabs(_selection_target_slower) > 0 )){ + _selection_mediaplayer->setPlaySpeed( _selection_target_slower ); + info << ": Speed x" << std::setprecision(3) << _selection_target_slower; + Action::manager().store(info.str()); + } + if ( _selection_mediaplayer->timeline()->gapAt( _selection_mediaplayer->timeline()->end()) ) { + + if ( ImGuiToolkit::MenuItemIcon(7, 0, "Remove end gap" )){ + info << ": Remove end gap "; + if ( _selection_mediaplayer->timeline()->removeGaptAt(_selection_mediaplayer->timeline()->end()) ) + Action::manager().store(info.str()); + } + + } + ImGui::EndPopup(); + } + +} + + bool SourceController::SourceButton(Source *s, ImVec2 framesize) { bool ret = false; @@ -2722,13 +2879,22 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) if ( ImGuiToolkit::ButtonIconMultistate(iconsloop, ¤t_loop) ) mp->setLoop( (MediaPlayer::LoopMode) current_loop ); + + std::ostringstream oss; + oss << SystemToolkit::base_filename( mp->filename() ); + // speed slider (if enough space) float speed = static_cast(mp->playSpeed()); if ( rendersize.x > _min_width * 1.4f ) { ImGui::SameLine(0, MAX(_h_space * 2.f, rendersize.x - _min_width * 1.6f) ); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - _buttons_height ); - if (ImGui::DragFloat( "##Speed", &speed, 0.01f, -10.f, 10.f, "Speed x %.1f", 2.f)) + if (ImGui::DragFloat( "##Speed", &speed, 0.01f, -10.f, 10.f, "Speed " UNICODE_MULTIPLY " %.1f", 2.f)) mp->setPlaySpeed( static_cast(speed) ); + + if (ImGui::IsItemDeactivatedAfterEdit()){ + oss << ": Speed x" << std::setprecision(3) << speed; + Action::manager().store(oss.str()); + } } /// @@ -2742,15 +2908,18 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) ImGui::OpenPopup( "MenuTimeline" ); if (ImGui::BeginPopup( "MenuTimeline" )) { - if (ImGui::MenuItem(UNICODE_MULTIPLY " " ICON_FA_CARET_RIGHT " Reset speed" )){ + if (ImGuiToolkit::MenuItemIcon(19,15,"Reset speed" )){ speed = 1.f; mp->setPlaySpeed( static_cast(speed) ); + oss << ": Speed x1"; + Action::manager().store(oss.str()); } if (ImGui::MenuItem(ICON_FA_WINDOW_CLOSE " Reset timeline" )){ timeline_zoom = 1.f; mp->timeline()->clearFading(); mp->timeline()->clearGaps(); - Action::manager().store("Timeline Reset"); + oss << ": Reset timeline"; + Action::manager().store(oss.str()); } if (ImGui::BeginMenu(ICON_FA_RANDOM " Auto fading")) { @@ -2759,15 +2928,19 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) if (ImGui::MenuItem(names[i])) { mp->timeline()->autoFading( 250 * (int ) pow(2, i) ); mp->timeline()->smoothFading( 2 * (i + 1) ); - Action::manager().store("Timeline Auto fading"); + oss << ": Timeline Auto fading " << 250 * (int ) pow(2, i); + Action::manager().store(oss.str()); } } ImGui::EndMenu(); } if (ImGui::BeginMenu(ICON_FA_CUT " Auto cut" )){ - if (ImGui::MenuItem("Cut faded areas")) - if (mp->timeline()->autoCut()) - Action::manager().store("Timeline Auto cut"); + + if (ImGuiToolkit::MenuItemIcon(14, 12, "Cut faded areas" )) + if (mp->timeline()->autoCut()){ + oss << ": Cut faded areas"; + Action::manager().store(oss.str()); + } ImGui::EndMenu(); } @@ -2820,7 +2993,11 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) else if (released) { tl->refresh(); - Action::manager().store("Timeline change"); + if (Settings::application.widget.timeline_editmode) + oss << ": Timeline cut"; + else + oss << ": Timeline opacity"; + Action::manager().store(oss.str()); } // custom timeline slider @@ -2840,10 +3017,27 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) ImGui::SetCursorScreenPos(bottom + ImVec2(1.f, 0.5f * _timeline_height)); if (Settings::application.widget.timeline_editmode) { - // action auto cut - if (ImGuiToolkit::IconButton(9, 3, "Cut at cursor")) { - if (mp->timeline()->cut(mp->position())) - Action::manager().store("Timeline Cut"); + // action cut + if (mp->isPlaying()) { + ImGuiToolkit::HelpIcon("Pause video to enable cut options", 9, 3); + } + else if (ImGuiToolkit::IconButton(9, 3, "Cut at cursor")) { + ImGui::OpenPopup("timeline_cut_context_menu"); + } + if (ImGui::BeginPopup("timeline_cut_context_menu")){ + if (ImGuiToolkit::MenuItemIcon(1,0,"Cut left")){ + if (mp->timeline()->cut(mp->position(), true, false)) { + oss << ": Timeline cut"; + Action::manager().store(oss.str()); + } + } + if (ImGuiToolkit::MenuItemIcon(2,0,"Cut right")){ + if (mp->timeline()->cut(mp->position(), false, false)){ + oss << ": Timeline cut"; + Action::manager().store(oss.str()); + } + } + ImGui::EndPopup(); } } else { @@ -2858,7 +3052,8 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp) ImGui::PopButtonRepeat(); if (_actionsmooth > 0 && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - Action::manager().store("Timeline Smooth"); + oss << ": Timeline opacity smooth"; + Action::manager().store(oss.str()); _actionsmooth = 0; } } diff --git a/UserInterfaceManager.h b/UserInterfaceManager.h index 0599e02..3c4a8e3 100644 --- a/UserInterfaceManager.h +++ b/UserInterfaceManager.h @@ -120,14 +120,24 @@ class SourceController InfoVisitor info_; SourceList selection_; + bool _selection_context_menu; + MediaPlayer *_selection_mediaplayer; + double _selection_target_slower; + double _selection_target_faster; + void RenderSelectionContextMenu(); + + // re-usable ui parts void DrawButtonBar(ImVec2 bottom, float width); const char *SourcePlayIcon(Source *s); - bool SourceButton(Source *s, ImVec2 framesize); - void RenderSelectedSources(); - void RenderSelection(size_t i); - void RenderSingleSource(Source *s); + // Render the sources dynamically selected + void RenderSelectedSources(); + // Render a stored selection + void RenderSelection(size_t i); + // Render a single source + void RenderSingleSource(Source *s); + // Render a single media player bool media_playing_mode_; bool slider_pressed_; void RenderMediaPlayer(MediaPlayer *mp); diff --git a/rsc/images/icons.dds b/rsc/images/icons.dds index 4e51eeb..0a46542 100644 Binary files a/rsc/images/icons.dds and b/rsc/images/icons.dds differ