Skip to content

Commit c3d92f0

Browse files
authored
Create BuyOne_Save track template with items selected or within time selection.lua (ReaTeam#1173)
* Create BuyOne_Save track template with items selected or within time selection.lua * Update BuyOne_Save track template with items selected or within time selection.lua
1 parent a3b20d0 commit c3d92f0

File tree

1 file changed

+309
-0
lines changed

1 file changed

+309
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
--[[
2+
ReaScript name: Save track template with items selected or within time selection
3+
Author: BuyOne
4+
Website: https://forum.cockos.com/member.php?u=134058
5+
Version: 1.0
6+
Changelog: Initial release
7+
Licence: WTFPL
8+
REAPER: at least v5.962
9+
About: The script allows saving track template with selected
10+
items or items within time selection provided option
11+
'Include track items in template'
12+
is enabled in 'Save track template' dialogue.
13+
REAPER native action saves all items on selected tracks.
14+
Without the option enabled the script is useless.
15+
16+
Inspired by a feature request at
17+
https://forum.cockos.com/showthread.php?t=277300
18+
19+
The script works in 2 stages, first it triggers
20+
the native action 'Track: Save tracks as track template...'
21+
to allow you to save track template as normal.
22+
23+
Once 'Save track template' dialogue is closed the script
24+
immediately triggers custom 'Select .RTrackTemplate file'
25+
dialogue inviting you to select the template just saved
26+
in order to process it.
27+
After the newly saved template is selected and button 'Open'
28+
is clicked in the dialogue the script completes its task
29+
leaving the track template only containing items you selected.
30+
31+
'Select .RTrackTemplate file' dialogue points to the
32+
/TrackTemplates folder inside REAPER resource directory
33+
which is the default location for saving track templates.
34+
If you saved a template elsewhere, simply navigate to that
35+
location.
36+
The script is blind to the path of the newly saved template
37+
file, to the actual save operation and to the state of the
38+
option 'Include track items in template', therefore
39+
'Select .RTrackTemplate file' dialogue will appear even if
40+
you closed native 'Save track template' dialogue without
41+
saving a template or didn't enable the option to save items.
42+
43+
If after saving a template you closed 'Select .RTrackTemplate file'
44+
dialogue without allowing the script to process the template
45+
file you'll be given an opportunity to keep the data until
46+
the next run.
47+
48+
Items whose start coincides with time selection end
49+
or whose end coincides with time selection start
50+
aren't considered to be included in time selection.
51+
52+
Since REAPER saves folder parent track templates with all
53+
their children, time selection bounds and selected state
54+
apply to items on their children tracks, to the parent
55+
track they apply in case it has any items, so if the parent
56+
track is selected the children tracks don't have to be
57+
explicitly selected.
58+
59+
]]
60+
61+
62+
local r = reaper
63+
64+
function Msg(param, cap) -- caption second or none
65+
local cap = cap and type(cap) == 'string' and #cap > 0 and cap..' = ' or ''
66+
reaper.ShowConsoleMsg(cap..tostring(param)..'\n')
67+
end
68+
69+
70+
function Esc(str)
71+
if not str then return end -- prevents error
72+
-- isolating the 1st return value so that if vars are initialized in a row outside of the function the next var isn't assigned the 2nd return value
73+
local str = str:gsub('[%(%)%+%-%[%]%.%^%$%*%?%%]','%%%0')
74+
return str
75+
end
76+
77+
78+
function Error_Tooltip(text)
79+
local x, y = r.GetMousePosition()
80+
r.TrackCtl_SetToolTip(text:upper():gsub('.','%0 '), x, y, true) -- spaced out // topmost true
81+
local time_init = r.time_precise()
82+
repeat
83+
until r.time_precise()-time_init >= 0.7
84+
end
85+
86+
87+
function ACT(comm_ID, midi) -- midi is boolean
88+
local comm_ID = comm_ID and r.NamedCommandLookup(comm_ID)
89+
local act = comm_ID and comm_ID ~= 0 and (midi and r.MIDIEditor_LastFocused_OnCommand(comm_ID, false) -- islistviewcommand false
90+
or r.Main_OnCommand(comm_ID, 0)) -- only if valid command_ID
91+
end
92+
93+
94+
function Get_Children_Tracks(tr, t)
95+
local cnt = 0
96+
for i = r.CSurf_TrackToID(tr, false), r.CountTracks(0)-1 do -- mcpView false // starting loop from the 1st child
97+
local chld_tr = r.GetTrack(0, i)
98+
if r.GetTrackDepth(chld_tr) > 0 then
99+
t[#t+1] = chld_tr
100+
cnt = cnt+1
101+
end
102+
end
103+
end
104+
105+
106+
function weed_out_item_chunks(itm_t, line, templ_t, idx)
107+
local GUID = line:match('<ITEM (.+)') or line:match('IGUID (.+)')
108+
-- search for a GUID of an item found in the line of the template code among the items slated for removal
109+
local found
110+
for k, v in ipairs(itm_t) do
111+
if v == GUID then
112+
table.remove(itm_t, k) -- to optimize so that next cycle is shorter
113+
found = true break
114+
end
115+
end
116+
if found then -- if turns out to be an item slated for removal
117+
local i = idx -- starting from the index of the item chunk first line in the template code
118+
repeat
119+
if templ_t[i+1] and not templ_t[i+1]:match('<TRACK') then
120+
templ_t[i] = '' -- remove lines by replacing with empty space unless next block is <TRACK because it will be preceded by closure of the previous <TRACK block which should be left intact to ensure integrity of the code, the template won't load otherwise
121+
end
122+
i = i+1
123+
until templ_t[i]:match('<ITEM') or templ_t[i]:match('<TRACK') or i == #templ_t -- until the index of the next chunk block or template end
124+
end
125+
end
126+
127+
128+
function Process_Track_Template(templ_t, itm_t, filename)
129+
130+
local idx_init
131+
for idx, line in ipairs(templ_t) do
132+
if line:match('<ITEM') then idx_init = idx end -- store table index of the first line in item chunk
133+
if idx_init and (line:match('<ITEM (.+)') or line:match('IGUID')) then -- in some track template files the <ITEM block start may not include the GUID so watch for the line where it appears inside the item chunk
134+
weed_out_item_chunks(itm_t, line, templ_t, idx_init)
135+
idx_init = nil
136+
end
137+
end
138+
139+
local templ = ''
140+
for k, line in ipairs(templ_t) do
141+
if #line > 0 then -- not a line of a removed item chunk
142+
local lb = k == 1 and '' or '\n' -- only add line break from 2nd line onwards
143+
templ = templ..lb..line
144+
end
145+
end
146+
147+
local f = io.open(filename, 'w')
148+
f:write(templ)
149+
f:close()
150+
151+
end
152+
153+
154+
function Delete_Ext_States(sect)
155+
local i = 1
156+
repeat
157+
r.DeleteExtState(sect, i, true) -- persist true
158+
i = i+1
159+
until r.GetExtState(sect, i) == '' -- first key without stored value
160+
end
161+
162+
163+
local _, scr_name, sect_ID, cmd_ID, _,_,_ = r.get_action_context()
164+
local named_ID = r.ReverseNamedCommandLookup(cmd_ID)
165+
local state = r.GetExtState(named_ID, '1') -- check if there're stored data from previous run when the user didn't update the template file
166+
local path = r.GetResourcePath()
167+
local sep = path:match('[\\/]')
168+
local tr_tmpl_fld = path..sep..'TrackTemplates'..sep
169+
170+
-- Allow user to process template file with stored data if there're any
171+
if #state > 0 then
172+
local resp = r.MB('Last time you didn\'t update the template file.\n\n\t Wish to update it now?\n\n After cancellation or assention and then\n\n\t declining the dialogue,\n\n'..(' '):rep(10)..'the stored data will be removed.', 'PROMPT', 4)
173+
if resp == 6 then -- OK
174+
::RETRY::
175+
local retval, filename = r.GetUserFileNameForRead(tr_tmpl_fld, 'Select a .RTrackTemplate file', '.RTrackTemplate')
176+
if retval and not filename:match('%.RTrackTemplate$') then
177+
local resp = r.MB(' Invalid file type. Wish to retry?\n\nIf not, the stored data will be removed.', 'ERROR', 4)
178+
if resp == 6 then -- OK
179+
goto RETRY
180+
end
181+
elseif retval then
182+
local templ_t = {}
183+
for line in io.lines(filename) do -- lines aren't followed by line break
184+
templ_t[#templ_t+1] = line
185+
end
186+
local itm_t = {}
187+
local i = 1
188+
repeat -- construct item GUID table from extended states
189+
itm_t[#itm_t+1] = r.GetExtState(named_ID, i)
190+
i = i+1
191+
until r.GetExtState(named_ID, i) == '' -- first key without stored value
192+
Process_Track_Template(templ_t, itm_t, filename)
193+
end
194+
end
195+
Delete_Ext_States(named_ID)
196+
return r.defer(function() do return end end) end
197+
198+
199+
-- Start main routine
200+
201+
local sel_tr_cnt = r.CountSelectedTracks(0)
202+
203+
if sel_tr_cnt == 0 then
204+
Error_Tooltip('\n\n no selected tracks \n\n')
205+
return r.defer(function() do return end end) end
206+
207+
local resp = r.MB('\"YES\" — items within time selection on selected tracks\n\n\"NO\" — selected items on selected tracks', 'PROMPT', 3)
208+
if resp == 2 then -- Cancel
209+
return r.defer(function() do return end end) end
210+
211+
212+
local itm_t = {}
213+
local GET = r.GetMediaItemInfo_Value
214+
215+
if resp == 6 then -- items within time selection
216+
local st, fin = r.GetSet_LoopTimeRange(false, false, 0, 0, false) -- isSet, isLoop, allowautoseek false
217+
if st == fin then
218+
r.MB('Time selection isn\'t set.', 'ERROR', 0)
219+
return r.defer(function() do return end end) end
220+
221+
local tr_t = {}
222+
for i = 0, sel_tr_cnt-1 do
223+
local tr = r.GetSelectedTrack(0,i)
224+
tr_t[#tr_t+1] = tr
225+
Get_Children_Tracks(tr, tr_t) -- folder parent track templates are saved with their children so children track items have to be taken into account
226+
end
227+
228+
local itm_cnt = 0
229+
for _, tr in ipairs(tr_t) do
230+
local tr_itm_cnt = r.GetTrackNumMediaItems(tr)
231+
itm_cnt = itm_cnt+tr_itm_cnt
232+
for i = 0, tr_itm_cnt-1 do
233+
local itm = r.GetTrackMediaItem(tr, i)
234+
local itm_st = GET(itm, 'D_POSITION')
235+
local itm_end = itm_st + GET(itm, 'D_LENGTH')
236+
if itm_st >= fin or itm_end <= st then -- only items which are explicitly not within time selection
237+
local retval, GUID = r.GetSetMediaItemInfo_String(itm, 'GUID', '', false) -- setNewValue false
238+
itm_t[#itm_t+1] = GUID
239+
end
240+
end
241+
end
242+
243+
local err = itm_cnt == 0 and 'No items on selected tracks.' or itm_cnt == #itm_t and 'No items within time selection\n\n'..(' '):rep(10)..'on selected tracks.'
244+
if err then
245+
r.MB(err, 'ERROR', 0)
246+
return r.defer(function() do return end end) end
247+
248+
elseif resp == 7 then -- selected items
249+
250+
local tr_t = {}
251+
for i = 0, sel_tr_cnt-1 do
252+
local tr = r.GetSelectedTrack(0,i)
253+
tr_t[#tr_t+1] = tr
254+
Get_Children_Tracks(tr, tr_t) -- folder parent track templates are saved with their children so children track items have to be taken into account
255+
end
256+
257+
local itm_cnt = 0
258+
for _, tr in ipairs(tr_t) do
259+
local tr_itm_cnt = r.GetTrackNumMediaItems(tr)
260+
itm_cnt = itm_cnt+tr_itm_cnt
261+
for i = 0, tr_itm_cnt-1 do
262+
local itm = r.GetTrackMediaItem(tr, i)
263+
if not r.IsMediaItemSelected(itm) then -- only collect non-selected items
264+
local retval, GUID = r.GetSetMediaItemInfo_String(itm, 'GUID', '', false) -- setNewValue false
265+
itm_t[#itm_t+1] = GUID
266+
end
267+
end
268+
end
269+
270+
local err = itm_cnt == 0 and 'No items on selected tracks.' or itm_cnt == #itm_t and 'No selected items.'
271+
if err then
272+
r.MB(err, 'ERROR', 0)
273+
return r.defer(function() do return end end) end
274+
275+
end
276+
277+
-- this particular method has been preferred over using chunk to create a template file because track templates contain some values not present in chunks, in particular additional envelope point values, two item length values, item GUID appearing in the item chunk start besides its IGUID inside the chunk
278+
ACT(40392) -- Track: Save tracks as track template...
279+
::RETRY::
280+
local retval, filename = r.GetUserFileNameForRead(tr_tmpl_fld, 'Select a .RTrackTemplate file', '.RTrackTemplate')
281+
282+
if retval and not filename:match('%.RTrackTemplate$') then
283+
local resp = r.MB('Invalid file type. Wish to retry?', 'ERROR', 4)
284+
if resp == 6 then -- OK
285+
goto RETRY
286+
else retval = nil end -- to trigger the next prompt
287+
end
288+
289+
if not retval then -- user closed the dialogue without file
290+
local s = ' '
291+
local resp = r.MB(' If a template was saved its file was not updated.\n\n'..s:rep(7)..'Wish to keep the data until the next run?\n\n'..s:rep(8)..'(only relevant if a template was saved)\n\n'..s:rep(10)..'If so, at next run during this session\n\n'..s:rep(13)..'you\'ll be asked to update the file.', 'PROMPT', 4)
292+
if resp == 6 then -- OK // store GUIDs of items whose chunks are to be removed from track template
293+
for k, GUID in ipairs(itm_t) do
294+
r.SetExtState(named_ID, k, GUID, false) -- persist false
295+
end
296+
end
297+
return r.defer(function() do return end end) end
298+
299+
local templ_t = {}
300+
for line in io.lines(filename) do -- lines aren't followed by line break
301+
templ_t[#templ_t+1] = line
302+
end
303+
304+
Process_Track_Template(templ_t, itm_t, filename)
305+
306+
do return r.defer(function() do return end end) end -- prevent undo point creation
307+
308+
309+

0 commit comments

Comments
 (0)