1/* $Id$
2 *
3 * Copyright 2004, Joe English
4 *
5 * Support routines for scrollable widgets.
6 *
7 * (This is sort of half-baked; needs some work)
8 *
9 * Scrollable interface:
10 *
11 * 	+ 'first' is controlled by [xy]view widget command
12 * 	  and other scrolling commands like 'see';
13 *      + 'total' depends on widget contents;
14 *      + 'last' depends on first, total, and widget size.
15 *
16 * Choreography (typical usage):
17 *
18 * 	1. User adjusts scrollbar, scrollbar widget calls its -command
19 * 	2. Scrollbar -command invokes the scrollee [xy]view widget method
20 * 	3. TtkScrollviewCommand calls TtkScrollTo(), which updates
21 * 	   'first' and schedules a redisplay.
22 * 	4. Once the scrollee knows 'total' and 'last' (typically in
23 * 	   the LayoutProc), call TtkScrolled(h,first,last,total) to
24 * 	   synchronize the scrollbar.
25 * 	5. The scrollee -[xy]scrollcommand is called (in an idle callback)
26 * 	6. Which calls the scrollbar 'set' method and redisplays the scrollbar.
27 *
28 * If the scrollee has internal scrolling (e.g., a 'see' method),
29 * it should TtkScrollTo() directly (step 2).
30 *
31 * If the widget value changes, it should call TtkScrolled() (step 4).
32 * (This usually happens automatically when the widget is redisplayed).
33 *
34 * If the scrollee's -[xy]scrollcommand changes, it should call
35 * TtkScrollbarUpdateRequired, which will invoke step (5) (@@@ Fix this)
36 */
37
38#include <tk.h>
39#include "ttkTheme.h"
40#include "ttkWidget.h"
41
42/* Private data:
43 */
44#define SCROLL_UPDATE_PENDING  (0x1)
45#define SCROLL_UPDATE_REQUIRED (0x2)
46
47struct ScrollHandleRec
48{
49    unsigned 	flags;
50    WidgetCore	*corePtr;
51    Scrollable	*scrollPtr;
52};
53
54/* TtkCreateScrollHandle --
55 * 	Initialize scroll handle.
56 */
57ScrollHandle TtkCreateScrollHandle(WidgetCore *corePtr, Scrollable *scrollPtr)
58{
59    ScrollHandle h = (ScrollHandle)ckalloc(sizeof(*h));
60
61    h->flags = 0;
62    h->corePtr = corePtr;
63    h->scrollPtr = scrollPtr;
64
65    scrollPtr->first = 0;
66    scrollPtr->last = 1;
67    scrollPtr->total = 1;
68    return h;
69}
70
71/* UpdateScrollbar --
72 *	Call the -scrollcommand callback to sync the scrollbar.
73 * 	Returns: Whatever the -scrollcommand does.
74 */
75static int UpdateScrollbar(Tcl_Interp *interp, ScrollHandle h)
76{
77    Scrollable *s = h->scrollPtr;
78    WidgetCore *corePtr = h->corePtr;
79    char arg1[TCL_DOUBLE_SPACE + 2];
80    char arg2[TCL_DOUBLE_SPACE + 2];
81    int code;
82
83    h->flags &= ~SCROLL_UPDATE_REQUIRED;
84
85    if (s->scrollCmd == NULL) {
86	return TCL_OK;
87    }
88
89    arg1[0] = arg2[0] = ' ';
90    Tcl_PrintDouble(interp, (double)s->first / s->total, arg1+1);
91    Tcl_PrintDouble(interp, (double)s->last / s->total, arg2+1);
92
93    Tcl_Preserve(corePtr);
94    code = Tcl_VarEval(interp, s->scrollCmd, arg1, arg2, NULL);
95    if (WidgetDestroyed(corePtr)) {
96	Tcl_Release(corePtr);
97	return TCL_ERROR;
98    }
99    Tcl_Release(corePtr);
100
101    if (code != TCL_OK && !Tcl_InterpDeleted(interp)) {
102	/* Disable the -scrollcommand, add to stack trace:
103	 */
104	ckfree(s->scrollCmd);
105	s->scrollCmd = 0;
106
107	Tcl_AddErrorInfo(interp, /* @@@ "horizontal" / "vertical" */
108		"\n    (scrolling command executed by ");
109	Tcl_AddErrorInfo(interp, Tk_PathName(h->corePtr->tkwin));
110	Tcl_AddErrorInfo(interp, ")");
111    }
112    return code;
113}
114
115/* UpdateScrollbarBG --
116 * 	Idle handler to update the scrollbar.
117 */
118static void UpdateScrollbarBG(ClientData clientData)
119{
120    ScrollHandle h = (ScrollHandle)clientData;
121    Tcl_Interp *interp = h->corePtr->interp;
122    int code;
123
124    h->flags &= ~SCROLL_UPDATE_PENDING;
125    Tcl_Preserve((ClientData) interp);
126    code = UpdateScrollbar(interp, h);
127    if (code == TCL_ERROR && !Tcl_InterpDeleted(interp)) {
128	Tcl_BackgroundError(interp);
129    }
130    Tcl_Release((ClientData) interp);
131}
132
133/* TtkScrolled --
134 * 	Update scroll info, schedule scrollbar update.
135 */
136void TtkScrolled(ScrollHandle h, int first, int last, int total)
137{
138    Scrollable *s = h->scrollPtr;
139
140    /* Sanity-check inputs:
141     */
142    if (total <= 0) {
143	first = 0;
144	last = 1;
145	total = 1;
146    }
147
148    if (last > total) {
149	first -= (last - total);
150	if (first < 0) first = 0;
151	last = total;
152    }
153
154    if (s->first != first || s->last != last || s->total != total
155	    || (h->flags & SCROLL_UPDATE_REQUIRED))
156    {
157	s->first = first;
158	s->last = last;
159	s->total = total;
160
161	if (!(h->flags & SCROLL_UPDATE_PENDING)) {
162	    Tcl_DoWhenIdle(UpdateScrollbarBG, (ClientData)h);
163	    h->flags |= SCROLL_UPDATE_PENDING;
164	}
165    }
166}
167
168/* TtkScrollbarUpdateRequired --
169 * 	Force a scrollbar update at the next call to TtkScrolled(),
170 * 	even if scroll parameters haven't changed (e.g., if
171 * 	-yscrollcommand has changed).
172 */
173
174void TtkScrollbarUpdateRequired(ScrollHandle h)
175{
176    h->flags |= SCROLL_UPDATE_REQUIRED;
177}
178
179/* TtkScrollviewCommand --
180 * 	Widget [xy]view command implementation.
181 *
182 *  $w [xy]view -- return current view region
183 *  $w [xy]view $index -- set topmost item
184 *  $w [xy]view moveto $fraction
185 *  $w [xy]view scroll $number $what -- scrollbar interface
186 */
187int TtkScrollviewCommand(
188    Tcl_Interp *interp, int objc, Tcl_Obj *const objv[], ScrollHandle h)
189{
190    Scrollable *s = h->scrollPtr;
191    int newFirst = s->first;
192
193    if (objc == 2) {
194	Tcl_Obj *result[2];
195	result[0] = Tcl_NewDoubleObj((double)s->first / s->total);
196	result[1] = Tcl_NewDoubleObj((double)s->last / s->total);
197	Tcl_SetObjResult(interp, Tcl_NewListObj(2, result));
198	return TCL_OK;
199    } else if (objc == 3) {
200	if (Tcl_GetIntFromObj(interp, objv[2], &newFirst) != TCL_OK) {
201	    return TCL_ERROR;
202	}
203    } else {
204	double fraction;
205	int count;
206
207	switch (Tk_GetScrollInfoObj(interp, objc, objv, &fraction, &count)) {
208	    case TK_SCROLL_ERROR:
209		return TCL_ERROR;
210	    case TK_SCROLL_MOVETO:
211		newFirst = (int) ((fraction * s->total) + 0.5);
212		break;
213	    case TK_SCROLL_UNITS:
214		newFirst = s->first + count;
215		break;
216	    case TK_SCROLL_PAGES: {
217		int perPage = s->last - s->first;	/* @@@ */
218		newFirst = s->first + count * perPage;
219		break;
220	    }
221	}
222    }
223
224    TtkScrollTo(h, newFirst);
225
226    return TCL_OK;
227}
228
229void TtkScrollTo(ScrollHandle h, int newFirst)
230{
231    Scrollable *s = h->scrollPtr;
232
233    if (newFirst >= s->total)
234	newFirst = s->total - 1;
235    if (newFirst > s->first && s->last >= s->total) /* don't scroll past end */
236	newFirst = s->first;
237    if (newFirst < 0)
238	newFirst = 0;
239
240    if (newFirst != s->first) {
241	s->first = newFirst;
242	TtkRedisplayWidget(h->corePtr);
243    }
244}
245
246void TtkFreeScrollHandle(ScrollHandle h)
247{
248    if (h->flags & SCROLL_UPDATE_PENDING) {
249	Tcl_CancelIdleCall(UpdateScrollbarBG, (ClientData)h);
250    }
251    ckfree((ClientData)h);
252}
253
254