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