Use C++ types for ScreenData list
[geeqie.git] / src / fullscreen.cc
1 /*
2  * Copyright (C) 2004 John Ellis
3  * Copyright (C) 2008 - 2016 The Geeqie Team
4  *
5  * Author: John Ellis
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 #include "main.h"
23 #include "fullscreen.h"
24
25 #include <algorithm>
26 #include <string>
27 #include <vector>
28
29 #include "image.h"
30 #include "image-load.h"
31 #include "misc.h"
32 #include "ui-fileops.h"
33 #include "ui-misc.h"
34 #include "window.h"
35
36 enum {
37         FULLSCREEN_CURSOR_HIDDEN = 1 << 0,
38         FULLSCREEN_CURSOR_NORMAL = 1 << 1,
39         FULLSCREEN_CURSOR_BUSY   = 1 << 2
40 };
41
42 static void fullscreen_prefs_get_geometry(gint screen, GtkWidget *widget, GdkRectangle &geometry,
43                                    GdkScreen **dest_screen, gboolean *same_region);
44
45 /*
46  *----------------------------------------------------------------------------
47  * full screen functions
48  *----------------------------------------------------------------------------
49  */
50
51 static void clear_mouse_cursor(GtkWidget *widget, gint state)
52 {
53         GdkWindow *window = gtk_widget_get_window(widget);
54         GdkDisplay *display;
55
56         if (!window) return;
57
58         display = gdk_display_get_default();
59
60         if (state & FULLSCREEN_CURSOR_BUSY)
61                 {
62                 GdkCursor *cursor;
63
64                 cursor = gdk_cursor_new_for_display(display, GDK_WATCH);
65                 gdk_window_set_cursor(window, cursor);
66                 g_object_unref(G_OBJECT(cursor));
67                 }
68         else if (state & FULLSCREEN_CURSOR_NORMAL)
69                 {
70                 gdk_window_set_cursor(window, nullptr);
71                 }
72         else
73                 {
74                 GdkCursor *cursor;
75
76                 cursor = gdk_cursor_new_for_display(display, GDK_BLANK_CURSOR);
77                 gdk_window_set_cursor(window, cursor);
78                 g_object_unref(G_OBJECT(cursor));
79                 }
80 }
81
82 static gboolean fullscreen_hide_mouse_cb(gpointer data)
83 {
84         auto fs = static_cast<FullScreenData *>(data);
85
86         if (!fs->hide_mouse_id) return FALSE;
87
88         fs->cursor_state &= ~FULLSCREEN_CURSOR_NORMAL;
89         if (!(fs->cursor_state & FULLSCREEN_CURSOR_BUSY)) clear_mouse_cursor(fs->window, fs->cursor_state);
90
91         g_source_remove(fs->hide_mouse_id);
92         fs->hide_mouse_id = 0;
93         return FALSE;
94 }
95
96 static void fullscreen_hide_mouse_disable(FullScreenData *fs)
97 {
98         if (fs->hide_mouse_id)
99                 {
100                 g_source_remove(fs->hide_mouse_id);
101                 fs->hide_mouse_id = 0;
102                 }
103 }
104
105 static void fullscreen_hide_mouse_reset(FullScreenData *fs)
106 {
107         fullscreen_hide_mouse_disable(fs);
108         fs->hide_mouse_id = g_timeout_add(FULL_SCREEN_HIDE_MOUSE_DELAY, fullscreen_hide_mouse_cb, fs);
109 }
110
111 static gboolean fullscreen_mouse_moved(GtkWidget *, GdkEventMotion *, gpointer data)
112 {
113         auto fs = static_cast<FullScreenData *>(data);
114
115         if (!(fs->cursor_state & FULLSCREEN_CURSOR_NORMAL))
116                 {
117                 fs->cursor_state |= FULLSCREEN_CURSOR_NORMAL;
118                 if (!(fs->cursor_state & FULLSCREEN_CURSOR_BUSY)) clear_mouse_cursor(fs->window, fs->cursor_state);
119                 }
120         fullscreen_hide_mouse_reset(fs);
121
122         return FALSE;
123 }
124
125 static void fullscreen_busy_mouse_disable(FullScreenData *fs)
126 {
127         if (fs->busy_mouse_id)
128                 {
129                 g_source_remove(fs->busy_mouse_id);
130                 fs->busy_mouse_id = 0;
131                 }
132 }
133
134 static void fullscreen_mouse_set_busy(FullScreenData *fs, gboolean busy)
135 {
136         fullscreen_busy_mouse_disable(fs);
137
138         if (!!(fs->cursor_state & FULLSCREEN_CURSOR_BUSY) == (busy)) return;
139
140         if (busy)
141                 {
142                 fs->cursor_state |= FULLSCREEN_CURSOR_BUSY;
143                 }
144         else
145                 {
146                 fs->cursor_state &= ~FULLSCREEN_CURSOR_BUSY;
147                 }
148
149         clear_mouse_cursor(fs->window, fs->cursor_state);
150 }
151
152 static gboolean fullscreen_mouse_set_busy_cb(gpointer data)
153 {
154         auto fs = static_cast<FullScreenData *>(data);
155
156         fs->busy_mouse_id = 0;
157         fullscreen_mouse_set_busy(fs, TRUE);
158         return FALSE;
159 }
160
161 static void fullscreen_mouse_set_busy_idle(FullScreenData *fs)
162 {
163         if (!fs->busy_mouse_id)
164                 {
165                 fs->busy_mouse_id = g_timeout_add(FULL_SCREEN_BUSY_MOUSE_DELAY,
166                                                   fullscreen_mouse_set_busy_cb, fs);
167                 }
168 }
169
170 static void fullscreen_image_update_cb(ImageWindow *, gpointer data)
171 {
172         auto fs = static_cast<FullScreenData *>(data);
173
174         if (fs->imd->il &&
175             image_loader_get_pixbuf(fs->imd->il) != image_get_pixbuf(fs->imd))
176                 {
177                 fullscreen_mouse_set_busy_idle(fs);
178                 }
179 }
180
181 static void fullscreen_image_complete_cb(ImageWindow *, gboolean preload, gpointer data)
182 {
183         auto fs = static_cast<FullScreenData *>(data);
184
185         if (!preload) fullscreen_mouse_set_busy(fs, FALSE);
186 }
187
188 #define XSCREENSAVER_BINARY     "xscreensaver-command"
189 #define XSCREENSAVER_COMMAND    "xscreensaver-command -deactivate >&- 2>&- &"
190
191 static void fullscreen_saver_deactivate()
192 {
193         static gboolean checked = FALSE;
194         static gboolean found = FALSE;
195
196         if (!checked)
197                 {
198                 checked = TRUE;
199                 found = file_in_path(XSCREENSAVER_BINARY);
200                 }
201
202         if (found)
203                 {
204                 runcmd(XSCREENSAVER_COMMAND);
205                 }
206 }
207
208 static gboolean fullscreen_saver_block_cb(gpointer)
209 {
210         if (options->fullscreen.disable_saver)
211                 {
212                 fullscreen_saver_deactivate();
213                 }
214
215         return TRUE;
216 }
217
218 static gboolean fullscreen_delete_cb(GtkWidget *, GdkEventAny *, gpointer data)
219 {
220         auto fs = static_cast<FullScreenData *>(data);
221
222         fullscreen_stop(fs);
223         return TRUE;
224 }
225
226 FullScreenData *fullscreen_start(GtkWidget *window, ImageWindow *imd,
227                                  void (*stop_func)(FullScreenData *, gpointer), gpointer stop_data)
228 {
229         FullScreenData *fs;
230         GdkScreen *screen;
231         GdkRectangle rect;
232         GdkGeometry geometry;
233
234         if (!window || !imd) return nullptr;
235
236         fs = g_new0(FullScreenData, 1);
237
238         fs->cursor_state = FULLSCREEN_CURSOR_HIDDEN;
239
240         fs->normal_window = window;
241         fs->normal_imd = imd;
242
243         fs->stop_func = stop_func;
244         fs->stop_data = stop_data;
245
246         DEBUG_1("full screen requests screen %d", options->fullscreen.screen);
247         fullscreen_prefs_get_geometry(options->fullscreen.screen, window, rect,
248                                       &screen, &fs->same_region);
249
250         fs->window = window_new("fullscreen", nullptr, nullptr, _("Full screen"));
251         DEBUG_NAME(fs->window);
252
253         g_signal_connect(G_OBJECT(fs->window), "delete_event",
254                          G_CALLBACK(fullscreen_delete_cb), fs);
255
256         /* few cosmetic details */
257         gtk_window_set_decorated(GTK_WINDOW(fs->window), FALSE);
258         gtk_container_set_border_width(GTK_CONTAINER(fs->window), 0);
259
260         /* keep window above others, if requested */
261         if (options->fullscreen.above) {
262                 gq_gtk_window_set_keep_above(GTK_WINDOW(fs->window), TRUE);
263         }
264
265         /* set default size and position, so the window appears where it was before */
266         gtk_window_set_default_size(GTK_WINDOW(fs->window), rect.width, rect.height);
267         gq_gtk_window_move(GTK_WINDOW(fs->window), rect.x, rect.y);
268
269         /* By setting USER_POS and USER_SIZE, most window managers will
270          * not request positioning of the full screen window (for example twm).
271          *
272          * In addition, setting gravity to STATIC will result in the
273          * decorations of twm to not effect the requested window position,
274          * the decorations will simply be off screen, except in multi monitor setups :-/
275          */
276         geometry.min_width = 1;
277         geometry.min_height = 1;
278         geometry.base_width = rect.width;
279         geometry.base_height = rect.height;
280         geometry.win_gravity = GDK_GRAVITY_STATIC;
281         gtk_window_set_geometry_hints(GTK_WINDOW(fs->window), fs->window, &geometry,
282                         static_cast<GdkWindowHints>(GDK_HINT_WIN_GRAVITY | GDK_HINT_USER_POS | GDK_HINT_USER_SIZE));
283
284         gtk_widget_realize(fs->window);
285
286         if ((options->fullscreen.screen % 100) == 0)
287                 {
288                 GdkWindow *gdkwin;
289                 gdkwin = gtk_widget_get_window(fs->window);
290                 if (gdkwin != nullptr)
291                         gdk_window_set_fullscreen_mode(gdkwin, GDK_FULLSCREEN_ON_ALL_MONITORS);
292                 }
293
294         /* make window fullscreen -- let Gtk do it's job, don't screw it in any way */
295         gtk_window_fullscreen(GTK_WINDOW(fs->window));
296
297         /* move it to requested screen */
298         if (options->fullscreen.screen >= 0)
299                 {
300                 gtk_window_set_screen(GTK_WINDOW(fs->window), screen);
301                 }
302
303         fs->imd = image_new(FALSE);
304
305         gq_gtk_container_add(GTK_WIDGET(fs->window), fs->imd->widget);
306
307         image_background_set_color_from_options(fs->imd, TRUE);
308         image_set_delay_flip(fs->imd, options->fullscreen.clean_flip);
309         image_auto_refresh_enable(fs->imd, fs->normal_imd->auto_refresh);
310
311         if (options->fullscreen.clean_flip)
312                 {
313                 image_set_update_func(fs->imd, fullscreen_image_update_cb, fs);
314                 image_set_complete_func(fs->imd, fullscreen_image_complete_cb, fs);
315                 }
316
317         gtk_widget_show(fs->imd->widget);
318
319         if (fs->same_region)
320                 {
321                 DEBUG_2("Original window is not visible, enabling std. fullscreen mode");
322                 image_move_from_image(fs->imd, fs->normal_imd);
323                 }
324         else
325                 {
326                 DEBUG_2("Original window is still visible, enabling presentation fullscreen mode");
327                 image_copy_from_image(fs->imd, fs->normal_imd);
328                 }
329
330         if (options->stereo.enable_fsmode) {
331                 image_stereo_set(fs->imd, options->stereo.fsmode);
332         }
333
334         gtk_widget_show(fs->window);
335
336         /* for hiding the mouse */
337         g_signal_connect(G_OBJECT(fs->imd->pr), "motion_notify_event",
338                            G_CALLBACK(fullscreen_mouse_moved), fs);
339         clear_mouse_cursor(fs->window, fs->cursor_state);
340
341         /* set timer to block screen saver */
342         fs->saver_block_id = g_timeout_add(60 * 1000, fullscreen_saver_block_cb, fs);
343
344         /* hide normal window */
345          /** @FIXME properly restore this window on show
346          */
347         if (fs->same_region)
348                 {
349                 if (options->hide_window_in_fullscreen)
350                         {
351                         gtk_widget_hide(fs->normal_window);
352                         }
353                 image_change_fd(fs->normal_imd, nullptr, image_zoom_get(fs->normal_imd));
354                 }
355
356         return fs;
357 }
358
359 void fullscreen_stop(FullScreenData *fs)
360 {
361         if (!fs) return;
362
363         if (fs->saver_block_id) g_source_remove(fs->saver_block_id);
364
365         fullscreen_hide_mouse_disable(fs);
366         fullscreen_busy_mouse_disable(fs);
367         gdk_keyboard_ungrab(GDK_CURRENT_TIME);
368
369         if (fs->same_region)
370                 {
371                 image_move_from_image(fs->normal_imd, fs->imd);
372                 if (options->hide_window_in_fullscreen)
373                         {
374                         gtk_widget_show(fs->normal_window);
375                         }
376                 if (options->stereo.enable_fsmode)
377                         {
378                         image_stereo_set(fs->normal_imd, options->stereo.mode);
379                         }
380                 }
381
382
383         if (fs->stop_func) fs->stop_func(fs, fs->stop_data);
384
385         gq_gtk_widget_destroy(fs->window);
386
387         gtk_window_present(GTK_WINDOW(fs->normal_window));
388
389         g_free(fs);
390 }
391
392
393 /*
394  *----------------------------------------------------------------------------
395  * full screen preferences and utils
396  *----------------------------------------------------------------------------
397  */
398
399 /**
400  * @struct ScreenData
401  * screen numbers for fullscreen_prefs are as follows: \n
402  *   0  use default display size \n
403  * 101  screen 0, monitor 0 \n
404  * 102  screen 0, monitor 1 \n
405  * 201  screen 1, monitor 0 \n
406  */
407 struct ScreenData {
408         gint number;
409         std::string description;
410         GdkRectangle geometry;
411 };
412
413 static std::vector<ScreenData> fullscreen_prefs_list()
414 {
415         std::vector<ScreenData> list;
416         GdkDisplay *display;
417         GdkScreen *screen;
418         gint monitors;
419
420         display = gdk_display_get_default();
421         screen = gdk_display_get_default_screen(display);
422         monitors = gdk_display_get_n_monitors(display);
423
424         for (gint j = -1; j < monitors; j++)
425                 {
426                 GdkRectangle rect;
427                 gchar *name;
428                 gchar *subname;
429
430                 name = gdk_screen_make_display_name(screen);
431
432                 if (j < 0)
433                         {
434                         rect.x = 0;
435                         rect.y = 0;
436                         rect.width = gdk_screen_get_width(screen);
437                         rect.height = gdk_screen_get_height(screen);
438                         subname = g_strdup(_("Full size"));
439                         }
440                 else
441                         {
442                         auto monitor = gdk_display_get_monitor(display, j);
443                         gdk_monitor_get_geometry(monitor, &rect);
444                         subname = g_strdup(gdk_monitor_get_model(monitor));
445                         if (subname == nullptr)
446                                 {
447                                 subname = g_strdup_printf("%s %d", _("Monitor"), j + 1);
448                                 }
449                         }
450
451                 ScreenData sd;
452                 sd.number = 100 + j + 1;
453                 sd.description = std::string(_("Screen")) + " " + name + ", " + subname;
454                 sd.geometry = rect;
455
456                 DEBUG_1("Screen %d %30s %4d,%4d (%4dx%4d)",
457                         sd.number, sd.description.c_str(), sd.geometry.x, sd.geometry.y, sd.geometry.width, sd.geometry.height);
458
459                 list.push_back(sd);
460
461                 g_free(name);
462                 g_free(subname);
463                 }
464
465         return list;
466 }
467
468 /* screen is interpreted as such:
469  *  -1  window manager determines size and position, fallback is (1) active monitor
470  *   0  full size of screen containing widget
471  *   1  size of monitor containing widget
472  * 100  full size of screen 1 (screen, monitor counts start at 1)
473  * 101  size of monitor 1 on screen 1
474  * 203  size of monitor 3 on screen 2
475  * returns:
476  * dest_screen: screen to place widget [use gtk_window_set_screen()]
477  * same_region: the returned region will overlap the current location of widget.
478  */
479 static void fullscreen_prefs_get_geometry(gint screen, GtkWidget *widget, GdkRectangle &geometry,
480                                    GdkScreen **dest_screen, gboolean *same_region)
481 {
482         ScreenData sd;
483         bool found = false; /// @todo: use std::optional in c++17
484
485         if (screen >= 100)
486                 {
487                 std::vector<ScreenData> list = fullscreen_prefs_list();
488                 auto it = std::find_if(list.cbegin(), list.cend(), [screen](const ScreenData &sd){ return sd.number == screen; });
489                 if (it != list.cend())
490                         {
491                         sd = *it;
492                         found = true;
493                         }
494                 }
495         else if (screen < 0) screen = 1;
496
497         if (found)
498                 {
499                 GdkDisplay *display;
500                 GdkScreen *screen;
501
502                 display = gdk_display_get_default();
503                 screen = gdk_display_get_default_screen(display);
504
505                 geometry = sd.geometry;
506
507                 if (dest_screen) *dest_screen = screen;
508                 if (same_region) *same_region = (!widget || !gtk_widget_get_window(widget) ||
509                                         (screen == gtk_widget_get_screen(widget) &&
510                                          (sd.number%100 == 0 ||
511                                           sd.number%100 == gdk_screen_get_monitor_at_window(screen, gtk_widget_get_window(widget))+1)));
512                 }
513         else if (screen != 1 || !widget || !gtk_widget_get_window(widget))
514                 {
515                 GdkScreen *screen;
516
517                 if (widget)
518                         {
519                         screen = gtk_widget_get_screen(widget);
520                         }
521                 else
522                         {
523                         screen = gdk_screen_get_default();
524                         }
525
526                 geometry.x = 0;
527                 geometry.y = 0;
528                 geometry.width = gdk_screen_get_width(screen);
529                 geometry.height = gdk_screen_get_height(screen);
530
531                 if (dest_screen) *dest_screen = screen;
532                 if (same_region) *same_region = TRUE;
533                 }
534         else
535                 {
536                 GdkDisplay *display;
537                 GdkMonitor *monitor;
538
539                 display = gtk_widget_get_display(widget);
540                 monitor = gdk_display_get_monitor_at_window(display, gtk_widget_get_window(widget));
541
542                 gdk_monitor_get_geometry(monitor, &geometry);
543
544                 if (dest_screen) *dest_screen = gtk_widget_get_screen(widget);
545                 if (same_region) *same_region = TRUE;
546                 }
547 }
548
549 #pragma GCC diagnostic push
550 #pragma GCC diagnostic ignored "-Wunused-function"
551 gint fullscreen_prefs_find_screen_for_widget_unused(GtkWidget *widget)
552 {
553         GdkScreen *screen;
554         gint monitor;
555         gint n;
556
557         if (!widget || !gtk_widget_get_window(widget)) return 0;
558
559         screen = gtk_widget_get_screen(widget);
560         monitor = gdk_screen_get_monitor_at_window(screen, gtk_widget_get_window(widget));
561
562         n = 100 + monitor + 1;
563
564         DEBUG_1("Screen appears to be %d", n);
565
566         return n;
567 }
568 #pragma GCC diagnostic pop
569
570 enum {
571         FS_MENU_COLUMN_NAME = 0,
572         FS_MENU_COLUMN_VALUE
573 };
574
575 #define BUTTON_ABOVE_KEY  "button_above"
576
577 static void fullscreen_prefs_selection_cb(GtkWidget *combo, gpointer data)
578 {
579         auto value = static_cast<gint *>(data);
580         GtkTreeModel *store;
581         GtkTreeIter iter;
582         GtkWidget *button;
583
584         if (!value) return;
585
586         store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
587         if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter)) return;
588         gtk_tree_model_get(store, &iter, FS_MENU_COLUMN_VALUE, value, -1);
589
590         button = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(combo), BUTTON_ABOVE_KEY));
591         if (button)
592                 {
593                 gtk_widget_set_sensitive(button, *value != -1);
594                 }
595 }
596
597 static void fullscreen_prefs_selection_add(GtkListStore *store, const gchar *text, gint value)
598 {
599         GtkTreeIter iter;
600
601         gtk_list_store_append(store, &iter);
602         gtk_list_store_set(store, &iter, FS_MENU_COLUMN_NAME, text,
603                                          FS_MENU_COLUMN_VALUE, value, -1);
604 }
605
606 GtkWidget *fullscreen_prefs_selection_new(const gchar *text, gint *screen_value, gboolean *)
607 {
608         GtkWidget *vbox;
609         GtkWidget *hbox;
610         GtkWidget *combo;
611         GtkListStore *store;
612         GtkCellRenderer *renderer;
613         gint current = 0;
614         gint n;
615
616         if (!screen_value) return nullptr;
617
618         vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PREF_PAD_GAP);
619         DEBUG_NAME(vbox);
620         hbox = pref_box_new(vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
621         if (text) pref_label_new(hbox, text);
622
623         store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);
624         combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
625         g_object_unref(store);
626
627         renderer = gtk_cell_renderer_text_new();
628         gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, TRUE);
629         gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), renderer,
630                                        "text", FS_MENU_COLUMN_NAME, NULL);
631
632         fullscreen_prefs_selection_add(store, _("Determined by Window Manager"), -1);
633         fullscreen_prefs_selection_add(store, _("Active screen"), 0);
634         if (*screen_value == 0) current = 1;
635         fullscreen_prefs_selection_add(store, _("Active monitor"), 1);
636         if (*screen_value == 1) current = 2;
637
638         n = 3;
639         std::vector<ScreenData> list = fullscreen_prefs_list();
640         for (const ScreenData &sd : list)
641                 {
642                 fullscreen_prefs_selection_add(store, sd.description.c_str(), sd.number);
643                 if (*screen_value == sd.number) current = n;
644
645                 n++;
646                 }
647
648         gtk_combo_box_set_active(GTK_COMBO_BOX(combo), current);
649
650         gq_gtk_box_pack_start(GTK_BOX(hbox), combo, FALSE, FALSE, 0);
651         gtk_widget_show(combo);
652
653         g_signal_connect(G_OBJECT(combo), "changed",
654                          G_CALLBACK(fullscreen_prefs_selection_cb), screen_value);
655
656         return vbox;
657 }
658 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */