1 /**
2  * \file    uimachinewindow.c
3  * \brief   Native GTK3 main emulator window code.
4  *
5  * \author Marcus Sutton <loggedoubt@gmail.com>
6  * \author Michael C. Martin <mcmartin@gmail.com>
7  */
8 
9 /* This file is part of VICE, the Versatile Commodore Emulator.
10  * See README for copyright notice.
11  *
12  *  This program is free software; you can redistribute it and/or modify
13  *  it under the terms of the GNU General Public License as published by
14  *  the Free Software Foundation; either version 2 of the License, or
15  *  (at your option) any later version.
16  *
17  *  This program is distributed in the hope that it will be useful,
18  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
19  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  *  GNU General Public License for more details.
21  *
22  *  You should have received a copy of the GNU General Public License
23  *  along with this program; if not, write to the Free Software
24  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
25  *  02111-1307  USA.
26  *
27  */
28 
29 /* \note It should be possible to compile, link and run vsid while
30  *       this entire file (amongst others) is contained inside an #if
31  *       0 wrapper.
32  */
33 #if 1
34 
35 /* #define DEBUGPOINTER */
36 
37 #include "vice.h"
38 
39 #include <gtk/gtk.h>
40 
41 #include "cairo_renderer.h"
42 #include "opengl_renderer.h"
43 #include "quartz_renderer.h"
44 #include "lightpen.h"
45 #include "mousedrv.h"
46 #include "videoarch.h"
47 
48 #include "ui.h"
49 #include "uimachinemenu.h"
50 #include "uimachinewindow.h"
51 
52 #ifdef DEBUGPOINTER
53 #define VICE_EMPTY_POINTER  NULL
54 #else
55 #define VICE_EMPTY_POINTER  canvas->blank_ptr
56 #endif
57 
58 /** \brief Last recoreded X position of the mouse, for computing
59  *         relative movement.
60  *  \todo  This caching method should be less awful.
61  *  \sa    event_box_motion_cb */
62 static gdouble last_mouse_x = -1;
63 
64 /** \brief Last recoreded Y position of the mouse, for computing
65  *         relative movement.
66  *  \todo  This caching method should be less awful.
67  *  \sa    event_box_motion_cb */
68 static gdouble last_mouse_y = -1;
69 
70 /** \brief If nonzero, the next mouse motion event will ignored by the
71  *         mouse driver.
72  *  \sa    event_box_motion_cb */
73 static int warping = 0;
74 
75 /** \brief If nonzero, this is a handle for the pointer device
76  *  \sa    event_box_motion_cb */
77 static GdkDevice *pointer = NULL;
78 
79 /** \brief If nonzero, this is a handle for the canvas under the pointer device
80  *  \sa    event_box_motion_cb */
81 static video_canvas_t *pointercanvas = NULL;
82 
83 /** \brief If nonzero, this is a handle for the seat associated with mouse grab
84  *  \sa    event_box_motion_cb */
85 static GdkSeat *pointerseat = NULL;
86 
87 /** \brief  Ignore the hide-mouse-cursor event handlers
88  *
89  * Used during dialogs
90  */
91 static gboolean ignore_mouse_hide = FALSE;
92 
93 
94 /** \brief  Set mouse-hide-ignore state
95  *
96  * \param[in]   state   enable/disable ignoring the mouse pointer hiding
97  */
ui_set_ignore_mouse_hide(gboolean state)98 void ui_set_ignore_mouse_hide(gboolean state)
99 {
100     ignore_mouse_hide = state;
101 }
102 
103 
104 /** \brief Callback for handling mouse motion events over the emulated
105  *         screen.
106  *
107  *  Mouse motion events influence three different subsystems: the
108  *  light-pen (if any), the emulated mouse (if any), and the UI-level
109  *  routines that hide the mouse pointer if it comes to rest over the
110  *  machine's screen.
111  *
112  *  Moving the mouse pointer resets the number of frames the mouse was
113  *  held still.
114  *
115  *  Light pen position information is computed based on the new mouse
116  *  position and what part of the machine window is actually in use
117  *  based on current scaling and aspect ratio settings.
118  *
119  *  Mouse information is computed based on the difference between the
120  *  current mouse location and the last recorded mouse location. If
121  *  the mouse has been captured by the emulator, this also then warps
122  *  the mouse pointer back to the middle of the emulated
123  *  screen. (These warps will trigger an additional call to this
124  *  function, but an additional flag will prevent them from being
125  *  processed as true input.)
126  *
127  *  Information relevant to these processes is cached in the
128  *  video_canvas_s structure for use as needed.
129  *
130  *  \param widget    The widget that sent the event.
131  *  \param event     The GdkEventMotion event itself.
132  *  \param user_data The video canvas data structure associated with
133  *                   this machine window.
134  *  \return TRUE if no further event processing is necessary.
135  *
136  *  \todo Information involving mouse-warping is not cached with the
137  *        canvas yet, and should be for cleaner C128 support.
138  *
139  *  \todo Pointer warping does not work on Wayland. GTK3 and its GDK
140  *        substrate simply do not provide an implementation for
141  *        gdk_device_warp(), and Wayland's window model doesn't really
142  *        support pointer warping the way GDK envisions. Wayland's
143  *        window model envisions using pointer constraints to confine
144  *        the mouse pointer within a target window, and then using
145  *        relative mouse motion events to capture additional attempts
146  *        at motion outside of it. SDL2 implements this and it may
147  *        provide a useful starting point for this alternative
148  *        implementation.
149  *
150  * \sa event_box_mouse_button_cb Further light pen and mouse button
151  *     handling.
152  * \sa event_box_scroll_cb Further mouse button handling.
153  * \sa event_box_stillness_tick_cb More of the hide-idle-mouse-pointer
154  *     logic.
155  * \sa event_box_cross_cb More of the hide-idle-mouse-pointer logic.
156  */
event_box_motion_cb(GtkWidget * widget,GdkEvent * event,gpointer user_data)157 static gboolean event_box_motion_cb(GtkWidget *widget,
158                                     GdkEvent *event, gpointer user_data)
159 {
160     video_canvas_t *canvas = (video_canvas_t *)user_data;
161 
162     canvas->still_frames = 0;
163 
164     if (event->type == GDK_MOTION_NOTIFY) {
165         GdkEventMotion *motion = (GdkEventMotion *)event;
166         double render_w = canvas->geometry->screen_size.width;
167         double render_h = canvas->geometry->last_displayed_line - canvas->geometry->first_displayed_line + 1;
168         int pen_x = (motion->x - canvas->screen_origin_x) * render_w / canvas->screen_display_w;
169         int pen_y = (motion->y - canvas->screen_origin_y) * render_h / canvas->screen_display_h;
170         if (pen_x < 0 || pen_y < 0 || pen_x >= render_w || pen_y >= render_h) {
171             /* Mouse pointer is offscreen, so the light pen is disabled. */
172             canvas->pen_x = -1;
173             canvas->pen_y = -1;
174             canvas->pen_buttons = 0;
175         } else {
176             canvas->pen_x = pen_x;
177             canvas->pen_y = pen_y;
178         }
179         if (warping) {
180             warping = 0;
181         } else {
182             if (last_mouse_x > 0 && last_mouse_y > 0) {
183                 mouse_move((pen_x-last_mouse_x) * canvas->videoconfig->scalex,
184                            (pen_y-last_mouse_y) * canvas->videoconfig->scaley);
185             }
186             if (_mouse_enabled) {
187                 GdkWindow *window = gtk_widget_get_window(gtk_widget_get_toplevel(widget));
188                 GdkScreen *screen = gdk_window_get_screen(window);
189                 int window_w = gdk_window_get_width(window);
190                 int window_h = gdk_window_get_height(window);
191                 gdk_device_warp(motion->device, screen,
192                                 (window_w / 2) + motion->x_root - motion->x,
193                                 (window_h / 2) + motion->y_root - motion->y);
194                 warping = 1;
195             }
196         }
197         last_mouse_x = pen_x;
198         last_mouse_y = pen_y;
199         pointer = motion->device;
200         pointercanvas = canvas;
201     }
202 
203     return FALSE;
204 }
205 
206 /** \brief Callback for handling mouse button events over the emulated
207  *         screen.
208  *
209  *  This forwards any button press or release events on to the light
210  *  pen and mouse subsystems.
211  *
212  *  \param widget    The widget that sent the event.
213  *  \param event     The GdkEventButton event itself.
214  *  \param user_data The video canvas data structure associated with
215  *                   this machine window.
216  *  \return TRUE if no further event processing is necessary.
217  *
218  *  \sa event_box_mouse_motion_cb Further handling of light pen and
219  *      mouse events.
220  *  \sa event_box_scroll_cb Further handling of mouse button events.
221  */
event_box_mouse_button_cb(GtkWidget * widget,GdkEvent * event,gpointer user_data)222 static gboolean event_box_mouse_button_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data)
223 {
224     video_canvas_t *canvas = (video_canvas_t *)user_data;
225 
226     if (event->type == GDK_BUTTON_PRESS) {
227         int button = ((GdkEventButton *)event)->button;
228         if (button == 1) {
229             /* Left mouse button */
230             canvas->pen_buttons |= LP_HOST_BUTTON_1;
231         } else if (button == 3) {
232             /* Right mouse button */
233             canvas->pen_buttons |= LP_HOST_BUTTON_2;
234         }
235         mouse_button(button-1, 1);
236     } else if (event->type == GDK_BUTTON_RELEASE) {
237         int button = ((GdkEventButton *)event)->button;
238         if (button == 1) {
239             /* Left mouse button */
240             canvas->pen_buttons &= ~LP_HOST_BUTTON_1;
241         } else if (button == 3) {
242             /* Right mouse button */
243             canvas->pen_buttons &= ~LP_HOST_BUTTON_2;
244         }
245         mouse_button(button-1, 0);
246     }
247     /* Ignore all other mouse button events, though we'll be sent
248      * things like double- and triple-click. */
249     return FALSE;
250 }
251 
252 /** \brief Callback for handling mouse scroll wheel events over the
253  *         emulated screen.
254  *
255  *  GTK generates these by translating button presses on buttons 4 and
256  *  5 into scroll events; we convert them back and forward them on to
257  *  the mouse subsystem.
258  *
259  *  "Smooth scroll" events are also processed, interpreted as "up
260  *  scroll" or "down scroll" based on the vertical component of the
261  *  smooth-scroll event.
262  *
263  *  \param widget    The widget that sent the event.
264  *  \param event     The GdkEventScroll event itself.
265  *  \param user_data The video canvas data structure associated with
266  *                   this machine window.
267  *  \return TRUE if no further event processing is necessary.
268  *
269  *  \sa event_box_scroll_cb Further handling of mouse button events.
270  */
event_box_scroll_cb(GtkWidget * widget,GdkEvent * event,gpointer user_data)271 static gboolean event_box_scroll_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data)
272 {
273     GdkScrollDirection dir = ((GdkEventScroll *)event)->direction;
274     gdouble smooth_x = 0.0, smooth_y = 0.0;
275     switch (dir) {
276     case GDK_SCROLL_UP:
277         mouse_button(3, 1);
278         break;
279     case GDK_SCROLL_DOWN:
280         mouse_button(4, 1);
281         break;
282     case GDK_SCROLL_SMOOTH:
283         /* Isolate the Y component of a smooth scroll */
284         if (gdk_event_get_scroll_deltas(event, &smooth_x, &smooth_y)) {
285             if (smooth_y < 0) {
286                 mouse_button(3, 1);
287             } else if (smooth_y > 0) {
288                 mouse_button(4, 1);
289             }
290         }
291         break;
292     default:
293         /* Ignore left and right scroll */
294         break;
295     }
296     return FALSE;
297 }
298 
299 
300 /** \brief Create a reusable cursor that may be used as part of this
301  *         widget.
302  *
303  *  GDK cursors are tied to specific displays, so they need to be
304  *  created for each machine window individually.
305  *
306  *  \param widget The widget that will be using this cursor.
307  *  \param name   The name of the cursor to create.
308  *  \return A new, non-floating, GdkCursor reference, or NULL on
309  *          failure.
310  *
311  *  \note Users coming to this code from the more X11-centric GTK2
312  *        will notice that the array of guaranteed-available cursors
313  *        is much smaller. Please continue to only use the cursors
314  *        listed in the documentation for gdk_cursor_new_from_name()
315  *        here.
316  */
make_cursor(GtkWidget * widget,const char * name)317 static GdkCursor *make_cursor(GtkWidget *widget, const char *name)
318 {
319     GdkDisplay *display = gtk_widget_get_display(widget);
320     GdkCursor *result = NULL;
321 
322     if (display) {
323         result = gdk_cursor_new_from_name(display, name);
324         if (result != NULL) {
325             g_object_ref_sink(G_OBJECT(result));
326         }
327     }
328     return result;
329 }
330 
331 
332 /** \brief Frame-advance callback for the hide-mouse-when-idle logic.
333  *
334  *  This function is called as the "tick callback" whenever the mouse
335  *  is hovering over the machine's screen. Its job is primarily to
336  *  manage the mouse cursor:
337  *
338  *  - If the light pen is active, the cursor is always visible and is
339  *    shaped like a crosshair.
340  *  - If the mouse is grabbed, the cursor is never visible.
341  *  - Otherwise, the cursor is visible as a normal mouse pointer as
342  *    long as it's been 60 or fewer ticks since the last time the
343  *    mouse moved.
344  *
345  *  \param widget    The widget that sent the event.
346  *  \param clock     The GdkFrameClock that's managing our ticks.
347  *  \param user_data The video canvas data structure associated with
348  *                   this machine window.
349  *  \return TRUE if no further event processing is necessary.
350  *
351  *  \sa event_box_cross_cb  Manages the lifecycle of this tick
352  *      callback.
353  *  \sa event_box_motion_cb Manages the "ticks since the last time the
354  *      mouse moved" counter.
355  */
event_box_stillness_tick_cb(GtkWidget * widget,GdkFrameClock * clock,gpointer user_data)356 static gboolean event_box_stillness_tick_cb(GtkWidget *widget, GdkFrameClock *clock, gpointer user_data)
357 {
358     video_canvas_t *canvas = (video_canvas_t *)user_data;
359 
360     ++canvas->still_frames;
361 
362     if (ignore_mouse_hide) {
363         GdkWindow *window = gtk_widget_get_window(widget);
364         if (window != NULL) {
365             gdk_window_set_cursor(window, NULL);
366             return TRUE;
367         }
368     }
369 
370 
371     if (_mouse_enabled || (!lightpen_enabled && canvas->still_frames > 60)) {
372         if (canvas->blank_ptr == NULL) {
373             canvas->blank_ptr = make_cursor(widget, "none");
374         }
375         if (canvas->blank_ptr != NULL) {
376             GdkWindow *window = gtk_widget_get_window(widget);
377 
378             if (window) {
379                 gdk_window_set_cursor(window, VICE_EMPTY_POINTER);
380             }
381         }
382     } else {
383         GdkWindow *window = gtk_widget_get_window(widget);
384         if (canvas->pen_ptr == NULL) {
385             canvas->pen_ptr = make_cursor(widget, "crosshair");
386         }
387         if (window) {
388             if (lightpen_enabled && canvas->pen_ptr) {
389                 gdk_window_set_cursor(window, canvas->pen_ptr);
390             } else {
391                 gdk_window_set_cursor(window, NULL);
392             }
393         }
394     }
395 
396     return G_SOURCE_CONTINUE;
397 }
398 
399 /** \brief Callback for managing the hide-pointer-on-idle timings.
400  *
401  *  This callback fires whenever the machine window's canvas gains or
402  *  loses focus over the mouse pointer. It manages the logic that
403  *  hides the mouse pointer after inactivity. Entering the window will
404  *  start the timer, and leaving it will stop it.
405  *
406  *  Leaving the window entirely will also be interpreted as removing
407  *  the light pen from the screen.
408  *
409  *  \param widget    The widget that sent the event.
410  *  \param event     The GdkEventCrossing event itself.
411  *  \param user_data The video canvas data structure associated with
412  *                   this machine window.
413  *  \return TRUE if no further event processing is necessary.
414  *
415  *  \sa event_box_stillness_tick_cb The timer managed by this function.
416  */
event_box_cross_cb(GtkWidget * widget,GdkEvent * event,gpointer user_data)417 static gboolean event_box_cross_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data)
418 {
419     video_canvas_t *canvas = (video_canvas_t *)user_data;
420     GdkEventCrossing *crossing = (GdkEventCrossing *)event;
421 
422     if (!canvas || !event ||
423         (event->type != GDK_ENTER_NOTIFY && event->type != GDK_LEAVE_NOTIFY) ||
424         crossing->mode != GDK_CROSSING_NORMAL) {
425         /* Spurious event. Most likely, this is an event fired because
426          * clicking the canvas altered grab status. */
427         return FALSE;
428     }
429 
430     if (_mouse_enabled) {
431         if (crossing->type == GDK_LEAVE_NOTIFY) {
432             if (pointer) {
433                 /* warp the pointer into the center of the window */
434                 GdkWindow *window = gtk_widget_get_window(canvas->drawing_area);
435                 GdkScreen *screen = gdk_window_get_screen(window);
436                 int window_w = gdk_window_get_width(window);
437                 int window_h = gdk_window_get_height(window);
438                 gdk_device_warp(pointer, screen,
439                                     (window_w / 2) + crossing->x_root - crossing->x,
440                                     (window_h / 2) + crossing->y_root - crossing->y);
441                 warping = 1;
442                 /* grab the pointer */
443                 if (pointerseat == NULL) {
444                     pointerseat = gdk_device_get_seat (pointer);
445                     if (gdk_seat_grab(pointerseat, window,
446                                 GDK_SEAT_CAPABILITY_ALL_POINTING,
447                                 FALSE, VICE_EMPTY_POINTER, event,
448                                 NULL, NULL) != GDK_GRAB_SUCCESS) {
449                         pointerseat = NULL;
450                     }
451                     /* printf("event_box_cross_cb pointer grab\n"); */
452                 }
453             }
454 
455             return FALSE;
456         }
457     } else {
458         /* ungrab the pointer when mouse is not enabled */
459         /* printf("event_box_cross_cb pointer ungrab\n"); */
460         ui_mouse_ungrab_pointer();
461     }
462 
463     if (crossing->type == GDK_ENTER_NOTIFY) {
464         canvas->still_frames = 0;
465         if (canvas->still_frame_callback_id == 0) {
466             canvas->still_frame_callback_id = gtk_widget_add_tick_callback(canvas->drawing_area,
467                                                                            event_box_stillness_tick_cb,
468                                                                            canvas, NULL);
469         }
470     } else {
471         GdkWindow *window = gtk_widget_get_window(canvas->drawing_area);
472 
473         if (window) {
474             gdk_window_set_cursor(window, NULL);
475         }
476         if (canvas->still_frame_callback_id != 0) {
477             gtk_widget_remove_tick_callback(canvas->drawing_area, canvas->still_frame_callback_id);
478             canvas->still_frame_callback_id = 0;
479         }
480         canvas->pen_x = -1;
481         canvas->pen_y = -1;
482         canvas->pen_buttons = 0;
483     }
484     return FALSE;
485 }
486 
487 /** \brief Create a new machine window.
488  *
489  *  A machine window is a GtkGrid that has a menu bar on top, a status
490  *  bar on the bottom, and a renderer-backend specific drawing area in
491  *  the middle. The canvas argument has its relevant fields populated
492  *  by this process.
493  *
494  *  \param canvas The video canvas to populate.
495  *
496  *  \todo At the moment, the renderer backend is selected at compile
497  *        time and cannot be changed. It would be nice to be able to
498  *        fall back to simpler backends if more specialized ones
499  *        fail. This is difficult at present because we cannot know if
500  *        OpenGL is available until long after the window is created
501  *        and realized.
502  */
machine_window_create(video_canvas_t * canvas)503 static void machine_window_create(video_canvas_t *canvas)
504 {
505     GtkWidget *new_drawing_area, *new_event_box;
506     GtkWidget *menu_bar;
507 
508     /* TODO: Make the rendering process transparent enough that this can be selected and altered as-needed */
509 #ifdef HAVE_GTK3_OPENGL
510     canvas->renderer_backend = &vice_opengl_backend;
511 #else
512     canvas->renderer_backend = &vice_cairo_backend;
513 #endif
514 
515     new_drawing_area = canvas->renderer_backend->create_widget(canvas);
516     canvas->drawing_area = new_drawing_area;
517 
518     new_event_box = gtk_event_box_new();
519     gtk_container_add(GTK_CONTAINER(new_event_box), new_drawing_area);
520 
521     gtk_widget_add_events(new_event_box, GDK_POINTER_MOTION_MASK);
522     gtk_widget_add_events(new_event_box, GDK_BUTTON_PRESS_MASK);
523     gtk_widget_add_events(new_event_box, GDK_BUTTON_RELEASE_MASK);
524     gtk_widget_add_events(new_event_box, GDK_SCROLL_MASK);
525 
526     g_signal_connect(new_event_box, "enter-notify-event", G_CALLBACK(event_box_cross_cb), canvas);
527     g_signal_connect(new_event_box, "leave-notify-event", G_CALLBACK(event_box_cross_cb), canvas);
528     g_signal_connect(new_event_box, "motion-notify-event", G_CALLBACK(event_box_motion_cb), canvas);
529     g_signal_connect(new_event_box, "button-press-event", G_CALLBACK(event_box_mouse_button_cb), canvas);
530     g_signal_connect(new_event_box, "button-release-event", G_CALLBACK(event_box_mouse_button_cb), canvas);
531     g_signal_connect(new_event_box, "scroll-event", G_CALLBACK(event_box_scroll_cb), canvas);
532 
533     /* I'm pretty sure when running x128 we get two menu instances, so this
534      * should go somewhere else: call ui_menu_bar_create() once and attach the
535      * result menu to each GtkWindow instance
536      */
537     menu_bar = ui_machine_menu_bar_create();
538 
539     gtk_container_add(GTK_CONTAINER(canvas->grid), menu_bar);
540     gtk_container_add(GTK_CONTAINER(canvas->grid), new_event_box);
541 
542     pointercanvas = canvas;
543 
544     return;
545 }
546 
ui_machine_window_init(void)547 void ui_machine_window_init(void)
548 {
549     ui_set_create_window_func(machine_window_create);
550     return;
551 }
552 
553 /** \brief grab the mouse pointer when mouse emulation is enabled
554  *
555  *  \todo when the emulator starts up with mouse enabled (eg via command line
556  *        options) then "pointer" will not be known and the mouse pointer can
557  *        not be warped. this needs to be fixed somehow.
558  */
ui_mouse_grab_pointer(void)559 void ui_mouse_grab_pointer(void)
560 {
561     /* printf("ui_mouse_grab_pointer\n"); */
562     if (_mouse_enabled) {
563         /* warp the pointer into the center of the window */
564         /* FIXME: we somehow need to find out how to find out about the GdkDevice
565                   for the pointer here */
566         if ((pointercanvas) && (pointer)) {
567             gint root_x, root_y;
568             GdkWindow *window = gtk_widget_get_window(pointercanvas->drawing_area);
569             GdkScreen *screen = gdk_window_get_screen(window);
570             int window_w = gdk_window_get_width(window);
571             int window_h = gdk_window_get_height(window);
572             gdk_window_get_root_origin (window, &root_x, &root_y);
573             gdk_device_warp(pointer, screen, (window_w / 2) + root_x, (window_h / 2) + root_y);
574             warping = 1;
575         }
576         /* the event handlers will take care of the actual grabbing */
577     }
578 }
579 
580 /** \brief ungrab the mouse pointer when it was grabbed before
581  */
ui_mouse_ungrab_pointer(void)582 void ui_mouse_ungrab_pointer(void)
583 {
584     /* printf("ui_mouse_ungrab_pointer\n"); */
585     if (pointerseat) {
586         gdk_seat_ungrab (pointerseat);
587         pointerseat = NULL;
588     }
589 }
590 
591 #endif
592