]> git.mxchange.org Git - flightgear.git/blob - src/GUI/WaypointList.cxx
Merge branch 'next' of gitorious.org:fg/flightgear into next
[flightgear.git] / src / GUI / WaypointList.cxx
1
2
3 #ifdef HAVE_CONFIG_H
4 #  include "config.h"
5 #endif
6
7 #include "WaypointList.hxx"
8
9 #include <algorithm>
10 #include <boost/tuple/tuple.hpp>
11
12 #include <plib/puAux.h>
13
14 #include <simgear/structure/callback.hxx>
15 #include <simgear/sg_inlines.h>
16
17 #include <Main/globals.hxx>
18 #include <Main/fg_props.hxx>
19
20 #include <Navaids/positioned.hxx>
21 #include <Autopilot/route_mgr.hxx>
22
23 // select if the widget grabs keys necessary to fly aircraft from the keyboard,
24 // or not. See http://code.google.com/p/flightgear-bugs/issues/detail?id=338
25 // for discussion about why / what is going on.
26 #define AVOID_FLIGHT_KEYS 1
27
28 using namespace flightgear;
29
30 enum {
31   SCROLL_NO = 0,
32   SCROLL_UP,
33   SCROLL_DOWN
34 };
35   
36 static const double BLINK_TIME = 0.3;
37 static const int DRAG_START_DISTANCE_PX = 5;
38   
39 class FlightPlanWaypointModel : 
40   public WaypointList::Model, 
41   public SGPropertyChangeListener
42 {
43 public:
44   FlightPlanWaypointModel(flightgear::FlightPlan* fp) :
45     _fp(fp)
46   {    
47     SGPropertyNode* routeEdited = fgGetNode("/autopilot/route-manager/signals/edited", true);
48     SGPropertyNode* flightplanChanged = fgGetNode("/autopilot/route-manager/signals/flightplan-changed", true);
49     routeEdited->addChangeListener(this);
50     flightplanChanged->addChangeListener(this);
51   }
52   
53   ~FlightPlanWaypointModel()
54   {
55     SGPropertyNode* routeEdited = fgGetNode("/autopilot/route-manager/signals/edited", true);
56     SGPropertyNode* flightplanChanged = fgGetNode("/autopilot/route-manager/signals/flightplan-changed", true);
57     routeEdited->removeChangeListener(this);
58     flightplanChanged->removeChangeListener(this);
59   }
60   
61 // implement WaypointList::Model
62   virtual unsigned int numWaypoints() const
63   {
64     return _fp->numLegs();
65   }
66   
67   virtual int currentWaypoint() const
68   {
69     return _fp->currentIndex();
70   }
71   
72   virtual flightgear::Waypt* waypointAt(unsigned int index) const
73   {
74     if (index >= numWaypoints()) {
75       return NULL;
76     }
77     
78     return _fp->legAtIndex(index)->waypoint();
79   }
80
81   virtual void deleteAt(unsigned int index)
82   {
83     _fp->deleteIndex(index);
84   }
85   
86   virtual void moveWaypointToIndex(unsigned int srcIndex, unsigned int destIndex)
87   {
88     SG_LOG(SG_GENERAL, SG_INFO, "moveWaypoint: from " << srcIndex << " to " << destIndex);
89     if (destIndex > srcIndex) {
90       --destIndex;
91     }
92     
93     unsigned int currentWpIndex = currentWaypoint();
94     WayptRef w(waypointAt(currentWpIndex));
95     _fp->deleteIndex(currentWpIndex);
96     
97     SG_LOG(SG_GENERAL, SG_INFO, "wpt:" << w->ident());
98     _fp->insertWayptAtIndex(w, destIndex);
99
100     if (srcIndex == currentWpIndex) {
101         // current waypoint was moved
102         _fp->setCurrentIndex(destIndex);
103     }
104   }
105   
106   virtual void setUpdateCallback(SGCallback* cb)
107   {
108     _cb = cb;
109   }
110     
111 // implement SGPropertyChangeListener
112   void valueChanged(SGPropertyNode *prop)
113   {
114     if (prop->getNameString() == "edited") {
115       if (_cb) {
116         (*_cb)();
117       }
118     }
119     
120     if (prop->getNameString() == "flightplan-changed") {
121       _fp = 
122         static_cast<FGRouteMgr*>(globals->get_subsystem("route-manager"))->flightPlan();
123     }
124   }
125 private:
126   flightgear::FlightPlan* _fp;
127   SGCallback* _cb;
128 };
129
130 //////////////////////////////////////////////////////////////////////////////
131
132 static void drawClippedString(puFont& font, const char* s, int x, int y, int maxWidth)
133 {
134   int fullWidth = font.getStringWidth(s);
135   if (fullWidth <= maxWidth) { // common case, easy and efficent
136     font.drawString(s, x, y);
137     return;
138   }
139   
140   std::string buf(s);
141   int len = buf.size();
142   do {
143     buf.resize(--len);
144     fullWidth = font.getStringWidth(buf.c_str());
145   } while (fullWidth > maxWidth);
146   
147   font.drawString(buf.c_str(), x, y);
148 }
149
150 //////////////////////////////////////////////////////////////////////////////
151
152 WaypointList::WaypointList(int x, int y, int width, int height) :
153   puFrame(x, y, width, height),
154   GUI_ID(FGCLASS_WAYPOINTLIST),
155   _scrollPx(0),
156   _dragging(false),
157   _dragScroll(SCROLL_NO),
158   _showLatLon(false),
159   _model(NULL),
160   _updateCallback(NULL),
161   _scrollCallback(NULL),
162   _blink(false)
163 {
164   // pretend to be a list, so fgPopup doesn't mess with our mouse events
165   type |= PUCLASS_LIST;  
166   flightgear::FlightPlan* fp = 
167     static_cast<FGRouteMgr*>(globals->get_subsystem("route-manager"))->flightPlan();
168   setModel(new FlightPlanWaypointModel(fp));
169   setSize(width, height);
170   setValue(-1);
171   
172   _blinkTimer.stamp();
173 }
174
175 WaypointList::~WaypointList()
176 {
177   delete _model;
178   delete _updateCallback;
179   delete _scrollCallback;
180 }
181
182 void WaypointList::setUpdateCallback(SGCallback* cb)
183 {
184   _updateCallback = cb;
185 }
186
187 void WaypointList::setScrollCallback(SGCallback* cb)
188 {
189   _scrollCallback = cb;
190 }
191
192 void WaypointList::setSize(int width, int height)
193 {
194   double scrollP = getVScrollPercent();
195   _heightPx = height;
196   puFrame::setSize(width, height);
197   
198   if (wantsVScroll()) {
199     setVScrollPercent(scrollP);
200   } else {
201     _scrollPx = 0;
202   }
203 }
204
205 int WaypointList::checkHit ( int button, int updown, int x, int y )
206 {
207   if ( isHit( x, y ) || ( puActiveWidget () == this ) )
208   {
209     doHit ( button, updown, x, y ) ;
210     return TRUE ;
211   }
212
213   return FALSE ;
214 }
215
216
217 void WaypointList::doHit( int button, int updown, int x, int y )
218 {
219   puFrame::doHit(button, updown, x, y);  
220   if (updown == PU_DRAG) {
221     handleDrag(x, y);
222     return;
223   }
224   
225   if (button != active_mouse_button) {
226     return;
227   }
228       
229   if (updown == PU_UP) {
230     puDeactivateWidget();
231     if (_dragging) {
232       doDrop(x, y);
233       return;
234     }
235   } else if (updown == PU_DOWN) {
236     puSetActiveWidget(this, x, y);
237     _mouseDownX = x;
238     _mouseDownY = y;
239     return;
240   }
241   
242 // update selection
243   int row = rowForY(y - abox.min[1]);
244   if (row >= (int) _model->numWaypoints()) {
245     row = -1; // 'no selection'
246   }
247
248   if (row == getSelected()) {
249     _showLatLon = !_showLatLon;
250     puPostRefresh();
251     return;
252   }
253   
254   setSelected(row);
255 }
256
257 void WaypointList::handleDrag(int x, int y)
258 {
259   if (!_dragging) {
260     // don't start drags immediately, require a certain mouse movement first
261     int manhattanLength = abs(x - _mouseDownX) + abs(y - _mouseDownY);
262     if (manhattanLength < DRAG_START_DISTANCE_PX) {
263       return;
264     }
265     
266     _dragSourceRow = rowForY(y - abox.min[1]);
267     Waypt* wp = _model->waypointAt(_dragSourceRow);
268     if (!wp || wp->flag(WPT_GENERATED)) {
269       return; // don't allow generated points to be dragged
270     }
271     
272     _dragging = true;
273     _dragScroll = SCROLL_NO;
274   }
275   
276   if (y < abox.min[1]) {
277     if (_dragScroll != SCROLL_DOWN) {
278       _dragScroll = SCROLL_DOWN;
279       _dragScrollTime.stamp();
280     }
281   } else if (y > abox.max[1]) {
282     if (_dragScroll != SCROLL_UP) {
283       _dragScroll = SCROLL_UP;
284       _dragScrollTime.stamp();
285     }
286   } else {
287     _dragScroll = SCROLL_NO;
288     _dragTargetRow = rowForY(y - abox.min[1] - (rowHeightPx() / 2));
289   }
290 }
291
292 void WaypointList::doDrop(int x, int y)
293 {
294   _dragging = false;
295   puDeactivateWidget();
296   
297   SG_LOG(SG_GENERAL, SG_INFO, "doDrop");
298   
299   if ((y < abox.min[1]) || (y >= abox.max[1])) {
300     SG_LOG(SG_GENERAL, SG_INFO, "y out of bounds:" << y);
301     return;
302   }
303   
304   if (_dragSourceRow == _dragTargetRow) {
305     SG_LOG(SG_GENERAL, SG_INFO, "source and target row match");
306     return;
307   }
308   
309   _model->moveWaypointToIndex(_dragSourceRow, _dragTargetRow);
310   
311   // keep row indexes linged up when moving an item down the list
312   if (_dragSourceRow < _dragTargetRow) {
313     --_dragTargetRow;
314   }
315   
316   setSelected(_dragTargetRow);
317 }
318
319 void WaypointList::invokeDownCallback(void)
320 {
321   _dragging = false;
322   _dragScroll = SCROLL_NO;
323   SG_LOG(SG_GENERAL, SG_INFO, "cancel drag");
324 }
325
326 int WaypointList::rowForY(int y) const
327 {
328   if (!_model) {
329     return -1;
330   }
331   
332   // flip y to increase down, not up (as rows do)
333   int flipY = _heightPx - y;
334   int row = (flipY + _scrollPx) / rowHeightPx();
335   return row;
336 }
337
338 void WaypointList::draw( int dx, int dy )
339 {
340   puFrame::draw(dx, dy);
341
342   if (!_model) {
343     return;
344   }
345
346   if (_dragScroll != SCROLL_NO) {
347     doDragScroll();
348   }
349   
350   double dt = (SGTimeStamp::now() - _blinkTimer).toSecs();
351   if (dt > BLINK_TIME) {
352     _blinkTimer.stamp();
353     _blink = !_blink;
354   }
355   
356   glEnable(GL_SCISSOR_TEST);
357   GLint sx = (int) abox.min[0],
358     sy = abox.min[1];
359   GLsizei w = (GLsizei) abox.max[0] - abox.min[0],
360     h = _heightPx;
361     
362   sx += border_thickness;
363   sy += border_thickness;
364   w -= 2 * border_thickness;
365   h -= 2 * border_thickness;
366     
367   glScissor(sx + dx, sy + dy, w, h);
368   int row = firstVisibleRow(), 
369     final = lastVisibleRow(),
370     rowHeight = rowHeightPx(),
371     y = rowHeight;
372   
373   y -= (_scrollPx % rowHeight); // partially draw the first row
374   
375   _arrowWidth = legendFont.getStringWidth(">");
376   for ( ; row <= final; ++row, y += rowHeight) {
377     drawRow(dx, dy, row, y);
378   } // of row drawing iteration
379   
380   glDisable(GL_SCISSOR_TEST);
381     
382   if (_dragging) {
383     // draw the insert marker after the rows
384     int insertY = (_dragTargetRow * rowHeight) - _scrollPx;
385     SG_CLAMP_RANGE(insertY, 0, std::min(_heightPx, totalHeightPx()));
386     
387     glColor4f(1.0f, 0.5f, 0.0f, 0.8f);
388     glLineWidth(3.0f);
389     glBegin(GL_LINES);
390       glVertex2f(dx + abox.min[0], dy + abox.max[1] - insertY);
391       glVertex2f(dx + abox.max[0], dy + abox.max[1] - insertY);
392     glEnd();
393   }
394 }
395
396 void WaypointList::drawRow(int dx, int dy, int rowIndex, int y)
397 {
398   flightgear::Waypt* wp(_model->waypointAt(rowIndex));
399     
400   bool isSelected = (rowIndex == getSelected());
401   bool isCurrent = (rowIndex == _model->currentWaypoint());
402   bool isDragSource = (_dragging && (rowIndex == _dragSourceRow));
403   
404   puBox bkgBox = abox;
405   bkgBox.min[1] = abox.max[1] - y;
406   bkgBox.max[1] = bkgBox.min[1] + rowHeightPx();
407   
408   puColour col;
409   puFont* f = &legendFont;
410   bool drawBox = false;
411   
412   if (wp->flag(WPT_MISS)) {
413     drawBox = true;
414     puSetColor(col, 1.0, 0.0, 0.0, 0.3);  // red
415   } else if (wp->flag(WPT_ARRIVAL) || wp->flag(WPT_DEPARTURE)) {
416     drawBox = true;
417     puSetColor(col, 0.0, 0.0, 0.0, 0.3);
418   } else if (wp->flag(WPT_APPROACH)) {
419     drawBox = true;
420     puSetColor(col, 0.0, 0.0, 0.1, 0.3);
421   }
422   
423   if (isDragSource) {
424     // draw later, on *top* of text string
425   } else if (isSelected) { // -PLAIN means selected, apparently
426     bkgBox.draw(dx, dy, -PUSTYLE_PLAIN, colour, false, 0);
427   } else if (drawBox) {
428     bkgBox.draw(dx, dy, PUSTYLE_PLAIN, &col, false, 0);
429   }
430   
431   if (isCurrent) {
432     glColor4f (1.0, 0.5, 0.0, 1.0) ;
433   } else {
434     glColor4fv ( colour [ PUCOL_LEGEND ] ) ;
435   }
436   
437   int xx = dx + abox.min[0] + PUSTR_LGAP;
438   int yy = dy + abox.max[1] - y ;
439   yy += 4; // center text in row height
440   
441   if (isCurrent) {
442     f->drawString(">", xx, yy);
443   }
444   
445   int x = xx;
446   x += _arrowWidth + PUSTR_LGAP;
447   
448   // row textual data
449
450   char buffer[128];
451   int count = ::snprintf(buffer, 128, "%03d   %-5s", rowIndex, wp->ident().c_str());
452  
453   FGPositioned* src = wp->source(); 
454   if (src && !src->name().empty() && (src->name() != wp->ident())) { 
455     // append name if present, and different to id
456     ::snprintf(buffer + count, 128 - count, " (%s)", src->name().c_str());
457   }
458
459   drawClippedString(legendFont, buffer, x, yy, 300);
460   x += 300 + PUSTR_LGAP;
461   
462   if (_showLatLon) {
463     SGGeod p(wp->position());
464     char ns = (p.getLatitudeDeg() > 0.0) ? 'N' : 'S';
465     char ew = (p.getLongitudeDeg() > 0.0) ? 'E' : 'W';
466     
467     ::snprintf(buffer, 128 - count, "%4.2f%c %4.2f%c",
468       fabs(p.getLongitudeDeg()), ew, fabs(p.getLatitudeDeg()), ns);
469   } else if (rowIndex > 0) {
470     double courseDeg;
471     double distanceM;
472     Waypt* prev = _model->waypointAt(rowIndex - 1);
473     boost::tie(courseDeg, distanceM) = wp->courseAndDistanceFrom(prev->position());
474   
475     ::snprintf(buffer, 128 - count, "%03.0f %5.1fnm",
476       courseDeg, distanceM * SG_METER_TO_NM);
477   }
478
479   f->drawString(buffer, x, yy);
480   x += 100 + PUSTR_LGAP;
481   
482   if (wp->altitudeRestriction() != RESTRICT_NONE) {
483     int altHundredFt = (wp->altitudeFt() + 50) / 100; // round to nearest 100ft
484     if (altHundredFt < 100) {
485       count = ::snprintf(buffer, 128, "%d'", altHundredFt * 100);
486     } else { // display as a flight-level
487       count = ::snprintf(buffer, 128, "FL%d", altHundredFt);
488     }
489     
490     f->drawString(buffer, x, yy);
491   } // of valid wp altitude
492   x += 60 + PUSTR_LGAP;
493   
494   if (wp->speedRestriction() == SPEED_RESTRICT_MACH) {
495     count = ::snprintf(buffer, 126, "%03.2fM", wp->speedMach());
496     f->drawString(buffer, x, yy);
497   } else if (wp->speedRestriction() != RESTRICT_NONE) {
498     count = ::snprintf(buffer, 126, "%dKts", (int) wp->speedKts());
499     f->drawString(buffer, x, yy);
500   }
501   
502   if (isDragSource) {
503     puSetColor(col, 1.0, 0.5, 0.0, 0.5);
504     bkgBox.draw(dx, dy, PUSTYLE_PLAIN, &col, false, 0);
505   }
506 }
507
508 const double SCROLL_PX_SEC = 200.0;
509
510 void WaypointList::doDragScroll()
511 {
512   double dt = (SGTimeStamp::now() - _dragScrollTime).toSecs();
513   _dragScrollTime.stamp();
514   int deltaPx = (int)(dt * SCROLL_PX_SEC);
515   
516   if (_dragScroll == SCROLL_UP) {
517     _scrollPx = _scrollPx - deltaPx;
518     SG_CLAMP_RANGE(_scrollPx, 0, scrollRangePx());
519     _dragTargetRow = firstVisibleRow();
520   } else {
521     _scrollPx = _scrollPx + deltaPx;
522     SG_CLAMP_RANGE(_scrollPx, 0, scrollRangePx());
523     _dragTargetRow = lastFullyVisibleRow() + 1;
524   }
525   
526   if (_scrollCallback) {
527     (*_scrollCallback)();
528   }
529 }
530
531 int WaypointList::getSelected()
532 {
533   return getIntegerValue();
534 }
535
536 void WaypointList::setSelected(int rowIndex)
537 {
538   if (rowIndex == getSelected()) {
539     return;
540   }
541   
542   setValue(rowIndex);
543   invokeCallback();
544   if (rowIndex == -1) {
545     return;
546   }
547
548   ensureRowVisible(rowIndex);
549 }
550
551 void WaypointList::ensureRowVisible(int rowIndex)
552 {
553   if ((rowIndex >= firstFullyVisibleRow()) && (rowIndex <= lastFullyVisibleRow())) {
554     return; // already visible, fine
555   }
556   
557   // ideal position would place the desired row in the middle of the
558   // visible section - hence subtract half the visible height.
559   int targetScrollPx = (rowIndex * rowHeightPx()) - (_heightPx / 2);
560   
561   // clamp the scroll value to something valid
562   SG_CLAMP_RANGE(targetScrollPx, 0, scrollRangePx());
563   _scrollPx = targetScrollPx;
564   
565   puPostRefresh();
566   if (_scrollCallback) { // keep scroll observers in sync
567     (*_scrollCallback)();
568   }
569 }
570
571 unsigned int WaypointList::numWaypoints() const
572 {
573   if (!_model) {
574     return 0;
575   }
576   
577   return _model->numWaypoints();
578 }
579
580 bool WaypointList::wantsVScroll() const
581 {
582   return totalHeightPx() > _heightPx;
583 }
584
585 float WaypointList::getVScrollPercent() const
586 {
587   float scrollRange = scrollRangePx();
588   if (scrollRange < 1.0f) {
589     return 0.0;
590   }
591   
592   return _scrollPx / scrollRange;
593 }
594
595 float WaypointList::getVScrollThumbPercent() const
596 {
597   return _heightPx / (float) totalHeightPx();
598 }
599
600 void WaypointList::setVScrollPercent(float perc)
601 {
602   float scrollRange = scrollRangePx();
603   _scrollPx = (int)(scrollRange * perc);
604 }
605
606 int WaypointList::firstVisibleRow() const
607 {
608   return _scrollPx / rowHeightPx();
609 }
610
611 int WaypointList::firstFullyVisibleRow() const
612 {
613   int rh = rowHeightPx();
614   return (_scrollPx + rh - 1) / rh;
615 }
616   
617 int WaypointList::numVisibleRows() const
618 {
619   int rh = rowHeightPx();
620   int topOffset = _scrollPx % rh; // pixels of first visible row
621   return (_heightPx - topOffset + rh - 1) / rh;
622
623 }
624
625 int WaypointList::numFullyVisibleRows() const
626 {
627   int rh = rowHeightPx();
628   int topOffset = _scrollPx % rh; // pixels of first visible row
629   return (_heightPx - topOffset) / rh;
630 }
631
632 int WaypointList::rowHeightPx() const
633 {
634   return legendFont.getStringHeight() + PUSTR_BGAP;
635 }
636
637 int WaypointList::scrollRangePx() const
638 {
639   return std::max(0, totalHeightPx() - _heightPx);
640 }
641
642 int WaypointList::totalHeightPx() const
643 {
644   if (!_model) {
645     return 0;
646   }
647   
648   return (int) _model->numWaypoints() * rowHeightPx();
649 }
650
651 int WaypointList::lastFullyVisibleRow() const
652 {
653   int row = firstFullyVisibleRow() + numFullyVisibleRows();
654   return std::min(row, (int) _model->numWaypoints() - 1);
655 }
656
657 int WaypointList::lastVisibleRow() const
658 {
659   int row = firstVisibleRow() + numVisibleRows();
660   return std::min(row, (int) _model->numWaypoints() - 1);
661 }
662
663 void WaypointList::setModel(Model* model)
664 {
665   if (_model) {
666     delete _model;
667   }
668   
669   _model = model;
670   _model->setUpdateCallback(make_callback(this, &WaypointList::modelUpdateCallback));
671   
672   puPostRefresh();
673 }
674
675 int WaypointList::checkKey (int key, int updown )
676 {
677   if ((updown == PU_UP) || !isVisible () || !isActive () || (window != puGetWindow())) {
678     return FALSE ;
679   }
680
681 #ifdef AVOID_FLIGHT_KEYS
682     return FALSE;
683 #endif
684   
685   switch (key)
686   {
687   case PU_KEY_HOME:
688     setSelected(0);
689     break;
690
691   case PU_KEY_END:
692     setSelected(_model->numWaypoints() - 1);
693     break ;
694
695   case PU_KEY_UP        :
696   case PU_KEY_PAGE_UP   :
697     if (getSelected() >= 0) {
698       setSelected(getSelected() - 1);
699     }
700     break ;
701
702   case PU_KEY_DOWN      :
703   case PU_KEY_PAGE_DOWN : {
704     int newSel = getSelected() + 1;
705     if (newSel >= (int) _model->numWaypoints()) {
706       setSelected(-1);
707     } else {
708       setSelected(newSel);
709     }
710     break ;
711   }
712   
713   case '-':
714     if (getSelected() >= 0) {
715       Waypt* wp = _model->waypointAt(getSelected());
716       if (wp->flag(WPT_GENERATED)) {
717         break;
718       }
719       
720       if (wp->altitudeRestriction() != RESTRICT_NONE) {
721         int curAlt = (static_cast<int>(wp->altitudeFt()) + 50) / 100;
722         if (curAlt <= 0) {
723           wp->setAltitude(0, RESTRICT_NONE);
724         } else {
725           wp->setAltitude((curAlt - 10) * 100, wp->altitudeRestriction());
726         }
727       }
728     }
729     break;
730     
731   case '=':
732     if (getSelected() >= 0) {
733       flightgear::Waypt* wp = _model->waypointAt(getSelected());
734       if (wp->flag(WPT_GENERATED)) {
735         break;
736       }
737         
738       if (wp->altitudeRestriction() == RESTRICT_NONE) {
739         wp->setAltitude(1000, RESTRICT_AT);
740       } else {
741         int curAlt = (static_cast<int>(wp->altitudeFt()) + 50) / 100;
742         wp->setAltitude((curAlt + 10) * 100, wp->altitudeRestriction());
743       }
744     }
745     break;
746   
747   case 0x7f: // delete
748     if (getSelected() >= 0) {
749       int index = getSelected();
750       Waypt* wp = _model->waypointAt(getSelected());
751       if (wp->flag(WPT_GENERATED)) {
752         break;
753       }
754       
755       _model->deleteAt(index);
756       setSelected(index - 1);
757     }
758     break;
759
760   default :
761     return FALSE;
762   }
763
764   return TRUE ;
765 }
766
767 void WaypointList::modelUpdateCallback()
768 {
769   // local stuff
770   
771   if (_updateCallback) {
772     (*_updateCallback)();
773   }
774 }
775
776 //////////////////////////////////////////////////////////////////////////////
777
778
779 static void handle_scrollbar(puObject* scrollbar)
780 {
781   ScrolledWaypointList* self = (ScrolledWaypointList*)scrollbar->getUserData();
782   self->setScrollPercent(scrollbar->getFloatValue());
783 }
784
785 static void waypointListCb(puObject* wpl)
786 {
787   ScrolledWaypointList* self = (ScrolledWaypointList*)wpl->getUserData();
788   self->setValue(wpl->getIntegerValue());
789   self->invokeCallback();
790 }
791
792 ScrolledWaypointList::ScrolledWaypointList(int x, int y, int width, int height) :
793   puGroup(x,y),
794   _scrollWidth(16)
795 {
796   // ensure our type is compound, so fgPopup::applySize doesn't descend into
797   // us, and try to cast our children's user-data to GUIInfo*.
798   type |= PUCLASS_LIST;
799   
800   init(width, height);
801 }
802
803 void ScrolledWaypointList::setValue(float v)
804 {
805   puGroup::setValue(v);
806   _list->setValue(v);
807 }
808
809 void ScrolledWaypointList::setValue(int v)
810 {
811   puGroup::setValue(v);
812   _list->setValue(v);
813 }
814
815 void ScrolledWaypointList::init(int w, int h)
816 {
817   _list = new WaypointList(0, 0, w, h);
818   _list->setUpdateCallback(make_callback(this, &ScrolledWaypointList::modelUpdated));
819   _hasVScroll = _list->wantsVScroll();
820   _list->setUserData(this);
821   _list->setCallback(waypointListCb);
822   
823   _list->setScrollCallback(make_callback(this, &ScrolledWaypointList::updateScroll));
824   
825   _scrollbar = new puaScrollBar(w - _scrollWidth, 0, h, 
826     1 /*arrow*/, 1 /* vertical */, _scrollWidth);
827   _scrollbar->setMinValue(0.0);
828   _scrollbar->setMaxValue(1.0);
829   _scrollbar->setUserData(this);
830   _scrollbar->setCallback(handle_scrollbar);
831   close(); // close the group
832   
833   setSize(w, h);
834 }
835
836 void ScrolledWaypointList::modelUpdated()
837 {  
838   int w, h;
839   getSize(&w, &h);
840   updateWantsScroll(w, h);
841 }
842
843 void ScrolledWaypointList::setScrollPercent(float v)
844 {
845   // slider's min is the bottom, so invert the value
846   _list->setVScrollPercent(1.0f - v); 
847 }
848
849 void ScrolledWaypointList::setSize(int w, int h)
850 {
851   updateWantsScroll(w, h);
852   puGroup::setSize(w, h);
853 }
854
855 void ScrolledWaypointList::updateWantsScroll(int w, int h)
856 {
857   _hasVScroll = _list->wantsVScroll();
858   
859   if (_hasVScroll) {
860     _scrollbar->reveal();
861     _scrollbar->setPosition(w - _scrollWidth, 0);
862     _scrollbar->setSize(_scrollWidth, h);
863     _list->setSize(w - _scrollWidth, h);
864     updateScroll();
865   } else {
866     _scrollbar->hide();
867     _list->setSize(w, h);
868   }
869 }
870
871 void ScrolledWaypointList::updateScroll()
872 {
873  // _scrollbar->setMaxValue(_list->numWaypoints());
874   _scrollbar->setValue(1.0f - _list->getVScrollPercent());
875   _scrollbar->setSliderFraction(_list->getVScrollThumbPercent());
876 }
877