/* * This file is part of firk's window manager for x11 (fwm) * Copyright (C) 2022, 2023 firk * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include "fwm-menu.h" #include "xdg_parser.h" #include "dirscan.h" #include "xkrap.h" #include "xsupp.h" typedef struct { int x0, y0; unsigned w, h, bw; } winsizes; char const * font_name; #ifdef ALLOW_STANDALONE static winprops wp; #endif static GC gc; static struct { unsigned w, h; } root; static struct { Window win; winsizes s; int hovered, clicked; } but; extern void fgcolor(unsigned char c) { static int last_c = -1; if(last_c==c) return; last_c = c; XSetForeground(display, gc, base_color(c)); } extern void bgcolor(unsigned char c) { static int last_c = -1; if(last_c==c) return; last_c = c; XSetBackground(display, gc, base_color(c)); } static int update_screen_size(int w, int h) { if(w<0) w = 0; if(h<0) h = 0; if(w>30000) w = 30000; if(h>30000) h = 30000; if(root.w==(unsigned)w && root.h==(unsigned)h) return 0; root.w = w; root.h = h; return 1; } #ifdef ALLOW_STANDALONE static void calc_sizes(winsizes *ws) { int x, y, w, h, bw; x = wp.x0; y = wp.y0; w = wp.w; h = wp.h; bw = wp.bw; if(w<=0) w = 100; if(h<=0) h = 10; if(w>10000) w = 10000; if(h>10000) h = 10000; if(bw<0) bw = 0; if(bw>100) bw = 100; if(x<0) { x += root.w; if(x<-1) x = -1; x = x - (w - 1); } if(y<0) { y += root.h; if(y<-1) y = -1; y = y - (h - 1); } ws->x0 = x; ws->y0 = y; ws->w = w; ws->h = h; ws->bw = bw; } #endif extern void x11_init(winprops const *wp0) { XSetWindowAttributes wa; update_screen_size(WidthOfScreen(screen), HeightOfScreen(screen)); #ifdef ALLOW_STANDALONE wp = *wp0; if(wp.o<2) calc_sizes(&but.s); else #endif { but.s.w = 100; but.s.h = 10; but.s.bw = 1; } but.win = LIBFWM_CreateWidgetWindow(WCLASS_WIDGET, "fwm-menu", "MENU", but.s.x0, but.s.y0, but.s.w, but.s.h, but.s.bw, base_color(BLACK), base_color(WHITE), ExposureMask|ButtonPressMask|ButtonReleaseMask|PointerMotionMask|LeaveWindowMask|StructureNotifyMask, #ifdef ALLOW_STANDALONE (wp.o==1)?True: #endif False); /* #ifdef ALLOW_STANDALONE wa.override_redirect = (wp.o==1)?True:False; #else wa.override_redirect = False; #endif wa.event_mask = ExposureMask|ButtonPressMask|ButtonReleaseMask|PointerMotionMask|LeaveWindowMask|StructureNotifyMask; wa.background_pixel = base_color(BLACK); wa.border_pixel = base_color(WHITE); but.win = XCreateWindow(display, rootwin, but.s.x0, but.s.y0, but.s.w, but.s.h, but.s.bw, CopyFromParent, CopyFromParent, CopyFromParent, CWOverrideRedirect|CWEventMask|CWBackPixel|CWBorderPixel, &wa); if(!LIBFWM_SetWClass(but.win, WCLASS_WIDGET, "fwm-menu")) logprint("SetWClass() failed"); XSetStandardProperties(display, but.win, "MENU", "MENU", None, NULL, 0, NULL); */ XSelectInput(display, rootwin, StructureNotifyMask); gc = XCreateGC(display, but.win, 0, NULL); fgcolor(WHITE); bgcolor(BLACK); if(font_name && *font_name) XSetFont(display, gc, XLoadFont(display, font_name)); XClearWindow(display, but.win); XMapRaised(display, but.win); XSync(display, False); } static void redraw_button(void) { if(but.s.w<3 || but.s.h<3) return; fgcolor(but.clicked?3:(but.hovered?1:0)); XFillRectangle(display, but.win, gc, 0, 0, but.s.w, but.s.h); fgcolor(WHITE); XDrawString(display, but.win, gc, 5, 14, "< MENU", 6); } static void redraw_menu_add_zone(t_menu_list *ml, int x, int y, int w, int h) { unsigned x1, y1, x2, y2, yy; if(w<=0 || h<=0) return; if(x<0) { w+=x; x=0; if(w<=0) return; } if(y<0) { h+=y; y=0; if(h<=0) return; } x1 = x; y1 = y; x2 = x+w; y2 = y+h; if(x2>ml->gui.w) x2 = ml->gui.w; if(y2>ml->gui.h) y2 = ml->gui.h; if(x1>=x2 || y1>=y2) return; if(y1gui.h-y2MENU_VPAD+ml->e.display_count*MENU_CELL_HEIGHT) ml->gui.vpad_need_redraw = 1; if(x1gui.w-x2gui.hpad_need_redraw = 1; if(y1e.display_start+y1; if(yy>=ml->e.n) break; ml->e.list[yy].need_redraw = 1; } } static unsigned long resize_ticks; static int resize_pending; static unsigned long sel_ticks; static int sel_pending; static int press_pending; static size_t press_JM, press_sel; static int press_x, press_y; static void redraw_menu(t_menu_list *ml, int force); static int menu_list_click(size_t JM, int x, int y) { t_menu_list *ml; unsigned j; size_t idx; ml = menus+JM; if(x=ml->gui.w || y=ml->e.display_count) return 0; idx = ml->e.display_start+j; if(ml->e.sel!=idx) return 0; assert(idxe.n); if(ml->e.list[idx].isdir) return 0; run_menu_entry(JM, idx); return 1; } static void menu_list_set_sel(size_t JM, int x, int y) { t_menu_list *ml; unsigned j; size_t oldsel, newsel; sel_ticks = get_ms(); sel_pending = 1; ml = menus+JM; if(x=ml->gui.w) { newsel = ml->e.sel_stable; goto found; } if(y>=MENU_VPAD) { j = (y-MENU_VPAD)/MENU_CELL_HEIGHT; if(je.display_count) { newsel = ml->e.display_start+j; assert(newsele.n); goto found; } } newsel = ml->e.n; found: oldsel = ml->e.sel; if(oldsel!=newsel) { if(oldsele.n) ml->e.list[oldsel].need_redraw = 1; if(newsele.n) ml->e.list[newsel].need_redraw = 1; ml->e.sel = newsel; redraw_menu(ml, 0); } return; } static void redraw_menu(t_menu_list *ml, int force) { size_t j, start, max, sel, eh; t_menu_entry *e; char const *title; unsigned w, h, y, hmax; XPoint points[3]; if(ml->gui.w<3 || ml->gui.h<3) return; w = ml->gui.w; h = ml->gui.h; if(w<=2*MENU_HPAD || h<=2*MENU_VPAD) { fgcolor(0); XFillRectangle(display, ml->gui.win, gc, 0, 0, ml->gui.w, ml->gui.h); return; } hmax = 2*MENU_VPAD+ml->e.n*MENU_CELL_HEIGHT; if(h>hmax) { if(force || ml->gui.vpad_need_redraw) { fgcolor(0); XFillRectangle(display, ml->gui.win, gc, 0, hmax, w, h-hmax); } h = hmax; } w -= 2*MENU_HPAD; h -= 2*MENU_VPAD; ml->e.display_count = eh = h/MENU_CELL_HEIGHT; ml->e.display_sstep = (eh>10)?5:((eh>5)?eh-5:1); h = eh*MENU_CELL_HEIGHT; start = ml->e.display_start; sel = ml->e.sel; max = ml->e.n; e = ml->e.list; assert(eh<=max); if((force || ml->gui.hpad_need_redraw) && MENU_HPAD) { fgcolor(0); XFillRectangle(display, ml->gui.win, gc, 0, MENU_VPAD, MENU_HPAD, h); XFillRectangle(display, ml->gui.win, gc, ml->gui.w-MENU_HPAD, MENU_VPAD, MENU_HPAD, h); } if(start>max-eh) { ml->e.display_start = start = max-eh; force = 1; } if((force || ml->gui.vpad_need_redraw) && MENU_VPAD) { fgcolor(0); XFillRectangle(display, ml->gui.win, gc, 0, 0, ml->gui.w, MENU_VPAD); XFillRectangle(display, ml->gui.win, gc, 0, MENU_VPAD+h, ml->gui.w, ml->gui.h-MENU_VPAD-h); if(start) { fgcolor(15); points[0].x = ml->gui.w/2; points[0].y = MENU_VPAD/4; points[1].x = ml->gui.w/2-5; points[2].x = ml->gui.w/2+5; points[2].y = points[1].y = MENU_VPAD/4*3+1; XFillPolygon(display, ml->gui.win, gc, points, 3, Convex, CoordModeOrigin); } if(startgui.w/2; points[2].y = MENU_VPAD + h + MENU_VPAD/4*3; points[0].x = ml->gui.w/2-5; points[1].x = ml->gui.w/2+5; points[1].y = points[0].y = MENU_VPAD + h + MENU_VPAD/4-1; XFillPolygon(display, ml->gui.win, gc, points, 3, Convex, CoordModeOrigin); } } ml->gui.hpad_need_redraw = ml->gui.vpad_need_redraw = 0; for(j=0; je.list + (start+j); if(!force && !e->need_redraw) continue; e->need_redraw = 0; y = MENU_VPAD+j*MENU_CELL_HEIGHT; fgcolor((start+j==sel)?((press_pending && menus+press_JM==ml)?3:1):0); XFillRectangle(display, ml->gui.win, gc, MENU_HPAD, y, w, MENU_CELL_HEIGHT); fgcolor(15); if(w>MENU_CELL_HPAD*2) { if(e->isdir) { if(w>MENU_CELL_HPAD*2+MENU_CELL_DIRW) { if(ml->gui.sub_dir) { points[0].x = MENU_HPAD+w-(MENU_CELL_HPAD/2+MENU_CELL_DIRW/4); points[1].x = MENU_HPAD+w-(MENU_CELL_HPAD/2+MENU_CELL_DIRW/4*3); points[2].x = MENU_HPAD+w-(MENU_CELL_HPAD/2+MENU_CELL_DIRW/4*3); } else { points[0].x = MENU_HPAD+MENU_CELL_HPAD/2+MENU_CELL_DIRW/4; points[1].x = MENU_HPAD+MENU_CELL_HPAD/2+MENU_CELL_DIRW/4*3; points[2].x = MENU_HPAD+MENU_CELL_HPAD/2+MENU_CELL_DIRW/4*3; } points[0].y = y+MENU_CELL_HEIGHT/2; points[1].y = y+MENU_CELL_HEIGHT/4; points[2].y = y+MENU_CELL_HEIGHT-MENU_CELL_HEIGHT/4; XFillPolygon(display, ml->gui.win, gc, points, 3, Convex, CoordModeOrigin); draw_string_limwidth_utf8(display, ml->gui.win, gc, MENU_HPAD+MENU_CELL_HPAD+(ml->gui.sub_dir?0:MENU_CELL_DIRW), y+MENU_CELL_OFFS, w-2*MENU_CELL_HPAD-MENU_CELL_DIRW, e->name, strlen(e->name)); } } else { if(!(title = e->xdg.title)) title = e->name; draw_string_limwidth_utf8(display, ml->gui.win, gc, MENU_HPAD+MENU_CELL_HPAD, y+MENU_CELL_OFFS, w-2*MENU_CELL_HPAD, title, strlen(title)); } } } } static unsigned int int_to_usize(int v) { if(v<=0) return 0; if(v>=10000) return 10000; return (unsigned int)v; } extern void x11_redraw(void) { size_t j; redraw_button(); for(j=0; je.list; je.n; j++,me++) { st = me->xdg.title?me->xdg.title:me->name; w = text_width_utf8(gc, st, strlen(st)); if(me->isdir) w += MENU_CELL_DIRW; if(w>0 && (unsigned)w>maxw) maxw = w; } if(maxw>MENU_WIDTH_MAX-MENU_HPAD*2-MENU_CELL_HPAD*2) mw = MENU_WIDTH_MAX; else mw = maxw+MENU_HPAD*2+MENU_CELL_HPAD*2; if(mwMENU_BORDER_WIDTH*2) maxh-=MENU_BORDER_WIDTH*2; else maxh = 0; if(maxhmaxh) mh = maxh; mh -= (mh-MENU_VPAD*2)%MENU_CELL_HEIGHT; /* 3) x0,y0 */ if(idx>=1) { x0 = menus[idx-1].gui.x0 - mw + MENU_HPAD; jj = menus[idx-1].e.sel; if(jj>menus[idx-1].e.display_start) jj -= menus[idx-1].e.display_start; else jj = 0; y0 = menus[idx-1].gui.y0 + jj*MENU_CELL_HEIGHT; if(!ml->e.n) y0 += MENU_VPAD; } else { x0 = but.s.x0 - mw + MENU_HPAD; y0 = but.s.y0; } if(x0<0) x0 = 0; if(y0<0) y0 = 0; if(mh>=root.h) y0 = 0; else if(root.h-mh<(unsigned)y0) y0 = root.h-mh; /* 4) store results */ if(ml->gui.w!=mw || ml->gui.h!=mh || ml->gui.x0!=x0 || ml->gui.y0!=y0) { ml->gui.w = mw; ml->gui.h = mh; ml->gui.x0 = x0; ml->gui.y0 = y0; return 1; } return 0; } static void x11_menu_list_open(char const *name) { t_menu_list *ml; XSetWindowAttributes wa; menu_list_open(name); assert(NM>=1 && NM<=MAX_MENU_NESTING); ml = menus+(NM-1); x11_menu_list_calc_coords(NM-1, ml); wa.override_redirect = True; wa.event_mask = ExposureMask|ButtonPressMask|ButtonReleaseMask|PointerMotionMask|LeaveWindowMask; wa.background_pixel = base_color(0); wa.border_pixel = base_color(15); ml->gui.win = XCreateWindow(display, rootwin, ml->gui.x0, ml->gui.y0, ml->gui.w, ml->gui.h, MENU_BORDER_WIDTH, CopyFromParent, CopyFromParent, CopyFromParent, CWOverrideRedirect|CWEventMask|CWBackPixel|CWBorderPixel, &wa); XSetStandardProperties(display, ml->gui.win, "MENU", "MENU", None, NULL, 0, NULL); XClearWindow(display, ml->gui.win); XMapRaised(display, ml->gui.win); redraw_menu(ml, 1); #ifdef AUTOCLOSE_ON_CLICK_OUTSIDE if(NM==1) XGrabPointer(display, rootwin, True, ButtonPressMask|PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime); /* ^ this triggers LeaveNotify event for the button window */ #endif XSync(display, False); } static void x11_menu_list_close(void) { t_menu_list *ml; assert(NM>=1 && NM<=MAX_MENU_NESTING); ml = menus+(NM-1); XDestroyWindow(display, ml->gui.win); menu_list_close(); #ifdef AUTOCLOSE_ON_CLICK_OUTSIDE if(!NM) XUngrabPointer(display, CurrentTime); /* ^ this triggers EnterNotify event for the window contains pointer now */ #endif } static void x11_menu_list_fix(void) { size_t j, jj; sel_pending = 0; for(j=0; jj+1) x11_menu_list_close(); jj = menus[j].e.sel_stable = menus[j].e.sel; if(jjx0 || but.s.y0!=ws->y0 || but.s.w!=ws->w || but.s.h!=ws->h || but.s.bw!=ws->bw) { but.s = *ws; button_moved = 1; } } #ifdef ALLOW_STANDALONE if(!ws && wp.o<2) { calc_sizes(&ws2); /* TODO: may be calc_sizes() should use actual width/height here instead of preconfigured ones; * anyway we are not resizing window here, only moving */ if(but.s.x0!=ws2.x0 || but.s.y0!=ws2.y0) { but.s.x0 = ws2.x0; but.s.y0 = ws2.y0; XMoveWindow(display, but.win, but.s.x0, but.s.y0); button_moved = 1; } } #endif if(button_moved || resize_pending) { for(j=0; jgui.win, ml->gui.x0, ml->gui.y0, ml->gui.w, ml->gui.h); } x11_redraw(); } resize_pending = 0; } extern void x11_event_loop(void) { XEvent ev; unsigned long t; while(1) { if(sel_pending && !LIBFWM_WaitEventUntil(sel_ticks+SEL_TIMEOUT_MS, 1)) { x11_menu_list_fix(); continue; } if(resize_pending && !LIBFWM_WaitEventUntil(resize_ticks+RESIZE_MS, 1)) { resize_fix(NULL); continue; } XNextEvent(display, &ev); if(x11_handle_event(&ev)) continue; } } static void reset_press_pending(void) { t_menu_list *ml; if(!press_pending) return; press_pending = 0; if(press_JM>=NM) return; ml = menus+press_JM; if(ml->e.sel!=press_sel || press_sel>=ml->e.n) return; ml->e.list[press_sel].need_redraw = 1; redraw_menu(ml, 0); } extern int x11_handle_event(XEvent *ev) { size_t j; int ym; t_menu_list *ml; winsizes ws; switch(ev->type) { case ConfigureNotify: if(ev->xconfigure.window==rootwin) { if(update_screen_size(ev->xconfigure.width, ev->xconfigure.height)) { resize_ticks = get_ms(); resize_pending = 1; } return 1; } if(ev->xconfigure.window==but.win) { ws.x0 = ev->xconfigure.x; ws.y0 = ev->xconfigure.y; ws.w = int_to_usize(ev->xconfigure.width); ws.h = int_to_usize(ev->xconfigure.height); ws.bw = int_to_usize(ev->xconfigure.border_width); resize_fix(&ws); return 1; } for(j=0,ml=menus; jxconfigure.window==ml->gui.win) { ml->gui.w = int_to_usize(ev->xconfigure.width); ml->gui.h = int_to_usize(ev->xconfigure.height); redraw_menu(ml, 1); return 1; } break; case Expose: if(ev->xexpose.window==but.win) { if(ev->xexpose.count==0) redraw_button(); return 1; } for(j=0,ml=menus; jxexpose.window==ml->gui.win) { redraw_menu_add_zone(menus+j, ev->xexpose.x, ev->xexpose.y, ev->xexpose.width, ev->xexpose.height); if(ev->xexpose.count==0) redraw_menu(menus+j, 0); return 1; } break; case ButtonPress: if(ev->xbutton.window==but.win) { if(ev->xbutton.button==Button1 && but.hovered) { but.clicked = 1; redraw_button(); } return 1; } for(j=0,ml=menus; jxbutton.window==ml->gui.win) { if(ev->xbutton.button==Button1) { menu_list_set_sel(j, ev->xbutton.x, ev->xbutton.y); if(sel_pending) x11_menu_list_fix(); press_pending = 1; press_JM = j; press_sel = ml->e.sel; press_x = ev->xbutton.x; press_y = ev->xbutton.y; if(ml->e.sele.n) { ml->e.list[ml->e.sel].need_redraw = 1; redraw_menu(ml, 0); } return 1; } if(ev->xbutton.button==Button4) { reset_press_pending(); if(ml->e.display_start > ml->e.display_sstep) { ml->e.display_start-=ml->e.display_sstep; redraw_menu(ml, 1); } else if(ml->e.display_start) { ml->e.display_start = 0; redraw_menu(ml, 1); } menu_list_set_sel(j, ev->xbutton.x, ev->xbutton.y); return 1; } if(ev->xbutton.button==Button5) { size_t sstep, start, count, total; reset_press_pending(); sstep = ml->e.display_sstep; count = ml->e.display_count; start = ml->e.display_start; total = ml->e.n; if(total<=count) start = 0; else if(start>total-count) start = total-count; else if(start+sstep>=total-count) start = total-count; else start += sstep; /* else if(start>sstep) start -= sstep; else start = 0;*/ // printf("total %u count %u start %u sstep %u\n", total, count, start, sstep); if(start!=ml->e.display_start) { ml->e.display_start = start; redraw_menu(ml, 1); } menu_list_set_sel(j, ev->xbutton.x, ev->xbutton.y); return 1; } return 1; } #ifdef AUTOCLOSE_ON_CLICK_OUTSIDE if(NM && (ev->xbutton.button==Button1 || ev->xbutton.button==Button2 || ev->xbutton.button==Button3)) { but.clicked = 0; but.hovered = 0; while(NM) x11_menu_list_close(); redraw_button(); } #endif break; case ButtonRelease: if(ev->xbutton.window==but.win) { if(ev->xbutton.button==Button1 && but.clicked) { but.clicked = 0; if(NM) { while(NM) x11_menu_list_close(); } else x11_menu_list_open(NULL); redraw_button(); } return 1; } for(j=0,ml=menus; jxbutton.window==ml->gui.win) { if(ev->xbutton.button==Button1) { if(!press_pending) return 1; if(press_JM==j || press_x==ev->xbutton.x || press_y==ev->xbutton.y) { if(menu_list_click(j, ev->xbutton.x, ev->xbutton.y)>0) { #ifdef AUTOCLOSE_ON_CLICK_OUTSIDE but.clicked = 0; but.hovered = 0; while(NM) x11_menu_list_close(); redraw_button(); #endif } } reset_press_pending(); return 1; } } reset_press_pending(); break; case MotionNotify: reset_press_pending(); if(ev->xmotion.window==but.win) { if(!but.hovered) { but.hovered = 1; redraw_button(); } return 1; } for(j=0,ml=menus; jxmotion.window==ml->gui.win) { menu_list_set_sel(j, ev->xmotion.x, ev->xmotion.y); return 1; } break; case LeaveNotify: reset_press_pending(); if(ev->xcrossing.window==but.win) { if(but.hovered && !NM) { but.hovered = 0; redraw_button(); } return 1; } for(j=0,ml=menus; jxcrossing.window==ml->gui.win) { menu_list_set_sel(j, -1, -1); return 1; } break; } return 0; }