application-window.vala 13.7 KB
Newer Older
Adrien Plazas's avatar
Adrien Plazas committed
1
// This file is part of GNOME Games. License: GPL-3.0+.
2

3
4
[GtkTemplate (ui = "/org/gnome/Games/ui/application-window.ui")]
private class Games.ApplicationWindow : Gtk.ApplicationWindow {
5
	private const uint WINDOW_SIZE_UPDATE_DELAY_MILLISECONDS = 500;
6
7
	private const uint FOCUS_OUT_DELAY_MILLISECONDS = 500;

Adrien Plazas's avatar
Adrien Plazas committed
8
9
10
11
12
13
14
15
	private UiState _ui_state;
	public UiState ui_state {
		set {
			if (value == ui_state)
				return;

			_ui_state = value;

16
17
			switch (ui_state) {
			case UiState.COLLECTION:
18
				content_box.set_visible_child (collection_box);
19
20
				header_bar.set_visible_child (collection_header_bar);

21
22
				is_fullscreen = false;

23
				if (display_box.runner != null) {
24
					display_box.runner.stop ();
25
26
27
					display_box.runner = null;
				}

28
29
				break;
			case UiState.DISPLAY:
30
				content_box.set_visible_child (display_box);
31
32
				header_bar.set_visible_child (display_header_bar);

Adrien Plazas's avatar
Adrien Plazas committed
33
				search_mode = false;
34
35
36

				break;
			}
Adrien Plazas's avatar
Adrien Plazas committed
37
38
39
40
		}
		get { return _ui_state; }
	}

41
42
43
44
45
46
47
48
49
50
51
52
53
	private bool _is_fullscreen;
	public bool is_fullscreen {
		set {
			_is_fullscreen = value && (ui_state == UiState.DISPLAY);

			if (_is_fullscreen)
				fullscreen ();
			else
				unfullscreen ();
		}
		get { return _is_fullscreen; }
	}

Adrien Plazas's avatar
Adrien Plazas committed
54
55
56
57
58
	private bool _search_mode;
	public bool search_mode {
		set { _search_mode = value && (ui_state == UiState.COLLECTION); }
		get { return _search_mode; }
	}
Adrien Plazas's avatar
Adrien Plazas committed
59

60
61
	public bool loading_notification { set; get; }

Adrien Plazas's avatar
Adrien Plazas committed
62
	[GtkChild]
63
64
65
66
67
	private Gtk.Stack content_box;
	[GtkChild]
	private CollectionBox collection_box;
	[GtkChild]
	private DisplayBox display_box;
68

Adrien Plazas's avatar
Adrien Plazas committed
69
	[GtkChild]
70
71
72
73
74
	private Gtk.Stack header_bar;
	[GtkChild]
	private CollectionHeaderBar collection_header_bar;
	[GtkChild]
	private DisplayHeaderBar display_header_bar;
75

76
77
	private Settings settings;

78
	private Binding box_search_binding;
79
	private Binding box_fullscreen_binding;
80
	private Binding header_bar_search_binding;
81
	private Binding header_bar_fullscreen_binding;
82
	private Binding loading_notification_binding;
Adrien Plazas's avatar
Adrien Plazas committed
83

84
	private Cancellable run_game_cancellable;
85
	private Cancellable quit_game_cancellable;
86

87
	private long window_size_update_timeout;
88
89
	private long focus_out_timeout_id;

90
	private uint inhibit_cookie;
91
	private Gtk.ApplicationInhibitFlags inhibit_flags;
92

Adrien Plazas's avatar
Adrien Plazas committed
93
	public ApplicationWindow (ListModel collection) {
94
		collection_box.collection = collection;
Adrien Plazas's avatar
Adrien Plazas committed
95
	}
Adrien Plazas's avatar
Adrien Plazas committed
96

Adrien Plazas's avatar
Adrien Plazas committed
97
	construct {
98
99
		settings = new Settings ("org.gnome.Games");

100
101
102
103
104
105
106
107
108
		int width, height;
		settings.get ("window-size", "(ii)", out width, out height);
		Gdk.Screen? screen = get_screen ();
		if (screen != null) {
			width = int.min (width, screen.get_width ());
			height = int.min (height, screen.get_height ());
		}
		resize (width, height);

109
110
111
		if (settings.get_boolean ("window-maximized"))
			maximize ();

112
113
		box_search_binding = bind_property ("search-mode", collection_box, "search-mode",
		                                    BindingFlags.BIDIRECTIONAL);
114
115
		loading_notification_binding = bind_property ("loading-notification", collection_box, "loading-notification",
		                                              BindingFlags.DEFAULT);
116
117
		header_bar_search_binding = bind_property ("search-mode", collection_header_bar, "search-mode",
		                                           BindingFlags.BIDIRECTIONAL);
118
119
120

		box_fullscreen_binding = bind_property ("is-fullscreen", display_box, "is-fullscreen",
		                                        BindingFlags.BIDIRECTIONAL);
121
122
		header_bar_fullscreen_binding = bind_property ("is-fullscreen", display_header_bar, "is-fullscreen",
		                                               BindingFlags.BIDIRECTIONAL);
123

124
		window_size_update_timeout = -1;
125
		focus_out_timeout_id = -1;
126
		inhibit_cookie = 0;
127
		inhibit_flags = 0;
128
129

		set_show_menubar (false); // Essential, see bug #771683
130
	}
Adrien Plazas's avatar
Adrien Plazas committed
131

132
133
134
135
136
137
	public void run_game (Game game) {
		if (run_game_cancellable != null)
			run_game_cancellable.cancel ();

		run_game_cancellable = new Cancellable ();

138
139
140
141
142
143
144
145
		var cancellable = new Cancellable ();
		run_game_cancellable = cancellable;

		run_game_with_cancellable (game, cancellable);

		// Only reset the cancellable if another one didn't replace it.
		if (run_game_cancellable == cancellable)
			run_game_cancellable = null;
146

147
		inhibit (Gtk.ApplicationInhibitFlags.IDLE | Gtk.ApplicationInhibitFlags.LOGOUT);
148
149
	}

150
151
152
153
154
155
156
157
158
159
160
161
	public bool quit_game () {
		// If the window have been deleted/hidden we probably don't want to
		// prompt the user.
		if (!visible)
			return true;

		if (run_game_cancellable != null)
			run_game_cancellable.cancel ();

		if (quit_game_cancellable != null)
			quit_game_cancellable.cancel ();

162
163
164
165
166
167
168
169
		var cancellable = new Cancellable ();
		quit_game_cancellable = cancellable;

		var result = quit_game_with_cancellable (cancellable);

		// Only reset the cancellable if another one didn't replace it.
		if (quit_game_cancellable == cancellable)
			quit_game_cancellable = null;
170

171
		return result;
172
173
	}

174
175
176
177
178
179
180
	public override void size_allocate (Gtk.Allocation allocation) {
		base.size_allocate (allocation);

		if (window_size_update_timeout == -1 && !is_maximized)
			window_size_update_timeout = Timeout.add (WINDOW_SIZE_UPDATE_DELAY_MILLISECONDS, store_window_size);
	}

181
182
183
184
185
	[GtkCallback]
	public bool on_delete_event () {
		return !quit_game ();
	}

186
187
188
189
	[GtkCallback]
	public bool on_key_pressed (Gdk.EventKey event) {
		var default_modifiers = Gtk.accelerator_get_default_mod_mask ();

190
		if ((event.keyval == Gdk.Key.q || event.keyval == Gdk.Key.Q) &&
191
		    (event.state & default_modifiers) == Gdk.ModifierType.CONTROL_MASK) {
192
193
194
			if (!quit_game ())
				return false;

195
196
197
198
199
			destroy ();

			return true;
		}

200
		return handle_collection_key_event (event) || handle_display_key_event (event);
201
202
	}

203
204
	[GtkCallback]
	public bool on_window_state_event (Gdk.EventWindowState event) {
205
206
207
		var is_maximized = (bool) (event.new_window_state & Gdk.WindowState.MAXIMIZED);
		settings.set_boolean ("window-maximized", is_maximized);

208
		is_fullscreen = (bool) (event.new_window_state & Gdk.WindowState.FULLSCREEN);
209
		update_pause (false);
210

211
212
213
214
215
216
217
		if (!(bool) (event.changed_mask & Gdk.WindowState.FOCUSED))
			return false;

		var focused = (bool) (event.new_window_state & Gdk.WindowState.FOCUSED);
		var playing = (ui_state == UiState.DISPLAY);

		if (focused && playing)
218
			inhibit (Gtk.ApplicationInhibitFlags.IDLE);
219
220

		if (!focused)
221
			uninhibit (Gtk.ApplicationInhibitFlags.IDLE);
222

223
224
225
		return false;
	}

Adrien Plazas's avatar
Adrien Plazas committed
226
227
	[GtkCallback]
	private void on_game_activated (Game game) {
228
229
230
		run_game (game);
	}

231
232
233
234
	[GtkCallback]
	private void on_display_back () {
		if (quit_game ())
			ui_state = UiState.COLLECTION;
235

236
		uninhibit (Gtk.ApplicationInhibitFlags.IDLE | Gtk.ApplicationInhibitFlags.LOGOUT);
237
238
	}

239
	private void run_game_with_cancellable (Game game, Cancellable cancellable) {
240
241
		display_header_bar.game_title = game.name;
		display_box.header_bar.game_title = game.name;
242
243
		ui_state = UiState.DISPLAY;

244
245
		// Reset the UI parts depending on the runner to avoid an
		// inconsistent state is case we couldn't retrieve it.
246
		reset_display_page ();
247

248
249
250
251
252
253
254
		var runner = try_get_runner (game);
		if (runner == null)
			return;

		display_header_bar.can_fullscreen = runner.can_fullscreen;
		display_box.header_bar.can_fullscreen = runner.can_fullscreen;
		display_box.runner = runner;
255
256
		display_header_bar.media_set = runner.media_set;
		display_box.header_bar.media_set = runner.media_set;
257

258
259
		is_fullscreen = settings.get_boolean ("fullscreen") && runner.can_fullscreen;

260
261
262
263
264
265
266
267
268
		bool resume = false;
		if (runner.can_resume)
			resume = prompt_resume_with_cancellable (cancellable);

		if (!try_run_with_cancellable (runner, resume, cancellable))
			prompt_resume_fail_with_cancellable (runner, cancellable);
	}

	private Runner? try_get_runner (Game game) {
269
		try {
270
			var runner = game.get_runner ();
271
272
273
			string error_message;
			if (runner.check_is_valid (out error_message))
				return runner;
274

275
			reset_display_page ();
276
277
278
			display_box.display_running_game_failed (game, error_message);

			return null;
279
280
		}
		catch (Error e) {
281
			warning (e.message);
282
			reset_display_page ();
Adrien Plazas's avatar
Adrien Plazas committed
283
			display_box.display_running_game_failed (game, _("An unexpected error occurred."));
284

285
			return null;
286
		}
287
	}
288

289
290
291
	private bool prompt_resume_with_cancellable (Cancellable cancellable) {
		var dialog = new ResumeDialog ();
		dialog.set_transient_for (this);
292

293
		cancellable.cancelled.connect (() => {
294
			dialog.destroy ();
295
		});
296

297
298
		var response = dialog.run ();
		dialog.destroy ();
299

300
301
		if (cancellable.is_cancelled ())
			response = Gtk.ResponseType.CANCEL;
302

303
304
		if (response == Gtk.ResponseType.CANCEL)
			return false;
305

306
307
		return true;
	}
308

309
	private bool try_run_with_cancellable (Runner runner, bool resume, Cancellable cancellable) {
Adrien Plazas's avatar
Adrien Plazas committed
310
		try {
311
			if (resume)
312
				display_box.runner.resume ();
313
			else
314
315
316
				runner.start ();

			return true;
Adrien Plazas's avatar
Adrien Plazas committed
317
		}
318
		catch (Error e) {
319
			warning (e.message);
Adrien Plazas's avatar
Adrien Plazas committed
320

321
322
323
			return false;
		}
	}
324

325
326
327
	private void prompt_resume_fail_with_cancellable (Runner runner, Cancellable cancellable) {
		var dialog = new ResumeFailedDialog ();
		dialog.set_transient_for (this);
328

329
		cancellable.cancelled.connect (() => {
330
			dialog.destroy ();
331
		});
332

333
334
		var response = dialog.run ();
		dialog.destroy ();
335

336
337
		if (cancellable.is_cancelled ())
			response = Gtk.ResponseType.CANCEL;
338

339
340
341
		if (response == Gtk.ResponseType.CANCEL) {
			display_box.runner = null;
			ui_state = UiState.COLLECTION;
342

Adrien Plazas's avatar
Adrien Plazas committed
343
			return;
Adrien Plazas's avatar
Adrien Plazas committed
344
		}
345
346
347
348
349
350
351

		try {
			runner.start ();
		}
		catch (Error e) {
			warning (e.message);
		}
Adrien Plazas's avatar
Adrien Plazas committed
352
	}
353

354
	public bool quit_game_with_cancellable (Cancellable cancellable) {
355
		if (display_box.runner == null)
356
357
			return true;

358
		display_box.runner.stop ();
359

360
		if (display_box.runner.can_quit_safely)
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
			return true;

		var dialog = new QuitDialog ();
		dialog.set_transient_for (this);

		cancellable.cancelled.connect (() => {
			dialog.destroy ();
		});

		var response = dialog.run ();
		dialog.destroy ();

		if (cancellable.is_cancelled ())
			return cancel_quitting_game ();

		if (response == Gtk.ResponseType.ACCEPT)
			return true;

		return cancel_quitting_game ();
	}

	private bool cancel_quitting_game () {
383
		if (display_box.runner != null)
384
385
386
387
388
389
			try {
				display_box.runner.resume ();
			}
			catch (Error e) {
				warning (e.message);
			}
390
391
392

		return false;
	}
393

394
395
	[GtkCallback]
	private void on_toplevel_focus () {
396
397
		update_pause (true);
	}
398

399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
	private bool store_window_size () {
		Gdk.Screen? screen = get_screen ();
		if (screen == null)
			return false;

		int width = 0;
		int height = 0;

		get_size (out width, out height);

		width = int.min (width, screen.get_width ());
		height = int.min (height, screen.get_height ());

		settings.set ("window-size", "(ii)", width, height);

		Source.remove ((uint) window_size_update_timeout);
		window_size_update_timeout = -1;

		return false;
	}

420
421
422
423
424
425
426
	private void update_pause (bool with_delay) {
		if (focus_out_timeout_id != -1) {
			Source.remove ((uint) focus_out_timeout_id);
			focus_out_timeout_id = -1;
		}

		if (!can_update_pause ())
427
428
429
			return;

		if (has_toplevel_focus)
430
431
432
433
434
435
			try {
				display_box.runner.resume ();
			}
			catch (Error e) {
				warning (e.message);
			}
436
437
		else if (with_delay)
			focus_out_timeout_id = Timeout.add (FOCUS_OUT_DELAY_MILLISECONDS, on_focus_out_delay_elapsed);
438
439
440
		else
			display_box.runner.pause ();
	}
441

442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
	private bool on_focus_out_delay_elapsed () {
		focus_out_timeout_id = -1;

		if (!can_update_pause ())
			return false;

		if (!has_toplevel_focus)
			display_box.runner.pause ();

		return false;
	}

	private bool can_update_pause () {
		if (ui_state != UiState.DISPLAY)
			return false;

		if (display_box.runner == null)
			return false;

461
462
463
		if (run_game_cancellable != null)
			return false;

464
465
466
		if (quit_game_cancellable != null)
			return false;

467
468
469
		return true;
	}

470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
	private bool handle_collection_key_event (Gdk.EventKey event) {
		if (ui_state != UiState.COLLECTION)
			return false;

		var default_modifiers = Gtk.accelerator_get_default_mod_mask ();

		if ((event.keyval == Gdk.Key.f || event.keyval == Gdk.Key.F) &&
		    (event.state & default_modifiers) == Gdk.ModifierType.CONTROL_MASK) {
			if (!search_mode)
				search_mode = true;

			return true;
		}

		return collection_box.search_bar_handle_event (event);
	}
486
487
488
489
490
491
492
493

	private bool handle_display_key_event (Gdk.EventKey event) {
		if (ui_state != UiState.DISPLAY)
			return false;

		var default_modifiers = Gtk.accelerator_get_default_mod_mask ();

		if ((event.keyval == Gdk.Key.f || event.keyval == Gdk.Key.F) &&
494
495
		    (event.state & default_modifiers) == Gdk.ModifierType.CONTROL_MASK &&
		    display_header_bar.can_fullscreen) {
496
			is_fullscreen = !is_fullscreen;
497
			settings.set_boolean ("fullscreen", is_fullscreen);
498
499
500
501

			return true;
		}

502
		if (event.keyval == Gdk.Key.F11 && display_header_bar.can_fullscreen) {
503
			is_fullscreen = !is_fullscreen;
504
			settings.set_boolean ("fullscreen", is_fullscreen);
505
506
507
508

			return true;
		}

509
		if (event.keyval == Gdk.Key.Escape && display_header_bar.can_fullscreen) {
510
			is_fullscreen = false;
511
			settings.set_boolean ("fullscreen", false);
512
513
514
515
516
517

			return true;
		}

		return false;
	}
518

519
520
521
522
523
524
	private void inhibit (Gtk.ApplicationInhibitFlags flags) {
		if ((inhibit_flags & flags) == flags)
			return;

		Gtk.ApplicationInhibitFlags new_flags = (inhibit_flags | flags);
		uint new_cookie = application.inhibit (this, new_flags, _("Playing a game"));
525
526

		if (inhibit_cookie != 0)
527
			application.uninhibit (inhibit_cookie);
528

529
530
		inhibit_cookie = new_cookie;
		inhibit_flags = new_flags;
531
532
	}

533
534
	private void uninhibit (Gtk.ApplicationInhibitFlags flags) {
		if ((inhibit_flags & flags) == 0)
535
536
			return;

537
538
539
540
541
542
		Gtk.ApplicationInhibitFlags new_flags = (inhibit_flags & ~flags);
		uint new_cookie = 0;

		if ((bool) new_flags)
			new_cookie = application.inhibit (this, new_flags, _("Playing a game"));

543
544
545
		if (inhibit_cookie != 0)
			application.uninhibit (inhibit_cookie);

546
547
		inhibit_cookie = new_cookie;
		inhibit_flags = new_flags;
548
	}
549
550
551
552
553
554
555
556

	private void reset_display_page () {
		display_header_bar.can_fullscreen = false;
		display_box.header_bar.can_fullscreen = false;
		display_box.runner = null;
		display_header_bar.media_set = null;
		display_box.header_bar.media_set = null;
	}
557
}