Smart History Viewer

This script allows you to view object and module history across all baselines. The dialog displays history in listviews on three tabs, showing complete object, module and session history.

The object tab displays history for the current object. You can browse through objects using the Prev and Next buttons or select the object directly by selecting it in the module window. The module tab displays all module history as well as object deletions. The sessions tab displays the history of all editing sessions and baseline creations. All listviews can be sorted by any column.

This script demonstrates some very strange vaguaries of the history system such as the fact that module level attribute changes are recorded as modifyObject events, but with the object absolute number being zero! (see lines 924 and 971). Take a look at line 1009 to see how session numbers are handled.

For those of you that are interested, this script is a good example of how to use the DxlObject type together with skip lists to build practical data structures.

//  Smart History Viewer - Standalone version.

/*
    S M A R T   D X L   L I M I T E D

	Description:

	Provides a dialog to view history accross multiple baselines.

	Save this file, and tophat.bmp to <DOORS ADDINS>/SmartDXL

	Author:	Tony Goodman	 tony@smartdxl.com

    	Change History:

	25-NOV-2008		Tony Goodman	Initial version.

	24-FEB-2011		Tony Goodman	Locate bitmap from addins path.
											Add banner title and website url.
											Use more complex names for global variables.

	17-May-2011		Tony Goodman	Standalone version. Single DXL file for easy installation
											(With the exception of the optional logo bitmap file)
*/

pragma runLim, 0

/****************************
	Smart DXL logo bitmap file - this is loaded once when first dialog is displayed.
****************************/
Bitmap smartDxlLogo = null

string SMART_DXL_DB_TITLE      = "Smart DXL"
string SMART_DXL_BANNER_WEBSITE  = "www.smartdxl.com"
string SMART_DXL_BANNER_BITMAP    = "SmartDXL\\tophat.bmp"

DBE smartDxlBanner = null

bool smartDxlLogoFound = false

string smartDxlBannerText = ""

int   smartDxlLogoWidth   = 0
int   smartDxlLogoHeight  = 0

/****************************
	getAbsolutePath
****************************/
string getAbsolutePath(string sFileName)
{
	string sResult  = ""

	string sAddinsPathList      = (getenv "ADDINS") ";"
	string sDoorsAddinsPathList = (getenv "DOORSADDINS") ";"

	if (sAddinsPathList != sDoorsAddinsPathList)
	{
		sAddinsPathList = sDoorsAddinsPathList ";" sAddinsPathList
	}

	Regexp rPath = regexp "([^;]*);(.*)"

	string sLeft    = ""
	string sRight   = ""

	if (rPath sAddinsPathList)
	{
		// test each path in the semicolon separated list
		while (rPath sAddinsPathList)
		{
			  sLeft  = sAddinsPathList[match 1]
			  sRight = sAddinsPathList[match 2]

			  if (fileExists_ (sLeft "\\" sFileName))
			  {
					sResult  = sLeft "\\" sFileName
					break;
			  }
			  else
			  {
					sAddinsPathList = sRight
			  }
		}
	}
	return(sResult)
}

/*****************************
  smartDrawLogo
*****************************/
void smartDrawLogo(DBE cnvs)
{
	string smartDxlBitmapFile = ""
	Stat   st = null

	if (null smartDxlLogo)
	{
		// bitmap has not been loaded yet, so load it
		smartDxlBitmapFile = getAbsolutePath(SMART_DXL_BANNER_BITMAP)

		// check that the file exists
		st = create(smartDxlBitmapFile)

		if (!null st)
		{
			// file does exist so we can load the bitmap
			delete(st)

			smartDxlLogo = loadBitmap(cnvs, smartDxlBitmapFile, false, smartDxlLogoWidth, smartDxlLogoHeight)
		}
    }

    // draw the bitmap if it was successfully loaded
    if (!null smartDxlLogo)
    {
    	drawBitmap(cnvs, smartDxlLogo, 0, 0)
		smartDxlLogoFound = true
	}
}

/****************************
	doLogoMouse
****************************/
void doLogoMouse(DBE cnv, int but, bool ctrl, int x, int y)
{
	if (x > width(cnv) - width(cnv, SMART_DXL_BANNER_WEBSITE) - 5)
	{
		activateURL(SMART_DXL_BANNER_WEBSITE)
	}
}

/****************************
	doRepaintSmartDxlBanner
****************************/
void doRepaintSmartDxlBanner(DBE cnv)
{
	int canvasWidth  = width(cnv)
	int titleHeight  = 0
	int y = 0

	realBackground(cnv, realColor_White)

	smartDrawLogo(cnv)

	realColor(cnv, realColor_Black)
	font(cnv, 4, HeadingsFont)

	y = (height(cnv) / 2) + (height(cnv, smartDxlBannerText) / 2) - 3

	draw(cnv, smartDxlLogoWidth + 3, y, smartDxlBannerText)

	realColor(cnv, realColor_Blue)
	font(cnv, 9, HeadingsFont)
	draw(cnv, canvasWidth - width(cnv, SMART_DXL_BANNER_WEBSITE) - 5, y - 1, SMART_DXL_BANNER_WEBSITE)
	line(cnv, canvasWidth - width(cnv, SMART_DXL_BANNER_WEBSITE) - 5, y, canvasWidth - 5, y)
}

/****************************
	addSmartDxlBanner
****************************/
void addSmartDxlBanner(DB db)
{
	smartDxlBanner = canvas(db, 0, 32, doRepaintSmartDxlBanner)

	set(smartDxlBanner, doLogoMouse)
}

/****************************
	showHelp
****************************/
void showHelp(string helpFile)
{
	system("\"C:\\Program Files\\Internet Explorer\\iexplore.exe\" -nohome \"" (getAbsolutePath(helpFile)) "\"")
}

/****************************
	smartDialog
****************************/
DB smartDialog(string banner, int options)
{
	DB smartDb = null

	smartDb = create(SMART_DXL_DB_TITLE, options)

	smartDxlBannerText = banner

	addSmartDxlBanner(smartDb)

    return(smartDb)
}

/****************************
	smartDialog
****************************/
DB smartDialog(string dbTitle)
{
	return(smartDialog(dbTitle, styleCentered|styleFloating))
}	

/****************************
	smartDialog
****************************/
DB smartDialog(Module mParent, string banner, int options)
{
	DB smartDb = null

	smartDb = create(mParent, SMART_DXL_DB_TITLE, options)

	smartDxlBannerText = banner

	addSmartDxlBanner(smartDb)

    return(smartDb)
}

/****************************
	smartDialog
****************************/
DB smartDialog(Module mParent, string dbTitle)
{
	return(smartDialog(mParent, dbTitle, styleCentered|styleFloating))
}

/****************************
	smartDialog
****************************/
DB smartDialog(DB dbParent, string banner, int options)
{
	DB  smartDb     = null

	smartDb = create(dbParent, SMART_DXL_DB_TITLE, options)

	smartDxlBannerText = banner

	addSmartDxlBanner(smartDb)

    return(smartDb)
}

/****************************
	smartDialog
****************************/
DB smartDialog(DB dbParent, string banner)
{
	return(smartDialog(dbParent, banner, styleCentered|styleFloating))
}

//  Sort Callbacks for listViews

/****************************
	doSortString
****************************/
int doSortString(string s1, string s2)
{
	return(cistrcmp(s1, s2))
}

/****************************
	doSortDate
****************************/
int doSortDate(string s1, string s2)
{
	Date d1 = "1 January 1970"
	Date d2 = "1 January 1970"

	if (s1 != "")
	{
		d1 = date(s1)
	}

	if (s2 != "")
	{
		d2 = date(s2)
	}

	if (d1 > d2)
	{
		return(-1)
	}
	else if (d2 > d1)
	{
		return(1)
	}

	return(0)
}

/****************************
	doSortInteger
****************************/
int doSortInteger(string s1, string s2)
{
	int i1 = intOf(s1)
	int i2 = intOf(s2)

	if (i1 > i2)
	{
		return(1)
	}
	else if (i2 > i1)
	{
		return(-1)
	}

	return(0)
}

/****************************
	doSortLegal
****************************/
int doSortLegal(string s1, string s2)
{
	int i1 = 0
	int i2 = 0

	i1 = intOf(s1)
	i2 = intOf(s2)

	if (i1 > i2)
	{
		return(1)
	}
	else if (i2 > i1)
	{
		return(-1)
	}
	else
	{
		if (length(s1) > length(i1 ""))
		{
			if (length(s2) > length(i2 ""))
			{
				return(doSortLegal(s1[length(i1 "") + 1:], s2[length(i2 "") + 1:]))
			}

			return(1)
		}
		else if (length(s2) > length(i2 ""))
		{
			return(-1)
		}

		return(0)
	}

	return(0)
}

// columns in listview
const int BS_NAME_COLUMN       = 0
const int BS_CREATED_BY_COLUMN = 1
const int BS_CREATED_ON_COLUMN = 2
const int BS_DELETED_BY_COLUMN = 3
const int BS_DELETED_ON_COLUMN = 4

string bsListItems[] = {}

DB  bsMainDb       = null
DBE bsBaselinesDbe = null

// list of all baselines found in the module
Skip bsSelectedBaselinesSkip = null

// list of baselines selected in the listview by the user
Skip bsAllBaselinesSkip      = null

// true if the user selects one or more baselines.
// false if no baselines are selected or the user clicks cancel
bool bsBaselinesSelected = false

/****************************
	bsUpdateBaselineList

	Update the list view with entries for all baselines found in the given
	module. Also adds each baseline to the list of all baselines.
	This list is used later to get a handle on the selected baselines according
	to their position in the listview.
****************************/
void bsUpdateBaselineList(Module m)
{
    int      i            = 0
    Baseline b            = null
    string   strDate      = ""
    Date     baselineDate = null
    Date     deletedDate  = null

    // clear entries from the list view
    empty(bsBaselinesDbe)

    // loop through al baselines in the module
    for b in all m do
    {
        strDate = ""
        baselineDate = dateOf(b)

        if (!null baselineDate)
        {
            strDate = stringOf(baselineDate)
        }

        // add baseline to list of all baselines
        put(bsAllBaselinesSkip, i, b)

        // add an entry for this baseline to list view
        insert(bsBaselinesDbe, i, major(b) "." minor(b) " " suffix(b), iconNone)

        // set column values
        set(bsBaselinesDbe, i, BS_CREATED_BY_COLUMN, user(b))
        set(bsBaselinesDbe, i, BS_CREATED_ON_COLUMN, strDate)

        if (deleted(b))
        {
            strDate = ""
            deletedDate = deletedOn(b)

            if (!null deletedDate)
            {
                strDate = stringOf(deletedDate)
            }

            set(bsBaselinesDbe, i, BS_DELETED_BY_COLUMN, deletedBy(b))
            set(bsBaselinesDbe, i, BS_DELETED_ON_COLUMN, strDate)
        }
        i++
    }
}

/****************************
	bsDoCancel
	Callback for Cancel button.
****************************/
void bsDoCancel(DB db)
{
	bsBaselinesSelected = false
	release(bsMainDb)
}

/****************************
	bsDoApply
	Callback for OK button.
	Gets selections from the listview and updates the selected baseline list.
****************************/
void bsDoApply(DB db)
{
	int i = 0
	Baseline b = null

	bsBaselinesSelected = false

	// loop through entries in the list view
	for (i = 0; i < noElems(bsBaselinesDbe); i++)
	{
		// if the entry is checked (selected) ...
		if (getCheck(bsBaselinesDbe, i))
		{
			bsBaselinesSelected = true

			// look up the corresponding baseline
			if (find(bsAllBaselinesSkip, i, b))
			{
				// add to selected baseline list
				put(bsSelectedBaselinesSkip, i, b)
			}
		}
	}

	if (!bsBaselinesSelected)
	{
		infoBox("No baselines selected!")
		return
	}

	release(bsMainDb)
}

/****************************
	bsDoSelectAll

	Callback for the Select All button.
	Sets all the entries in the list to checked.
****************************/
void bsDoSelectAll(DB db)
{
	int i = 0

	for (i = 0; i < noElems(bsBaselinesDbe); i++)
	{
		setCheck(bsBaselinesDbe, i, true)
	}
}

/****************************
	bsDoDeselectAll

	Callback for the Deselect All button.
	Sets all the entries in the list to unchecked.
****************************/
void bsDoDeselectAll(DB db)
{
	int i = 0

	for (i = 0; i < noElems(bsBaselinesDbe); i++)
	{
		setCheck(bsBaselinesDbe, i, false)
	}
}

/****************************
	bsGetUserSelectedBaselines

	Launches a dialog displaying a list of baselines for the specified
	module. The user is able to select multiple baselines from the list.

	The selected baselines are returned in the skip list, indexed by an integer
	starting at zero.

	If selectAll is TRUE then the all entries in the list are initially checked.

	Returns TRUE if the user selects on or more baselines, otherwise FALSE.
****************************/
bool bsGetUserSelectedBaselines(Module m, Skip skip, string dbTitle, string msg, bool selectAll)
{
	Baseline b = null

	bsSelectedBaselinesSkip = create
	bsAllBaselinesSkip = create

	bsMainDb = smartDialog(m, dbTitle, styleCentered)

	label(bsMainDb, msg)

	bsBaselinesDbe = listView(bsMainDb, listViewOptionCheckboxes | listViewOptionSortText, 455, 10, bsListItems)

	close(bsMainDb, false, bsDoCancel)

	apply(bsMainDb, "Select All", bsDoSelectAll)
	apply(bsMainDb, "Deselect All", bsDoDeselectAll)
	apply(bsMainDb, bsDoApply)
	ok(bsMainDb, "Cancel", bsDoCancel)

	realize(bsMainDb)

	insertColumn(bsBaselinesDbe, BS_NAME_COLUMN,       "Baseline",    80, iconNone)
    insertColumn(bsBaselinesDbe, BS_CREATED_BY_COLUMN, "Created By",  80, iconNone)
    insertColumn(bsBaselinesDbe, BS_CREATED_ON_COLUMN, "Created On", 105, iconNone)
    insertColumn(bsBaselinesDbe, BS_DELETED_BY_COLUMN, "Deleted By",  80, iconNone)
    insertColumn(bsBaselinesDbe, BS_DELETED_ON_COLUMN, "Deleted On", 105, iconNone)

    set(bsBaselinesDbe, BS_NAME_COLUMN,       doSortLegal)
    set(bsBaselinesDbe, BS_CREATED_BY_COLUMN, doSortString)
    set(bsBaselinesDbe, BS_CREATED_ON_COLUMN, doSortDate)
    set(bsBaselinesDbe, BS_DELETED_BY_COLUMN, doSortString)
    set(bsBaselinesDbe, BS_DELETED_ON_COLUMN, doSortDate)

    bsUpdateBaselineList(m)

    if (selectAll)
    {
	    bsDoSelectAll(bsMainDb)
    }

	block(bsMainDb)

	hide(bsMainDb)
	destroy(bsMainDb)
	bsMainDb = null

	for b in bsSelectedBaselinesSkip do
	{
		put(skip, (int key bsSelectedBaselinesSkip), b)
	}

	delete(bsSelectedBaselinesSkip)
	delete(bsAllBaselinesSkip)

	return(bsBaselinesSelected)
}

/*

    Data structures and utilities for handling history records.

    History types:
	    const HistoryType unknown
		const HistoryType createType
		const HistoryType modifyType
		const HistoryType deleteType
		const HistoryType createAttr
		const HistoryType modifyAttr
		const HistoryType deleteAttr
		const HistoryType createObject
		const HistoryType copyObject
		const HistoryType modifyObject
		const HistoryType deleteObject
		const HistoryType unDeleteObject
		const HistoryType purgeObject
		const HistoryType moveObject
		const HistoryType clipCutObject
		const HistoryType clipMoveObject
		const HistoryType clipCopyObject
		const HistoryType createModule
		const HistoryType baselineModule
		const HistoryType partitionModule
		const HistoryType acceptModule
		const HistoryType returnModule
		const HistoryType rejoinModule
		const HistoryType createLink
		const HistoryType modifyLink
		const HistoryType deleteLink
		const HistoryType insertOLE
		const HistoryType removeOLE
		const HistoryType changeOLE
		const HistoryType pasteOLE
		const HistoryType cutOLE
		const HistoryType readLocked
		const HistoryType commentObject
		const HistoryType commentModule
*/

// indices to DxlObject structure
const string HIST_AUTHOR       = "author"
const string HIST_DATE         = "date"
const string HIST_BASELINE     = "baseline"
const string HIST_SESSION      = "session"
const string HIST_DESCRIPTION  = "description"
const string HIST_OLD_VALUE    = "oldvalue"
const string HIST_NEW_VALUE    = "newvalue"
const string HIST_TYPE         = "type"
const string HIST_LOCKED       = "locked"

// data structure for object history
Skip skipObjects        = create
Skip skipNumRecords     = create

// data structure for module history
Skip skipModuleHistory  = create
int  numModuleRecords   = 0

// data structure for session history
Skip skipSessionHistory = create
int  numSessionRecords  = 0

Module  currMod               = null
Object  currObj               = null
int     currObjNumber         = 0

/****************************
	printObjectHistory

	Use for degug only. Prints all data structures.
****************************/
void printObjectHistory()
{
	Object      o           = null
	Skip        skipHistory = null
	DxlObject   hist        = null
	HistoryType historyType = null
	int         objNo       = 0
	int         rec         = 0

	for skipHistory in skipObjects do
	{
		// get object number
		objNo = (int key skipObjects)

		// lookup number of history records stored for this object
		find(skipNumRecords, objNo, rec)

		print("Object " (int key skipObjects) " has " rec " history records\n")

		for hist in skipHistory do
		{
			print("\tHistory record " (int key skipHistory) "\n")

			if (!(bool hist->HIST_LOCKED))
			{
				print("\t\tDate: " (Date hist->HIST_DATE) "\n")
				print("\t\tAuthor: " (string hist->HIST_AUTHOR) "\n")
				print("\t\tSession: " (int hist->HIST_SESSION) "\n")
				print("\t\tBaseline: " (string hist->HIST_BASELINE) "\n")

				historyType = (HistoryType hist->HIST_TYPE)

				print("\t\tDescription: " (string hist->HIST_DESCRIPTION) "\n")
				print("\t\t\tFrom: " (string hist->HIST_OLD_VALUE) "\n")
				print("\t\t\tTo: " (string hist->HIST_NEW_VALUE) "\n")
			}
			else
			{
				print("Read Locked Data\n")
			}
		}
		return
	}
}

/****************************
	isObjectOperation
****************************/
bool isObjectOperation(HistoryType historyType)
{
    return((historyType == createObject)   ||
           (historyType == copyObject)     ||
           (historyType == deleteObject)   ||
           (historyType == unDeleteObject) ||
           (historyType == purgeObject)    ||
           (historyType == clipCutObject)  ||
           (historyType == clipCopyObject) ||
           (historyType == createLink)     ||
           (historyType == modifyLink)     ||
           (historyType == deleteLink)     ||
           (historyType == insertOLE)      ||
           (historyType == removeOLE)      ||
           (historyType == changeOLE)      ||
           (historyType == pasteOLE))
}

/****************************
	isModuleOperation

	This function deliberately includes the deleteObject and purgeObject
	operations as these do not appear in the object history.

	modifyObject is required for changes to module level attributes.
****************************/
bool isModuleOperation(HistoryType historyType)
{
	return(historyType == createModule ||
	       historyType == baselineModule ||
	       historyType == partitionModule ||
	       historyType == acceptModule ||
	       historyType == rejoinModule ||
	       historyType == returnModule ||
	       historyType == commentModule ||
	       historyType == createAttr ||
	       historyType == modifyAttr ||
	       historyType == deleteAttr ||
	       historyType == createType ||
	       historyType == modifyType ||
	       historyType == deleteType ||
	       historyType == deleteObject ||
	       historyType == purgeObject ||
	       historyType == modifyObject)
}

/****************************
	getObjectHistory
****************************/
void getObjectHistory(Module m, string currBaseline)
{
	Object      o            = null
	int         objNo        = 0
	int         rec          = 0
	bool        locked       = false
	History     h            = null
	HistoryType historyType  = null

	// loop through all objects in the module an extract object history
	for o in entire(m) do
	{
		objNo = o."Absolute Number"

		// skip list to hold history records for the object
		Skip skipHistory = null

		// see if the history list already exists, if not then create it
		if (find(skipObjects, objNo, skipHistory))
		{
			//print("skipHistory found for object " objNo "\n")
			// skip list exists - find next index to use
			if (find(skipNumRecords, objNo, rec))
			{
				//print("numrecords found = " rec "\n")
			}
			//rec++
		}
		else
		{
			skipHistory = create

			// new skip list, so first entry will be indexed at zero
			rec = 0
		}

		// loop through history records for the object
		for h in o do
		{
			// create structure to hold details of the history record
			DxlObject hist = new

			// history record may be read-locked
			locked = h.readlocked
			hist->HIST_LOCKED = locked

			// record the type
			historyType = h.type
			hist->HIST_TYPE = historyType

			// if it is not locked we can read the details
			if (!locked)
			{
				hist->HIST_DATE = h.date
				hist->HIST_AUTHOR = h.author
				hist->HIST_SESSION = h.sessionNo
				hist->HIST_BASELINE = currBaseline
				//hist->HIST_DETAILS = "<no further details>"

				if (historyType == modifyObject)
				{
					// object modification has old and new values
					hist->HIST_DESCRIPTION = "Modify Attribute: " h.attrName

					// TBD use buffers to hold old and new values.
					hist->HIST_OLD_VALUE = h.oldValue
					hist->HIST_NEW_VALUE = h.newValue
				}
				else if (historyType == moveObject or historyType == clipMoveObject)
				{
					// object was moved or copied and pasted
					hist->HIST_DESCRIPTION = "Move object from " h.position " to " h.newPosition ""
				}
				else if (historyType == clipCopyObject)
				{
					// object was created by copying another
					hist->HIST_DESCRIPTION = "Copy object from " h.oldAbsNo " to " h.absNo ""
				}
				else if (historyType == commentObject)
				{
					hist->HIST_DESCRIPTION = "Comment"
					hist->HIST_NEW_VALUE = h.newValue
				}
				else if (isObjectOperation(historyType))
				{
					hist->HIST_DESCRIPTION = goodStringOf(historyType)
				}
				else
				{
					hist->HIST_DESCRIPTION = "<unknown history type>"
				}
			}

			// add history structure to list of history records for this object
			put(skipHistory, rec++, hist)
		}

		// store the number of history records in the list
		delete(skipNumRecords, objNo)
		put(skipNumRecords, objNo, rec)

		// add list of history records for this object to the object list
		delete(skipObjects, objNo)
		put(skipObjects, objNo, skipHistory)
	}
}

/****************************
	getModuleHistory
****************************/
void getModuleHistory(Module m, string currBaseline)
{
	int         rec          = 0
	bool        locked       = false
	string      modification = ""
	History     h            = null
	HistoryType historyType  = null

	// loop through history records (this loops through ALL history
	// because we want the object delete and purge history records
	// in addition to the module specific history records.
	for h in m do
	{
		historyType = h.type

		if (!isModuleOperation(historyType))
		{
			continue
		}

		// filter out modifyObject records that are not for module level attributes
		if (historyType == modifyObject && h.absNo != 0)
		{
			continue
		}

		// create structure to hold details of the history record
		DxlObject hist = new

		// record the type
		hist->HIST_TYPE = historyType

		// history record may be read-locked
		locked = h.readlocked
		hist->HIST_LOCKED = locked

		// if it is not locked we can read the details
		if (!locked)
		{
			hist->HIST_DATE = h.date
			hist->HIST_AUTHOR = h.author
			hist->HIST_SESSION = h.sessionNo
			hist->HIST_BASELINE = currBaseline
			//hist->HIST_DETAILS = "<no further details>"

			modification = goodStringOf(historyType)
			hist->HIST_DESCRIPTION = modification

			if (historyType == createAttr || historyType == modifyAttr || historyType == deleteAttr)
			{
				// attribute modification
				hist->HIST_DESCRIPTION = modification ": " h.attrName
			}
			else if (historyType == createType || historyType == modifyType || historyType == deleteType)
			{
				// attribute type modification
				hist->HIST_DESCRIPTION = modification ": " h.typeName
			}
			else if (historyType == commentModule)
			{
				hist->HIST_DESCRIPTION = "Comment"
				hist->HIST_NEW_VALUE = h.newValue
			}
			else if (historyType == deleteObject || historyType == purgeObject)
			{
				hist->HIST_DESCRIPTION = modification ": " h.absNo ""
			}
			else if (historyType == modifyObject && (h.absNo == 0))
			{
				// it seems that modifications to module level attributes are stored
				// as modifyObject history records with an absolute number of zero.
				hist->HIST_DESCRIPTION = "Modify Attribute: " h.attrName

				// TBD use buffers to hold old and new values.
				hist->HIST_OLD_VALUE = h.oldValue
				hist->HIST_NEW_VALUE = h.newValue
			}
		}

		// store history structure in skip list
		put(skipModuleHistory, numModuleRecords++, hist)
	}
}

/****************************
	getSessionHistory
****************************/
void getSessionHistory(Module m)
{
	HistorySession hs        = null
	string         sBaseline = ""

	for hs in m do
	{
		// create structure to hold details of the history record
		DxlObject hist = new

		hist->HIST_DATE = when(hs)
		hist->HIST_AUTHOR = who(hs) 

		// Sessions are 0-based in the core;  the 1+ here is to account for the fact that
        // number(s) uses this 0-based scheme whereas sessionNo does
        // this 1+ internally.
		hist->HIST_SESSION = number(hs) + 1

		sBaseline = baseline(hs)

		if (!null sBaseline)
		{
			hist->HIST_BASELINE = sBaseline
			hist->HIST_DESCRIPTION = "Created baseline " sBaseline
		}			

		//hist->HIST_DETAILS = "<no details>"

		put(skipSessionHistory, numSessionRecords++, hist)
	}
}

/****************************
	getHistory
****************************/
void getHistory(Module m)
{
	string      currBaseline = ""
	string      s            = ""
	Baseline    b            = null

	// extract baseline information
	if (isBaseline(m))
	{
		b = baselineInfo(m)

		// only display the suffix if is is not empty
		s = (suffix b) ""

		if (s != "")
		{
			s = " (" s ")"
		}

		currBaseline = (major b) "." (minor b) "" s
	}
	else
	{
		currBaseline = "current"
	}

	// get object history records
	getObjectHistory(m, currBaseline)

	// get module history records
	getModuleHistory(m, currBaseline)
}

// listview column indices
const int HIST_AUTHOR_COLUMN        = 0
const int HIST_SESSION_COLUMN       = 1
const int HIST_DATE_COLUMN          = 2
const int HIST_BASELINE_COLUMN      = 3
const int HIST_DESCRIPTION_COLUMN   = 4
const int HIST_RECORD_COLUMN        = 5

// tab indices
const int TAB_OBJECT   = 0
const int TAB_MODULE   = 1
const int TAB_SESSIONS = 2

int selectedTab = TAB_OBJECT

// main dialog
DB      dbMain                = null
DBE     dbeTab                = null

// elements
DBE     dbeLabel        = null
DBE     dbeListView     = null
DBE     dbeDetailsFrame = null
DBE     dbeOldValue     = null
DBE     dbeNewValue       = null
DBE     btnPrev         = null
DBE     btnNext         = null

string  columnTitles[]        = {}

string tabLabels[] = { "Object", "Module", "Sessions" }

string details = ""

//Trigger g_mTrigger            = null
Trigger preSyncTrigger        = null
Trigger postSyncTrigger       = null

Skip skipOldValues = null
Skip skipNewValues = null

/****************************
	clearDetailSkips
****************************/
void clearDetailSkips()
{
	// dynamically store the old and new values in a skip list, referenced by
	// history record number. this allows us to look up the values for display
	// when the user selects an entry in the listview.
	if (!null skipOldValues)
	{
		delete(skipOldValues)
	}
	skipOldValues = create

	if (!null skipNewValues)
	{
		delete(skipNewValues)
	}
	skipNewValues = create
}

/****************************
	showObjectHistory

	Update the dialog with information read from the data structures.
****************************/
void showObjectHistory()
{
	Skip        skipHistory = null
	DxlObject   hist        = null
	HistoryType historyType = null
	int         objNo       = 0
	int         rec         = 0

	objNo = currObj."Absolute Number"

	clearDetailSkips()

	// check that there is an entry for this object in the list
	if (find(skipObjects, objNo, skipHistory) && find(skipNumRecords, objNo, rec))
	{
		if (rec > 0)
		{
			// there are history records for this object
			for hist in skipHistory do
			{
				rec = noElems(dbeListView)

				// insert a row in the list view
				insert(dbeListView, rec, "", iconNone)

				historyType = (HistoryType hist->HIST_TYPE)

				if (!(bool hist->HIST_LOCKED))
				{
					set(dbeListView, rec, HIST_AUTHOR_COLUMN,       (string hist->HIST_AUTHOR))
					set(dbeListView, rec, HIST_SESSION_COLUMN,      (int hist->HIST_SESSION) "")
					set(dbeListView, rec, HIST_DATE_COLUMN,         stringOf(Date hist->HIST_DATE) "")
					set(dbeListView, rec, HIST_BASELINE_COLUMN,     (string hist->HIST_BASELINE))
					set(dbeListView, rec, HIST_DESCRIPTION_COLUMN,  (string hist->HIST_DESCRIPTION))

					// used as an index to the details lists
					set(dbeListView, rec, HIST_RECORD_COLUMN,       rec "")

					// store details in lists - these can then be retrieved when user selects
					// an entry in the listview
					put(skipOldValues, rec, (string hist->HIST_OLD_VALUE))
					put(skipNewValues, rec, (string hist->HIST_NEW_VALUE))
				}
				else
				{
					set(dbeListView, rec, HIST_DESCRIPTION_COLUMN,  goodStringOf(historyType) ": <Read Locked Data>")
					set(dbeListView, rec, HIST_AUTHOR_COLUMN,       "<Read Locked Data>")
					set(dbeListView, rec, HIST_SESSION_COLUMN,      "<Read Locked Data>")
					set(dbeListView, rec, HIST_DATE_COLUMN,         "<Read Locked Data>")
					set(dbeListView, rec, HIST_BASELINE_COLUMN,     "<Read Locked Data>")
					set(dbeListView, rec, HIST_RECORD_COLUMN,       rec "")
				}
			}
		}
		else
		{
			// there are no history records for this object
			insert(dbeListView, 0, "", iconNone)
			set(dbeListView, 0, HIST_DATE_COLUMN, "<No history found>" "")
		}

	}
	else
	{
		// there is no entry for this object
		insert(dbeListView, 0, "", iconNone)
		set(dbeListView, 0, HIST_DESCRIPTION_COLUMN, "<Object not found>")
	}
}

/****************************
	showModuleHistory

	Update the dialog with information read from the data structures.
****************************/
void showModuleHistory()
{
	DxlObject   hist        = null
	HistoryType historyType = null
	int         rec         = 0

	clearDetailSkips()

	if (numModuleRecords > 0)
	{
		// there are history records for this object
		for hist in skipModuleHistory do
		{
			rec = noElems(dbeListView)

			// insert a row in the list view
			insert(dbeListView, rec, "", iconNone)

			historyType = (HistoryType hist->HIST_TYPE)

			if (!(bool hist->HIST_LOCKED))
			{
				set(dbeListView, rec, HIST_AUTHOR_COLUMN,       (string hist->HIST_AUTHOR))
				set(dbeListView, rec, HIST_SESSION_COLUMN,      (int hist->HIST_SESSION) "")
				set(dbeListView, rec, HIST_DATE_COLUMN,         stringOf(Date hist->HIST_DATE) "")
				set(dbeListView, rec, HIST_BASELINE_COLUMN,     (string hist->HIST_BASELINE))
				set(dbeListView, rec, HIST_DESCRIPTION_COLUMN,  (string hist->HIST_DESCRIPTION))
				set(dbeListView, rec, HIST_RECORD_COLUMN,       rec "")

				// store details in lists - these can then be retrieved when user selects
				// an entry in the listview
				put(skipOldValues, rec, (string hist->HIST_OLD_VALUE))
				put(skipNewValues, rec, (string hist->HIST_NEW_VALUE))

			}
			else
			{
				set(dbeListView, rec, HIST_DESCRIPTION_COLUMN,  goodStringOf(historyType) ": <Read Locked Data>")
				set(dbeListView, rec, HIST_AUTHOR_COLUMN,       "<Read Locked Data>")
				set(dbeListView, rec, HIST_SESSION_COLUMN,      "<Read Locked Data>")
				set(dbeListView, rec, HIST_DATE_COLUMN,         "<Read Locked Data>")
				set(dbeListView, rec, HIST_BASELINE_COLUMN,     "<Read Locked Data>")
				set(dbeListView, rec, HIST_RECORD_COLUMN,       rec "")
			}
		}
	}
	else
	{
		// there are no history records for this object
		insert(dbeListView, 0, "", iconNone)
		set(dbeListView, 0, HIST_DATE_COLUMN, "<No history found>")
	}

}

/****************************
	showSessionHistory

	Update the dialog with information read from the data structures.
****************************/
void showSessionHistory()
{
	DxlObject   hist        = null
	int         rec         = 0

	clearDetailSkips()

	if (numSessionRecords > 0)
	{
		// there are history records for this object
		for hist in skipSessionHistory do
		{
			rec = noElems(dbeListView)

			// insert a row in the list view
			insert(dbeListView, rec, "", iconNone)

			set(dbeListView, rec, HIST_AUTHOR_COLUMN,       (string hist->HIST_AUTHOR))
			set(dbeListView, rec, HIST_SESSION_COLUMN,      (int hist->HIST_SESSION) "")
			set(dbeListView, rec, HIST_DATE_COLUMN,         stringOf(Date hist->HIST_DATE) "")
			set(dbeListView, rec, HIST_BASELINE_COLUMN,     (string hist->HIST_BASELINE))
			set(dbeListView, rec, HIST_DESCRIPTION_COLUMN,  (string hist->HIST_DESCRIPTION))
		}
	}
	else
	{
		// there are no history records for this object
		insert(dbeListView, 0, "", iconNone)
		set(dbeListView, 0, HIST_DATE_COLUMN, "<No sessions found>")
	}
}

/****************************
	getBaselineHistory
****************************/
bool getBaselineHistory(Skip skipBaselines)
{
	Baseline thisBasline      = null
	Module   moduleBaseline   = null
	int      bCount           = 0
	int      numBaselines     = 0
	bool     autoShutdown     = false

	// count baselines for progress
	for thisBasline in skipBaselines do
	{
		numBaselines++
		//print("Baseline " (major b) "." (minor b) (suffix b) " " (user b) " " (dateOf b) " " (annotation b) "\n")
	}

	progressStart(dbMain, "Please Wait", "Reading history from baselines", numBaselines)

	// load history from current version
	getHistory(currMod)

	// get session history records
	getSessionHistory(currMod)

	// loop through list of baselines
	for thisBasline in skipBaselines do
	{
		progressStep(++bCount)
		progressMessage("Reading history from baseline " bCount " of " numBaselines "")

		if (progressCancelled)
		{
            if (confirm("Are you sure you want to cancel?"))
            {
	            progressStop
	            //delete(skipBaselines)
                return(false)
            }
        }

		// open the baseline in the background
		moduleBaseline = load(currMod, thisBasline, false)

		if (null moduleBaseline)
		{
			print("ERROR opening baseline " (major thisBasline) "." (minor thisBasline) " (" (suffix thisBaseline) ")\n")
			continue
		}

		// read history from baseline
		getHistory(moduleBaseline)

		close(moduleBaseline)
	}

	progressStop()

	return(true)
}

/****************************
	doCancel

	Callback on Cancel button on details dialog
****************************/
void doCancel(DB db)
{
	//release(dbDetails)
}

/****************************
	doHelp

	Callback on help button.
****************************/
void doHelp(DB db)
{

}

/****************************
	refreshDisplay
****************************/
void refreshDisplay()
{
	// refresh current selected object
	(current ModuleRef__) = currMod
    (current ObjectRef__) = currObj

    refresh(currMod)

    empty(dbeListView)
    set(dbeOldValue, "")
    set(dbeNewValue, "")

    if (selectedTab == TAB_OBJECT)
    {
    	// update dialog to show history records for the selected object
    	set(dbeLabel, "History for Object " identifier(currObj))

    	showObjectHistory()
    	show(btnPrev)
		show(btnNext)
		//show(btnDetails)
		//inactive(btnDetails)
	}
	else if (selectedTab == TAB_MODULE)
	{
		set(dbeLabel, "History for Module " fullName(currMod))

		showModuleHistory()
		hide(btnPrev)
		hide(btnNext)
		//hide(btnDetails)
	}
	else if (selectedTab == TAB_SESSIONS)
	{
		set(dbeLabel, "Session history for Module " fullName(currMod))

		showSessionHistory()
		hide(btnPrev)
		hide(btnNext)
		//hide(btnDetails)
	}
}

/****************************
	preSyncFn
****************************/
bool preSyncFn(Trigger t)
{
	// ensure correct current object and module BEFORE object sync
    (current ModuleRef__) = currMod
    (current ObjectRef__) = currObj

    return(true)
}

/****************************
	postSyncFn
****************************/
void postSyncFn(Trigger t)
{
	// we can safely do this because current module and object were
	// set BEFORE object sync.
	//print("post sync fired\n")

    currObj = current Object
    refreshDisplay()
}

/****************************
	doClose
****************************/
void doClose(DB db)
{
	//if (!null g_mTrigger)
	//{
	//	delete g_mTrigger
	//}

	if (!null preSyncTrigger)
	{
        delete preSyncTrigger
    }

    if (!null postSyncTrigger)
    {
        delete postSyncTrigger
    }

    // TBD delete data astructures

	hide(dbMain)
	destroy(dbMain)
	dbMain = null
}

/****************************
	fnTrigger
****************************/
bool fnTrigger(Trigger xx)
{
    doClose(dbMain)

    return true
}

/****************************
	doPrev
****************************/
void doPrev(DBE dbe)
{
	Object oPrev = previous currObj

    if (oPrev != null)
    {
        currObj = oPrev
        currObjNumber = currObj."Absolute Number"
        refreshDisplay()
    }
}

/****************************
	doNext
****************************/
void doNext(DBE dbe)
{
	Object oNext = next currObj

    if (oNext != null)
    {
        currObj = oNext
        currObjNumber = currObj."Absolute Number"
        refreshDisplay()
    }
}

/****************************
	doSelect

	In shareable edit mode the user may have moved to an object that is not
	locked. Therefore we must check that the current object is locked for edit.
****************************/
void doSelect(DBE dbe, int iSelected)
{
	string sNew = ""
	string sOld = ""

	// get record number from listview - this gives an index to old and new values lists
	int record = intOf(getColumnValue(dbeListView, iSelected, HIST_RECORD_COLUMN))

	if (find(skipNewValues, record, sNew))
	{
		set(dbeNewValue, sNew)

		if (find(skipOldValues, record, sOld))
		{
			set(dbeOldValue, sOld)
		}
	}
}

/****************************
	doUnselect
****************************/
void doUnselect(DBE dbe, int iSelected)
{
	set(dbeOldValue, "")
	set(dbeNewValue, "")
}

/****************************
	doActivate

	Callback when user double-clicks on entry in the list view
****************************/
void doActivate(DBE dbe, int iSelected)
{
    //doDetails(dbeListView)
}

/****************************
	doTabSelect
****************************/
void doTabSelect(DBE dbe)
{
	selectedTab = get(dbeTab)

	refreshDisplay()
}

/****************************
	showHistoryDialog
****************************/
void showHistoryDialog(Skip skip)
{
	string dispString = ""

	currMod = current Module

	// trigger to close dialog when module closes
	//g_mTrigger = trigger(module, close, 10, fnTrigger)

	dbMain = smartDialog(currMod, "Smart History Viewer", styleCentered | styleFloating)

	dbeTab = tab(dbMain, tabLabels, 690, 440, doTabSelect)
	dbeTab->"bottom"->"form"
	dbeTab->"right"->"form"

	dbeLabel = label(dbMain, "History for Object " identifier(currObj))
	dbeLabel->"top"->"inside"->dbeTab
	dbeLabel->"left"->"inside"->dbeTab

	dbeListView  = listView(dbMain, listViewOptionSortText, 650, 10, columnTitles)

	dbeListView->"top"->"spaced"->dbeLabel
    dbeListView->"left"->"inside"->dbeTab
    dbeListView->"right"->"inside"->dbeTab
    dbeListView->"bottom"->"unattached"

    dbeDetailsFrame = frame(dbMain, "Details of selected history record", 650, 100)
    dbeDetailsFrame->"top"->"spaced"->dbeListView
    dbeDetailsFrame->"left"->"inside"->dbeTab
    dbeDetailsFrame->"right"->"inside"->dbeTab
    dbeDetailsFrame->"bottom"->"unattached"

    dbeOldValue = richText(dbMain, "Old value", "", 325, 100, true)
    dbeOldValue->"top"->"inside"->dbeDetailsFrame
    dbeOldValue->"left"->"inside"->dbeDetailsFrame
    dbeOldValue->"right"->"unattached"
    dbeOldValue->"bottom"->"inside"->dbeDetailsFrame

    dbeNewValue = richText(dbMain, "New value", "", 325, 100, true)
    dbeNewValue->"top"->"inside"->dbeDetailsFrame
    dbeNewValue->"left"->"spaced"->dbeOldValue
    dbeNewValue->"right"->"inside"->dbeDetailsFrame
    dbeNewValue->"bottom"->"inside"->dbeDetailsFrame

	btnPrev = button(dbMain, "< Previous", doPrev, styleStandardSize)
    btnPrev->"top"->"spaced"->dbeDetailsFrame
    btnPrev->"left"->"inside"->dbeTab
    btnPrev->"bottom"->"inside"->dbeTab
    btnPrev->"right"->"unattached"

    // Next button
    btnNext = button(dbMain, "Next >", doNext, styleStandardSize)
    btnNext->"top"->"spaced"->dbeDetailsFrame
    btnNext->"left"->"spaced"->btnPrev
    btnNext->"bottom"->"inside"->dbeTab
    btnNext->"right"->"unattached"

	// hide the close button, but associate callback with dialog
	close(dbMain, false, doClose)
    // Help button
    ok(dbMain, "Close", doClose)
    apply(dbMain, "Help", doHelp)

	realize dbMain

	insertColumn(dbeListView, HIST_AUTHOR_COLUMN,        "Author",        120, null)
    insertColumn(dbeListView, HIST_SESSION_COLUMN,       "Session",        50, null)
    insertColumn(dbeListView, HIST_DATE_COLUMN,          "Date",          120, null)
    insertColumn(dbeListView, HIST_BASELINE_COLUMN,      "Baseline",       70, null)
    insertColumn(dbeListView, HIST_DESCRIPTION_COLUMN,   "Description",   250, null)
    insertColumn(dbeListView, HIST_RECORD_COLUMN,        "Record",         50, null)

	// associate sort callbacks with each column
	set(dbeListView, HIST_AUTHOR_COLUMN, doSortString)
	set(dbeListView, HIST_SESSION_COLUMN, doSortInteger)
	set(dbeListView, HIST_DATE_COLUMN, doSortDate)
	set(dbeListView, HIST_BASELINE_COLUMN, doSortString)
	set(dbeListView, HIST_DESCRIPTION_COLUMN, doSortString)
	set(dbeListView, HIST_RECORD_COLUMN, doSortInteger)

	setSortColumn(dbeListView, HIST_DATE_COLUMN)

	// callbacks on listview
	set(dbeListView, doSelect, doUnselect, doActivate)

	// resizing
	setExtraHeightShare(dbeListView, 1.0)
    setExtraWidthShare(dbeListView, 1.0)
    setExtraWidthShare(dbeOldValue, 0.5)
    setExtraWidthShare(dbeNewValue, 0.5)

	// load history from baselines
	if (getBaselineHistory(skip))
	{
		//printObjectHistory()

		// history loaded successfully - refresh display
		refreshDisplay()

		// It is important to reset the current module before creating the triggers
		(current ModuleRef__) = currMod

		// set up triggers for object sync
		if (!null preSyncTrigger)
		{
	        delete preSyncTrigger
	    }
	    preSyncTrigger = trigger(module->object, sync, 10, preSyncFn)

	    if (!null postSyncTrigger)
	    {
	        delete postSyncTrigger
	    }

	    postSyncTrigger = trigger(module->object, sync , 10, postSyncFn)

		show(dbMain)
	}
	else
	{
		// loading of history was cancelled by user
		hide(dbMain)
		destroy(dbMain)
		dbMain = null
	}
}

/****************************
	MAIN
****************************/
currMod = current Module
currObj = current Object

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

if (null currObj)
{
	infoBox("No current Object!")
	halt
}

Skip skip = create

// lauch dialog for user to select baselines
if (!bsGetUserSelectedBaselines(currMod,
                                skip,
                                "Smart History Viewer",
                                "Please select the baselines for which you would like to view history.\n" //-
                                "Note that loading numerous baselines may take some time.",
                                true))
{
	halt
}

showHistoryDialog(skip)