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