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.
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
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
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.
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.
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"
139 #include "framework/volumeactivity.hpp"
142 #include "gamebackground.cpp"
144 std::string
help_strings_to_string(char const **strings
) {
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];
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
);
177 App::App(Screen
&screenref
)
178 : PixmapStorage(screenref
),
183 background_image
= NULL
;
188 pop_all_activities();
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);
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())
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. */
255 set_color(GD_GDASH_GRAY2
);
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
);
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.
309 { _("About GDash"), _(About::comments
) },
310 { _("Copyright"), _(About::copyright
) },
311 { _("License"), _(About::license
) },
312 { _("Website"), _(About::website
) },
315 // TRANSLATORS: about dialog box categories.
320 { _("Authors"), About::authors
},
321 { _("Artists"), About::artists
},
322 { _("Documenters"), About::documenters
},
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
;
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
);
380 void App::timer2_event() {
381 if (topmost_activity()) {
382 topmost_activity()->timer2_event();
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. */
395 /* if we have sound, key f9 will push a volume activity.
396 * that activity will receive the keypresses. */
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
404 /* f10 is reserved for the menu in the gtk version. do not pass to the activity,
405 * if we may get this somehow. */
408 /* fullscreen switch. do not pass to the app. */
411 /* other key. pass to the activity. */
412 if (topmost_activity()) {
413 topmost_activity()->keypress_event(keycode
, gfxlib_keycode
);
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();
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();
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();
498 void App::pop_all_activities() {
499 while (topmost_activity())
504 Activity
*App::topmost_activity() const {
505 if (running_activities
.empty())
507 return running_activities
.top();
511 bool App::redraw_queued() const {
512 Activity
*topmost
= topmost_activity();
515 return topmost
->redraw_queued
;