17bda64e7d5d2f7f54d64d5bdb86b51d56d49411
[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 ViewFullscreenCB(Widget w, void *udata, void *cdata);
39
40 static void WindowRealized(MainWindow *win);
41
42 static int blank_cursor_init = 0;
43 static Pixmap blank_cursor_pixmap;
44 static Cursor blank_cursor;
45
46 static void init_blank_cursor(Widget w) {
47     char data = 0;
48     
49     XColor c;
50     
51     blank_cursor_pixmap = XCreateBitmapFromData(XtDisplay(w), XtWindow(w), &data, 1, 1);
52     if(!blank_cursor_pixmap) return;
53     
54     blank_cursor = XCreatePixmapCursor(XtDisplay(w), blank_cursor_pixmap, blank_cursor_pixmap, &c, &c, 0, 0);
55     
56     XFreePixmap(XtDisplay(w), blank_cursor_pixmap);
57     blank_cursor_init = 1;
58 }
59
60 static void window_close_handler(Widget window, void *udata, void *cdata) {
61     WindowClosePlayer(main_window);
62     ApplicationExit();
63 }
64
65 static unsigned int keycodeF;
66
67 static void windowKeyEH(Widget widget, XtPointer data, XEvent *event, Boolean *dispatch) {
68     MainWindow *win = data;
69     if(win->fullscreen && event->xkey.keycode == keycodeF) {
70         WindowFullscreen(main_window, FALSE);
71         *dispatch = FALSE;
72     }
73 }
74
75 static int main_window_is_realized = 0;
76
77 static void resizeEH(Widget widget, XtPointer data, XEvent *event, Boolean *dispatch) {  
78     if(!main_window_is_realized) {
79         if(XtIsRealized(widget)) {
80             main_window_is_realized = 1;
81             WindowRealized(data);
82         }
83     }
84     WindowAdjustAspectRatio(data);
85 }
86
87 static void WindowRealized(MainWindow *win) {
88     char *open_file = GetOpenFileArg();
89     if(open_file) {
90         size_t len = strlen(open_file);
91         char *file = XtMalloc(len+1);
92         memcpy(file, open_file, len);
93         file[len] = 0;
94         WindowSetFile(win, file);
95         PlayerOpenFile(win);
96         CleanOpenFileArg();
97     }
98     
99     if(!blank_cursor_init) {
100         init_blank_cursor(win->player_widget);
101     }
102 }
103
104 static void playerWidgetInputCB(Widget widget, XtPointer u, XtPointer c) {
105     MainWindow *win = u;
106     XmDrawingAreaCallbackStruct *cb = c;
107     
108     if(win->player && win->player->isactive) {
109         PlayerHandleInput(win, win->player, cb);
110     }
111 }
112
113 static void windowGrabButton(MainWindow *win) {
114     //printf("grab\n");
115     XtGrabButton(
116                 win->player_widget,
117                 AnyButton,
118                 AnyModifier,
119                 True,
120                 ButtonPressMask | ButtonReleaseMask | PointerMotionMask | FocusChangeMask | EnterWindowMask | LeaveWindowMask,
121                 GrabModeAsync,
122                 GrabModeAsync,
123                 None,
124                 None);
125     win->buttongrab = True;
126 }
127
128 static void playerEH(Widget widget, XtPointer data, XEvent *event, Boolean *dispatch) {
129     MainWindow *win = data;
130     int etype = event->type;
131     
132     ///*
133     if(etype == EnterNotify) {
134         //printf("enter\n");
135         windowGrabButton(win);
136         return;
137     }
138     if(etype == LeaveNotify) {
139         //printf("leave\n");
140         //XtUngrabButton(win->player_widget, AnyButton, AnyModifier); 
141         //win->buttongrab = False;
142         return;
143     }
144     
145     int pass = 0;
146     if(etype == ButtonPress || etype == ButtonRelease || etype == KeyPress || etype == KeyRelease) {
147         //printf("button press\n");
148         pass = 1;
149     }
150     
151     if(!win->player || win->player->window == 0) return;
152     
153     WindowHandlePlayerEvent(win, event);
154     
155     if(pass) {
156         // redirect key events to the player window
157         //printf("redirect\n");
158         event->xkey.window = win->player->window;
159         XSendEvent(
160                 XtDisplay(win->player_widget),
161                 win->player->window,
162                 True,
163                 0,
164                 event);
165     }
166 }
167
168 #define IGNORE_MOTION_THRESHOLD_MS 1000
169 #define MOTION_POS_THRESHOLD_PIX   5
170
171 #define DOUBLE_CLICK_TIME_MS       500
172
173 void WindowHandlePlayerEvent(MainWindow *win, XEvent *event) {
174     // event handler for intercepted player mouse events
175     // win->player is not NULL
176     
177     int etype = event->type;
178     
179     if(etype == MotionNotify) {
180         Time cur_motion_time = event->xmotion.time;
181         int x = event->xmotion.x;
182         int y = event->xmotion.y;
183         if(win->cursorhidden && cur_motion_time - win->player_event_time < IGNORE_MOTION_THRESHOLD_MS) {
184             int diff_x = abs(x - win->mouse_x_orig);
185             int diff_y = abs(y - win->mouse_y_orig);
186             if(diff_x > MOTION_POS_THRESHOLD_PIX || diff_y > MOTION_POS_THRESHOLD_PIX) {
187                 WindowShowPlayerCursor(win);
188             }
189         } else {
190             win->mouse_x_orig = x;
191             win->mouse_y_orig = y;
192         }
193         win->player_event_time = cur_motion_time;
194         win->motion_playback_time = win->player->playback_time;
195     } else if(etype == ButtonPress) {
196         Time t = event->xbutton.time;
197         if(t - win->button_press_time < DOUBLE_CLICK_TIME_MS) {
198             // double click
199             WindowFullscreen(main_window, !win->fullscreen);
200             win->button_press_time = 0;
201         } else {
202             win->button_press_time = t;
203         }
204     } else if(etype == ButtonRelease) {
205         win->player_event_time = event->xbutton.time;
206     }
207 }
208
209 MainWindow* WindowCreate(Display *display) {
210     Arg args[32];
211     int n;
212      
213     MainWindow *window = malloc(sizeof(MainWindow));
214     memset(window, 0, sizeof(MainWindow));
215     main_window = window;
216     
217     // toplevel window
218     n = 0;
219     XtSetArg(args[n], XmNtitle, APP_NAME); n++;
220     window->window = XtAppCreateShell(
221             APP_NAME,
222             APP_CLASS,
223             applicationShellWidgetClass,
224             //vendorShellWidgetClass,
225             display,
226             args,
227             n);
228     
229     // close handler
230     Atom wm_delete_window;
231     wm_delete_window = XmInternAtom(
232             display,
233             "WM_DELETE_WINDOW",
234             0);
235     XmAddWMProtocolCallback(
236             window->window,
237             wm_delete_window,
238             window_close_handler,
239             window);
240     
241     // resize handler
242     XtAddEventHandler(window->window, StructureNotifyMask, False, resizeEH, window);
243     
244     n = 0;
245     XtSetArg(args[n], XmNwidth, 360); n++;
246     XtSetArg(args[n], XmNheight, 220); n++;
247     XtSetArg(args[n], XmNshadowThickness, 0); n++;
248     Widget container = XmCreateForm(window->window, "form", args, n);
249     XtManageChild(container);
250     XtAddEventHandler(container, KeyPressMask, FALSE, windowKeyEH, window);
251     
252     n = 0;
253     XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
254     XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
255     XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
256     WindowCreateMenu(window, container, args, n);
257     
258     n = 0;
259     XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
260     XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
261     XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
262     XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
263     XtSetArg(args[n], XmNtopWidget, window->menubar); n++;
264     XtSetArg(args[n], XmNbackground, BlackPixelOfScreen(XtScreen(window->window))); n++;
265     window->player_widget = XmCreateDrawingArea(container, "player", args, n);
266     XtManageChild(window->player_widget);
267     XtAddCallback(window->player_widget, XmNinputCallback, playerWidgetInputCB, window);
268     XmProcessTraversal(window->player_widget, XmTRAVERSE_CURRENT);
269     XtAddEventHandler(window->player_widget, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | FocusChangeMask |
270                  EnterWindowMask | KeyPressMask | KeyReleaseMask |
271                   LeaveWindowMask, FALSE, playerEH, window);
272     
273     
274     // get F keycode
275     keycodeF = XKeysymToKeycode(XtDisplay(window->window), XStringToKeysym("F"));
276     
277     return window;
278 }
279
280 MainWindow* GetMainWindow(void) {
281     return main_window;
282 }
283
284 void WindowShow(MainWindow *win) {
285     XtRealizeWidget(win->window);
286 }
287
288 /*
289  * Creates a XmPushButton menu item
290  */
291 static Widget createMenuItem(
292         Widget menu,
293         char *name,
294         char *label,
295         char mnemonic,
296         const char *accelerator,
297         char *accelerator_text,
298         XtCallbackProc callback,
299         void *cbData)
300 {
301     Arg args[16];
302     int n = 0;
303     
304     XmString s1 = XmStringCreateSimple(label);
305     XtSetArg(args[n], XmNlabelString, s1); n++;
306     XtSetArg(args[n], XmNmnemonic, mnemonic); n++;
307     XmString at = NULL;
308     if(accelerator && accelerator_text) {
309         at = XmStringCreateSimple(accelerator_text);
310         XtSetArg(args[n], XmNaccelerator, accelerator); n++;
311         XtSetArg(args[n], XmNacceleratorText, at); n++;
312     }
313     
314     Widget menuItem = XmCreatePushButtonGadget(menu, name, args, n);
315     XtManageChild(menuItem);
316     XmStringFree(s1);
317     if(at) XmStringFree(at);
318     
319     if(callback) {
320         XtAddCallback(menuItem, XmNactivateCallback, (XtCallbackProc)callback, cbData);
321     }
322     
323     return menuItem;
324 }
325
326 static void WindowCreateMenu(MainWindow *win, Widget parent, Arg *mbargs, int nmbargs) {
327     Widget menubar = XmCreateMenuBar(parent, "menubar", mbargs, nmbargs);
328     XtManageChild(menubar);
329     win->menubar = menubar;
330     
331     Arg args[16];
332     int n;
333     
334     // menus
335     XmString s = XmStringCreateSimple("File");
336     Widget fileMenuItem = XtVaCreateManagedWidget(
337             "menuitem",
338             xmCascadeButtonWidgetClass,
339             menubar,
340             XmNlabelString, s,
341             NULL);
342     XmStringFree(s);
343     Widget fileMenu = XmVaCreateSimplePulldownMenu(menubar, "menu", 0, NULL, NULL);
344     
345     s = XmStringCreateSimple("Playback");
346     Widget playMenuItem = XtVaCreateManagedWidget(
347             "menuitem",
348             xmCascadeButtonWidgetClass,
349             menubar,
350             XmNlabelString, s,
351             NULL);
352     XmStringFree(s);
353     Widget playMenu = XmVaCreateSimplePulldownMenu(menubar, "menu", 1, NULL, NULL);
354     
355     s = XmStringCreateSimple("View");
356     Widget viewMenuItem = XtVaCreateManagedWidget(
357             "menuitem",
358             xmCascadeButtonWidgetClass,
359             menubar,
360             XmNlabelString, s,
361             NULL);
362     XmStringFree(s);
363     Widget viewMenu = XmVaCreateSimplePulldownMenu(menubar, "menu", 2, NULL, NULL);
364     
365     // file menu
366     createMenuItem(fileMenu, "fileOpen", "Open...", 'O', "Ctrl<Key>O", "Ctrl+O", FileOpenCB, NULL);
367     
368     // view menu
369     createMenuItem(viewMenu, "viewFullscreen", "Fullscreen", 'F', "<Key>F", "F", ViewFullscreenCB, NULL);
370 }
371
372 void go_fullscreen(Display *dsp, Window win)
373 {
374   XEvent xev;
375   Atom wm_state = XInternAtom(dsp, "_NET_WM_STATE", False);
376   Atom fullscreen = XInternAtom(dsp, "_NET_WM_STATE_FULLSCREEN", False);
377   memset(&xev, 0, sizeof(xev));
378   xev.type = ClientMessage;
379   xev.xclient.window = win;
380   xev.xclient.message_type = wm_state;
381   xev.xclient.format = 32;
382   xev.xclient.data.l[0] = 1; // _NET_WM_STATE_ADD
383   xev.xclient.data.l[1] = fullscreen;
384   xev.xclient.data.l[2] = 0;
385   XSendEvent(dsp, DefaultRootWindow(dsp), False,
386     SubstructureNotifyMask, &xev);
387 }
388
389 static Atom net_wm_state;
390 static Atom net_wm_state_fullscreen;
391 static int net_wm_atoms_initialized = 0;
392
393 void WindowFullscreen(MainWindow *win, bool enableFullscreen) {
394     Display *dpy = XtDisplay(win->window);
395     
396     // init net_wm_state atoms
397     if(!net_wm_atoms_initialized) {
398         net_wm_state = XInternAtom(dpy, "_NET_WM_STATE", False);
399         net_wm_state_fullscreen = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False);
400         net_wm_atoms_initialized = 1;
401     }
402     
403     WindowMenubarSetVisible(win, !enableFullscreen);
404     if(enableFullscreen && !win->fullscreen) {
405         XtUnmanageChild(main_window->menubar);
406         main_window->fullscreen = TRUE;
407     } else if(!enableFullscreen && win->fullscreen) {
408         XtManageChild(main_window->menubar);
409         main_window->fullscreen = FALSE;
410     }
411     
412     XEvent ev;
413     memset(&ev, 0, sizeof(XEvent));
414     ev.type = ClientMessage;
415     ev.xclient.window = XtWindow(win->window);
416     ev.xclient.message_type = net_wm_state;
417     ev.xclient.format = 32;
418     ev.xclient.data.l[0] = enableFullscreen ? 1 : 0;
419     ev.xclient.data.l[1] = net_wm_state_fullscreen;
420     ev.xclient.data.l[2] = 0;
421     XSendEvent(dpy, DefaultRootWindow(dpy), False, SubstructureNotifyMask | SubstructureRedirectMask, &ev);
422 }
423
424 void WindowMenubarSetVisible(MainWindow *win, bool visible) {
425     if(visible) {
426         XtManageChild(main_window->menubar);
427         XtVaSetValues(main_window->player_widget, XmNtopAttachment, XmATTACH_WIDGET, XmNtopWidget, main_window->menubar, NULL);
428     } else {
429         XtUnmanageChild(main_window->menubar);
430         XtVaSetValues(main_window->player_widget, XmNtopAttachment, XmATTACH_FORM, NULL);
431     }
432 }
433
434 void WindowSetFile(MainWindow *win, char *file) {
435     if(win->file) {
436         XtFree(win->file);
437     }
438     win->file = file;
439 }
440
441 static void filedialog_end(
442         Widget widget,
443         MainWindow *data,
444         XmFileSelectionBoxCallbackStruct *selection)
445 {
446     XtUnmanageChild(widget);
447     XtDestroyWidget(widget);
448 }
449
450 static void filedialog_select(
451         Widget widget,
452         MainWindow *data,
453         XmFileSelectionBoxCallbackStruct *selection)
454 {
455     char *value = NULL;
456     if(selection->value) {
457         XmStringGetLtoR(selection->value, XmSTRING_DEFAULT_CHARSET, &value);
458         if(value) {
459             WindowSetFile(data, value);
460             // no need to free the value, because it is stored in MainWindow
461             
462             PlayerOpenFile(data);
463         }
464     }
465     filedialog_end(widget, data, NULL);
466 }
467
468
469
470
471 static void FileOpenCB(Widget w, void *udata, void *cdata) {
472     MainWindow *win = main_window;
473     
474     Arg args[16];
475     int n = 0;
476     
477     XtSetArg(args[n], XnNshowViewMenu, 1); n++;
478     Widget dialog = XnCreateFileSelectionDialog(win->window, "dialog", args, n);
479     XtAddCallback(dialog, XmNokCallback, (XtCallbackProc)filedialog_select, win);
480     XtAddCallback(dialog, XmNcancelCallback, (XtCallbackProc)filedialog_end, win);
481     
482     Widget dirUp = XnFileSelectionBoxGetChild(dialog, XnFSB_DIR_UP_BUTTON);
483     XtUnmanageChild(dirUp);
484     
485     XtManageChild(dialog);
486 }
487
488 static void ViewFullscreenCB(Widget w, void *udata, void *cdata) {
489     if(main_window->fullscreen) {
490         WindowFullscreen(main_window, FALSE);
491     } else {
492         WindowFullscreen(main_window, TRUE);
493     }
494     
495 }
496
497 void WindowAdjustAspectRatio(MainWindow *win) {
498     if(!win->player) return;
499     if(!win->player->isactive || win->player->width <= 0 || win->player->height <= 0) return;
500       
501     // we have a running player width video
502     // adjust window aspect ratio (the window aspect ratio is different from
503     // the video, because of window decoration, menubar and other extra controls)
504     
505     Dimension win_width, win_height;
506     XtVaGetValues(win->window, XmNwidth, &win_width, XmNheight, &win_height, NULL);
507     Dimension player_width, player_height;
508     XtVaGetValues(win->player_widget, XmNwidth, &player_width, XmNheight, &player_height, NULL);
509     
510     double r = (double)win->player->width / (double)win->player->height;
511     double p_width = player_width;
512     double p_height = p_width / r;
513     
514     Dimension new_width = p_width + win_width - player_width;
515     Dimension new_height = p_height + win_height - player_height;
516     
517     XSizeHints hints;
518     hints.flags = PAspect;
519     hints.min_aspect.x = new_width;
520     hints.min_aspect.y = new_height;
521     hints.max_aspect.x = new_width;
522     hints.max_aspect.y = new_height;
523     XSetWMNormalHints(XtDisplay(win->window), XtWindow(win->window), &hints);
524 }
525
526 void WindowClosePlayer(MainWindow *win) {
527     if(win->player) {
528         PlayerDestroy(win->player);
529     }
530     win->player = NULL;
531     WindowShowPlayerCursor(win);
532 }
533
534 void WindowHidePlayerCursor(MainWindow *win) {
535     if(!win->cursorhidden && win->player && win->player->window != 0) {
536         XDefineCursor(XtDisplay(win->player_widget), XtWindow(win->player_widget), blank_cursor);
537         win->cursorhidden = True;
538         XFlush(XtDisplay(win->player_widget));
539     }
540 }
541
542 void WindowShowPlayerCursor(MainWindow *win) {
543     if(win->cursorhidden && win->player && win->player->window != 0) {
544         XDefineCursor(XtDisplay(win->player_widget), XtWindow(win->player_widget), None);
545         XFlush(XtDisplay(win->player_widget));
546     }
547     win->cursorhidden = False;
548 }