b0d9a3420792569cc92d22c2abe74e0ceead7c64
[uwplayer.git] / application / window.c
1 /*
2  * Copyright 2022 Olaf Wintermann
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a 
5  * copy of this software and associated documentation files (the "Software"), 
6  * to deal in the Software without restriction, including without limitation 
7  * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
8  * and/or sell copies of the Software, and to permit persons to whom the 
9  * Software is furnished to do so, subject to the following conditions:
10  * 
11  * The above copyright notice and this permission notice shall be included in 
12  * all copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
17  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
19  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
20  * DEALINGS IN THE SOFTWARE.
21  */
22
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26
27 #include "window.h"
28 #include "main.h"
29 #include "player.h"
30
31 #include "Fsb.h"
32
33 static MainWindow *main_window;
34
35 static void WindowCreateMenu(MainWindow *win, Widget parent, Arg *args, int nargs);
36
37 static void FileOpenCB(Widget w, void *udata, void *cdata);
38 static void FileQuitCB(Widget w, void *udata, void *cdata);
39 static void PlayRepeatCB(Widget w, void *udata, void *cdata);
40 static void ViewFullscreenCB(Widget w, void *udata, void *cdata);
41
42 static void WindowRealized(MainWindow *win);
43
44 static int blank_cursor_init = 0;
45 static Pixmap blank_cursor_pixmap;
46 static Cursor blank_cursor;
47
48 static void init_blank_cursor(Widget w) {
49     char data = 0;
50     
51     XColor c;
52     
53     blank_cursor_pixmap = XCreateBitmapFromData(XtDisplay(w), XtWindow(w), &data, 1, 1);
54     if(!blank_cursor_pixmap) return;
55     
56     blank_cursor = XCreatePixmapCursor(XtDisplay(w), blank_cursor_pixmap, blank_cursor_pixmap, &c, &c, 0, 0);
57     
58     XFreePixmap(XtDisplay(w), blank_cursor_pixmap);
59     blank_cursor_init = 1;
60 }
61
62 static void window_close_handler(Widget window, void *udata, void *cdata) {
63     FileQuitCB(window, NULL, NULL);
64 }
65
66 static unsigned int keycodeF;
67
68 static void windowKeyEH(Widget widget, XtPointer data, XEvent *event, Boolean *dispatch) {
69     MainWindow *win = data;
70     if(win->fullscreen && event->xkey.keycode == keycodeF) {
71         WindowFullscreen(main_window, FALSE);
72         *dispatch = FALSE;
73     }
74 }
75
76 static int main_window_is_realized = 0;
77
78 static void resizeEH(Widget widget, XtPointer data, XEvent *event, Boolean *dispatch) {  
79     if(!main_window_is_realized) {
80         if(XtIsRealized(widget)) {
81             main_window_is_realized = 1;
82             WindowRealized(data);
83         }
84     }
85     WindowAdjustAspectRatio(data);
86 }
87
88 static void WindowRealized(MainWindow *win) {
89     char *open_file = GetOpenFileArg();
90     if(open_file) {
91         size_t len = strlen(open_file);
92         char *file = XtMalloc(len+1);
93         memcpy(file, open_file, len);
94         file[len] = 0;
95         WindowSetFile(win, file);
96         PlayerOpenFile(win);
97         CleanOpenFileArg();
98     }
99     
100     if(!blank_cursor_init) {
101         init_blank_cursor(win->player_widget);
102     }
103 }
104
105 static void playerWidgetInputCB(Widget widget, XtPointer u, XtPointer c) {
106     MainWindow *win = u;
107     XmDrawingAreaCallbackStruct *cb = c;
108     
109     if(win->player && win->player->isactive) {
110         PlayerHandleInput(win, win->player, cb);
111     }
112 }
113
114 static void windowGrabButton(MainWindow *win) {
115     //printf("grab\n");
116     XtGrabButton(
117                 win->player_widget,
118                 AnyButton,
119                 AnyModifier,
120                 True,
121                 ButtonPressMask | ButtonReleaseMask | PointerMotionMask | FocusChangeMask | EnterWindowMask | LeaveWindowMask,
122                 GrabModeAsync,
123                 GrabModeAsync,
124                 None,
125                 None);
126     win->buttongrab = True;
127 }
128
129 static void playerEH(Widget widget, XtPointer data, XEvent *event, Boolean *dispatch) {
130     MainWindow *win = data;
131     int etype = event->type;
132     
133     ///*
134     if(etype == EnterNotify) {
135         //printf("enter\n");
136         windowGrabButton(win);
137         return;
138     }
139     if(etype == LeaveNotify) {
140         //printf("leave\n");
141         //XtUngrabButton(win->player_widget, AnyButton, AnyModifier); 
142         //win->buttongrab = False;
143         return;
144     }
145     
146     int pass = 0;
147     if(etype == ButtonPress || etype == ButtonRelease || etype == KeyPress || etype == KeyRelease) {
148         //printf("button press\n");
149         pass = 1;
150     }
151     
152     if(!win->player || win->player->window == 0) return;
153     
154     WindowHandlePlayerEvent(win, event);
155     
156     if(pass) {
157         // redirect key events to the player window
158         //printf("redirect\n");
159         event->xkey.window = win->player->window;
160         XSendEvent(
161                 XtDisplay(win->player_widget),
162                 win->player->window,
163                 True,
164                 0,
165                 event);
166     }
167 }
168
169 #define IGNORE_MOTION_THRESHOLD_MS 1000
170 #define MOTION_POS_THRESHOLD_PIX   5
171
172 #define DOUBLE_CLICK_TIME_MS       500
173
174 void WindowHandlePlayerEvent(MainWindow *win, XEvent *event) {
175     // event handler for intercepted player mouse events
176     // win->player is not NULL
177     
178     int etype = event->type;
179     
180     if(etype == MotionNotify) {
181         Time cur_motion_time = event->xmotion.time;
182         int x = event->xmotion.x;
183         int y = event->xmotion.y;
184         if(win->cursorhidden && cur_motion_time - win->player_event_time < IGNORE_MOTION_THRESHOLD_MS) {
185             int diff_x = abs(x - win->mouse_x_orig);
186             int diff_y = abs(y - win->mouse_y_orig);
187             if(diff_x > MOTION_POS_THRESHOLD_PIX || diff_y > MOTION_POS_THRESHOLD_PIX) {
188                 WindowShowPlayerCursor(win);
189             }
190         } else {
191             win->mouse_x_orig = x;
192             win->mouse_y_orig = y;
193         }
194         win->player_event_time = cur_motion_time;
195         win->motion_playback_time = win->player->playback_time;
196     } else if(etype == ButtonPress) {
197         Time t = event->xbutton.time;
198         if(t - win->button_press_time < DOUBLE_CLICK_TIME_MS) {
199             // double click
200             WindowFullscreen(main_window, !win->fullscreen);
201             win->button_press_time = 0;
202         } else {
203             win->button_press_time = t;
204         }
205     } else if(etype == ButtonRelease) {
206         win->player_event_time = event->xbutton.time;
207     }
208 }
209
210 MainWindow* WindowCreate(Display *display) {
211     Arg args[32];
212     int n;
213      
214     MainWindow *window = malloc(sizeof(MainWindow));
215     memset(window, 0, sizeof(MainWindow));
216     main_window = window;
217     
218     // toplevel window
219     n = 0;
220     XtSetArg(args[n], XmNtitle, APP_NAME); n++;
221     window->window = XtAppCreateShell(
222             APP_NAME,
223             APP_CLASS,
224             applicationShellWidgetClass,
225             //vendorShellWidgetClass,
226             display,
227             args,
228             n);
229     
230     // close handler
231     Atom wm_delete_window;
232     wm_delete_window = XmInternAtom(
233             display,
234             "WM_DELETE_WINDOW",
235             0);
236     XmAddWMProtocolCallback(
237             window->window,
238             wm_delete_window,
239             window_close_handler,
240             window);
241     
242     // resize handler
243     XtAddEventHandler(window->window, StructureNotifyMask, False, resizeEH, window);
244     
245     n = 0;
246     XtSetArg(args[n], XmNwidth, 360); n++;
247     XtSetArg(args[n], XmNheight, 220); n++;
248     XtSetArg(args[n], XmNshadowThickness, 0); n++;
249     Widget container = XmCreateForm(window->window, "form", args, n);
250     XtManageChild(container);
251     XtAddEventHandler(container, KeyPressMask, FALSE, windowKeyEH, window);
252     
253     n = 0;
254     XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
255     XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
256     XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
257     WindowCreateMenu(window, container, args, n);
258     
259     n = 0;
260     XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
261     XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
262     XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
263     XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
264     XtSetArg(args[n], XmNtopWidget, window->menubar); n++;
265     XtSetArg(args[n], XmNbackground, BlackPixelOfScreen(XtScreen(window->window))); n++;
266     window->player_widget = XmCreateDrawingArea(container, "player", args, n);
267     XtManageChild(window->player_widget);
268     XtAddCallback(window->player_widget, XmNinputCallback, playerWidgetInputCB, window);
269     XmProcessTraversal(window->player_widget, XmTRAVERSE_CURRENT);
270     XtAddEventHandler(window->player_widget, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | FocusChangeMask |
271                  EnterWindowMask | KeyPressMask | KeyReleaseMask |
272                   LeaveWindowMask, FALSE, playerEH, window);
273     
274     
275     // get F keycode
276     keycodeF = XKeysymToKeycode(XtDisplay(window->window), XStringToKeysym("F"));
277     
278     return window;
279 }
280
281 MainWindow* GetMainWindow(void) {
282     return main_window;
283 }
284
285 void WindowShow(MainWindow *win) {
286     XtRealizeWidget(win->window);
287 }
288
289 /*
290  * Creates a XmPushButton menu item
291  */
292 static Widget createMenuItem(
293         Widget menu,
294         char *name,
295         char *label,
296         char mnemonic,
297         const char *accelerator,
298         char *accelerator_text,
299         XtCallbackProc callback,
300         void *cbData)
301 {
302     Arg args[16];
303     int n = 0;
304     
305     XmString s1 = XmStringCreateSimple(label);
306     XtSetArg(args[n], XmNlabelString, s1); n++;
307     XtSetArg(args[n], XmNmnemonic, mnemonic); n++;
308     XmString at = NULL;
309     if(accelerator && accelerator_text) {
310         at = XmStringCreateSimple(accelerator_text);
311         XtSetArg(args[n], XmNaccelerator, accelerator); n++;
312         XtSetArg(args[n], XmNacceleratorText, at); n++;
313     }
314     
315     Widget menuItem = XmCreatePushButtonGadget(menu, name, args, n);
316     XtManageChild(menuItem);
317     XmStringFree(s1);
318     if(at) XmStringFree(at);
319     
320     if(callback) {
321         XtAddCallback(menuItem, XmNactivateCallback, (XtCallbackProc)callback, cbData);
322     }
323     
324     return menuItem;
325 }
326
327 /*
328  * Creates a XmToggleButton menu item
329  */
330 static Widget createToggleMenuItem(
331         Widget menu,
332         char *name,
333         char *label,
334         char mnemonic,
335         Boolean defaultValue,
336         const char *accelerator,
337         char *accelerator_text,
338         XtCallbackProc callback,
339         void *cbData)
340 {
341     Arg args[16];
342     int n = 0;
343     
344     XmString s1 = XmStringCreateSimple(label);
345     XtSetArg(args[n], XmNlabelString, s1); n++;
346     XtSetArg(args[n], XmNmnemonic, mnemonic); n++;
347     XtSetArg(args[n], XmNset, defaultValue); n++;
348     XmString at = NULL;
349     if(accelerator && accelerator_text) {
350         at = XmStringCreateSimple(accelerator_text);
351         XtSetArg(args[n], XmNaccelerator, accelerator); n++;
352         XtSetArg(args[n], XmNacceleratorText, at); n++;
353     }
354     
355     Widget menuItem = XmCreateToggleButtonGadget(menu, name, args, n);
356     XtManageChild(menuItem);
357     XmStringFree(s1);
358     if(at) XmStringFree(at);
359     
360     if(callback) {
361         XtAddCallback(menuItem, XmNvalueChangedCallback, (XtCallbackProc)callback, cbData);
362     }
363     
364     return menuItem;
365 }
366
367 static void WindowCreateMenu(MainWindow *win, Widget parent, Arg *mbargs, int nmbargs) {
368     Widget menubar = XmCreateMenuBar(parent, "menubar", mbargs, nmbargs);
369     XtManageChild(menubar);
370     win->menubar = menubar;
371     
372     Arg args[16];
373     int n;
374     
375     // menus
376     XmString s = XmStringCreateSimple("File");
377     XtVaCreateManagedWidget(
378             "menuitem",
379             xmCascadeButtonWidgetClass,
380             menubar,
381             XmNlabelString, s,
382             NULL);
383     XmStringFree(s);
384     Widget fileMenu = XmVaCreateSimplePulldownMenu(menubar, "menu", 0, NULL, NULL);
385     
386     s = XmStringCreateSimple("Playback");
387     XtVaCreateManagedWidget(
388             "menuitem",
389             xmCascadeButtonWidgetClass,
390             menubar,
391             XmNlabelString, s,
392             NULL);
393     XmStringFree(s);
394     Widget playMenu = XmVaCreateSimplePulldownMenu(menubar, "menu", 1, NULL, NULL);
395     
396     s = XmStringCreateSimple("View");
397     Widget viewMenuItem = XtVaCreateManagedWidget(
398             "menuitem",
399             xmCascadeButtonWidgetClass,
400             menubar,
401             XmNlabelString, s,
402             NULL);
403     XmStringFree(s);
404     Widget viewMenu = XmVaCreateSimplePulldownMenu(menubar, "menu", 2, NULL, NULL);
405     
406     // file menu
407     createMenuItem(fileMenu, "fileOpen", "Open...", 'O', "Ctrl<Key>O", "Ctrl+O", FileOpenCB, NULL);
408     createMenuItem(fileMenu, "fileQuit", "Exit", 'E', "Ctrl<Key>Q", "Ctrl+Q", FileQuitCB, NULL);
409     
410     // play menu
411     createToggleMenuItem(playMenu, "playRepeatTrack", "Repeat", 'R', False, NULL, NULL, PlayRepeatCB, win);
412     
413     // view menu
414     createMenuItem(viewMenu, "viewFullscreen", "Fullscreen", 'F', "<Key>F", "F", ViewFullscreenCB, NULL);
415 }
416
417 void go_fullscreen(Display *dsp, Window win)
418 {
419   XEvent xev;
420   Atom wm_state = XInternAtom(dsp, "_NET_WM_STATE", False);
421   Atom fullscreen = XInternAtom(dsp, "_NET_WM_STATE_FULLSCREEN", False);
422   memset(&xev, 0, sizeof(xev));
423   xev.type = ClientMessage;
424   xev.xclient.window = win;
425   xev.xclient.message_type = wm_state;
426   xev.xclient.format = 32;
427   xev.xclient.data.l[0] = 1; // _NET_WM_STATE_ADD
428   xev.xclient.data.l[1] = fullscreen;
429   xev.xclient.data.l[2] = 0;
430   XSendEvent(dsp, DefaultRootWindow(dsp), False,
431     SubstructureNotifyMask, &xev);
432 }
433
434 static Atom net_wm_state;
435 static Atom net_wm_state_fullscreen;
436 static int net_wm_atoms_initialized = 0;
437
438 void WindowFullscreen(MainWindow *win, bool enableFullscreen) {
439     Display *dpy = XtDisplay(win->window);
440     
441     // init net_wm_state atoms
442     if(!net_wm_atoms_initialized) {
443         net_wm_state = XInternAtom(dpy, "_NET_WM_STATE", False);
444         net_wm_state_fullscreen = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False);
445         net_wm_atoms_initialized = 1;
446     }
447     
448     WindowMenubarSetVisible(win, !enableFullscreen);
449     if(enableFullscreen && !win->fullscreen) {
450         XtUnmanageChild(main_window->menubar);
451         main_window->fullscreen = TRUE;
452     } else if(!enableFullscreen && win->fullscreen) {
453         XtManageChild(main_window->menubar);
454         main_window->fullscreen = FALSE;
455     }
456     
457     XEvent ev;
458     memset(&ev, 0, sizeof(XEvent));
459     ev.type = ClientMessage;
460     ev.xclient.window = XtWindow(win->window);
461     ev.xclient.message_type = net_wm_state;
462     ev.xclient.format = 32;
463     ev.xclient.data.l[0] = enableFullscreen ? 1 : 0;
464     ev.xclient.data.l[1] = net_wm_state_fullscreen;
465     ev.xclient.data.l[2] = 0;
466     XSendEvent(dpy, DefaultRootWindow(dpy), False, SubstructureNotifyMask | SubstructureRedirectMask, &ev);
467 }
468
469 void WindowMenubarSetVisible(MainWindow *win, bool visible) {
470     if(visible) {
471         XtManageChild(main_window->menubar);
472         XtVaSetValues(main_window->player_widget, XmNtopAttachment, XmATTACH_WIDGET, XmNtopWidget, main_window->menubar, NULL);
473     } else {
474         XtUnmanageChild(main_window->menubar);
475         XtVaSetValues(main_window->player_widget, XmNtopAttachment, XmATTACH_FORM, NULL);
476     }
477 }
478
479 void WindowSetFile(MainWindow *win, char *file) {
480     if(win->file) {
481         XtFree(win->file);
482     }
483     win->file = file;
484 }
485
486 static void filedialog_end(
487         Widget widget,
488         MainWindow *data,
489         XmFileSelectionBoxCallbackStruct *selection)
490 {
491     XtUnmanageChild(widget);
492     XtDestroyWidget(widget);
493 }
494
495 static void filedialog_select(
496         Widget widget,
497         MainWindow *data,
498         XmFileSelectionBoxCallbackStruct *selection)
499 {
500     char *value = NULL;
501     if(selection->value) {
502         XmStringGetLtoR(selection->value, XmSTRING_DEFAULT_CHARSET, &value);
503         if(value) {
504             WindowSetFile(data, value);
505             // no need to free the value, because it is stored in MainWindow
506             
507             PlayerOpenFile(data);
508         }
509     }
510     filedialog_end(widget, data, NULL);
511 }
512
513
514
515
516 static void FileOpenCB(Widget w, void *udata, void *cdata) {
517     MainWindow *win = main_window;
518     
519     Arg args[16];
520     int n = 0;
521     
522     XtSetArg(args[n], XnNshowViewMenu, 1); n++;
523     Widget dialog = XnCreateFileSelectionDialog(win->window, "dialog", args, n);
524     XtAddCallback(dialog, XmNokCallback, (XtCallbackProc)filedialog_select, win);
525     XtAddCallback(dialog, XmNcancelCallback, (XtCallbackProc)filedialog_end, win);
526     
527     Widget dirUp = XnFileSelectionBoxGetChild(dialog, XnFSB_DIR_UP_BUTTON);
528     XtUnmanageChild(dirUp);
529     
530     XtManageChild(dialog);
531 }
532
533 static void FileQuitCB(Widget w, void *udata, void *cdata) {
534     WindowClosePlayer(main_window);
535     ApplicationExit();
536 }
537
538 static void PlayRepeatCB(Widget w, void *udata, void *cdata) {
539     MainWindow *win = udata;
540     win->repeatTrack = XmToggleButtonGadgetGetState(w);
541 }
542
543 static void ViewFullscreenCB(Widget w, void *udata, void *cdata) {
544     if(main_window->fullscreen) {
545         WindowFullscreen(main_window, FALSE);
546     } else {
547         WindowFullscreen(main_window, TRUE);
548     }
549     
550 }
551
552 void WindowAdjustAspectRatio(MainWindow *win) {
553     if(!win->player) return;
554     if(!win->player->isactive || win->player->width <= 0 || win->player->height <= 0) return;
555       
556     // we have a running player width video
557     // adjust window aspect ratio (the window aspect ratio is different from
558     // the video, because of window decoration, menubar and other extra controls)
559     
560     Dimension win_width, win_height;
561     XtVaGetValues(win->window, XmNwidth, &win_width, XmNheight, &win_height, NULL);
562     Dimension player_width, player_height;
563     XtVaGetValues(win->player_widget, XmNwidth, &player_width, XmNheight, &player_height, NULL);
564     
565     double r = (double)win->player->width / (double)win->player->height;
566     double p_width = player_width;
567     double p_height = p_width / r;
568     
569     Dimension new_width = p_width + win_width - player_width;
570     Dimension new_height = p_height + win_height - player_height;
571     
572     XSizeHints hints;
573     hints.flags = PAspect;
574     hints.min_aspect.x = new_width;
575     hints.min_aspect.y = new_height;
576     hints.max_aspect.x = new_width;
577     hints.max_aspect.y = new_height;
578     XSetWMNormalHints(XtDisplay(win->window), XtWindow(win->window), &hints);
579 }
580
581 void WindowClosePlayer(MainWindow *win) {
582     if(win->player) {
583         PlayerDestroy(win->player);
584     }
585     win->player = NULL;
586     WindowShowPlayerCursor(win);
587 }
588
589 void WindowHidePlayerCursor(MainWindow *win) {
590     if(!win->cursorhidden && win->player && win->player->window != 0) {
591         XDefineCursor(XtDisplay(win->player_widget), XtWindow(win->player_widget), blank_cursor);
592         win->cursorhidden = True;
593         XFlush(XtDisplay(win->player_widget));
594     }
595 }
596
597 void WindowShowPlayerCursor(MainWindow *win) {
598     if(win->cursorhidden && win->player && win->player->window != 0) {
599         XDefineCursor(XtDisplay(win->player_widget), XtWindow(win->player_widget), None);
600         XFlush(XtDisplay(win->player_widget));
601     }
602     win->cursorhidden = False;
603 }