Skip to content

Commit 25744ed

Browse files
authored
Add ability to duplicate lyric phrases/words #3923 (#6947)
1 parent 1ac0cef commit 25744ed

4 files changed

Lines changed: 114 additions & 1 deletion

File tree

README.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Issue Tracker is found here: www.github.com/xLightsSequencer/xLights/issues
1111
XLIGHTS/NUTCRACKER RELEASE NOTES
1212
---------------------------------
1313
2026.16 August ??, 2026
14+
-enh (derwin12) Added ability to duplicate lyrics phrases/words (#3923)
1415
-bug (scott) Fix multi-row lasso-selected effects pasting 1-3 rows below the
1516
target row instead of the row you clicked (#6944)
1617
-enh (scott) Layout: right-click a model and choose "Wire to Closest Controller with

src-ui-wx/sequencer/EffectsGrid.cpp

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#include "wx/glcanvas.h"
1212
#include "wx/wx.h"
13+
#include <wx/choicdlg.h>
1314
#include <wx/clipbrd.h>
1415
#include <wx/numdlg.h>
1516
#include <wx/progdlg.h>
@@ -64,6 +65,7 @@
6465

6566
#include <cmath>
6667
#include <limits>
68+
#include <map>
6769

6870
#include <log.h>
6971

@@ -125,6 +127,7 @@ const long EffectsGrid::ID_GRID_MNU_AUTOLABEL = wxNewId();
125127
const long EffectsGrid::ID_GRID_MNU_HALVETIMINGS = wxNewId();
126128
const long EffectsGrid::ID_GRID_MNU_BREAKDOWN_WORD = wxNewId();
127129
const long EffectsGrid::ID_GRID_MNU_BREAKDOWN_WORDS = wxNewId();
130+
const long EffectsGrid::ID_GRID_MNU_DUPLICATE_TO_TIMING_TRACK = wxNewId();
128131
const long EffectsGrid::ID_GRID_MNU_ALIGN_START_TIMES = wxNewId();
129132
const long EffectsGrid::ID_GRID_MNU_ALIGN_END_TIMES = wxNewId();
130133
const long EffectsGrid::ID_GRID_MNU_ALIGN_BOTH_TIMES = wxNewId();
@@ -672,6 +675,11 @@ void EffectsGrid::rightClick(wxMouseEvent& event) {
672675
mnuLayer.Append(ID_GRID_MNU_REMOVE_SHIMMER, "Remove \"-shimmer\"");
673676
}
674677
mSelectedEffect = selectedEffect;
678+
679+
if (mSequenceElements->GetNumberOfTimingElements() > 1) {
680+
wxMenuItem* menu_duplicate_to_track = mnuLayer.Append(ID_GRID_MNU_DUPLICATE_TO_TIMING_TRACK, "Duplicate Selection to Timing Track...");
681+
menu_duplicate_to_track->Enable(selectedEffect->GetParentEffectLayer()->GetSelectedEffectCount() >= 1);
682+
}
675683
}
676684
mnuLayer.AppendSeparator();
677685
wxMenuItem* menu_effect_lock = mnuLayer.Append(ID_GRID_MNU_LOCK, "Lock");
@@ -1499,6 +1507,9 @@ void EffectsGrid::OnGridPopup(wxCommandEvent& event) {
14991507
wxCommandEvent eventRowHeaderChanged(EVT_ROW_HEADINGS_CHANGED);
15001508
wxPostEvent(mParent, eventRowHeaderChanged);
15011509
}
1510+
} else if (id == ID_GRID_MNU_DUPLICATE_TO_TIMING_TRACK) {
1511+
spdlog::debug("OnGridPopup - ID_GRID_MNU_DUPLICATE_TO_TIMING_TRACK");
1512+
DuplicateSelectedTimingToTrack();
15021513
} else if (id == ID_GRID_MNU_FIND) {
15031514
Find();
15041515
} else if (id == ID_GRID_MNU_FIND_NEXT) {
@@ -9338,6 +9349,103 @@ void EffectsGrid::DuplicateSelectedEffects() {
93389349
}
93399350
}
93409351

9352+
void EffectsGrid::DuplicateSelectedTimingToTrack() {
9353+
if (mSelectedEffect == nullptr) {
9354+
return;
9355+
}
9356+
9357+
TimingElement* sourceElement = dynamic_cast<TimingElement*>(mSelectedEffect->GetParentEffectLayer()->GetParentElement());
9358+
if (sourceElement == nullptr) {
9359+
return;
9360+
}
9361+
9362+
// Gather the highlighted marks from every layer (Phrase/Word/Phoneme) of the
9363+
// source track, not just the row that was right-clicked - a selection can span
9364+
// multiple layers (e.g. a phrase and its words highlighted together).
9365+
std::map<int, std::vector<Effect*>> selectedEffectsByLayer;
9366+
for (size_t li = 0; li < sourceElement->GetEffectLayerCount(); ++li) {
9367+
EffectLayer* layer = sourceElement->GetEffectLayer((int)li);
9368+
for (int i = 0; i < layer->GetEffectCount(); ++i) {
9369+
Effect* e = layer->GetEffect(i);
9370+
if (e->GetSelected() != EFFECT_NOT_SELECTED) {
9371+
selectedEffectsByLayer[(int)li].push_back(e);
9372+
}
9373+
}
9374+
}
9375+
if (selectedEffectsByLayer.empty()) {
9376+
int layerIndex = -1;
9377+
EffectLayer* sourceLayer = mSelectedEffect->GetParentEffectLayer();
9378+
for (size_t i = 0; i < sourceElement->GetEffectLayerCount(); ++i) {
9379+
if (sourceElement->GetEffectLayer((int)i) == sourceLayer) {
9380+
layerIndex = (int)i;
9381+
break;
9382+
}
9383+
}
9384+
if (layerIndex < 0) {
9385+
return;
9386+
}
9387+
selectedEffectsByLayer[layerIndex].push_back(mSelectedEffect);
9388+
}
9389+
9390+
wxArrayString choices;
9391+
std::vector<TimingElement*> candidates;
9392+
int numTiming = mSequenceElements->GetNumberOfTimingElements();
9393+
for (int i = 0; i < numTiming; ++i) {
9394+
TimingElement* te = mSequenceElements->GetTimingElement(i);
9395+
if (te == nullptr || te == sourceElement) {
9396+
continue;
9397+
}
9398+
choices.Add(te->GetName());
9399+
candidates.push_back(te);
9400+
}
9401+
9402+
if (choices.IsEmpty()) {
9403+
wxMessageBox("No other timing tracks found in this sequence.", "Duplicate Selection to Timing Track",
9404+
wxOK | wxICON_INFORMATION, (wxWindow*)mParent);
9405+
return;
9406+
}
9407+
9408+
wxSingleChoiceDialog dlg((wxWindow*)mParent, "Duplicate the selected timing marks to which timing track?",
9409+
"Duplicate Selection to Timing Track", choices);
9410+
if (dlg.ShowModal() != wxID_OK) {
9411+
return;
9412+
}
9413+
9414+
TimingElement* destElement = candidates[dlg.GetSelection()];
9415+
9416+
mSequenceElements->get_undo_mgr().CreateUndoStep();
9417+
int skipped = 0;
9418+
for (auto& [layerIndex, selectedEffects] : selectedEffectsByLayer) {
9419+
while ((int)destElement->GetEffectLayerCount() <= layerIndex) {
9420+
destElement->AddEffectLayer();
9421+
}
9422+
EffectLayer* destLayer = destElement->GetEffectLayer(layerIndex);
9423+
destLayer->UnSelectAllEffects();
9424+
for (Effect* e : selectedEffects) {
9425+
long start = e->GetStartTimeMS();
9426+
long end = e->GetEndTimeMS();
9427+
if (destLayer->HasEffectsInTimeRange(start, end)) {
9428+
skipped++;
9429+
continue;
9430+
}
9431+
Effect* newef = destLayer->AddEffect(0, e->GetEffectName(), e->GetSettingsAsString(), e->GetPaletteAsString(), start, end, EFFECT_SELECTED, false);
9432+
if (newef != nullptr) {
9433+
mSequenceElements->get_undo_mgr().CaptureAddedEffect(destElement->GetName(), destLayer->GetIndex(), newef->GetID());
9434+
}
9435+
}
9436+
}
9437+
destElement->SetCollapsed(false);
9438+
9439+
if (skipped > 0) {
9440+
wxMessageBox(wxString::Format("%d timing mark(s) were skipped because they overlap existing marks on the destination track.", skipped),
9441+
"Duplicate Selection to Timing Track", wxOK | wxICON_WARNING, (wxWindow*)mParent);
9442+
}
9443+
9444+
mSequenceElements->PopulateRowInformation();
9445+
wxCommandEvent eventRowHeaderChanged(EVT_ROW_HEADINGS_CHANGED);
9446+
wxPostEvent(mParent, eventRowHeaderChanged);
9447+
}
9448+
93419449
void EffectsGrid::DrawSongStructureOverlays(xlGraphicsContext* ctx) {
93429450
if (mSequenceElements == nullptr || mTimeline == nullptr) return;
93439451
const SongStructureManager& ssm = mSequenceElements->GetSongStructureManager();

src-ui-wx/sequencer/EffectsGrid.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ class EffectsGrid : public GRAPHICS_BASE_CLASS
147147
void RemapSelectedDMXEffectValues(const std::vector<std::tuple<int, int, float, int, std::string>>& dmxmappings);
148148
void ConvertSelectedEffectsTo(const std::string& effectName);
149149
void DuplicateSelectedEffects();
150+
void DuplicateSelectedTimingToTrack();
150151
void CreateTimingFromSelectedEffects();
151152
bool IsTopModelVisible();
152153
bool IsMouseOverTiming(int y);
@@ -458,6 +459,7 @@ class EffectsGrid : public GRAPHICS_BASE_CLASS
458459
static const long ID_GRID_MNU_HALVETIMINGS;
459460
static const long ID_GRID_MNU_BREAKDOWN_WORD;
460461
static const long ID_GRID_MNU_BREAKDOWN_WORDS;
462+
static const long ID_GRID_MNU_DUPLICATE_TO_TIMING_TRACK;
461463
static const long ID_GRID_MNU_ALIGN_START_TIMES;
462464
static const long ID_GRID_MNU_ALIGN_END_TIMES;
463465
static const long ID_GRID_MNU_ALIGN_BOTH_TIMES;

src-ui-wx/xLightsMain.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8061,10 +8061,12 @@ void xLightsFrame::SetXFadePort(int i)
80618061

80628062
void xLightsFrame::LoadPhonemeDictionaries()
80638063
{
8064+
wxString exeDir = wxFileName::FileName(wxStandardPaths::Get().GetExecutablePath()).GetPath();
80648065
std::vector<std::string> searchDirs = {
80658066
CurrentDir.ToStdString(),
80668067
(wxStandardPaths::Get().GetResourcesDir() + "/dictionaries").ToStdString(),
8067-
wxFileName::FileName(wxStandardPaths::Get().GetExecutablePath()).GetPath().ToStdString()
8068+
(exeDir + "/dictionaries").ToStdString(),
8069+
exeDir.ToStdString()
80688070
};
80698071

80708072
wxProgressDialog dlg("Loading", "Loading phoneme dictionaries", 100, this, wxPD_APP_MODAL | wxPD_AUTO_HIDE);

0 commit comments

Comments
 (0)