/* $Id: ttkNotebook.c,v 1.8 2007/01/11 19:59:26 jenglish Exp $
 * Copyright (c) 2004, Joe English
 *
 * NOTE-ACTIVE: activeTabIndex is not always correct (it's
 * more trouble than it's worth to track this 100%)
 */

#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <tk.h>

#include "ttkTheme.h"
#include "ttkWidget.h"
#include "ttkManager.h"

#define MIN(a,b) ((a) < (b) ? (a) : (b))
#define MAX(a,b) ((a) > (b) ? (a) : (b))

/*------------------------------------------------------------------------
 * +++ Tab resources.
 */

#define DEFAULT_MIN_TAB_WIDTH 24

static const char *TabStateStrings[] = { "normal", "disabled", "hidden", 0 };
typedef enum {
    TAB_STATE_NORMAL, TAB_STATE_DISABLED, TAB_STATE_HIDDEN
} TAB_STATE;

typedef struct
{
    /* Internal data:
     */
    int 	width, height;		/* Requested size of tab */
    Ttk_Box	parcel;			/* Tab position */

    /* Tab options:
     */
    TAB_STATE 	state;

    /* Child window options:
     */
    Tcl_Obj	*paddingObj;		/* Padding inside pane */
    Ttk_Padding	padding;
    Tcl_Obj 	*stickyObj;
    Ttk_Sticky	sticky;

    /* Label options:
     */
    Tcl_Obj *textObj;
    Tcl_Obj *imageObj;
    Tcl_Obj *compoundObj;
    Tcl_Obj *underlineObj;

} Tab;

/* Two different option tables are used for tabs:
 * TabOptionSpecs is used to draw the tab, and only includes resources
 * relevant to the tab.
 *
 * PaneOptionSpecs includes additional options for child window placement
 * and is used to configure the slave.
 */
static Tk_OptionSpec TabOptionSpecs[] =
{
    {TK_OPTION_STRING_TABLE, "-state", "", "",
	"normal", -1,Tk_Offset(Tab,state),
	0,(ClientData)TabStateStrings,0 },
    {TK_OPTION_STRING, "-text", "text", "Text", "",
	Tk_Offset(Tab,textObj), -1, 0,0,GEOMETRY_CHANGED },
    {TK_OPTION_STRING, "-image", "image", "Image", NULL/*default*/,
	Tk_Offset(Tab,imageObj), -1, TK_OPTION_NULL_OK,0,GEOMETRY_CHANGED },
    {TK_OPTION_STRING_TABLE, "-compound", "compound", "Compound",
	"none", Tk_Offset(Tab,compoundObj), -1,
	0,(ClientData)ttkCompoundStrings,GEOMETRY_CHANGED },
    {TK_OPTION_INT, "-underline", "underline", "Underline", "-1",
	Tk_Offset(Tab,underlineObj), -1, 0,0,GEOMETRY_CHANGED },
    {TK_OPTION_END}
};

static Tk_OptionSpec PaneOptionSpecs[] =
{
    {TK_OPTION_STRING, "-padding", "padding", "Padding", "0",
	Tk_Offset(Tab,paddingObj), -1, 0,0,GEOMETRY_CHANGED },
    {TK_OPTION_STRING, "-sticky", "sticky", "Sticky", "nsew",
	Tk_Offset(Tab,stickyObj), -1, 0,0,GEOMETRY_CHANGED },

    WIDGET_INHERIT_OPTIONS(TabOptionSpecs)
};

/*------------------------------------------------------------------------
 * +++ Notebook resources.
 */
typedef struct
{
    Tcl_Obj *widthObj;		/* Default width */
    Tcl_Obj *heightObj;		/* Default height */
    Tcl_Obj *paddingObj;	/* Padding around notebook */

    Ttk_Manager *mgr;		/* Geometry manager */
    Tk_OptionTable tabOptionTable;	/* Tab options */
    Tk_OptionTable paneOptionTable;	/* Tab+pane options */
    int currentIndex;		/* index of currently selected tab */
    int activeIndex;		/* index of currently active tab */
    Ttk_Layout tabLayout;	/* Sublayout for tabs */

    Ttk_Box clientArea;		/* Where to pack slave widgets */
} NotebookPart;

typedef struct
{
    WidgetCore core;
    NotebookPart notebook;
} Notebook;

static Tk_OptionSpec NotebookOptionSpecs[] =
{
    WIDGET_TAKES_FOCUS,

    {TK_OPTION_INT, "-width", "width", "Width", "0",
	Tk_Offset(Notebook,notebook.widthObj),-1,
	0,0,GEOMETRY_CHANGED },
    {TK_OPTION_INT, "-height", "height", "Height", "0",
	Tk_Offset(Notebook,notebook.heightObj),-1,
	0,0,GEOMETRY_CHANGED },
    {TK_OPTION_STRING, "-padding", "padding", "Padding", NULL,
	Tk_Offset(Notebook,notebook.paddingObj),-1,
	TK_OPTION_NULL_OK,0,GEOMETRY_CHANGED },

    WIDGET_INHERIT_OPTIONS(ttkCoreOptionSpecs)
};

/* Notebook style options:
 */
typedef struct
{
    Ttk_PositionSpec	tabPosition;	/* Where to place tabs */
    Ttk_Padding 	tabMargins;	/* Margins around tab row */
    Ttk_PositionSpec 	tabPlacement;	/* How to pack tabs within tab row */
    Ttk_Orient		tabOrient;	/* ... */
    int 		minTabWidth;	/* Minimum tab width */
    Ttk_Padding 	padding;	/* External padding */
} NotebookStyle;

static void NotebookStyleOptions(Notebook *nb, NotebookStyle *nbstyle)
{
    Tcl_Obj *objPtr;

    nbstyle->tabPosition = TTK_PACK_TOP | TTK_STICK_W;
    if ((objPtr = Ttk_QueryOption(nb->core.layout, "-tabposition", 0)) != 0) {
	TtkGetLabelAnchorFromObj(NULL, objPtr, &nbstyle->tabPosition);
    }

    /* compute tabPlacement and tabOrient as function of tabPosition:
     */
    if (nbstyle->tabPosition & TTK_PACK_LEFT) {
	nbstyle->tabPlacement = TTK_PACK_TOP | TTK_STICK_E;
	nbstyle->tabOrient = TTK_ORIENT_VERTICAL;
    } else if (nbstyle->tabPosition & TTK_PACK_RIGHT) {
	nbstyle->tabPlacement = TTK_PACK_TOP | TTK_STICK_W;
	nbstyle->tabOrient = TTK_ORIENT_VERTICAL;
    } else if (nbstyle->tabPosition & TTK_PACK_BOTTOM) {
	nbstyle->tabPlacement = TTK_PACK_LEFT | TTK_STICK_N;
	nbstyle->tabOrient = TTK_ORIENT_HORIZONTAL;
    } else { /* Assume TTK_PACK_TOP */
	nbstyle->tabPlacement = TTK_PACK_LEFT | TTK_STICK_S;
	nbstyle->tabOrient = TTK_ORIENT_HORIZONTAL;
    }

    nbstyle->tabMargins = Ttk_UniformPadding(0);
    if ((objPtr = Ttk_QueryOption(nb->core.layout, "-tabmargins", 0)) != 0) {
	Ttk_GetBorderFromObj(NULL, objPtr, &nbstyle->tabMargins);
    }

    nbstyle->padding = Ttk_UniformPadding(0);
    if ((objPtr = Ttk_QueryOption(nb->core.layout, "-padding", 0)) != 0) {
	Ttk_GetPaddingFromObj(NULL,nb->core.tkwin,objPtr,&nbstyle->padding);
    }

    nbstyle->minTabWidth = DEFAULT_MIN_TAB_WIDTH;
    if ((objPtr = Ttk_QueryOption(nb->core.layout, "-mintabwidth", 0)) != 0) {
	Tcl_GetIntFromObj(NULL, objPtr, &nbstyle->minTabWidth);
    }
}

/*------------------------------------------------------------------------
 * +++ Tab management.
 */

/*
 * IdentifyTab --
 * 	Return the index of the tab at point x,y,
 * 	or -1 if no tab at that point.
 */
static int IdentifyTab(Notebook *nb, int x, int y)
{
    int index;
    for (index = 0; index < Ttk_NumberSlaves(nb->notebook.mgr); ++index) {
	Tab *tab = Ttk_SlaveData(nb->notebook.mgr,index);
	if (	tab->state != TAB_STATE_HIDDEN
	     && Ttk_BoxContains(tab->parcel, x,y))
	{
	    return index;
	}
    }
    return -1;
}

/*
 * ActivateTab --
 * 	Set the active tab index, redisplay if necessary.
 */
static void ActivateTab(Notebook *nb, int index)
{
    if (index != nb->notebook.activeIndex) {
	nb->notebook.activeIndex = index;
	TtkRedisplayWidget(&nb->core);
    }
}

/*
 * TabState --
 * 	Return the state of the specified tab, based on
 * 	notebook state, currentIndex, activeIndex, and user-specified tab state.
 *	The USER1 bit is set for the leftmost tab, and USER2
 * 	is set for the rightmost tab.
 */
static Ttk_State TabState(Notebook *nb, int index)
{
    Ttk_State state = nb->core.state;
    Tab *tab = Ttk_SlaveData(nb->notebook.mgr, index);

    if (index == nb->notebook.currentIndex) {
	state |= TTK_STATE_SELECTED;
    } else {
	state &= ~TTK_STATE_FOCUS;
    }

    if (index == nb->notebook.activeIndex) {
	state |= TTK_STATE_ACTIVE;
    }
    if (index == 0) {
    	state |= TTK_STATE_USER1;
    }
    if (index == Ttk_NumberSlaves(nb->notebook.mgr) - 1) {
    	state |= TTK_STATE_USER2;
    }
    if (tab->state == TAB_STATE_DISABLED) {
	state |= TTK_STATE_DISABLED;
    }

    return state;
}

/*------------------------------------------------------------------------
 * +++ Geometry management - size computation.
 */

/* TabrowSize --
 *	Compute max height and total width of all tabs (horizontal layouts)
 *	or total height and max width (vertical layouts).
 *
 * Side effects:
 * 	Sets width and height fields for all tabs.
 *
 * Notes:
 * 	Hidden tabs are included in the perpendicular computation
 * 	(max height/width) but not parallel (total width/height).
 */
static void TabrowSize(
    Notebook *nb, Ttk_Orient orient, int *widthPtr, int *heightPtr)
{
    Ttk_Layout tabLayout = nb->notebook.tabLayout;
    int tabrowWidth = 0, tabrowHeight = 0;
    int i;

    for (i = 0; i < Ttk_NumberSlaves(nb->notebook.mgr); ++i) {
	Tab *tab = Ttk_SlaveData(nb->notebook.mgr, i);
	Ttk_State tabState = TabState(nb,i);

	Ttk_RebindSublayout(tabLayout, tab);
	Ttk_LayoutSize(tabLayout,tabState,&tab->width,&tab->height);

	if (orient == TTK_ORIENT_HORIZONTAL) {
	    tabrowHeight = MAX(tabrowHeight, tab->height);
	    if (tab->state != TAB_STATE_HIDDEN) { tabrowWidth += tab->width; }
	} else {
	    tabrowWidth = MAX(tabrowWidth, tab->width);
	    if (tab->state != TAB_STATE_HIDDEN) { tabrowHeight += tab->height; }
	}
    }

    *widthPtr = tabrowWidth;
    *heightPtr = tabrowHeight;
}

/* NotebookSize -- GM and widget size hook.
 *
 * Total height is tab height + client area height + pane internal padding
 * Total width is max(client width, tab width) + pane internal padding
 * Client area size determined by max size of slaves,
 * overridden by -width and/or -height if nonzero.
 */

static int NotebookSize(void *clientData, int *widthPtr, int *heightPtr)
{
    Notebook *nb = clientData;
    NotebookStyle nbstyle;
    Ttk_Padding padding;
    Ttk_LayoutNode *clientNode = Ttk_LayoutFindNode(nb->core.layout, "client");
    int clientWidth = 0, clientHeight = 0,
    	reqWidth = 0, reqHeight = 0,
	tabrowWidth = 0, tabrowHeight = 0;
    int i;

    NotebookStyleOptions(nb, &nbstyle);

    /* Compute max requested size of all slaves:
     */
    for (i = 0; i < Ttk_NumberSlaves(nb->notebook.mgr); ++i) {
	Tk_Window slaveWindow = Ttk_SlaveWindow(nb->notebook.mgr, i);
	Tab *tab = Ttk_SlaveData(nb->notebook.mgr, i);
	int slaveWidth
	    = Tk_ReqWidth(slaveWindow) + Ttk_PaddingWidth(tab->padding);
	int slaveHeight
	    = Tk_ReqHeight(slaveWindow) + Ttk_PaddingHeight(tab->padding);

	clientWidth = MAX(clientWidth, slaveWidth);
	clientHeight = MAX(clientHeight, slaveHeight);
    }

    /* Client width/height overridable by widget options:
     */
    Tcl_GetIntFromObj(NULL, nb->notebook.widthObj,&reqWidth);
    Tcl_GetIntFromObj(NULL, nb->notebook.heightObj,&reqHeight);
    if (reqWidth > 0)
	clientWidth = reqWidth;
    if (reqHeight > 0)
	clientHeight = reqHeight;

    /* Tab row:
     */
    TabrowSize(nb, nbstyle.tabOrient, &tabrowWidth, &tabrowHeight);
    tabrowHeight += Ttk_PaddingHeight(nbstyle.tabMargins);
    tabrowWidth += Ttk_PaddingWidth(nbstyle.tabMargins);

    /* Account for exterior and interior padding:
     */
    padding = nbstyle.padding;
    if (clientNode) {
	Ttk_Padding ipad =
	    Ttk_LayoutNodeInternalPadding(nb->core.layout, clientNode);
	padding = Ttk_AddPadding(padding, ipad);
    }

    *widthPtr = MAX(tabrowWidth, clientWidth) + Ttk_PaddingWidth(padding);
    *heightPtr = tabrowHeight + clientHeight + Ttk_PaddingHeight(padding);

    return 1;
}

/*------------------------------------------------------------------------
 * +++ Geometry management - layout.
 */

/* SqueezeTabs --
 *	If the notebook is not wide enough to display all tabs,
 *	attempt to decrease tab widths to fit.
 *
 *	All tabs are shrunk by an equal amount, but will not be made
 *	smaller than the minimum width.  (If all the tabs still do
 *	not fit in the available space, the rightmost tabs are truncated).
 *
 *	The algorithm does not always yield an optimal layout, but does
 *	have the important property that decreasing the available width
 *	by one pixel will cause at most one tab to shrink by one pixel;
 *	this means that tabs resize "smoothly" when the window shrinks
 *	and grows.
 *
 * @@@ <<NOTE-TABPOSITION>> bug: only works for horizontal orientations
 */

static void SqueezeTabs(
    Notebook *nb, int desiredWidth, int availableWidth, int minTabWidth)
{
    int nTabs = Ttk_NumberSlaves(nb->notebook.mgr);
    int shrinkage = desiredWidth - availableWidth;
    int extra = 0;
    int i;

    for (i = 0; i < nTabs; ++i) {
	Tab *tab = Ttk_SlaveData(nb->notebook.mgr,i);
	int shrink = (shrinkage/nTabs) + (i < (shrinkage%nTabs)) + extra;
	int shrinkability = MAX(0, tab->width - minTabWidth);
	int delta = MIN(shrinkability, shrink);
	tab->width -= delta;
	extra = shrink - delta;
    }
}

/* PlaceTabs --
 * 	Compute all tab parcels.
 */
static void PlaceTabs(
    Notebook *nb, Ttk_Box tabrowBox, Ttk_PositionSpec tabPlacement)
{
    Ttk_Layout tabLayout = nb->notebook.tabLayout;
    int nTabs = Ttk_NumberSlaves(nb->notebook.mgr);
    int i;

    for (i = 0; i < nTabs; ++i) {
	Tab *tab = Ttk_SlaveData(nb->notebook.mgr, i);
	Ttk_State tabState = TabState(nb, i);

	if (tab->state != TAB_STATE_HIDDEN) {
	    Ttk_Padding expand = Ttk_UniformPadding(0);
	    Tcl_Obj *expandObj = Ttk_QueryOption(tabLayout,"-expand",tabState);

	    if (expandObj) {
		Ttk_GetBorderFromObj(NULL, expandObj, &expand);
	    }

	    tab->parcel =
		Ttk_ExpandBox(
		    Ttk_PositionBox(&tabrowBox,
			tab->width, tab->height, tabPlacement),
		    expand);
	}
    }
}

/* NotebookDoLayout --
 *	Computes notebook layout and places tabs.
 *
 * Side effects:
 * 	Sets clientArea, used to place slave panes.
 */
static void NotebookDoLayout(void *recordPtr)
{
    Notebook *nb = recordPtr;
    Tk_Window nbwin = nb->core.tkwin;
    Ttk_Box cavity = Ttk_WinBox(nbwin);
    int tabrowWidth = 0, tabrowHeight = 0;
    Ttk_LayoutNode *clientNode = Ttk_LayoutFindNode(nb->core.layout, "client");
    Ttk_Box tabrowBox;
    NotebookStyle nbstyle;

    NotebookStyleOptions(nb, &nbstyle);

    /* Notebook internal padding:
     */
    cavity = Ttk_PadBox(cavity, nbstyle.padding);

    /* Layout for notebook background (base layout):
     */
    Ttk_PlaceLayout(nb->core.layout, nb->core.state, Ttk_WinBox(nbwin));

    /* Place tabs:
     */
    TabrowSize(nb, nbstyle.tabOrient, &tabrowWidth, &tabrowHeight);
    tabrowBox = Ttk_PadBox(
		    Ttk_PositionBox(&cavity,
			tabrowWidth + Ttk_PaddingWidth(nbstyle.tabMargins),
			tabrowHeight + Ttk_PaddingHeight(nbstyle.tabMargins),
			nbstyle.tabPosition),
		    nbstyle.tabMargins);

    if (tabrowWidth > tabrowBox.width) {
	SqueezeTabs(nb, tabrowWidth, tabrowBox.width, nbstyle.minTabWidth);
    }
    PlaceTabs(nb, tabrowBox, nbstyle.tabPlacement);

    /* Layout for client area frame:
     */
    if (clientNode) {
	Ttk_PlaceLayoutNode(nb->core.layout, clientNode, cavity);
	cavity = Ttk_LayoutNodeInternalParcel(nb->core.layout, clientNode);
    }

    if (cavity.height <= 0) cavity.height = 1;
    if (cavity.width <= 0) cavity.width = 1;

    nb->notebook.clientArea = cavity;
}

/*
 * NotebookPlaceSlave --
 * 	Set the position and size of a child widget
 * 	based on the current client area and slave options:
 */
static void NotebookPlaceSlave(Notebook *nb, int slaveIndex)
{
    Tab *tab = Ttk_SlaveData(nb->notebook.mgr, slaveIndex);
    Tk_Window slaveWindow = Ttk_SlaveWindow(nb->notebook.mgr, slaveIndex);
    Ttk_Box slaveBox =
	Ttk_StickBox(Ttk_PadBox(nb->notebook.clientArea, tab->padding),
	    Tk_ReqWidth(slaveWindow), Tk_ReqHeight(slaveWindow),tab->sticky);

    Ttk_PlaceSlave(nb->notebook.mgr, slaveIndex,
	slaveBox.x, slaveBox.y, slaveBox.width, slaveBox.height);
}

/* NotebookPlaceSlaves --
 * 	Geometry manager hook.
 */
static void NotebookPlaceSlaves(void *recordPtr)
{
    Notebook *nb = recordPtr;
    int currentIndex = nb->notebook.currentIndex;
    if (currentIndex >= 0) {
	NotebookDoLayout(nb);
	NotebookPlaceSlave(nb, currentIndex);
    }
}

/*
 * SelectTab(nb, index) --
 * 	Change the currently-selected tab.
 */
static void SelectTab(Notebook *nb, int index)
{
    Tab *tab = Ttk_SlaveData(nb->notebook.mgr,index);
    int currentIndex = nb->notebook.currentIndex;

    if (index == currentIndex) {
	return;
    }

    if (TabState(nb, index) & TTK_STATE_DISABLED) {
	return;
    }

    /* Unhide the tab if it is currently hidden and being selected.
     */
    if (tab->state == TAB_STATE_HIDDEN) {
	tab->state = TAB_STATE_NORMAL;
    }

    if (currentIndex >= 0) {
	Ttk_UnmapSlave(nb->notebook.mgr, currentIndex);
    }

    NotebookPlaceSlave(nb, index);

    nb->notebook.currentIndex = index;
    TtkRedisplayWidget(&nb->core);

    TtkSendVirtualEvent(nb->core.tkwin, "NotebookTabChanged");
}

/* NextTab --
 * 	Returns the index of the next tab after the specified tab
 * 	in the normal state (e.g., not hidden or disabled),
 * 	or -1 if all tabs are disabled or hidden.
 */
static int NextTab(Notebook *nb, int index)
{
    int nTabs = Ttk_NumberSlaves(nb->notebook.mgr);
    int nextIndex;

    /* Scan forward for following usable tab:
     */
    for (nextIndex = index + 1; nextIndex < nTabs; ++nextIndex) {
	Tab *tab = Ttk_SlaveData(nb->notebook.mgr, nextIndex);
	if (tab->state == TAB_STATE_NORMAL) {
	    return nextIndex;
	}
    }

    /* Not found -- scan backwards.
     */
    for (nextIndex = index - 1; nextIndex >= 0; --nextIndex) {
	Tab *tab = Ttk_SlaveData(nb->notebook.mgr, nextIndex);
	if (tab->state == TAB_STATE_NORMAL) {
	    return nextIndex;
	}
    }

    /* Still nothing.  Give up.
     */
    return -1;
}

/* SelectNearestTab --
 * 	Handles the case where the current tab is forgotten, hidden,
 * 	or destroyed. Select the next available tab; or, if there is none,
 * 	leaves all tabs unselected.
 */
static void SelectNearestTab(Notebook *nb)
{
    int nextIndex = NextTab(nb, nb->notebook.currentIndex);

    if (nextIndex >= 0) {
	SelectTab(nb, nextIndex);
    } else {
	/* No available tabs -- unmap current one.
	 * ASSERT: this is safe to do even when the slave is being destroyed.
	 */
	if (nb->notebook.currentIndex >= 0) {
	    Ttk_UnmapSlave(nb->notebook.mgr, nb->notebook.currentIndex);
	    TtkSendVirtualEvent(nb->core.tkwin, "NotebookTabChanged");
	}
	nb->notebook.currentIndex = -1;
    }
}

/* TabAdded -- GM SlaveAdded hook.
 */
static void TabAdded(Ttk_Manager *mgr, int slaveIndex) { /* No-op */ }

/* TabRemoved -- GM SlaveRemoved hook.
 * 	Select the next tab if the current one is being removed.
 * 	Adjust currentIndex to account for removed slave if needed.
 */
static void TabRemoved(Ttk_Manager *mgr, int index)
{
    Notebook *nb = mgr->managerData;

    if (index == nb->notebook.currentIndex) {
	SelectNearestTab(nb);
    }

    if (index < nb->notebook.currentIndex) {
	--nb->notebook.currentIndex;
    }

    TtkRedisplayWidget(&nb->core);
}

/* TabConfigured -- GM slaveConfigured hook.
 */
static int TabConfigured(
    Tcl_Interp *interp, Ttk_Manager *mgr, Ttk_Slave *slave, unsigned mask)
{
    Tab *tab = slave->slaveData;
    Ttk_Sticky sticky = tab->sticky;
    Tk_Window tkwin = mgr->masterWindow;

    /* Check options:
     * @@@ TODO: validate -image option.
     */
    if (Ttk_GetStickyFromObj(interp, tab->stickyObj, &sticky) != TCL_OK) {
	return TCL_ERROR;
    }
    if (Ttk_GetPaddingFromObj(interp,tkwin,tab->paddingObj,&tab->padding)
	!= TCL_OK)
    {
	return TCL_ERROR;
    }

    tab->sticky = sticky;
    return TCL_OK;
}

static Ttk_ManagerSpec NotebookManagerSpec =
{
    { "notebook", Ttk_GeometryRequestProc, Ttk_LostSlaveProc },
    PaneOptionSpecs, sizeof(Tab),

    NotebookSize,
    NotebookPlaceSlaves,
    TabAdded,
    TabRemoved,
    TabConfigured
};

/*------------------------------------------------------------------------
 * +++ Event handlers.
 */

/* NotebookEventHandler --
 * 	Tracks the active tab.
 */
static const int NotebookEventMask
    = StructureNotifyMask
    | PointerMotionMask
    | LeaveWindowMask
    ;
static void NotebookEventHandler(ClientData clientData, XEvent *eventPtr)
{
    Notebook *nb = clientData;

    if (eventPtr->type == DestroyNotify) { /* Remove self */
	Tk_DeleteEventHandler(nb->core.tkwin,
	    NotebookEventMask, NotebookEventHandler, clientData);
    } else if (eventPtr->type == MotionNotify) {
	int index = IdentifyTab(nb, eventPtr->xmotion.x, eventPtr->xmotion.y);
	ActivateTab(nb, index);
    } else if (eventPtr->type == LeaveNotify) {
	ActivateTab(nb, -1);
    }
}

/*------------------------------------------------------------------------
 * +++ Utilities.
 */

/* FindTabIndex --
 *	Find the index of the specified tab.
 *	Tab identifiers are one of:
 *
 *	+ positional specifications @x,y,
 *	+ "current",
 *	+ numeric indices [0..nTabs],
 *	+ slave window names
 *
 *	Stores index of specified tab in *index_rtn, -1 if not found.
 *
 *	Returns TCL_ERROR and leaves an error message in interp->result
 *	if the tab identifier was incorrect.
 *
 *	See also: GetTabIndex.
 */
static int FindTabIndex(
    Tcl_Interp *interp, Notebook *nb, Tcl_Obj *objPtr, int *index_rtn)
{
    const char *string = Tcl_GetString(objPtr);
    int x, y;

    *index_rtn = -1;

    /* Check for @x,y ...
     */
    if (string[0] == '@' && sscanf(string, "@%d,%d",&x,&y) == 2) {
	*index_rtn = IdentifyTab(nb, x, y);
	return TCL_OK;
    }

    /* ... or "current" ...
     */
    if (!strcmp(string, "current")) {
	*index_rtn = nb->notebook.currentIndex;
	return TCL_OK;
    }

    /* ... or integer index or slave window name:
     */
    if (Ttk_GetSlaveFromObj(
	    interp, nb->notebook.mgr, objPtr, index_rtn) != NULL)
    {
	return TCL_OK;
    }

    /* Nothing matched; Ttk_GetSlaveFromObj will have left error message.
     */
    return TCL_ERROR;
}

/* GetTabIndex --
 * 	Get the index of an existing tab.
 * 	Tab identifiers are as per FindTabIndex.
 * 	Returns TCL_ERROR if the tab does not exist.
 */
static int GetTabIndex(
    Tcl_Interp *interp, Notebook *nb, Tcl_Obj *objPtr, int *index_rtn)
{
    int status = FindTabIndex(interp, nb, objPtr, index_rtn);

    if (status == TCL_OK && *index_rtn < 0) {
	Tcl_ResetResult(interp);
	Tcl_AppendResult(interp,
	    "tab '", Tcl_GetString(objPtr), "' not found",
	    NULL);
	status = TCL_ERROR;
    }
    return status;
}

/*------------------------------------------------------------------------
 * +++ Widget command routines.
 */

/* $nb add window ?options ... ?
 */
static int NotebookAddCommand(
    Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[], void *recordPtr)
{
    Notebook *nb = recordPtr;
    int index = nb->notebook.mgr->nSlaves;
    Tk_Window slaveWindow;

    if (objc <= 2 || objc % 2 != 1) {
	Tcl_WrongNumArgs(interp, 2, objv, "window ?options...?");
	return TCL_ERROR;
    }

    slaveWindow = Tk_NameToWindow(interp,Tcl_GetString(objv[2]),nb->core.tkwin);
    if (!slaveWindow) {
	return TCL_ERROR;
    }

    /* Create and initialize new tab:
     */
    if (TCL_OK != Ttk_AddSlave(
	    interp, nb->notebook.mgr, slaveWindow, index, objc-3,objv+3) )
    {
    	return TCL_ERROR;
    }

    /* If no tab is currently selected (or if this is the first tab),
     * select this one:
     */
    if (nb->notebook.currentIndex < 0) {
	SelectTab(nb, index);
    }

    TtkResizeWidget(&nb->core);

    return TCL_OK;
}

/* $nb insert $index $tab ?options...?
 * 	Insert new tab, or move existing one.
 */
static int NotebookInsertCommand(
    Tcl_Interp *interp, int objc, Tcl_Obj *const objv[], void *recordPtr)
{
    Notebook *nb = recordPtr;
    int current = nb->notebook.currentIndex;
    int srcIndex, destIndex;
    int status = TCL_OK;

    if (objc < 4) {
	Tcl_WrongNumArgs(interp, 2,objv, "index slave ?options...?");
	return TCL_ERROR;
    }

    if (!strcmp(Tcl_GetString(objv[2]), "end")) {
	destIndex = Ttk_NumberSlaves(nb->notebook.mgr);
    } else if (!Ttk_GetSlaveFromObj(
		interp, nb->notebook.mgr, objv[2], &destIndex)) {
	return TCL_ERROR;
    }

    if (!Ttk_GetSlaveFromObj(interp, nb->notebook.mgr, objv[3], &srcIndex)) {
	/* Try adding new slave:
	 */
	Tk_Window slaveWindow =
	    Tk_NameToWindow(interp,Tcl_GetString(objv[3]),nb->core.tkwin);
	if (!slaveWindow) {
	    return TCL_ERROR;
	}

	if (Ttk_AddSlave(interp, nb->notebook.mgr, slaveWindow,
		destIndex, objc - 4, objv + 4) != TCL_OK)
	{
	    return TCL_ERROR;
	}
	if (nb->notebook.currentIndex <= destIndex) {
	    ++nb->notebook.currentIndex;
	}
	return TCL_OK;
    }

    /* else - move existing slave: */

    if (destIndex >= nb->notebook.mgr->nSlaves) {
	destIndex  = nb->notebook.mgr->nSlaves - 1;
    }
    Ttk_ReorderSlave(nb->notebook.mgr, srcIndex, destIndex);

    /* Adjust internal indexes:
     */
    nb->notebook.activeIndex = -1;
    if (current == srcIndex) {
	nb->notebook.currentIndex = destIndex;
    } else if (destIndex <= current && current < srcIndex) {
	++nb->notebook.currentIndex;
    } else if (srcIndex < current && current <= destIndex) {
	--nb->notebook.currentIndex;
    }

    if (objc > 4) {
	status = Ttk_ConfigureSlave(interp, nb->notebook.mgr,
	     nb->notebook.mgr->slaves[destIndex], objc-4,objv+4);
    }

    TtkRedisplayWidget(&nb->core);

    return status;
}

/* $nb forget $item --
 * 	Removes the selected tab.
 */
static int NotebookForgetCommand(
    Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[], void *recordPtr)
{
    Notebook *nb = recordPtr;
    int index;

    if (objc != 3) {
	Tcl_WrongNumArgs(interp, 2, objv, "tab");
	return TCL_ERROR;
    }

    if (GetTabIndex(interp, nb, objv[2], &index) != TCL_OK) {
	return TCL_ERROR;
    }

    Ttk_ForgetSlave(nb->notebook.mgr, index);

    return TCL_OK;
}

/* $nb identify $x $y --
 * 	Returns name of tab element at $x,$y; empty string if none.
 */
static int NotebookIdentifyCommand(
    Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[], void *recordPtr)
{
    Notebook *nb = recordPtr;
    Ttk_LayoutNode *node = NULL;
    int x, y, tabIndex;

    if (objc != 4) {
	Tcl_WrongNumArgs(interp, 2, objv, "x y");
	return TCL_ERROR;
    }

    if (   Tcl_GetIntFromObj(interp, objv[2], &x) != TCL_OK
	|| Tcl_GetIntFromObj(interp, objv[3], &y) != TCL_OK)
    {
	return TCL_ERROR;
    }

    tabIndex = IdentifyTab(nb, x, y);
    if (tabIndex >= 0) {
	Tab *tab = Ttk_SlaveData(nb->notebook.mgr, tabIndex);
	Ttk_State state = TabState(nb, tabIndex);
	Ttk_Layout tabLayout = nb->notebook.tabLayout;

	Ttk_RebindSublayout(tabLayout, tab);
	Ttk_PlaceLayout(tabLayout, state, tab->parcel);

	node = Ttk_LayoutIdentify(tabLayout, x, y); 
    }

    if (node) {
	const char *elementName = Ttk_LayoutNodeName(node);
	Tcl_SetObjResult(interp,Tcl_NewStringObj(elementName,-1));
    }

    return TCL_OK;
}

/* $nb index $item --
 * 	Returns the integer index of the tab specified by $item,
 * 	the empty string if $item does not identify a tab.
 *	See above for valid item formats.
 */
static int NotebookIndexCommand(
    Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[], void *recordPtr)
{
    Notebook *nb = recordPtr;
    int index, status;

    if (objc != 3) {
	Tcl_WrongNumArgs(interp, 2, objv, "tab");
	return TCL_ERROR;
    }

    /*
     * Special-case for "end":
     */
    if (!strcmp("end", Tcl_GetString(objv[2]))) {
	Tcl_SetObjResult(interp, Tcl_NewIntObj(nb->notebook.mgr->nSlaves));
	return TCL_OK;
    }

    status = FindTabIndex(interp, nb, objv[2], &index);
    if (status == TCL_OK && index >= 0) {
	Tcl_SetObjResult(interp, Tcl_NewIntObj(index));
    }

    return status;
}

/* $nb select ?$item? --
 * 	Select the specified tab, or return the widget path of
 * 	the currently-selected pane.
 */
static int NotebookSelectCommand(
    Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[], void *recordPtr)
{
    Notebook *nb = recordPtr;

    if (objc == 2) {
	if (nb->notebook.currentIndex >= 0) {
	    Tk_Window pane = Ttk_SlaveWindow(
		nb->notebook.mgr, nb->notebook.currentIndex);
	    Tcl_SetObjResult(interp, Tcl_NewStringObj(Tk_PathName(pane), -1));
	}
	return TCL_OK;
    } else if (objc == 3) {
	int index, status = GetTabIndex(interp, nb, objv[2], &index);
	if (status == TCL_OK) {
	    SelectTab(nb, index);
	}
	return status;
    } /*else*/
    Tcl_WrongNumArgs(interp, 2, objv, "?tab?");
    return TCL_ERROR;
}

/* $nb tabs --
 * 	Return list of tabs.
 */
static int NotebookTabsCommand(
    Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[], void *recordPtr)
{
    Notebook *nb = recordPtr;
    Ttk_Manager *mgr = nb->notebook.mgr;
    Tcl_Obj *result;
    int i;

    if (objc != 2) {
	Tcl_WrongNumArgs(interp, 2, objv, "");
	return TCL_ERROR;
    }

    result = Tcl_NewListObj(0, NULL);
    for (i = 0; i < mgr->nSlaves; ++i) {
	const char *pathName = Tk_PathName(Ttk_SlaveWindow(mgr,i));
	Tcl_ListObjAppendElement(interp, result, Tcl_NewStringObj(pathName,-1));
    }
    Tcl_SetObjResult(interp, result);

    return TCL_OK;
}

/* $nb tab $tab ?-option ?value -option value...??
 */
static int NotebookTabCommand(
    Tcl_Interp *interp, int objc, Tcl_Obj * CONST objv[], void *recordPtr)
{
    Notebook *nb = recordPtr;
    Ttk_Manager *mgr = nb->notebook.mgr;
    int index;
    Ttk_Slave *slave;
    Tab *tab;

    if (objc < 3) {
	Tcl_WrongNumArgs(interp, 2, objv, "tab ?-option ?value??...");
	return TCL_ERROR;
    }

    if (GetTabIndex(interp, nb, objv[2], &index) != TCL_OK) {
	return TCL_ERROR;
    }

    slave = mgr->slaves[index];
    tab = Ttk_SlaveData(mgr, index);

    if (objc == 3) {
	return TtkEnumerateOptions(interp, tab,
	    PaneOptionSpecs, nb->notebook.paneOptionTable, nb->core.tkwin);
    } else if (objc == 4) {
	return TtkGetOptionValue(interp, tab, objv[3],
	    nb->notebook.paneOptionTable, nb->core.tkwin);
    } /* else */

    if (Ttk_ConfigureSlave(interp, mgr, slave, objc - 3,objv + 3) != TCL_OK) {
	return TCL_ERROR;
    }

    /* If the current tab has become disabled or hidden,
     * select the next nondisabled, unhidden one:
     */
    if (index == nb->notebook.currentIndex && tab->state != TAB_STATE_NORMAL) {
	SelectNearestTab(nb);
    }

    TtkResizeWidget(&nb->core);
    return TCL_OK;
}

/* Subcommand table:
 */
static WidgetCommandSpec NotebookCommands[] =
{
    { "add",    	NotebookAddCommand },
    { "configure",	TtkWidgetConfigureCommand },
    { "cget",		TtkWidgetCgetCommand },
    { "forget",		NotebookForgetCommand },
    { "identify",	NotebookIdentifyCommand },
    { "index",		NotebookIndexCommand },
    { "insert",  	NotebookInsertCommand },
    { "instate",	TtkWidgetInstateCommand },
    { "select",		NotebookSelectCommand },
    { "state",  	TtkWidgetStateCommand },
    { "tab",   		NotebookTabCommand },
    { "tabs",   	NotebookTabsCommand },
    { 0,0 }
};

/*------------------------------------------------------------------------
 * +++ Widget class hooks.
 */

static int NotebookInitialize(Tcl_Interp *interp, void *recordPtr)
{
    Notebook *nb = recordPtr;

    nb->notebook.mgr = Ttk_CreateManager(
	    &NotebookManagerSpec, recordPtr, nb->core.tkwin);

    nb->notebook.tabOptionTable = Tk_CreateOptionTable(interp,TabOptionSpecs);
    nb->notebook.paneOptionTable = Tk_CreateOptionTable(interp,PaneOptionSpecs);

    nb->notebook.currentIndex = -1;
    nb->notebook.activeIndex = -1;
    nb->notebook.tabLayout = 0;

    nb->notebook.clientArea = Ttk_MakeBox(0,0,1,1);

    Tk_CreateEventHandler(
	nb->core.tkwin, NotebookEventMask, NotebookEventHandler, recordPtr);

    return TCL_OK;
}

static void NotebookCleanup(void *recordPtr)
{
    Notebook *nb = recordPtr;

    Ttk_DeleteManager(nb->notebook.mgr);
    Tk_DeleteOptionTable(nb->notebook.tabOptionTable);
    Tk_DeleteOptionTable(nb->notebook.paneOptionTable);

    if (nb->notebook.tabLayout)
	Ttk_FreeLayout(nb->notebook.tabLayout);
}

static int NotebookConfigure(Tcl_Interp *interp, void *clientData, int mask)
{
    Notebook *nb = clientData;

    /*
     * Error-checks:
     */
    if (nb->notebook.paddingObj) {
	/* Check for valid -padding: */
	Ttk_Padding unused;
	if (Ttk_GetPaddingFromObj(
		    interp, nb->core.tkwin, nb->notebook.paddingObj, &unused)
		!= TCL_OK) {
	    return TCL_ERROR;
	}
    }

    return TtkCoreConfigure(interp, clientData, mask);
}

/* NotebookGetLayout  --
 * 	GetLayout widget hook.
 */
static Ttk_Layout NotebookGetLayout(
    Tcl_Interp *interp, Ttk_Theme theme, void *recordPtr)
{
    Notebook *nb = recordPtr;
    Ttk_Layout notebookLayout = TtkWidgetGetLayout(interp, theme, recordPtr);
    Ttk_Layout tabLayout;

    if (!notebookLayout) {
	return NULL;
    }

    tabLayout = Ttk_CreateSublayout(
	interp, theme, notebookLayout, ".Tab",	nb->notebook.tabOptionTable);

    if (tabLayout) {
	if (nb->notebook.tabLayout) {
	    Ttk_FreeLayout(nb->notebook.tabLayout);
	}
	nb->notebook.tabLayout = tabLayout;
    }

    return notebookLayout;
}

/* +++ Display routines.
 */

static void DisplayTab(Notebook *nb, int index, Drawable d)
{
    Ttk_Layout tabLayout = nb->notebook.tabLayout;
    Tab *tab = Ttk_SlaveData(nb->notebook.mgr, index);
    Ttk_State state = TabState(nb, index);

    if (tab->state != TAB_STATE_HIDDEN) {
	Ttk_RebindSublayout(tabLayout, tab);
	Ttk_PlaceLayout(tabLayout, state, tab->parcel);
	Ttk_DrawLayout(tabLayout, state, d);
    }
}

static void NotebookDisplay(void *clientData, Drawable d)
{
    Notebook *nb = clientData;
    int index;

    /* Draw notebook background (base layout):
     */
    Ttk_DrawLayout(nb->core.layout, nb->core.state, d);

    /* Draw tabs from left to right, but draw the current tab last
     * so it will overwrite its neighbors.
     */
    for (index = 0; index < nb->notebook.mgr->nSlaves; ++index) {
	if (index != nb->notebook.currentIndex) {
	    DisplayTab(nb, index, d);
	}
    }
    if (nb->notebook.currentIndex >= 0) {
	DisplayTab(nb, nb->notebook.currentIndex, d);
    }
}

/*------------------------------------------------------------------------
 * +++ Widget specification and layout definitions.
 */

static WidgetSpec NotebookWidgetSpec =
{
    "TNotebook",		/* className */
    sizeof(Notebook),		/* recordSize */
    NotebookOptionSpecs,	/* optionSpecs */
    NotebookCommands,		/* subcommands */
    NotebookInitialize,		/* initializeProc */
    NotebookCleanup,		/* cleanupProc */
    NotebookConfigure,		/* configureProc */
    TtkNullPostConfigure,		/* postConfigureProc */
    NotebookGetLayout, 		/* getLayoutProc */
    NotebookSize,		/* geometryProc */
    NotebookDoLayout,		/* layoutProc */
    NotebookDisplay		/* displayProc */
};

TTK_BEGIN_LAYOUT(NotebookLayout)
    TTK_NODE("Notebook.client", TTK_FILL_BOTH)
TTK_END_LAYOUT

TTK_BEGIN_LAYOUT(TabLayout)
    TTK_GROUP("Notebook.tab", TTK_FILL_BOTH,
	TTK_GROUP("Notebook.padding", TTK_PACK_TOP|TTK_FILL_BOTH,
	    TTK_GROUP("Notebook.focus", TTK_PACK_TOP|TTK_FILL_BOTH,
		TTK_NODE("Notebook.label", TTK_PACK_TOP))))
TTK_END_LAYOUT

/*------------------------------------------------------------------------
 * +++ Initialization.
 */

MODULE_SCOPE 
void TtkNotebook_Init(Tcl_Interp *interp)
{
    Ttk_Theme themePtr = Ttk_GetDefaultTheme(interp);

    Ttk_RegisterLayout(themePtr, "Tab", TabLayout);
    Ttk_RegisterLayout(themePtr, "TNotebook", NotebookLayout);

    RegisterWidget(interp, "ttk::notebook", &NotebookWidgetSpec);
}

/*EOF*/
