]> Shamusworld >> Repos - apple2/blob - src/gui/gui.cpp
5bbd85e24eb9e778f5b8d6cd8d45a2f6b23f49c4
[apple2] / src / gui / gui.cpp
1 //
2 // GUI.CPP
3 //
4 // Graphical User Interface support
5 // by James L. Hammons
6 //
7 // JLH = James L. Hammons <jlhamm@acm.org>
8 //
9 // WHO  WHEN        WHAT
10 // ---  ----------  ------------------------------------------------------------
11 // JLH  02/03/2006  Created this file
12 // JLH  03/13/2006  Added functions to allow shutting down GUI externally
13 // JLH  03/22/2006  Finalized basic multiple window support
14 //
15 // STILL TO DO:
16 //
17 // - Memory leak on quitting with a window active [DONE]
18 // - Multiple window handling [DONE]
19 //
20
21 #include "gui.h"
22 #include "menu.h"                                                               // Element class methods are pulled in here...
23 #include "window.h"
24 #include "button.h"
25 #include "text.h"
26 #include "diskwindow.h"
27 #include "video.h"
28 #include "apple2.h"
29
30 // Debug support
31 //#define DEBUG_MAIN_LOOP
32
33 // New main screen buffering
34 // This works, but the colors are rendered incorrectly. Also, it seems that there's
35 // fullscreen blitting still going on--dragging the disk is fast at first but then
36 // gets painfully slow. Not sure what's going on there.
37 #define USE_NEW_MAINBUFFERING
38
39 //#ifdef DEBUG_MAIN_LOOP
40 #include "log.h"
41 //#endif
42
43 /*
44 Work flow: Draw floppy drive.
45 If disk in drive, MO shows eject graphic, otherwise show load graphic.
46 If hit 'new blank image':
47         If disk in drive, ask if want to save if modified
48         else, load it
49 If hit 'swap disks', swap disks.
50 */
51
52
53 GUI::GUI(SDL_Surface * surface): menuItem(new MenuItems())
54 {
55         Element::SetScreen(surface);
56 //      windowList.push_back(new Menu());
57
58 // Create drive windows, and config windows here...
59         windowList.push_back(new Window(30, 30, 200, 100));
60         windowList.push_back(new Window(30, 140, 200, 100));
61         windowList.push_back(new Button(30, 250, "Click!"));
62         windowList.push_back(new Text(30, 20, floppyDrive.GetImageName(0)));
63         windowList.push_back(new Text(30, 130, floppyDrive.GetImageName(1)));
64         windowList.push_back(new DiskWindow(&floppyDrive, 240, 20));
65 }
66
67 GUI::~GUI()
68 {
69         // Clean up menuItem, if any
70
71         if (menuItem)
72                 delete menuItem;
73
74         // Clean up the rest
75
76         for(std::list<Element *>::iterator i=windowList.begin(); i!=windowList.end(); i++)
77                 if (*i)
78                         delete *i;
79 }
80
81 void GUI::AddMenuTitle(const char * title)
82 {
83         menuItem->title = title;
84         menuItem->item.clear();
85 }
86
87 void GUI::AddMenuItem(const char * item, Element * (* a)(void)/*= NULL*/, SDLKey k/*= SDLK_UNKNOWN*/)
88 {
89         menuItem->item.push_back(NameAction(item, a, k));
90 }
91
92 void GUI::CommitItemsToMenu(void)
93 {
94 //We could just do a simple check here to see if more than one item is in the list,
95 //and if so fail. Make it so you build the menu first before allowing any other action. [DONE]
96
97 //Right now, we just silently fail...
98         if (windowList.size() > 1)
99         {
100                 WriteLog("GUI: Can't find menu--more than one item in windowList!\n");
101                 return;
102         }
103
104         ((Menu *)(*windowList.begin()))->Add(*menuItem);
105 }
106
107 void GUI::Run(void)
108 {
109         exitGUI = false;
110         showMouse = true;
111         SDL_Event event;
112         std::list<Element *>::iterator i;
113
114         SDL_EnableKeyRepeat(150, 75);
115
116         // Also: Need to pick up backbuffer (for those windows that have them)
117         //       BEFORE drawing...
118
119         // Initial update... [Now handled correctly in the constructor]
120         // Uh, still needed here, though... Only makes sense that it should
121         for(i=windowList.begin(); i!=windowList.end(); i++)
122                 (*i)->Draw();
123
124 #ifndef USE_NEW_MAINBUFFERING
125         RenderScreenBuffer();
126 #else
127         FlipMainScreen();
128 #endif
129
130         // Main loop
131         while (!exitGUI)
132         {
133 //              if (SDL_PollEvent(&event))
134                 if (SDL_WaitEvent(&event))
135                 {
136 #ifdef DEBUG_MAIN_LOOP
137 WriteLog("An event was found!");
138 #endif
139                         if (event.type == SDL_USEREVENT)
140                         {
141 #ifdef DEBUG_MAIN_LOOP
142 WriteLog(" -- SDL_USEREVENT\n");
143 #endif
144 //Mebbe add another user event for screen refresh? Why not!
145                                 if (event.user.code == WINDOW_CLOSE)
146                                 {
147                                         for(i=windowList.begin(); i!=windowList.end(); i++)
148                                         {
149                                                 if (*i == (Element *)event.user.data1)
150                                                 {
151                                                         delete *i;
152                                                         windowList.erase(i);
153                                                         break;
154                                                 }
155                                         }
156                                 }
157                                 else if (event.user.code == MENU_ITEM_CHOSEN)
158                                 {
159                                         // Confused? Let me enlighten... What we're doing here is casting
160                                         // data1 as a pointer to a function which returns a Element pointer and
161                                         // which takes no parameters (the "(Element *(*)(void))" part), then
162                                         // derefencing it (the "*" in front of that) in order to call the
163                                         // function that it points to. Clear as mud? Yeah, I hate function
164                                         // pointers too, but what else are you gonna do?
165                                         Element * window = (*(Element *(*)(void))event.user.data1)();
166
167                                         if (window)
168                                                 windowList.push_back(window);
169
170                                         while (SDL_PollEvent(&event));  // Flush the event queue...
171
172                                         event.type = SDL_MOUSEMOTION;
173                                         int mx, my;
174                                         SDL_GetMouseState(&mx, &my);
175                                         event.motion.x = mx, event.motion.y = my;
176                                     SDL_PushEvent(&event);                      // & update mouse position...!
177
178                                         oldMouse.x = mouse.x, oldMouse.y = mouse.y;
179                                         mouse.x = mx, mouse.y = my;             // This prevents "mouse flash"...
180                                 }
181 //There's a *small* problem with the following approach--if a window and a bunch of
182 //child widgets send this message, we'll get a bunch of unnecessary refresh events...
183 //This could be controlled by having the main window refresh itself intelligently...
184
185 //What we could do instead is set a variable in Element and check it after the fact
186 //to see whether or not a refresh is needed.
187 //[This is what we do now.]
188
189 //Dirty rectangle is also possible...
190                                 else if (event.user.code == SCREEN_REFRESH_NEEDED)
191 #ifndef USE_NEW_MAINBUFFERING
192                                         RenderScreenBuffer();
193 #else
194                                         FlipMainScreen();
195 #endif
196                         }
197                         else if (event.type == SDL_ACTIVEEVENT)
198                         {
199 //Need to do a screen refresh here...
200                                 if (event.active.state == SDL_APPMOUSEFOCUS)
201                                         showMouse = (event.active.gain ? true : false);
202
203 #ifndef USE_NEW_MAINBUFFERING
204                                 RenderScreenBuffer();
205 #else
206                                         FlipMainScreen();
207 #endif
208                         }
209                         else if (event.type == SDL_KEYDOWN)
210                         {
211 #ifdef DEBUG_MAIN_LOOP
212 WriteLog(" -- SDL_KEYDOWN\n");
213 #endif
214                                 if (event.key.keysym.sym == SDLK_F1)
215                                         exitGUI = true;
216
217 //Not sure that this is the right way to handle this...
218 //Probably should only give this to the top level window...
219 //                              for(i=windowList.begin(); i!=windowList.end(); i++)
220 //                                      (*i)->HandleKey(event.key.keysym.sym);
221                                 windowList.back()->HandleKey(event.key.keysym.sym);
222                         }
223                         else if (event.type == SDL_MOUSEMOTION)
224                         {
225 #ifdef DEBUG_MAIN_LOOP
226 WriteLog(" -- SDL_MOUSEMOTION\n");
227 #endif
228 //This is for tracking a custom mouse cursor, which we're not doing--YET.
229                                 oldMouse.x = mouse.x, oldMouse.y = mouse.y;
230                                 mouse.x = event.motion.x, mouse.y = event.motion.y;
231
232 //Not sure that this is the right way to handle this...
233 //Right now, we should probably only do mouseover for the last item in the list...
234 //And now we do!
235 //Though, it seems to screw other things up. Maybe it IS better to pass it to all windows?
236 //Or maybe to just the ones that aren't completely obscured?
237 //Probably. Right now, a disk's close button that should be obscured by one sitting on
238 //top of it gets redrawn. Not good. !!! FIX !!!
239                                 for(i=windowList.begin(); i!=windowList.end(); i++)
240                                         (*i)->HandleMouseMove(mouse.x, mouse.y);
241 //                              windowList.back()->HandleMouseMove(mouse.x, mouse.y);
242                         }
243                         else if (event.type == SDL_MOUSEBUTTONDOWN)
244                         {
245 #ifdef DEBUG_MAIN_LOOP
246 WriteLog(" -- SDL_MOUSEBUTTONDOWN\n");
247 #endif
248 //Not sure that this is the right way to handle this...
249 // What we should do here is ensure that whatever has been clicked on gets moved to the
250 // highest priority--in our current data schema that would be the end of the list... !!! FIX !!!
251 //[DONE]
252
253 /*
254
255 We could do the following:
256
257 - Go through list and find which window has been clicked on (if any). If more
258   than one is clicked on, take the one highest in the Z order (closer to the end
259   of the list).
260
261 - If item is highest in Z order, pass click through to window and exit.
262
263 - Otherwise, restore backing store on each window in reverse order.
264
265 - Remove item clicked on from the list. Put removed item at the end of the list.
266
267 - Go through list and pass click through to each window in the list. Also do a
268   blit to backing store and a Draw() for each window.
269
270 Could also do a check (if not clicked on highest Z window) to see which windows
271 it overlaps and just do restore/redraw for those that overlap. To wit:
272
273 - Create new list containing only those windows that overlap the clicking on window.
274
275 - Go through list and do a blit to backing store and a Draw() for each window.
276
277 - Go through list and pass click through to each window in the list.
278
279 */
280
281 #if 0
282 #if 0
283                                 for(i=windowList.begin(); i!=windowList.end(); i++)
284                                         (*i)->HandleMouseButton(event.button.x, event.button.y, true);
285 #else
286 // We use the 1st algorithm here, since it's simpler. If we need to, we can optimize
287 // to the 2nd...
288
289                                 // Walk backward through the list and see if a window was hit.
290                                 // This will automagically return us the window with the highest Z.
291
292                                 std::list<Element *>::reverse_iterator ri;
293                                 std::list<Element *>::iterator hit;// = windowList.end();
294
295                                 for(ri=windowList.rbegin(); ri!=windowList.rend(); ri++)
296                                 {
297                                         if ((*ri)->Inside(event.button.x, event.button.y))
298                                         {
299                                                 // Here's a bit of STL weirdness: Converting from a reverse
300                                                 // iterator to a regular iterator requires backing the iterator
301                                                 // up a position after grabbing it's base() OR going forward
302                                                 // one position with the reverse iterator before grabbing base().
303                                                 // Ugly, but it gets the job done...
304                                                 hit = (++ri).base();
305                                                 // Put it back where we found it, so the tests following this
306                                                 // don't fail...
307                                                 ri--;
308                                                 break;
309                                         }
310                                 }
311
312                                 // If we hit the highest in the list, then pass the event through
313                                 // to the window for handling. if we hit no windows, then pass the
314                                 // event to all windows. Otherwise, we need to shuffle windows.
315
316 //NOTE: We need to pass the click to all windows regardless of whether they're topmost or not...
317                                 if (ri == windowList.rbegin())
318                                 {
319                                         for(i=windowList.begin(); i!=windowList.end(); i++)
320                                                 (*i)->HandleMouseButton(event.button.x, event.button.y, true);
321                                 }
322                                 else if (ri == windowList.rend())
323                                 {
324                                         for(i=windowList.begin(); i!=windowList.end(); i++)
325                                                 (*i)->HandleMouseButton(event.button.x, event.button.y, true);
326                                 }
327                                 else
328                                 {
329 // - Otherwise, restore backing store on each window in reverse order.
330                                         for(ri=windowList.rbegin(); ri!=windowList.rend(); ri++)
331                                                 (*ri)->RestoreScreenFromBackstore();
332                                         // At this point, the screen has been restored...
333
334 // - Remove item clicked on from the list. Put removed item at the end of the list.
335                                         windowList.push_back(*hit);
336                                         windowList.erase(hit);
337 // - Go through list and pass click through to each window in the list. Also do a
338 //  blit to backing store and a Draw() for each window.
339                                         for(i=windowList.begin(); i!= windowList.end(); i++)
340                                         {
341                                                 // Grab bg into backstore
342                                                 (*i)->SaveScreenToBackstore();
343                                                 // Pass click
344                                                 (*i)->HandleMouseButton(event.button.x, event.button.y, true);
345                                                 // Draw?
346                                                 (*i)->Draw();
347                                         }
348                                 }
349 #endif
350 #endif
351 /*
352 A slightly different way to handle this would be to loop through all windows, compare
353 all those above it to see if they obscure it; if so then subdivide it's update rectangle
354 to eliminate drawing the parts that aren't shown. The beauty of this approach is that
355 you don't have to care what order the windows are drawn in and you don't need to worry
356 about the order of restoring the backing store.
357
358 You *do* still need to determine the Z-order of the windows, in order to get the subdivisions
359 correct, but that's not too terrible.
360
361 Also, when doing a window drag, the coverage lists for all windows have to be regenerated.
362 */
363                                 std::list<Element *>::reverse_iterator ri;
364                                 bool movedWindow = false;
365
366                                 for(ri=windowList.rbegin(); ri!=windowList.rend(); ri++)
367                                 {
368                                         if ((*ri)->Inside(event.button.x, event.button.y))
369                                         {
370                                                 // Remove item clicked on from the list & put removed item at the
371                                                 // end of the list, thus putting the window at the top of the Z
372                                                 // order. But IFF window is not already topmost!
373                                                 if (ri != windowList.rbegin())
374                                                 {
375                                                         windowList.push_back(*ri);
376                                                         // Here's a bit of STL weirdness: Converting from a reverse
377                                                         // iterator to a regular iterator requires backing the iterator
378                                                         // up a position after grabbing it's base() OR going forward
379                                                         // one position with the reverse iterator before grabbing base().
380                                                         // Ugly, but it get the job done...
381                                                         windowList.erase((++ri).base());
382                                                         movedWindow = true;
383                                                 }
384
385                                                 break;
386                                         }
387                                 }
388
389 //Small problem here: we should only pass the *hit* to the topmost window and pass
390 //*misses* to everyone else... Otherwise, you can have overlapping draggable windows
391 //and be able to drag both by clicking on a point that intersects both...
392 //(though that may be an interesting way to handle things!)
393 //The thing is that you want to do it on purpose (like with a special grouping widget)
394 //instead of by accident. So, !!! FIX !!!
395                                 // Pass the click on to all windows
396 //                              for(i=windowList.begin(); i!=windowList.end(); i++)
397 //                                      (*i)->HandleMouseButton(event.button.x, event.button.y, true);
398                                 windowList.back()->HandleMouseButton(event.button.x, event.button.y, true);
399
400 //                              // & bail if nothing changed...
401                                 if (movedWindow)
402 //                                      return;
403 {
404                                 // Check for overlap/build coverage lists [O((n^2)/2) algorithm!]
405 //One way to optimize this would be to only reset coverage lists from the point in
406 //the Z order where the previous window was.
407                                 for(i=windowList.begin(); i!=windowList.end(); i++)
408                                 {
409 //One other little quirk: Probably need to clear the backing store as well!
410 //Not sure...
411                                         (*i)->ResetCoverageList();
412
413                                         // This looks odd, but it's just a consequence of iterator weirdness.
414                                         // Otherwise we could just stick a j+1 in the for loop below. :-P
415                                         std::list<Element *>::iterator j = i;
416                                         j++;
417
418                                         for(; j!=windowList.end(); j++)
419                                                 (*i)->AdjustCoverageList((*j)->GetExtents());
420
421 //                                      (*i)->HandleMouseButton(event.button.x, event.button.y, true);
422                                         (*i)->Draw();
423                                 }
424 }
425                         }
426                         else if (event.type == SDL_MOUSEBUTTONUP)
427                         {
428 #ifdef DEBUG_MAIN_LOOP
429 WriteLog(" -- SDL_MOUSEBUTTONUP\n");
430 #endif
431 //Not sure that this is the right way to handle this...
432                                 for(i=windowList.begin(); i!=windowList.end(); i++)
433                                         (*i)->HandleMouseButton(event.button.x, event.button.y, false);
434 //I think we should only do topmost here...
435 //Or should we???
436 //                              windowList.back()->HandleMouseButton(event.button.x, event.button.y, false);
437                         }
438 #ifdef DEBUG_MAIN_LOOP
439 else
440         WriteLog(" -- Unknown event\n");
441 #endif
442
443                         if (Element::ScreenNeedsRefreshing())
444                         {
445 #ifndef USE_NEW_MAINBUFFERING
446 #ifdef DEBUG_MAIN_LOOP
447 WriteLog("Screen refresh called!\n");
448 #endif
449                                 RenderScreenBuffer();
450                                 Element::ScreenWasRefreshed();
451 #else
452                                 FlipMainScreen();
453                                 Element::ScreenWasRefreshed();
454 #endif
455                         }
456                 }
457 //hm. Works, but slows things way down.
458 //Now we use WaitEvents() instead. Yay!
459 //SDL_Delay(10);
460         }
461
462         SDL_EnableKeyRepeat(0, 0);
463 //      return false;
464 }
465
466 void GUI::Stop(void)
467 {
468         exitGUI = true;
469 }