7 #include "WaypointList.hxx"
10 #include <boost/tuple/tuple.hpp>
12 #include <plib/puAux.h>
14 #include <simgear/structure/callback.hxx>
15 #include <simgear/sg_inlines.h>
17 #include <Main/globals.hxx>
18 #include <Main/fg_props.hxx>
20 #include <Navaids/positioned.hxx>
21 #include <Autopilot/route_mgr.hxx>
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
28 using namespace flightgear;
36 static const double BLINK_TIME = 0.3;
37 static const int DRAG_START_DISTANCE_PX = 5;
39 class FlightPlanWaypointModel :
40 public WaypointList::Model,
41 public SGPropertyChangeListener
44 FlightPlanWaypointModel(flightgear::FlightPlan* fp) :
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);
53 ~FlightPlanWaypointModel()
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);
61 // implement WaypointList::Model
62 virtual unsigned int numWaypoints() const
64 return _fp->numLegs();
67 virtual int currentWaypoint() const
69 return _fp->currentIndex();
72 virtual flightgear::Waypt* waypointAt(unsigned int index) const
74 if (index >= numWaypoints()) {
78 return _fp->legAtIndex(index)->waypoint();
81 virtual void deleteAt(unsigned int index)
83 _fp->deleteIndex(index);
86 virtual void moveWaypointToIndex(unsigned int srcIndex, unsigned int destIndex)
88 SG_LOG(SG_GENERAL, SG_INFO, "moveWaypoint: from " << srcIndex << " to " << destIndex);
89 if (destIndex > srcIndex) {
93 int currentWpIndex = currentWaypoint();
95 WayptRef w = _fp->legAtIndex(srcIndex)->waypoint();
96 _fp->deleteIndex(srcIndex);
97 _fp->insertWayptAtIndex(w, destIndex);
99 if ((signed int) srcIndex == currentWpIndex) {
100 // current waypoint was moved
101 _fp->setCurrentIndex(destIndex);
105 virtual void setUpdateCallback(SGCallback* cb)
110 // implement SGPropertyChangeListener
111 void valueChanged(SGPropertyNode *prop)
113 if (prop->getNameString() == "edited") {
119 if (prop->getNameString() == "flightplan-changed") {
121 static_cast<FGRouteMgr*>(globals->get_subsystem("route-manager"))->flightPlan();
125 flightgear::FlightPlan* _fp;
129 //////////////////////////////////////////////////////////////////////////////
131 static void drawClippedString(puFont& font, const char* s, int x, int y, int maxWidth)
133 int fullWidth = font.getStringWidth(s);
134 if (fullWidth <= maxWidth) { // common case, easy and efficent
135 font.drawString(s, x, y);
140 int len = buf.size();
143 fullWidth = font.getStringWidth(buf.c_str());
144 } while (fullWidth > maxWidth);
146 font.drawString(buf.c_str(), x, y);
149 //////////////////////////////////////////////////////////////////////////////
151 WaypointList::WaypointList(int x, int y, int width, int height) :
152 puFrame(x, y, width, height),
153 GUI_ID(FGCLASS_WAYPOINTLIST),
156 _dragScroll(SCROLL_NO),
159 _updateCallback(NULL),
160 _scrollCallback(NULL),
163 // pretend to be a list, so fgPopup doesn't mess with our mouse events
164 type |= PUCLASS_LIST;
165 flightgear::FlightPlan* fp =
166 static_cast<FGRouteMgr*>(globals->get_subsystem("route-manager"))->flightPlan();
167 setModel(new FlightPlanWaypointModel(fp));
168 setSize(width, height);
174 WaypointList::~WaypointList()
177 delete _updateCallback;
178 delete _scrollCallback;
181 void WaypointList::setUpdateCallback(SGCallback* cb)
183 _updateCallback = cb;
186 void WaypointList::setScrollCallback(SGCallback* cb)
188 _scrollCallback = cb;
191 void WaypointList::setSize(int width, int height)
193 double scrollP = getVScrollPercent();
195 puFrame::setSize(width, height);
197 if (wantsVScroll()) {
198 setVScrollPercent(scrollP);
204 int WaypointList::checkHit ( int button, int updown, int x, int y )
206 if ( isHit( x, y ) || ( puActiveWidget () == this ) )
208 doHit ( button, updown, x, y ) ;
216 void WaypointList::doHit( int button, int updown, int x, int y )
218 puFrame::doHit(button, updown, x, y);
219 if (updown == PU_DRAG) {
224 if (button != active_mouse_button) {
228 if (updown == PU_UP) {
229 puDeactivateWidget();
234 } else if (updown == PU_DOWN) {
235 puSetActiveWidget(this, x, y);
242 int row = rowForY(y - abox.min[1]);
243 if (row >= (int) _model->numWaypoints()) {
244 row = -1; // 'no selection'
247 if (row == getSelected()) {
248 _showLatLon = !_showLatLon;
256 void WaypointList::handleDrag(int x, int y)
259 // don't start drags immediately, require a certain mouse movement first
260 int manhattanLength = abs(x - _mouseDownX) + abs(y - _mouseDownY);
261 if (manhattanLength < DRAG_START_DISTANCE_PX) {
265 _dragSourceRow = rowForY(y - abox.min[1]);
266 Waypt* wp = _model->waypointAt(_dragSourceRow);
267 if (!wp || wp->flag(WPT_GENERATED)) {
268 return; // don't allow generated points to be dragged
272 _dragScroll = SCROLL_NO;
275 if (y < abox.min[1]) {
276 if (_dragScroll != SCROLL_DOWN) {
277 _dragScroll = SCROLL_DOWN;
278 _dragScrollTime.stamp();
280 } else if (y > abox.max[1]) {
281 if (_dragScroll != SCROLL_UP) {
282 _dragScroll = SCROLL_UP;
283 _dragScrollTime.stamp();
286 _dragScroll = SCROLL_NO;
287 _dragTargetRow = rowForY(y - abox.min[1] - (rowHeightPx() / 2));
291 void WaypointList::doDrop(int x, int y)
294 puDeactivateWidget();
296 SG_LOG(SG_GENERAL, SG_INFO, "doDrop");
298 if ((y < abox.min[1]) || (y >= abox.max[1])) {
299 SG_LOG(SG_GENERAL, SG_INFO, "y out of bounds:" << y);
303 if (_dragSourceRow == _dragTargetRow) {
304 SG_LOG(SG_GENERAL, SG_INFO, "source and target row match");
308 _model->moveWaypointToIndex(_dragSourceRow, _dragTargetRow);
310 // keep row indexes linged up when moving an item down the list
311 if (_dragSourceRow < _dragTargetRow) {
315 setSelected(_dragTargetRow);
318 void WaypointList::invokeDownCallback(void)
321 _dragScroll = SCROLL_NO;
322 SG_LOG(SG_GENERAL, SG_INFO, "cancel drag");
325 int WaypointList::rowForY(int y) const
331 // flip y to increase down, not up (as rows do)
332 int flipY = _heightPx - y;
333 int row = (flipY + _scrollPx) / rowHeightPx();
337 void WaypointList::draw( int dx, int dy )
339 puFrame::draw(dx, dy);
345 if (_dragScroll != SCROLL_NO) {
349 double dt = (SGTimeStamp::now() - _blinkTimer).toSecs();
350 if (dt > BLINK_TIME) {
355 glEnable(GL_SCISSOR_TEST);
356 GLint sx = (int) abox.min[0],
358 GLsizei w = (GLsizei) abox.max[0] - abox.min[0],
361 sx += border_thickness;
362 sy += border_thickness;
363 w -= 2 * border_thickness;
364 h -= 2 * border_thickness;
366 glScissor(sx + dx, sy + dy, w, h);
367 int row = firstVisibleRow(),
368 final = lastVisibleRow(),
369 rowHeight = rowHeightPx(),
372 y -= (_scrollPx % rowHeight); // partially draw the first row
374 _arrowWidth = legendFont.getStringWidth(">");
375 for ( ; row <= final; ++row, y += rowHeight) {
376 drawRow(dx, dy, row, y);
377 } // of row drawing iteration
379 glDisable(GL_SCISSOR_TEST);
382 // draw the insert marker after the rows
383 int insertY = (_dragTargetRow * rowHeight) - _scrollPx;
384 SG_CLAMP_RANGE(insertY, 0, std::min(_heightPx, totalHeightPx()));
386 glColor4f(1.0f, 0.5f, 0.0f, 0.8f);
389 glVertex2f(dx + abox.min[0], dy + abox.max[1] - insertY);
390 glVertex2f(dx + abox.max[0], dy + abox.max[1] - insertY);
395 void WaypointList::drawRow(int dx, int dy, int rowIndex, int y)
397 flightgear::Waypt* wp(_model->waypointAt(rowIndex));
399 bool isSelected = (rowIndex == getSelected());
400 bool isCurrent = (rowIndex == _model->currentWaypoint());
401 bool isDragSource = (_dragging && (rowIndex == _dragSourceRow));
404 bkgBox.min[1] = abox.max[1] - y;
405 bkgBox.max[1] = bkgBox.min[1] + rowHeightPx();
408 puFont* f = &legendFont;
409 bool drawBox = false;
411 if (wp->flag(WPT_MISS)) {
413 puSetColor(col, 1.0, 0.0, 0.0, 0.3); // red
414 } else if (wp->flag(WPT_ARRIVAL) || wp->flag(WPT_DEPARTURE)) {
416 puSetColor(col, 0.0, 0.0, 0.0, 0.3);
417 } else if (wp->flag(WPT_APPROACH)) {
419 puSetColor(col, 0.0, 0.0, 0.1, 0.3);
423 // draw later, on *top* of text string
424 } else if (isSelected) { // -PLAIN means selected, apparently
425 bkgBox.draw(dx, dy, -PUSTYLE_PLAIN, colour, false, 0);
426 } else if (drawBox) {
427 bkgBox.draw(dx, dy, PUSTYLE_PLAIN, &col, false, 0);
431 glColor4f (1.0, 0.5, 0.0, 1.0) ;
433 glColor4fv ( colour [ PUCOL_LEGEND ] ) ;
436 int xx = dx + abox.min[0] + PUSTR_LGAP;
437 int yy = dy + abox.max[1] - y ;
438 yy += 4; // center text in row height
441 f->drawString(">", xx, yy);
445 x += _arrowWidth + PUSTR_LGAP;
450 int count = ::snprintf(buffer, 128, "%03d %-5s", rowIndex, wp->ident().c_str());
452 FGPositioned* src = wp->source();
453 if (src && !src->name().empty() && (src->name() != wp->ident())) {
454 // append name if present, and different to id
455 ::snprintf(buffer + count, 128 - count, " (%s)", src->name().c_str());
458 drawClippedString(legendFont, buffer, x, yy, 300);
459 x += 300 + PUSTR_LGAP;
462 SGGeod p(wp->position());
463 char ns = (p.getLatitudeDeg() > 0.0) ? 'N' : 'S';
464 char ew = (p.getLongitudeDeg() > 0.0) ? 'E' : 'W';
466 ::snprintf(buffer, 128 - count, "%4.2f%c %4.2f%c",
467 fabs(p.getLongitudeDeg()), ew, fabs(p.getLatitudeDeg()), ns);
468 } else if (rowIndex > 0) {
471 Waypt* prev = _model->waypointAt(rowIndex - 1);
472 boost::tie(courseDeg, distanceM) = wp->courseAndDistanceFrom(prev->position());
474 ::snprintf(buffer, 128 - count, "%03.0f %5.1fnm",
475 courseDeg, distanceM * SG_METER_TO_NM);
478 f->drawString(buffer, x, yy);
479 x += 100 + PUSTR_LGAP;
481 if (wp->altitudeRestriction() != RESTRICT_NONE) {
482 int altHundredFt = (wp->altitudeFt() + 50) / 100; // round to nearest 100ft
483 if (altHundredFt < 100) {
484 count = ::snprintf(buffer, 128, "%d'", altHundredFt * 100);
485 } else { // display as a flight-level
486 count = ::snprintf(buffer, 128, "FL%d", altHundredFt);
489 f->drawString(buffer, x, yy);
490 } // of valid wp altitude
491 x += 60 + PUSTR_LGAP;
493 if (wp->speedRestriction() == SPEED_RESTRICT_MACH) {
494 count = ::snprintf(buffer, 126, "%03.2fM", wp->speedMach());
495 f->drawString(buffer, x, yy);
496 } else if (wp->speedRestriction() != RESTRICT_NONE) {
497 count = ::snprintf(buffer, 126, "%dKts", (int) wp->speedKts());
498 f->drawString(buffer, x, yy);
502 puSetColor(col, 1.0, 0.5, 0.0, 0.5);
503 bkgBox.draw(dx, dy, PUSTYLE_PLAIN, &col, false, 0);
507 const double SCROLL_PX_SEC = 200.0;
509 void WaypointList::doDragScroll()
511 double dt = (SGTimeStamp::now() - _dragScrollTime).toSecs();
512 _dragScrollTime.stamp();
513 int deltaPx = (int)(dt * SCROLL_PX_SEC);
515 if (_dragScroll == SCROLL_UP) {
516 _scrollPx = _scrollPx - deltaPx;
517 SG_CLAMP_RANGE(_scrollPx, 0, scrollRangePx());
518 _dragTargetRow = firstVisibleRow();
520 _scrollPx = _scrollPx + deltaPx;
521 SG_CLAMP_RANGE(_scrollPx, 0, scrollRangePx());
522 _dragTargetRow = lastFullyVisibleRow() + 1;
525 if (_scrollCallback) {
526 (*_scrollCallback)();
530 int WaypointList::getSelected()
532 return getIntegerValue();
535 void WaypointList::setSelected(int rowIndex)
537 if (rowIndex == getSelected()) {
543 if (rowIndex == -1) {
547 ensureRowVisible(rowIndex);
550 void WaypointList::ensureRowVisible(int rowIndex)
552 if ((rowIndex >= firstFullyVisibleRow()) && (rowIndex <= lastFullyVisibleRow())) {
553 return; // already visible, fine
556 // ideal position would place the desired row in the middle of the
557 // visible section - hence subtract half the visible height.
558 int targetScrollPx = (rowIndex * rowHeightPx()) - (_heightPx / 2);
560 // clamp the scroll value to something valid
561 SG_CLAMP_RANGE(targetScrollPx, 0, scrollRangePx());
562 _scrollPx = targetScrollPx;
565 if (_scrollCallback) { // keep scroll observers in sync
566 (*_scrollCallback)();
570 unsigned int WaypointList::numWaypoints() const
576 return _model->numWaypoints();
579 bool WaypointList::wantsVScroll() const
581 return totalHeightPx() > _heightPx;
584 float WaypointList::getVScrollPercent() const
586 float scrollRange = scrollRangePx();
587 if (scrollRange < 1.0f) {
591 return _scrollPx / scrollRange;
594 float WaypointList::getVScrollThumbPercent() const
596 return _heightPx / (float) totalHeightPx();
599 void WaypointList::setVScrollPercent(float perc)
601 float scrollRange = scrollRangePx();
602 _scrollPx = (int)(scrollRange * perc);
605 int WaypointList::firstVisibleRow() const
607 return _scrollPx / rowHeightPx();
610 int WaypointList::firstFullyVisibleRow() const
612 int rh = rowHeightPx();
613 return (_scrollPx + rh - 1) / rh;
616 int WaypointList::numVisibleRows() const
618 int rh = rowHeightPx();
619 int topOffset = _scrollPx % rh; // pixels of first visible row
620 return (_heightPx - topOffset + rh - 1) / rh;
624 int WaypointList::numFullyVisibleRows() const
626 int rh = rowHeightPx();
627 int topOffset = _scrollPx % rh; // pixels of first visible row
628 return (_heightPx - topOffset) / rh;
631 int WaypointList::rowHeightPx() const
633 return legendFont.getStringHeight() + PUSTR_BGAP;
636 int WaypointList::scrollRangePx() const
638 return std::max(0, totalHeightPx() - _heightPx);
641 int WaypointList::totalHeightPx() const
647 return (int) _model->numWaypoints() * rowHeightPx();
650 int WaypointList::lastFullyVisibleRow() const
652 int row = firstFullyVisibleRow() + numFullyVisibleRows();
653 return std::min(row, (int) _model->numWaypoints() - 1);
656 int WaypointList::lastVisibleRow() const
658 int row = firstVisibleRow() + numVisibleRows();
659 return std::min(row, (int) _model->numWaypoints() - 1);
662 void WaypointList::setModel(Model* model)
669 _model->setUpdateCallback(make_callback(this, &WaypointList::modelUpdateCallback));
674 int WaypointList::checkKey (int key, int updown )
676 if ((updown == PU_UP) || !isVisible () || !isActive () || (window != puGetWindow())) {
680 #ifdef AVOID_FLIGHT_KEYS
691 setSelected(_model->numWaypoints() - 1);
695 case PU_KEY_PAGE_UP :
696 if (getSelected() >= 0) {
697 setSelected(getSelected() - 1);
702 case PU_KEY_PAGE_DOWN : {
703 int newSel = getSelected() + 1;
704 if (newSel >= (int) _model->numWaypoints()) {
713 if (getSelected() >= 0) {
714 Waypt* wp = _model->waypointAt(getSelected());
715 if (wp->flag(WPT_GENERATED)) {
719 if (wp->altitudeRestriction() != RESTRICT_NONE) {
720 int curAlt = (static_cast<int>(wp->altitudeFt()) + 50) / 100;
722 wp->setAltitude(0, RESTRICT_NONE);
724 wp->setAltitude((curAlt - 10) * 100, wp->altitudeRestriction());
731 if (getSelected() >= 0) {
732 flightgear::Waypt* wp = _model->waypointAt(getSelected());
733 if (wp->flag(WPT_GENERATED)) {
737 if (wp->altitudeRestriction() == RESTRICT_NONE) {
738 wp->setAltitude(1000, RESTRICT_AT);
740 int curAlt = (static_cast<int>(wp->altitudeFt()) + 50) / 100;
741 wp->setAltitude((curAlt + 10) * 100, wp->altitudeRestriction());
747 if (getSelected() >= 0) {
748 int index = getSelected();
749 Waypt* wp = _model->waypointAt(getSelected());
750 if (wp->flag(WPT_GENERATED)) {
754 _model->deleteAt(index);
755 setSelected(index - 1);
766 void WaypointList::modelUpdateCallback()
770 if (_updateCallback) {
771 (*_updateCallback)();
775 //////////////////////////////////////////////////////////////////////////////
778 static void handle_scrollbar(puObject* scrollbar)
780 ScrolledWaypointList* self = (ScrolledWaypointList*)scrollbar->getUserData();
781 self->setScrollPercent(scrollbar->getFloatValue());
784 static void waypointListCb(puObject* wpl)
786 ScrolledWaypointList* self = (ScrolledWaypointList*)wpl->getUserData();
787 self->setValue(wpl->getIntegerValue());
788 self->invokeCallback();
791 ScrolledWaypointList::ScrolledWaypointList(int x, int y, int width, int height) :
795 // ensure our type is compound, so fgPopup::applySize doesn't descend into
796 // us, and try to cast our children's user-data to GUIInfo*.
797 type |= PUCLASS_LIST;
802 void ScrolledWaypointList::setValue(float v)
804 puGroup::setValue(v);
808 void ScrolledWaypointList::setValue(int v)
810 puGroup::setValue(v);
814 void ScrolledWaypointList::init(int w, int h)
816 _list = new WaypointList(0, 0, w, h);
817 _list->setUpdateCallback(make_callback(this, &ScrolledWaypointList::modelUpdated));
818 _hasVScroll = _list->wantsVScroll();
819 _list->setUserData(this);
820 _list->setCallback(waypointListCb);
822 _list->setScrollCallback(make_callback(this, &ScrolledWaypointList::updateScroll));
824 _scrollbar = new puaScrollBar(w - _scrollWidth, 0, h,
825 1 /*arrow*/, 1 /* vertical */, _scrollWidth);
826 _scrollbar->setMinValue(0.0);
827 _scrollbar->setMaxValue(1.0);
828 _scrollbar->setUserData(this);
829 _scrollbar->setCallback(handle_scrollbar);
830 close(); // close the group
835 void ScrolledWaypointList::modelUpdated()
839 updateWantsScroll(w, h);
842 void ScrolledWaypointList::setScrollPercent(float v)
844 // slider's min is the bottom, so invert the value
845 _list->setVScrollPercent(1.0f - v);
848 void ScrolledWaypointList::setSize(int w, int h)
850 updateWantsScroll(w, h);
851 puGroup::setSize(w, h);
854 void ScrolledWaypointList::updateWantsScroll(int w, int h)
856 _hasVScroll = _list->wantsVScroll();
859 _scrollbar->reveal();
860 _scrollbar->setPosition(w - _scrollWidth, 0);
861 _scrollbar->setSize(_scrollWidth, h);
862 _list->setSize(w - _scrollWidth, h);
866 _list->setSize(w, h);
870 void ScrolledWaypointList::updateScroll()
872 // _scrollbar->setMaxValue(_list->numWaypoints());
873 _scrollbar->setValue(1.0f - _list->getVScrollPercent());
874 _scrollbar->setSliderFraction(_list->getVScrollThumbPercent());