2 * Copyright 2022 Olaf Wintermann
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:
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
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.
33 static MainWindow *main_window;
35 static void WindowCreateMenu(MainWindow *win, Widget parent, Arg *args, int nargs);
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 PlayRepeatListCB(Widget w, void *udata, void *cdata);
41 static void PlayAutoPlayCB(Widget w, void *udata, void *cdata);
42 static void ViewFullscreenCB(Widget w, void *udata, void *cdata);
44 static void WindowRealized(MainWindow *win);
46 static int blank_cursor_init = 0;
47 static Pixmap blank_cursor_pixmap;
48 static Cursor blank_cursor;
50 static void init_blank_cursor(Widget w) {
55 blank_cursor_pixmap = XCreateBitmapFromData(XtDisplay(w), XtWindow(w), &data, 1, 1);
56 if(!blank_cursor_pixmap) return;
58 blank_cursor = XCreatePixmapCursor(XtDisplay(w), blank_cursor_pixmap, blank_cursor_pixmap, &c, &c, 0, 0);
60 XFreePixmap(XtDisplay(w), blank_cursor_pixmap);
61 blank_cursor_init = 1;
64 static void window_close_handler(Widget window, void *udata, void *cdata) {
65 FileQuitCB(window, NULL, NULL);
68 static unsigned int keycodeF;
70 static void windowKeyEH(Widget widget, XtPointer data, XEvent *event, Boolean *dispatch) {
71 MainWindow *win = data;
72 if(win->fullscreen && event->xkey.keycode == keycodeF) {
73 WindowFullscreen(main_window, FALSE);
78 static int main_window_is_realized = 0;
80 static void resizeEH(Widget widget, XtPointer data, XEvent *event, Boolean *dispatch) {
81 if(!main_window_is_realized) {
82 if(XtIsRealized(widget)) {
83 main_window_is_realized = 1;
87 WindowAdjustAspectRatio(data);
90 static void WindowRealized(MainWindow *win) {
91 char *open_file = GetOpenFileArg();
93 size_t len = strlen(open_file);
94 char *file = XtMalloc(len+1);
95 memcpy(file, open_file, len);
97 WindowSetFile(win, file);
102 if(!blank_cursor_init) {
103 init_blank_cursor(win->player_widget);
107 static void playerWidgetInputCB(Widget widget, XtPointer u, XtPointer c) {
109 XmDrawingAreaCallbackStruct *cb = c;
111 if(win->player && win->player->isactive) {
112 PlayerHandleInput(win, win->player, cb);
116 static void windowGrabButton(MainWindow *win) {
123 ButtonPressMask | ButtonReleaseMask | PointerMotionMask | FocusChangeMask | EnterWindowMask | LeaveWindowMask,
128 win->buttongrab = True;
131 static void playerEH(Widget widget, XtPointer data, XEvent *event, Boolean *dispatch) {
132 MainWindow *win = data;
133 int etype = event->type;
136 if(etype == EnterNotify) {
138 windowGrabButton(win);
141 if(etype == LeaveNotify) {
143 //XtUngrabButton(win->player_widget, AnyButton, AnyModifier);
144 //win->buttongrab = False;
149 if(etype == ButtonPress || etype == ButtonRelease || etype == KeyPress || etype == KeyRelease) {
150 //printf("button press\n");
154 if(!win->player || win->player->window == 0) return;
156 WindowHandlePlayerEvent(win, event);
159 // redirect key events to the player window
160 //printf("redirect\n");
161 event->xkey.window = win->player->window;
163 XtDisplay(win->player_widget),
171 #define IGNORE_MOTION_THRESHOLD_MS 1000
172 #define MOTION_POS_THRESHOLD_PIX 5
174 #define DOUBLE_CLICK_TIME_MS 500
176 void WindowHandlePlayerEvent(MainWindow *win, XEvent *event) {
177 // event handler for intercepted player mouse events
178 // win->player is not NULL
180 int etype = event->type;
182 if(etype == MotionNotify) {
183 Time cur_motion_time = event->xmotion.time;
184 int x = event->xmotion.x_root;
185 int y = event->xmotion.y_root;
186 if(win->cursorhidden && cur_motion_time - win->player_event_time < IGNORE_MOTION_THRESHOLD_MS) {
187 int diff_x = abs(x - win->mouse_x);
188 int diff_y = abs(y - win->mouse_y);
189 if(diff_x > MOTION_POS_THRESHOLD_PIX || diff_y > MOTION_POS_THRESHOLD_PIX) {
190 WindowShowPlayerCursor(win);
196 win->player_event_time = cur_motion_time;
197 win->motion_playback_time = win->player->playback_time;
201 if(win->pwbuttonpressed) {
202 Display *dp = XtDisplay(win->window);
204 XtUngrabPointer(win->player_widget, CurrentTime);
207 memset(&xev, 0, sizeof(xev));
208 xev.type = ClientMessage;
209 xev.xclient.message_type = XInternAtom(dp, "_NET_WM_MOVERESIZE", False);
210 xev.xclient.window = XtWindow(win->window);
211 xev.xclient.format = 32;
212 xev.xclient.data.l[0] = x;
213 xev.xclient.data.l[1] = y;
214 xev.xclient.data.l[2] = 8; // _NET_WM_MOVERESIZE_MOVE
215 xev.xclient.data.l[3] = 1; // button1
216 xev.xclient.data.l[4] = 1; // source indication
218 XSendEvent(dp, DefaultRootWindow(dp), False, SubstructureRedirectMask | SubstructureNotifyMask, &xev);
220 win->pwbuttonpressed = FALSE;
222 } else if(etype == ButtonPress) {
223 Time t = event->xbutton.time;
224 if(t - win->button_press_time < DOUBLE_CLICK_TIME_MS) {
226 WindowFullscreen(main_window, !win->fullscreen);
227 win->button_press_time = 0;
229 win->button_press_time = t;
231 win->pwbuttonpressed = 1;
232 } else if(etype == ButtonRelease) {
233 win->player_event_time = event->xbutton.time;
234 win->pwbuttonpressed = FALSE;
238 MainWindow* WindowCreate(Display *display) {
242 MainWindow *window = malloc(sizeof(MainWindow));
243 memset(window, 0, sizeof(MainWindow));
244 main_window = window;
248 XtSetArg(args[n], XmNtitle, APP_NAME); n++;
249 window->window = XtAppCreateShell(
252 applicationShellWidgetClass,
253 //vendorShellWidgetClass,
259 Atom wm_delete_window;
260 wm_delete_window = XmInternAtom(
264 XmAddWMProtocolCallback(
267 window_close_handler,
271 XtAddEventHandler(window->window, StructureNotifyMask, False, resizeEH, window);
274 XtSetArg(args[n], XmNwidth, 360); n++;
275 XtSetArg(args[n], XmNheight, 220); n++;
276 XtSetArg(args[n], XmNshadowThickness, 0); n++;
277 Widget container = XmCreateForm(window->window, "form", args, n);
278 XtManageChild(container);
279 XtAddEventHandler(container, KeyPressMask, FALSE, windowKeyEH, window);
282 XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
283 XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
284 XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
285 WindowCreateMenu(window, container, args, n);
288 XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
289 XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
290 XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
291 XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
292 XtSetArg(args[n], XmNtopWidget, window->menubar); n++;
293 XtSetArg(args[n], XmNbackground, BlackPixelOfScreen(XtScreen(window->window))); n++;
294 window->player_widget = XmCreateDrawingArea(container, "player", args, n);
295 XtManageChild(window->player_widget);
296 XtAddCallback(window->player_widget, XmNinputCallback, playerWidgetInputCB, window);
297 XmProcessTraversal(window->player_widget, XmTRAVERSE_CURRENT);
298 XtAddEventHandler(window->player_widget, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | FocusChangeMask |
299 EnterWindowMask | KeyPressMask | KeyReleaseMask |
300 LeaveWindowMask, FALSE, playerEH, window);
304 keycodeF = XKeysymToKeycode(XtDisplay(window->window), XStringToKeysym("F"));
309 MainWindow* GetMainWindow(void) {
313 void WindowShow(MainWindow *win) {
314 XtRealizeWidget(win->window);
318 * Creates a XmPushButton menu item
320 static Widget createMenuItem(
325 const char *accelerator,
326 char *accelerator_text,
327 XtCallbackProc callback,
333 XmString s1 = XmStringCreateSimple(label);
334 XtSetArg(args[n], XmNlabelString, s1); n++;
335 XtSetArg(args[n], XmNmnemonic, mnemonic); n++;
337 if(accelerator && accelerator_text) {
338 at = XmStringCreateSimple(accelerator_text);
339 XtSetArg(args[n], XmNaccelerator, accelerator); n++;
340 XtSetArg(args[n], XmNacceleratorText, at); n++;
343 Widget menuItem = XmCreatePushButtonGadget(menu, name, args, n);
344 XtManageChild(menuItem);
346 if(at) XmStringFree(at);
349 XtAddCallback(menuItem, XmNactivateCallback, (XtCallbackProc)callback, cbData);
356 * Creates a XmToggleButton menu item
358 static Widget createToggleMenuItem(
363 Boolean defaultValue,
364 const char *accelerator,
365 char *accelerator_text,
366 XtCallbackProc callback,
372 XmString s1 = XmStringCreateSimple(label);
373 XtSetArg(args[n], XmNlabelString, s1); n++;
374 XtSetArg(args[n], XmNmnemonic, mnemonic); n++;
375 XtSetArg(args[n], XmNset, defaultValue); n++;
377 if(accelerator && accelerator_text) {
378 at = XmStringCreateSimple(accelerator_text);
379 XtSetArg(args[n], XmNaccelerator, accelerator); n++;
380 XtSetArg(args[n], XmNacceleratorText, at); n++;
383 Widget menuItem = XmCreateToggleButtonGadget(menu, name, args, n);
384 XtManageChild(menuItem);
386 if(at) XmStringFree(at);
389 XtAddCallback(menuItem, XmNvalueChangedCallback, (XtCallbackProc)callback, cbData);
395 static void WindowCreateMenu(MainWindow *win, Widget parent, Arg *mbargs, int nmbargs) {
396 Widget menubar = XmCreateMenuBar(parent, "menubar", mbargs, nmbargs);
397 XtManageChild(menubar);
398 win->menubar = menubar;
404 XmString s = XmStringCreateSimple("File");
405 XtVaCreateManagedWidget(
407 xmCascadeButtonWidgetClass,
412 Widget fileMenu = XmVaCreateSimplePulldownMenu(menubar, "menu", 0, NULL, NULL);
414 s = XmStringCreateSimple("Playback");
415 XtVaCreateManagedWidget(
417 xmCascadeButtonWidgetClass,
422 Widget playMenu = XmVaCreateSimplePulldownMenu(menubar, "menu", 1, NULL, NULL);
424 s = XmStringCreateSimple("View");
425 Widget viewMenuItem = XtVaCreateManagedWidget(
427 xmCascadeButtonWidgetClass,
432 Widget viewMenu = XmVaCreateSimplePulldownMenu(menubar, "menu", 2, NULL, NULL);
435 createMenuItem(fileMenu, "fileOpen", "Open...", 'O', "Ctrl<Key>O", "Ctrl+O", FileOpenCB, NULL);
436 createMenuItem(fileMenu, "fileQuit", "Exit", 'E', "Ctrl<Key>Q", "Ctrl+Q", FileQuitCB, NULL);
439 win->playRepeatTrackButton = createToggleMenuItem(playMenu, "playRepeatTrack", "Repeat", 'R', False, NULL, NULL, PlayRepeatCB, win);
440 win->playRepeatListButton = createToggleMenuItem(playMenu, "playRepeatList", "Repeat List", 'L', False, NULL, NULL, PlayRepeatListCB, win);
441 win->playAutoPlayButton = createToggleMenuItem(playMenu, "playAutoNext", "Autoplay Folder", 'A', False, NULL, NULL, PlayAutoPlayCB, win);
442 XtVaSetValues(win->playRepeatTrackButton, XmNindicatorType, XmONE_OF_MANY, NULL);
443 XtVaSetValues(win->playRepeatListButton, XmNindicatorType, XmONE_OF_MANY, NULL);
444 XtVaSetValues(win->playAutoPlayButton, XmNindicatorType, XmONE_OF_MANY, NULL);
447 createMenuItem(viewMenu, "viewFullscreen", "Fullscreen", 'F', "<Key>F", "F", ViewFullscreenCB, NULL);
450 void go_fullscreen(Display *dsp, Window win)
453 Atom wm_state = XInternAtom(dsp, "_NET_WM_STATE", False);
454 Atom fullscreen = XInternAtom(dsp, "_NET_WM_STATE_FULLSCREEN", False);
455 memset(&xev, 0, sizeof(xev));
456 xev.type = ClientMessage;
457 xev.xclient.window = win;
458 xev.xclient.message_type = wm_state;
459 xev.xclient.format = 32;
460 xev.xclient.data.l[0] = 1; // _NET_WM_STATE_ADD
461 xev.xclient.data.l[1] = fullscreen;
462 xev.xclient.data.l[2] = 0;
463 XSendEvent(dsp, DefaultRootWindow(dsp), False,
464 SubstructureNotifyMask, &xev);
467 static Atom net_wm_state;
468 static Atom net_wm_state_fullscreen;
469 static int net_wm_atoms_initialized = 0;
471 void WindowFullscreen(MainWindow *win, bool enableFullscreen) {
472 Display *dpy = XtDisplay(win->window);
474 // init net_wm_state atoms
475 if(!net_wm_atoms_initialized) {
476 net_wm_state = XInternAtom(dpy, "_NET_WM_STATE", False);
477 net_wm_state_fullscreen = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False);
478 net_wm_atoms_initialized = 1;
481 WindowMenubarSetVisible(win, !enableFullscreen);
482 if(enableFullscreen && !win->fullscreen) {
483 XtUnmanageChild(main_window->menubar);
484 main_window->fullscreen = TRUE;
485 } else if(!enableFullscreen && win->fullscreen) {
486 XtManageChild(main_window->menubar);
487 main_window->fullscreen = FALSE;
491 memset(&ev, 0, sizeof(XEvent));
492 ev.type = ClientMessage;
493 ev.xclient.window = XtWindow(win->window);
494 ev.xclient.message_type = net_wm_state;
495 ev.xclient.format = 32;
496 ev.xclient.data.l[0] = enableFullscreen ? 1 : 0;
497 ev.xclient.data.l[1] = net_wm_state_fullscreen;
498 ev.xclient.data.l[2] = 0;
499 XSendEvent(dpy, DefaultRootWindow(dpy), False, SubstructureNotifyMask | SubstructureRedirectMask, &ev);
502 void WindowMenubarSetVisible(MainWindow *win, bool visible) {
504 XtManageChild(main_window->menubar);
505 XtVaSetValues(main_window->player_widget, XmNtopAttachment, XmATTACH_WIDGET, XmNtopWidget, main_window->menubar, NULL);
507 XtUnmanageChild(main_window->menubar);
508 XtVaSetValues(main_window->player_widget, XmNtopAttachment, XmATTACH_FORM, NULL);
512 void WindowSetFile(MainWindow *win, char *file) {
519 static void filedialog_end(
522 XmFileSelectionBoxCallbackStruct *selection)
524 XtUnmanageChild(widget);
525 XtDestroyWidget(widget);
528 static void filedialog_select(
531 XmFileSelectionBoxCallbackStruct *selection)
534 if(selection->value) {
535 XmStringGetLtoR(selection->value, XmSTRING_DEFAULT_CHARSET, &value);
537 WindowSetFile(data, value);
538 // no need to free the value, because it is stored in MainWindow
540 PlayerOpenFile(data);
543 filedialog_end(widget, data, NULL);
549 static void FileOpenCB(Widget w, void *udata, void *cdata) {
550 MainWindow *win = main_window;
555 XtSetArg(args[n], XnNshowViewMenu, 1); n++;
556 Widget dialog = XnCreateFileSelectionDialog(win->window, "dialog", args, n);
557 XtAddCallback(dialog, XmNokCallback, (XtCallbackProc)filedialog_select, win);
558 XtAddCallback(dialog, XmNcancelCallback, (XtCallbackProc)filedialog_end, win);
560 Widget dirUp = XnFileSelectionBoxGetChild(dialog, XnFSB_DIR_UP_BUTTON);
561 XtUnmanageChild(dirUp);
563 XtManageChild(dialog);
566 static void FileQuitCB(Widget w, void *udata, void *cdata) {
567 WindowClosePlayer(main_window);
571 static void PlayRepeatCB(Widget w, void *udata, void *cdata) {
572 MainWindow *win = udata;
573 win->repeatTrack = XmToggleButtonGadgetGetState(w);
575 win->autoplayFolder = 0;
576 XtVaSetValues(win->playRepeatListButton, XmNset, 0, NULL);
577 XtVaSetValues(win->playAutoPlayButton, XmNset, 0, NULL);
580 static void PlayRepeatListCB(Widget w, void *udata, void *cdata) {
581 MainWindow *win = udata;
582 win->repeatList = XmToggleButtonGadgetGetState(w);
583 win->repeatTrack = 0;
584 win->autoplayFolder = 0;
585 XtVaSetValues(win->playRepeatTrackButton, XmNset, 0, NULL);
586 XtVaSetValues(win->playAutoPlayButton, XmNset, 0, NULL);
589 static void PlayAutoPlayCB(Widget w, void *udata, void *cdata) {
590 MainWindow *win = udata;
591 win->autoplayFolder = XmToggleButtonGadgetGetState(w);
592 win->repeatTrack = 0;
594 XtVaSetValues(win->playRepeatTrackButton, XmNset, 0, NULL);
595 XtVaSetValues(win->playRepeatListButton, XmNset, 0, NULL);
598 static void ViewFullscreenCB(Widget w, void *udata, void *cdata) {
599 if(main_window->fullscreen) {
600 WindowFullscreen(main_window, FALSE);
602 WindowFullscreen(main_window, TRUE);
607 void WindowAdjustAspectRatio(MainWindow *win) {
608 if(!win->player) return;
609 if(!win->player->isactive || win->player->width <= 0 || win->player->height <= 0) return;
611 // we have a running player width video
612 // adjust window aspect ratio (the window aspect ratio is different from
613 // the video, because of window decoration, menubar and other extra controls)
615 Dimension win_width, win_height;
616 XtVaGetValues(win->window, XmNwidth, &win_width, XmNheight, &win_height, NULL);
617 Dimension player_width, player_height;
618 XtVaGetValues(win->player_widget, XmNwidth, &player_width, XmNheight, &player_height, NULL);
620 double r = (double)win->player->width / (double)win->player->height;
621 double p_width = player_width;
622 double p_height = p_width / r;
624 Dimension new_width = p_width + win_width - player_width;
625 Dimension new_height = p_height + win_height - player_height;
628 hints.flags = PAspect;
629 hints.min_aspect.x = new_width;
630 hints.min_aspect.y = new_height;
631 hints.max_aspect.x = new_width;
632 hints.max_aspect.y = new_height;
633 XSetWMNormalHints(XtDisplay(win->window), XtWindow(win->window), &hints);
636 void WindowClosePlayer(MainWindow *win) {
638 PlayerDestroy(win->player);
641 WindowShowPlayerCursor(win);
644 void WindowHidePlayerCursor(MainWindow *win) {
645 if(!win->cursorhidden && win->player && win->player->window != 0) {
646 XDefineCursor(XtDisplay(win->player_widget), XtWindow(win->player_widget), blank_cursor);
647 win->cursorhidden = True;
648 XFlush(XtDisplay(win->player_widget));
652 void WindowShowPlayerCursor(MainWindow *win) {
653 if(win->cursorhidden && win->player && win->player->window != 0) {
654 XDefineCursor(XtDisplay(win->player_widget), XtWindow(win->player_widget), None);
655 XFlush(XtDisplay(win->player_widget));
657 win->cursorhidden = False;