FAYT script : Dynamic Tab Groups

Dynamic Tab Groups

Release history

V2.0 : Added ability to generate dynamic buttons for the open, append and close actions.
:warning:Requires Opus 13.16.1 min.
FAYT Dynamic TabGroup.opusscriptinstall (12.0 KB)

V1.3 : Added ability to close all previously opened dynamic groups (extra entry in close command suggestions).
FAYT Dynamic TabGroup.opusscriptinstall (9.9 KB)

V1.2 : Fixed typo in textcolor value
FAYT Dynamic TabGroup.opusscriptinstall (9.6 KB)

V1.1 : Initial release
FAYT Dynamic TabGroup.opusscriptinstall (9.6 KB)

Main concept

The main usage is when you need to use TabGroups for specific tasks, but only needs them temporarily.
This FAYT script will remember which tabs were opened when one group was loaded and will allow to close them all at a later stage.
The script introduces some commands that you can access through the FAYT interface (and is inspired by one of the examples given in this section of the forum).
Version 2.0 introduced the FAYTDynGroupsManage command that can be used in a menu or toolbar to generate dynamic buttons that replicates the open/append/close actions.

Available commands

Once you enter the FAYT through its shortcut key ($ by default) you can either :

  • open / append : lists all the TabGroups from your settings to let you choose which one to open (just after the active tabs) or append (at the end of all opened tabs).

    • Each TabGroup is presented alongside information about it : wether it's a single or dual pane (single = ▬ / dual = ▬ ▬), and the number of tabs (plus their up/left and down/right location when dual pane tab group : ◤ 4 tabs | ◢ 2 tabs)
    • Single pane tab groups are opened on the active pane / Dual pane tab groups are opened according to your settings for this tab group
    • Tab groups are always opened without closing other tabs even if it configured to do so, since it goes against the concept of this FAYT script.
    • Note that if "Open new tabs next to the active tab" is not checked under Settings/Tabs : open and append will have the same behavior and will be appending new tabs after all the already opened tabs.
  • close : lists all opened groups and let you choose which group you want to close tabs. Tabs are closed based on the tab group they were loaded from, even if you changed its location afterwards.

    • Example shows that two groups have been opened (from tabs names and from the list of options presented in FAYT field) and you can choose which one to close (giving you information about how many tabs will be closed in each pane)
    • [V1.3] A close *ALL* entry is added which permits to close all dynamic tabs in a single operation (number of groups and tabs per pane are listed in the suggestion list).
  • make permanent : Lets you choose one group that you don't want to close by mistake. It becomes "permanent" in the lister and won't be presented in the close list.

  • help : displays a window explaining each command

  • forget all : same as make permanent but for all opened tab groups

  • rebase group ids : see help.

Comand to use in a menu/toolbar

FAYTDynGroupsManage

Option Switch type Comment
LISTGROUPS /O Can take the values [OPEN,APPEND,CLOSE]. Put in a menu or toolbar, will generate the appropriate buttons.
RUN_ACTION /K For the iiner works of the script. Called when a generated button is clicked (authorized values: [OPEN,APPEND,CLOSE])
RUN_ARG /K For the iiner works of the script. Contains information for the RUN_ACTION. For open/append will accept the full path to the Tab Group name (including the potential folder structure). For close, accepts the tab group Id that has been assigned by the script when the Tab Group was loaded.

Usage example:
TabGroups Manager.dcf (1.6 KB)

Configuration

The buttons section is dedicated to the settings for the

  • ID ordering mode : see help
  • Log to console : will display some debug option in console if set to Yes
  • Suggestion retention delay : to improve performance, each command's suggestion menu is not rebuilt unless this delay has passed since last display of a suggestion.
  • Tabs naming scheme : lets you define the way the tabs opened with open or append command are renamed. You can use %id in the name to make the group id appear in the tab name. Other available identifiers from GO TABNAME (%N, %R, %P, %!) can also be used.

Quick keys configuration

image

  • You can there enable an option to use aliases instead of full commands (by default the alias is the first letter of the command).

Source code

// Adds a FAYT Quick Key which contains 4 commands :
// * open: 		lists all the TabGroups declared within Opus so you can decide to add one.
//				Note that the group will be opened without closing other tabs, even if it'the group settings.
//
// * close: 	lists for the current lister, all groups that have been opened prevously with the add command
//				that still have tabs opeened in at least one of the pane.
//				Executing the command will close these tabs.
//				/!\ Tabs are deleted regardless of the current folder they are 
//
// * rebase Group IDs: 	Each time a tab group is added by open command, it is assigned an ID (starting at 1 for frst group, incrementing by 1).
//						Depending on the configuration of the script, ID management will either :
//						  - Always increment [incremental setting], even when groups are closed
//						  - Use ID equals to highest ID in use + 1 [optimized setting]
//						  - Use the smallest possible ID, filling the gaps left after closing groups, if any [extreme setting].
//						This command can be usefull when you're using the <b>incremental</b> setting and want to reset the current ID count to 'highest in use + 1' and restart from there for future groups.
//
// * make permanent: 	lists all current inserted groups. Executing the command will remove markers for that group so it can no longer be removed
//						by this script. It's made permanent (unless you decide to close the tabs by yourself).
//
// * forget All : 	Similar to making everything permanent. It will also reinitialize the group IDs to 1.
//
// * help : Will open a popup with the help for the different commands.
// ---

// (?:^| |\(|\[|\/|\\)([a-zA-ZäöüßÄÖÜ])

// V2.0 : Adding dynamic buttons list


function OnInit(initData) {
	initData.name = "FAYT Dynamic TabGroups";
	initData.version = "2.0";
	initData.copyright = "(c) 2024 Stéphane Alessio";
	initData.desc = "";
	initData.default_enable = true;
	initData.min_version = "13.16.1";
	initData.group = "FAYT Scripts";

	// settings & defaults
	initData.config = DOpus.Create().OrderedMap();
	initData.config_desc = DOpus.Create().OrderedMap();
	initData.config_groups = DOpus.Create().OrderedMap();
	initData.config_group_order = DOpus.NewVector('Buttons', 'Tabs', 'FAYT');

	var option_name = "";

	option_name = "Open buttons pre-label";
	initData.Config[option_name] = "⬛ ";
	initData.config_desc(option_name) = "";
	initData.config_groups(option_name) = "Buttons";

	option_name = "Open buttons image";
	initData.Config[option_name] = "#newtab";
	initData.config_desc(option_name) = "";
	initData.config_groups(option_name) = "Buttons";

	option_name = "Append buttons pre-label";
	initData.Config[option_name] = "Append";
	initData.config_desc(option_name) = "▶ ";
	initData.config_groups(option_name) = "Buttons";

	option_name = "Append buttons image";
	initData.Config[option_name] = "#duplicatetab";
	initData.config_desc(option_name) = "";
	initData.config_groups(option_name) = "Buttons";

	option_name = "Close buttons pre-label";
	initData.Config[option_name] = "Close #";
	initData.config_desc(option_name) = "🗙 ";
	initData.config_groups(option_name) = "Buttons";

	option_name = "Close buttons image";
	initData.Config[option_name] = "#closetab";
	initData.config_desc(option_name) = "";
	initData.config_groups(option_name) = "Buttons";

	option_name = "Tabs naming scheme";
	initData.Config[option_name] = "⬛%id▶ %N";
	initData.config_desc(option_name) = "Use '%id' where you want the group ID to be inserted. Other options (%N, %R, %P, %!) are described in Opus manual (GO TABNAME)";
	initData.config_groups(option_name) = "Tabs";
	// use with Script.Config["Tabs naming scheme"]

	option_name = "ID ordering mode";
	initData.Config[option_name] = "incremental";
	initData.config_desc(option_name) = "Defines the ordering mode of TabGroups identifiers.\n - incremental: new id is always latest + 1.\n - optimized: new id is highest still opened + 1.\n - extreme: new id is smallest available (filling gaps if any).";
	initData.config_groups(option_name) = "Tabs";

	option_name = "Log to console";
	initData.Config[option_name] = false;
	initData.config_desc(option_name) = "true to have some logs to console. false to make it mute.";
	initData.config_groups(option_name) = "Tabs";

	option_name = "Suggestions retention delay";
	initData.Config[option_name] = 4000;
	initData.config_desc(option_name) = "Represents the time in ms between two requests for suggestions under which the suggestion list is not rebuilt";
	initData.config_groups(option_name) = "FAYT";

}


///////////////////////////////////////////////////////////////////////////
// Define these values so our script is easier to read.
var FLAG_UseAliasInsteadOfCommand = (1<<0);
// var FLAG_OptB = (1<<1);
// var FLAG_OptC = (1<<2);
// ...


///////////////////////////////////////////////////////////////////////////
// Generic FAYTcommand class
function FAYTcommand(verb) {
	this.verb = verb;
	this.alias = verb.substring(0,1).toLowerCase();
	this.GetCommand = function(scriptFAYTData) { return ( (scriptFAYTData.flags & FLAG_UseAliasInsteadOfCommand)?this.alias:this.verb ); }
	this.lastSuggestionBuildTime = new Date(0);
	this.suggestionRetentionTime = 4000;
	this.latestSuggestions = DOpus.Create.Map();
	this.BuildSuggestions = function(scriptFAYTData) { this.latestSuggestions = null; return this.latestSuggestions; };
	this.ExecuteAction = function(scriptFAYTData) { return null; }
	this.MatchesInput = function(scriptFAYTData) { 
		return this.GetCommand(scriptFAYTData).match(new RegExp('^' + scriptFAYTData.cmdline.substring(0, this.GetCommand(scriptFAYTData).length), "i"));
	}
	this.CheckIfRebuildNeeded = function () {
		var curDate = new Date();

		var rebuildTimeMsg = new Array();
		rebuildTimeMsg.push("[" + this.verb.toUpperCase() + " menu] ");
		rebuildTimeMsg.push(((curDate.valueOf() - this.lastSuggestionBuildTime.valueOf()) < this.suggestionRetentionTime)?"use previous suggestions":"REBUILD");
		rebuildTimeMsg.push("diff=" + (curDate.valueOf() - this.lastSuggestionBuildTime.valueOf()) + " vs. ref=" + this.suggestionRetentionTime);
		rebuildTimeMsg.push("curDate=" + curDate.valueOf() + " / lastDate=" + this.lastSuggestionBuildTime.valueOf());
		dout(rebuildTimeMsg.join(" | "));

		return (!(curDate.valueOf() - this.lastSuggestionBuildTime.valueOf()) < this.suggestionRetentionTime);
	}
}

///////////////////////////////////////////////////////////////////////////


// DOpus.Output("************************ Building actions list");
FAYTcommandsList = DOpus.Create.Map();

// OPEN
// =====
openCommand = new FAYTcommand("open");
openCommand.BuildSuggestions = BuildGroupsSuggestionsFromSettings;
openCommand.ExecuteAction = FAYT_ExecuteOpenAction;
openCommand.GrpPaths = null;
openCommand.TabGroups = null;
FAYTcommandsList(openCommand.verb) = openCommand;


// APPEND
// =======
openAtEndCommand = new FAYTcommand("append");
openAtEndCommand.BuildSuggestions = BuildGroupsSuggestionsFromSettings;
openAtEndCommand.ExecuteAction = FAYT_ExecuteOpenAction;
openAtEndCommand.GrpPaths = null;
openAtEndCommand.TabGroups = null;
FAYTcommandsList(openAtEndCommand.verb) = openAtEndCommand;

// CLOSE
// ======
closeCommand = new FAYTcommand("close");
closeCommand.BuildSuggestions = function(sfd) {
	if (!this.CheckIfRebuildNeeded()) {
		this.lastSuggestionBuildTime = new Date();
		return this.latestSuggestions;
	}

	return BuildGroupsSuggestionsFromTabVars(this, sfd);
}
closeCommand.ExecuteAction = FAYT_ExecuteCloseAction;
closeCommand.GrpTabs = null;
FAYTcommandsList(closeCommand.verb) = closeCommand;

// HELP
// ====
helpCommand = new FAYTcommand("help");
helpCommand.alias = "help";
helpCommand.latestSuggestions("help") = "Displays help about Dynamic TabGroups commands.";
helpCommand.BuildSuggestions = function(sfd) { return this.latestSuggestions; };
helpCommand.ExecuteAction = FAYT_ExecuteHelpAction;
FAYTcommandsList(helpCommand.verb) = helpCommand;


// REBASE GROUP IDS
// ================
rebaseGpIdsCommand = new FAYTcommand("rebase group ids");
rebaseGpIdsCommand.alias = "rebase group ids";
rebaseGpIdsCommand.latestSuggestions("rebase group ids") = "Optimizes for future groups the highest group id to 'highest in use'+1";
rebaseGpIdsCommand.BuildSuggestions = function(sfd) { return this.latestSuggestions; };
rebaseGpIdsCommand.ExecuteAction = function(sfd) {
	var seqVar;
	sfd.tab.CloseFAYT();
	var lister = sfd.tab.lister;
	if (!lister.vars.exists("DynGrpMaxSeq")) return;
	seqVar = lister.vars("DynGrpMaxSeq");
	// dout("Current SeqIdValue = " + seqVar.value);
	var highestId = 0;
	for (var e = new Enumerator(lister.tabs); !e.atEnd(); e.moveNext()) {
		var tab = e.item();
		if (tab.vars.exists("DynGrpId")) {
			highestId = Math.max(highestId, tab.vars("DynGrpId"));
		}
	}
	seqVar.value = highestId;
	// dout("Newly optimized seq id = " + seqVar.value);
};
FAYTcommandsList(rebaseGpIdsCommand.verb) = rebaseGpIdsCommand;


// MAKE PERMANENT
// ==============
makePermanentCommand = new FAYTcommand("make permanent");
makePermanentCommand.BuildSuggestions = function(sfd) {
	if (!this.CheckIfRebuildNeeded()) {
		this.lastSuggestionBuildTime = new Date();
		return this.latestSuggestions;
	}

	return BuildGroupsSuggestionsFromTabVars(this, sfd);
}
makePermanentCommand.ExecuteAction = FAYT_ExecuteMakePermanentAction;
makePermanentCommand.GrpTabs = null;
FAYTcommandsList(makePermanentCommand.verb) = makePermanentCommand;


// FORGET ALL
// ==========
forgetAllCommand = new FAYTcommand("forget all");
forgetAllCommand.latestSuggestions("forget all") = "Make all currently opened dynamic groups permanent";
forgetAllCommand.BuildSuggestions = function(sfd) { return this.latestSuggestions; }
forgetAllCommand.ExecuteAction = FAYT_ExecuteForgetAllAction;
FAYTcommandsList(forgetAllCommand.verb) = forgetAllCommand;


// === END OF COMMANDS DEFINITIONS ========================================================================

function OnAddCommands(addCmdData) {
	var cmd = addCmdData.AddCommand();
	cmd.name = "FAYTDynGroups";
	cmd.method = "OnFAYTDynGroups";
	cmd.desc = "";
	cmd.label = "FAYTDynGroups";
	cmd.template = "";
	//cmd.dynamic_args = "LISTGROUPS";
	cmd.hide = true; // Hide from button editor menus
	cmd.icon = "script";

	var fayt = cmd.fayt;
	fayt.enable = true;
	fayt.key = "$";
	fayt.textcolor = "#ff8000";
	fayt.backcolor = "#000000";
	fayt.label = "Dynamic Tab Groups";
	fayt.flags = DOpus.Create.Map();
	fayt.realtime = true; // Call on each keypress, not just after return
	fayt.wantempty = false;

	fayt.flags[FLAG_UseAliasInsteadOfCommand] = "Use alias instead of command";
	// fayt.flags[FLAG_OptB] = "Option B";
	// fayt.flags[FLAG_OptC] = "Option C";
	// ...

	var cmd = addCmdData.AddCommand();
	cmd.name = "FAYTDynGroupsManage";
	cmd.method = "OnFAYTDynGroupsManage";
	cmd.desc = "";
	cmd.label = "FAYTDynGroupsManage";
	cmd.template = "LISTGROUPS/O[OPEN,APPEND,CLOSE],RUN_ACTION/K,RUN_ARG/K";
	cmd.dynamic_args = "LISTGROUPS";
	cmd.hide = false; // Hide from button editor menus
	cmd.icon = "script";

	var cmd = addCmdData.AddCommand();
	cmd.name = "FAYTDynGroups_ClearAll";
	cmd.method = "OnFAYTDynGroups_ClearAll";
	cmd.desc = "";
	cmd.label = "FAYTDynGroups_ClearAll";
	cmd.template = "";
	cmd.hide = false; // Hide from button editor menus
	cmd.icon = "script";
}

function OnFAYTDynGroups(scriptFAYTData) {
	//dout("***IN*** '" + scriptFAYTData.cmdline + "' Flags=" + scriptFAYTData.flags + " Key=" + scriptFAYTData.key  + "(" + scriptFAYTData.key.length + ")" + " suggest=" + scriptFAYTData.suggest);
	//Script.UpdateButtons(true);

	var localSuggestions = DOpus.Create.Map();

	// dout("fayt = "+ scriptFAYTData.fayt);
	if (scriptFAYTData.fayt != "FAYTDynGroups")
	{
		dout('Unexpected FAYT: "' + scriptFAYTData.fayt + '"');
		scriptFAYTData.tab.CloseFAYT();
		return;
	}

	// Set suggestions retention delay for all actions
	SetRetentionDelayOnActions();

	// Build suggestion list from possible actions (all matching actions feed the suggestions list)
	for (var e = new Enumerator(FAYTcommandsList); !e.atEnd(); e.moveNext()) {
		// dout("Command <" + FAYTcommandsList(e.item()).verb + "> ==> GetCommand = <" + FAYTcommandsList(e.item()).GetCommand(scriptFAYTData) + ">");
		if (FAYTcommandsList(e.item()).MatchesInput(scriptFAYTData))
			localSuggestions.merge(FAYTcommandsList(e.item()).BuildSuggestions(scriptFAYTData));
	}

	var tab = scriptFAYTData.tab;

	if (scriptFAYTData.key == "return")
	{
		//Script.UpdateButtons(true);
		// Execute the action upon each FAYTcommand that provided this suggestion (should be only one)
		for (var e = new Enumerator(FAYTcommandsList); !e.atEnd(); e.moveNext())
			if (FAYTcommandsList(e.item()).latestSuggestions.exists(scriptFAYTData.cmdline))
				FAYTcommandsList(e.item()).ExecuteAction(scriptFAYTData);
		return;
	}

	if (scriptFAYTData.suggest)
	{
		// dout("Suggesting " + ((localSuggestions!=null)?localSuggestions.count:0) + " results");
		tab.UpdateFAYTSuggestions(localSuggestions);
	}
}

function OnFAYTDynGroupsManage(scriptCommandData) {
	//dout("** Managing Dyn Tab Groups **");
	var args = scriptCommandData.func.args;

	if (!args.got_arg.RUN_ACTION)
		return;

	if (!args.got_arg.RUN_ARG) {
		dout("** ERROR: Missing argument RUN_ARG.");
		return;
	}

	if (args.RUN_ACTION.toLowerCase() == "append" || args.RUN_ACTION.toLowerCase() == "open") {
		var allTabGroups = GetGroupsFromSettings(DOpus.TabGroups);
		if (!allTabGroups.exists(args.RUN_ARG)) {
			dout("Unable to find this group (" + args.RUN_ARG + ") in the existing tab groups (#" + allTabGroups.count + " items).");
		}
		DoOpenAction(scriptCommandData.func.sourcetab, allTabGroups(args.RUN_ARG), args.RUN_ARG, args.RUN_ACTION.toLowerCase());
	}
	if (args.RUN_ACTION.toLowerCase() == "close")
		RunCloseAction(scriptCommandData.func.sourcetab.lister, args.RUN_ARG);
}

function OnAddButtons(addButtonsData) {
	// Make sure we're being called because of the LIST argument, otherwise bail.
	if (!addButtonsData.args.got_arg.LISTGROUPS)
		return false;
	// dout("ListGroups arg = " + addButtonsData.args.LISTGROUPS);

	if (addButtonsData.args.LISTGROUPS.toLowerCase() == "append" || addButtonsData.args.LISTGROUPS.toLowerCase() == "open") {
		var allTabGroups = BuildOpenButtonsFromSettings(DOpus.TabGroups, addButtonsData.buttons, addButtonsData.args.LISTGROUPS.toLowerCase());
		// dout("allTabGroups populated with #" + allTabGroups.count + " items");
	}
	else if (addButtonsData.args.LISTGROUPS.toLowerCase() == "close") {
		var allOpenedMarkedTabs = BuildCloseButtonsFromTabVars(addButtonsData.lister, addButtonsData.buttons);
	}

	return true;
}

function SetRetentionDelayOnActions() {
	for (var e = new Enumerator(FAYTcommandsList); !e.atEnd(); e.moveNext())
		FAYTcommandsList(e.item()).suggestionRetentionTime = Script.Config["Suggestions retention delay"];
}

function FAYT_ExecuteOpenAction(scriptFAYTData) {
	var name = scriptFAYTData.cmdline;
	var tab = scriptFAYTData.tab;

	if (this.latestSuggestions.exists(scriptFAYTData.cmdline))
		dout("[" + this.verb + "] Execute on : '" + name + "' '>> Group Name : '" + this.GrpPaths(name) + "'");
	else
		dout("Trying to execute on unknown group");

	// Checking call is made for the right command - Really necessary ???
	var regex = new RegExp("^" + this.GetCommand(scriptFAYTData) + " ", "i");
	if (!name.match(regex)) {
		dout("Unknown action : " + name);
		return;
	}

	tab.CloseFAYT();
	// dout("Request to open group '" + this.GrpPaths(name) + "' from " + this.verb + " command");

	var tabGroup = this.TabGroups(name);
	var grpPath = this.GrpPaths(name);
	var action = this.verb;

	DoOpenAction(tab, tabGroup, grpPath, action);
}

function DoOpenAction(tab, tabGroup, grpPath, action) {
	// If open is called in "append" mode, we need to make the last tabs of each pane active (by selecting them)
	var cmd = DOpus.Create.Command();
	tab.lister.Update();
	var srcLastTab 	= null;
	var destLastTab = null;
	var activePane = "right"; 	// will be changed to left in the loop below if active tab is found in the lister.tabsleft
	if (action == "append") {
		// dout("Need to do special things to open tabs 'at the end'");
		var lastLeftTab = null;
		for (var e = new Enumerator(tab.lister.tabsleft); !e.atEnd(); e.moveNext()) {
			// dout("Parsing left tabs => " + e.item().displayed_label);
			lastLeftTab = e.item();
			if (lastLeftTab == tab.lister.activetab) activePane = "left";
		}
		if (activePane == "left") srcLastTab = lastLeftTab;
		else destLastTab = lastLeftTab;

		var lastRightTab = null;
		for (var e = new Enumerator(tab.lister.tabsright); !e.atEnd(); e.moveNext()) {
			// dout("Parsing right tabs => " + e.item().displayed_label);
			lastRightTab = e.item();
		}
		if (activePane == "right") srcLastTab = lastRightTab;
		else destLastTab = lastRightTab;


		// We need to 'select' (e.g. make active) the last tab on src & dest 
		// ... but do it only if the tabgroup we're about to open has tabs to open there

		var opensTabsInSrc 	= true;		// by default - will be true for all "non-dual" tabgroups.
		var opensTabsInDest = false;	// by default - will be false for all "non-dual" tabgroups.
		if (tabGroup.dual) {
			opensTabsInSrc = (activePane == "left")?(tabGroup.lefttabs.count > 0):(tabGroup.righttabs.count > 0);
			opensTabsInDest = (activePane == "left")?(tabGroup.righttabs.count > 0):(tabGroup.lefttabs.count > 0);
		}
		// dout("opensTabsInSrc = " + opensTabsInSrc + " | opensTabsInDest = " + opensTabsInDest);

		if (tab.lister.dual != 0 && opensTabsInDest)
			cmd.RunCommand("Go TABSELECT=$" + destLastTab);
		if (opensTabsInSrc)
			cmd.RunCommand("Go TABSELECT=$" + srcLastTab);
	}

	// Loading the selected TabGroup
	cmd.AddLine('GO TABGROUPLOAD "' + grpPath + '" TABCLOSEALL=no');
	var r = cmd.Run();

	// Getting the newly opened tabs to rename them and set the tab vars to identofy them later (other commands such as close, etc ...)
	var openedTabs = DOpus.Create.Vector();
	var seqId = GetNextSequenceId(tab.lister);

	cmd.Clear();

	// Setting tab vars
	for (var e = new Enumerator(cmd.results.newtabs); !e.atEnd(); e.moveNext()) {
		var newTab = e.item();
		// dout("  New Tab => " + newTab.displayed_label + " >> " + newTab.path);
		newTab.vars.Set("DynGrpId", seqId);
		newTab.vars("DynGrpId").persist = true;
		newTab.vars.Set("DynGrpName", grpPath);
		newTab.vars("DynGrpName").persist = true;
		openedTabs.push_back(newTab);
	}

	tab.lister.Update();
	var activeTab = tab.lister.activetab;
	// dout("== Active tab is on the " + ((activeTab.right)?"Right/Bottom pane":"Left/Up pane"));
	var currentPane = (activeTab.right)?"right":"left";

	// Renaming (if a pattern has been set by the user in the script config)
	var newTabName = Script.Config["Tabs naming scheme"].replace(/%id/, seqId);
	// dout("New tab name : " + newTabName);
	if (newTabName != "") {
		for (var e = new Enumerator(openedTabs); !e.atEnd(); e.moveNext()) {
			var newTab = e.item();
			// dout("Tab " + newTab.displayed_label + " is on " + ((newTab.right)?"Right/Bottom pane":"Left/Up pane"));
			cmd.Clear();

			cmd.SetSourceTab(newTab);
			cmd.AddLine('GO TABNAME="' + newTabName + '"');
			var res = cmd.Run();
			// dout("Command status (>0 expected) = " + res + " | " + cmd.results.result);
		}
	}	
}

function FAYT_ExecuteCloseAction(scriptFAYTData) {
	var name = scriptFAYTData.cmdline;
	var tab = scriptFAYTData.tab;

	if (this.latestSuggestions.exists(scriptFAYTData.cmdline))
		// dout("[" + this.verb + "] Execute on : '" + name + "' '>> Group Name : '" + this.latestSuggestions(name) + "'");
		dout("[" + this.verb + "] Execute on : '" + name + "' '>> # tabs = '" + this.GrpTabs(name).count + "'");
	else
		dout("Trying to execute on unknown group");

	var regex = new RegExp("^" + this.GetCommand(scriptFAYTData) + " ", "i");
	if (!name.match(regex)) {
		dout("Unknown action : " + name);
		return;
	}

	tab.CloseFAYT();
	// dout("Request to close group with " + this.GrpTabs(name).count + " tabs.");
	var tabsList = this.GrpTabs(name);
	DoCloseAction(tabsList);
}

function RunCloseAction(lister, grpIdToClose) {
	// Note: grpIdToClose == -1 <=> Close All Dyn Tab Groups
	var allTabs = GetAllMarkedTabs(lister, grpIdToClose);
	if (grpIdToClose != -1 && allTabs.count > 1) {
		dout("CLOSE ERROR: 0 or 1 key should have been found.");
		return;
	}

	var listToClose = DOpus.Create.Vector();
	for (var e = new Enumerator(allTabs); !e.atEnd(); e.moveNext()) {
		// Extract list from map returned by GetAllMarkedTabs
			// Iterate on key from the returned map -- Should only be one unless call with grpId == -1
			// For each key, the item is a map with two entries "left" and "right", and each entry as a value of { id: , name: , list}
			// Check Ids match, otherwise error
		var key = e.item();
		var item = allTabs(key);
		if (item.Exists("left")) {
			if (grpIdToClose != -1 && item("left").id != grpIdToClose) {
				dout("Unexpected Id found: " + item("left").id + " / when expecting: " + grpIdToClose);
				continue;
			}
			listToClose.append(item("left").list);
		}
		if (item.Exists("right")) {
			if (grpIdToClose != -1 && item("right").id != grpIdToClose) {
				dout("Unexpected Id found: " + item("right").id + " / when expecting: " + grpIdToClose);
				continue;
			}
			listToClose.append(item("right").list);
		}
	}
	// Send list to DoCloseAction(list)
	DoCloseAction(listToClose);
}

function DoCloseAction(tabsList) {
	var cmd = DOpus.Create.Command();
	for (var e = new Enumerator(tabsList); !e.atEnd(); e.moveNext()) {
		// DOpus.Output("closing tab ...");
		// dout("Trying to close tab '" + e.item().displayed_label + "'");
		cmd.AddLine("GO TABCLOSE=" + e.item());
	}
	cmd.Run();

	return;
}

function FAYT_ExecuteMakePermanentAction(scriptFAYTData) {
	var name = scriptFAYTData.cmdline;
	var tab = scriptFAYTData.tab;

	if (this.latestSuggestions.exists(scriptFAYTData.cmdline))
		// dout("[" + this.verb + "] Execute on : '" + name + "' '>> Group Name : '" + this.latestSuggestions(name) + "'");
		dout("[" + this.verb + "] Execute on : '" + name + "' '>> # tabs = '" + this.GrpTabs(name).count + "'");
	else
		dout("Trying to execute on unknown group");

	var regex = new RegExp("^" + this.GetCommand(scriptFAYTData) + " ", "i");
	if (!name.match(regex)) {
		dout("Unknown action : " + name);
		return;
	}

	tab.CloseFAYT();
	dout("Request to make permanent group with " + this.GrpTabs(name).count + " tabs.");

	var id = -1;
	for (var e = new Enumerator(this.GrpTabs(name)); !e.atEnd(); e.moveNext()) {
		var tab = e.item();
		// dout("Trying to make permanent tab '" + tab.displayed_label + "'");
		if (tab.vars.Exists("DynGrpId")) tab.vars.Delete("DynGrpId");
		if (tab.vars.Exists("DynGrpName")) tab.vars.Delete("DynGrpName");	
	}

	return;
}

function FAYT_ExecuteForgetAllAction(scriptFAYTData) {
	var name = scriptFAYTData.cmdline;
	var tab = scriptFAYTData.tab;

	if (this.latestSuggestions.exists(scriptFAYTData.cmdline))
		// dout("[" + this.verb + "] Execute on : '" + name + "' '>> Group Name : '" + this.latestSuggestions(name) + "'");
		dout("[" + this.verb + "] Executing.");
	else
		dout("Trying to execute on unknown group");

	var regex = new RegExp("^" + this.GetCommand(scriptFAYTData) + " ", "i");
	if (!name.match(regex)) {
		dout("Unknown action : " + name);
		return;
	}

	tab.CloseFAYT();
	// dout("Request to forget all.");

	var id = -1;
	for (var e = new Enumerator(scriptFAYTData.tab.lister.tabs); !e.atEnd(); e.moveNext()) {
		var tab = e.item();
		// dout("Trying to make permanent tab '" + tab.displayed_label + "'");
		if (tab.vars.Exists("DynGrpId")) tab.vars.Delete("DynGrpId");
		if (tab.vars.Exists("DynGrpName")) tab.vars.Delete("DynGrpName");	
	}

	return;
}

function FAYT_ExecuteHelpAction(scriptFAYTData) {
	var tab = scriptFAYTData.tab;
	tab.CloseFAYT();

	var Dlg = DOpus.Dlg;
	Dlg.window = tab;
	Dlg.template = "helpDlg";
	Dlg.Show();
}

function BuildGroupsSuggestionsFromSettings(scriptFAYTData) {
	if (!this.CheckIfRebuildNeeded()) {
		this.lastSuggestionBuildTime = new Date();
		return this.latestSuggestions;
	}

	this.latestSuggestions = DOpus.Create.Map();
	this.GrpPaths = DOpus.Create.Map();
	this.TabGroups = DOpus.Create.Map();

	// Parse TabGroups
	// Analyzing tabgroups
	var allTabGroups = GetGroupsFromSettings(DOpus.TabGroups);
	// dout("Total # of tabGroups : " + allTabGroups.count);

	for (var e = new Enumerator(allTabGroups); !e.atEnd(); e.moveNext()) {
		var grpPath = e.item();
		// dout("grpPath = " + grpPath);
		var grp = allTabGroups(grpPath);
		var hint = grpPath;
		// dout("padding after with " + (maxGroupLength + 6 - grpPath.length) + " spaces");
		hint += "       ";
		if (grp.desc != "")
			hint += "  { " + grp.desc + " }       ";
		hint += GetGroupInfo(grp).tabInfos;

		// this.latestSuggestions("open " + GetExtendedInitials(initials, this.latestSuggestions)) = e.item();
		var initials = GetInitials(grpPath);
		var grpCommand = this.GetCommand(scriptFAYTData) + " " + GetExtendedInitials(this, initials, scriptFAYTData);
		this.latestSuggestions(grpCommand) = hint;

		this.GrpPaths(grpCommand) = grpPath;
		this.TabGroups(grpCommand) = grp;
	}

	this.lastSuggestionBuildTime = new Date();
	return this.latestSuggestions;
}

function BuildGroupsSuggestionsFromTabVars(faytCommand, scriptFAYTData) {
	if (!faytCommand.CheckIfRebuildNeeded()) {
		faytCommand.lastSuggestionBuildTime = new Date();
		return faytCommand.latestSuggestions;
	}

	faytCommand.latestSuggestions = DOpus.Create.Map();
	faytCommand.GrpTabs = DOpus.Create.Map();

	// Find all tabs marked as "in a dynamic group" (based on tab vars) both in leftabs and in righttabs
	var allTabs = GetAllMarkedTabs(scriptFAYTData.tab.lister);

	// Enumerate all keys to build suggestions & GrpTabs object.
	var nbGroups = 0;
	var nbLeft = 0;
	var nbRight = 0;
	var allMatchingTabs = DOpus.Create.Vector();

	for (var e = new Enumerator(allTabs); !e.atEnd(); e.moveNext()) {
		var key = e.item();
		var tabs = allTabs(e.item());

		var grpCommand = "";

		if (tabs.exists("left")) {
			var id = tabs("left").id;
			var grpName = tabs("left").name;
			nbLeft += tabs("left").list.count;

			var initials = GetInitials(tabs("left").id + " - " + tabs("left").name);
			grpCommand = faytCommand.GetCommand(scriptFAYTData) + " " + GetExtendedInitials(faytCommand, initials, scriptFAYTData);
			faytCommand.GrpTabs(grpCommand) = tabs("left").list;
			allMatchingTabs.append(tabs("left").list);
		}

		if (tabs.exists("right")) {
			var id = tabs("right").id;
			var grpName = tabs("right").name;
			nbRight += tabs("right").list.count;

			if (grpCommand == "") {	// There were no 'left/up' tabs for this group
				var initials = GetInitials(tabs("right").id + " - " + tabs("right").name);
				grpCommand = faytCommand.GetCommand(scriptFAYTData) + " " + GetExtendedInitials(faytCommand, initials, scriptFAYTData);
				oldTabs = DOpus.Create.Vector();
			}
			else {
				// hint = faytCommand.latestSuggestions(grpCommand) + " | ";
				oldTabs = faytCommand.GrpTabs(grpCommand);
			}

			oldTabs.append(tabs("right").list);
			allMatchingTabs.append(tabs("right").list);
			faytCommand.GrpTabs(grpCommand) = oldTabs;
		}

		var hint = "[Grp:" + id + "] " + grpName + "    [ ";
		hint += GetHint(scriptFAYTData.tab.lister, nbLeft, nbRight);
		hint += " ]"

		faytCommand.latestSuggestions(grpCommand) = hint;
	}

	// V1.3 : Adding a close ALL command
	var closeAllCommand = faytCommand.GetCommand(scriptFAYTData) + " *ALL*";
	var hint  = "[Grp:ALL (#" + allTabs.count + ")]" + "    ";
	if (scriptFAYTData.tab.lister.dual == 0)		// Single Display
		hint += "[ ▬  " + nbLeft + " tab" + ((nbLeft>1)?"s ]":" ]");
	else if (scriptFAYTData.tab.lister.dual == 1) {	// Dual Display - Vertical Layout 
		hint += "[ ◀ " + nbLeft + " tab" + ((nbLeft>1)?"s ":" ");			//  ◀
		hint += "▶ " + nbRight + " tab" + ((nbRight>1)?"s ":" ") + "]";		//  ▶
	}
	else if (scriptFAYTData.tab.lister.dual == 2) {	// Dual Display - Horizontal Layout
		hint += "[ ▲ " + nbLeft + " tab" + ((nbLeft>1)?"s ":" ");			//  ▲
		hint += "▼ " + nbRight + " tab" + ((nbRight>1)?"s ":" ") + "]";		//  ▼
	}
	faytCommand.GrpTabs(closeAllCommand) = allMatchingTabs;
	faytCommand.latestSuggestions(closeAllCommand) = hint;


	faytCommand.lastSuggestionBuildTime = new Date();
	return faytCommand.latestSuggestions;
}

function GetAllMarkedTabs(lister, id) {
	searchId = id || -1; 	// -1 = Search All, otherwise search only a specific Id

	var allTabs = DOpus.Create.Map();
	lister.Update();	// refresh with new tabs, deleted tabs, etc ...
	for (var e = new Enumerator(lister.tabsleft); !e.atEnd(); e.moveNext()) {
		var tab = e.item();
		// dout("working on tab '" + tab.displayed_label + "'");
		if (tab.vars.exists("DynGrpId")) {
			if (searchId != -1 && searchId != tab.vars("DynGrpId"))	// if we search for a specific Id, skip non matching tabs
				continue;
			var grpName = "[Unknown]";
			if (tab.vars.exists("DynGrpName"))
				grpName = tab.vars("DynGrpName")

			var key = tab.vars("DynGrpId") + "_" + grpName;
			if (!allTabs.exists(key)) {
				allTabs(key) = DOpus.Create.Map();
				allTabs(key)("left") = {"id": tab.vars("DynGrpId"), "name": grpName, "list": DOpus.Create.Vector() };
			}
			allTabs(key)("left").list.push_back(tab);
		}
	}
	for (var e = new Enumerator(lister.tabsright); !e.atEnd(); e.moveNext()) {
		var tab = e.item();
		if (tab.vars.exists("DynGrpId")) {
			if (searchId != -1 && searchId != tab.vars("DynGrpId"))	// if we search for a specific Id, skip non matching tabs
				continue;
			var grpName = "[Unknown]"
			if (tab.vars.exists("DynGrpName"))
				grpName = tab.vars("DynGrpName");

			var key = tab.vars("DynGrpId") + "_" + grpName;
			if (!allTabs.exists(key))
				allTabs(key) = DOpus.Create.Map();
			if (!allTabs(key).exists("right"))
				allTabs(key)("right") = { "id": tab.vars("DynGrpId"), "name": grpName, "list": DOpus.Create.Vector() };
			allTabs(key)("right").list.push_back(tab);
		}
	}

	return allTabs;	
}

function GetExtendedInitials(faytCommand, initials, scriptFAYTData) {
	// dout("GetExtendedInitials for '" + initials + "'");
	if (!faytCommand.latestSuggestions.exists(faytCommand.GetCommand(scriptFAYTData) + " " + initials)) return initials;
	// dout("Already exists");
	var i=1;
	while (faytCommand.latestSuggestions.exists(faytCommand.GetCommand(scriptFAYTData) + " " + initials + "_" + i)) i++;
	// dout("Requires renaming to '" + initials + "_" + i + "'");
	return initials + "_" + i;
}

function GetGroupsFromSettings(tabGroupList, path) {
	var rootPath = path || "";
	// dout("Path = '" + rootPath + "' | Input tabGroupList #" + tabGroupList.count + " items");
	var m = DOpus.Create.Map();
	for (var e = new Enumerator(tabGroupList); !e.atEnd(); e.moveNext()) {
		var grp = e.item();
		if (grp.hidden) continue;
		// dout("GetGroupsFromSettings : Processing : " + grp.name);
		if (grp.folder)
			m.merge(GetGroupsFromSettings(grp, rootPath + grp.name + "/"));
		else
			m(rootPath + grp.name) = grp;
	}
	// dout("Ending GetGroupsFromSettings for path=" + rootPath);
	return m;
}

function BuildOpenButtonsFromSettings(tabGroupList, addButtonHelper, action, path) {
	var rootPath = path || "";
	// dout("Rebuilding Open/Append buttons for path (" + rootPath + ") ...");
	var prefix = (action == "open")?Script.Config["Open buttons pre-label"]:Script.Config["Append buttons pre-label"] + " ";
	var icon = (action == "open")?Script.Config["Open buttons image"]:Script.Config["Append buttons image"];
	var folderIcon = "#folder";

	// dout("Path = '" + rootPath + "' | Input tabGroupList #" + tabGroupList.count + " items");
	var m = DOpus.Create.Map();
	for (var e = new Enumerator(tabGroupList); !e.atEnd(); e.moveNext()) {
		var grp = e.item();
		// dout("Processing GRP='" + grp.name + "'");
		if (grp.hidden) continue;
		// dout("GetGroupsFromSettings : Processing : " + grp.name);
		if (grp.folder) {
			if (!grp.count) continue;	// Do not process empty groups
			if (!CheckGroupNotEmpty(grp)) continue; 	// Do not process empty groups
			// dout("Group Folder : " + grp.name + " / Grp size = " + grp.count);
			var menu = addButtonHelper.AddMenu();
			menu.label = DoubleAmpersands(grp.name);
			// button.func = 'FAYTDynGroupsManage RUN_ACTION=' + action + ' RUN_ARG="' + rootPath + grp.name + '"';
			menu.image = folderIcon;
			menu.childimages = true;
			menu.notablabel = true;

			m.merge(BuildOpenButtonsFromSettings(grp, menu.children, action, rootPath + grp.name + "/"));
		}
		else {
			m(rootPath + grp.name) = grp;
			var button = addButtonHelper.AddButton();
			button.label = DoubleAmpersands(prefix + grp.name);
			button.func = 'FAYTDynGroupsManage RUN_ACTION=' + action + ' RUN_ARG="' + rootPath + grp.name + '"';
			button.image = icon;
			button.notablabel = true;
			var grpInfos = GetGroupInfo(grp);
			button.desc = grpInfos.tabInfos + ((grpInfos.desc == "")?"":("\n" + grpInfos.desc));
		}
	}
	return m;
}

function BuildCloseButtonsFromTabVars(lister, addButtonHelper) {
	// dout("Rebuilding Close buttons ...");
	var allMarkedTabs = GetAllMarkedTabs(lister);
	// dout("Found markers = #" + allMarkedTabs.count);

	var prefix = Script.Config["Close buttons pre-label"];
	var icon = Script.Config["Close buttons image"];

	var lastButton = undefined;
	for (var e = new Enumerator(allMarkedTabs); !e.atEnd(); e.moveNext()) {
		var dynGroupMarker = e.item();
		// dout("Marker = " + dynGroupMarker);

		var match = /^(\d+)_(.*)$/.exec(dynGroupMarker);
		var grpId = -1;
		var grpName = "";
		if (match && match.length == 3) {
			grpId =  +match[1];
			grpName = match[2];
		}
		if (grpId == -1) continue;	// Ignore group that can not be parsed correctly
		// dout("Processing Group #" + grpId + " - Name = '" + grpName + "'");

		var button = addButtonHelper.AddButton();
		button.label = DoubleAmpersands(prefix + grpId + ": " + GetLastPathPart(grpName));
		button.func = 'FAYTDynGroupsManage RUN_ACTION=close RUN_ARG=' + grpId;
		button.image = icon;
		button.notablabel = true;
		button.separator = false;

		var leftCount = 0;
		var rightCount = 0;
		if (allMarkedTabs(dynGroupMarker).exists("left")) leftCount = +allMarkedTabs(dynGroupMarker)("left").list.count;
		if (allMarkedTabs(dynGroupMarker).exists("right")) rightCount = +allMarkedTabs(dynGroupMarker)("right").list.count;

		button.desc = '\uD83D\uDCC1' + " " + grpName + "\n" + GetHint(lister, leftCount, rightCount);
		lastButton = button;
	}

	// If any dyn group found : add at the end a "Close ALL" after a separator !!!
	if (allMarkedTabs.count > 0) {
		lastButton.separator = true;

		var button = addButtonHelper.AddButton();
		button.label = DoubleAmpersands("Close ALL dynamic groups [" + allMarkedTabs.count + "]");
		button.func = 'FAYTDynGroupsManage RUN_ACTION=close RUN_ARG=-1';
		button.image = icon;
		button.notablabel = true;
		button.separator = false;
	}
}

function GetHint(lister, leftCount, rightCount) {
	var hint ="";
	if (lister.dual == 0)
		hint += " ▬  " + leftCount + " tab" + ((leftCount>1)?"s":"");
	else {
		var leftSymbol = "◀";
		var rightSymbol = "▶";
		if (lister.dual == 2) {
			leftSymbol = "▲";
			rightSymbol = "▼";
		}

		if (leftCount > 0)
			hint +=	leftSymbol + " " + leftCount + " tab" + ((leftCount>1)?"s ":" ");
		if (rightCount > 0) {
			if (leftCount>0) hint += " | ";
			hint +=	rightSymbol + " " + rightCount + " tab" + ((rightCount>1)?"s ":" ");
		}
	}
	return hint;
}

function GetGroupInfo(grp) {
	var hint = "";
	if (grp.dual) {
		// Manage extra desc for dual listers
		var leftCount = 0;
		if (grp.lefttabs.count != undefined && grp.lefttabs.count > 0)		leftCount = grp.lefttabs.count;
		var rightCount = 0;
		if (grp.righttabs.count != undefined && grp.righttabs.count > 0) 	rightCount = grp.righttabs.count;

		// hint += "▬▬  ";
		hint += "◤ " + ((leftCount > 0) ? leftCount : "no") 	+ " tab" + ((leftCount > 1) ? "s" : "") + " | ";
		hint += "◢ " + ((rightCount > 0) ? rightCount : "no") 	+ " tab" + ((rightCount > 1) ? "s" : "");
	}
	else {
		hint += "▬  " + grp.tabs.count + " tab" + ((grp.tabs.count > 1) ? "s" : "");
	}

	return {tabInfos: hint, desc: grp.desc };	
}

function CheckGroupNotEmpty(grp) {
	for (var e = new Enumerator(grp); !e.atEnd(); e.moveNext()) {
		var subgroup = e.item();
		// dout("Checking group:" + subgroup.name);
		if (subgroup.hidden) continue;
		if (subgroup.folder)
			if (CheckGroupNotEmpty(subgroup)) return true;
			else continue;
		else return true;
	}
	return false;
}

function GetInitials(s) {
	// dout("GetInitials: >" + s + "<");
	var reFolders = new RegExp("/([a-zA-Z])(?=.*?/)","g");
	var regexp = /(?:^| |\(|\[|\/|\\|-)([0-9a-zA-ZäöüßÄÖÜ])/g;

	var out = "";
	// First Letter of each folder in path
	var foldersFirstLetters = ("/"+s).match(reFolders);
	if (foldersFirstLetters) {
		// dout("folder match");
		for (var i = 0; i < foldersFirstLetters.length; i++) {
			// dout("adding:" + foldersFirstLetters[i]);
			out += foldersFirstLetters[i].replace('/', '');
		}
	}
	// dout("Folders 'id' = " + out);

	// Then get first letter of each word
	var r = GetLastPathPart(s).match(regexp);
	for (var i = 0; i < r.length; i++)
		out += r[i].replace(/ |\(|\[|\\/, '');

	// dout ("output=" + out.toUpperCase());
	return out.toUpperCase();
}

function GetNextSequenceId(lister) {
	// Get ID ordering mode to choose proper action
	var orderingMode = Script.Config["ID ordering mode"].toLowerCase();
	if (!arrayIncludes([ "incremental", "optimized", "extreme" ], orderingMode))
		orderingMode = "incremental";		// default mode when non valid mode in settings.

	// Read or create DynGrpMaxSeq variable on lister
	var seqVar;
	if (lister.vars.exists("DynGrpMaxSeq")) {
		// dout("Sequence exists");
		seqVar = lister.vars("DynGrpMaxSeq");
		dout("Seq Value found = " + seqVar.value);
	}
	else {
		lister.vars.Set("DynGrpMaxSeq", 0);
		dout("Sequence exists after adding ? = " + lister.vars.exists("DynGrpMaxSeq"));
		seqVar = lister.vars("DynGrpMaxSeq");
		seqVar.persist = true;
	}

	if (orderingMode == "incremental") {
		seqVar.value = seqVar.value + 1;
		dout("Lister seq id returned = " + seqVar.value);
		return seqVar.value;
	}
	else if (orderingMode == "optimized") {
		// Search highest used id
		var highestId = 0;
		for (var e = new Enumerator(lister.tabs); !e.atEnd(); e.moveNext()) {
			var tab = e.item();
			if (tab.vars.exists("DynGrpId")) {
				highestId = Math.max(highestId, tab.vars("DynGrpId"));
			}
		}
		seqVar.value = highestId + 1;
		return seqVar.value;
	}
	else {	// orderingMode == "extreme"
		// var highestId = 0;
		var grpIds = DOpus.Create.Map();
		for (var e = new Enumerator(lister.tabs); !e.atEnd(); e.moveNext()) {
			var tab = e.item();
			if (tab.vars.exists("DynGrpId")) {
				grpIds(tab.vars("DynGrpId")) = tab.vars("DynGrpId");
				// highestId = Math.max(highestId, tab.vars("DynGrpId"));
			}
		}

		// Setting  sequenceId in lister in case we then revert to another mode.
		seqVar.value = seqVar.value + 1;

		// Now, searching first available id
		var i = 1;
		while (grpIds.exists(""+i)) i++;
		dout("index " + i + " is available. Returning index.");
		return i;	
	}
}

function GetLastPathPart(s) {
	s = s + "";
	while (s.length > 0 && s.slice(-1) == "/")
	{
		s = s.slice(0,-1);
	}
	var slashPos = s.lastIndexOf("/");
	if (slashPos != -1)
	{
		s = s.slice(slashPos+1);
	}
	return s;
}


///////////////////////////////////////////////////////////////////////////
function arrayIncludes(arr, searchElement) {
	for (var i = 0; i < arr.length; i++)
		if ("" + arr[i] === "" + searchElement)
			return true;
	return false;
}

///////////////////////////////////////////////////////////////////////////
// Command to clear all variables in Lister and in tabs
// ... everything is forgottent ...
function OnFAYTDynGroups_ClearAll(scriptCmdData) {
	for(var e = new Enumerator(FAYTcommandsList); !e.atEnd(); e.moveNext()) {
		e.item().latestSuggestions = null;
		e.item().lastSuggestionBuildTime = 0;
	}

	var lister = scriptCmdData.func.sourcetab.lister;
	if (lister.vars.exists("DynGrpMaxSeq"))  lister.vars.Delete("DynGrpMaxSeq");
	for (var e = new Enumerator(lister.tabs); !e.atEnd(); e.moveNext()) {
		var tab = e.item();
		if (tab.vars.exists("DynGrpId")) tab.vars.Delete("DynGrpId");
		if (tab.vars.exists("DynGrpName")) tab.vars.Delete("DynGrpName");
	}
}

///////////////////////////////////////////////////////////////////////////
function DoubleAmpersands(str) {
	return str.replace(/&/g,"&&");
}

///////////////////////////////////////////////////////////////////////////
if (!String.prototype.padStart) {
	String.prototype.padStart = function padStart(targetLength,padString) {
        targetLength = targetLength>>0; //truncate if number or convert non-number to 0;
        // dout("in padstart / targetLength=" + targetLength);
        padString = String((typeof padString !== 'undefined' ? padString : ' '));
        if (this.length > targetLength) {
            return String(this);
        }
        else {
            targetLength = targetLength-this.length;
            if (targetLength > padString.length) {
                padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed
            }
            return padString.slice(0,targetLength) + String(this);
        }
    };
}

///////////////////////////////////////////////////////////////////////////
if (!String.prototype.repeat) {
	String.prototype.repeat = function(count) {
	    'use strict';
	    if (this == null)
			throw new TypeError('can\'t convert ' + this + ' to object');

	    var str = '' + this;
	    count = +count;
	    if (count != count)
			count = 0;

	    if (count < 0)
			throw new RangeError('repeat count must be non-negative');

	    if (count == Infinity)
			throw new RangeError('repeat count must be less than infinity');

	    count = Math.floor(count);
	    if (str.length == 0 || count == 0)
			return '';

	    // Ensuring count is a 31-bit integer allows us to heavily optimize the
	    // main part. But anyway, most current (August 2014) browsers can't handle
	    // strings 1 << 28 chars or longer, so:
	    if (str.length * count >= 1 << 28)
			throw new RangeError('repeat count must not overflow maximum string size');

	    var maxCount = str.length * count;
	    count = Math.floor(Math.log(count) / Math.log(2));
	    while (count) {
	       str += str;
	       count--;
	    }
	    str += str.substring(0, maxCount - str.length);
	    return str;
	};
}

///////////////////////////////////////////////////////////////////////////
// Helper dout function
function dout(msg, error, time) {
	if (error == undefined) error = false;
	if (time == undefined) time = true;
	// DOpus.Output("msg=" + msg);
	// var mode = Script.Config["ID ordering mode"];
	var logToConsole = Script.Config["Log to console"];
	
	if (logToConsole) DOpus.output(msg, error, time);
}


==SCRIPT RESOURCES
<resources>
	<resource name="helpDlg" type="dialog">
		<dialog height="316" lang="francais" resize="yes" title="Dynamic Groups FAYT help" width="580">
			<control close="0" default="yes" height="14" name="btClose" resize="xy" title="Close" type="button" width="62" x="510" y="296" />
			<control height="284" name="mupText1" resize="wh" scroll="yes" title="&lt;b&gt;&lt;#347deb&gt;open [o] / append [a]&lt;/#&gt;&lt;/b&gt;\nLists all the TabGroups declared within Opus so you can decide which one to add.\nExecuting the command will open the different tabs from the tab group (after active tabs with &lt;b&gt;&lt;#347deb&gt;open&lt;/#&gt;&lt;/b&gt;, at the end of all tabs with &lt;b&gt;&lt;#347deb&gt;append&lt;/#&gt;&lt;/b&gt;).\nIn a dual pane lister: if the selected tab group is a single pane group, tabs are opened in the active pane.\nIn a single pane lister: if the selected tab group is a dual pane group, lister is converted to dual pane, and tabs are opened as configured.\nNote that the group will be opened without closing other tabs, even if it&apos;s configured to do so.\n\n\n&lt;b&gt;&lt;#347deb&gt;close [c]&lt;/#&gt;&lt;/b&gt;\nLists all groups (for the current lister) that have been prevously opened with the &lt;b&gt;&lt;#347deb&gt;open&lt;/#&gt;&lt;/b&gt; command that still have tabs opened in at least one of the panes.\nExecuting the command will close these tabs.\n&lt;u&gt;/!\&lt;/u&gt;Tabs are closed regardless of the current folder they might now represent (e.g. even if it has changed since opening).\n\n\n&lt;b&gt;&lt;#347deb&gt;rebase group ids&lt;/#&gt;&lt;/b&gt;\nEach time a tab group is added by &lt;b&gt;&lt;#347deb&gt;open&lt;/#&gt;&lt;/b&gt; command, it is assigned an ID (starting at 1 for frst group, incrementing by 1).\nDepending on the configuration of the script, ID management will either :\n - Always increment [&lt;b&gt;incremental&lt;/b&gt; setting], even when groups are closed\n - Use ID equals to highest ID in use + 1 [&lt;b&gt;optimized&lt;/b&gt; setting]\n - Use the smallest possible ID, filling the gaps left after closing groups, if any [&lt;b&gt;extreme&lt;/b&gt; setting].\nThis command can be usefull when you&apos;re using the &lt;b&gt;incremental&lt;/b&gt; setting and want to reset the current ID count to &apos;highest in use + 1&apos; and restart from there for future groups.\n\n\n&lt;b&gt;&lt;#347deb&gt;make permanent [m]&lt;/#&gt;&lt;/b&gt;\nLists all current inserted groups.\nExecuting the command will remove markers for that group so it can no longer be removed by this script. It&apos;s like it&apos;s been made permanent (unless you decide to close the tabs by yourself).\n\n\n&lt;b&gt;&lt;#347deb&gt;forget all [f]&lt;/#&gt;&lt;/b&gt;\nSimilar to making everything permanent. It will also reinitialize the group IDs to 1.\n\n\n&lt;b&gt;&lt;#347deb&gt;help&lt;/#&gt;&lt;/b&gt;\nOpens this help window." type="markuptext" width="564" x="8" y="8" />
		</dialog>
	</resource>
</resources>

6 Likes

I am referring to code line numbers 212 and 213.

I tried to change the Find as you type background color to #FFFFFF but it didn't do anything.
The FAYT background color after entering $ is still #000000 black.
Am I looking in the wrong place ?

Is the double # syntax for the font color a typographical error ?

It works fine as it is though. I only need to type a couple blind characters of 'open' before I get the drop down menu. Then all is well.

Thankyou for posting this ! :slight_smile:
I know this code was quite an effort to do.

1 Like

Those define the default colors assigned to the script when first installed, but if you want to edit the colors in your config, it's done through Preferences / Filtering and Sorting / Quick Keys, like with the built-in FAYT modes.

1 Like

Thanks for the feedback.
I fixed the typo in the textcolor line.
V1.2 below :
FAYT Dynamic TabGroup.opusscriptinstall (9.6 KB)

Regarding the number of characters to enter before having the open suggestions, one is enough on my end.
If you want quicker access, you can toggle on the use of alias :

That way, open command will appear upon entering "o+Space" (add will go for "a+Space", and son on ...).

@Leo @PassThePeas
Thanks !

I have found this script very useful !
It has become one of my favorites.

So far I have only used open/append, help and close.

There is only one very small problem.
On my Win 10 machine, open and append have the same effect.
The dynamic tab group(s) is/are added to the right of all other tabs regardless of my choice of function or location of the active tab(s).
I did change $ to ^ using preferences Quick Keys, something Leo pointed out in the above post.
My machine has no difference in effect between ^o and ^a .

This is a very minor point as the script works fine.
Dragging tabs is more pragmatic In My Humble Opinion.
However, the code does include the two separate functions.

Also, perhaps it would be good to have a ^c all function ?

I am happy with this script.
My reason for posting is equally to suggest to other people to seriously try this script.
Thankyou @PassThePeas

Thanks for the feedback, and glad it is useful to you.
Regarding the similar behavior of open and append commands, it might be related to the global setting about how new tabs are added (Settings/Tabs) :

And for the close ALL function, this should do the trick (V1.3) :
FAYT Dynamic TabGroup.opusscriptinstall (9.9 KB)

3 Likes

Yes, checking Open new tabs next to the active tab
in Preferences / Folder Tabs / Folder Tab Bar fixed the open/append problem.
The commands now work as is documented in the help command.

The new close All function works perfectly.
Thankyou !

Here is a new version that takes advantage of the newly introduced feature to build dynamic buttons. I made it 2.0 since this is a major change as what the add-in can offer.

FAYT Dynamic TabGroup.opusscriptinstall (12.0 KB)

Requires: Opus 13.16.1 at least.
Changes:

  • Minor cosmetic changes to the way Tab Groups are displayed in the FAYT area
  • New command to generate dynamic buttons in a menu or a toolbar FAYTDynGroupsManage, that can be run with option LISTGROUPS:
    • LISTGROUPS=open will display buttons to open tab groups (presents a folder structure similar to the one you set up in the Settings/Tabs/Groups)
    • LISTGROUPS=append which replicates the append function of the FAYT, generating one button for each tab group customized
    • LISTGROUPS=close which lists all the groups that have been added to the current lister (one button per group, which closes the group upon clicking). One last additionnal button to close all opened groups.

This is what it can look like:

Associated menu/buttons settings:

Notes:

  • I had to refactor a bit of the old code so the code triggered by FAYT call and buttons call is the same
  • I reorganized the settings, with groups. I had to remove the script from the Script window and add it again to have the new settings properly displayed.
2 Likes

Thanks very much !
I worked on this for an hour or so this morning.

Also an excellent example of use of labels in menus.

1 Like

This was working very well here when you posted this.

Today I am getting a script error.
The tab groups open or append , but the script no longer knows them as Dynamic Tab Groups.

Have I done something wrong to cause this ?
I have tried both FAYT and the button mode. Both result in this.
I also attempted to restore an earlier Directory Opus backup, but it was before version 2.0.
I get the same error after installing 2.0 on the backup.

Same issue here. It was working fine earlier this week, and I just made an update to the last beta yesterday (13.16.5) and I get (almost) the same issue (error reports line 1000 here, which is even weirder since access to the vars object is on line 1001 as your error message reports).

I'll investigate to see if this is something that latest Opus beta broke, or if it's something that's wrong in the code that needs to be fixed.
I'll let you know as soon as I progress.

What Opus version are you running?

Looks like a recent update broke Lister.Update() method that leads to a corrupdate lister object.
I'll report it in the Help & Support section.

EDIT: If you want to follow resolution: A recent update broke Lister.Update() method: lister object gets corrupted (no more vars)

I am using 13.16.4 beta.
I almost didn't report this as I thought it must be something I had done wrong.
Thankyou !

It's been solved in 13.16.5.