Hide Empty Folder Tracks
I would like to select all Folder tracks and only all folder tracks in the session that are empty, and make them inactive and hide them?
I dont wanna hide any other empty tracks such as VCAs or empty audio tracks, only empty folder tracks.
Scheps Tracks selector can of course select all my folder tracks, but not all the Empty folder tracks. Wich would be a great future update. :-)
Linked from:
- samuel henriques @samuel_henriques
Hello @mikkeljm,
Try this,
It will do as you ask the first parent folders, folders inside folders wont work with this code.
Let me know if this is it.function clickAllGroupSymbol() { sf.ui.proTools.groupsEnsureGroupListIsVisible(); sf.ui.proTools.mainWindow.groupListView.children.whoseRole.is("AXColumn").first .children.whoseRole.is("AXRow").first .children.whoseRole.is("AXCell").first .buttons.first.elementClick(); sf.ui.proTools.trackDeselectAll(); }; function openAllFolderTracks() { sf.ui.proTools.invalidate() const selectedTrackHeaders = sf.ui.proTools.selectedTrackHeaders; clickAllGroupSymbol(); sf.ui.proTools.invalidate(); // Select original Track Selection if (selectedTrackHeaders.length > 0) { selectedTrackHeaders[0].trackSelect() selectedTrackHeaders[0].trackScrollToView() sf.ui.proTools.trackSelectByName({ names: selectedTrackHeaders.map(tr => tr.normalizedTrackName) }) }; }; /** * @param {AxPtTrackHeader} track */ function getTrackDepth(track) { return Math.floor((track.titleButton.frame.x - track.frame.x) / 15); } /** * @author Christian Scheuer - https://forum.soundflow.org/-1784/how-to-select-specific-type-of-tracks-inside-a-folder */ function getTrackFolderStructure() { var root = { children: [] }; var stack = []; stack[0] = root; var flatList = sf.ui.proTools.visibleTrackHeaders.map(t => ({ track: t, depth: getTrackDepth(t) })); for (var i = 0; i < flatList.length; i++) { var flatItem = flatList[i]; var node = { name: flatItem.track.normalizedTrackName, type: flatItem.track.title.value.match(/ - (.*)$/)[1].trim().replace(/track$/i, '').trim().toLowerCase(), track: flatItem.track, children: [] }; stack[flatItem.depth] = node; stack[flatItem.depth - 1].children.push(node); } return root; }; function main(callback) { const isTrackListVisible = sf.ui.proTools.getMenuItem("View", "Other Displays", "Track List").isMenuChecked const selectedTrackHeaders = sf.ui.proTools.selectedTrackHeaders; try { //Open All folders clickAllGroupSymbol(); callback(); } catch (err) { throw err } finally { sf.ui.proTools.invalidate(); // Select original Track Selection if (selectedTrackHeaders.length > 0) { selectedTrackHeaders[0].trackSelect() selectedTrackHeaders[0].trackScrollToView() sf.ui.proTools.trackSelectByName({ names: selectedTrackHeaders.map(tr => tr.normalizedTrackName) }) }; //Close track list if originaly closed if (!isTrackListVisible) sf.ui.proTools.menuClick({ menuPath: ["View", "Other Displays", "Track List"], targetValue: "Disable" }) }; }; function hideAndMakeInactiveEmptyFolders() { sf.ui.proTools.mainWindow.invalidate(); const root = getTrackFolderStructure(); const emptyFolders = root.children.filter(track => track.type.endsWith("folder") && track.children.length === 0 ).map(folder => folder.name) sf.ui.proTools.trackSelectByName({ names: emptyFolders }); sf.ui.proTools.trackHideAndMakeInactiveSelected(); }; main(hideAndMakeInactiveEmptyFolders);
Nathan Salefski @nathansalefski
Hi Samuel, I was wondering how I could rewrite this to detect empty folders regardless of how deeply nested they are within other folders. Currently just trying to log their names. I assume it has something to do with
function getTrackDepth(track) {}
maybe @chrscheuer can shed some light as well:/** * @param {AxPtTrackHeader} track */ function getTrackDepth(track) { return Math.floor((track.titleButton.frame.x - track.frame.x) / 15); } /** * @author Christian Scheuer - https://forum.soundflow.org/-1784/how-to-select-specific-type-of-tracks-inside-a-folder */ function getTrackFolderStructure() { var root = { children: [] }; var stack = []; stack[0] = root; var flatList = sf.ui.proTools.visibleTrackHeaders.map(t => ({ track: t, depth: getTrackDepth(t) })); for (var i = 0; i < flatList.length; i++) { var flatItem = flatList[i]; var node = { name: flatItem.track.normalizedTrackName, type: flatItem.track.title.value.match(/ - (.*)$/)[1].trim().replace(/track$/i, '').trim().toLowerCase(), track: flatItem.track, children: [] }; stack[flatItem.depth] = node; stack[flatItem.depth - 1].children.push(node); } return root; }; const root = getTrackFolderStructure(); const emptyFolders = root.children.filter(track => track.type.endsWith("folder") && track.children.length === 0 ).map(folder => folder.name) log(emptyFolders)
samuel henriques @samuel_henriques
Hello Nathan,
chatGPT to the help.function findEmptyFolder(obj) { const result = []; function traverse(node) { if (node.children && node.type && node.type.endsWith("folder") && Array.isArray(node.children) && node.children.length === 0) { result.push(node); } if (node.children && Array.isArray(node.children)) { for (const child of node.children) { traverse(child); } } } traverse(obj); return result; }; const emptyFolderNames = findEmptyFolder(root).map(el => el.name); log(emptyFolderNames);
This will take the root object and return the empty folder names.
Nathan Salefski @nathansalefski
Wow this is AMAZING! This will be a huge help! THANKS! Here's it all pieced together for anyone looking
/** * @author Christian Scheuer - https://forum.soundflow.org/-1784/how-to-select-specific-type-of-tracks-inside-a-folder */ // Get Track Depth function getTrackDepth(track) { return Math.floor((track.titleButton.frame.x - track.frame.x) / 15); } // Get Folder Structure function getTrackFolderStructure() { var root = { children: [] }; var stack = []; stack[0] = root; var flatList = sf.ui.proTools.visibleTrackHeaders.map(t => ({ track: t, depth: getTrackDepth(t) })); for (var i = 0; i < flatList.length; i++) { var flatItem = flatList[i]; var node = { name: flatItem.track.normalizedTrackName, type: flatItem.track.title.value.match(/ - (.*)$/)[1].trim().replace(/track$/i, '').trim().toLowerCase(), track: flatItem.track, children: [] }; stack[flatItem.depth] = node; stack[flatItem.depth - 1].children.push(node); } return root; }; const root = getTrackFolderStructure(); /** * Samuel Henriques - https://forum.soundflow.org/-8814#post-5 */ // Works Regardless of Folder Structure function findEmptyFolder(obj) { const result = []; function traverse(node) { if (node.children && node.type && node.type.endsWith("folder") && Array.isArray(node.children) && node.children.length === 0) { result.push(node); } if (node.children && Array.isArray(node.children)) { for (const child of node.children) { traverse(child); } } } traverse(obj); return result; }; const emptyFolderNames = findEmptyFolder(root).map(el => el.name); log(emptyFolderNames);
- In reply tosamuel_henriques⬆:
Nathan Salefski @nathansalefski
Interestingly, when I try to
sf.ui.proTools.trackSelectByName({names: emptyFolderNames});
it will not select Empty folders nested deeper than one level deep. Any clue as to why? Ideally I'd be able to select and delete all empty folders in the session no matter where they are. Here's a quick screen recording https://we.tl/t-ZRDqtzIOMpNathan Salefski @nathansalefski
It seems to just be this, which funnily enough, Kitch and I already solved How to use looseMatch in Popup Menu #post-21
- In reply tonathansalefski⬆:
samuel henriques @samuel_henriques
I'm not having this problem, try to invalidate before the script so pro tools will get the most updated track list and order,
sf.ui.proTools.invalidate();
Nathan Salefski @nathansalefski
Can you see if you have a problem with your track list view at its minimum size
samuel henriques @samuel_henriques
works all the way to micro
Nathan Salefski @nathansalefski
Ah I meant this
samuel henriques @samuel_henriques
Ahh yes, this way it will become unstable, not selecting the correct ones
samuel henriques @samuel_henriques
weird. if track list is closed the problem goes away.
samuel henriques @samuel_henriques
actually no. It's still unstable
Nathan Salefski @nathansalefski
@Kitch and I ran into this problem lol Hide Empty Folder Tracks #post-8
samuel henriques @samuel_henriques
yes, i see it now
Nathan Salefski @nathansalefski
This seems to be working:
// Mouse Drag Funtion function mouseDrag(startPos, endPos) { const timeToWaitBeforeDropping = 250; sf.mouse.down({ position: startPos, }); sf.mouse.drag({ position: endPos }); sf.wait({ intervalMs: timeToWaitBeforeDropping }); sf.mouse.up({ position: endPos }); }; /** * @authors Kitch Membery & Nathan Salefksi - https://forum.soundflow.org/-10790#post-28 */ function resizeTrackListView(editWindow, editWindowTrackListFrame, size) { const editWindowMenuItem = sf.ui.proTools.getMenuItem('Window', 'Edit'); if (!(editWindowMenuItem.isMenuChecked)) { sf.ui.proTools.menuClick({ menuPath: ['Window', 'Edit',] }, `Could Not Show Edit Window` ); sf.wait({ intervalMs: 100 }); sf.ui.proTools.mainWindow.invalidate(); } const trackListMenuItem = sf.ui.proTools.getMenuItem('View', 'Other Displays', 'Track List'); if (!(trackListMenuItem.isMenuChecked)) { sf.ui.proTools.menuClick({ menuPath: ['View', 'Other Displays', 'Track List'], }, `Could Not Show Track List` ); sf.wait({ intervalMs: 100 }); sf.ui.proTools.mainWindow.invalidate(); } const trackListResizePoint = { x: editWindow.frame.x + editWindowTrackListFrame.w + 21, y: editWindow.frame.y + (editWindow.frame.h / 2), }; const startDrag = trackListResizePoint; let endDrag = {} if (size === 'Increase') { endDrag = { x: trackListResizePoint.x + 150, y: trackListResizePoint.y }; mouseDrag(startDrag, endDrag); } else if (size === 'Decrease') { endDrag = { x: trackListResizePoint.x - 150, y: trackListResizePoint.y }; mouseDrag(startDrag, endDrag); } } /** * @author Christian Scheuer - https://forum.soundflow.org/-1784/how-to-select-specific-type-of-tracks-inside-a-folder */ function getTrackDepth(track) { return Math.floor((track.titleButton.frame.x - track.frame.x) / 15); } function getTrackFolderStructure() { var root = { children: [] }; var stack = []; stack[0] = root; var flatList = sf.ui.proTools.visibleTrackHeaders.map(t => ({ track: t, depth: getTrackDepth(t) })); for (var i = 0; i < flatList.length; i++) { var flatItem = flatList[i]; var node = { name: flatItem.track.normalizedTrackName, type: flatItem.track.title.value.match(/ - (.*)$/)[1].trim().replace(/track$/i, '').trim().toLowerCase(), track: flatItem.track, children: [] }; stack[flatItem.depth] = node; stack[flatItem.depth - 1].children.push(node); } return root; }; /** * Samuel Henriques - https://forum.soundflow.org/-8814#post-5 */ function findEmptyFolder(obj) { const result = []; function traverse(node) { if (node.children && node.type && node.type.endsWith("folder") && Array.isArray(node.children) && node.children.length === 0) { result.push(node); } if (node.children && Array.isArray(node.children)) { for (const child of node.children) { traverse(child); } } } traverse(obj); return result; }; function main() { sf.ui.proTools.appActivateMainWindow(); sf.ui.proTools.mainWindow.invalidate(); const root = getTrackFolderStructure(); const emptyFolderNames = findEmptyFolder(root).map(el => el.name); const editWindow = sf.ui.proTools.windows.whoseTitle.startsWith('Edit: ').first; let editWindowTrackListFrame = editWindow.tables.whoseTitle.is('Track List').first.frame; if (editWindowTrackListFrame.w >= 85 && editWindowTrackListFrame.w <= 250) { resizeTrackListView(editWindow, editWindowTrackListFrame, 'Increase'); sf.ui.proTools.trackSelectByName({ names: emptyFolderNames }); editWindowTrackListFrame = editWindow.tables.whoseTitle.is('Track List').first.frame; resizeTrackListView(editWindow, editWindowTrackListFrame, 'Decrease'); } else { sf.ui.proTools.trackSelectByName({ names: emptyFolderNames }); } } main();
samuel henriques @samuel_henriques
:) thats cool
Nathan Salefski @nathansalefski
Teamwork makes the dream work
- In reply tosamuel_henriques⬆:
Ben Rubin @Ben_Rubin
Jumping on an old thread here, how can I modify this script to ignore tracks with certain names, @samuel_henriques ? So it will still see the folder as "empty" even if it has certain tracks with specific names?
thanks!
- MIn reply tomikkeljm⬆:mikkeljm @mikkeljm
Thanks Samuel.
This did the trick. I love it. Big help.
- In reply tomikkeljm⬆:Chris Shaw @Chris_Shaw2025-04-16 21:15:39.386Z2025-04-17 00:33:38.298Z
With SF 5.10.3 the code for this becomes much simpler.
The PT SDK can now returnparentFolder
for tracks.
Using this we can get all folder track names, check if it is a parent of any other track and if not, hide/deactivate or delete it.
If a folder is hidden / deleted then the function calls itself again (a recursive function) until the number of tracks in the session hasn't changed.
This will delete or deactivate and hide all empty folders and(!) subfolders:sf.ui.proTools.appActivateMainWindow(); const getTracks = () => sf.app.proTools.tracks.invalidate().allItems; function deleteFolder(trackName) { sf.app.proTools.selectTracksByName({ trackNames: [trackName] }) sf.ui.proTools.menuClick({ menuPath: ["Track", "Delete"] }) } function deactivateFolder(trackName) { sf.app.proTools.setTrackInactiveState({ trackNames: [trackName], enabled: true }) } function hideFolderTrack(trackName) { sf.app.proTools.setTrackHiddenState({ trackNames: [trackName], enabled: true }) } let deletedOrHiddenFolders = [] let pass = 1 function hideAndDeactivateEmptyFolderTracks() { const uid = "hideDeactivateEmptyFolders" + Date.now(); let tracksInSession = getTracks(); let inactiveTracksInSession = tracksInSession.filter(t => t.isInactive) const folderTracks = tracksInSession.filter(t => t.type.endsWith("Folder")).filter(t => !t.isInactive); let folderTrackNames = folderTracks.map(f => f.name); folderTrackNames.forEach((folderName, index) => { sf.interaction.notify({ uid, title: "Hide and deactivate empty folder tracks", message: `${folderName}:Checking for sub-tracks. Pass ${pass}`, progress: (index + 1) / folderTrackNames.length, keepAliveMs: 500 }) // if (getTracks().find(t => t.name == folderName).isInactive) return function getSubTracks(folderName) { const tracksInFolder = getTracks().filter(t => t.parentFolderName == folderName); const tracksInFolderNames = tracksInFolder.map(t => t.name); let activeSubTrackCount = 0 tracksInFolder.forEach(t => { if (!t.isInactive) { activeSubTrackCount++; } }) if (activeSubTrackCount == 0) { hideFolderTrack(folderName); deactivateFolder(folderName); deletedOrHiddenFolders.push(folderName) return } if (tracksInFolderNames.length == 1) { const subTrack = getTracks().find(t => t.name == tracksInFolderNames[0]) // if (subTrack.isInactive) getSubTracks(subTrack.name); if (subTrack.isInactive ) return; if (subTrack.type.endsWith("Folder") && !subTrack.isInactive) { getSubTracks(subTrack.name) } if (subTrack.type.endsWith("Folder") && subTrack.isInactive) { hideFolderTrack(folderName); deactivateFolder(folderName); deletedOrHiddenFolders.push(folderName) return } } if (tracksInFolderNames.length == 0) { hideFolderTrack(folderName); deactivateFolder(folderName); deletedOrHiddenFolders.push(folderName) } } getSubTracks(folderName) // Compare number of inactive tracks in session after deactivating // If different run again }) const updatedInactiveTracksCount = getTracks().filter(t => t.isInactive).length if (updatedInactiveTracksCount != inactiveTracksInSession.length) { pass++ hideAndDeactivateEmptyFolderTracks(); } // const tracksHiddenAndDeactivatedCount = getTracks().filter(t => t.isInactive).length - inactiveTracksInSession.length sf.interaction.notify({ title: "Hide Empty Folders", message: `${tracksHiddenAndDeactivatedCount} folders Hidden and made inactive` }) } function deleteEmptyFolders() { let tracksInSession = getTracks(); const folderTracks = tracksInSession.filter(t => t.type.endsWith("Folder")); const folderTrackNames = folderTracks.map(f => f.name); folderTrackNames.forEach(folderName => { const tracksInFolder = tracksInSession.filter(t => t.parentFolderName == folderName).map(t => t.name); if (tracksInFolder.length == 0) { deleteFolder(folderName); deletedOrHiddenFolders.push(folderName) } }) // Compare number of tracks in session after deletion // If different run again if (tracksInSession.length != getTracks().length) deleteEmptyFolders() sf.interaction.notify({ title: "Delete Empty Folders", message: `${deletedOrHiddenFolders.length} folders deleted` }) } function main() { const deleteOrHide = sf.interaction.displayDialog({ prompt: "Delete or deactivate/hide all empty folders?", buttons: ["Cancel", "Delete", "Hide"], defaultButton: "Hide" }).button deleteOrHide == "Delete" ? deleteEmptyFolders() : hideAndDeactivateEmptyFolderTracks() } main()
Chris Shaw @Chris_Shaw2025-04-16 22:02:09.971Z
Updated:
I misread the original post.
I've updated the code above to give the option to either make empty folders inactive and hidden or delete them.Ben Rubin @Ben_Rubin
Thanks, @Chris_Shaw. Wondering if you would be able to answer my question above when you get a moment?
thanks much
benChris Shaw @Chris_Shaw2025-04-17 00:19:03.960Z
Not sure I understand the request.
I'm interpreting that as "If the script encounters a folder with a certain name then do not deactivate and hide."
I'm not sure what you mean bySo it will still see the folder as "empty" even if it has certain tracks with specific names
Ben Rubin @Ben_Rubin
So in my template, I have a bunch of Instrument Busses which are folder tracks and I move all the tracks I intake into the proper folder, Drums, Vocals etc. But often I don't use them all if that instrument is not being used. So if for example there's no Drums, I don't need the Drums folder. But there's always three aux tracks already in there, ie "Kick SansAmp" "Kick Snare Krush" and "Drums Buss Slam." So I'd like the script to ignore these tracks and still basically recognize the Drums folder as "empty" for this purpose so I can hide or delete it. Does that make sense?
Chris Shaw @Chris_Shaw2025-04-17 00:37:03.677Z
That's WAYY more complicated than simply adjusting the above script unfortunately
- In reply toChris_Shaw⬆:
Chris Shaw @Chris_Shaw2025-04-17 00:16:25.220Z
Updated again:
Hiding folder tracks needed more recursion.
Updated the code aboveBen Rubin @Ben_Rubin
this script is great, btw. i know you're super busy, but hoping you're up for the challenge at some point!
Ben Rubin @Ben_Rubin
ChatGPT to the rescue! For anyone coming across this thread who is looking to do what I want to do, here is a script that can ignore tracks in folders with specific names.
sf.ui.proTools.appActivateMainWindow(); const getTracks = () => sf.app.proTools.tracks.invalidate().allItems; function deleteFolders(trackNames) { sf.app.proTools.selectTracksByName({ trackNames }); sf.ui.proTools.menuClick({ menuPath: ["Track", "Delete"] }); try { sf.ui.proTools.confirmationButtonDialog.elementWaitFor({ waitType: "Appear", timeout: 500 }); sf.ui.proTools.confirmationButtonDialog.buttons.whoseTitle.is("Delete").first.elementClick(); } catch (e) {} } function deactivateFolders(trackNames) { sf.app.proTools.setTrackInactiveState({ trackNames, enabled: true }); } function hideFolderTracks(trackNames) { sf.app.proTools.setTrackHiddenState({ trackNames, enabled: true }); } let deletedOrHiddenFolders = []; let pass = 1; const IGNORED_TRACK_NAMES = [ "KICK SANSAMP", "KICK SNARE KRUSH", "DRUMS BUSS SLAM" ]; function hideAndDeactivateEmptyFolderTracks() { const uid = "hideDeactivateEmptyFolders" + Date.now(); let tracksInSession = getTracks(); let inactiveTracksInSession = tracksInSession.filter(t => t.isInactive); const folderTracks = tracksInSession.filter(t => t.type.endsWith("Folder")); let folderTrackNames = folderTracks.map(f => f.name); let foldersToProcess = []; folderTrackNames.forEach((folderName, index) => { sf.interaction.notify({ uid, title: "Hide and deactivate empty folder tracks", message: `${folderName}:Checking for sub-tracks. Pass ${pass}`, progress: (index + 1) / folderTrackNames.length, keepAliveMs: 500 }); function getSubTracks(folderName) { const tracksInFolder = getTracks().filter(t => t.parentFolderName == folderName); const tracksInFolderNames = tracksInFolder.map(t => t.name); let activeSubTrackCount = 0; tracksInFolder.forEach(t => { const isIgnored = IGNORED_TRACK_NAMES.includes(t.name.toUpperCase()); if (!t.isInactive && !isIgnored) { activeSubTrackCount++; } }); const onlyIgnoredOrInactive = tracksInFolder.every(t => IGNORED_TRACK_NAMES.includes(t.name.toUpperCase()) || t.isInactive ); const folderTrack = getTracks().find(t => t.name === folderName && t.type.endsWith("Folder")); if ((activeSubTrackCount === 0 || onlyIgnoredOrInactive) && folderTrack) { foldersToProcess.push(folderName); return; } if (tracksInFolderNames.length === 1) { const subTrack = getTracks().find(t => t.name === tracksInFolderNames[0]); const isIgnored = IGNORED_TRACK_NAMES.includes(subTrack.name.toUpperCase()); if (subTrack.isInactive && isIgnored && folderTrack) { foldersToProcess.push(folderName); return; } if (subTrack.type.endsWith("Folder")) { getSubTracks(subTrack.name); } } if (tracksInFolderNames.length === 0 && folderTrack) { foldersToProcess.push(folderName); } tracksInFolder.forEach(subTrack => { if (subTrack.type.endsWith("Folder")) { getSubTracks(subTrack.name); } }); } getSubTracks(folderName); }); if (foldersToProcess.length > 0) { hideFolderTracks(foldersToProcess); deactivateFolders(foldersToProcess); deletedOrHiddenFolders.push(...foldersToProcess); } const updatedInactiveFolders = getTracks().filter(t => t.type.endsWith("Folder") && t.isInactive).length; const previousInactiveFolders = inactiveTracksInSession.filter(t => t.type.endsWith("Folder")).length; if (updatedInactiveFolders != previousInactiveFolders) { pass++; hideAndDeactivateEmptyFolderTracks(); } sf.interaction.notify({ title: "Hide Empty Folders", message: `${foldersToProcess.length} folders Hidden and made inactive` }); } function deleteEmptyFolders() { let tracksInSession = getTracks(); const folderTracks = tracksInSession.filter(t => t.type.endsWith("Folder")); const folderTrackNames = folderTracks.map(f => f.name); let foldersToDelete = []; folderTrackNames.forEach(folderName => { const tracksInFolder = tracksInSession.filter(t => t.parentFolderName == folderName); const onlyIgnoredOrInactive = tracksInFolder.every(t => IGNORED_TRACK_NAMES.includes(t.name.toUpperCase()) || t.isInactive ); if (tracksInFolder.length === 0 || onlyIgnoredOrInactive) { foldersToDelete.push(folderName); } }); if (foldersToDelete.length > 0) { deleteFolders(foldersToDelete); deletedOrHiddenFolders.push(...foldersToDelete); } if (tracksInSession.length != getTracks().length) deleteEmptyFolders(); sf.interaction.notify({ title: "Delete Empty Folders", message: `${deletedOrHiddenFolders.length} folders deleted` }); } function main() { const deleteOrHide = sf.interaction.displayDialog({ prompt: "Delete or deactivate/hide all empty folders?", buttons: ["Cancel", "Delete", "Hide"], defaultButton: "Hide" }).button; deleteOrHide == "Delete" ? deleteEmptyFolders() : hideAndDeactivateEmptyFolderTracks(); } main();
Just change the string in
const IGNORED_TRACK_NAMES
to your folder names and you are good to go. works with active or inactive folders.