1 // $Id: xxRoot.cc 5749 2014-10-11 19:42:10Z flaterco $
2 
3 /*  xxRoot  XTide "root" window (control panel, top-level logic)
4 
5     Copyright (C) 1998  David Flater.
6 
7     This program is free software: you can redistribute it and/or modify
8     it under the terms of the GNU General Public License as published by
9     the Free Software Foundation, either version 3 of the License, or
10     (at your option) any later version.
11 
12     This program is distributed in the hope that it will be useful,
13     but WITHOUT ANY WARRANTY; without even the implied warranty of
14     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15     GNU General Public License for more details.
16 
17     You should have received a copy of the GNU General Public License
18     along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 #include "xtide.hh"
22 #include "xxGraphMode.hh"
23 #include "xxDisclaimer.hh"
24 #include "xxReconfigurable.hh"
25 #include "xxLocationList.hh"
26 #include "xxGlobe.hh"
27 #include "xxMap.hh"
28 #include "xxHorizDialog.hh"
29 #include "xxUnsignedChooser.hh"
30 #include "xxMultiChoice.hh"
31 #include "xxErrorBox.hh"
32 #include "xxHelpBox.hh"
33 #include "xxTextMode.hh"
34 
35 
36 static const unsigned controlPanelInitialHeight (900U);
37 
38 // Completely wild guess of how much room is needed by the window dressing on
39 // an xxHelpBox (pixels).
40 static const unsigned windowDressingWidth (32U);
41 
42 
errorCallback(const Dstr & errorMessage,Error::ErrType fatality)43 static void errorCallback (const Dstr &errorMessage, Error::ErrType fatality) {
44   xxroot->newErrorBox (errorMessage, fatality);
45 }
46 
47 
dismissCallback(Widget w unusedParameter,XtPointer client_data unusedParameter,XtPointer call_data unusedParameter)48 static void dismissCallback (Widget w unusedParameter,
49                              XtPointer client_data unusedParameter,
50                              XtPointer call_data unusedParameter) {
51   xxroot->unrealize();
52 }
53 
54 
applyCallback(Widget w unusedParameter,XtPointer client_data unusedParameter,XtPointer call_data unusedParameter)55 static void applyCallback (Widget w unusedParameter,
56                            XtPointer client_data unusedParameter,
57                            XtPointer call_data unusedParameter) {
58   xxroot->apply (xxRoot::justApply);
59 }
60 
61 
changeUserDefaults(const Settings & newUserDefaults)62 static void changeUserDefaults (const Settings &newUserDefaults) {
63   Global::settings = Settings();
64   Global::settings.applyXResources();
65   Global::settings.apply (newUserDefaults);
66   Global::settings.applyCommandLine();
67   Global::settings.fixUpDeprecatedSettings();
68   xxX::installColors();
69 }
70 
71 
apply(ApplyProtocol protocol)72 void xxRoot::apply (ApplyProtocol protocol) {
73 
74   // Build new settings for ~/.xtide.xml.
75   Settings newUserDefaults;
76   newUserDefaults.nullify();
77 
78   // There are several ways to implement this, all of which involve
79   // messy nested logic because the association between dialog classes
80   // and configurable interpretations is not simple.  This way at
81   // least has no duplicated code and at most two redundant
82   // operations (the failed dynamic casts).
83 
84   for (BetterMap<const Dstr, xxRedrawable*>::iterator it = dialogs.begin();
85        it != dialogs.end();
86        ++it) {
87     xxRedrawable *redrawable (it->second);
88     assert (redrawable);
89     assert (newUserDefaults.count(it->first) == 1);
90     Configurable &configurable (newUserDefaults[it->first]);
91     assert (!configurable.caption.isNull());
92     assert (configurable.kind == Configurable::settingKind);
93 
94     xxMultiChoice *multiChoice (dynamic_cast<xxMultiChoice*>(redrawable));
95     if (multiChoice) {
96       const unsigned choice (multiChoice->choice());
97       switch (configurable.interpretation) {
98       case Configurable::booleanInterp:
99 	switch (choice) {
100 	case 0:
101 	  configurable.isNull = false;
102 	  configurable.c = 'y';
103 	  break;
104 	case 1:
105 	  configurable.isNull = false;
106 	  configurable.c = 'n';
107 	  break;
108 	case 2:
109 	  break;
110 	default:
111 	  assert (false);
112 	}
113 	break;
114       case Configurable::glDoubleInterp:
115         assert (choice < 14);
116 	switch (choice) {
117 	case 12:
118 	  configurable.isNull = false;
119 	  configurable.d = 360.0;
120 	  break;
121 	case 13:
122 	  break;
123 	default:
124 	  configurable.isNull = false;
125 	  configurable.d = (double)choice * 30.0 - 180.0;
126 	  break;
127 	}
128 	break;
129       case Configurable::unitInterp:
130 	switch (choice) {
131 	case 0:
132 	  configurable.isNull = false;
133 	  configurable.s = "ft";
134 	  break;
135 	case 1:
136 	  configurable.isNull = false;
137 	  configurable.s = "m";
138 	  break;
139 	case 2:
140 	  configurable.isNull = false;
141 	  configurable.s = "x";
142 	  break;
143 	case 3:
144 	  break;
145 	default:
146 	  assert (false);
147 	}
148 	break;
149       case Configurable::gsInterp:
150 	switch (choice) {
151 	case 0:
152 	  configurable.isNull = false;
153 	  configurable.c = 'd';
154 	  break;
155 	case 1:
156 	  configurable.isNull = false;
157 	  configurable.c = 'l';
158 	  break;
159 	case 2:
160 	  configurable.isNull = false;
161 	  configurable.c = 's';
162 	  break;
163 	case 3:
164 	  break;
165 	default:
166 	  assert (false);
167 	}
168 	break;
169       default:
170         assert (false);
171       }
172 
173     } else {
174       xxUnsignedChooser *unsignedChooser (
175                                  dynamic_cast<xxUnsignedChooser*>(redrawable));
176       if (unsignedChooser) {
177         const unsigned choice (unsignedChooser->choice());
178         assert (configurable.interpretation == Configurable::posIntInterp);
179 	if (choice != UINT_MAX) {
180           assert (choice > 0);
181 	  configurable.isNull = false;
182 	  configurable.u = choice;
183 	}
184       } else {
185         xxHorizDialog *horizDialog (dynamic_cast<xxHorizDialog*>(redrawable));
186         assert (horizDialog);
187         const Dstr value (horizDialog->val());
188 	switch (configurable.interpretation) {
189 	case Configurable::posDoubleInterp:
190 	case Configurable::nonnegativeDoubleInterp:
191 	case Configurable::opacityDoubleInterp:
192 	  {
193 	    Global::GetDoubleReturn gdr (Global::getDouble (
194                                                    value,
195                                                    configurable.interpretation,
196 						   configurable.d));
197             switch (gdr) {
198             case Global::inputNotOK:
199               return;
200             case Global::emptyInput:
201               break;
202             case Global::inputOK:
203               configurable.isNull = false;
204               break;
205             default:
206               assert (false);
207             }
208 	  }
209 	  break;
210         case Configurable::textInterp:
211 	  if (value.length()) {
212 	    configurable.isNull = false;
213 	    configurable.s = value;
214 	  }
215 	  break;
216 	case Configurable::colorInterp:
217 	  if (value.length()) {
218 	    // Sanity check color
219 	    uint8_t r, g, b;
220 	    if (!(Colors::parseColor (value, r, g, b, Error::nonfatal)))
221 	      return;
222 	    configurable.isNull = false;
223 	    configurable.s = value;
224 	  }
225 	  break;
226 	case Configurable::timeFormatInterp:
227 	  if (value.length()) {
228 	    // Sanity check strftime format string
229 	    if (value.strchr('"') != -1 ||
230                 value.strchr('>') != -1 ||
231 		value.strchr('<') != -1) {
232 	      Global::barf (Error::XMLPARSE,
233                             "Please don't use nasty characters like \", >, and < in your date/time formats.",
234                             Error::nonfatal);
235 	      return;
236 	    }
237 	    configurable.isNull = false;
238 	    configurable.s = value;
239 	  }
240 	  break;
241 	case Configurable::eventMaskInterp:
242 	  if (value.length()) {
243 	    if (!Global::isValidEventMask (value)) {
244 	      Global::barf (Error::BAD_EVENTMASK, Error::nonfatal);
245 	      return;
246 	    }
247 	    configurable.isNull = false;
248 	    configurable.s = value;
249 	  }
250 	  break;
251 	default:
252 	  assert (false);
253 	}
254       }
255     }
256   }
257 
258   changeUserDefaults (newUserDefaults);
259   globalRedraw();
260 
261   // Provided that the world did not explode as a result of applying those
262   // settings, only then might they be saved.
263   if (protocol == applyAndSave)
264     newUserDefaults.save();
265 }
266 
267 
saveCallback(Widget w unusedParameter,XtPointer client_data unusedParameter,XtPointer call_data unusedParameter)268 static void saveCallback (Widget w unusedParameter,
269                           XtPointer client_data unusedParameter,
270                           XtPointer call_data unusedParameter) {
271   xxroot->apply (xxRoot::applyAndSave);
272 }
273 
274 
helpCallback(Widget w unusedParameter,XtPointer client_data unusedParameter,XtPointer call_data unusedParameter)275 static void helpCallback (Widget w unusedParameter,
276                           XtPointer client_data unusedParameter,
277                           XtPointer call_data unusedParameter) {
278   Dstr helpstring ("\
279 XTide Control Panel\n\
280 \n\
281 The Control Panel is used to change global XTide settings.  These settings\n\
282 take precedence over compiled-in defaults and X application defaults, but\n\
283 they are overridden by settings made on the command line.  Therefore, any\n\
284 settings that you make in the Control Panel will not have any visible effect\n\
285 if you have also made these settings on the command line.\n\
286 \n\
287 Some settings have dialog boxes for you to type in; others have pull-down\n\
288 menus or '+' and '-' controls.  In each case, there is a way to leave the\n\
289 field blank if you want to keep the inherited settings.  Either delete all\n\
290 text in the dialog box, or choose the \"(blank)\" option on the pull-down\n\
291 menu, or lay on the '-' button until the field reads \"(blank)\".\n\
292 \n\
293 The settings identified as \"default\" settings (widths, heights, and\n\
294 aspect ratio) will not affect existing windows.  Widths and heights will\n\
295 affect all new windows; aspect ratio will only affect new windows that\n\
296 are created from the location chooser.  (This is because the aspect is\n\
297 preserved from one tide window to any new windows that it spawns.)\n\
298 \n\
299 Use the Apply button to apply the new settings to the current XTide session\n\
300 without making them permanent.  Use Save to make them permanent.\n\
301 \n\
302 If necessary, resize the Control Panel to keep all of the settings visible.\n\
303 \n\
304 About colors:  When entering colors, use either standard X11 color names\n\
305 or 24-bit hex specs of the form rgb:hh/hh/hh.  Do not use more or less bits;\n\
306 it will not work.\n\
307 \n\
308 About the event mask:  Events to suppress (p = phase of moon, S =\n\
309 sunrise, s = sunset, M = moonrise, m = moonset), or x to suppress\n\
310 none.  E.g, to suppress all sun and moon events, set eventmask to the\n\
311 value pSsMm.\n\
312 \n\
313 About date/time formats:  Please refer to the man page for strftime to see\n\
314 how these formats work.  Here is how to get XTide to use 24-hour time instead\n\
315 of AM/PM:\n\
316    Hour format  %H\n\
317    Time format  %H:%M %Z");
318   (void) xxroot->newHelpBox (helpstring);
319 }
320 
321 
322 // This function is passed to Settings::applyXResources.
323 
324 static XrmDatabase database;
getResource(const Dstr & name,Dstr & val_out)325 static const bool getResource (const Dstr &name, Dstr &val_out) {
326   Dstr n1 ("xtide*"), n2 ("XTide*");
327   n1 += name;
328   n2 += name;
329   XrmValue xv;
330   char *str_type[20]; // Waste.
331   if (XrmGetResource (database, n1.aschar(), n2.aschar(), str_type, &xv)) {
332     val_out = (char *)xv.addr;
333     return true;
334   }
335   return false;
336 }
337 
338 
xxRoot(int argc,char ** argv)339 xxRoot::xxRoot (int argc, char **argv):
340   xxWindow (argc, argv),
341   popupCount(0) {
342 
343   setTitle ("Control Panel");
344 
345   // Switches recognized by X11 are removed from the command line by
346   // the xxWidget constructor.
347   database = XtScreenDatabase (xxX::screen);
348   Global::settings.applyXResources (getResource);
349   Global::settings.applyUserDefaults();
350   Global::settings.applyCommandLine (argc, argv);
351   Global::settings.fixUpDeprecatedSettings();
352   xxX::connectPostSettingsHook (popup->widget());
353 
354   Global::setErrorCallback (&errorCallback);
355 
356   {
357     // Can't do this until after the settings have been processed.
358     Arg args[2] =  {
359       {XtNbackground, (XtArgVal)xxX::pixels[Colors::background]},
360       {XtNforeground, (XtArgVal)xxX::pixels[Colors::foreground]}
361     };
362     XtSetValues (popup->widget(), args, 2);
363   }{
364     // Slightly different than what xxWindow would provide.
365     Arg formArgs[3] =  {
366       {XtNbackground, (XtArgVal)xxX::pixels[Colors::background]},
367       {XtNforeground, (XtArgVal)xxX::pixels[Colors::foreground]},
368       {XtNvSpace, (XtArgVal)0}
369     };
370     Widget formWidget = xxX::createXtWidget ("", formWidgetClass,
371       popup->widget(), formArgs, 3);
372     container = xxX::wrap (formWidget);
373   }{
374     Arg labelArgs[6] =  {
375       {XtNbackground, (XtArgVal)xxX::pixels[Colors::background]},
376       {XtNforeground, (XtArgVal)xxX::pixels[Colors::foreground]},
377       {(char*)XtNleft, (XtArgVal)XawChainLeft},
378       {(char*)XtNright, (XtArgVal)XawChainLeft},
379       {(char*)XtNtop, (XtArgVal)XawChainTop},
380       {(char*)XtNbottom, (XtArgVal)XawChainTop}
381     };
382     Widget labelWidget = xxX::createXtWidget (
383       "-------------- XTide Control Panel --------------",
384       labelWidgetClass, container->widget(), labelArgs, 6);
385     label = new xxWidget (labelWidget);
386   }{
387     Arg vpArgs[10] =  {
388       {XtNheight, (XtArgVal)controlPanelInitialHeight},
389       {XtNbackground, (XtArgVal)xxX::pixels[Colors::background]},
390       {XtNforeground, (XtArgVal)xxX::pixels[Colors::foreground]},
391       {XtNallowVert, (XtArgVal)1},
392       {XtNforceBars, (XtArgVal)1},
393       {(char*)XtNfromVert, (XtArgVal)label->widget()},
394       {(char*)XtNleft, (XtArgVal)XawChainLeft},
395       {(char*)XtNright, (XtArgVal)XawChainRight},
396       {(char*)XtNtop, (XtArgVal)XawChainTop},
397       {(char*)XtNbottom, (XtArgVal)XawChainBottom}
398     };
399 
400     Widget viewportWidget = xxX::createXtWidget ("", viewportWidgetClass,
401       container->widget(), vpArgs, 10);
402     viewport = new xxWidget (viewportWidget);
403     xxX::fixBorder (viewportWidget);
404 
405     require (scrollbarWidget = XtNameToWidget (viewportWidget, "vertical"));
406     Arg sbArgs[2] = {
407       {XtNbackground, (XtArgVal)xxX::pixels[Colors::background]},
408       {XtNforeground, (XtArgVal)xxX::pixels[Colors::foreground]}
409     };
410     XtSetValues (scrollbarWidget, sbArgs, 2);
411     setRudeness (scrollbarWidget);
412   }{
413     Arg boxArgs[4] =  {
414       {XtNbackground, (XtArgVal)xxX::pixels[Colors::background]},
415       {XtNforeground, (XtArgVal)xxX::pixels[Colors::foreground]},
416       {XtNvSpace, (XtArgVal)0},
417       {XtNorientation, (XtArgVal)XtorientVertical}
418     };
419     Widget boxWidget = xxX::createXtWidget ("", boxWidgetClass,
420       viewport->widget(), boxArgs, 4);
421     viewBox = new xxWidget (boxWidget);
422   }
423 
424   // Get current settings, but convert the deprecated ones.
425   Settings settings;
426   settings.nullify();
427   settings.applyUserDefaults();
428   settings.fixUpDeprecatedSettings();
429 
430   static constString toggleChoices[] = {"Yes",
431                                         "No",
432                                         "(blank)",
433                                         NULL};
434 
435   static constString unitsChoices[] = {"Feet",
436                                        "Meters",
437                                        "No preference",
438                                        "(blank)",
439                                        NULL};
440 
441   static constString glChoices[] = {"180",
442                                     "150 W",
443                                     "120 W",
444                                     "90 W",
445                                     "60 W",
446                                     "30 W",
447                                     "0",
448                                     "30 E",
449                                     "60 E",
450                                     "90 E",
451                                     "120 E",
452                                     "150 E",
453                                     "Max stations",
454                                     "(blank)",
455                                     NULL};
456 
457   static constString gsChoices[] = {"d",
458 				    "l",
459 				    "s",
460 				    "(blank)",
461 				    NULL};
462 
463   // Create all of the dialogs.
464   for (ConfigurablesMap::iterator it = settings.begin();
465        it != settings.end();
466        ++it) {
467     Configurable &configurable (it->second);
468     if (!configurable.caption.isNull()) {
469       if (configurable.kind == Configurable::settingKind) {
470 	switch (configurable.interpretation) {
471 	case Configurable::booleanInterp:
472 	  {
473 	    unsigned firstChoice;
474 	    if (configurable.isNull)
475 	      firstChoice = 2;
476 	    else if (configurable.c == 'n')
477 	      firstChoice = 1;
478 	    else
479 	      firstChoice = 0;
480 	    dialogs[it->first] = new xxMultiChoice (
481 						*viewBox,
482 	                                        configurable.caption.aschar(),
483                                                 toggleChoices,
484                                                 firstChoice);
485 	  }
486 	  break;
487 	case Configurable::posIntInterp:
488 	  dialogs[it->first] = new xxUnsignedChooser (
489                                                 *viewBox,
490 	                                        configurable.caption.aschar(),
491 						configurable.u,
492 						configurable.isNull,
493 						configurable.minValue);
494 	  break;
495 	case Configurable::posDoubleInterp:
496 	case Configurable::nonnegativeDoubleInterp:
497 	case Configurable::opacityDoubleInterp:
498 	  {
499 	    char temp[80];
500 	    if (configurable.isNull)
501 	      strcpy (temp, "");
502 	    else
503 	      sprintf (temp, "%0.2f", configurable.d);
504 	    dialogs[it->first] = new xxHorizDialog (
505                                                 *viewBox,
506 	                                        configurable.caption.aschar(),
507 						temp);
508 	  }
509 	  break;
510 	case Configurable::glDoubleInterp:
511 	  {
512 	    unsigned firstChoice = 13;
513 	    if (!configurable.isNull) {
514 	      if (configurable.d == 360.0)
515 		firstChoice = 12;
516 	      else
517 		firstChoice = (unsigned) ((configurable.d + 180.0) / 30.0);
518 	    }
519 	    dialogs[it->first] = new xxMultiChoice (
520                                                 *viewBox,
521 	                                        configurable.caption.aschar(),
522 						glChoices,
523 						firstChoice);
524 	  }
525 	  break;
526 	case Configurable::colorInterp:
527 	case Configurable::eventMaskInterp:
528 	case Configurable::timeFormatInterp:
529 	case Configurable::textInterp:
530 	  dialogs[it->first] = new xxHorizDialog (
531                            *viewBox,
532 			   configurable.caption.aschar(),
533 			   configurable.isNull ? "" : configurable.s.aschar());
534 	  break;
535 	case Configurable::unitInterp:
536 	  {
537 	    unsigned firstChoice;
538 	    if (configurable.isNull)
539 	      firstChoice = 3;
540 	    else if (configurable.s == "ft")
541 	      firstChoice = 0;
542 	    else if (configurable.s == "m")
543 	      firstChoice = 1;
544 	    else
545 	      firstChoice = 2;
546 	    dialogs[it->first] = new xxMultiChoice (
547                                                 *viewBox,
548 						configurable.caption.aschar(),
549 						unitsChoices,
550 						firstChoice);
551 	  }
552 	  break;
553 	case Configurable::gsInterp:
554 	  {
555 	    unsigned firstChoice;
556 	    if (configurable.isNull)
557 	      firstChoice = 3;
558 	    else if (configurable.c == 'd')
559 	      firstChoice = 0;
560 	    else if (configurable.c == 'l')
561 	      firstChoice = 1;
562 	    else
563 	      firstChoice = 2;
564 	    dialogs[it->first] = new xxMultiChoice (
565                                                 *viewBox,
566 						configurable.caption.aschar(),
567 						gsChoices,
568 						firstChoice);
569 	  }
570 	  break;
571 	default:
572 	  assert (false);
573 	}
574       }
575     }
576   }
577 
578   {
579     Arg applyArgs[9] =  {
580       {XtNvisual, (XtArgVal)xxX::visual},
581       {XtNcolormap, (XtArgVal)xxX::colormap},
582       {XtNbackground, (XtArgVal)xxX::pixels[Colors::button]},
583       {XtNforeground, (XtArgVal)xxX::pixels[Colors::foreground]},
584       {(char*)XtNfromVert, (XtArgVal)viewport->widget()},
585       {(char*)XtNleft, (XtArgVal)XawChainLeft},
586       {(char*)XtNright, (XtArgVal)XawChainLeft},
587       {(char*)XtNtop, (XtArgVal)XawChainBottom},
588       {(char*)XtNbottom, (XtArgVal)XawChainBottom}
589     };
590     Widget buttonWidget = xxX::createXtWidget ("Apply", commandWidgetClass,
591       container->widget(), applyArgs, 9);
592     XtAddCallback (buttonWidget, XtNcallback, applyCallback,
593      (XtPointer)this);
594     applyButton = new xxWidget (buttonWidget);
595   }{
596     Arg saveArgs[10] =  {
597       {XtNvisual, (XtArgVal)xxX::visual},
598       {XtNcolormap, (XtArgVal)xxX::colormap},
599       {XtNbackground, (XtArgVal)xxX::pixels[Colors::button]},
600       {XtNforeground, (XtArgVal)xxX::pixels[Colors::foreground]},
601       {(char*)XtNfromVert, (XtArgVal)viewport->widget()},
602       {(char*)XtNfromHoriz, (XtArgVal)applyButton->widget()},
603       {(char*)XtNleft, (XtArgVal)XawChainLeft},
604       {(char*)XtNright, (XtArgVal)XawChainLeft},
605       {(char*)XtNtop, (XtArgVal)XawChainBottom},
606       {(char*)XtNbottom, (XtArgVal)XawChainBottom}
607     };
608     Widget buttonWidget = xxX::createXtWidget ("Save", commandWidgetClass,
609       container->widget(), saveArgs, 10);
610     XtAddCallback (buttonWidget, XtNcallback, saveCallback,
611      (XtPointer)this);
612     saveButton = new xxWidget (buttonWidget);
613   }{
614     Arg buttonArgs[10] =  {
615       {XtNvisual, (XtArgVal)xxX::visual},
616       {XtNcolormap, (XtArgVal)xxX::colormap},
617       {XtNbackground, (XtArgVal)xxX::pixels[Colors::button]},
618       {XtNforeground, (XtArgVal)xxX::pixels[Colors::foreground]},
619       {(char*)XtNfromVert, (XtArgVal)viewport->widget()},
620       {(char*)XtNfromHoriz, (XtArgVal)saveButton->widget()},
621       {(char*)XtNleft, (XtArgVal)XawChainLeft},
622       {(char*)XtNright, (XtArgVal)XawChainLeft},
623       {(char*)XtNtop, (XtArgVal)XawChainBottom},
624       {(char*)XtNbottom, (XtArgVal)XawChainBottom}
625     };
626     Widget buttonWidget = xxX::createXtWidget ("Dismiss", commandWidgetClass,
627       container->widget(), buttonArgs, 10);
628     XtAddCallback (buttonWidget, XtNcallback, dismissCallback,
629      (XtPointer)this);
630     dismissButton = new xxWidget (buttonWidget);
631   }{
632     Arg buttonArgs[10] =  {
633       {XtNvisual, (XtArgVal)xxX::visual},
634       {XtNcolormap, (XtArgVal)xxX::colormap},
635       {XtNbackground, (XtArgVal)xxX::pixels[Colors::button]},
636       {XtNforeground, (XtArgVal)xxX::pixels[Colors::foreground]},
637       {(char*)XtNfromVert, (XtArgVal)viewport->widget()},
638       {(char*)XtNfromHoriz, (XtArgVal)dismissButton->widget()},
639       {(char*)XtNleft, (XtArgVal)XawChainLeft},
640       {(char*)XtNright, (XtArgVal)XawChainLeft},
641       {(char*)XtNtop, (XtArgVal)XawChainBottom},
642       {(char*)XtNbottom, (XtArgVal)XawChainBottom}
643     };
644     Widget buttonWidget = xxX::createXtWidget ("?", commandWidgetClass,
645       container->widget(), buttonArgs, 10);
646     XtAddCallback (buttonWidget, XtNcallback, helpCallback,
647       (XtPointer)this);
648     helpButton = new xxWidget (buttonWidget);
649   }
650 
651   obeyMouseWheel (popup->widget());
652   obeyMouseWheel (scrollbarWidget);
653   obeyMouseWheel (viewBox->widget());
654 }
655 
656 
dup()657 void xxRoot::dup() {
658   ++popupCount;
659 }
660 
661 
dup(xxWindow * child)662 void xxRoot::dup (xxWindow *child) {
663   dup();
664   children.insert (child);
665 }
666 
667 
release()668 void xxRoot::release () {
669   assert (popupCount > 0);
670   if (--popupCount == 0)
671     exit (0);
672 }
673 
674 
release(xxWindow * child)675 void xxRoot::release (xxWindow *child) {
676   release ();
677   children.erase (child);
678 }
679 
680 
globalRedraw()681 void xxRoot::globalRedraw() {
682   for (std::set<xxWindow*>::iterator it = children.begin();
683        it != children.end();
684        ++it)
685     (*it)->globalRedraw();
686 
687   for (BetterMap<const Dstr, xxRedrawable*>::iterator it = dialogs.begin();
688        it != dialogs.end();
689        ++it)
690     it->second->globalRedraw();
691 
692   Arg buttonArgs[2] =  {
693     {XtNbackground, (XtArgVal)xxX::pixels[Colors::button]},
694     {XtNforeground, (XtArgVal)xxX::pixels[Colors::foreground]}
695   };
696   assert (applyButton);
697   XtSetValues (applyButton->widget(), buttonArgs, 2);
698   assert (saveButton);
699   XtSetValues (saveButton->widget(), buttonArgs, 2);
700   assert (helpButton);
701   XtSetValues (helpButton->widget(), buttonArgs, 2);
702   assert (dismissButton);
703   XtSetValues (dismissButton->widget(), buttonArgs, 2);
704 
705   Arg args[2] =  {
706     {XtNbackground, (XtArgVal)xxX::pixels[Colors::background]},
707     {XtNforeground, (XtArgVal)xxX::pixels[Colors::foreground]}
708   };
709   assert (label);
710   XtSetValues (label->widget(), args, 2);
711   assert (viewBox);
712   XtSetValues (viewBox->widget(), args, 2);
713   assert (viewport);
714   XtSetValues (viewport->widget(), args, 2);
715   assert (container.get());
716   XtSetValues (container->widget(), args, 2);
717   assert (popup.get());
718   XtSetValues (popup->widget(), args, 2);
719   XtSetValues (scrollbarWidget, args, 2);
720 }
721 
722 
run()723 void xxRoot::run() {
724   if (!Global::disclaimerDisabled()) {
725     (void) new xxDisclaimer (*popup, true);
726     handleXEvents (returnWhenIdle);
727   }
728 
729   if (Global::settings["l"].isNull)
730     newChooser ();
731   else
732     commandLineWindows ();
733 
734   handleXEvents (loopForever);
735 }
736 
737 
newHelpBox(const Dstr & help)738 xxHelpBox * const xxRoot::newHelpBox (const Dstr &help) {
739   return new xxHelpBox (*popup, help);
740 }
741 
742 
newGraph(Station * station,Timestamp t)743 xxGraphMode * const xxRoot::newGraph (Station *station, Timestamp t) {
744   return new xxGraphMode (*popup, station, t);
745 }
746 
747 
newGraph(const StationRef & stationRef)748 xxGraphMode * const xxRoot::newGraph (const StationRef &stationRef) {
749   return newGraph (stationRef.load(), (time_t)time(NULL));
750 }
751 
752 
newPlain(Station * station,Timestamp t)753 xxTextMode * const xxRoot::newPlain (Station *station, Timestamp t) {
754   return new xxTextMode (*popup, station, Mode::plain, t);
755 }
756 
757 
newRaw(Station * station,Timestamp t)758 xxTextMode * const xxRoot::newRaw (Station *station, Timestamp t) {
759   return new xxTextMode (*popup, station, Mode::raw, t);
760 }
761 
762 
newMediumRare(Station * station,Timestamp t)763 xxTextMode * const xxRoot::newMediumRare (Station *station, Timestamp t) {
764   return new xxTextMode (*popup, station, Mode::mediumRare, t);
765 }
766 
767 
newClock(Station * station,xxClock::ButtonsStyle buttonsStyle)768 xxClock * const xxRoot::newClock (Station *station,
769 				  xxClock::ButtonsStyle buttonsStyle) {
770   return new xxClock (*popup, station, buttonsStyle);
771 }
772 
773 
newClock(Station * station)774 xxClock * const xxRoot::newClock (Station *station) {
775   return newClock (station,
776                    (Global::settings["cb"].c == 'n' ? xxClock::noButtons :
777                                                       xxClock::buttons));
778 }
779 
780 
commandLineWindows()781 void xxRoot::commandLineWindows () {
782   Settings &settings (Global::settings);
783   Timestamp t ((time_t)time(NULL));
784   Mode::Mode mode;
785   {
786     const Configurable &m (settings["m"]);
787     if (m.isNull)
788       mode = Mode::clock;
789     else
790       mode = (Mode::Mode)m.c;
791   }{
792     const Configurable &l (settings["l"]);
793     const Configurable &ml (settings["ml"]);
794     DstrVector::const_iterator it (l.v.begin());
795     DstrVector::const_iterator stop (l.v.end());
796     while (it != stop) {
797       StationRef *stationRef (Global::stationIndex().getStationRefByName(*it));
798       if (stationRef) {
799         const Configurable &b (settings["b"]);
800 	if (!b.isNull) {
801 	  t = Timestamp (b.s, stationRef->timezone);
802 	  if (t.isNull())
803 	    Global::cant_mktime (b.s, stationRef->timezone, Error::fatal);
804 	}
805         Station *station (stationRef->load());
806 
807 	if (!ml.isNull) {
808 	  station->markLevel = ml.p;
809 	  if (ml.p.Units() != station->predictUnits())
810 	    station->markLevel.Units (station->predictUnits());
811 	}
812 
813 	xxWindow *d (NULL);
814 	if (mode == Mode::graph)
815 	  d = newGraph (station, t);
816 	else if (mode == Mode::plain)
817 	  d = newPlain (station, t);
818 	else if (mode == Mode::raw)
819 	  d = newRaw (station, t);
820 	else if (mode == Mode::mediumRare)
821 	  d = newMediumRare (station, t);
822 	else if (mode == Mode::about) {
823           d = newAbout (station);
824           delete station;
825 	} else
826 	  d = newClock (station);
827         assert (d);
828 
829 	// Apply -geometry to first one only.
830 	if (it == l.v.begin())
831 	  d->move (settings["X"].s);
832 
833       } else {
834 	Dstr details ("Could not find: ");
835 	details += *it;
836 	Global::barf (Error::STATION_NOT_FOUND, details);
837       }
838       ++it;
839     }
840   }
841 }
842 
843 
newChooser()844 void xxRoot::newChooser () {
845   if (Global::settings["fe"].c == 'n')
846     (void) new xxGlobe (*popup);
847   else
848     (void) new xxMap (*popup);
849 }
850 
851 
newMap()852 void xxRoot::newMap () {
853   (void) new xxMap (*popup);
854 }
855 
856 
newGlobe()857 void xxRoot::newGlobe () {
858   (void) new xxGlobe (*popup);
859 }
860 
861 
lineTooLong(const Dstr & line,const unsigned widthLimit)862 static bool lineTooLong (const Dstr &line, const unsigned widthLimit) {
863   return (xxX::stringWidth (xxX::monoFontStruct, line) > widthLimit);
864 }
865 
866 
867 // This function is not very efficient, but its use is usually avoided.
wrapLongLine(Dstr & line,const unsigned widthLimit)868 static void wrapLongLine (Dstr &line,
869 			  const unsigned widthLimit) {
870   Dstr whatFitsOnScreen, wrappedLine;
871   while (line.length()) {
872     whatFitsOnScreen += line[0];
873     if (lineTooLong (whatFitsOnScreen, widthLimit)) {
874       whatFitsOnScreen -= whatFitsOnScreen.length()-1;
875       if (wrappedLine.length())
876 	wrappedLine += '\n';
877       wrappedLine += whatFitsOnScreen;
878       whatFitsOnScreen = (char*)NULL;
879     } else
880       line /= 1;
881   }
882   // Don't worry about eating blank lines here--if it were blank it
883   // would not have been too long in the first place.
884   if (whatFitsOnScreen.length()) {
885     if (wrappedLine.length())
886       wrappedLine += '\n';
887     wrappedLine += whatFitsOnScreen;
888   }
889   line = wrappedLine;
890 }
891 
892 
wrapLongLines(Dstr & verbiage)893 static void wrapLongLines (Dstr &verbiage) {
894   assert (WidthOfScreen(xxX::screen) > 0 &&
895           (unsigned)WidthOfScreen(xxX::screen) > windowDressingWidth);
896   const unsigned widthLimit (WidthOfScreen(xxX::screen) - windowDressingWidth);
897   Dstr formattedVerbiage, line;
898   verbiage.getline (line);
899   while (!line.isNull()) {
900     // Skip wrapping for short enough lines (which should be most of them)
901     if (lineTooLong (line, widthLimit))
902       wrapLongLine (line, widthLimit);
903     if (formattedVerbiage.length())
904       formattedVerbiage += '\n';
905     formattedVerbiage += line;
906     verbiage.getline (line);
907   }
908   verbiage = formattedVerbiage;
909 }
910 
911 
newAbout(const Station * station)912 xxHelpBox * const xxRoot::newAbout (const Station *station) {
913   assert (station);
914   Dstr verbiage;
915   station->aboutMode (verbiage, Format::text, "ISO-8859-1");
916   // If necessary, wrap text to make the pop-up fit on the screen.  If this
917   // is not done, a station with long descriptive text that is not formatted
918   // with linebreaks results in a ludicrously wide window that cannot then be
919   // resized.
920   wrapLongLines (verbiage);
921   return newHelpBox (verbiage);
922 }
923 
924 
newAboutXTide()925 void const xxRoot::newAboutXTide () {
926   (void) new xxDisclaimer (*popup, false);
927 }
928 
929 
newErrorBox(const Dstr & errmsg,Error::ErrType fatality)930 void xxRoot::newErrorBox (const Dstr &errmsg, Error::ErrType fatality) {
931   static bool snakeBit (false); // Hide double-barfs.
932   if (!snakeBit)
933     (void) new xxErrorBox (*popup, errmsg, fatality);
934   if (fatality == Error::fatal) {
935     snakeBit = true;
936 
937     // This is a bad thing.  We have to grind crank on the X server to
938     // get the error box to appear, but this might also cause
939     // previously queued events to do stuff--possibly even leading to
940     // another fatal error.  The callback that led to the current sad
941     // state of affairs is being held hostage on the stack and did not
942     // get to finish whatever it was doing.
943     handleXEvents (xxRoot::loopForever);
944   }
945 }
946 
947 
948 // Similar to, but critically different from, xxWindow::realize.
realize()949 void xxRoot::realize() {
950   if (!_isRealized) {
951     Widget popupWidget (popup->widget());
952     XtRealizeWidget (popupWidget);
953     XSetWMProtocols (xxX::display, XtWindow(popupWidget), &xxX::killAtom, 1);
954     _isRealized = true;
955     setTitle_NET_WM();
956     dup();
957   }
958 }
959 
960 
961 // Similar to, but critically different from, xxWindow::unrealize.
unrealize()962 void xxRoot::unrealize() {
963   if (_isRealized) {
964     XtUnrealizeWidget (popup->widget());
965     _isRealized = false;
966     release();
967 
968     // Since the window vanishes, the Dismiss button misses the
969     // LeaveWindow event.  The next time the window is realized,
970     // Dismiss is still stuck in a set and/or highlighted state.
971     // This fixes that.
972     XtCallActionProc (dismissButton->widget(), "reset", NULL, NULL, 0);
973   }
974 }
975 
976 
dismiss()977 xxWindow * const xxRoot::dismiss() {
978   unrealize();
979   // ISO/IEC 14882:2003 5.3.5 specifies that deleting a null pointer
980   // shall have no effect.
981   return NULL;
982 }
983 
984 
handleXEvents(HandleXEventsReturnProtocol protocol)985 void xxRoot::handleXEvents (HandleXEventsReturnProtocol protocol) {
986   XEvent foo;
987   std::map<Window, XEvent> procrastinatedResizes;
988   do {
989     bool blockOnce (protocol == loopForever);
990     while (XtAppPending(xxX::appContext) || blockOnce) {
991       blockOnce = false;
992       XtAppNextEvent (xxX::appContext, &foo);
993       if (foo.type == ConfigureNotify)
994 	// Newer window managers like Metacity, Compiz, KWin, Xfwm, and
995 	// Openbox got the idea to deliver a stream of resize events while a
996 	// window is being resized instead of just one for the final size.
997 	// This was not in the contract, but now we have to deal with it to
998 	// avoid looking stupid.  Put off resizes and discard those that
999 	// become redundant before we get around to them.
1000         procrastinatedResizes[foo.xconfigure.window] = foo;
1001       else
1002         XtDispatchEvent (&foo);
1003     }
1004     // Catch up on resizes when there's nothing better to do.
1005     for (std::map<Window, XEvent>::iterator it (procrastinatedResizes.begin());
1006 	 it != procrastinatedResizes.end();
1007 	 ++it)
1008       XtDispatchEvent (&(it->second));
1009     procrastinatedResizes.clear();
1010   } while (protocol == loopForever);
1011 }
1012