Command.Navigation.FileGroupNavigation: Navigate between and inside filegroups (v1.22 - 16.11.21)

TL;DR
A script/buttons for navigating through filegroups.
It allows to

  • set the direction of navigation (previous, next)
  • set the stepsize (how many groups to skip, default = 1)
  • collapse every other group so you just see the group you navigated to
  • loop the navigation (starting at the beginning when navigating over the end and vice versa)

Jumplist Dialog
Allows you to jump between groups via a dialog with a list of all groups or inside of the current group between items.
image

Usecase
I often work with grouped files (by filename which represents the date, so one group per day). When going through the folder contents the groups help to get an overview, but finding a certain date can become difficult if there are many groups. This script helps me stepping through the groups, even more helpful is that it can be configured to collapse all other groups except the currently focused one.

Usage
The command is called

FileGroupNavigation

so the basic command for navigating to the next group is

FileGroupNavigation NAVIGATE next

and to the previous group

FileGroupNavigation NAVIGATE previous

The new navigatetarget parameter lets you define where to jump to in the next group

FileGroupNavigation NAVIGATE next
NAVIGATETARGET [start,middle,end]

Further parameters are the stepsize, which defines how many groups you want to go forward/backward (default is next; so going to the next or previous group). Use positive numbers otherwise it probably will flip the behaviour of "next/previous".

FileGroupNavigation NAVIGATE next STEP=2

for going to the after next group.

And the last parameter is for collapsing the other groups so just the focused group is expanded and you can concentrate on its content. Default is off (so not collapsing others).

FileGroupNavigation NAVIGATE next COLLAPSEOTHERS

The new group jumplist dialog is accessible via

FileGroupNavigation GROUPJUMPLIST
and it can be told to show the items per group
FileGroupNavigation GROUPJUMPLIST SHOWFILECOUNT

The jumplist for items is called via

FileGroupNavigation ITEMJUMPLIST
with the opportunity to show the full item path with
FileGroupNavigation ITEMJUMPLIST SHOWFULLITEMPATH

Resources/Installation
Installation Guide

Version History

Button for going to previous group (@disablenosel because the next group is calculated from the selected file). Copy to menu when in customize mode

<?xml version="1.0"?>
<button backcol="none" display="icon" label_pos="right" textcol="none">
	<label>Previous Group</label>
	<icon1>#viewerprev</icon1>
	<function type="normal">
		<instruction>@disablenosel </instruction>
		<instruction>FileGroupNavigation NAVIGATE PREVIOUS COLLAPSEOTHERS</instruction>
	</function>
</button>

And a button for going to the next group

<?xml version="1.0"?>
<button backcol="none" display="icon" label_pos="right" textcol="none">
	<label>Next Group</label>
	<icon1>#viewernext</icon1>
	<function type="normal">
		<instruction>@disablenosel </instruction>
		<instruction>FileGroupNavigation NAVIGATE NEXT COLLAPSEOTHERS</instruction>
	</function>
</button>

And a button for the new jumplist

<?xml version="1.0"?>
<button backcol="none" display="both" hotkey="alt+G" textcol="none">
	<label>Group jumplist</label>
	<icon1>#undodlg</icon1>
	<function type="normal">
		<instruction>FileGroupNavigation GROUPJUMPLIST SHOWFILECOUNT </instruction>
	</function>
</button>

Code

// Command.FileGroupNavigation
// (c) 2021 Felix

// This is a script for Directory Opus.
// See https://www.gpsoft.com.au/DScripts/redirect.asp?page=scripts for development information.

// Called by Directory Opus to initialize the script
function OnInit(initData)
{
	initData.name = "Command.Navigation.FileGroupNavigation";
	initData.version = "v1.22 (2021.11.16)";
	initData.copyright = "(c) 2021 Felix";
	initData.url = "https://resource.dopus.com/t/command-filegroupnavigation-navigate-in-filegroups-previous-next/38735";
	initData.desc = "Navigating between and inside of filegroups";
	initData.default_enable = true;
	initData.min_version = "12.0";
	initData.group = "Command";

	var cmd = initData.AddCommand();
	cmd.name = "FileGroupNavigation";
	cmd.method = "OnFileGroupNavigation";
	cmd.desc = "Navigating between and inside of filegroups";
	cmd.label = "OnFileGroupNavigation";
	//https://www.gpsoft.com.au/help/opus12/index.html#!Documents/Argument_Types.htm
	//https://www.gpsoft.com.au/help/opus12/index.html#!Documents/Internal_Command_Arguments.htm
	cmd.template = "NAVIGATE/K[previous,next],NAVIGATETARGET/K[start,middle,end],STEP/N,COLLAPSEOTHERS/S,GROUPJUMPLIST/S,ITEMJUMPLIST/S,SHOWFILECOUNT/S,SHOWFULLITEMPATH/S";
	cmd.hide = false;
	cmd.icon = "groupby";
}

// Implement the FileGroupNavigation command
function OnFileGroupNavigation(scriptCmdData)
{
	var args = scriptCmdData.func.args;
	var direction = 0; //the direction to go, prev/next, implied via sign
	var collapseOthers = args.got_arg.COLLAPSEOTHERS; //collapse non focused groups
	var stepSize = 1; //how many groups to skip, default is 1
	var itemSelectorIndex = 0; //select first item in group

	if(args.got_arg.GROUPJUMPLIST)
	{
		ShowGroupJumplist(scriptCmdData, args.got_arg.SHOWFILECOUNT);
		return;							//Using jumplist does exclude using the prev/next buttons so exit here
	}
	if(args.got_arg.ITEMJUMPLIST)
	{
		ShowItemJumplist(scriptCmdData, args.got_arg.SHOWFULLITEMPATH);
		return;							//Using jumplist does exclude using the prev/next buttons so exit here
	}
	if(args.got_arg.NAVIGATE)
    {
		var navigateArg = args.NAVIGATE.toLowerCase();
		if(navigateArg == "previous") 
			direction = -1;
		else if(navigateArg == "next")
			direction = 1;
		
		if(args.got_arg.NAVIGATETARGET)
		{
			var navigateTarget = args.NAVIGATETARGET.toLowerCase();
			if(navigateTarget == "start")
				itemSelectorIndex = 0;
			else if(navigateTarget == "middle")
				itemSelectorIndex = 1;
			else if(navigateTarget == "end")
				itemSelectorIndex = 2;
		}
	}
	if(args.got_arg.STEP)
	{
		try
		{
			stepSize = parseInt(args.STEP);
		}
		catch(e) { stepSize = 1; Log(e, true); }
	}

	if(direction != 0)
		GroupNavigation(scriptCmdData.func, direction * stepSize, collapseOthers, ItemSelectors[itemSelectorIndex]);
}

//
//-------------------------- Group navigation
//

//Go Next/Previous or with step size functionality
function GroupNavigation(func, direction, collapseOthers, itemSelector)
{
	var tab = func.sourcetab;
	tab.Update();
	
	var groups = tab.filegroups;
	if(groups.count > 0)
	{
		var focusedItem = tab.GetFocusItem();
		var containingGroup = focusedItem.filegroup;
		if(containingGroup != null)
		{				
			var groupIndex = GetGroupIndex(containingGroup, groups);
			if(groupIndex > -1)
			{
				var nextGroupResult = GetNextGroup(groups, groupIndex, direction);
				var nextGroup = nextGroupResult.group;			
				GoToGroup(nextGroup, collapseOthers, itemSelector, func);
			}
		}
	}
}

//Select the first item in the desired group and focus it
function GoToGroup(nextGroup, collapseOthers, itemSelector, func)
{
	var tab = func.sourcetab;
	var cmd = func.command;
	var nextSelectedItem = itemSelector(nextGroup);
	ClearCommand(cmd);
	tab.Update();

	if(collapseOthers)
	{
		ClearCommand(cmd);
		cmd.AddLine("Go GROUPCOLLAPSE *");
		cmd.Run();
	}
	ClearCommand(cmd);
	cmd.RunCommand("Go GROUPEXPAND \"" + nextGroup + "\""); //Make sure it is definitly expanded

	ClearCommand(cmd);
	tab.Update();
	
	FocusItem(nextSelectedItem, cmd, tab);
}

//Select item and bring to focus
function FocusItem(item, cmd, tab)
{
	cmd.AddFile(item);
	cmd.AddLine("SELECT NONE");		
	cmd.AddLine("Select FROMSCRIPT EXACT MAKEVISIBLE SETFOCUS");		
	cmd.Run();
	tab.Update();
}

//
//-------------------------- Jumplist
//

//Returns first item of group for GoToGroup
function FirstItemInGroupSelector(group)
{
	return group.members(0);
}

//Returns middle item of group for GoToGroup
function MiddleItemInGroupSelector(group)
{
	return group.members(group.count > 0 ? group.count / 2 : 0);
}

//Returns last item of group for GoToGroup
function LastItemInGroupSelector(group)
{
	return group.members(group.count > 0 ? group.count - 1 : 0);
}

//Used for Jumplist dialog
var ItemSelectors = [ FirstItemInGroupSelector, MiddleItemInGroupSelector, LastItemInGroupSelector ];

//Show a list of all groups in this tab and go to the selected one
function ShowGroupJumplist(cmdData, showItemCount)
{
	var tab = cmdData.func.sourcetab;
	var groups = tab.filegroups;
	var jumplistDlg = cmdData.func.Dlg;
    var groupVector = DOpus.Create.Vector();
	var groupArray = []; //Convert groups to array for easier access
	var menuVector = DOpus.Create.Vector();
	var groupIndex = -1; //Has an item a focus? Then mark the containing group bold
	
	if(!groups.count)
		return;//Maybe some information here?

	var focusedItem = tab.selected.count > 0 ? tab.selected(0) : null; //get first selected file
	if(focusedItem)
	{
		var containingGroup = focusedItem.filegroup;
		if(containingGroup)
		{
			groupIndex = GetGroupIndex(containingGroup, groups);			
		}
	}
	var index = 0;
	for (var groupEnum = new Enumerator(groups); !groupEnum.atEnd(); groupEnum.moveNext())
	{
		var groupItem = groupEnum.item();
		if(groupItem.count > 0)
		{
			var itemName = showItemCount ? groupItem + " (" + groupItem.count + ")" : String(groupItem);
			groupVector.push_back(itemName);
			groupArray.push(groupItem);
			if(groupIndex > -1)
			{
				if(index == groupIndex)
					menuVector.push_back(1); //Mark bold
				else
					menuVector.push_back(0);
			}
			else
				menuVector.push_back(0);
		}

		index++;
	}
	
	jumplistDlg.choices = groupVector;
	jumplistDlg.selection = (groupIndex > -1) ? groupIndex : 0;
	jumplistDlg.title   = 'Jump to group';
	jumplistDlg.message = 'Select the group you want to jump to';
	jumplistDlg.buttons = "Jump to start|Jump to middle|Jumpt to end|Cancel";
	var jumpListResult = jumplistDlg.Show();
	if(jumpListResult > 0)
	{
		var selectedGroupIndex = jumplistDlg.selection;
		var group = GetGroupByName(groupArray[selectedGroupIndex], groups);
		if(group != null)
		{
			GoToGroup(group, true, ItemSelectors[jumpListResult - 1], cmdData.func);
			Log(selectedGroupIndex);
		}
	}	
}

//Display a dialog to jump to file in the current group
function ShowItemJumplist(cmdData, showFullItemPath)
{
	var tab = cmdData.func.sourcetab;
	var groups = tab.filegroups;
	
	//if(groups.count == 1 && groups(0).count == 0) 
	if(!groups.count) //undefined so no groups are shown / Default group
		return;
	
	var jumplistDlg = cmdData.func.Dlg;
    var itemVector = DOpus.Create.Vector();
	var itemArray = []; //Convert groups to array for easier access
	var groupIndex = -1; //Has an item a focus? Then mark the containing group bold
	//var focusedItem = tab.GetFocusItem(); //PROBABLY WRONG 
	var focusedItem = tab.selected.count > 0 ? tab.selected(0) : null; //get first selected file
	if(focusedItem)
	{
		var containingGroup = focusedItem.filegroup;
		if(containingGroup != null)
		{
			groupIndex = GetGroupIndex(containingGroup, groups);	
			var items = containingGroup.members;
			for(var i = 0; i < items.count; i++)
			{
				itemVector.push_back(showFullItemPath ? items(i) : items(i).name);
				itemArray.push(items(i));
			}
				
			itemVector.sort();
			itemArray.sort();
			
			jumplistDlg.choices = itemVector;
			jumplistDlg.selection = ArrayIndexOf(itemArray, focusedItem);
			jumplistDlg.title   = 'Jump to file';
			jumplistDlg.message = "Select the file you want to jump in the group '" + containingGroup + "'";
			jumplistDlg.buttons = "OK|Cancel";
			var jumpListResult = jumplistDlg.Show();
			if(jumpListResult > 0)
			{
				var selectedItemIndex = jumplistDlg.selection;
				var selectedItem = itemArray[selectedItemIndex];
				if(selectedItem)
				{
					FocusItem(selectedItem, cmdData.func.Command, tab);
				}
			}
		}		
	}
}

//
//-------------------------- Helper methods
//

//clear the command for reuse
function ClearCommand(cmd)
{
	cmd.Clear();
	cmd.ClearFailed();
	cmd.ClearFiles();
}

//Get the index of the current group of the tabs group
function GetGroupIndex(group, groups)
{
	var index = 0;
	
	for (var groupEnum = new Enumerator(groups); !groupEnum.atEnd(); groupEnum.moveNext())
	{
		var groupItem = groupEnum.item();
		if(groupItem.id === group.id)
			return index;
		index++;
	}
	return -1; // not found
}

//Get a group object by name
function GetGroupByName(groupName, groups)
{
	for (var groupEnum = new Enumerator(groups); !groupEnum.atEnd(); groupEnum.moveNext())
	{
		var groupItem = groupEnum.item();
		if(String(groupName) == String(groupItem))
			return groupItem;
	}
	return null; // not found
}

//get the next group according to stepsize (default 1). Direction (forward/backward) is defined by the sign
function GetNextGroup(groups, currentIndex, navigationDirection)
{
	var index = mod((currentIndex + navigationDirection), groups.count);
	var group = groups(index);
	while(group.count == 0) //to prevent default group, maybe better checking for != null?
	{
		index = mod((index + navigationDirection), groups.count);
		group = groups(index);
	}
	return {group: group, index: index};
}

function ArrayIndexOf(array, item)
{
	for(var i = 0; i < array.length; i++)
		if(String(array[i].path) == String(item.path))
			return i;
	return -1;
}

//https://stackoverflow.com/questions/4467539/javascript-modulo-gives-a-negative-result-for-negative-numbers
//Allowing negative mod for looping in arrays
function mod(n, m) 
{
  return ((n % m) + m) % m;
}

function Log(msg, e)
{
	DOpus.output(String(msg), e || false);
}
4 Likes