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