20130427
[gdash.git] / src / framework / app.cpp
bloba9558d6ebed51120494a960375aaeeef0168fd0a
1 /*
2 * Copyright (c) 2007-2013, Czirkos Zoltan http://code.google.com/p/gdash/
4 * Permission is hereby granted, free of charge, to any person obtaining
5 * a copy of this software and associated documentation files (the
6 * "Software"), to deal in the Software without restriction, including
7 * without limitation the rights to use, copy, modify, merge, publish,
8 * distribute, sublicense, and/or sell copies of the Software, and to
9 * permit persons to whom the Software is furnished to do so, subject to
10 * the following conditions:
12 * The above copyright notice and this permission notice shall be
13 * included in all copies or substantial portions of the Software.
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
19 * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
20 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 /**
25 * @defgroup Framework
27 * This group is the documentation of the Activity the App and
28 * the Command class, which together control the graphical presentation
29 * of everything in GDash (except the editor) to the user.
31 * An Activity object is a screen on which the user can do something,
32 * for example play the game, select a file or read about a cave. Most
33 * of the activities fill the whole screen. (Some of them are small
34 * windows centered on the screen, but still they are modal.) An
35 * activity receives events from the application, and can organize its
36 * inner workings according to these events. There are keypress, timer,
37 * redraw and other kinds of events.
39 * The activities are contained in an App object which organizes them into
40 * a stack. Every time an event is received from the user or the timer,
41 * the App object forwards the events (maybe after some preprocessing)
42 * to the topmost activity on the stack. New activities are put on top
43 * of the stack, and an activity can quit by requesting the App to
44 * pop it off the top of the stack.
46 * The App object has the responsibility to connect the low level user
47 * interface (screen, keyboard, graphics objects) to the activities.
48 * The main program (which may be an SDL or a GTK implementation)
49 * creates the App object, and translates the operating system and
50 * graphics library specific events to function calls on the App
51 * object. For example, when the user presses the Escape key, the
52 * .keyboard_event() method of the App is to be called; and after that,
53 * the App will forward this event to the topmost activity. If it is
54 * the title screen, the program will presumably quit. If it is the
55 * game, the cave will be restarted. It is always only the topmost
56 * activity, which will receive the events from the app. Those which
57 * are occluded by these do not receive any events, and so they are
58 * stopped.
60 * In order to provide a means of communication between activities,
61 * Command objects can be created. Each activity is allowed to push
62 * as many command objects as it wants into the command queue of the
63 * App from any of its event methods. The App will check the command
64 * queue after calling any of the event methods of the topmost activity,
65 * and execute all commands which are queued.
67 * The starting of the game from the title screen is a typical use of
68 * these commands objects. When the user presses space, his name is asked
69 * for. After that, he can start the game by typing his name and pressing
70 * enter. This can be implemented by the TitleScreenActivity, the
71 * InputTextActivity and the GameActivity classes.
72 * - First, the TitleScreenActivity creates the InputTextActivity.
73 * - The InputTextActivity does not know, what it should do with the
74 * text the user typed.
75 * - However, it is also given a NewGameCommand, which is a command
76 * of one string parameter. When this command is executed, it creates
77 * a GameActivity and gives it to the App to push it onto the stack.
78 * - If the user presses enter after typing his name, the
79 * InputTextActivity is popped from the stack, and the GameActivity
80 * is pushed.
81 * - If the user presses escape, the command is not executed, so after
82 * popping the InputTextActivity nothing else happens. And so the
83 * TitleScreenActivity is on the top again.
85 * The command queue also serves the purpose of preventing object
86 * creation/destruction ordering problems from happening. For example,
87 * in the above scenario, the InputTextActivity object should not be
88 * destructed before setting the player name parameter in the NewGameCommand
89 * and executing the command itself. So activities are also pushed and
90 * popped by means of using PopActivityCommand and PushActivityCommand
91 * commands. The above interaction in detail:
92 * - The user presses enter after typing his name.
93 * - The NewGameCommand is parametrized with the name of the user.
94 * - A PopActivityCommand is enqueued in the App.
95 * - The NewGameCommand is also enqueued.
96 * - The App checks the command queue.
97 * - First, it executes the PopActivityCommand, so the InputTextActivity
98 * is destructed. (The name and starting cave parameters are not lost,
99 * because they are in the NewGameCommand.)
100 * - The NewGameCommand is executed. It creates the GameActivity and
101 * pushes it onto the top of the activity stack.
102 * - The game runs.
104 * The App class has some methods which can be called for common tasks like
105 * selecting a file or asking a user a yes or no question. By default, these
106 * methods push the corresponding activity. However, they can be overridden
107 * by inheriting from the App, to provide the user with toolkit-specific
108 * dialog boxes, for example GTK+ file open dialogs or message dialogs.
109 * Activities should use these methods to perform these tasks instead of
110 * hard-coding anything.
113 #include "config.h"
115 #include <algorithm>
116 #include <glib/gi18n.h>
118 #include "framework/app.hpp"
120 #include "framework/commands.hpp"
121 #include "gfx/pixbuf.hpp"
122 #include "gfx/fontmanager.hpp"
123 #include "gfx/pixbuffactory.hpp"
124 #include "gfx/screen.hpp"
125 #include "input/gameinputhandler.hpp"
127 #include "misc/about.hpp"
128 #include "misc/helptext.hpp"
129 #include "sound/sound.hpp"
130 #include "settings.hpp"
132 #include "framework/selectfileactivity.hpp"
133 #include "framework/askyesnoactivity.hpp"
134 #include "framework/showtextactivity.hpp"
135 #include "framework/inputtextactivity.hpp"
136 #include "framework/settingsactivity.hpp"
137 #include "framework/messageactivity.hpp"
138 #ifdef HAVE_SDL
139 #include "framework/volumeactivity.hpp"
140 #endif
142 #include "gamebackground.cpp"
144 std::string help_strings_to_string(char const **strings) {
145 std::string s;
146 for (int n = 0; strings[n] != NULL; n += 2)
147 s += SPrintf("%c%s %c%s\n") % GD_COLOR_INDEX_YELLOW % strings[n] % GD_COLOR_INDEX_LIGHTBLUE % strings[n + 1];
148 return s;
152 static std::string help_text_to_string(helpdata const help_text[]) {
153 std::string s = SPrintf("%c") % GD_COLOR_INDEX_LIGHTBLUE;
154 for (int i = 0; g_strcmp0(help_text[i].stock_id, HELP_LAST_LINE) != 0; ++i) {
155 GdElementEnum element = help_text[i].element;
156 if (element != O_NONE) {
157 // pixbuf missing here
158 if (help_text[i].heading == NULL) {
159 /* add element name only if no other text given */
160 s += SPrintf("%c%s%c\n") % GD_COLOR_INDEX_YELLOW % visible_name_no_attribute(element);
163 if (help_text[i].heading) {
164 /* some words in big letters */
165 s += SPrintf("%c%s%c\n") % GD_COLOR_INDEX_YELLOW % _(help_text[i].heading);
167 if (help_text[i].keyname)
168 s += SPrintf(" %c%s\t") % GD_COLOR_INDEX_YELLOW % _(help_text[i].keyname);
169 if (help_text[i].description)
170 s += SPrintf("%c%s\n") % GD_COLOR_INDEX_LIGHTBLUE % _(help_text[i].description);
171 s += "\n";
173 return s;
177 App::App(Screen &screenref)
178 : PixmapStorage(screenref),
179 screen(&screenref) {
180 font_manager = NULL;
181 gameinput = NULL;
182 caveset = NULL;
183 background_image = NULL;
187 App::~App() {
188 pop_all_activities();
189 gd_music_stop();
190 release_pixmaps();
191 delete gameinput;
192 delete font_manager;
196 void App::release_pixmaps() {
197 delete background_image;
198 background_image = NULL;
202 void App::set_no_activity_command(SmartPtr<Command> command) {
203 no_activity_command = command;
207 void App::set_quit_event_command(SmartPtr<Command> command) {
208 quit_event_command = command;
212 void App::set_request_restart_command(SmartPtr<Command> command) {
213 request_restart_command = command;
217 void App::set_start_editor_command(SmartPtr<Command> command) {
218 start_editor_command = command;
222 void App::clear_screen() {
223 /* create if needed */
224 if (!background_image) {
225 Pixbuf *pixbuf = screen->pixbuf_factory.create_from_inline(sizeof(gamebackground), gamebackground);
226 background_image = screen->create_scaled_pixmap_from_pixbuf(*pixbuf, false);
227 delete pixbuf;
229 /* tile */
230 for (int y = 0; y < screen->get_height(); y += background_image->get_height())
231 for (int x = 0; x < screen->get_width(); x += background_image->get_width())
232 screen->blit(*background_image, x, y);
236 void App::draw_window(int rx, int ry, int rw, int rh) {
237 /* if we have a "redraw all", we cannot build on the fact that the
238 * background stayed. so rather clear the screen befure drawing a window */
239 if (screen->must_redraw_all_before_flip())
240 clear_screen();
242 int const size = screen->get_pixmap_scale();
243 screen->fill_rect(rx - 2 * size, ry - 2 * size, rw + 4 * size, rh + 4 * size, GdColor::from_rgb(64, 64, 64));
244 screen->fill_rect(rx - 1 * size, ry - 1 * size, rw + 2 * size, rh + 2 * size, GdColor::from_rgb(128, 128, 128));
245 screen->fill_rect(rx, ry, rw, rh, GdColor::from_rgb(0, 0, 0));
249 void App::draw_scrollbar(int min, int current, int max) {
250 /* positions in char heights */
251 const int upper = 3, lower = 3;
252 /* if nothing to scroll, return immediately. */
253 if (max <= min)
254 return;
255 set_color(GD_GDASH_GRAY2);
256 /* bar */
257 screen->fill_rect(screen->get_width() - font_manager->get_font_width_narrow(),
258 upper * font_manager->get_font_height(),
259 font_manager->get_font_width_narrow(),
260 screen->get_height() - (upper+lower) * font_manager->get_font_height(),
261 GdColor::from_rgb(0, 0, 0));
262 /* up & down arrow */
263 blittext_n(screen->get_width() - font_manager->get_font_width_narrow(),
264 screen->get_height() - (lower+1) * font_manager->get_font_height(), CPrintf("%c") % GD_DOWN_CHAR);
265 blittext_n(screen->get_width() - font_manager->get_font_width_narrow(),
266 upper * font_manager->get_font_height(), CPrintf("%c") % GD_UP_CHAR);
267 /* slider */
268 double pos = current / double(max-min);
269 pos = pos * (screen->get_height() - font_manager->get_font_height() * (upper+lower+2) - font_manager->get_font_height());
270 pos = pos + font_manager->get_font_height() * (upper+1);
271 blittext_n(screen->get_width() - font_manager->get_font_width_narrow(),
272 pos, CPrintf("%c") % GD_FULL_BOX_CHAR);
276 void App::select_file_and_do_command(const char *title, const char *start_dir, const char *glob, bool for_save, const char *defaultname, SmartPtr<Command1Param<std::string> > command_when_successful) {
277 enqueue_command(new PushActivityCommand(this, new SelectFileActivity(this, title, start_dir, glob, for_save, defaultname, command_when_successful)));
281 void App::ask_yesorno_and_do_command(char const *question, const char *yes_answer, char const *no_answer, SmartPtr<Command> command_when_yes, SmartPtr<Command> command_when_no) {
282 enqueue_command(new PushActivityCommand(this, new AskYesNoActivity(this, question, yes_answer, no_answer, command_when_yes, command_when_no)));
285 void App::show_text_and_do_command(char const *title_line, std::string const &text, SmartPtr<Command> command_after_exit) {
286 enqueue_command(new PushActivityCommand(this, new ShowTextActivity(this, title_line, text, command_after_exit)));
290 void App::show_settings(Setting *settings) {
291 enqueue_command(new PushActivityCommand(this, new SettingsActivity(this, settings)));
294 void App::show_message(std::string const &primary, std::string const &secondary, SmartPtr<Command> command_after_exit) {
295 enqueue_command(new PushActivityCommand(this, new MessageActivity(this, primary, secondary, command_after_exit)));
298 void App::show_help(helpdata const help_text[]) {
299 // TRANSLATORS: 'H' uppercase because of title capitalization in English.
300 show_text_and_do_command("GDash Help", help_text_to_string(help_text));
303 void App::show_about_info() {
304 // TRANSLATORS: about dialog box categories.
305 struct String {
306 char const *title;
307 char const *text;
308 } strings[] = {
309 { _("About GDash"), _(About::comments) },
310 { _("Copyright"), _(About::copyright) },
311 { _("License"), _(About::license) },
312 { _("Website"), _(About::website) },
313 { NULL, NULL }
315 // TRANSLATORS: about dialog box categories.
316 struct StringArray {
317 char const *title;
318 char const **texts;
319 } stringarrays[] = {
320 { _("Authors"), About::authors },
321 { _("Artists"), About::artists },
322 { _("Documenters"), About::documenters },
323 { NULL, NULL }
325 std::string text;
327 for (String *s = strings; s->title != NULL; ++s) {
328 text += SPrintf("%c%s\n%c%s\n\n") % GD_COLOR_INDEX_YELLOW % s->title % GD_COLOR_INDEX_LIGHTBLUE % s->text;
330 for (StringArray *s = stringarrays; s->title != NULL; ++s) {
331 text += SPrintf("%c%s%c\n") % GD_COLOR_INDEX_YELLOW % s->title % GD_COLOR_INDEX_LIGHTBLUE;
332 for (char const **t = s->texts; *t != NULL; ++t) {
333 text += SPrintf("%c%c %c%s\n") % GD_COLOR_INDEX_YELLOW % GD_PLAYER_CHAR % GD_COLOR_INDEX_LIGHTBLUE % *t;
335 text += '\n';
338 show_text_and_do_command(PACKAGE_NAME PACKAGE_VERSION, text);
342 void App::input_text_and_do_command(char const *title_line, char const *default_text, SmartPtr<Command1Param<std::string> > command_when_successful) {
343 enqueue_command(new PushActivityCommand(this, new InputTextActivity(this, title_line, default_text, command_when_successful)));
347 /** select color for the next text drawing */
348 void App::set_color(const GdColor &color) {
349 font_manager->set_color(color);
353 /** write something to the screen, with normal characters.
354 * x=-1 -> center horizontally */
355 int App::blittext_n(int x, int y, const char *text) {
356 return font_manager->blittext_n(x, y, text);
360 /** set status line (the last line in the screen) to text */
361 void App::status_line(const char *text) {
362 font_manager->blittext_n(-1, screen->get_height() - font_manager->get_font_height(), GD_GDASH_GRAY2, text);
366 /** set title line (the first line in the screen) to text */
367 void App::title_line(const char *text) {
368 font_manager->blittext_n(-1, 0, GD_GDASH_WHITE, text);
372 void App::timer_event(int ms_elapsed) {
373 if (topmost_activity()) {
374 topmost_activity()->timer_event(ms_elapsed);
376 process_commands();
380 void App::timer2_event() {
381 if (topmost_activity()) {
382 topmost_activity()->timer2_event();
383 process_commands();
388 void App::keypress_event(Activity::KeyCode keycode, int gfxlib_keycode) {
389 /* send it to the gameinput object. */
390 gameinput->keypress(gfxlib_keycode);
392 /* and process it. */
393 switch (keycode) {
394 case F9:
395 /* if we have sound, key f9 will push a volume activity.
396 * that activity will receive the keypresses. */
397 #ifdef HAVE_SDL
398 if (gd_sound_enabled && dynamic_cast<VolumeActivity *>(topmost_activity()) == NULL) {
399 enqueue_command(new PushActivityCommand(this, new VolumeActivity(this)));
401 #endif // ifdef HAVE_SDL
402 break;
403 case F10:
404 /* f10 is reserved for the menu in the gtk version. do not pass to the activity,
405 * if we may get this somehow. */
406 break;
407 case F11:
408 /* fullscreen switch. do not pass to the app. */
409 break;
410 default:
411 /* other key. pass to the activity. */
412 if (topmost_activity()) {
413 topmost_activity()->keypress_event(keycode, gfxlib_keycode);
414 process_commands();
416 break;
421 void App::redraw_event(bool full) {
422 Activity *topmost = topmost_activity();
423 if (topmost != NULL) {
424 topmost->redraw_event(full);
425 topmost->redraw_queued = false;
430 void App::quit_event() {
431 if (quit_event_command != NULL)
432 quit_event_command->execute();
436 /* process pending commands */
437 void App::process_commands() {
438 while (!command_queue.empty()) {
439 SmartPtr<Command> command = command_queue.front();
440 command_queue.pop();
441 command->execute();
444 if (running_activities.empty() && no_activity_command != NULL) {
445 no_activity_command->execute();
446 no_activity_command.release(); /* process this one only once! so forget the object. */
451 void App::request_restart() {
452 request_restart_command->execute();
456 void App::start_editor() {
457 start_editor_command->execute();
461 /* enqueue a command for executing after the event.
462 * and empty command is allowed to be pushed, and has no effect. */
463 void App::enqueue_command(SmartPtr<Command> the_command) {
464 if (the_command != NULL)
465 command_queue.push(the_command);
469 void App::push_activity(Activity *the_activity) {
470 /* the currently topmost activity will be hidden by the new one */
471 if (topmost_activity()) {
472 topmost_activity()->hidden_event();
474 /* push the new one to the top */
475 running_activities.push(the_activity);
476 the_activity->pushed_event();
477 the_activity->shown_event();
478 redraw_event(true);
482 void App::pop_activity() {
483 if (!running_activities.empty()) {
484 Activity *popped_activity = running_activities.top();
485 popped_activity->hidden_event();
486 delete running_activities.top();
487 running_activities.pop();
488 /* send a redraw to the topmost one */
489 Activity *topmost = topmost_activity();
490 if (topmost != NULL) {
491 topmost->shown_event();
492 redraw_event(true);
498 void App::pop_all_activities() {
499 while (topmost_activity())
500 pop_activity();
504 Activity *App::topmost_activity() const {
505 if (running_activities.empty())
506 return NULL;
507 return running_activities.top();
511 bool App::redraw_queued() const {
512 Activity *topmost = topmost_activity();
513 if (topmost == NULL)
514 return false;
515 return topmost->redraw_queued;