5 * $Log: tableVisi.C,v $
6 * Revision 1.6 1995/12/19 00:46:19 tamches
7 * calls to tvMetric constructor use new args
8 * changeNumSigFigs now recognizes possibility of column pix width change
10 * Revision 1.5 1995/12/03 21:09:19 newhall
11 * changed units labeling to match type of data being displayed
13 * Revision 1.4 1995/11/20 20:20:20 tamches
14 * horizontal & vertical grid lines no longer expand past the
17 * Revision 1.3 1995/11/15 01:05:16 tamches
18 * fixed bug which clipped cells incorrectly
20 * Revision 1.2 1995/11/08 21:46:46 tamches
23 * Revision 1.1 1995/11/04 00:45:20 tamches
24 * First version of new table visi
30 #include "tableVisi.h"
32 /* ************************************************************* */
34 extern "C" {bool isnan(double);}
35 void tableVisi::double2string(char *buffer, double val) const {
42 char conversionString[100];
43 sprintf(conversionString, "%%.%dg", numSigFigs);
44 sprintf(buffer, conversionString, val);
46 // cout << "from " << buffer << " to " << flush;
49 if (strlen(buffer)==0)
52 const char *decPtr = strchr(buffer, '.');
54 if (decPtr && (decPtr - buffer <= 3))
55 return; // no commas will be added since there aren't at least 4 integer digits
58 // try for exponential notation
59 decPtr = strchr(buffer, 'e');
62 decPtr = &buffer[strlen(buffer)]; // the '\0'
65 // invariant: decPtr now points to the character AFTER the
66 // last integer digit (i.e., the decimal point), or the '\0'
69 // Now let's walk backwards, looking for places to insert commas
70 char integerPartBuffer[200]; // will include commas
71 char *integerPart = integerPartBuffer;
72 const char *walkPtr = decPtr-1;
74 while (walkPtr >= buffer) {
75 if (copyCount > 0 && copyCount % 3 == 0)
79 *integerPart++ = *walkPtr--;
82 char decimalPart[200];
83 // will incl dec point, if applicable. May be
85 strcpy(decimalPart, decPtr);
87 // note: integerPartBuffer is backwards...we must reverse it
88 char *bufferPtr = buffer;
89 while (--integerPart >= integerPartBuffer)
90 *bufferPtr++ = *integerPart;
91 strcpy(bufferPtr, decimalPart);
93 // cout << buffer << endl;
96 /* ************************************************************* */
98 XFontStruct *tableVisi::myXLoadQueryFont(const string &fontName) const {
99 XFontStruct *result = XLoadQueryFont(Tk_Display(theTkWindow),
100 fontName.string_of());
101 if (result == NULL) {
102 cerr << "could not find font " << fontName << endl;
109 XColor *tableVisi::myTkGetColor(Tcl_Interp *interp, const string &colorName) const {
110 XColor *result = Tk_GetColor(interp, theTkWindow, Tk_GetUid(colorName.string_of()));
111 if (result == NULL) {
112 cerr << "could not allocate color " << colorName << endl;
118 /* ************************************************************* */
120 tableVisi::tableVisi(Tcl_Interp *interp,
122 const string &metricFontName,
123 const string &metricUnitsFontName,
124 const string &focusFontName,
125 const string &cellFontName,
126 const string &iLineColorName,
127 const string &iMetricColorName,
128 const string &iMetricUnitsColorName,
129 const string &iFocusColorName,
130 const string &cellColorName,
131 const string &backgroundColorName,
134 // metrics[], foci[], indirectMetrics[], indirectFoci[], and cells[][]
135 // are all initialized to zero-sized arrays.
137 theTkWindow = iTkWindow;
138 theDisplay = Tk_Display(theTkWindow);
140 offscreenPixmap = (Pixmap)NULL;
141 // sorry, can't XCreatePixmap() until the window becomes mapped.
143 backgroundColor = myTkGetColor(interp, backgroundColorName);
145 offset_x = offset_y = 0;
146 all_cells_width = all_cells_height = 0;
148 metricNameFont = myXLoadQueryFont(metricFontName);
149 metricUnitsFont = myXLoadQueryFont(metricUnitsFontName);
150 focusNameFont = myXLoadQueryFont(focusFontName);
151 cellFont = myXLoadQueryFont(cellFontName);
153 focusLongNameMode = true;
154 numSigFigs = iSigFigs;
155 // dataFormat = Current;
157 maxFocusNamePixWidth = 0;
159 // The GC's, like offscreenPixmap, can't be created until the
160 // window becomes mapped.
161 lineColor = myTkGetColor(interp, iLineColorName);
164 metricNameColor = myTkGetColor(interp, iMetricColorName);
167 metricUnitsColor = myTkGetColor(interp, iMetricUnitsColorName);
168 metricUnitsGC = NULL;
170 focusNameColor = myTkGetColor(interp, iFocusColorName);
173 cellColor = myTkGetColor(interp, cellColorName);
177 tableVisi::~tableVisi() {
178 // arrays metrics[], foci[], indirectMetrics[], indirectFoci[], cells[][] will
179 // delete themselves.
181 Tk_FreeColor(cellColor);
182 Tk_FreeColor(focusNameColor);
183 Tk_FreeColor(metricUnitsColor);
184 Tk_FreeColor(metricNameColor);
185 Tk_FreeColor(lineColor);
186 Tk_FreeColor(backgroundColor);
188 XFreeFont(theDisplay, focusNameFont);
189 XFreeFont(theDisplay, metricUnitsFont);
190 XFreeFont(theDisplay, metricNameFont);
192 if (!offscreenPixmap)
193 // the offscreen pixmap was never allocated(!)...so, we never
194 // got around to mapping the window!
197 XFreeGC(theDisplay, cellGC);
198 XFreeGC(theDisplay, focusNameGC);
199 XFreeGC(theDisplay, metricUnitsGC);
200 XFreeGC(theDisplay, metricNameGC);
201 XFreeGC(theDisplay, lineColorGC);
202 XFreeGC(theDisplay, backgroundGC);
204 XFreePixmap(theDisplay, offscreenPixmap);
207 bool tableVisi::tryFirst() {
208 if (offscreenPixmap) {
209 // the offscreen pixmap has been allocated, so the window
210 // has presumably been mapped already
211 assert(Tk_WindowId(theTkWindow) != 0);
215 // the offscreen pixmap hasn't been allocated, so it's now time
216 // to check to see if it should be.
217 if (Tk_WindowId(theTkWindow) == 0)
218 return false; // nuts; not ready yet
220 // Ready to allocate graphical structures now!
221 offscreenPixmap = XCreatePixmap(Tk_Display(theTkWindow),
222 Tk_WindowId(theTkWindow),
223 1, 1, // dummy width, height
224 Tk_Depth(theTkWindow));
226 values.foreground = backgroundColor->pixel;
227 backgroundGC = XCreateGC(Tk_Display(theTkWindow), Tk_WindowId(theTkWindow),
228 GCForeground, &values);
230 values.foreground = lineColor->pixel;
231 lineColorGC = XCreateGC(Tk_Display(theTkWindow), Tk_WindowId(theTkWindow),
232 GCForeground, &values);
234 values.foreground = metricNameColor->pixel;
235 values.font = metricNameFont->fid;
236 metricNameGC = XCreateGC(Tk_Display(theTkWindow), Tk_WindowId(theTkWindow),
237 GCForeground | GCFont, &values);
239 values.foreground = metricUnitsColor->pixel;
240 values.font = metricUnitsFont->fid;
241 metricUnitsGC = XCreateGC(Tk_Display(theTkWindow), Tk_WindowId(theTkWindow),
242 GCForeground | GCFont, &values);
244 values.foreground = focusNameColor->pixel;
245 values.font = focusNameFont->fid;
246 focusNameGC = XCreateGC(Tk_Display(theTkWindow), Tk_WindowId(theTkWindow),
247 GCForeground | GCFont, &values);
249 values.foreground = cellColor->pixel;
250 values.font = cellFont->fid;
251 cellGC = XCreateGC(Tk_Display(theTkWindow), Tk_WindowId(theTkWindow),
252 GCForeground | GCFont, &values);
257 void tableVisi::resizeScrollbars(Tcl_Interp *interp) {
258 // used to be a tcl routine (resize1Scrollbar):
260 const int visible_cell_width = Tk_Width(theTkWindow) -
261 getFocusAreaPixWidth();
262 resizeScrollbar(interp, ".horizScrollbar",
263 all_cells_width, // total
266 const int visible_cell_height = Tk_Height(theTkWindow) -
267 getMetricAreaPixHeight();
268 resizeScrollbar(interp, ".vertScrollbar",
269 all_cells_height, // total
270 visible_cell_height);
273 bool tableVisi::adjustHorizSBOffset(Tcl_Interp *interp) {
275 getScrollBarValues(interp, ".horizScrollbar", first, last);
276 return adjustHorizSBOffset(interp, first);
279 bool tableVisi::adjustVertSBOffset(Tcl_Interp *interp) {
281 getScrollBarValues(interp, ".vertScrollbar", first, last);
282 return adjustVertSBOffset(interp, first);
285 void tableVisi::resize(Tcl_Interp *interp) {
286 // does not redraw. Does things like resize the offscreen pixmap
288 if (offscreenPixmap) {
289 XFreePixmap(Tk_Display(theTkWindow), offscreenPixmap);
291 offscreenPixmap = XCreatePixmap(Tk_Display(theTkWindow),
292 Tk_WindowId(theTkWindow),
293 Tk_Width(theTkWindow), Tk_Height(theTkWindow),
294 Tk_Depth(theTkWindow));
298 resizeScrollbars(interp);
299 adjustHorizSBOffset(interp);
300 adjustVertSBOffset(interp);
303 void tableVisi::draw(bool xsynch) const {
304 if (!offscreenPixmap)
305 return; // we haven't done a tryFirst() yet
307 bool doubleBuffer = !xsynch;
309 Drawable theDrawable = doubleBuffer ? offscreenPixmap : Tk_WindowId(theTkWindow);
311 // XClearArea() works only on windows; the following works on pixmaps, too:
312 XFillRectangle(Tk_Display(theTkWindow), theDrawable,
315 Tk_Width(theTkWindow), Tk_Height(theTkWindow));
317 drawFocusNames(theDrawable);
318 // leftmost part of screen; unaffected by offset_x
319 drawMetricNames(theDrawable);
320 // topmost part of screen; unaffected byy offset_y
321 drawCells(theDrawable);
324 XCopyArea(Tk_Display(theTkWindow),
325 offscreenPixmap, // src drawable
326 Tk_WindowId(theTkWindow), // dest drawable
327 backgroundGC, // only a dummy GC is needed here (well, sort of)
328 0, 0, // src x, y offsets
329 Tk_Width(theTkWindow), Tk_Height(theTkWindow),
330 0, 0 // dest x, y offsets
335 * private metric helper functions
338 void tableVisi::drawMetricNames(Drawable theDrawable) const {
339 int curr_x = offset_x + getFocusAreaPixWidth();
341 const int minVisibleX = getFocusAreaPixWidth();
342 const int maxVisibleX = Tk_Width(theTkWindow) - 1;
344 const int metric_name_baseline = getMetricNameBaseline();
345 const int metric_units_baseline = getMetricUnitsBaseline();
347 // we need to clip 2 GC's: metricNameGC and metricUnitsGC.
348 // we don't need to clip lineGC, since our manual clipping is effective
350 clipRect.x = getFocusAreaPixWidth();
352 clipRect.width = Tk_Width(theTkWindow) - clipRect.x + 1;
353 clipRect.height = Tk_Height(theTkWindow);
355 XSetClipRectangles(Tk_Display(theTkWindow), metricNameGC,
356 0, 0, &clipRect, 1, YXBanded);
357 XSetClipRectangles(Tk_Display(theTkWindow), metricUnitsGC,
358 0, 0, &clipRect, 1, YXBanded);
360 for (unsigned metriclcv=0; metriclcv < indirectMetrics.size(); metriclcv++) {
361 if (curr_x > maxVisibleX)
362 break; // everthing else will be too far right
364 const tvMetric &theMetric = metrics[indirectMetrics[metriclcv]];
365 const int next_x = curr_x + theMetric.getColPixWidth();
367 if (next_x - 1 < minVisibleX) {
372 if (curr_x >= minVisibleX) // clipping
373 drawMetricVertLine(theDrawable, curr_x);
375 int curr_middle_x = (curr_x + next_x - 1) / 2;
377 // draw the metric name:
378 int metric_name_left = curr_middle_x - theMetric.getNamePixWidth() / 2;
379 const string &metricNameStr = theMetric.getName();
380 XDrawString(Tk_Display(theTkWindow), theDrawable,
382 metric_name_left, metric_name_baseline,
383 metricNameStr.string_of(), metricNameStr.length());
385 // draw the metric units:
386 int metric_units_left = curr_middle_x - theMetric.getUnitsPixWidth() / 2;
387 const string &metricUnitsNameStr = theMetric.getUnitsName();
388 XDrawString(Tk_Display(theTkWindow), theDrawable,
390 metric_units_left, metric_units_baseline,
391 metricUnitsNameStr.string_of(), metricUnitsNameStr.length());
396 if (curr_x >= minVisibleX) // clipping
397 drawMetricVertLine(theDrawable, curr_x);
399 XSetClipMask(Tk_Display(theTkWindow), metricNameGC, None);
400 XSetClipMask(Tk_Display(theTkWindow), metricUnitsGC, None);
403 void tableVisi::drawMetricVertLine(Drawable theDrawable, int x) const {
404 int line_height = getMetricAreaPixHeight() + get_total_cell_y_pix();
405 ipmin(line_height, Tk_Height(theTkWindow));
407 XDrawLine(Tk_Display(theTkWindow), theDrawable,
409 x, 0, x, 0+line_height-1);
412 unsigned tableVisi::getMetricAreaPixHeight() const {
413 return 3 + metricNameFont->ascent + metricNameFont->descent + 3 +
414 metricUnitsFont->ascent + metricUnitsFont->descent + 3;
417 unsigned tableVisi::getMetricNameBaseline() const {
418 return 3 + metricNameFont->ascent - 1;
421 unsigned tableVisi::getMetricUnitsBaseline() const {
422 return 3 + metricNameFont->ascent + metricNameFont->descent + 3 +
423 metricUnitsFont->ascent - 1;
427 * private focus helper functions
430 void tableVisi::drawFocusNames(Drawable theDrawable) const {
431 int curr_y = offset_y + getMetricAreaPixHeight();
433 const int minVisibleY = getMetricAreaPixHeight();
434 const int maxVisibleY = Tk_Height(theTkWindow) - 1;
438 clipRect.y = getMetricAreaPixHeight();
439 clipRect.width = Tk_Width(theTkWindow);
440 clipRect.height = Tk_Height(theTkWindow) - clipRect.y + 1;
442 XSetClipRectangles(Tk_Display(theTkWindow), focusNameGC,
443 0, 0, &clipRect, 1, YXBanded);
445 for (unsigned focuslcv = 0; focuslcv < indirectFoci.size(); focuslcv++) {
446 if (curr_y > maxVisibleY)
449 const tvFocus &theFocus = foci[indirectFoci[focuslcv]];
450 const int next_y = curr_y + getFocusLinePixHeight();
451 if (next_y - 1 < minVisibleY) {
456 if (curr_y >= minVisibleY)
457 drawFocusHorizLine(theDrawable, curr_y);
459 int curr_y_baseline = curr_y + getVertPixFocusTop2Baseline();
461 const string &theString = focusLongNameMode ? theFocus.getLongName() :
462 theFocus.getShortName();
464 XDrawString(Tk_Display(theTkWindow), theDrawable,
466 getHorizPixBeforeFocusName(),
468 theString.string_of(), theString.length());
473 XSetClipMask(Tk_Display(theTkWindow), focusNameGC, None);
475 if (curr_y >= minVisibleY)
476 drawFocusHorizLine(theDrawable, curr_y);
479 void tableVisi::drawFocusHorizLine(Drawable theDrawable, int y) const {
480 int line_width = getFocusAreaPixWidth() + get_total_cell_x_pix();
481 ipmin(line_width, get_visible_x_pix());
483 XDrawLine(Tk_Display(theTkWindow), theDrawable,
485 0, y, 0+line_width-1, y);
488 unsigned tableVisi::getFocusLinePixHeight() const {
489 return 2 + focusNameFont->ascent + focusNameFont->descent + 2;
492 unsigned tableVisi::getVertPixFocusTop2Baseline() const {
493 return 2 + focusNameFont->ascent;
496 unsigned tableVisi::getFocusAreaPixWidth() const {
497 return getHorizPixBeforeFocusName() + maxFocusNamePixWidth +
498 getHorizPixBeforeFocusName();
502 * private cell helper functions
505 void tableVisi::drawCells(Drawable theDrawable) const {
506 int curr_x = offset_x + getFocusAreaPixWidth();
508 const int minVisibleX = 0;
509 const int maxVisibleX = Tk_Width(theTkWindow)-1;
511 // we need to clip the GCs used for drawing cells s.t. neither the
512 // metrics nor foci are overwritten.
514 clipRect.x = getFocusAreaPixWidth();
515 clipRect.y = getMetricAreaPixHeight();
516 clipRect.width = Tk_Width(theTkWindow) - clipRect.x + 1;
517 clipRect.height = Tk_Height(theTkWindow) - clipRect.y + 1;
519 XSetClipRectangles(Tk_Display(theTkWindow), cellGC,
520 0, 0, &clipRect, 1, YXBanded);
522 for (unsigned metriclcv = 0; metriclcv < indirectMetrics.size(); metriclcv++) {
523 if (curr_x > maxVisibleX)
526 const tvMetric &theMetric = metrics[indirectMetrics[metriclcv]];
527 const int next_x = curr_x + theMetric.getColPixWidth();
529 if (next_x - 1 < minVisibleX) {
534 const vector<tvCell> &thisMetricCells = cells[indirectMetrics[metriclcv]];
535 drawCells1Col(theDrawable,
536 (curr_x + next_x - 1) / 2, // middle x
537 offset_y + getMetricAreaPixHeight(), // start y
543 XSetClipMask(Tk_Display(theTkWindow), cellGC, None);
546 void tableVisi::drawCells1Col(Drawable theDrawable, int middle_x, int top_y,
547 const vector<tvCell> &thisMetricCells) const {
548 // uses getVertPixFocusTop2Baseline() and getFocusLinePixHeight()
552 int maxVisibleY = Tk_Height(theTkWindow)-1;
554 for (unsigned focuslcv=0; focuslcv < indirectFoci.size(); focuslcv++) {
555 if (curr_y > maxVisibleY)
558 const int next_y = curr_y + getFocusLinePixHeight();
559 if (next_y - 1 < minVisibleY) {
564 const tvCell &theCell = thisMetricCells[indirectFoci[focuslcv]];
565 if (!theCell.isValid()) {
570 // making a new "string" would be too expensive (calls new):
572 double2string(buffer, theCell.getData());
574 int buffer_len = strlen(buffer);
575 int string_pix_width = XTextWidth(cellFont, buffer, buffer_len);
577 XDrawString(Tk_Display(theTkWindow), theDrawable,
579 middle_x - string_pix_width / 2,
580 curr_y + getVertPixCellTop2Baseline(),
587 unsigned tableVisi::getVertPixCellTop2Baseline() const {
588 return 2 + cellFont->ascent;
591 /* *************************************************************** */
593 void tableVisi::clearMetrics(Tcl_Interp *interp) {
595 indirectMetrics.resize(0);
603 void tableVisi::clearFoci(Tcl_Interp *interp) {
605 indirectFoci.resize(0);
607 unsigned numMetrics = getNumMetrics();
608 for (unsigned i=0; i < numMetrics; i++) {
609 vector<tvCell> &theVec = cells[i];
613 all_cells_height = 0;
614 maxFocusNamePixWidth = 0;
620 void tableVisi::addMetric(const string &metricName, const string &metricUnits) {
621 tvMetric newTvMetric(metricName, metricUnits, metricNameFont, metricUnitsFont, cellFont,
623 metrics += newTvMetric;
624 indirectMetrics += (metrics.size()-1);
625 cells += vector<tvCell>();
627 all_cells_width += newTvMetric.getColPixWidth();
629 assert(metrics.size() == indirectMetrics.size());
630 assert(cells.size() == metrics.size());
633 void tableVisi::changeUnitsLabel (unsigned which, const string &new_name) {
634 if (which < indirectMetrics.size()) {
635 tvMetric &theMetric = metrics[indirectMetrics[which]];
636 theMetric.changeUnitsName(new_name);
640 void tableVisi::addFocus(const string &focusName) {
641 tvFocus newTvFocus(focusName, focusNameFont);
643 indirectFoci += (foci.size()-1);
645 unsigned numMetrics = metrics.size();
646 for (unsigned metriclcv=0; metriclcv < numMetrics; metriclcv++) {
647 vector<tvCell> &metricCells = cells[metriclcv];
648 metricCells += tvCell();
651 if (focusLongNameMode)
652 ipmax(maxFocusNamePixWidth, newTvFocus.getLongNamePixWidth());
654 ipmax(maxFocusNamePixWidth, newTvFocus.getShortNamePixWidth());
656 all_cells_height += getFocusLinePixHeight();
658 assert(foci.size() == indirectFoci.size());
661 int tableVisi::partitionMetrics(int left, int right) {
662 const tvMetric &pivot = metrics[indirectMetrics[left]];
668 while (metrics[indirectMetrics[--r]] > pivot)
671 while (metrics[indirectMetrics[++l]] < pivot)
675 unsigned temp = indirectMetrics[l];
676 indirectMetrics[l] = indirectMetrics[r];
677 indirectMetrics[r] = temp;
684 void tableVisi::sortMetrics(int left, int right) {
686 int middle = partitionMetrics(left, right);
687 sortMetrics(left, middle);
688 sortMetrics(middle+1, right);
692 void tableVisi::sortMetrics() {
693 sortMetrics(0, metrics.size()-1);
696 void tableVisi::unsortMetrics() {
697 for (unsigned i=0; i < indirectMetrics.size(); i++)
698 indirectMetrics[i] = i;
701 int tableVisi::partitionFoci(int left, int right) {
702 const tvFocus &pivot = foci[indirectFoci[left]];
710 } while (foci[indirectFoci[r]].greater_than(pivot, focusLongNameMode));
714 } while (foci[indirectFoci[l]].less_than(pivot, focusLongNameMode));
717 unsigned temp = indirectFoci[l];
718 indirectFoci[l] = indirectFoci[r];
719 indirectFoci[r] = temp;
726 void tableVisi::sortFoci(int left, int right) {
728 int middle = partitionFoci(left, right);
729 sortFoci(left, middle);
730 sortFoci(middle+1, right);
734 void tableVisi::sortFoci() {
735 sortFoci(0, foci.size()-1);
738 void tableVisi::unsortFoci() {
739 for (unsigned i=0; i < indirectFoci.size(); i++)
743 bool tableVisi::setFocusNameMode(Tcl_Interp *interp, bool longNameMode) {
744 // returns true iff any changes
745 if (focusLongNameMode == longNameMode)
748 focusLongNameMode = longNameMode;
750 // recalculate maxFocusNamePixWidth:
751 maxFocusNamePixWidth=0;
752 for (unsigned focuslcv=0; focuslcv < foci.size(); focuslcv++) {
753 const tvFocus &theFocus = foci[focuslcv];
755 if (focusLongNameMode)
756 ipmax(maxFocusNamePixWidth, theFocus.getLongNamePixWidth());
758 ipmax(maxFocusNamePixWidth, theFocus.getShortNamePixWidth());
766 bool tableVisi::setSigFigs(unsigned newNumSigFigs) {
767 if (newNumSigFigs == numSigFigs)
770 numSigFigs = newNumSigFigs;
772 // we'll be recalcing this from scratch, since col widths can change
774 for (unsigned met=0; met < metrics.size(); met++) {
775 // sorted order is not important here...
776 tvMetric &theMetric = metrics[met];
778 theMetric.changeNumSigFigs(newNumSigFigs, cellFont);
780 all_cells_width += theMetric.getColPixWidth();
786 void tableVisi::invalidateCell(unsigned theMetric, unsigned theFocus) {
787 cells[theMetric][theFocus].invalidate();
790 void tableVisi::setCellValidData(unsigned theMetric, unsigned theFocus, double data) {
791 cells[theMetric][theFocus].setValidData(data);
794 /* *************************************************************** */
796 bool tableVisi::adjustHorizSBOffset(Tcl_Interp *interp, float newFirst) {
797 // doesn't redraw; returns true iff any changes.
798 newFirst = moveScrollBar(interp, ".horizScrollbar", newFirst);
800 int total_cell_width = get_total_cell_x_pix();
801 int old_offset_x = offset_x;
802 offset_x = -(int)(newFirst * total_cell_width); // yes, always <= 0
804 return (offset_x != old_offset_x);
807 bool tableVisi::adjustVertSBOffset(Tcl_Interp *interp, float newFirst) {
808 // doesn't redraw; returns true iff any changes.
809 newFirst = moveScrollBar(interp, ".vertScrollbar", newFirst);
811 int total_cell_height = get_total_cell_y_pix();
812 int old_offset_y = offset_y;
813 offset_y = -(int)(newFirst * total_cell_height); // yes, always <= 0
815 return (offset_y != old_offset_y);