1 //
2 // Main demo program for the Fast Light Tool Kit (FLTK).
3 //
4 // Copyright 1998-2021 by Bill Spitzak and others.
5 //
6 // This library is free software. Distribution and use rights are outlined in
7 // the file "COPYING" which should have been included with this file.  If this
8 // file is missing or damaged, see the license at:
9 //
10 //     https://www.fltk.org/COPYING.php
11 //
12 // Please see the following page on how to report bugs and issues:
13 //
14 //     https://www.fltk.org/bugs.php
15 //
16 
17 /*
18   General information on directory structure and file handling.
19 
20   The "classic" autotools/make system creates executables in their source
21   folders, i.e. fluid/fluid, test/demo and test/xyz, resp.. The menu file is
22   in folder test/, as is the main demo(.exe) program. In the following text
23   and directory lists all test and demo executables are represented by "demo"
24   and the fluid executable by "fluid", no matter what OS (under Windows: *.exe).
25 
26   The CMake build system generates all executables in the build tree and copies
27   the supporting test data files to the build tree as well. This structure is
28   different and needs to be handled separately in this program.
29 
30   Additionally, different OS platforms create different types of files, for
31   instance "app bundles" on macOS. All this needs to be considered.
32 
33   The overall structure, relative to the FLTK source dir (fltk) and the build
34   tree (build):
35 
36   (1) Autotools / Make:
37 
38     fltk/fluid              fluid (../fluid/fluid)
39     fltk/test               demo, demo.menu, working directory, data files
40     fltk/test/images        images for help_dialog(.html)
41 
42   (2) CMake + make (e.g. Unix)
43 
44     build/bin               fluid
45     build/bin/test          test and demo programs
46     build/data              demo.menu, working directory, data files
47     build/data/images       images for help_dialog(.html)
48 
49   (3) CMake + Visual Studio (TYPE == build type: Debug, Release, ...)
50 
51     build/bin/TYPE          fluid
52     build/bin/test/TYPE     test and demo programs
53     build/data              demo.menu, working directory, data files
54     build/data/images       images for help_dialog(.html)
55 
56   (4) macOS                 The setup is similar to Windows and Linux:
57                             Makefiles: like (1) or (2)
58                             Xcode: like (3), i.e. similar to VS layout
59 
60   The built executable 'demo' can also be executed with the menu filename
61   as commandline argument. In this case all the support (data) files are
62   expected to be in the same directory as the menu file or relative paths
63   as needed by the test programs, for instance help_dialog which needs
64   help_dialog.html and related image files.
65 */
66 
67 #include <stdio.h>
68 #include <string.h>
69 #include <stdlib.h>
70 #if defined(WIN32) && !defined(__CYGWIN__)
71 #  include <direct.h>
72 #  ifndef __WATCOMC__
73 // Visual C++ 2005 incorrectly displays a warning about the use of POSIX APIs
74 // on Windows, which is supposed to be POSIX compliant...
75 #    define chdir _chdir
76 #    define putenv _putenv
77 #  endif // !__WATCOMC__
78 #elif defined __APPLE__
79 #include <ApplicationServices/ApplicationServices.h>
80 #include <unistd.h> // for chdir()
81 #include <stdio.h>
82 #include <stdlib.h> // for system()
83 #include <string.h>
84 #else
85 #  include <unistd.h>
86 #endif
87 #include <errno.h>
88 
89 #include <FL/Fl.H>
90 #include <FL/Fl_Double_Window.H>
91 #include <FL/Fl_Box.H>
92 #include <FL/Fl_Button.H>
93 #include <FL/Fl_Choice.H>
94 #include <FL/filename.H>
95 #include <FL/platform.H>
96 #include <FL/fl_ask.H>  // fl_alert()
97 #include <FL/fl_utf8.h> // fl_getcwd()
98 
99 #define FORM_W 350
100 #define FORM_H 440
101 
102 /* Define a macro to decide if a trailing 'd' needs to be removed
103    from the executable file name. Current versions of Visual Studio
104    bundled IDE solutions add a 'd' to the executable file name
105    ('demod.exe') in Debug configurations that needs to be removed.
106    This is no longer true with CMake-generated IDE's starting with
107    FLTK 1.3.6, but in FLTK 1.3.x the OLD behavior is still used if
108    the provided Visual Studio IDE projects are used.
109 */
110 
111 #if defined(_MSC_VER) && defined(_DEBUG) && !defined(CMAKE_INTDIR)
112 # define DEBUG_EXE_WITH_D 1
113 #else
114 # define DEBUG_EXE_WITH_D 0
115 #endif
116 
117 /* The form description */
118 
119 void doexit(Fl_Widget *, void *);
120 void doback(Fl_Widget *, void *);
121 void dobut(Fl_Widget *, long);
doscheme(Fl_Choice * c,void *)122 void doscheme(Fl_Choice *c, void *) {
123   Fl::scheme(c->text(c->value()));
124 }
125 
126 Fl_Double_Window *form = 0;
127 Fl_Group *demogrp = 0;
128 Fl_Button *but[9];
129 
130 // Allocate space to edit commands and arguments from demo.menu.
131 // We "trust demo.menu" that strings don't overflow
132 
133 char cmdbuf[256];           // commandline w/o arguments
134 char params[256];           // commandline arguments
135 
136 // Global path variables for all platforms and build systems
137 // to avoid duplication and dynamic allocation
138 
139 char app_path   [FL_PATH_MAX];          // directory of all demo binaries
140 char fluid_path [FL_PATH_MAX];          // binary directory of fluid
141 char data_path  [FL_PATH_MAX];          // working directory of all demos
142 char command    [2 * FL_PATH_MAX + 40]; // command to be executed
143 
144 // platform specific suffix for executable files
145 
146 #ifdef _WIN32
147 # if DEBUG_EXE_WITH_D
148 const char* suffix = "d.exe";		// exe name with trailing 'd'
149 # else
150 const char* suffix = ".exe";		// exe name w/o trailing 'd'
151 # endif
152 #elif defined __APPLE__
153 const char *suffix = ".app";
154 #else
155 const char *suffix = "";
156 #endif
157 
158 // CMake defines the "build type" subdirectory for multi configuration
159 // build setups like Visual Studio and Xcode
160 
161 #ifdef CMAKE_INTDIR
162 const char *cmake_intdir = "/" CMAKE_INTDIR;
163 #else
164 const char *cmake_intdir = 0;
165 #endif
166 
167 // debug output function
debug_var(const char * varname,const char * value)168 void debug_var(const char *varname, const char *value) {
169   printf("%-10s = %s\n", varname, value);
170   fflush(stdout); // Windows needs that
171 }
172 
create_the_forms()173 void create_the_forms() {
174   Fl_Widget *obj;
175   form = new Fl_Double_Window(FORM_W,FORM_H);
176   form->size_range(FORM_W,FORM_H,FORM_W+1,FORM_H+1); // XXX: +1 needed or window can't be made resizable later
177   // Parent group for demo
178   demogrp = new Fl_Group(0,0,FORM_W,FORM_H);
179   demogrp->resizable(0);
180   demogrp->begin();
181   // Demo
182   obj = new Fl_Box(FL_FRAME_BOX,10,15,330,40,"FLTK Demonstration");
183   obj->color(FL_GRAY-4);
184   obj->labelsize(24);
185   obj->labelfont(FL_BOLD);
186   obj->labeltype(FL_ENGRAVED_LABEL);
187   obj = new Fl_Box(FL_FRAME_BOX,10,65,330,330,0);
188   obj->color(FL_GRAY-8);
189   obj = new Fl_Button(280,405,60,25,"Exit");
190   obj->callback(doexit);
191   Fl_Choice *choice = new Fl_Choice(75, 405, 100, 25, "Scheme:");
192   choice->labelfont(FL_HELVETICA_BOLD);
193   choice->add("none");
194   choice->add("gtk+");
195   choice->add("gleam");
196   choice->add("plastic");
197   choice->callback((Fl_Callback *)doscheme);
198   Fl::scheme(NULL);
199   if (!Fl::scheme()) choice->value(0);
200   else if (!strcmp(Fl::scheme(), "gtk+")) choice->value(1);
201   else if (!strcmp(Fl::scheme(), "gleam")) choice->value(2);
202   else if (!strcmp(Fl::scheme(), "plastic")) choice->value(3);
203   else choice->value(0);
204   obj = new Fl_Button(10,15,330,380); obj->type(FL_HIDDEN_BUTTON);
205   obj->callback(doback);
206   obj = but[0] = new Fl_Button( 30, 85,90,90);
207   obj = but[1] = new Fl_Button(130, 85,90,90);
208   obj = but[2] = new Fl_Button(230, 85,90,90);
209   obj = but[3] = new Fl_Button( 30,185,90,90);
210   obj = but[4] = new Fl_Button(130,185,90,90);
211   obj = but[5] = new Fl_Button(230,185,90,90);
212   obj = but[6] = new Fl_Button( 30,285,90,90);
213   obj = but[7] = new Fl_Button(130,285,90,90);
214   obj = but[8] = new Fl_Button(230,285,90,90);
215   for (int i=0; i<9; i++) {
216     but[i]->align(FL_ALIGN_WRAP);
217     but[i]->callback(dobut, i);
218   }
219   demogrp->end();
220   // End window
221   form->end();
222   form->resizable(form);
223 }
224 
225 /* Maintaining and building up the menus. */
226 
227 typedef struct {
228   char name[64];
229   int numb;
230   char iname[9][64];
231   char icommand[9][64];
232 } MENU;
233 
234 #define MAXMENU 32
235 
236 MENU menus[MAXMENU];
237 int mennumb = 0;
238 
239 /* Return the number of a given menu name. */
find_menu(const char * nnn)240 int find_menu(const char* nnn) {
241   int i;
242   for (i=0; i<mennumb; i++)
243     if (strcmp(menus[i].name,nnn) == 0) return i;
244   return -1;
245 }
246 
247 /* Create a new menu with name nnn */
create_menu(const char * nnn)248 void create_menu(const char* nnn) {
249   if (mennumb == MAXMENU -1) return;
250   strcpy(menus[mennumb].name,nnn);
251   menus[mennumb].numb = 0;
252   mennumb++;
253 }
254 
255 /* Add an item to a menu */
addto_menu(const char * men,const char * item,const char * comm)256 void addto_menu(const char* men, const char* item, const char* comm) {
257   int n = find_menu(men);
258   if (n<0) { create_menu(men); n = find_menu(men); }
259   if (menus[n].numb == 9) return;
260   strcpy(menus[n].iname[menus[n].numb],item);
261   strcpy(menus[n].icommand[menus[n].numb],comm);
262   menus[n].numb++;
263 }
264 
265 /* Button to Item conversion and back. */
266 
267 int b2n[][9] = {
268         { -1, -1, -1, -1,  0, -1, -1, -1, -1},
269         { -1, -1, -1,  0, -1,  1, -1, -1, -1},
270         {  0, -1, -1, -1,  1, -1, -1, -1,  2},
271         {  0, -1,  1, -1, -1, -1,  2, -1,  3},
272         {  0, -1,  1, -1,  2, -1,  3, -1,  4},
273         {  0, -1,  1,  2, -1,  3,  4, -1,  5},
274         {  0, -1,  1,  2,  3,  4,  5, -1,  6},
275         {  0,  1,  2,  3, -1,  4,  5,  6,  7},
276         {  0,  1,  2,  3,  4,  5,  6,  7,  8}
277 };
278 int n2b[][9] = {
279         {  4, -1, -1, -1, -1, -1, -1, -1, -1},
280         {  3,  5, -1, -1, -1, -1, -1, -1, -1},
281         {  0,  4,  8, -1, -1, -1, -1, -1, -1},
282         {  0,  2,  6,  8, -1, -1, -1, -1, -1},
283         {  0,  2,  4,  6,  8, -1, -1, -1, -1},
284         {  0,  2,  3,  5,  6,  8, -1, -1, -1},
285         {  0,  2,  3,  4,  5,  6,  8, -1, -1},
286         {  0,  1,  2,  3,  5,  6,  7,  8, -1},
287         {  0,  1,  2,  3,  4,  5,  6,  7,  8}
288 };
289 
290 /* Transform a button number to an item number when there are
291   maxnumb items in total. -1 if the button should not exist. */
but2numb(int bnumb,int maxnumb)292 int but2numb(int bnumb, int maxnumb)
293 { return b2n[maxnumb][bnumb]; }
294 
295 /* Transform an item number to a button number when there are
296   maxnumb items in total. -1 if the item should not exist. */
numb2but(int inumb,int maxnumb)297 int numb2but(int inumb, int maxnumb)
298 { return n2b[maxnumb][inumb]; }
299 
300 /* Pushing and Popping menus */
301 
302 char stack[64][32];
303 int stsize = 0;
304 
305 /* Push a menu to be visible */
push_menu(const char * nnn)306 void push_menu(const char* nnn) {
307   int n,i,bn;
308   int men = find_menu(nnn);
309   if (men < 0) return;
310   n = menus[men].numb;
311   for (i=0; i<9; i++) but[i]->hide();
312   for (i=0; i<n; i++)
313   {
314     bn = numb2but(i,n-1);
315     but[bn]->show();
316     but[bn]->label(menus[men].iname[i]);
317     if (menus[men].icommand[i][0] != '@') but[bn]->tooltip(menus[men].icommand[i]);
318     else but[bn]->tooltip(0);
319   }
320   if (stack[stsize]!=nnn)
321     strcpy(stack[stsize],nnn);
322   stsize++;
323 }
324 
325 /* Pop a menu */
pop_menu()326 void pop_menu() {
327   if (stsize<=1) return;
328   stsize -= 2;
329   push_menu(stack[stsize]);
330 }
331 
332 /* The callback Routines */
333 
334 /* Handle a button push */
dobut(Fl_Widget *,long arg)335 void dobut(Fl_Widget *, long arg) {
336   int men = find_menu(stack[stsize-1]);
337   int n = menus[men].numb;
338   int bn = but2numb( (int) arg, n-1);
339 
340   // menu ?
341 
342   if (menus[men].icommand[bn][0] == '@') {
343     push_menu(menus[men].icommand[bn]);
344     return;
345   }
346 
347   // not a menu: run test/demo/fluid executable
348   // find and separate "command" and "params"
349 
350   // skip leading spaces in command
351   char *start_command = menus[men].icommand[bn];
352   while (*start_command == ' ') ++start_command;
353 
354   strcpy(cmdbuf, start_command); // here still full command w/params
355 
356   // find the space between the command and parameters if one exists
357   char *start_params = strchr(cmdbuf, ' ');
358   if (start_params) {
359     *start_params = '\0';         // terminate command
360     start_params++;               // skip space
361     strcpy(params, start_params); // copy parameters
362   } else {
363     params[0] = '\0';             // empty string
364   }
365 
366   // select application path: either app_path or fluid_path
367 
368   const char *path = app_path;
369   if (!strncmp(cmdbuf, "fluid", 5))
370     path = fluid_path;
371 
372   // format commandline with optional parameters
373 
374 #if defined(__APPLE__) // macOS
375 
376   if (params[0]) {
377     // we assume that we have only one argument which is a filename in 'data_path'
378     sprintf(command, "open '%s/%s%s' --args '%s/%s'", path, cmdbuf, suffix, data_path, params);
379   } else {
380     sprintf(command, "open '%s/%s%s'", path, cmdbuf, suffix);
381   }
382 
383 #else // other platforms
384 
385   if (params[0])
386     sprintf(command, "%s/%s%s %s", path, cmdbuf, suffix, params);
387   else
388     sprintf(command, "%s/%s%s", path, cmdbuf, suffix);
389 
390 #endif
391 
392   // finally, execute program (the system specific part)
393 
394 #ifdef _WIN32
395 
396   STARTUPINFO         suInfo;         // Process startup information
397   PROCESS_INFORMATION prInfo;         // Process information
398 
399   memset(&suInfo, 0, sizeof(suInfo));
400   suInfo.cb = sizeof(suInfo);
401 
402   debug_var("Command", command);
403 
404   BOOL stat = CreateProcess(NULL, command, NULL, NULL, FALSE,
405                             NORMAL_PRIORITY_CLASS, NULL,
406                             NULL, &suInfo, &prInfo);
407   if (!stat) {
408     DWORD err = GetLastError();
409     fl_alert("Error starting process, error #%lu\n'%s'", err, command);
410   }
411 
412 #elif defined __APPLE__
413 
414   debug_var("Command", command);
415 
416   system(command);
417 
418 #else // other platforms (Unix, Linux)
419 
420   strcat(command, " &"); // run in background
421 
422   debug_var("Command", command);
423 
424   if (system(command) == -1) {
425     fl_alert("Could not start program, errno = %d\n'%s'", errno, command);
426   }
427 
428 #endif // _WIN32
429 
430 }
431 
doback(Fl_Widget *,void *)432 void doback(Fl_Widget *, void *) {pop_menu();}
433 
doexit(Fl_Widget *,void *)434 void doexit(Fl_Widget *, void *) {exit(0);}
435 
436 /*
437   Load the menu file. Returns whether successful.
438 */
load_the_menu(const char * const menu)439 int load_the_menu(const char * const menu) {
440   FILE *fin = 0;
441   char line[256], mname[64],iname[64],cname[64];
442   int i, j;
443 
444   fin = fl_fopen(menu, "r");
445 
446   if (fin == NULL)
447     return 0;
448 
449   for (;;) {
450     if (fgets(line,256,fin) == NULL) break;
451     // remove all carriage returns that Cygwin may have inserted
452     char *s = line, *d = line;
453     for (;;++d) {
454       while (*s=='\r') s++;
455       *d = *s++;
456       if (!*d) break;
457     }
458     // interpret the line
459     j = 0; i = 0;
460     while (line[i] == ' ' || line[i] == '\t') i++;
461     if (line[i] == '\n') continue;
462     if (line[i] == '#') continue;
463     while (line[i] != ':' && line[i] != '\n') mname[j++] = line[i++];
464     mname[j] = '\0';
465     if (line[i] == ':') i++;
466     j = 0;
467     while (line[i] != ':' && line[i] != '\n') {
468       if (line[i] == '\\') {
469         i++;
470         if (line[i] == 'n') iname[j++] = '\n';
471         else iname[j++] = line[i];
472         i++;
473       } else
474         iname[j++] = line[i++];
475     }
476     iname[j] = '\0';
477     if (line[i] == ':') i++;
478     j = 0;
479     while (line[i] != ':' && line[i] != '\n') cname[j++] = line[i++];
480     cname[j] = '\0';
481     addto_menu(mname,iname,cname);
482   }
483   fclose(fin);
484   return 1;
485 }
486 
487 // Fix '\' in Windows paths (convert to '/') and cut off filename (optional, default)
fix_path(char * path,int strip_filename=1)488 void fix_path(char *path, int strip_filename = 1) {
489   if (!path[0])
490     return;
491 #ifdef _WIN32 // convert '\' to '/'
492   char *p = path;
493   while (*p) {
494     if (*p == '\\')
495       *p = '/';
496     p++;
497   }
498 #endif // _WIN32
499   if (strip_filename) {
500     char *pos = strrchr(path, '/');
501     if (pos)
502       *pos = 0;
503   }
504 }
505 
main(int argc,char ** argv)506 int main(int argc, char **argv) {
507 
508   putenv((char *)"FLTK_DOCDIR=../documentation/html"); // used by fluid
509 
510   char menu[FL_PATH_MAX];
511 
512   // construct app_path for all executable files
513 
514   fl_filename_absolute(app_path, sizeof(app_path), argv[0]);
515 #ifdef __APPLE__
516     char *q = strstr(app_path, "/Contents/MacOS/");
517     if (q) *q = 0;
518 #endif
519   fix_path(app_path);
520 
521   // fluid's path is relative to app_path:
522   // - "../fluid" for autoconf/make builds
523   // - ".." (parent directory) for single configuration CMake builds
524   // - "../../$CMAKE_INTDIR" for multi-config (Visual Studio or Xcode) CMake builds
525 
526   strcpy(fluid_path, app_path);
527 
528   if (cmake_intdir)
529     fix_path(fluid_path); // remove intermediate (build type) folder, e.g. "/Debug"
530 
531   fix_path(fluid_path); // remove folder name ("test")
532 
533 #if !defined(GENERATED_BY_CMAKE)
534   strcat(fluid_path, "/fluid");
535 #else
536   // CMake: potentially Visual Studio or Xcode (multi config)
537   if (cmake_intdir)
538     strcat(fluid_path, cmake_intdir); // append e.g. "/Debug"
539 
540 #endif // GENERATED_BY_CMAKE
541 
542   // construct data_path for the menu file and all resources (data files)
543   // CMake: replace "/bin/test/*" with "/data"
544   // autotools: use app_path directly
545 
546   strcpy(data_path, app_path);
547 
548 #if defined(GENERATED_BY_CMAKE)
549   {
550     char *pos = strstr(data_path, "/bin/test");
551     if (pos)
552       strcpy(pos, "/data");
553   }
554 #endif // GENERATED_BY_CMAKE
555 
556   // Construct the menu file name, optionally overridden by command args.
557   // Use data_path and append "/<exe-file-name>.menu"
558 
559   const char *fn = fl_filename_name(argv[0]);
560   strcpy(menu, data_path);
561   strcat(menu, "/");
562   strcat(menu, fn);
563 #if DEBUG_EXE_WITH_D
564   char* dexe_found = strstr(menu, "d.exe");
565   if (dexe_found) strcpy(dexe_found, ".exe"); // strip trailing 'd'
566 #endif
567   fl_filename_setext(menu, sizeof(menu), ".menu");
568 
569   // parse commandline
570 
571   int i = 0;
572   if (!Fl::args(argc, argv, i) || i < argc-1)
573     Fl::fatal("Usage: %s <switches> <menufile>\n%s", argv[0], Fl::help);
574   if (i < argc) {
575     // override menu file *and* data path !
576     fl_filename_absolute(menu, sizeof(menu), (const char *)argv[i]);
577     strcpy(data_path, menu);
578     fix_path(data_path);
579   }
580 
581   // set current work directory to 'data_path'
582 
583   if (chdir(data_path) == -1) { /* ignore */ }
584 
585   // Create forms first
586   create_the_forms();
587 
588   {
589     char cwd[1024];
590     fl_getcwd(cwd, sizeof(cwd));
591     fix_path(cwd, 0);
592 
593     debug_var("app_path",   app_path);
594     debug_var("fluid_path", fluid_path);
595     debug_var("data_path",  data_path);
596     debug_var("menu file",  menu);
597     debug_var("cwd",        cwd);
598     printf("\n");
599     fflush(stdout);
600   }
601 
602   if (!load_the_menu(menu))
603     Fl::fatal("Can't open menu file '%s'", menu);
604 
605   push_menu("@main");
606   form->show(argc, argv);
607   Fl::run();
608   return 0;
609 }
610