From 1c4531f1f693b2244ae3dec1d54bf4cd6c06ebc5 Mon Sep 17 00:00:00 2001 From: immortalx74 Date: Thu, 19 May 2022 05:16:57 +0300 Subject: [PATCH 01/12] Test WIP branch --- Editor.lua | 1 + UndoRedo.lua | 8 ++++++++ Util.lua | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/Editor.lua b/Editor.lua index 08119a9..e9f0538 100644 --- a/Editor.lua +++ b/Editor.lua @@ -91,6 +91,7 @@ function Editor.InsertNote(cx, cy) App.current_pitch = App.last_note_clicked.pitch Editor.PlayNote() -- push undo here + UR.PushUndo(e_OpType.Insert, {new_note}) end function Editor.EraseNotes(cx, cy) diff --git a/UndoRedo.lua b/UndoRedo.lua index e3f3a1d..e842ecc 100644 --- a/UndoRedo.lua +++ b/UndoRedo.lua @@ -9,6 +9,11 @@ UR = function UR.PushUndo(type, note_list) local new_rec = {type = type, note_list = note_list} UR.undo_stack[#UR.undo_stack + 1] = new_rec + + -- clear redo stack + if #UR.redo_stack > 0 then + Util.ClearTable(UR.redo_stack) + end end function UR.PopUndo() @@ -27,6 +32,9 @@ function UR.PopUndo() -- push to the redo stack, pop from the undo UR.redo_stack[#UR.redo_stack + 1] = last_rec UR.undo_stack[#UR.undo_stack] = nil + + Util.ClearTable(App.note_list_selected) + App.last_note_clicked = nil end function UR.PushRedo() diff --git a/Util.lua b/Util.lua index 11c53b7..a825a2a 100644 --- a/Util.lua +++ b/Util.lua @@ -135,6 +135,10 @@ function Util.CreateMIDI() end function Util.CopyNote(note) + if note == nil then + msg("note is nil") + return + end local t = {idx = note.idx, offset = note.offset, string_idx = note.string_idx, pitch = note.pitch, velocity = note.velocity, off_velocity = note.off_velocity, duration = note.duration} return t end From e492d753c5f4f1afc626fdb9cbe35a5be2ce6c77 Mon Sep 17 00:00:00 2001 From: immortalx74 Date: Fri, 20 May 2022 21:21:06 +0300 Subject: [PATCH 02/12] Undo/Redo implementation for note properties --- App.lua | 20 +++++++-- Constants.lua | 3 ++ Editor.lua | 100 ++++++++++++++++++++++++++--------------- UI.lua | 12 ++--- UndoRedo.lua | 121 +++++++++++++++++++++++++++++++++++++++++++++++--- Util.lua | 9 ++++ 6 files changed, 214 insertions(+), 51 deletions(-) diff --git a/App.lua b/App.lua index 78b8bb9..5fc8c72 100644 --- a/App.lua +++ b/App.lua @@ -16,7 +16,8 @@ App = editor_win_y = 0, can_init_drag = false, current_pitch = 0, - settings_open = true, + is_new_note = false, + last_click_was_inside_editor = false, -- metrics window_w = 800, @@ -40,7 +41,7 @@ App = swap_pitchfret_order = false, default_velocity = 80, default_off_velocity = 65, - + -- defaults wheel_delta = 50, active_tool = e_Tool.Select, @@ -68,7 +69,7 @@ App = {caption = "4/4 Tri", beats = 4, subs = 3}, {caption = "8/4 Tri", beats = 8, subs = 3} }, - + instrument = { {num_strings = 4, open = {28, 33, 38, 43}, recent = {28, 33, 38, 43}, "E1", "A1", "D2", "G2"}, @@ -110,6 +111,19 @@ function App.Loop() App.mouse_x, App.mouse_y = reaper.ImGui_GetMousePos(App.ctx) App.window_w = reaper.ImGui_GetWindowWidth(App.ctx) + -- if reaper.ImGui_GetKeyMods(App.ctx) == reaper.ImGui_KeyModFlags_Ctrl() and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then + -- UR.PopUndo() + -- end + -- if reaper.ImGui_GetKeyMods(App.ctx) == reaper.ImGui_KeyModFlags_Ctrl() + reaper.ImGui_KeyModFlags_Shift() and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then + -- UR.PopRedo() + -- end + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and not reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModShift()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then + msg("undo") + end + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModShift()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then + msg("redo") + end + UI.Render_CB_Strings() UI.Render_CB_Signature() UI.Render_CB_Quantize() diff --git a/Constants.lua b/Constants.lua index 4c2a048..99ef39d 100644 --- a/Constants.lua +++ b/Constants.lua @@ -39,6 +39,9 @@ e_OpType = { Insert = 1, Delete = 2, + ModifyPitchAndDuration = 3, + ModifyVelocityAndOffVelocity = 4, + Move = 5 -- more here... } diff --git a/Editor.lua b/Editor.lua index e9f0538..9c3ec8a 100644 --- a/Editor.lua +++ b/Editor.lua @@ -1,36 +1,8 @@ Editor = {} -function Editor.PlayNote() - if App.last_note_clicked == nil or App.audition_notes == false then return; end - local idx = App.last_note_clicked.idx - reaper.StuffMIDIMessage(0, 0x90, App.current_pitch, App.note_list[idx].velocity) -end - -function Editor.StopNote() - if App.last_note_clicked == nil or App.audition_notes == false then return; end - local idx = App.last_note_clicked.idx - reaper.StuffMIDIMessage(0, 0x80, App.current_pitch, App.note_list[idx].velocity) -end - -function Editor.SelectNotes(cx, cy) - for i, note in ipairs(App.note_list) do - if (cx >= note.offset) and (cx < note.offset + note.duration) and (cy == note.string_idx) then - if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) then - App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) - else - if not Util.IsNoteSelected(note) then - Util.ClearTable(App.note_list_selected) - App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) - end - end - App.last_note_clicked = Util.CopyNote(note) - App.current_pitch = App.last_note_clicked.pitch - end - end -end - function Editor.OnMouseButtonClick(mbutton, cx, cy) App.can_init_drag = true + App.last_click_was_inside_editor = true if mbutton == e_MouseButton.Left or mbutton == e_MouseButton.Right then if Util.IsCellEmpty(cx, cy, true) then @@ -52,12 +24,27 @@ function Editor.OnMouseButtonClick(mbutton, cx, cy) end function Editor.OnMouseButtonRelease(mbutton) - App.can_init_drag = false - - if mbutton == e_MouseButton.Left or mbutton == e_MouseButton.Right then - Editor.StopNote() - App.last_note_clicked = nil - Util.UpdateSelectedNotes() + if App.last_click_was_inside_editor then + App.can_init_drag = false + App.last_click_was_inside_editor = false + + if mbutton == e_MouseButton.Left or mbutton == e_MouseButton.Right then + Editor.StopNote() + App.last_note_clicked = nil + + if #App.note_list_selected > 0 and not (App.is_new_note) then + if UR.last_op == e_OpType.ModifyPitchAndDuration then + UR.PushUndo(e_OpType.ModifyPitchAndDuration, App.note_list_selected) + elseif UR.last_op == e_OpType.ModifyVelocityAndOffVelocity then + UR.PushUndo(e_OpType.ModifyVelocityAndOffVelocity, App.note_list_selected) + elseif UR.last_op == e_OpType.Move then + UR.PushUndo(e_OpType.Move, App.note_list_selected) + end + end + + App.is_new_note = false + Util.UpdateSelectedNotes() + end end end @@ -82,6 +69,23 @@ function Editor.OnMouseButtonDrag(mbutton) end end +function Editor.SelectNotes(cx, cy) + for i, note in ipairs(App.note_list) do + if (cx >= note.offset) and (cx < note.offset + note.duration) and (cy == note.string_idx) then + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) then + App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) + else + if not Util.IsNoteSelected(note) then + Util.ClearTable(App.note_list_selected) + App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) + end + end + App.last_note_clicked = Util.CopyNote(note) + App.current_pitch = App.last_note_clicked.pitch + end + end +end + function Editor.InsertNote(cx, cy) local recent_pitch = App.instrument[App.num_strings - 3].recent[App.num_strings - cy] local new_note = {idx = #App.note_list + 1, offset = cx, string_idx = cy, pitch = recent_pitch, velocity = App.default_velocity, off_velocity = App.default_off_velocity, duration = 1} @@ -92,20 +96,29 @@ function Editor.InsertNote(cx, cy) Editor.PlayNote() -- push undo here UR.PushUndo(e_OpType.Insert, {new_note}) + App.is_new_note = true + UR.last_op = e_OpType.Insert end function Editor.EraseNotes(cx, cy) if Util.IsNoteAtCellSelected(cx, cy) then table.sort(App.note_list_selected, function (k1, k2) return k1.idx < k2.idx; end) local len = #App.note_list_selected + + -- push undo here (multiple) + UR.PushUndo(e_OpType.Delete, App.note_list_selected) + for i = len, 1, -1 do table.remove(App.note_list, App.note_list_selected[i].idx) end else local idx = Util.GetNoteIndexAtCell(cx, cy) + -- push undo here (single) + UR.PushUndo(e_OpType.Delete, {App.note_list[idx]}) table.remove(App.note_list, idx) end + UR.last_op = e_OpType.Delete Util.RecalculateStoredNoteIndices() Util.ClearTable(App.note_list_selected) end @@ -120,6 +133,8 @@ function Editor.MoveNotes(cx, cy, dx, dy) App.note_list[v.idx].string_idx = dst_string_idx end end + + UR.last_op = e_OpType.Move end function Editor.ModifyPitchAndDuration(cx, cy, dx, dy) @@ -136,6 +151,8 @@ function Editor.ModifyPitchAndDuration(cx, cy, dx, dy) Util.UpdateRecentPitch(App.num_strings - v.string_idx, App.note_list[v.idx].pitch) end + UR.last_op = e_OpType.ModifyPitchAndDuration + local idx = App.last_note_clicked.idx if App.current_pitch ~= App.note_list[idx].pitch then Editor.StopNote() @@ -152,5 +169,18 @@ function Editor.ModifyVelocityAndOffVelocity(cx, cy, dx, dy) App.note_list[v.idx].velocity = Util.Clamp(v.velocity + dy, 0, 127) end end + + UR.last_op = e_OpType.ModifyVelocityAndOffVelocity end +function Editor.PlayNote() + if App.last_note_clicked == nil or App.audition_notes == false then return; end + local idx = App.last_note_clicked.idx + reaper.StuffMIDIMessage(0, 0x90, App.current_pitch, App.note_list[idx].velocity) +end + +function Editor.StopNote() + if App.last_note_clicked == nil or App.audition_notes == false then return; end + local idx = App.last_note_clicked.idx + reaper.StuffMIDIMessage(0, 0x80, App.current_pitch, App.note_list[idx].velocity) +end diff --git a/UI.lua b/UI.lua index c9e58b6..09cece6 100644 --- a/UI.lua +++ b/UI.lua @@ -76,7 +76,7 @@ function UI.Render_SI_Measures() Util.HorSpacer(3) reaper.ImGui_SetNextItemWidth(App.ctx, App.si_measures_w) local ret, val = reaper.ImGui_SliderInt(App.ctx, "Measures##si_measures", App.num_measures, 1, 64) - App.num_measures = val + App.num_measures = Util.Clamp(val, 1, 64) end function UI.Render_CB_NoteDisplay() @@ -109,7 +109,7 @@ function UI.Render_BTN_Settings() local _, set_default_velocity = reaper.ImGui_SliderInt(App.ctx, "Default note velocity", App.default_velocity, 0, 127) App.default_velocity = set_default_velocity - + local _, set_default_off_velocity = reaper.ImGui_SliderInt(App.ctx, "Default note off-velocity", App.default_off_velocity, 0, 127) App.default_off_velocity = set_default_off_velocity reaper.ImGui_EndPopup(App.ctx) @@ -229,6 +229,9 @@ function UI.Render_Editor() local rect_y1 = App.editor_win_y + App.top_margin - 5 local rect_x2 = lane_end_x - 1 local rect_y2 = rect_y1 + 11 + (App.num_strings - 1) * App.lane_v_spacing + + -- debug draw editor mouse area + -- reaper.ImGui_DrawList_AddRect(draw_list, rect_x1, rect_y1, rect_x2, rect_y2, Colors.red) if reaper.ImGui_IsWindowHovered(App.ctx) then local cell_x = Util.GetCellX() @@ -265,10 +268,7 @@ function UI.Render_Editor() if reaper.ImGui_IsMouseDragging(App.ctx, 1) then Editor.OnMouseButtonDrag(e_MouseButton.Right) end - - -- debug draw editor mouse area - -- reaper.ImGui_DrawList_AddRect(draw_list, lane_start_x, App.editor_win_y + App.top_margin - 5, lane_end_x + 1, App.editor_win_y+App.top_margin - 5 + 11 + ((App.num_strings - 1) * App.lane_v_spacing), Colors.red) - + -- Mask rect reaper.ImGui_DrawList_AddRectFilled(draw_list, App.editor_win_x, App.editor_win_y + 2, App.editor_win_x + App.left_margin, App.editor_win_y + 140, Colors.bg) diff --git a/UndoRedo.lua b/UndoRedo.lua index e842ecc..9f8a735 100644 --- a/UndoRedo.lua +++ b/UndoRedo.lua @@ -1,5 +1,6 @@ UR = { + last_op = nil, undo_stack = {}, redo_stack = {} -- {type = ?, note_list = { {idx = ?, offset = ?, etc}, {idx = ?, offset = ?, etc}, ... } } @@ -7,32 +8,85 @@ UR = } function UR.PushUndo(type, note_list) - local new_rec = {type = type, note_list = note_list} + local new_rec = {type = type, note_list = Util.CopyTable(note_list)} UR.undo_stack[#UR.undo_stack + 1] = new_rec - + -- clear redo stack if #UR.redo_stack > 0 then Util.ClearTable(UR.redo_stack) + Util.RecalculateStoredNoteIndices() end end function UR.PopUndo() if #UR.undo_stack == 0 then return; end - local last_rec = UR.undo_stack[#UR.undo_stack] local type = last_rec.type - -- do op + if type == e_OpType.Delete then + for i, v in ipairs(last_rec.note_list) do + table.insert(App.note_list, v) + end + end + if type == e_OpType.Insert then for i, v in ipairs(last_rec.note_list) do table.remove(App.note_list, v.idx) end end + if type == e_OpType.ModifyPitchAndDuration then + local temp = {} + + for i, v in ipairs(last_rec.note_list) do + temp.pitch = App.note_list[v.idx].pitch + temp.duration = App.note_list[v.idx].duration + + App.note_list[v.idx].pitch = v.pitch + App.note_list[v.idx].duration = v.duration + + v.pitch = temp.pitch + v.duration = temp.duration + end + end + + if type == e_OpType.ModifyVelocityAndOffVelocity then + local temp = {} + + for i, v in ipairs(last_rec.note_list) do + temp.velocity = App.note_list[v.idx].velocity + temp.off_velocity = App.note_list[v.idx].off_velocity + + App.note_list[v.idx].velocity = v.velocity + App.note_list[v.idx].off_velocity = v.off_velocity + + v.velocity = temp.velocity + v.off_velocity = temp.off_velocity + end + end + + if type == e_OpType.Move then + local temp = {} + --NOTE pitch may be modified by moving note to different string. So we account for that too + for i, v in ipairs(last_rec.note_list) do + temp.offset = App.note_list[v.idx].offset + temp.string_idx = App.note_list[v.idx].string_idx + temp.pitch = App.note_list[v.idx].pitch + + App.note_list[v.idx].offset = v.offset + App.note_list[v.idx].string_idx = v.string_idx + App.note_list[v.idx].pitch = v.pitch + + v.offset = temp.offset + v.string_idx = temp.string_idx + v.pitch = temp.pitch + end + end + -- push to the redo stack, pop from the undo UR.redo_stack[#UR.redo_stack + 1] = last_rec UR.undo_stack[#UR.undo_stack] = nil - + Util.ClearTable(App.note_list_selected) App.last_note_clicked = nil end @@ -49,8 +103,61 @@ function UR.PopRedo() if type == e_OpType.Insert then for i, v in ipairs(last_rec.note_list) do - -- NOTE restoring notes to their original idx (slow). Could restore them to the end of the table. Needs thinking. - table.insert(App.note_list, v.idx, v) + table.insert(App.note_list, v) + end + end + + if type == e_OpType.Delete then + for i, v in ipairs(last_rec.note_list) do + table.remove(App.note_list, v.idx) + end + end + + if type == e_OpType.ModifyPitchAndDuration then + local temp = {} + + for i, v in ipairs(last_rec.note_list) do + temp.pitch = App.note_list[v.idx].pitch + temp.duration = App.note_list[v.idx].duration + + App.note_list[v.idx].pitch = v.pitch + App.note_list[v.idx].duration = v.duration + + v.pitch = temp.pitch + v.duration = temp.duration + end + end + + if type == e_OpType.ModifyVelocityAndOffVelocity then + local temp = {} + + for i, v in ipairs(last_rec.note_list) do + temp.velocity = App.note_list[v.idx].velocity + temp.off_velocity = App.note_list[v.idx].off_velocity + + App.note_list[v.idx].velocity = v.velocity + App.note_list[v.idx].off_velocity = v.off_velocity + + v.velocity = temp.velocity + v.off_velocity = temp.off_velocity + end + end + + if type == e_OpType.Move then + local temp = {} + + for i, v in ipairs(last_rec.note_list) do + temp.offset = App.note_list[v.idx].offset + temp.string_idx = App.note_list[v.idx].string_idx + temp.pitch = App.note_list[v.idx].pitch + + App.note_list[v.idx].offset = v.offset + App.note_list[v.idx].string_idx = v.string_idx + App.note_list[v.idx].pitch = v.pitch + + v.offset = temp.offset + v.string_idx = temp.string_idx + v.pitch = temp.pitch end end diff --git a/Util.lua b/Util.lua index a825a2a..c3e4da4 100644 --- a/Util.lua +++ b/Util.lua @@ -149,6 +149,15 @@ function Util.ClearTable(t) end end +function Util.CopyTable(t) + local new_t = {} + for i, v in ipairs(t) do + new_t[i] = v + end + + return new_t +end + function Util.IsNoteSelected(note) local idx = note.idx From 8e1aba37a141066a6c4e9708f543faae144dab89 Mon Sep 17 00:00:00 2001 From: immortalx74 Date: Tue, 24 May 2022 07:52:54 +0300 Subject: [PATCH 03/12] sync local --- App.lua | 79 ++++++++++++++++++++++++++++++++++++++++++++------- Clipboard.lua | 59 ++++++++++++++++++++++++++++++++++++++ Editor.lua | 55 +++++++++++++++++++++++++++++------ Reaffer.lua | 3 +- Toolbar.lua | 12 ++++---- UI.lua | 20 ++++++++++--- UndoRedo.lua | 16 ++++++----- Util.lua | 37 ++++++++++++++++-------- 8 files changed, 232 insertions(+), 49 deletions(-) create mode 100644 Clipboard.lua diff --git a/App.lua b/App.lua index 5fc8c72..aba2583 100644 --- a/App.lua +++ b/App.lua @@ -18,6 +18,7 @@ App = current_pitch = 0, is_new_note = false, last_click_was_inside_editor = false, + attempts_paste = false, -- metrics window_w = 800, @@ -111,19 +112,38 @@ function App.Loop() App.mouse_x, App.mouse_y = reaper.ImGui_GetMousePos(App.ctx) App.window_w = reaper.ImGui_GetWindowWidth(App.ctx) - -- if reaper.ImGui_GetKeyMods(App.ctx) == reaper.ImGui_KeyModFlags_Ctrl() and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then - -- UR.PopUndo() - -- end - -- if reaper.ImGui_GetKeyMods(App.ctx) == reaper.ImGui_KeyModFlags_Ctrl() + reaper.ImGui_KeyModFlags_Shift() and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then - -- UR.PopRedo() - -- end if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and not reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModShift()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then - msg("undo") + UR.PopUndo() end if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModShift()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then - msg("redo") + UR.PopRedo() end - + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_C()) then + Clipboard.Copy() + end + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_X()) then + Clipboard.Cut() + end + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_V()) then + if #Clipboard.note_list > 0 then App.attempts_paste = true; end + end + + if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_S()) then + App.active_tool = e_Tool.Select + end + if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_D()) then + App.active_tool = e_Tool.Draw + end + if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_E()) then + App.active_tool = e_Tool.Erase + end + if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_W()) then + App.active_tool = e_Tool.Move + end + if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Escape()) then + App.attempts_paste = false + end + UI.Render_CB_Strings() UI.Render_CB_Signature() UI.Render_CB_Quantize() @@ -134,6 +154,46 @@ function App.Loop() UI.Render_Editor() UI.Render_Toolbar() + --------------------- + if reaper.ImGui_BeginListBox(App.ctx, "Undo stack", 300, 200 ) then + if #UR.undo_stack > 0 then + for i, v in ipairs(UR.undo_stack) do + local rec = v.note_list + reaper.ImGui_Text(App.ctx, "[Rec: " .. i .. ", Type: " .. v.type .. "]") + for j, m in ipairs(rec) do + reaper.ImGui_Text(App.ctx, " Note: idx = " .. rec[j].idx) + end + end + end + reaper.ImGui_EndListBox(App.ctx) + end + + reaper.ImGui_SameLine(App.ctx) + + if reaper.ImGui_BeginListBox(App.ctx, "Redo stack", 300, 200 ) then + if #UR.redo_stack > 0 then + for i, v in ipairs(UR.redo_stack) do + local rec = v.note_list + reaper.ImGui_Text(App.ctx, "[Rec: " .. i .. ", Type: " .. v.type .. "]") + for j, m in ipairs(rec) do + reaper.ImGui_Text(App.ctx, " Note: idx = " .. rec[j].idx) + end + end + end + reaper.ImGui_EndListBox(App.ctx) + end + + if reaper.ImGui_BeginListBox(App.ctx, "Clipboard", 300, 200 ) then + if #Clipboard.note_list > 0 then + for i, v in ipairs(Clipboard.note_list) do + -- local note = v.note_list + reaper.ImGui_Text(App.ctx, "Note: " .. Clipboard.note_list[i].idx) + end + end + reaper.ImGui_EndListBox(App.ctx) + end + --------------------- + App.mouse_prev_x, App.mouse_prev_y = App.mouse_x, App.mouse_y reaper.ImGui_End(App.ctx) end @@ -142,6 +202,5 @@ function App.Loop() reaper.defer(App.Loop) else reaper.ImGui_DestroyContext(App.ctx) - -- reaper.ImGui_DetachFont(App.ctx, App.icon_font) end end \ No newline at end of file diff --git a/Clipboard.lua b/Clipboard.lua new file mode 100644 index 0000000..d2e2ec2 --- /dev/null +++ b/Clipboard.lua @@ -0,0 +1,59 @@ +Clipboard = +{ + note_list = {} +} + +function Clipboard.Cut() + if Clipboard.Copy() then + Editor.EraseNotes(App.note_list_selected[1].offset, App.note_list_selected[1].string_idx) + end +end + +function Clipboard.Copy() + if #App.note_list_selected == 0 then + return false + end + + Util.ClearTable(Clipboard.note_list) + Clipboard.note_list = Util.CopyTable(App.note_list_selected) + return true +end + +function Clipboard.Paste(cx, cy) + -- do paste + -- copy clipboard to temp table and sort by offset + -- first pass: check each note if it "fits". The first one that doesn't fit means operation should be cancelled + -- second pass: copy the notes from temp table to App.note_list + local temp = {} + temp = Util.CopyTable(Clipboard.note_list) + table.sort(temp, function (k1, k2) return k1.offset < k2.offset; end) + + local src_offset = temp[1].offset + local diff = 0 + local dst_offset = 0 + + for i, v in ipairs(temp) do + diff = v.offset - src_offset + dst_offset = cx + diff + + if Util.IsCellEmpty(dst_offset, v.string_idx, true) then + v.offset = dst_offset + else + msg("doesn't fit") + return -- return or break? + end + end + + for i, v in ipairs(temp) do + App.note_list[#App.note_list + 1] = Util.CopyNote(v) + end + + Util.ClearTable(App.note_list_selected) + + -- for i, v in ipairs(Clipboard.note_list) do + -- local new_note = Util.CopyNote(v) + -- new_note.offset = v.offset + cx + -- table.insert(App.note_list, new_note) + -- end + App.attempts_paste = false +end \ No newline at end of file diff --git a/Editor.lua b/Editor.lua index 9c3ec8a..dacb3da 100644 --- a/Editor.lua +++ b/Editor.lua @@ -1,6 +1,13 @@ Editor = {} function Editor.OnMouseButtonClick(mbutton, cx, cy) + if App.attempts_paste then + if mbutton == e_MouseButton.Left and Util.IsCellEmpty(cx, cy, true) then + Clipboard.Paste(cx, cy) + end + return + end + App.can_init_drag = true App.last_click_was_inside_editor = true @@ -123,17 +130,49 @@ function Editor.EraseNotes(cx, cy) Util.ClearTable(App.note_list_selected) end +-- function Editor.MoveNotes(cx, cy, dx, dy) +-- for i, v in ipairs(App.note_list_selected) do +-- if cy >= 0 and cy < App.num_strings and cx >= 0 then + +-- local nearest_left = Util.GetCellNearestOccupied(cx, cy, e_Direction.Left, v.idx) +-- local nearest_right = Util.GetCellNearestOccupied(cx, cy, e_Direction.Right, v.idx) +-- App.note_list[v.idx].offset = Util.Clamp(v.offset + dx + v.duration - 1, nearest_left, nearest_right - v.duration) + +-- local dst_string_idx = Util.Clamp(v.string_idx - dy, 0, App.num_strings - 1) +-- Util.ShiftOctaveIfOutsideRange(App.note_list[v.idx], dst_string_idx) +-- App.note_list[v.idx].string_idx = dst_string_idx +-- end +-- end + +-- UR.last_op = e_OpType.Move +-- end + function Editor.MoveNotes(cx, cy, dx, dy) - for i, v in ipairs(App.note_list_selected) do - if cy >= 0 and cy < App.num_strings then - App.note_list[v.idx].offset = v.offset + dx - - local dst_string_idx = Util.Clamp(v.string_idx - dy, 0, App.num_strings - 1) - Util.ShiftOctaveIfOutsideRange(App.note_list[v.idx], dst_string_idx) - App.note_list[v.idx].string_idx = dst_string_idx - end + if cy >= 0 and cy < App.num_strings and cx >= 0 then + + local nearest_left = Util.GetCellNearestOccupied(cx, cy, e_Direction.Left, App.last_note_clicked.idx) + local nearest_right = Util.GetCellNearestOccupied(cx, cy, e_Direction.Right, App.last_note_clicked.idx) + App.note_list[App.last_note_clicked.idx].offset = Util.Clamp(App.last_note_clicked.offset + dx + App.last_note_clicked.duration - 1, nearest_left, nearest_right - App.last_note_clicked.duration) + + local dst_string_idx = Util.Clamp(App.last_note_clicked.string_idx - dy, 0, App.num_strings - 1) + Util.ShiftOctaveIfOutsideRange(App.note_list[App.last_note_clicked.idx], dst_string_idx) + App.note_list[App.last_note_clicked.idx].string_idx = dst_string_idx + msg(dx) end + -- for i, v in ipairs(App.note_list_selected) do + -- if cy >= 0 and cy < App.num_strings and cx >= 0 then + + -- local nearest_left = Util.GetCellNearestOccupied(cx, cy, e_Direction.Left, v.idx) + -- local nearest_right = Util.GetCellNearestOccupied(cx, cy, e_Direction.Right, v.idx) + -- App.note_list[v.idx].offset = Util.Clamp(v.offset + dx + v.duration - 1, nearest_left, nearest_right - v.duration) + + -- local dst_string_idx = Util.Clamp(v.string_idx - dy, 0, App.num_strings - 1) + -- Util.ShiftOctaveIfOutsideRange(App.note_list[v.idx], dst_string_idx) + -- App.note_list[v.idx].string_idx = dst_string_idx + -- end + -- end + UR.last_op = e_OpType.Move end diff --git a/Reaffer.lua b/Reaffer.lua index 4ffb81b..bbfc194 100644 --- a/Reaffer.lua +++ b/Reaffer.lua @@ -11,8 +11,6 @@ -- Allow multiple selection with marquee. Don't mimic riffer exact behavior. (it always sets leftmost note as the "active" one?) -- Also, auto-scroll when marquee is close to editor edges -- --- Implement Undo system. Shouldn't keep selection. Just clear selection if a deleted note was selected. --- -- Clamp moving notes. They currently can end out of the editor boundaries and can overlap each other. -- -- Current dragging system allows each note's properties to be clamped individually @@ -35,6 +33,7 @@ dofile(script_path .. "Toolbar.lua") dofile(script_path .. "Util.lua") dofile(script_path .. "Editor.lua") dofile(script_path .. "UndoRedo.lua") +dofile(script_path .. "Clipboard.lua") App.Init() reaper.defer(App.Loop) \ No newline at end of file diff --git a/Toolbar.lua b/Toolbar.lua index c2d47fe..a5b4358 100644 --- a/Toolbar.lua +++ b/Toolbar.lua @@ -1,12 +1,12 @@ ToolBar = { {icon = "a", tooltip = "Create MIDI item in first selected track, at edit cursor"}, - {icon = "e", tooltip = "Select"}, - {icon = "f", tooltip = "Move"}, - {icon = "g", tooltip = "Draw"}, - {icon = "h", tooltip = "Erase"}, - {icon = "i", tooltip = "Undo"}, - {icon = "j", tooltip = "Redo"}, + {icon = "e", tooltip = "Select [S]"}, + {icon = "f", tooltip = "Move [W]"}, + {icon = "g", tooltip = "Draw [D]"}, + {icon = "h", tooltip = "Erase [E]"}, + {icon = "i", tooltip = "Undo [Ctrl + Z]"}, + {icon = "j", tooltip = "Redo [Ctrl + Shift + Z]"}, {icon = "b", tooltip = "Cut"}, {icon = "c", tooltip = "Copy"}, {icon = "d", tooltip = "Paste"} diff --git a/UI.lua b/UI.lua index 09cece6..f77b045 100644 --- a/UI.lua +++ b/UI.lua @@ -10,7 +10,7 @@ function UI.Render_Notes(draw_list) note_x = App.editor_win_x + 50 + (note.offset * App.note_w) - App.scroll_x note_y = App.editor_win_y + 30 + (note.string_idx * App.note_h) - 5 - reaper.ImGui_DrawList_AddRectFilled(draw_list, note_x, note_y, note_x + (App.note_w * note.duration) -1, note_y + App.note_h-1, Util.VelocityColor(note.velocity), 6) + reaper.ImGui_DrawList_AddRectFilled(draw_list, note_x, note_y, note_x + (App.note_w * note.duration) -1, note_y + App.note_h - 1, Util.VelocityColor(note.velocity), 6) if App.note_display_cur_idx == e_NoteDisplay.Pitch then str = Util.NotePitchToName(note.pitch) elseif App.note_display_cur_idx == e_NoteDisplay.Fret then @@ -33,7 +33,7 @@ function UI.Render_Notes(draw_list) reaper.ImGui_DrawList_AddText(draw_list, note_x + 5, note_y - 2, Colors.text, str) if Util.IsNoteSelected(note) then - reaper.ImGui_DrawList_AddRect(draw_list, note_x, note_y, note_x + (App.note_w * note.duration) -1, note_y + App.note_h-1, Colors.text, 40, reaper.ImGui_DrawFlags_None(), 1) + reaper.ImGui_DrawList_AddRect(draw_list, note_x, note_y, note_x + (App.note_w * note.duration) - 1, note_y + App.note_h - 1, Colors.text, 40, reaper.ImGui_DrawFlags_None(), 1) end end end @@ -149,6 +149,12 @@ function UI.Render_Toolbar() UR.PopUndo() elseif i == e_Tool.Redo then UR.PopRedo() + elseif i == e_Tool.Cut then + Clipboard.Cut() + elseif i == e_Tool.Copy then + Clipboard.Copy() + elseif i == e_Tool.Paste then + if #Clipboard.note_list > 0 then App.attempts_paste = true; end end end reaper.ImGui_PopStyleColor(App.ctx) @@ -229,7 +235,7 @@ function UI.Render_Editor() local rect_y1 = App.editor_win_y + App.top_margin - 5 local rect_x2 = lane_end_x - 1 local rect_y2 = rect_y1 + 11 + (App.num_strings - 1) * App.lane_v_spacing - + -- debug draw editor mouse area -- reaper.ImGui_DrawList_AddRect(draw_list, rect_x1, rect_y1, rect_x2, rect_y2, Colors.red) @@ -242,6 +248,12 @@ function UI.Render_Editor() local preview_x = App.editor_win_x + App.left_margin + (cell_x * App.note_w) - App.scroll_x local preview_y = App.editor_win_y + App.top_margin + (cell_y * App.note_h) - 5 reaper.ImGui_DrawList_AddRectFilled(draw_list, preview_x, preview_y, preview_x + App.note_w - 1, preview_y + App.note_h - 1, Colors.note_preview, 40) + + if App.attempts_paste then + reaper.ImGui_BeginTooltip(App.ctx) + reaper.ImGui_Text(App.ctx, "Select position to paste. [ESC] to cancel") + reaper.ImGui_EndTooltip(App.ctx) + end end if reaper.ImGui_IsMouseClicked(App.ctx, 0) then Editor.OnMouseButtonClick(e_MouseButton.Left, cell_x, cell_y) @@ -268,7 +280,7 @@ function UI.Render_Editor() if reaper.ImGui_IsMouseDragging(App.ctx, 1) then Editor.OnMouseButtonDrag(e_MouseButton.Right) end - + -- Mask rect reaper.ImGui_DrawList_AddRectFilled(draw_list, App.editor_win_x, App.editor_win_y + 2, App.editor_win_x + App.left_margin, App.editor_win_y + 140, Colors.bg) diff --git a/UndoRedo.lua b/UndoRedo.lua index 9f8a735..af44ece 100644 --- a/UndoRedo.lua +++ b/UndoRedo.lua @@ -20,6 +20,7 @@ end function UR.PopUndo() if #UR.undo_stack == 0 then return; end + local last_rec = UR.undo_stack[#UR.undo_stack] local type = last_rec.type @@ -64,7 +65,7 @@ function UR.PopUndo() v.off_velocity = temp.off_velocity end end - + if type == e_OpType.Move then local temp = {} --NOTE pitch may be modified by moving note to different string. So we account for that too @@ -108,14 +109,15 @@ function UR.PopRedo() end if type == e_OpType.Delete then - for i, v in ipairs(last_rec.note_list) do - table.remove(App.note_list, v.idx) + local len = #last_rec.note_list + for i = len, 1, -1 do + table.remove(App.note_list, last_rec.note_list[i].idx) end end if type == e_OpType.ModifyPitchAndDuration then local temp = {} - + for i, v in ipairs(last_rec.note_list) do temp.pitch = App.note_list[v.idx].pitch temp.duration = App.note_list[v.idx].duration @@ -130,7 +132,7 @@ function UR.PopRedo() if type == e_OpType.ModifyVelocityAndOffVelocity then local temp = {} - + for i, v in ipairs(last_rec.note_list) do temp.velocity = App.note_list[v.idx].velocity temp.off_velocity = App.note_list[v.idx].off_velocity @@ -142,10 +144,10 @@ function UR.PopRedo() v.off_velocity = temp.off_velocity end end - + if type == e_OpType.Move then local temp = {} - + for i, v in ipairs(last_rec.note_list) do temp.offset = App.note_list[v.idx].offset temp.string_idx = App.note_list[v.idx].string_idx diff --git a/Util.lua b/Util.lua index c3e4da4..1959802 100644 --- a/Util.lua +++ b/Util.lua @@ -93,20 +93,33 @@ function Util.IsCellEmpty(cx, cy, duration_inclusive) return true end --- TODO Need to do Left direction too. (Or not?) -function Util.GetCellNearestOccupied(cx, cy, direction) - local cur = Util.NumGridDivisions() +function Util.GetCellNearestOccupied(cx, cy, direction, note_idx) if direction == e_Direction.Right then + local cur = Util.NumGridDivisions() + for i, note in ipairs(App.note_list) do if (cy == note.string_idx) and (note.offset > cx) then - if note.offset < cur then + if note.offset < cur and i ~= note_idx then cur = note.offset end end end return cur end + + if direction == e_Direction.Left then + local cur = 0 + + for i, note in ipairs(App.note_list) do + if (cy == note.string_idx) and (note.offset <= cx) then + if note.offset + note.duration > cur and i ~= note_idx then + cur = note.offset + note.duration + end + end + end + return cur + end end function Util.CreateMIDI() @@ -114,16 +127,16 @@ function Util.CreateMIDI() local q = {0.25, 0.5, 1, 2, 4, 8, 16} -- {"1/1", "1/2", "1/4", "1/8", "1/16", "1/32", "1/64"}, ratio = ppq / q[App.quantize_cur_idx] - + local track = reaper.GetSelectedTrack(0, 0) if track == nil then return; end - + local start_time_secs = reaper.GetCursorPositionEx(0) local end_time_secs = reaper.TimeMap2_beatsToTime(0, 0, App.num_measures) local new_item = reaper.CreateNewMIDIItemInProj(track, start_time_secs, start_time_secs + end_time_secs) local take = reaper.GetActiveTake(new_item) local start_ppq = reaper.MIDI_GetPPQPosFromProjTime(take, start_time_secs) - + local note_begin local note_end @@ -154,7 +167,7 @@ function Util.CopyTable(t) for i, v in ipairs(t) do new_t[i] = v end - + return new_t end @@ -186,7 +199,7 @@ function Util.GetNoteIndexAtCell(cx, cy) return i end end - + return 0 -- Not found end @@ -210,11 +223,11 @@ end function Util.ShiftOctaveIfOutsideRange(note, target_string_idx) if note.string_idx == target_string_idx then return; end - + local min_pitch = App.instrument[App.num_strings - 3].open[App.num_strings - target_string_idx] local max_pitch = min_pitch + 24 - - + + if note.pitch > min_pitch and note.pitch > max_pitch then Editor.StopNote() note.pitch = note.pitch - 12 From 6e07bdd6f2dc3ecaae378e0fe97ff12357d4d888 Mon Sep 17 00:00:00 2001 From: immortalx74 Date: Tue, 31 May 2022 05:41:23 +0300 Subject: [PATCH 04/12] Refactor: Stop storing indices on note objects --- App.lua | 52 +++------------------ Clipboard.lua | 29 +++++------- Debug.lua | 70 +++++++++++++++++++++++++++++ Editor.lua | 122 ++++++++++++++++++++++++++------------------------ Reaffer.lua | 5 ++- Toolbar.lua | 6 +-- UI.lua | 3 +- UndoRedo.lua | 96 +++++++++++++++++++++++---------------- Util.lua | 52 ++++++++++++--------- 9 files changed, 245 insertions(+), 190 deletions(-) create mode 100644 Debug.lua diff --git a/App.lua b/App.lua index aba2583..d006328 100644 --- a/App.lua +++ b/App.lua @@ -46,8 +46,8 @@ App = -- defaults wheel_delta = 50, active_tool = e_Tool.Select, - num_strings = 6, - num_measures = 4, + num_strings = 8, + num_measures = 1, quantize_cur_idx = 5, signature_cur_idx = 3, note_display_cur_idx = e_NoteDisplay.Pitch, @@ -85,14 +85,15 @@ App = note_list = { - -- {idx = 1, offset = 6, string_idx = 2, pitch = 25, velocity = 127, off_velocity = 80, duration = 1}, + -- { offset = 6, string_idx = 2, pitch = 25, velocity = 127, off_velocity = 80, duration = 1}, }, -- table, storing last clicked note object. - -- That's different from last selected, as you can "re-click" an already selected note + -- That's different from last selected, as you can "re-click" an already selected note. + -- Also, stores an 'idx' field last_note_clicked, - note_list_selected = {} + note_list_selected = { indices = {} } } function App.Init() @@ -153,46 +154,7 @@ function App.Loop() UI.Render_TXT_Help() UI.Render_Editor() UI.Render_Toolbar() - - --------------------- - if reaper.ImGui_BeginListBox(App.ctx, "Undo stack", 300, 200 ) then - if #UR.undo_stack > 0 then - for i, v in ipairs(UR.undo_stack) do - local rec = v.note_list - reaper.ImGui_Text(App.ctx, "[Rec: " .. i .. ", Type: " .. v.type .. "]") - for j, m in ipairs(rec) do - reaper.ImGui_Text(App.ctx, " Note: idx = " .. rec[j].idx) - end - end - end - reaper.ImGui_EndListBox(App.ctx) - end - - reaper.ImGui_SameLine(App.ctx) - - if reaper.ImGui_BeginListBox(App.ctx, "Redo stack", 300, 200 ) then - if #UR.redo_stack > 0 then - for i, v in ipairs(UR.redo_stack) do - local rec = v.note_list - reaper.ImGui_Text(App.ctx, "[Rec: " .. i .. ", Type: " .. v.type .. "]") - for j, m in ipairs(rec) do - reaper.ImGui_Text(App.ctx, " Note: idx = " .. rec[j].idx) - end - end - end - reaper.ImGui_EndListBox(App.ctx) - end - - if reaper.ImGui_BeginListBox(App.ctx, "Clipboard", 300, 200 ) then - if #Clipboard.note_list > 0 then - for i, v in ipairs(Clipboard.note_list) do - -- local note = v.note_list - reaper.ImGui_Text(App.ctx, "Note: " .. Clipboard.note_list[i].idx) - end - end - reaper.ImGui_EndListBox(App.ctx) - end - --------------------- + if Debug.enabled then Debug.ShowContainers(); end App.mouse_prev_x, App.mouse_prev_y = App.mouse_x, App.mouse_y reaper.ImGui_End(App.ctx) diff --git a/Clipboard.lua b/Clipboard.lua index d2e2ec2..c597955 100644 --- a/Clipboard.lua +++ b/Clipboard.lua @@ -10,37 +10,33 @@ function Clipboard.Cut() end function Clipboard.Copy() - if #App.note_list_selected == 0 then - return false - end + if #App.note_list_selected == 0 then return false; end Util.ClearTable(Clipboard.note_list) Clipboard.note_list = Util.CopyTable(App.note_list_selected) return true end +-- NOTE: WIP. Doesn't check for overlapping notes, out of bounds of measures, etc function Clipboard.Paste(cx, cy) - -- do paste - -- copy clipboard to temp table and sort by offset - -- first pass: check each note if it "fits". The first one that doesn't fit means operation should be cancelled - -- second pass: copy the notes from temp table to App.note_list local temp = {} - temp = Util.CopyTable(Clipboard.note_list) - table.sort(temp, function (k1, k2) return k1.offset < k2.offset; end) + for i, v in ipairs(Clipboard.note_list) do + temp[#temp + 1] = Util.CopyNote(v) + end + table.sort(temp, function (k1, k2) return k1.offset < k2.offset; end) local src_offset = temp[1].offset local diff = 0 local dst_offset = 0 - + for i, v in ipairs(temp) do diff = v.offset - src_offset dst_offset = cx + diff - + if Util.IsCellEmpty(dst_offset, v.string_idx, true) then v.offset = dst_offset else - msg("doesn't fit") - return -- return or break? + return end end @@ -48,12 +44,7 @@ function Clipboard.Paste(cx, cy) App.note_list[#App.note_list + 1] = Util.CopyNote(v) end + UR.PushUndo(e_OpType.Insert, temp) Util.ClearTable(App.note_list_selected) - - -- for i, v in ipairs(Clipboard.note_list) do - -- local new_note = Util.CopyNote(v) - -- new_note.offset = v.offset + cx - -- table.insert(App.note_list, new_note) - -- end App.attempts_paste = false end \ No newline at end of file diff --git a/Debug.lua b/Debug.lua new file mode 100644 index 0000000..e3fb92f --- /dev/null +++ b/Debug.lua @@ -0,0 +1,70 @@ +Debug = {enabled = false} + +function Debug.ShowContainers() + local opt = {"Insert", "Delete", "Pitch + dur", "Vel", "Move"} + if reaper.ImGui_BeginListBox(App.ctx, "Undo stack", 300, 200 ) then + if #UR.undo_stack > 0 then + for i, v in ipairs(UR.undo_stack) do + local rec = v.note_list + reaper.ImGui_Text(App.ctx, "[Rec: " .. i .. ", Type: " .. opt[v.type] .. "]") + for j, m in ipairs(rec) do + reaper.ImGui_Text(App.ctx, " Note: " .. rec[j].offset .. "-" .. rec[j].string_idx) + end + end + end + reaper.ImGui_EndListBox(App.ctx) + end + + reaper.ImGui_SameLine(App.ctx) + + if reaper.ImGui_BeginListBox(App.ctx, "Redo stack", 300, 200 ) then + if #UR.redo_stack > 0 then + for i, v in ipairs(UR.redo_stack) do + local rec = v.note_list + reaper.ImGui_Text(App.ctx, "[Rec: " .. i .. ", Type: " .. opt[v.type] .. "]") + for j, m in ipairs(rec) do + reaper.ImGui_Text(App.ctx, " Note: " .. rec[j].offset .. "-" .. rec[j].string_idx) + end + end + end + reaper.ImGui_EndListBox(App.ctx) + end + + reaper.ImGui_SameLine(App.ctx) + if reaper.ImGui_BeginListBox(App.ctx, "Clipboard", 300, 200 ) then + if #Clipboard.note_list > 0 then + for i, v in ipairs(Clipboard.note_list) do + reaper.ImGui_Text(App.ctx, "Note: " .. v.offset .. "-" .. v.string_idx) + end + end + reaper.ImGui_EndListBox(App.ctx) + end + + if reaper.ImGui_BeginListBox(App.ctx, "Notes", 300, 200 ) then + if #App.note_list > 0 then + for i, v in ipairs(App.note_list) do + reaper.ImGui_Text(App.ctx, i .. " - X:" .. v.offset .. " - Y:" .. v.string_idx .. " - Dur:" ..v.duration) + end + end + reaper.ImGui_EndListBox(App.ctx) + end + + reaper.ImGui_SameLine(App.ctx) + if reaper.ImGui_BeginListBox(App.ctx, "Selected", 300, 200 ) then + if #App.note_list_selected > 0 then + for i, v in ipairs(App.note_list_selected) do + local idx = App.note_list_selected.indices[i] + reaper.ImGui_Text(App.ctx, idx .. " - X:" .. v.offset .. " - Y:" .. v.string_idx .. " - Dur:" ..v.duration) + end + end + reaper.ImGui_EndListBox(App.ctx) + end + + reaper.ImGui_SameLine(App.ctx) + if reaper.ImGui_BeginListBox(App.ctx, "Last Clicked", 300, 50 ) then + if App.last_note_clicked ~= nil then + reaper.ImGui_Text(App.ctx, App.last_note_clicked.idx .. " - X:" .. App.last_note_clicked.offset .. " - Y:" .. App.last_note_clicked.string_idx .. " - Dur:" ..App.last_note_clicked.duration) + end + reaper.ImGui_EndListBox(App.ctx) + end +end \ No newline at end of file diff --git a/Editor.lua b/Editor.lua index dacb3da..62c79ac 100644 --- a/Editor.lua +++ b/Editor.lua @@ -14,6 +14,7 @@ function Editor.OnMouseButtonClick(mbutton, cx, cy) if mbutton == e_MouseButton.Left or mbutton == e_MouseButton.Right then if Util.IsCellEmpty(cx, cy, true) then Util.ClearTable(App.note_list_selected) + Util.ClearTable(App.note_list_selected.indices) App.last_note_clicked = nil if App.active_tool == e_Tool.Draw then @@ -81,13 +82,17 @@ function Editor.SelectNotes(cx, cy) if (cx >= note.offset) and (cx < note.offset + note.duration) and (cy == note.string_idx) then if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) then App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) + App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i else - if not Util.IsNoteSelected(note) then + if not (Util.IsNoteAtCellSelected(note.offset, note.string_idx)) then Util.ClearTable(App.note_list_selected) + Util.ClearTable(App.note_list_selected.indices) App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) + App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i end end App.last_note_clicked = Util.CopyNote(note) + App.last_note_clicked.idx = i App.current_pitch = App.last_note_clicked.pitch end end @@ -95,10 +100,12 @@ end function Editor.InsertNote(cx, cy) local recent_pitch = App.instrument[App.num_strings - 3].recent[App.num_strings - cy] - local new_note = {idx = #App.note_list + 1, offset = cx, string_idx = cy, pitch = recent_pitch, velocity = App.default_velocity, off_velocity = App.default_off_velocity, duration = 1} + local new_note = {offset = cx, string_idx = cy, pitch = recent_pitch, velocity = App.default_velocity, off_velocity = App.default_off_velocity, duration = 1} App.note_list[#App.note_list + 1] = new_note App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(new_note) + App.note_list_selected.indices[#App.note_list_selected.indices + 1] = #App.note_list App.last_note_clicked = Util.CopyNote(new_note) + App.last_note_clicked.idx = #App.note_list App.current_pitch = App.last_note_clicked.pitch Editor.PlayNote() -- push undo here @@ -109,14 +116,12 @@ end function Editor.EraseNotes(cx, cy) if Util.IsNoteAtCellSelected(cx, cy) then - table.sort(App.note_list_selected, function (k1, k2) return k1.idx < k2.idx; end) - local len = #App.note_list_selected - -- push undo here (multiple) UR.PushUndo(e_OpType.Delete, App.note_list_selected) - for i = len, 1, -1 do - table.remove(App.note_list, App.note_list_selected[i].idx) + for i, v in ipairs(App.note_list_selected) do + local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) + table.remove(App.note_list, idx) end else local idx = Util.GetNoteIndexAtCell(cx, cy) @@ -126,73 +131,72 @@ function Editor.EraseNotes(cx, cy) end UR.last_op = e_OpType.Delete - Util.RecalculateStoredNoteIndices() Util.ClearTable(App.note_list_selected) + Util.ClearTable(App.note_list_selected.indices) end --- function Editor.MoveNotes(cx, cy, dx, dy) --- for i, v in ipairs(App.note_list_selected) do --- if cy >= 0 and cy < App.num_strings and cx >= 0 then - --- local nearest_left = Util.GetCellNearestOccupied(cx, cy, e_Direction.Left, v.idx) --- local nearest_right = Util.GetCellNearestOccupied(cx, cy, e_Direction.Right, v.idx) --- App.note_list[v.idx].offset = Util.Clamp(v.offset + dx + v.duration - 1, nearest_left, nearest_right - v.duration) - --- local dst_string_idx = Util.Clamp(v.string_idx - dy, 0, App.num_strings - 1) --- Util.ShiftOctaveIfOutsideRange(App.note_list[v.idx], dst_string_idx) --- App.note_list[v.idx].string_idx = dst_string_idx --- end --- end - --- UR.last_op = e_OpType.Move --- end - function Editor.MoveNotes(cx, cy, dx, dy) - if cy >= 0 and cy < App.num_strings and cx >= 0 then - - local nearest_left = Util.GetCellNearestOccupied(cx, cy, e_Direction.Left, App.last_note_clicked.idx) - local nearest_right = Util.GetCellNearestOccupied(cx, cy, e_Direction.Right, App.last_note_clicked.idx) - App.note_list[App.last_note_clicked.idx].offset = Util.Clamp(App.last_note_clicked.offset + dx + App.last_note_clicked.duration - 1, nearest_left, nearest_right - App.last_note_clicked.duration) + local all_fit = true + local base_diff = dx + App.last_note_clicked.duration - 1 + local leftmost = Util.NumGridDivisions(); local topmost = App.num_strings - 1; local rightmost = 0; local bottommost = 0 + + for i, v in ipairs(App.note_list_selected) do + local idx = App.note_list_selected.indices[i] + if App.note_list[idx].offset < leftmost then leftmost = App.note_list[idx].offset; end + if App.note_list[idx].offset + App.note_list[idx].duration - 1 > rightmost then rightmost = App.note_list[idx].offset + App.note_list[idx].duration - 1; end + if App.note_list[idx].string_idx < topmost then topmost = App.note_list[idx].string_idx; end + if App.note_list[idx].string_idx > bottommost then bottommost = App.note_list[idx].string_idx; end - local dst_string_idx = Util.Clamp(App.last_note_clicked.string_idx - dy, 0, App.num_strings - 1) - Util.ShiftOctaveIfOutsideRange(App.note_list[App.last_note_clicked.idx], dst_string_idx) - App.note_list[App.last_note_clicked.idx].string_idx = dst_string_idx - msg(dx) + if not Util.IsNewPositionOnStringEmpty(App.note_list_selected.indices[i], v.offset + base_diff, v.string_idx - dy) then + all_fit = false + break + end end - -- for i, v in ipairs(App.note_list_selected) do - -- if cy >= 0 and cy < App.num_strings and cx >= 0 then - - -- local nearest_left = Util.GetCellNearestOccupied(cx, cy, e_Direction.Left, v.idx) - -- local nearest_right = Util.GetCellNearestOccupied(cx, cy, e_Direction.Right, v.idx) - -- App.note_list[v.idx].offset = Util.Clamp(v.offset + dx + v.duration - 1, nearest_left, nearest_right - v.duration) + local base_x = App.note_list[App.last_note_clicked.idx].offset + local base_y = App.note_list[App.last_note_clicked.idx].string_idx + local l_bound = base_x - leftmost + local r_bound = Util.NumGridDivisions() - rightmost + base_x + local t_bound = base_y - topmost + local b_bound = App.num_strings - bottommost + base_y - -- local dst_string_idx = Util.Clamp(v.string_idx - dy, 0, App.num_strings - 1) - -- Util.ShiftOctaveIfOutsideRange(App.note_list[v.idx], dst_string_idx) - -- App.note_list[v.idx].string_idx = dst_string_idx - -- end - -- end - - UR.last_op = e_OpType.Move + if all_fit then + if cx >= l_bound and cx < r_bound and cy >= t_bound and cy < b_bound then + for i, v in ipairs(App.note_list_selected) do + local idx = App.note_list_selected.indices[i] + App.note_list[idx].offset = v.offset + dx + App.last_note_clicked.duration - 1 + + local dst_string_idx = Util.Clamp(v.string_idx - dy, 0, App.num_strings - 1) + Util.ShiftOctaveIfOutsideRange(App.note_list[idx], dst_string_idx) + App.note_list[idx].string_idx = dst_string_idx + end + end + + UR.last_op = e_OpType.Move + end end function Editor.ModifyPitchAndDuration(cx, cy, dx, dy) for i, v in ipairs(App.note_list_selected) do + -- duration - local nearest = Util.GetCellNearestOccupied(App.note_list[v.idx].offset, App.note_list[v.idx].string_idx, e_Direction.Right) + local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) + + local nearest = Util.GetCellNearestOccupied(App.note_list[idx].offset, App.note_list[idx].string_idx, e_Direction.Right) if cx >= App.last_note_clicked.offset then - App.note_list[v.idx].duration = Util.Clamp(v.duration + dx, 1, nearest - App.note_list[v.idx].offset) + App.note_list[idx].duration = Util.Clamp(v.duration + dx, 1, nearest - App.note_list[idx].offset) end -- pitch - local pitch_min = App.instrument[App.num_strings - 3].open[App.num_strings - App.note_list[v.idx].string_idx] + local pitch_min = App.instrument[App.num_strings - 3].open[App.num_strings - App.note_list[idx].string_idx] local pitch_max = pitch_min + 24 - App.note_list[v.idx].pitch = Util.Clamp(v.pitch + dy, pitch_min, pitch_max) - Util.UpdateRecentPitch(App.num_strings - v.string_idx, App.note_list[v.idx].pitch) + App.note_list[idx].pitch = Util.Clamp(v.pitch + dy, pitch_min, pitch_max) + Util.UpdateRecentPitch(App.num_strings - v.string_idx, App.note_list[idx].pitch) end UR.last_op = e_OpType.ModifyPitchAndDuration - local idx = App.last_note_clicked.idx + local idx = Util.GetNoteIndexAtCell(App.last_note_clicked.offset, App.last_note_clicked.string_idx) + if App.current_pitch ~= App.note_list[idx].pitch then Editor.StopNote() App.current_pitch = App.note_list[idx].pitch @@ -202,10 +206,12 @@ end function Editor.ModifyVelocityAndOffVelocity(cx, cy, dx, dy) for i, v in ipairs(App.note_list_selected) do + local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModShift()) then - App.note_list[v.idx].off_velocity = Util.Clamp(v.off_velocity + dy, 0, 127) + App.note_list[idx].off_velocity = Util.Clamp(v.off_velocity + dy, 0, 127) else - App.note_list[v.idx].velocity = Util.Clamp(v.velocity + dy, 0, 127) + App.note_list[idx].velocity = Util.Clamp(v.velocity + dy, 0, 127) end end @@ -214,12 +220,10 @@ end function Editor.PlayNote() if App.last_note_clicked == nil or App.audition_notes == false then return; end - local idx = App.last_note_clicked.idx - reaper.StuffMIDIMessage(0, 0x90, App.current_pitch, App.note_list[idx].velocity) + reaper.StuffMIDIMessage(0, 0x90, App.current_pitch, App.last_note_clicked.velocity) end function Editor.StopNote() if App.last_note_clicked == nil or App.audition_notes == false then return; end - local idx = App.last_note_clicked.idx - reaper.StuffMIDIMessage(0, 0x80, App.current_pitch, App.note_list[idx].velocity) + reaper.StuffMIDIMessage(0, 0x80, App.current_pitch, App.last_note_clicked.velocity) end diff --git a/Reaffer.lua b/Reaffer.lua index bbfc194..3b7f89f 100644 --- a/Reaffer.lua +++ b/Reaffer.lua @@ -6,8 +6,6 @@ -- When reducing measure count, keep all notes internally (even those on a higher measure count) and if necessary, -- shorten notes duration (ONLY visually) which happen to start on a valid measure, but their duration extends beyond the measure's boundary. -- --- Implement Cut, Copy, Paste --- -- Allow multiple selection with marquee. Don't mimic riffer exact behavior. (it always sets leftmost note as the "active" one?) -- Also, auto-scroll when marquee is close to editor edges -- @@ -20,6 +18,8 @@ -- There's currently no way to change tuning. Not an easy way to do that. -- What happens to the notes that fall outside of a string's range, -- when you change tuning during a session? Needs thinking +-- +-- BUG: When releasing and pressing again Ctrl, and clicking an already selected note, it gets re-added to note_list_selected. function msg(txt) reaper.ShowConsoleMsg(tostring(txt) .. "\n") @@ -34,6 +34,7 @@ dofile(script_path .. "Util.lua") dofile(script_path .. "Editor.lua") dofile(script_path .. "UndoRedo.lua") dofile(script_path .. "Clipboard.lua") +dofile(script_path .. "Debug.lua") App.Init() reaper.defer(App.Loop) \ No newline at end of file diff --git a/Toolbar.lua b/Toolbar.lua index a5b4358..ab12d66 100644 --- a/Toolbar.lua +++ b/Toolbar.lua @@ -7,7 +7,7 @@ ToolBar = {icon = "h", tooltip = "Erase [E]"}, {icon = "i", tooltip = "Undo [Ctrl + Z]"}, {icon = "j", tooltip = "Redo [Ctrl + Shift + Z]"}, - {icon = "b", tooltip = "Cut"}, - {icon = "c", tooltip = "Copy"}, - {icon = "d", tooltip = "Paste"} + {icon = "b", tooltip = "Cut [Ctrl + X]"}, + {icon = "c", tooltip = "Copy [Ctrl + C]"}, + {icon = "d", tooltip = "Paste [Ctrl + V]"} } \ No newline at end of file diff --git a/UI.lua b/UI.lua index f77b045..1dcd101 100644 --- a/UI.lua +++ b/UI.lua @@ -32,7 +32,7 @@ function UI.Render_Notes(draw_list) end reaper.ImGui_DrawList_AddText(draw_list, note_x + 5, note_y - 2, Colors.text, str) - if Util.IsNoteSelected(note) then + if Util.IsNoteSelected(i) then reaper.ImGui_DrawList_AddRect(draw_list, note_x, note_y, note_x + (App.note_w * note.duration) - 1, note_y + App.note_h - 1, Colors.text, 40, reaper.ImGui_DrawFlags_None(), 1) end end @@ -250,6 +250,7 @@ function UI.Render_Editor() reaper.ImGui_DrawList_AddRectFilled(draw_list, preview_x, preview_y, preview_x + App.note_w - 1, preview_y + App.note_h - 1, Colors.note_preview, 40) if App.attempts_paste then + reaper.ImGui_DrawList_AddLine(draw_list, preview_x, App.editor_win_y + App.top_margin, preview_x, App.editor_win_y + App.top_margin + ((App.num_strings - 1) * App.lane_v_spacing), Colors.red) reaper.ImGui_BeginTooltip(App.ctx) reaper.ImGui_Text(App.ctx, "Select position to paste. [ESC] to cancel") reaper.ImGui_EndTooltip(App.ctx) diff --git a/UndoRedo.lua b/UndoRedo.lua index af44ece..121d2dc 100644 --- a/UndoRedo.lua +++ b/UndoRedo.lua @@ -3,18 +3,25 @@ UR = last_op = nil, undo_stack = {}, redo_stack = {} - -- {type = ?, note_list = { {idx = ?, offset = ?, etc}, {idx = ?, offset = ?, etc}, ... } } - -- {type = ?, note_list = { {idx = ?, offset = ?, etc}, {idx = ?, offset = ?, etc}, ... } } + -- {type = ?, note_list = { {offset = ?, etc}, {idx = ?, offset = ?, etc}, ... } } + -- {type = ?, note_list = { {offset = ?, etc}, {idx = ?, offset = ?, etc}, ... } } + + -- NOTE If the type is move, include indices. + -- {type = ?, indices = {}, note_list = { {offset = ?, etc}, {idx = ?, offset = ?, etc}, ... } } } function UR.PushUndo(type, note_list) local new_rec = {type = type, note_list = Util.CopyTable(note_list)} + + if type == e_OpType.Move then + new_rec.indices = Util.CopyTable(note_list.indices) + end + UR.undo_stack[#UR.undo_stack + 1] = new_rec -- clear redo stack if #UR.redo_stack > 0 then Util.ClearTable(UR.redo_stack) - Util.RecalculateStoredNoteIndices() end end @@ -32,7 +39,8 @@ function UR.PopUndo() if type == e_OpType.Insert then for i, v in ipairs(last_rec.note_list) do - table.remove(App.note_list, v.idx) + local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) + table.remove(App.note_list, idx) end end @@ -40,11 +48,13 @@ function UR.PopUndo() local temp = {} for i, v in ipairs(last_rec.note_list) do - temp.pitch = App.note_list[v.idx].pitch - temp.duration = App.note_list[v.idx].duration + local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) + + temp.pitch = App.note_list[idx].pitch + temp.duration = App.note_list[idx].duration - App.note_list[v.idx].pitch = v.pitch - App.note_list[v.idx].duration = v.duration + App.note_list[idx].pitch = v.pitch + App.note_list[idx].duration = v.duration v.pitch = temp.pitch v.duration = temp.duration @@ -55,11 +65,13 @@ function UR.PopUndo() local temp = {} for i, v in ipairs(last_rec.note_list) do - temp.velocity = App.note_list[v.idx].velocity - temp.off_velocity = App.note_list[v.idx].off_velocity + local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) + + temp.velocity = App.note_list[idx].velocity + temp.off_velocity = App.note_list[idx].off_velocity - App.note_list[v.idx].velocity = v.velocity - App.note_list[v.idx].off_velocity = v.off_velocity + App.note_list[idx].velocity = v.velocity + App.note_list[idx].off_velocity = v.off_velocity v.velocity = temp.velocity v.off_velocity = temp.off_velocity @@ -68,15 +80,18 @@ function UR.PopUndo() if type == e_OpType.Move then local temp = {} - --NOTE pitch may be modified by moving note to different string. So we account for that too + -- pitch may be modified by moving note to different string. So we account for that too for i, v in ipairs(last_rec.note_list) do - temp.offset = App.note_list[v.idx].offset - temp.string_idx = App.note_list[v.idx].string_idx - temp.pitch = App.note_list[v.idx].pitch + -- local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) + local idx = last_rec.indices[i] - App.note_list[v.idx].offset = v.offset - App.note_list[v.idx].string_idx = v.string_idx - App.note_list[v.idx].pitch = v.pitch + temp.offset = App.note_list[idx].offset + temp.string_idx = App.note_list[idx].string_idx + temp.pitch = App.note_list[idx].pitch + + App.note_list[idx].offset = v.offset + App.note_list[idx].string_idx = v.string_idx + App.note_list[idx].pitch = v.pitch v.offset = temp.offset v.string_idx = temp.string_idx @@ -92,9 +107,6 @@ function UR.PopUndo() App.last_note_clicked = nil end -function UR.PushRedo() -end - function UR.PopRedo() if #UR.redo_stack == 0 then return; end @@ -109,9 +121,9 @@ function UR.PopRedo() end if type == e_OpType.Delete then - local len = #last_rec.note_list - for i = len, 1, -1 do - table.remove(App.note_list, last_rec.note_list[i].idx) + for i, v in ipairs(last_rec.note_list) do + local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) + table.remove(App.note_list, idx) end end @@ -119,11 +131,13 @@ function UR.PopRedo() local temp = {} for i, v in ipairs(last_rec.note_list) do - temp.pitch = App.note_list[v.idx].pitch - temp.duration = App.note_list[v.idx].duration + local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) - App.note_list[v.idx].pitch = v.pitch - App.note_list[v.idx].duration = v.duration + temp.pitch = App.note_list[idx].pitch + temp.duration = App.note_list[idx].duration + + App.note_list[idx].pitch = v.pitch + App.note_list[idx].duration = v.duration v.pitch = temp.pitch v.duration = temp.duration @@ -134,11 +148,13 @@ function UR.PopRedo() local temp = {} for i, v in ipairs(last_rec.note_list) do - temp.velocity = App.note_list[v.idx].velocity - temp.off_velocity = App.note_list[v.idx].off_velocity + local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) + + temp.velocity = App.note_list[idx].velocity + temp.off_velocity = App.note_list[idx].off_velocity - App.note_list[v.idx].velocity = v.velocity - App.note_list[v.idx].off_velocity = v.off_velocity + App.note_list[idx].velocity = v.velocity + App.note_list[idx].off_velocity = v.off_velocity v.velocity = temp.velocity v.off_velocity = temp.off_velocity @@ -149,13 +165,15 @@ function UR.PopRedo() local temp = {} for i, v in ipairs(last_rec.note_list) do - temp.offset = App.note_list[v.idx].offset - temp.string_idx = App.note_list[v.idx].string_idx - temp.pitch = App.note_list[v.idx].pitch + local idx = last_rec.indices[i] + + temp.offset = App.note_list[idx].offset + temp.string_idx = App.note_list[idx].string_idx + temp.pitch = App.note_list[idx].pitch - App.note_list[v.idx].offset = v.offset - App.note_list[v.idx].string_idx = v.string_idx - App.note_list[v.idx].pitch = v.pitch + App.note_list[idx].offset = v.offset + App.note_list[idx].string_idx = v.string_idx + App.note_list[idx].pitch = v.pitch v.offset = temp.offset v.string_idx = temp.string_idx diff --git a/Util.lua b/Util.lua index 1959802..87ec48c 100644 --- a/Util.lua +++ b/Util.lua @@ -110,9 +110,9 @@ function Util.GetCellNearestOccupied(cx, cy, direction, note_idx) if direction == e_Direction.Left then local cur = 0 - + for i, note in ipairs(App.note_list) do - if (cy == note.string_idx) and (note.offset <= cx) then + if (cy == note.string_idx) and (note.offset < cx) then if note.offset + note.duration > cur and i ~= note_idx then cur = note.offset + note.duration end @@ -122,6 +122,23 @@ function Util.GetCellNearestOccupied(cx, cy, direction, note_idx) end end +function Util.RangeOverlap(a1, a2, b1, b2) + if a2 < b1 or b2 < a1 then return false; end + return true +end + +function Util.IsNewPositionOnStringEmpty(note_idx, new_x, y) + for i, v in ipairs(App.note_list) do + if (y == v.string_idx) and (i ~= note_idx) and not (Util.IsNoteSelected(i)) then -- exclude notes on other strings, self and any selected notes + if Util.RangeOverlap(new_x, new_x + App.note_list[note_idx].duration - 1, v.offset, v.offset + v.duration - 1) then + return false + end + end + end + + return true +end + function Util.CreateMIDI() local ppq = 960 local q = {0.25, 0.5, 1, 2, 4, 8, 16} @@ -152,7 +169,7 @@ function Util.CopyNote(note) msg("note is nil") return end - local t = {idx = note.idx, offset = note.offset, string_idx = note.string_idx, pitch = note.pitch, velocity = note.velocity, off_velocity = note.off_velocity, duration = note.duration} + local t = {offset = note.offset, string_idx = note.string_idx, pitch = note.pitch, velocity = note.velocity, off_velocity = note.off_velocity, duration = note.duration} return t end @@ -171,23 +188,19 @@ function Util.CopyTable(t) return new_t end -function Util.IsNoteSelected(note) - local idx = note.idx - +function Util.IsNoteAtCellSelected(cx, cy) for i, v in ipairs(App.note_list_selected) do - if v.idx == idx then + if (cx >= v.offset) and (cx < v.offset + v.duration) and (cy == v.string_idx) then return true end - end + end return false end -function Util.IsNoteAtCellSelected(cx, cy) - for i, v in ipairs(App.note_list_selected) do - if (cx >= v.offset) and (cx < v.offset + v.duration) and (cy == v.string_idx) then - return true - end +function Util.IsNoteSelected(note_idx) + for i, v in ipairs(App.note_list_selected.indices) do + if v == note_idx then return true; end end return false @@ -199,21 +212,16 @@ function Util.GetNoteIndexAtCell(cx, cy) return i end end - + msg("not found") return 0 -- Not found end +-- Refreshes note_list_selected with the (presumably) modified notes from note_list, after mouse release function Util.UpdateSelectedNotes() if #App.note_list == 0 or #App.note_list_selected == 0 then return; end for i, v in ipairs(App.note_list_selected) do - App.note_list_selected[i] = Util.CopyNote(App.note_list[App.note_list_selected[i].idx]) - end -end - -function Util.RecalculateStoredNoteIndices() - if #App.note_list == 0 then return; end - for i, v in ipairs(App.note_list) do - v.idx = i + local idx = App.note_list_selected.indices[i] + App.note_list_selected[i] = Util.CopyNote(App.note_list[idx]) end end From 9d2e2765ba732a17b827ef001f712474a659579b Mon Sep 17 00:00:00 2001 From: immortalx74 Date: Tue, 31 May 2022 10:29:42 +0300 Subject: [PATCH 05/12] Fix: Prevent re-selecting an already selected note --- Debug.lua | 2 +- Editor.lua | 12 +++++------- Reaffer.lua | 2 -- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Debug.lua b/Debug.lua index e3fb92f..06fc5e2 100644 --- a/Debug.lua +++ b/Debug.lua @@ -1,4 +1,4 @@ -Debug = {enabled = false} +Debug = {enabled = true} function Debug.ShowContainers() local opt = {"Insert", "Delete", "Pitch + dur", "Vel", "Move"} diff --git a/Editor.lua b/Editor.lua index 62c79ac..c3e5c4a 100644 --- a/Editor.lua +++ b/Editor.lua @@ -79,17 +79,15 @@ end function Editor.SelectNotes(cx, cy) for i, note in ipairs(App.note_list) do - if (cx >= note.offset) and (cx < note.offset + note.duration) and (cy == note.string_idx) then + if (cx >= note.offset) and (cx < note.offset + note.duration) and (cy == note.string_idx) and not (Util.IsNoteAtCellSelected(note.offset, note.string_idx)) then if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) then App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i else - if not (Util.IsNoteAtCellSelected(note.offset, note.string_idx)) then - Util.ClearTable(App.note_list_selected) - Util.ClearTable(App.note_list_selected.indices) - App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) - App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i - end + Util.ClearTable(App.note_list_selected) + Util.ClearTable(App.note_list_selected.indices) + App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) + App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i end App.last_note_clicked = Util.CopyNote(note) App.last_note_clicked.idx = i diff --git a/Reaffer.lua b/Reaffer.lua index 3b7f89f..eb11e4e 100644 --- a/Reaffer.lua +++ b/Reaffer.lua @@ -18,8 +18,6 @@ -- There's currently no way to change tuning. Not an easy way to do that. -- What happens to the notes that fall outside of a string's range, -- when you change tuning during a session? Needs thinking --- --- BUG: When releasing and pressing again Ctrl, and clicking an already selected note, it gets re-added to note_list_selected. function msg(txt) reaper.ShowConsoleMsg(tostring(txt) .. "\n") From 3b87ef3b7bb0086a8599842e3c6b38bccc4d04ab Mon Sep 17 00:00:00 2001 From: immortalx74 Date: Tue, 31 May 2022 11:04:58 +0300 Subject: [PATCH 06/12] Preview notes on paste --- Constants.lua | 1 + Reaffer.lua | 2 ++ UI.lua | 21 ++++++++++++++++----- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Constants.lua b/Constants.lua index 99ef39d..3cb904e 100644 --- a/Constants.lua +++ b/Constants.lua @@ -51,6 +51,7 @@ Colors = bg = 0x0F0F0FFF, active_tool = 0x3D85E0FF, note_preview = 0xFFFFFF88, + note_preview_paste = 0xAAAAAA55, red = 0xFF0000FF, text = 0xFFFFFFFF } \ No newline at end of file diff --git a/Reaffer.lua b/Reaffer.lua index eb11e4e..24cea16 100644 --- a/Reaffer.lua +++ b/Reaffer.lua @@ -18,6 +18,8 @@ -- There's currently no way to change tuning. Not an easy way to do that. -- What happens to the notes that fall outside of a string's range, -- when you change tuning during a session? Needs thinking +-- +-- When pasting notes, clear current selection and select pasted notes function msg(txt) reaper.ShowConsoleMsg(tostring(txt) .. "\n") diff --git a/UI.lua b/UI.lua index 1dcd101..50e923c 100644 --- a/UI.lua +++ b/UI.lua @@ -245,15 +245,26 @@ function UI.Render_Editor() if App.mouse_x > rect_x1 and App.mouse_x < rect_x2 and App.mouse_y > rect_y1 and App.mouse_y < rect_y2 then if Util.IsCellEmpty(cell_x, cell_y, true) then - local preview_x = App.editor_win_x + App.left_margin + (cell_x * App.note_w) - App.scroll_x - local preview_y = App.editor_win_y + App.top_margin + (cell_y * App.note_h) - 5 - reaper.ImGui_DrawList_AddRectFilled(draw_list, preview_x, preview_y, preview_x + App.note_w - 1, preview_y + App.note_h - 1, Colors.note_preview, 40) - if App.attempts_paste then - reaper.ImGui_DrawList_AddLine(draw_list, preview_x, App.editor_win_y + App.top_margin, preview_x, App.editor_win_y + App.top_margin + ((App.num_strings - 1) * App.lane_v_spacing), Colors.red) + -- reaper.ImGui_DrawList_AddLine(draw_list, preview_x, App.editor_win_y + App.top_margin, preview_x, App.editor_win_y + App.top_margin + ((App.num_strings - 1) * App.lane_v_spacing), Colors.red) + local leftmost = Util.NumGridDivisions() + for i, v in ipairs(Clipboard.note_list) do + if v.offset < leftmost then leftmost = v.offset; end + end + + for i, v in ipairs(Clipboard.note_list) do + local cur_x = App.editor_win_x + App.left_margin + ((v.offset + cell_x - leftmost) * App.note_w) - App.scroll_x + local cur_y = App.editor_win_y + App.top_margin + (v.string_idx * App.note_h) - 5 + reaper.ImGui_DrawList_AddRectFilled(draw_list, cur_x, cur_y, cur_x + (v.duration * App.note_w) - 1, cur_y + App.note_h - 1, Colors.note_preview_paste, 40) + end + reaper.ImGui_BeginTooltip(App.ctx) reaper.ImGui_Text(App.ctx, "Select position to paste. [ESC] to cancel") reaper.ImGui_EndTooltip(App.ctx) + else + local preview_x = App.editor_win_x + App.left_margin + (cell_x * App.note_w) - App.scroll_x + local preview_y = App.editor_win_y + App.top_margin + (cell_y * App.note_h) - 5 + reaper.ImGui_DrawList_AddRectFilled(draw_list, preview_x, preview_y, preview_x + App.note_w - 1, preview_y + App.note_h - 1, Colors.note_preview, 40) end end if reaper.ImGui_IsMouseClicked(App.ctx, 0) then From d80cb8a54cc4b7324bf4a1b187743e7f13008e83 Mon Sep 17 00:00:00 2001 From: immortalx74 Date: Tue, 31 May 2022 16:31:23 +0300 Subject: [PATCH 07/12] Clipboard paste checks for overlapping notes and outer bounds --- Clipboard.lua | 23 ++++++++++++++--------- UI.lua | 9 +++++---- Util.lua | 2 +- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Clipboard.lua b/Clipboard.lua index c597955..fde55df 100644 --- a/Clipboard.lua +++ b/Clipboard.lua @@ -14,17 +14,17 @@ function Clipboard.Copy() Util.ClearTable(Clipboard.note_list) Clipboard.note_list = Util.CopyTable(App.note_list_selected) + table.sort(Clipboard.note_list, function (k1, k2) return k1.offset < k2.offset; end) return true end --- NOTE: WIP. Doesn't check for overlapping notes, out of bounds of measures, etc function Clipboard.Paste(cx, cy) local temp = {} for i, v in ipairs(Clipboard.note_list) do temp[#temp + 1] = Util.CopyNote(v) end - - table.sort(temp, function (k1, k2) return k1.offset < k2.offset; end) + + local right_bound = Util.NumGridDivisions() local src_offset = temp[1].offset local diff = 0 local dst_offset = 0 @@ -32,18 +32,23 @@ function Clipboard.Paste(cx, cy) for i, v in ipairs(temp) do diff = v.offset - src_offset dst_offset = cx + diff - - if Util.IsCellEmpty(dst_offset, v.string_idx, true) then - v.offset = dst_offset - else + v.offset = dst_offset + + if v.offset + v.duration > right_bound then return end + + for j, w in ipairs(App.note_list) do + if (v.string_idx == w.string_idx) and (Util.RangeOverlap(v.offset, v.offset + v.duration - 1, w.offset, w.offset + w.duration - 1)) then + return + end + end end - + for i, v in ipairs(temp) do App.note_list[#App.note_list + 1] = Util.CopyNote(v) end - + UR.PushUndo(e_OpType.Insert, temp) Util.ClearTable(App.note_list_selected) App.attempts_paste = false diff --git a/UI.lua b/UI.lua index 50e923c..56f1e4e 100644 --- a/UI.lua +++ b/UI.lua @@ -247,10 +247,11 @@ function UI.Render_Editor() if Util.IsCellEmpty(cell_x, cell_y, true) then if App.attempts_paste then -- reaper.ImGui_DrawList_AddLine(draw_list, preview_x, App.editor_win_y + App.top_margin, preview_x, App.editor_win_y + App.top_margin + ((App.num_strings - 1) * App.lane_v_spacing), Colors.red) - local leftmost = Util.NumGridDivisions() - for i, v in ipairs(Clipboard.note_list) do - if v.offset < leftmost then leftmost = v.offset; end - end + -- local leftmost = Util.NumGridDivisions() + -- for i, v in ipairs(Clipboard.note_list) do + -- if v.offset < leftmost then leftmost = v.offset; end + -- end + local leftmost = Clipboard.note_list[1].offset for i, v in ipairs(Clipboard.note_list) do local cur_x = App.editor_win_x + App.left_margin + ((v.offset + cell_x - leftmost) * App.note_w) - App.scroll_x diff --git a/Util.lua b/Util.lua index 87ec48c..57a1de1 100644 --- a/Util.lua +++ b/Util.lua @@ -135,7 +135,7 @@ function Util.IsNewPositionOnStringEmpty(note_idx, new_x, y) end end end - + return true end From de40aa6a334265180d603635f6aae5072630d234 Mon Sep 17 00:00:00 2001 From: immortalx74 Date: Tue, 31 May 2022 17:28:49 +0300 Subject: [PATCH 08/12] Fix: Broken multi-selection --- Editor.lua | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Editor.lua b/Editor.lua index c3e5c4a..bc2ce83 100644 --- a/Editor.lua +++ b/Editor.lua @@ -79,15 +79,19 @@ end function Editor.SelectNotes(cx, cy) for i, note in ipairs(App.note_list) do - if (cx >= note.offset) and (cx < note.offset + note.duration) and (cy == note.string_idx) and not (Util.IsNoteAtCellSelected(note.offset, note.string_idx)) then + if (cx >= note.offset) and (cx < note.offset + note.duration) and (cy == note.string_idx) then if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) then - App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) - App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i + if not (Util.IsNoteAtCellSelected(note.offset, note.string_idx)) then + App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) + App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i + end else - Util.ClearTable(App.note_list_selected) - Util.ClearTable(App.note_list_selected.indices) - App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) - App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i + if not (Util.IsNoteAtCellSelected(note.offset, note.string_idx)) then + Util.ClearTable(App.note_list_selected) + Util.ClearTable(App.note_list_selected.indices) + App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) + App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i + end end App.last_note_clicked = Util.CopyNote(note) App.last_note_clicked.idx = i From 1120e278ba40be02ffbc1688aa20a5f0529ed51f Mon Sep 17 00:00:00 2001 From: immortalx74 Date: Tue, 31 May 2022 20:55:34 +0300 Subject: [PATCH 09/12] Fix: Prevent redundant Undo entries, when mouse hasn't moved to a new cell. NOTE: Still doesn't prevent entries when the mouse has moved in and out of the cell, but no actual property change has happened. --- App.lua | 2 +- Constants.lua | 11 ++-- Debug.lua | 4 +- Editor.lua | 139 +++++++++++++++++++++++++++----------------------- 4 files changed, 85 insertions(+), 71 deletions(-) diff --git a/App.lua b/App.lua index d006328..993ac4d 100644 --- a/App.lua +++ b/App.lua @@ -47,7 +47,7 @@ App = wheel_delta = 50, active_tool = e_Tool.Select, num_strings = 8, - num_measures = 1, + num_measures = 4, quantize_cur_idx = 5, signature_cur_idx = 3, note_display_cur_idx = e_NoteDisplay.Pitch, diff --git a/Constants.lua b/Constants.lua index 3cb904e..4c7bfb4 100644 --- a/Constants.lua +++ b/Constants.lua @@ -37,11 +37,12 @@ e_MouseButton = e_OpType = { - Insert = 1, - Delete = 2, - ModifyPitchAndDuration = 3, - ModifyVelocityAndOffVelocity = 4, - Move = 5 + NoOp = 1, + Insert = 2, + Delete = 3, + ModifyPitchAndDuration = 4, + ModifyVelocityAndOffVelocity = 5, + Move = 6 -- more here... } diff --git a/Debug.lua b/Debug.lua index 06fc5e2..0ca2d13 100644 --- a/Debug.lua +++ b/Debug.lua @@ -1,7 +1,7 @@ -Debug = {enabled = true} +Debug = {enabled = false} function Debug.ShowContainers() - local opt = {"Insert", "Delete", "Pitch + dur", "Vel", "Move"} + local opt = {"NoOp", "Insert", "Delete", "Pitch + dur", "Vel", "Move"} if reaper.ImGui_BeginListBox(App.ctx, "Undo stack", 300, 200 ) then if #UR.undo_stack > 0 then for i, v in ipairs(UR.undo_stack) do diff --git a/Editor.lua b/Editor.lua index bc2ce83..1b66081 100644 --- a/Editor.lua +++ b/Editor.lua @@ -52,6 +52,7 @@ function Editor.OnMouseButtonRelease(mbutton) App.is_new_note = false Util.UpdateSelectedNotes() + UR.last_op = e_OpType.NoOp end end end @@ -138,86 +139,98 @@ function Editor.EraseNotes(cx, cy) end function Editor.MoveNotes(cx, cy, dx, dy) - local all_fit = true - local base_diff = dx + App.last_note_clicked.duration - 1 - local leftmost = Util.NumGridDivisions(); local topmost = App.num_strings - 1; local rightmost = 0; local bottommost = 0 - - for i, v in ipairs(App.note_list_selected) do - local idx = App.note_list_selected.indices[i] - if App.note_list[idx].offset < leftmost then leftmost = App.note_list[idx].offset; end - if App.note_list[idx].offset + App.note_list[idx].duration - 1 > rightmost then rightmost = App.note_list[idx].offset + App.note_list[idx].duration - 1; end - if App.note_list[idx].string_idx < topmost then topmost = App.note_list[idx].string_idx; end - if App.note_list[idx].string_idx > bottommost then bottommost = App.note_list[idx].string_idx; end + if dx ~= 0 or dy ~= 0 then + local all_fit = true + local base_diff = dx + App.last_note_clicked.duration - 1 + local leftmost = Util.NumGridDivisions(); local topmost = App.num_strings - 1; local rightmost = 0; local bottommost = 0 - if not Util.IsNewPositionOnStringEmpty(App.note_list_selected.indices[i], v.offset + base_diff, v.string_idx - dy) then - all_fit = false - break - end - end - - local base_x = App.note_list[App.last_note_clicked.idx].offset - local base_y = App.note_list[App.last_note_clicked.idx].string_idx - local l_bound = base_x - leftmost - local r_bound = Util.NumGridDivisions() - rightmost + base_x - local t_bound = base_y - topmost - local b_bound = App.num_strings - bottommost + base_y - - if all_fit then - if cx >= l_bound and cx < r_bound and cy >= t_bound and cy < b_bound then - for i, v in ipairs(App.note_list_selected) do - local idx = App.note_list_selected.indices[i] - App.note_list[idx].offset = v.offset + dx + App.last_note_clicked.duration - 1 - - local dst_string_idx = Util.Clamp(v.string_idx - dy, 0, App.num_strings - 1) - Util.ShiftOctaveIfOutsideRange(App.note_list[idx], dst_string_idx) - App.note_list[idx].string_idx = dst_string_idx + for i, v in ipairs(App.note_list_selected) do + local idx = App.note_list_selected.indices[i] + if App.note_list[idx].offset < leftmost then leftmost = App.note_list[idx].offset; end + if App.note_list[idx].offset + App.note_list[idx].duration - 1 > rightmost then rightmost = App.note_list[idx].offset + App.note_list[idx].duration - 1; end + if App.note_list[idx].string_idx < topmost then topmost = App.note_list[idx].string_idx; end + if App.note_list[idx].string_idx > bottommost then bottommost = App.note_list[idx].string_idx; end + + if not Util.IsNewPositionOnStringEmpty(App.note_list_selected.indices[i], v.offset + base_diff, v.string_idx - dy) then + all_fit = false + break end end - UR.last_op = e_OpType.Move + local base_x = App.note_list[App.last_note_clicked.idx].offset + local base_y = App.note_list[App.last_note_clicked.idx].string_idx + local l_bound = base_x - leftmost + local r_bound = Util.NumGridDivisions() - rightmost + base_x + local t_bound = base_y - topmost + local b_bound = App.num_strings - bottommost + base_y + + if all_fit then + if cx >= l_bound and cx < r_bound and cy >= t_bound and cy < b_bound then + for i, v in ipairs(App.note_list_selected) do + local idx = App.note_list_selected.indices[i] + App.note_list[idx].offset = v.offset + dx + App.last_note_clicked.duration - 1 + + local dst_string_idx = Util.Clamp(v.string_idx - dy, 0, App.num_strings - 1) + Util.ShiftOctaveIfOutsideRange(App.note_list[idx], dst_string_idx) + App.note_list[idx].string_idx = dst_string_idx + end + end + + UR.last_op = e_OpType.Move + end + else + UR.last_op = e_OpType.NoOp end end function Editor.ModifyPitchAndDuration(cx, cy, dx, dy) - for i, v in ipairs(App.note_list_selected) do + if dx ~= 0 or dy ~= 0 then + for i, v in ipairs(App.note_list_selected) do + + -- duration + local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) + + local nearest = Util.GetCellNearestOccupied(App.note_list[idx].offset, App.note_list[idx].string_idx, e_Direction.Right) + if cx >= App.last_note_clicked.offset then + App.note_list[idx].duration = Util.Clamp(v.duration + dx, 1, nearest - App.note_list[idx].offset) + end + -- pitch + local pitch_min = App.instrument[App.num_strings - 3].open[App.num_strings - App.note_list[idx].string_idx] + local pitch_max = pitch_min + 24 + App.note_list[idx].pitch = Util.Clamp(v.pitch + dy, pitch_min, pitch_max) + Util.UpdateRecentPitch(App.num_strings - v.string_idx, App.note_list[idx].pitch) + end - -- duration - local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) + local idx = Util.GetNoteIndexAtCell(App.last_note_clicked.offset, App.last_note_clicked.string_idx) - local nearest = Util.GetCellNearestOccupied(App.note_list[idx].offset, App.note_list[idx].string_idx, e_Direction.Right) - if cx >= App.last_note_clicked.offset then - App.note_list[idx].duration = Util.Clamp(v.duration + dx, 1, nearest - App.note_list[idx].offset) + if App.current_pitch ~= App.note_list[idx].pitch then + Editor.StopNote() + App.current_pitch = App.note_list[idx].pitch + Editor.PlayNote() end - -- pitch - local pitch_min = App.instrument[App.num_strings - 3].open[App.num_strings - App.note_list[idx].string_idx] - local pitch_max = pitch_min + 24 - App.note_list[idx].pitch = Util.Clamp(v.pitch + dy, pitch_min, pitch_max) - Util.UpdateRecentPitch(App.num_strings - v.string_idx, App.note_list[idx].pitch) - end - - UR.last_op = e_OpType.ModifyPitchAndDuration - - local idx = Util.GetNoteIndexAtCell(App.last_note_clicked.offset, App.last_note_clicked.string_idx) - - if App.current_pitch ~= App.note_list[idx].pitch then - Editor.StopNote() - App.current_pitch = App.note_list[idx].pitch - Editor.PlayNote() + + UR.last_op = e_OpType.ModifyPitchAndDuration + else + UR.last_op = e_OpType.NoOp end end function Editor.ModifyVelocityAndOffVelocity(cx, cy, dx, dy) - for i, v in ipairs(App.note_list_selected) do - local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) - - if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModShift()) then - App.note_list[idx].off_velocity = Util.Clamp(v.off_velocity + dy, 0, 127) - else - App.note_list[idx].velocity = Util.Clamp(v.velocity + dy, 0, 127) + if dy ~= 0 then + for i, v in ipairs(App.note_list_selected) do + local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) + + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModShift()) then + App.note_list[idx].off_velocity = Util.Clamp(v.off_velocity + dy, 0, 127) + else + App.note_list[idx].velocity = Util.Clamp(v.velocity + dy, 0, 127) + end end + + UR.last_op = e_OpType.ModifyVelocityAndOffVelocity + else + UR.last_op = e_OpType.NoOp end - - UR.last_op = e_OpType.ModifyVelocityAndOffVelocity end function Editor.PlayNote() From c9e7104401f31dddbda5d7b7e1e357845fb28030 Mon Sep 17 00:00:00 2001 From: immortalx74 Date: Thu, 2 Jun 2022 05:29:31 +0300 Subject: [PATCH 10/12] Marquee select, updated help text --- App.lua | 44 ++++++++------------------------------- Clipboard.lua | 8 ++++++-- Constants.lua | 3 ++- Debug.lua | 2 +- Editor.lua | 57 ++++++++++++++++++++++++++++++++++++++++++++++++--- Input.lua | 39 +++++++++++++++++++++++++++++++++++ Reaffer.lua | 8 ++------ UI.lua | 46 ++++++++++++++++++++++++++++++++--------- 8 files changed, 149 insertions(+), 58 deletions(-) create mode 100644 Input.lua diff --git a/App.lua b/App.lua index 993ac4d..9d38a6d 100644 --- a/App.lua +++ b/App.lua @@ -19,6 +19,7 @@ App = is_new_note = false, last_click_was_inside_editor = false, attempts_paste = false, + begin_marquee = false, -- metrics window_w = 800, @@ -45,8 +46,10 @@ App = -- defaults wheel_delta = 50, + scroll_margin = 50, + scroll_speed = 0.25, active_tool = e_Tool.Select, - num_strings = 8, + num_strings = 6, num_measures = 4, quantize_cur_idx = 5, signature_cur_idx = 3, @@ -93,7 +96,8 @@ App = -- Also, stores an 'idx' field last_note_clicked, - note_list_selected = { indices = {} } + note_list_selected = { indices = {} }, + marquee_box = {x1 = 0, y1 = 0, x2 = 0, y2 = 0} } function App.Init() @@ -113,38 +117,8 @@ function App.Loop() App.mouse_x, App.mouse_y = reaper.ImGui_GetMousePos(App.ctx) App.window_w = reaper.ImGui_GetWindowWidth(App.ctx) - if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and not reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModShift()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then - UR.PopUndo() - end - if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModShift()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then - UR.PopRedo() - end - if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_C()) then - Clipboard.Copy() - end - if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_X()) then - Clipboard.Cut() - end - if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_V()) then - if #Clipboard.note_list > 0 then App.attempts_paste = true; end - end - - if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_S()) then - App.active_tool = e_Tool.Select - end - if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_D()) then - App.active_tool = e_Tool.Draw - end - if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_E()) then - App.active_tool = e_Tool.Erase - end - if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_W()) then - App.active_tool = e_Tool.Move - end - if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Escape()) then - App.attempts_paste = false - end - + Input.GetShortcuts() + UI.Render_CB_Strings() UI.Render_CB_Signature() UI.Render_CB_Quantize() @@ -154,7 +128,7 @@ function App.Loop() UI.Render_TXT_Help() UI.Render_Editor() UI.Render_Toolbar() - if Debug.enabled then Debug.ShowContainers(); end + if Debug.enabled then Debug.ShowInfo(); end App.mouse_prev_x, App.mouse_prev_y = App.mouse_x, App.mouse_y reaper.ImGui_End(App.ctx) diff --git a/Clipboard.lua b/Clipboard.lua index fde55df..c515b12 100644 --- a/Clipboard.lua +++ b/Clipboard.lua @@ -33,7 +33,7 @@ function Clipboard.Paste(cx, cy) diff = v.offset - src_offset dst_offset = cx + diff v.offset = dst_offset - + if v.offset + v.duration > right_bound then return end @@ -45,11 +45,15 @@ function Clipboard.Paste(cx, cy) end end + Util.ClearTable(App.note_list_selected) + Util.ClearTable(App.note_list_selected.indices) + for i, v in ipairs(temp) do App.note_list[#App.note_list + 1] = Util.CopyNote(v) + App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(v) + App.note_list_selected.indices[#App.note_list_selected.indices + 1] = #App.note_list end UR.PushUndo(e_OpType.Insert, temp) - Util.ClearTable(App.note_list_selected) App.attempts_paste = false end \ No newline at end of file diff --git a/Constants.lua b/Constants.lua index 4c7bfb4..100e1bb 100644 --- a/Constants.lua +++ b/Constants.lua @@ -54,5 +54,6 @@ Colors = note_preview = 0xFFFFFF88, note_preview_paste = 0xAAAAAA55, red = 0xFF0000FF, - text = 0xFFFFFFFF + text = 0xFFFFFFFF, + marquee_box = 0xFFFFFF44 } \ No newline at end of file diff --git a/Debug.lua b/Debug.lua index 0ca2d13..01c0c03 100644 --- a/Debug.lua +++ b/Debug.lua @@ -1,6 +1,6 @@ Debug = {enabled = false} -function Debug.ShowContainers() +function Debug.ShowInfo() local opt = {"NoOp", "Insert", "Delete", "Pitch + dur", "Vel", "Move"} if reaper.ImGui_BeginListBox(App.ctx, "Undo stack", 300, 200 ) then if #UR.undo_stack > 0 then diff --git a/Editor.lua b/Editor.lua index 1b66081..badcaf9 100644 --- a/Editor.lua +++ b/Editor.lua @@ -32,6 +32,8 @@ function Editor.OnMouseButtonClick(mbutton, cx, cy) end function Editor.OnMouseButtonRelease(mbutton) + App.begin_marquee = false + if App.last_click_was_inside_editor then App.can_init_drag = false App.last_click_was_inside_editor = false @@ -58,12 +60,37 @@ function Editor.OnMouseButtonRelease(mbutton) end function Editor.OnMouseButtonDrag(mbutton) - if App.last_note_clicked == nil then return; end - App.can_init_drag = false + if App.last_click_was_inside_editor then + if App.mouse_x > App.editor_win_x + App.window_w - App.scroll_margin then + App.scroll_x = App.scroll_x + ((App.mouse_x - (App.editor_win_x + App.window_w - App.scroll_margin)) * App.scroll_speed) + reaper.ImGui_SetScrollX(App.ctx, App.scroll_x) + end + + if App.mouse_x < App.editor_win_x + App.left_margin + App.scroll_margin then + App.scroll_x = App.scroll_x - ((App.editor_win_x + App.left_margin + App.scroll_margin - App.mouse_x) * App.scroll_speed) + reaper.ImGui_SetScrollX(App.ctx, App.scroll_x) + end + end - -- cx/cy = current pos, dx/dy = difference from initial pos local cx = Util.GetCellX() local cy = Util.GetCellY() + + if mbutton == e_MouseButton.Left and App.active_tool == e_Tool.Select then + if not (App.begin_marquee) and App.last_note_clicked == nil then + App.marquee_box.x1 = App.mouse_x + App.marquee_box.y1 = App.mouse_y + App.begin_marquee = true + end + + if App.begin_marquee then + Editor.MarqueeSelectNotes(cx, cy) + end + end + + if App.last_note_clicked == nil then return; end + App.can_init_drag = false + + -- dx/dy = difference from initial pos local dx = cx - App.last_note_clicked.duration - App.last_note_clicked.offset + 1 local dy = App.last_note_clicked.string_idx - cy @@ -78,6 +105,30 @@ function Editor.OnMouseButtonDrag(mbutton) end end +function Editor.MarqueeSelectNotes(cx, cy) + Util.ClearTable(App.note_list_selected) + Util.ClearTable(App.note_list_selected.indices) + + local start_x = math.min(App.marquee_box.x1, App.marquee_box.x2) + local start_y = math.min(App.marquee_box.y1, App.marquee_box.y2) + local end_x = math.max(App.marquee_box.x1, App.marquee_box.x2) + local end_y = math.max(App.marquee_box.y1, App.marquee_box.y2) + + local cell_x_min = math.floor((start_x - App.editor_win_x + App.scroll_x -15) / App.note_w) - 1 + local cell_y_min = math.floor((start_y - App.editor_win_y - App.top_margin + 5) / App.note_h) + local cell_x_max = math.floor((end_x - App.editor_win_x + App.scroll_x -15) / App.note_w) - 1 + local cell_y_max = math.floor((end_y - App.editor_win_y - App.top_margin + 5) / App.note_h) + + for i, v in ipairs(App.note_list) do + if not (Util.IsNoteAtCellSelected(v.offset, v.string_idx)) then + if Util.RangeOverlap(v.offset, v.offset + v.duration - 1, cell_x_min, cell_x_max) and Util.RangeOverlap(v.string_idx, v.string_idx, cell_y_min, cell_y_max) then + App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(v) + App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i + end + end + end +end + function Editor.SelectNotes(cx, cy) for i, note in ipairs(App.note_list) do if (cx >= note.offset) and (cx < note.offset + note.duration) and (cy == note.string_idx) then diff --git a/Input.lua b/Input.lua new file mode 100644 index 0000000..2ab9788 --- /dev/null +++ b/Input.lua @@ -0,0 +1,39 @@ +Input = {} + +function Input.GetShortcuts() + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and not reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModShift()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then + UR.PopUndo() + end + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModShift()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then + UR.PopRedo() + end + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_C()) then + Clipboard.Copy() + end + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_X()) then + Clipboard.Cut() + end + if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Key_ModCtrl()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_V()) then + if #Clipboard.note_list > 0 then App.attempts_paste = true; end + end + + if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_F1()) then + Debug.enabled = not Debug.enabled + end + + if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_S()) then + App.active_tool = e_Tool.Select + end + if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_D()) then + App.active_tool = e_Tool.Draw + end + if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_E()) then + App.active_tool = e_Tool.Erase + end + if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_W()) then + App.active_tool = e_Tool.Move + end + if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Escape()) then + App.attempts_paste = false + end +end \ No newline at end of file diff --git a/Reaffer.lua b/Reaffer.lua index 24cea16..e284649 100644 --- a/Reaffer.lua +++ b/Reaffer.lua @@ -6,11 +6,6 @@ -- When reducing measure count, keep all notes internally (even those on a higher measure count) and if necessary, -- shorten notes duration (ONLY visually) which happen to start on a valid measure, but their duration extends beyond the measure's boundary. -- --- Allow multiple selection with marquee. Don't mimic riffer exact behavior. (it always sets leftmost note as the "active" one?) --- Also, auto-scroll when marquee is close to editor edges --- --- Clamp moving notes. They currently can end out of the editor boundaries and can overlap each other. --- -- Current dragging system allows each note's properties to be clamped individually -- Riffer works differently. It clamps all notes to the lowest possible change of that particular property (pitch, position, etc.) -- Sometimes the first behavior is desirable though. Maybe have a setting for this? @@ -19,7 +14,7 @@ -- What happens to the notes that fall outside of a string's range, -- when you change tuning during a session? Needs thinking -- --- When pasting notes, clear current selection and select pasted notes +-- Generated MIDI item is just a test. No reason to develop it further until articulations are implemented. function msg(txt) reaper.ShowConsoleMsg(tostring(txt) .. "\n") @@ -34,6 +29,7 @@ dofile(script_path .. "Util.lua") dofile(script_path .. "Editor.lua") dofile(script_path .. "UndoRedo.lua") dofile(script_path .. "Clipboard.lua") +dofile(script_path .. "Input.lua") dofile(script_path .. "Debug.lua") App.Init() diff --git a/UI.lua b/UI.lua index 56f1e4e..dbde489 100644 --- a/UI.lua +++ b/UI.lua @@ -122,9 +122,29 @@ function UI.Render_TXT_Help() if reaper.ImGui_IsItemHovered(App.ctx) then reaper.ImGui_BeginTooltip(App.ctx) + reaper.ImGui_Text(App.ctx, - "Help text,\n" .. - "Put quick help here...") + "Shortcuts:\n" .. + "Select tool (S)\n" .. + "Move tool (W)\n" .. + "Draw tool (D)\n" .. + "Erase tool (E)\n" .. + "Undo (Ctrl + Z)\n" .. + "Redo (Ctrl + Shift + Z)\n" .. + "Cut (Ctrl + X)\n" .. + "Copy (Ctrl + C)\n" .. + "Paste (Ctrl + V)\n\n" .. + "Usage:\n" .. + "Left Click with the Draw tool to insert a note.\n" .. + "Left Click + Drag horizontally to set duration \n" .. + "Left Click + Drag vertically to set pitch\n" .. + "Right Click + Drag vertically to set velocity\n" .. + "Right Click + Shift + Drag to set off-velocity\n" .. + "Left Click + Ctrl to select multiple notes\n" .. + "The same actions can be performed with the Select tool, when clicking on existing notes.\n" .. + "Click + Drag in an empty area with the Select tool to marquee-select notes.\n\n" .. + "Click on the Create MIDI button to generate a MIDI item on the selected track, at cursor position (WIP)") + reaper.ImGui_EndTooltip(App.ctx) end end @@ -247,10 +267,6 @@ function UI.Render_Editor() if Util.IsCellEmpty(cell_x, cell_y, true) then if App.attempts_paste then -- reaper.ImGui_DrawList_AddLine(draw_list, preview_x, App.editor_win_y + App.top_margin, preview_x, App.editor_win_y + App.top_margin + ((App.num_strings - 1) * App.lane_v_spacing), Colors.red) - -- local leftmost = Util.NumGridDivisions() - -- for i, v in ipairs(Clipboard.note_list) do - -- if v.offset < leftmost then leftmost = v.offset; end - -- end local leftmost = Clipboard.note_list[1].offset for i, v in ipairs(Clipboard.note_list) do @@ -258,14 +274,15 @@ function UI.Render_Editor() local cur_y = App.editor_win_y + App.top_margin + (v.string_idx * App.note_h) - 5 reaper.ImGui_DrawList_AddRectFilled(draw_list, cur_x, cur_y, cur_x + (v.duration * App.note_w) - 1, cur_y + App.note_h - 1, Colors.note_preview_paste, 40) end - reaper.ImGui_BeginTooltip(App.ctx) reaper.ImGui_Text(App.ctx, "Select position to paste. [ESC] to cancel") reaper.ImGui_EndTooltip(App.ctx) else - local preview_x = App.editor_win_x + App.left_margin + (cell_x * App.note_w) - App.scroll_x - local preview_y = App.editor_win_y + App.top_margin + (cell_y * App.note_h) - 5 - reaper.ImGui_DrawList_AddRectFilled(draw_list, preview_x, preview_y, preview_x + App.note_w - 1, preview_y + App.note_h - 1, Colors.note_preview, 40) + if not (App.begin_marquee) then + local preview_x = App.editor_win_x + App.left_margin + (cell_x * App.note_w) - App.scroll_x + local preview_y = App.editor_win_y + App.top_margin + (cell_y * App.note_h) - 5 + reaper.ImGui_DrawList_AddRectFilled(draw_list, preview_x, preview_y, preview_x + App.note_w - 1, preview_y + App.note_h - 1, Colors.note_preview, 40) + end end end if reaper.ImGui_IsMouseClicked(App.ctx, 0) then @@ -306,6 +323,15 @@ function UI.Render_Editor() reaper.ImGui_DrawList_AddText(draw_list, App.editor_win_x + 8, App.editor_win_y + 23 + (i * App.lane_v_spacing), Colors.text, str .. space .. "*") end + -- Marquee box + if App.begin_marquee then + App.marquee_box.x2 = App.mouse_x + App.marquee_box.y2 = App.mouse_y + + reaper.ImGui_DrawList_AddRectFilled(draw_list, App.marquee_box.x1, App.marquee_box.y1, App.marquee_box.x2, App.marquee_box.y2, Colors.marquee_box) + -- msg(App.marquee_box.x1 .. " , " .. App.marquee_box.y1 .. ", " .. App.marquee_box.x2 .. " , " .. App.marquee_box.y2) + end + reaper.ImGui_EndChild(App.ctx) end end \ No newline at end of file From 806377fad50065921af2c7d392e0602ff9c08ffa Mon Sep 17 00:00:00 2001 From: immortalx74 Date: Fri, 3 Jun 2022 04:46:55 +0300 Subject: [PATCH 11/12] Fix: Auto-scroll when clicking outside lanes area. Fix: Marquee triggering on window move. --- Editor.lua | 26 +++++++++++++------------- UI.lua | 1 + 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Editor.lua b/Editor.lua index badcaf9..fa32985 100644 --- a/Editor.lua +++ b/Editor.lua @@ -60,6 +60,9 @@ function Editor.OnMouseButtonRelease(mbutton) end function Editor.OnMouseButtonDrag(mbutton) + local cx = Util.GetCellX() + local cy = Util.GetCellY() + if App.last_click_was_inside_editor then if App.mouse_x > App.editor_win_x + App.window_w - App.scroll_margin then App.scroll_x = App.scroll_x + ((App.mouse_x - (App.editor_win_x + App.window_w - App.scroll_margin)) * App.scroll_speed) @@ -70,20 +73,17 @@ function Editor.OnMouseButtonDrag(mbutton) App.scroll_x = App.scroll_x - ((App.editor_win_x + App.left_margin + App.scroll_margin - App.mouse_x) * App.scroll_speed) reaper.ImGui_SetScrollX(App.ctx, App.scroll_x) end - end - - local cx = Util.GetCellX() - local cy = Util.GetCellY() - - if mbutton == e_MouseButton.Left and App.active_tool == e_Tool.Select then - if not (App.begin_marquee) and App.last_note_clicked == nil then - App.marquee_box.x1 = App.mouse_x - App.marquee_box.y1 = App.mouse_y - App.begin_marquee = true - end - if App.begin_marquee then - Editor.MarqueeSelectNotes(cx, cy) + if mbutton == e_MouseButton.Left and App.active_tool == e_Tool.Select then + if not (App.begin_marquee) and App.last_note_clicked == nil then + App.marquee_box.x1 = App.mouse_x + App.marquee_box.y1 = App.mouse_y + App.begin_marquee = true + end + + if App.begin_marquee then + Editor.MarqueeSelectNotes(cx, cy) + end end end diff --git a/UI.lua b/UI.lua index dbde489..ec462b1 100644 --- a/UI.lua +++ b/UI.lua @@ -292,6 +292,7 @@ function UI.Render_Editor() Editor.OnMouseButtonClick(e_MouseButton.Right, cell_x, cell_y) end end + if reaper.ImGui_IsMouseClicked(App.ctx, 0) then App.last_click_was_inside_editor = true; end -- NOTE putting this here so that marquee box can auto-scroll even when clicking outside the lanes end -- These have prob have to go out of the drawing function From 1cbbc86bb5a76b3efdeefc8f1fd0c162a3e052a7 Mon Sep 17 00:00:00 2001 From: immortalx74 Date: Fri, 3 Jun 2022 16:13:16 +0300 Subject: [PATCH 12/12] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3d2048d..5554f5e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # Reaffer ReaScript for Reaper, based on Ample Sound's riffer found in their guitar/bass VSTs. +![Reaffer](https://i.imgur.com/cfgMb1X.gif) \ No newline at end of file