Files
flwm/Menu.C
2014-11-24 16:29:11 -08:00

668 lines
16 KiB
C

// Menu.cxx
#include "config.h"
#include "Frame.H"
#if DESKTOPS
#include "Desktop.H"
#endif
#include <FL/Fl_Box.H>
#include <FL/Fl_Return_Button.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Menu_Item.H>
#include <FL/fl_draw.H>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "FrameWindow.H"
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
// it is possible for the window to be deleted or withdrawn while
// the menu is up. This will detect that case (with reasonable
// reliability):
static int
window_deleted(Frame* c)
{
return c->state() != NORMAL
&& c->state() != ICONIC
&& c->state() != OTHER_DESKTOP;
}
static void
frame_callback(Fl_Widget*, void*d)
{
Frame* c = (Frame*)d;
if (window_deleted(c)) return;
c->raise();
c->activate(2);
}
#if DESKTOPS
// raise it but also put it on the current desktop:
static void
move_frame_callback(Fl_Widget*, void*d)
{
Frame* c = (Frame*)d;
if (window_deleted(c)) return;
c->desktop(Desktop::current());
c->raise();
c->activate(2);
}
#endif
#define SCREEN_DX 1 // offset to corner of contents area
#define SCREEN_W (MENU_ICON_W-2) // size of area to draw contents in
#define SCREEN_H (MENU_ICON_H-2) // size of area to draw contents in
#define MAX_NESTING_DEPTH 32
extern Fl_Window* Root;
#if FL_MAJOR_VERSION < 2
static void
frame_label_draw(const Fl_Label* o, int X, int Y, int W, int H, Fl_Align align)
{
Frame* f = (Frame*)(o->value);
if (window_deleted(f)) return;
fl_draw_box(FL_THIN_DOWN_BOX, X, Y, MENU_ICON_W, MENU_ICON_H, FL_GRAY);
for (Frame* c = Frame::first; c; c = c->next) {
if (c->state() != UNMAPPED && (c==f || c->is_transient_for(f))) {
int x = ((c->x()-Root->x())*SCREEN_W+Root->w()/2)/Root->w();
int w = (c->w()*SCREEN_W+Root->w()-1)/Root->w();
if (w > SCREEN_W) w = SCREEN_W;
if (w < 3) w = 3;
if (x+w > SCREEN_W) x = SCREEN_W-w;
if (x < 0) x = 0;
int y = ((c->y()-Root->y())*SCREEN_H+Root->h()/2)/Root->h();
int h = (c->h()*SCREEN_H+Root->h()-1)/Root->h();
if (h > SCREEN_H) h = SCREEN_H;
if (h < 3) h = 3;
if (y+h > SCREEN_H) y = SCREEN_H-h;
if (y < 0) y = 0;
fl_color(FL_FOREGROUND_COLOR);
if (c->state() == ICONIC)
fl_rect(X+x+SCREEN_DX, Y+y+SCREEN_DX, w, h);
else
fl_rectf(X+x+SCREEN_DX, Y+y+SCREEN_DX, w, h);
}
}
fl_font(o->font, o->size);
fl_color((Fl_Color)o->color);
const char* l = f->label(); if (!l) l = "unnamed";
// double any ampersands to turn off the underscores:
char buf[256];
if (strchr(l,'&')) {
char* t = buf;
while (t < buf+254 && *l) {
if (*l=='&') *t++ = *l;
*t++ = *l++;
}
*t = 0;
l = buf;
}
fl_draw(l, X+MENU_ICON_W+3, Y, W-MENU_ICON_W-3, H, align);
}
static void
frame_label_measure(const Fl_Label* o, int& W, int& H)
{
Frame* f = (Frame*)(o->value);
if (window_deleted(f)) {W = MENU_ICON_W+3; H = MENU_ICON_H; return;}
const char* l = f->label(); if (!l) l = "unnamed";
fl_font(o->font, o->size);
fl_measure(l, W, H);
W += MENU_ICON_W+3;
if (W > MAX_MENU_WIDTH) W = MAX_MENU_WIDTH;
if (H < MENU_ICON_H) H = MENU_ICON_H;
}
// This labeltype is used for non-frame items so the text can line
// up with the icons:
static void
label_draw(const Fl_Label* o, int X, int Y, int W, int H, Fl_Align align)
{
fl_font(o->font, o->size);
fl_color((Fl_Color)o->color);
fl_draw(o->value, X+MENU_ICON_W+3, Y, W-MENU_ICON_W-3, H, align);
}
static void
label_measure(const Fl_Label* o, int& W, int& H)
{
fl_font(o->font, o->size);
fl_measure(o->value, W, H);
W += MENU_ICON_W+3;
if (W > MAX_MENU_WIDTH) W = MAX_MENU_WIDTH;
if (H < MENU_ICON_H) H = MENU_ICON_H;
}
#define FRAME_LABEL FL_FREE_LABELTYPE
#define TEXT_LABEL Fl_Labeltype(FL_FREE_LABELTYPE+1)
#endif // FL_MAJOR_VERSION < 2
////////////////////////////////////////////////////////////////
static void
cancel_cb(Fl_Widget* w, void*)
{
w->window()->hide();
}
#if DESKTOPS
static void
desktop_cb(Fl_Widget*, void* v)
{
Desktop::current((Desktop*)v);
}
static void
delete_desktop_cb(Fl_Widget*, void* v)
{
delete (Desktop*)v;
}
#if ASK_FOR_NEW_DESKTOP_NAME
static Fl_Input* new_desktop_input = 0;
static void
new_desktop_ok_cb(Fl_Widget* w, void*)
{
w->window()->hide();
Desktop::current(new Desktop(new_desktop_input->value(), Desktop::available_number()));
}
static void
new_desktop_cb(Fl_Widget*, void*)
{
if (!new_desktop_input) {
FrameWindow* w = new FrameWindow(190,90);
new_desktop_input = new Fl_Input(10,30,170,25,"New desktop name:");
new_desktop_input->align(FL_ALIGN_TOP_LEFT);
new_desktop_input->labelfont(FL_BOLD);
Fl_Return_Button* b = new Fl_Return_Button(100,60,80,20,"OK");
b->callback(new_desktop_ok_cb);
Fl_Button* b2 = new Fl_Button(10,60,80,20,"Cancel");
b2->callback(cancel_cb);
w->set_non_modal();
w->end();
}
char buf[120];
sprintf(buf, "Desktop %d", Desktop::available_number());
new_desktop_input->value(buf);
new_desktop_input->window()->hotspot(new_desktop_input);
new_desktop_input->window()->show();
}
#else // !ASK_FOR_NEW_DESKTOP_NAME
static void
new_desktop_cb(Fl_Widget*, void*)
{
char buf[120];
int i = Desktop::available_number();
sprintf(buf, "Desktop %d", i);
Desktop::current(new Desktop(buf, i));
}
#endif
#endif
////////////////////////////////////////////////////////////////
static void
exit_cb(Fl_Widget*, void*)
{
printf("exit_cb\n");
Frame::save_protocol();
exit(0);
}
static void
logout_cb(Fl_Widget*, void*)
{
static FrameWindow* w = 0;
if (!w) {
w = new FrameWindow(190,90);
Fl_Box* l = new Fl_Box(0, 0, 190, 60, "Really log out?");
l->labelfont(FL_BOLD);
Fl_Return_Button* b = new Fl_Return_Button(100,60,80,20,"OK");
b->callback(exit_cb);
Fl_Button* b2 = new Fl_Button(10,60,80,20,"Cancel");
b2->callback(cancel_cb);
w->set_non_modal();
w->end();
}
w->hotspot(w);
w->show();
}
////////////////////////////////////////////////////////////////
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#if XTERM_MENU_ITEM || WMX_MENU_ITEMS
static const char* xtermname = "xterm";
static void
spawn_cb(Fl_Widget*, void*n)
{
char* name = (char*)n;
// strange code thieved from 9wm to avoid leaving zombies
if (fork() == 0) {
if (fork() == 0) {
close(ConnectionNumber(fl_display));
if (name == xtermname) execlp(name, name, "-ut", (void*)0);
else execl(name, name, (void*)0);
fprintf(stderr, "flwm: can't run %s, %s\n", name, strerror(errno));
XBell(fl_display, 70);
exit(1);
}
exit(0);
}
wait((int *) 0);
}
#endif
static Fl_Menu_Item other_menu_items[] = {
#if XTERM_MENU_ITEM
{"New xterm", 0, spawn_cb, (void*)xtermname, 0, 0, 0, MENU_FONT_SIZE},
#endif
#if DESKTOPS
{"New desktop", 0, new_desktop_cb, 0, 0, 0, 0, MENU_FONT_SIZE},
#endif
{"Logout", 0, logout_cb, 0, 0, 0, 0, MENU_FONT_SIZE},
{0}};
#define num_other_items (sizeof(other_menu_items)/sizeof(Fl_Menu_Item))
// use this to fill in a menu location:
static void
init(Fl_Menu_Item& m, const char* data)
{
#ifdef HAVE_STYLES
m.style = 0;
#endif
m.label(data);
#if FL_MAJOR_VERSION > 2
m.flags = fltk::RAW_LABEL;
#else
m.flags = 0;
#endif
m.labeltype(FL_NORMAL_LABEL);
m.shortcut(0);
m.labelfont(MENU_FONT_SLOT);
m.labelsize(MENU_FONT_SIZE);
m.labelcolor(FL_FOREGROUND_COLOR);
}
#if WMX_MENU_ITEMS
// wmx commands are read from ~/.wmx,
// and stored null-separated here by scan_wmx_dir:
// The strings are kept in this single buffer for memory efficiency and
// to avoid freeing and fragmentation as the scan is done many times:
// Each string has a leading character for the nesting level.
static char* wmxbuffer = NULL;
static int wmxbufsize = 0;
static int num_wmx = 0;
static time_t wmx_time = 0;
static int wmx_pathlen = 0;
static int
scan_wmx_dir (char *path, int bufindex, int nest)
{
DIR* dir = opendir(path);
struct stat st;
int pathlen = strlen (path);
if (dir) {
struct dirent* ent;
while ((ent=readdir(dir))) {
if (ent->d_name[0] == '.')
continue;
strcpy(path+pathlen, ent->d_name);
if (stat(path, &st) < 0) continue;
int len = pathlen+strlen(ent->d_name);
while (bufindex+len+2 > wmxbufsize) { // worst-case alloc needs
wmxbufsize = wmxbufsize ? 2*wmxbufsize : 1024;
wmxbuffer = (char*)realloc(wmxbuffer, wmxbufsize);
}
int start = bufindex; // remember where it started so we can delete it
wmxbuffer[bufindex++] = nest; // remember nesting level
strcpy(wmxbuffer+bufindex, path);
bufindex += len+1;
num_wmx++;
if (S_ISDIR(st.st_mode) && (st.st_mode & 0555) && nest<MAX_NESTING_DEPTH) {
strcpy(path+len, "/");
int oldcount = num_wmx;
bufindex = scan_wmx_dir (path, bufindex, nest+1);
if (num_wmx == oldcount) {
// ignore empty directories
bufindex = start;
num_wmx--;
}
} else if (S_ISREG(st.st_mode) && (st.st_mode & 0111)) {
// make sure it exists and is an executable file:
} else {
// ignore other files
bufindex = start;
num_wmx--;
}
}
closedir(dir);
}
return bufindex;
}
// They are then split into this array of char* pointers for sorting:
static char** wmxlist = NULL;
static int wmxlistsize = 0;
// comparison for qsort
// We keep submenus together by noting that they're proper superstrings
static int
wmxCompare(const void *A, const void *B)
{
char* pA = *(char **)A+1; // skip leading nesting level indicator
char* pB = *(char **)B+1;
pA += strspn(pA, "/");
pB += strspn(pB, "/");
// caseless compare
while (*pA && *pB) {
if (toupper(*pA) > toupper(*pB))
return(1);
if (toupper(*pA) < toupper(*pB))
return(-1);
pA++;
pB++;
}
if (*pA)
return(1);
if (*pB)
return(-1);
return(0);
}
void
load_wmx()
{
const char* home=getenv("HOME"); if (!home) home = ".";
char path[1024];
strcpy(path, home);
if (path[strlen(path)-1] != '/') strcat(path, "/");
strcat(path, ".wmx/");
struct stat st; if (stat(path, &st) < 0) return;
if (st.st_mtime == wmx_time) return;
wmx_time = st.st_mtime;
num_wmx = 0;
wmx_pathlen = strlen(path);
scan_wmx_dir(path, 0, 0);
// Build wmxlist
if (num_wmx > wmxlistsize) {
if (wmxlist)
delete [] wmxlist;
wmxlist = new char *[num_wmx];
wmxlistsize = num_wmx;
}
for (int i=0; i<num_wmx; i++) {
char* cmd = wmxbuffer;
for (int j = 0; j < num_wmx; j++) {
wmxlist[j] = cmd;
cmd += strlen(cmd+1)+2;
}
}
qsort(wmxlist, num_wmx, sizeof(char *), wmxCompare);
}
#endif
////////////////////////////////////////////////////////////////
int exit_flag; // set by the -x switch
static int is_active_frame(Frame* c) {
for (Frame* a = Frame::activeFrame(); a; a = a->transient_for())
if (a == c) return 1;
return 0;
}
void
ShowTabMenu(int tab)
{
static char beenhere;
if (!beenhere) {
beenhere = 1;
#if FL_MAJOR_VERSION < 2
Fl::set_labeltype(FRAME_LABEL, frame_label_draw, frame_label_measure);
Fl::set_labeltype(TEXT_LABEL, label_draw, label_measure);
#endif
if (exit_flag) {
Fl_Menu_Item* m = other_menu_items+num_other_items-2;
m->label("Exit");
m->callback(exit_cb);
}
}
static Fl_Menu_Item* menu = 0;
static int arraysize = 0;
#if DESKTOPS
int one_desktop = !Desktop::first->next;
#endif
// count up how many items are on the menu:
int n = num_other_items;
#if WMX_MENU_ITEMS
load_wmx();
if (num_wmx) {
n -= 1; // delete "new xterm"
// count the wmx items, plus an extra for submenu terminators:
int level = 0;
for (int i=0; i<num_wmx; i++) {
int nextlev = (i==num_wmx-1) ? 0 : wmxlist[i+1][0];
if (nextlev < level) {
n += level-nextlev;
level = nextlev;
} else if (nextlev > level)
level++;
n++;
}
}
#endif
#if DESKTOPS
// count number of items per desktop in these variables:
int numsticky = 0;
Desktop* d;
for (d = Desktop::first; d; d = d->next) d->junk = 0;
#endif
// every frame contributes 1 item:
Frame* c;
for (c = Frame::first; c; c = c->next) {
if (c->state() == UNMAPPED || c->transient_for()) continue;
#if DESKTOPS
if (!c->desktop()) {
numsticky++;
} else {
c->desktop()->junk++;
}
#endif
n++;
}
#if DESKTOPS
if (!one_desktop) {
// add the sticky "desktop":
n += 2; if (!numsticky) n++;
if (Desktop::current()) {
n += numsticky;
Desktop::current()->junk += numsticky;
}
// every desktop contributes menu title, null terminator,
// and possible delete:
for (d = Desktop::first; d; d = d->next) {
n += 2; if (!d->junk) n++;
}
}
#endif
if (n > arraysize) {
delete[] menu;
menu = new Fl_Menu_Item[arraysize = n];
}
// build the menu:
n = 0;
const Fl_Menu_Item* preset = 0;
const Fl_Menu_Item* first_on_desk = 0;
#if DESKTOPS
if (one_desktop) {
#endif
for (c = Frame::first; c; c = c->next) {
if (c->state() == UNMAPPED || c->transient_for()) continue;
#if FL_MAJOR_VERSION < 2
init(menu[n],(char*)c);
menu[n].labeltype(FRAME_LABEL);
#else
init(menu[n],c->label());
#endif
menu[n].callback(frame_callback, c);
if (is_active_frame(c)) preset = menu+n;
n++;
}
if (n > 0) first_on_desk = menu;
#if DESKTOPS
} else for (d = Desktop::first; ; d = d->next) {
// this loop adds the "sticky" desktop last, when d==0
if (d == Desktop::current()) preset = menu+n;
init(menu[n], d ? d->name() : "Sticky");
menu[n].callback(desktop_cb, d);
menu[n].flags = FL_SUBMENU;
n++;
if (d && !d->junk) {
init(menu[n],"delete this desktop");
menu[n].callback(delete_desktop_cb, d);
n++;
} else if (!d && !numsticky) {
init(menu[n],"(empty)");
menu[n].callback_ = 0;
menu[n].deactivate();
n++;
} else {
if (d == Desktop::current()) first_on_desk = menu+n;
for (c = Frame::first; c; c = c->next) {
if (c->state() == UNMAPPED || c->transient_for()) continue;
if (c->desktop() == d || (!c->desktop() && d == Desktop::current())) {
init(menu[n],(char*)c);
#if FL_MAJOR_VERSION < 2
init(menu[n],(char*)c);
menu[n].labeltype(FRAME_LABEL);
#else
init(menu[n],c->label());
#endif
menu[n].callback(d == Desktop::current() ?
frame_callback : move_frame_callback, c);
if (d == Desktop::current() && is_active_frame(c)) preset = menu+n;
n++;
}
}
}
menu[n].label(0); n++; // terminator for submenu
if (!d) break;
}
#endif
// For ALT+Tab, move the selection forward or backward:
if (tab > 0 && first_on_desk) {
if (!preset)
preset = first_on_desk;
else {
preset++;
if (!preset->label() || preset->callback_ != frame_callback)
preset = first_on_desk;
}
} else if (tab < 0 && first_on_desk) {
if (preset && preset != first_on_desk)
preset--;
else {
// go to end of menu
preset = first_on_desk;
while (preset[1].label() && preset[1].callback_ == frame_callback)
preset++;
}
}
#if WMX_MENU_ITEMS
// put wmx-style commands above that:
if (num_wmx > 0) {
char* cmd;
int pathlen[MAX_NESTING_DEPTH];
int level = 0;
pathlen[0] = wmx_pathlen;
for (int i = 0; i < num_wmx; i++) {
cmd = wmxlist[i]+1; // skip level number
init(menu[n], cmd+pathlen[level]);
menu[n].callback(spawn_cb, cmd);
#if FL_MAJOR_VERSION < 2
#if DESKTOPS
if (one_desktop)
#endif
if (!level)
menu[n].labeltype(TEXT_LABEL);
#endif
int nextlev = (i==num_wmx-1) ? 0 : wmxlist[i+1][0];
if (nextlev < level) {
// add null terminators to turn off levels
for (; level>nextlev; level--)
init(menu[++n], 0);
} else if (nextlev > level) {
// This should be made a submenu
pathlen[++level] = strlen(cmd)+1; // extra for next trailing /
menu[n].flags = FL_SUBMENU;
menu[n].callback((Fl_Callback*)0);
}
n++;
}
}
// put the fixed menu items at the bottom:
#if XTERM_MENU_ITEM
if (num_wmx) // if wmx commands, delete the built-in xterm item:
memcpy(menu+n, other_menu_items+1, sizeof(other_menu_items)-sizeof(Fl_Menu_Item));
else
#endif
#endif
memcpy(menu+n, other_menu_items, sizeof(other_menu_items));
#if FL_MAJOR_VERSION < 2
#if DESKTOPS
if (one_desktop)
#endif
// fix the menus items so they are indented to align with window names:
while (menu[n].label()) menu[n++].labeltype(TEXT_LABEL);
#endif
const Fl_Menu_Item* picked =
menu->popup(Fl::event_x(), Fl::event_y(), 0, preset);
#if FL_MAJOR_VERSION < 2
if (picked && picked->callback()) picked->do_callback(0);
#endif
}
void ShowMenu() {ShowTabMenu(0);}