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