Category and Sub-Category Attributes

Screenshot
Suppose you have two enumerated attributes, one called “Category” and the other called “Sub Category”. You want the enumerations available for “Sub Category” to be dependant on the value of “Category”. This cannot be done in doors using enumerated attributes.

However, with some simple DXL, you can create the illusion of this functionality. Instead of using enumerations, use ordinary string attributes and use a dialog to edit the values.

This utility implements a dialog box that allows the user to select values for “Category” and “Sub Category”. Whenever the “Category” value is changed, the available values for “Sub Category” are automatically updated.

The “pseudo enumerations” are implemented as string arrays. Each row contains a “Category” value in the first position and available “Sub Category” values in subsequent positions.

The following implements the dialog and contains the definitions of the category and sub category values.

/*
	Using pseudo enumerations to implement category and sub-category
	attribute values.

	Dialog displays two choice elements.
	The first contains choices for the main category.
	The second contains choices for the sub-category.

	The choices available for the sub-category are dependant on the
	selected main category.

	This script assumes the existance of two ordinary string attributes,
	the names of which are defined below.

	Enumeration values are defined in a string array.

	Tony Goodman.
*/

/***************************************
	Attribute Names

	These must exist - no check is made.
***************************************/
const string ATTR_MAIN_CATEGORY = "Category"
const string ATTR_SUB_CATEGORY  = "Sub Category"

/***************************************
	Indices into string array of enumeration values

	By defining these here we can change the number of categories without
	changing the rest of the code.
***************************************/
const int NUM_MAIN_CATEGORIES  = 3
const int NUM_SUB_CATEGORIES   = 10
const int NUM_ENTRIES          = NUM_MAIN_CATEGORIES + (NUM_MAIN_CATEGORIES * NUM_SUB_CATEGORIES)
const int MAIN_CATEGORY_OFFSET = NUM_SUB_CATEGORIES + 1

/***************************************
	"Enumeration" Values

	First entry on the left is the main category. This is followed by the
	sub category values.

	It is important that there are exactly the same number entries in this
	array for each category.

	Empty strings are ignored - this allows us to have a varying number of
	sub categories for each category.
***************************************/
const string CATEGORIES[NUM_ENTRIES] = {
"Cheeses", "Brie",    "Cheddar", "Stilton", "Camembert", "Edam", "", "", "", "", "",
"Meats",   "Chicken", "Lamb",    "Beef",    "Pork",      "",     "", "", "", "", "",
"Fruits",  "Orange",  "Banana",  "Apple",   "Mango",     "",     "", "", "", "", ""
}

/***************************************
	Dialog elements
***************************************/
DB  dbMain			= null
DBE dbeLabel        = null
DBE dbeMainCategory = null
DBE dbeSubCategory	= null

/***************************************
	Global variables
***************************************/
Module currModule = null
Object currObject = null

/***************************************
	isMainCategory

	returns true if the index to the string array is a main category.

	e.g. if the number of sub-categories is 10, then main categories
	are at indices 0, 11, 22, 33 ...
***************************************/
bool isMainCategory(int i)
{
	return(i % MAIN_CATEGORY_OFFSET == 0)
}

/***************************************
	getMainChoiceIndex

	given the choice selected in the dialog, returns the index to the string
	array.
***************************************/
int getMainChoiceIndex(string mainCategory)
{
	int i = 0

	for (i = 0; i < NUM_MAIN_CATEGORIES; i++)
	{
		if (CATEGORIES[i * MAIN_CATEGORY_OFFSET] == mainCategory)
		{
			return(i)
		}
	}

	return(-1)
}

/***************************************
	getSubChoiceIndex

	given the choices selected in the dialog, returns the index to the string
	array for the selected sub category.
***************************************/
int getSubChoiceIndex(int mainSelection, string subCategory)
{
	int i          = 0
	int startIndex = 0
	int endIndex   = 0

	// convert into index to string array
	startIndex = mainSelection + 1
	endIndex   = startIndex + NUM_SUB_CATEGORIES

	// find matching sub category
	for (i = startIndex; i < endIndex; i++)
	{
		if (CATEGORIES[i] == subCategory)
		{
			return(i - startIndex)
		}
	}

	return(-1)
}

/***************************************
	printCategories
***************************************/
void printCategories()
{
	int i = 0

	for (i = 0; i < NUM_ENTRIES; i++)
	{
		if (isMainCategory(i))
		{
			print("\nCategory: " CATEGORIES[i] "\n")
		}
		else
		{
			print("\t" CATEGORIES[i] "\t")
		}
	}
}

/***************************************
	initSubCategoryChoices

	initialise the choices displayed in the sub category DBE.
	If the current object has a value for this attribute then this value
	is selected, but only if it is a legal choice based on the current main
	category.
***************************************/
void initSubCategoryChoices()
{
	int i              = 0
	int mainSelection  = 0
	string subCategory = ""

	empty(dbeSubCategory)

	mainSelection = get(dbeMainCategory)

	// if there is no selection in the main category DBE
	// then leave this DBE empty.
	if (mainSelection < 0)
	{
		return
	}

	// convert into index to string array
	mainSelection = mainSelection * MAIN_CATEGORY_OFFSET

	// extract string values from array
	for (i = mainSelection + 1; i < mainSelection + NUM_SUB_CATEGORIES; i++)
	{
		if (CATEGORIES[i] == "")
		{
			break
		}

		insert(dbeSubCategory, noElems(dbeSubCategory), CATEGORIES[i])
	}

	// check current object for current atribute value
	subCategory = currObject.ATTR_SUB_CATEGORY ""

	set(dbeSubCategory, getSubChoiceIndex(mainSelection, subCategory))
}

/***************************************
	initMainCategoryChoices

	initialise the choices displayed in the main category DBE.
	If the current object has a value for this attribute then this value
	is selected.

	The sub category DBE is also updated.
***************************************/
void initMainCategoryChoices()
{
	int i = 0
	string mainCategory = ""

	// populate main category DBE
	for (i = 0; i < NUM_ENTRIES; i += MAIN_CATEGORY_OFFSET)
	{
		insert(dbeMainCategory, noElems(dbeMainCategory), CATEGORIES[i])
	}

	// check current object for current atribute value
	mainCategory = currObject.ATTR_MAIN_CATEGORY ""

	// convert mainCategory to choice index and set selection in DBE
	set(dbeMainCategory, getMainChoiceIndex(mainCategory))

	// initialise sub category choices based on setting of main category
	initSubCategoryChoices()
}

/***************************************
	doEditCategory

	Callback when user clicks OK button.
***************************************/
void doEditCategory(DB db)
{
	int mainSelection = 0
	int subSelection  = 0

	// get user selections from dialog elements
	mainSelection = get(dbeMainCategory)
	subSelection  = get(dbeSubCategory)

	// enforce selection of main category
	if (mainSelection < 0)
	{
		infoBox("Please select main category")
		return
	}

	// enforce selection of sub category
	if (subSelection < 0)
	{
		infoBox("Please select sub category")
		return
	}

	// adjust selections for for real index into string array
	mainSelection = mainSelection * MAIN_CATEGORY_OFFSET
	subSelection  = mainSelection + subSelection + 1

	// set object attributes
	currObject.ATTR_MAIN_CATEGORY = CATEGORIES[mainSelection]
	currObject.ATTR_SUB_CATEGORY  = CATEGORIES[subSelection]

	// refresh display to show changes
	refresh(currModule)

	// we are finished
	hide(dbMain)
	destroy(dbMain)
	dbMain = null
}

/***************************************
	doSelectMainCategory

	Callback when user makes a selection in the main category DBE.
	Call function to update the choices available for sub category.
***************************************/
void doSelectMainCategory(DBE dbe)
{
	initSubCategoryChoices()
}

/***************************************
	MAIN
***************************************/
currModule = current Module

if (null currModule)
{
	infoBox("This utility can only be run from a module")
	halt
}

currObject = current Object

if (null currObject)
{
	infoBox("No current object.")
	halt
}

dbMain = create(currModule, "Edit Category")

dbeLabel = label(dbMain,
                 "Description of edit category utility.\n")

dbeMainCategory  = choice(dbMain, "Main Category: ", dummy, 0)
dbeSubCategory = choice(dbMain, "Sub Category: ", dummy, 0)

apply(dbMain, "OK", doEditCategory)

set(dbeMainCategory, doSelectMainCategory)

realize(dbMain)

initMainCategoryChoices()

show(dbMain)

This code allows you to add the script to the popup menu of a formal module.

//  Edit Category Menu Option

/*
	Creates a menu option to invoke the editCategory.dxl script. 

    Install this file in DOORSHOME/lib/dxl/config/formalPopupFiles
    for this menu option to appear on the formal module popup menu.

    Tony Goodman.
*/

createItem(alwaysOn,
		   "Edit Category ...",
		   null,
	   	   null,
		   modKeyCtrl,
		   null,
		   null,
		   "Edit Category.",
		   "",
		   doorsHome "lib/dxl/addins/editCategory.dxl")