Keep the edit cursor when opening an item in grid mode
Opening an item in the MIDI editor parks the edit cursor at the item start, and the open action then made that stick. Listening to a phrase and hitting Alt+Shift+G to write on top of it meant arrowing back to where you already were, one cell at a time, on every entry. The cursor now stays where it was and is only tidied onto the nearest grid line, so the first left/right press moves a whole cell rather than a remainder. The action reads the cursor before the open command runs, since the open command is what moves it. snapCursorToCell rounds in quarter notes, like everything else here, so the landing spot survives a tempo change, and clamps the cell index into the item: the floor is the cell containing the item start, which is where the action used to land, and the ceiling is the last cell that starts inside the item. Clamping the index rather than the time means a cursor parked before an off-grid item start rounds forward into the first whole cell instead of into the partial one, where a note would begin outside the item. The action also re-reads the take from the editor handle now, so the grid size and the spoken position come from the take actually being edited rather than from the selected item's active take. Toggling grid mode on with Alt+G is left alone: it does not move the cursor at all, and cell() floors, so an off-grid cursor still works. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+10
-4
@@ -14,20 +14,26 @@ if not item then G.say("No item selected") return end
|
||||
local take = reaper.GetActiveTake(item)
|
||||
if not take or not reaper.TakeIsMIDI(take) then G.say("Selected item is not MIDI") return end
|
||||
|
||||
-- Opening the editor parks the cursor at the item start; remember where the
|
||||
-- user actually was so it can be handed back.
|
||||
local cur = reaper.GetCursorPosition()
|
||||
|
||||
-- Item: Open in built-in MIDI editor (set default behaviour in preferences)
|
||||
reaper.Main_OnCommand(40153, 0)
|
||||
|
||||
local hwnd = reaper.MIDIEditor_GetActive()
|
||||
if not hwnd then G.say("Could not open MIDI editor") return end
|
||||
|
||||
take = reaper.MIDIEditor_GetTake(hwnd) or take
|
||||
|
||||
G.set("active", 1)
|
||||
|
||||
-- Preview must sound through this track's instrument, whatever else is armed.
|
||||
G.routePreview(reaper.MIDIEditor_GetTake(hwnd) or take)
|
||||
G.routePreview(take)
|
||||
|
||||
-- Start at the item's beginning so the first cell is the item's first cell.
|
||||
local pos = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
|
||||
reaper.SetEditCurPos(pos, true, false)
|
||||
-- Stay where you were listening, on the nearest cell boundary, rather than
|
||||
-- jumping back to the top of the item.
|
||||
G.snapCursorToCell(take, cur)
|
||||
|
||||
local p = G.getPitch(hwnd)
|
||||
if not G.inScale(p) then
|
||||
|
||||
@@ -181,6 +181,10 @@ the editor to toggle back off.
|
||||
the MIDI editor's `active_note_row`. No private cursor state exists, so grid
|
||||
mode and OSARA cannot drift out of sync — you can switch between them
|
||||
mid-phrase.
|
||||
- **Opening keeps your place.** `Alt+Shift+G` leaves the edit cursor where it
|
||||
already was, rounded to the nearest grid line, instead of dropping it back to
|
||||
the start of the item — so you can listen to a bar, hit it, and start writing
|
||||
right there. A cursor outside the item lands on the item's first or last cell.
|
||||
- **Turning grid mode off restores everything.** Each bound key forwards to
|
||||
what it did before, so nothing is permanently taken away.
|
||||
|
||||
|
||||
@@ -229,6 +229,37 @@ function M.gotoCell(take, idx)
|
||||
reaper.SetEditCurPos(t, true, false)
|
||||
end
|
||||
|
||||
--[[
|
||||
Put the cursor on the nearest cell boundary to time t (the edit cursor by
|
||||
default), without leaving the item.
|
||||
|
||||
Entering grid mode uses this instead of jumping to the item start: wherever
|
||||
you were already listening stays where you are, only tidied onto a real cell
|
||||
so the first left/right press moves a whole cell rather than a remainder.
|
||||
|
||||
Returns the cell index landed on.
|
||||
]]
|
||||
function M.snapCursorToCell(take, t)
|
||||
local g = M.gridQN(take)
|
||||
local item = reaper.GetMediaItemTake_Item(take)
|
||||
local lo = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
|
||||
local hi = lo + reaper.GetMediaItemInfo_Value(item, "D_LENGTH")
|
||||
|
||||
t = t or reaper.GetCursorPosition()
|
||||
local idx = math.floor(reaper.TimeMap2_timeToQN(0, t) / g + 0.5)
|
||||
|
||||
-- A cursor outside the item pulls back to the first or last cell the item
|
||||
-- covers. The first is the cell containing the item start, which is where
|
||||
-- plain "open in editor" used to land.
|
||||
local loIdx = math.floor(reaper.TimeMap2_timeToQN(0, lo) / g + 1e-9)
|
||||
local hiIdx = math.ceil(reaper.TimeMap2_timeToQN(0, hi) / g - 1e-9) - 1
|
||||
if hiIdx < loIdx then hiIdx = loIdx end
|
||||
if idx < loIdx then idx = loIdx elseif idx > hiIdx then idx = hiIdx end
|
||||
|
||||
M.gotoCell(take, idx)
|
||||
return idx
|
||||
end
|
||||
|
||||
------------------------------------------------------------- note queries
|
||||
|
||||
-- A cell counts as occupied by a note if the note overlaps it at all, so
|
||||
|
||||
@@ -304,4 +304,39 @@ eq(row, 67, "any back landed nearest the pitch it came from")
|
||||
said = at(6, 69, function() G.runNoteJump(1) end)
|
||||
eq(said, "No later notes", "any forward past the last note")
|
||||
|
||||
--------------------------------------------------- entering grid mode
|
||||
|
||||
-- Opening an item in grid mode keeps the cursor where it already was, rounded
|
||||
-- to the nearest cell, instead of dropping back to the item start.
|
||||
local itemPos, itemLen = 0, 10
|
||||
reaper.GetMediaItemTake_Item = function() return "item" end
|
||||
reaper.GetMediaItemInfo_Value = function(_, k)
|
||||
return k == "D_POSITION" and itemPos or itemLen
|
||||
end
|
||||
|
||||
local function snap(t) return G.snapCursorToCell(take, t) end
|
||||
eq(snap(2.4), 2, "snap rounds back to the nearer cell")
|
||||
eq(snap(2.6), 3, "snap rounds forward to the nearer cell")
|
||||
eq(snap(3), 3, "snap leaves a cursor already on a cell alone")
|
||||
eq(cursor, 3, "snap moves the edit cursor to the cell start")
|
||||
cursor = 6.7
|
||||
eq(snap(nil), 7, "snap defaults to the edit cursor")
|
||||
|
||||
-- A cursor outside the item lands on the item's first or last cell.
|
||||
itemPos, itemLen = 2, 4 -- the item spans cells 2 to 5
|
||||
eq(snap(0), 2, "snap from before the item lands on its first cell")
|
||||
eq(snap(99), 5, "snap from past the item lands on its last cell")
|
||||
eq(snap(6), 5, "snap at the item end lands on the last cell inside it")
|
||||
|
||||
-- Cells stay aligned to the timeline, not the item, so an item that starts
|
||||
-- off-grid still uses the cell that contains its start as the floor.
|
||||
itemPos, itemLen = 2.5, 4
|
||||
eq(snap(0), 2, "snap floors to the cell containing an off-grid item start")
|
||||
|
||||
-- The rounding follows the grid size, not a fixed quarter note.
|
||||
grid, itemPos, itemLen = 2, 0, 10
|
||||
eq(snap(2.9), 1, "snap rounds to half-note cells on a half-note grid")
|
||||
eq(snap(3.1), 2, "snap rounds up to half-note cells on a half-note grid")
|
||||
eq(cursor, 4, "half-note cell 2 starts at QN 4")
|
||||
|
||||
print(fails == 0 and "ALL PASS" or (fails .. " FAILURES"))
|
||||
|
||||
Reference in New Issue
Block a user