## popup-menu.pkg
# Compiled by:
#
src/lib/x-kit/widget/xkit-widget.sublib# A simple menu package.
#
# TODO:
# defaults for submenus XXX BUGGO FIXME
### "The Macintosh uses an experimental pointing
### device called a mouse. There is no evidence
### that people want to use these things."
###
### -- John Dvorak 1984
stipulate
include package threadkit; # threadkit is from
src/lib/src/lib/thread-kit/src/core-thread-kit/threadkit.pkg #
package wg = widget; # widget is from
src/lib/x-kit/widget/old/basic/widget.pkg #
package xc = xclient; # xclient is from
src/lib/x-kit/xclient/xclient.pkg #
package g2d = geometry2d; # geometry2d is from
src/lib/std/2d/geometry2d.pkgherein
package popup_menu
: (weak) Popup_Menu # Popup_Menu is from
src/lib/x-kit/widget/old/menu/popup-menu.api {
Popup_Menu(X)
=
POPUP_MENU List( Popup_Menu_Item(X) )
also
Popup_Menu_Item(X)
= POPUP_MENU_ITEM (String, X)
| POPUP_SUBMENU (String, Popup_Menu(X))
;
Popup_Menu_Position
= PUT_POPUP_MENU_UPPERLEFT_ON_SCREEN g2d::Point # Position the popup menu's upper-left corner at this screen coordinate.
| PUT_POPUP_MENU_ITEM_BENEATH_MOUSE Int
# Position the popu menu with mouse cursor centered over given item (0 is first item).
;
Where_Info
=
WHERE_INFO
{
mouse_button: xc::Mousebutton,
window_point: g2d::Point,
screen_point: g2d::Point,
timestamp: xc::xserver_timestamp::Xserver_Timestamp
};
menu_font = "8x13";
padding = 1; # padding between window border and actual menu items
border_thickness = 1; # Border width
inset = 1; # Add'l x padding to ensure highlighting encloses text and icon.
vspace = 1; # y padding per item, for same reason as above.
total_padding = padding+padding;
no_buttons
=
xc::make_mousebutton_state [];
Label = LABEL { box: g2d::Box,
text_pos: g2d::Point,
text: String
};
Menu_Representation(X)
=
MENU_REPRESENTATION
{
size: g2d::Size,
item_high: Int,
font: xc::Font,
label: Null_Or( Label ), # note: only top-level menus have labels
items: List( Item_Representation(X) )
}
also
Item_Representation(X)
#
= ITEM_REPRESENTATION
{ label: Label,
item: X
}
#
| SUB_MENU_REPRESENTATION
{ label: Label,
menu_pos: g2d::Point, # obsolete - position relative to parent menu
menu: Menu_Representation(X)
};
icon_high = 12;
icon_wide = 12;
icon_sp = 1; # minimum space between text and icon
submenu_image
=
xc::CS_PIXMAP
{
size => { wide=>icon_wide, high=>icon_high },
#
data => [ map byte::string_to_bytes
[
"\x7f\xc0", "\x40\x40", "\x40\x60", "\x4e\x60",
"\x40\x60", "\x4e\x60", "\x40\x60", "\x4e\x60",
"\x40\x60", "\x40\x60", "\x7f\xe0", "\x1f\xe0"
]
]
};
fun layout_menu (font, menu, label)
=
layout (menu, label)
where
(xc::font_high font)
->
{ ascent, descent };
text_wide = xc::text_width font;
fun menu_geometry (POPUP_MENU items, label)
=
case label
#
NULL => max_w ( 0, FALSE, 0, items);
THE s => max_w (text_wide s, FALSE, 1, items);
esac
where
fun max_w (m, has_subm, nitems, [])
=>
(total_padding + 2*inset + m, nitems, has_subm);
max_w (m, has_subm, nitems, POPUP_MENU_ITEM (s, _) ! r)
=>
max_w (int::max (m, text_wide s), has_subm, nitems+1, r);
max_w (m, has_subm, nitems, POPUP_SUBMENU (s, _) ! r)
=>
max_w (int::max (m, (text_wide s) + icon_wide + icon_sp), TRUE, nitems+1, r);
end;
end;
fun layout (menu as (POPUP_MENU items), label)
=
{ my (max_wid, item_high, tot_ht)
=
{ (menu_geometry (menu, label))
->
(max_wid, n, has_subm);
fonth = ascent + descent;
item_high = if has_subm int::max (fonth+vspace, icon_high);
else fonth + vspace;
fi;
(max_wid, item_high, n*item_high+total_padding);
};
fun make_center_label (y_pos, item_label)
=
{ wide = text_wide item_label;
#
LABEL {
box => { col=>0, row=>y_pos, wide=>max_wid, high=>item_high },
text_pos => { col=>(max_wid - wide) / 2, row=>y_pos+ascent },
text => item_label
};
};
fun make_label (y_pos, item_label)
=
{ wide = text_wide item_label;
#
LABEL
{ box => { col=>padding, row=>y_pos, wide=>max_wid-total_padding, high=>item_high },
text_pos => { col=>padding+inset, row=>y_pos+ascent },
text => item_label
};
};
fun do_items (_, [])
=>
[];
do_items (y_pos, item ! r)
=>
item_representation ! do_items (y_pos+item_high, r)
where
item_representation
=
case item
#
POPUP_MENU_ITEM (s, v)
=>
ITEM_REPRESENTATION { label => make_label (y_pos, s),
item => v
};
POPUP_SUBMENU (s, m)
=>
{ my menu as MENU_REPRESENTATION { size=> { wide, ... }, ... }
=
layout (m, NULL);
SUB_MENU_REPRESENTATION
{
label => make_label (y_pos, s),
# menu_pos = { col=>maxWid-(wide / 3), row=>yPos },
menu_pos => g2d::point::zero,
menu
};
};
esac;
end;
end;
my (label, items)
=
case label
#
NULL => (NULL, do_items (padding, items));
THE s => (THE (make_center_label (0, s)), do_items (padding+item_high, items));
esac;
MENU_REPRESENTATION
{
size => { wide=>max_wid, high=>tot_ht },
item_high,
font,
label,
items
};
}; # fun layout
end; # fun layout_menu
Mitem(X)
=
{ id: Int,
draw_on: Void -> Void,
draw_off: Void -> Void,
rep: Item_Representation(X)
};
# Create a menu window.
#
# This involves creating and mapping
# the window and setting up the code
# for drawing the items.
#
# "menu_position" gives the position to place
# the menu in screen coordinates.
#
fun create_menu_window (screen, submenu_icon, mrep, menu_position as { col=>menu_x, row=>menu_y } )
=
{ box => menu_box,
in_menu,
select => select_item,
close
}
where
mrep -> MENU_REPRESENTATION { size, font, item_high, label, items, ... };
#
size -> { wide=>menu_wide, high=>menu_high };
white = xc::white;
black = xc::black;
my (window, in_dictionary)
=
xc::make_simple_popup_window screen
{
background_color => xc::rgb8_white,
border_color => black,
#
site => { upperleft => g2d::point::subtract (menu_position, { col=>border_thickness, row=>border_thickness } ),
size,
border_thickness
}
: g2d::Window_Site
};
xc::ignore_all in_dictionary;
xc::show_window window;
my (items_box as { row=>items_y, ... }: g2d::Box)
=
{ my (x, y, w, h)
=
case label
#
NULL => (menu_x, menu_y, menu_wide, menu_high );
THE _ => (menu_x, menu_y+item_high, menu_wide, menu_high-item_high);
esac;
{ col => x+padding,
row => y+padding,
#
wide => w-total_padding,
high => h-total_padding
};
};
# Geometry of menu window:
#
menu_box
=
{ col => menu_x,
row => menu_y,
#
wide => menu_wide,
high => menu_high
};
# Geometry of menu window including border
#
all_box
=
{ col => menu_x - border_thickness,
row => menu_y - border_thickness,
#
wide => menu_wide + 2*border_thickness,
high => menu_high + 2*border_thickness
};
fun in_menu p
=
g2d::point::in_box (p, all_box);
fun close ()
=
xc::destroy_window window;
fore_pen = xc::make_pen [xc::p::FOREGROUND (xc::rgb8_from_rgb black), xc::p::BACKGROUND (xc::rgb8_from_rgb white) ];
back_pen = xc::make_pen [xc::p::FOREGROUND (xc::rgb8_from_rgb white), xc::p::BACKGROUND (xc::rgb8_from_rgb black) ];
fun draw_item (LABEL { text_pos, text, ... } ) pen
=
xc::draw_transparent_string
#
(xc::drawable_of_window window)
pen
font
(text_pos, text);
fun clear_item (LABEL { box, ... } ) pen
=
xc::fill_box
#
(xc::drawable_of_window window)
pen
box;
fun draw_submenu (LABEL { box, text_pos, text }) pen
=
{ icon = the submenu_icon;
#
box -> { col=>x, row=>y, wide, ... }: g2d::Box;
xc::draw_transparent_string
#
(xc::drawable_of_window window)
pen
font
(text_pos, text);
xc::texture_blt
#
(xc::drawable_of_window window)
pen
{ from => icon,
to_pos => { col => (x + wide) - (icon_wide+inset),
row => y+1
}
};
};
fun make_items ([], _) => [];
make_items (item ! r, n)
=>
{ my (draw, label)
=
case item
ITEM_REPRESENTATION { label, ... } => (draw_item label, label);
SUB_MENU_REPRESENTATION { label, ... } => (draw_submenu label, label);
esac;
clear = clear_item label;
fun draw_on () = { clear fore_pen; draw back_pen; };
fun draw_off () = { clear back_pen; draw fore_pen; };
draw fore_pen;
{ id => n, draw_on, draw_off, rep => item } ! make_items (r, n+1);
};
end;
items = make_items (items, 0);
fun select_item (pt as { col=>x, row=>y } )
=
g2d::point::in_box (pt, items_box) ?? THE (list::nth (items, int::quot (y - items_y, item_high)))
:: NULL;
case label
#
THE title => { clear_item title fore_pen;
draw_item title back_pen;
};
_ => ();
esac;
end; # fun create_menu
# NB: The "Menu_Representation(X)" constraint is
# because of a bug in the typechecker. XXX BUGGO FIXME
#
fun pop_menu (menu_rep: Menu_Representation(X), mbut, screen, icon, menupt, pos, mouse)
=
{ (xc::size_of_screen screen)
->
{ wide=>screen_wide, high=>screen_high };
# Adjust the position of a menu
# to insure that it will fit
# on the screen:
#
fun clip_menu ({ col=>x, row=>y }, MENU_REPRESENTATION { size=> { wide, high }, ... } )
=
{ col => int::max (border_thickness, if (x+wide < screen_wide - border_thickness) x; else (screen_wide - (wide+border_thickness));fi),
row => int::max (border_thickness, if (y+high < screen_high - border_thickness) y; else (screen_high - (high+border_thickness));fi)
};
m_mailop
=
mouse ==> xc::get_contents_of_envelope;
fun popup (menu_rep, menupt, mouse_pos as { col=>mx, row=>m_y }, leave_pred)
=
{ menu_rep -> MENU_REPRESENTATION { size as { wide, ... }, item_high, ... };
# Calculate menu origin based
# on mouse position:
#
menu_pt
=
case menupt
#
PUT_POPUP_MENU_UPPERLEFT_ON_SCREEN p => p;
PUT_POPUP_MENU_ITEM_BENEATH_MOUSE 0 => { col=>mx-(wide / 2), row=>m_y-(item_high / 2) };
PUT_POPUP_MENU_ITEM_BENEATH_MOUSE n => { col=>mx-(wide / 2), row=>m_y-(item_high / 2)-(item_high * n) };
esac;
menu_pos = clip_menu (menu_pt, menu_rep);
(create_menu_window (screen, icon, menu_rep, menu_pos))
->
{ box as { col=>menu_x, ... }: g2d::Box, in_menu, select, close };
fun flip_on ( { draw_on, ... }: Mitem(X)) = draw_on ();
fun flip_off ( { draw_off, ... }: Mitem(X)) = draw_off ();
fun same_item
( { id => a, ... }: Mitem(X),
{ id => b, ... }: Mitem(X)
)
=
a == b;
fun track_mouse (cur_item, pt)
=
{ cur_item
=
case (cur_item, select pt)
#
(NULL, NULL ) => NULL;
(THE a, NULL ) => { flip_off a; NULL;};
(NULL, THE b) => { flip_on b; THE b;};
#
(THE a, THE b) => if (same_item (a, b))
#
cur_item;
else
flip_off a;
flip_on b;
THE b;
fi;
esac;
fun next_mouse_mailop (cur_item, screen_pt)
=
case (block_until_mailop_fires m_mailop)
#
xc::MOUSE_MOTION { screen_point, ... }
=>
track_mouse (cur_item, screen_point);
xc::MOUSE_LAST_UP { mouse_button, screen_point, ... }
=>
case (cur_item: Null_Or( Mitem(X) ))
#
THE { rep=>ITEM_REPRESENTATION { item, ... }, ... }
=>
{ close();
(THE item, FALSE, screen_point);
};
_ => { close();
(NULL, FALSE, screen_point);
};
esac;
xc::MOUSE_UP { mouse_button, screen_point, ... }
=>
track_mouse (cur_item, screen_point);
xc::MOUSE_DOWN { screen_point, ... }
=>
track_mouse (cur_item, screen_point);
_ => track_mouse (cur_item, screen_pt);
esac;
case cur_item
#
THE { rep=>SUB_MENU_REPRESENTATION { menu, ... }, ... }
=>
{ pt -> { col => x, ... };
# If item has a submenu and mouse is on or to the
# right of the icon, put up submenu.
# If second field of answer is FALSE, user is done,
# so close up shop. Otherwise, some button is still
# down, so continue to track the
# mouse. If the mouse is really in one of our
# ancestors, this will be caught in trackMouse.
# This latter case could be short-circuited by
# checking here that the mouse is in our box,
# and, if not, returning directly.
#
if (x + (icon_wide+padding+inset) >= menu_x + wide)
#
fun prior pt
=
(leave_pred pt) or g2d::point::in_box (pt, box);
answer = popup (menu, PUT_POPUP_MENU_ITEM_BENEATH_MOUSE 0, pt, prior);
if (#2 answer)
#
track_mouse (cur_item, #3 answer);
else
close ();
answer;
fi;
else
next_mouse_mailop (cur_item, pt);
fi;
};
# If the mouse is not on a menu item, and is not
# even in the menu window (including border), and
# is in some ancestor menu, then close up and return.
NULL => if (not (in_menu pt) and (leave_pred pt))
close();
(NULL, TRUE, pt);
else
next_mouse_mailop (cur_item, pt);
fi;
_ => next_mouse_mailop (cur_item, pt);
esac;
};
track_mouse (NULL, mouse_pos);
}; # fun popup
xsession = xc::xsession_of_screen screen;
menu_cursor
=
xc::get_standard_xcursor
xsession
xc::cursors_old::sb_left_arrow;
xc::change_active_grab_cursor
xsession
menu_cursor;
#1 (popup (menu_rep, menupt, pos, \\ _ = FALSE));
}; # fun pop_menu
# Return TRUE iff the menu has a sub-menu:
#
fun has_submenu (POPUP_MENU items)
=
f items
where
fun f [] => FALSE;
f ((POPUP_MENU_ITEM _) ! r) => f r;
f _ => TRUE;
end;
end;
fun attach (selection_channel, widget, mbuts, menu, label, pos)
=
{ root_window = wg::root_window_of widget;
#
xsession = wg::xsession_of root_window;
screen = wg::screen_of root_window;
font = xc::find_else_open_font xsession menu_font;
menu_rep = layout_menu (font, menu, label);
icon = has_submenu menu ?? THE (xc::make_readonly_pixmap_from_clientside_pixmap screen submenu_image)
:: NULL;
fun realize_widget { window, window_size, kidplug as xc::KIDPLUG { from_mouse', ... } }
=
{ m_slot = make_mailslot ();
#
menu_mbs = xc::make_mousebutton_state mbuts;
make_thread "popup_menu" loop
where
fun loop ()
=
{ envelope
=
block_until_mailop_fires from_mouse';
case (xc::get_contents_of_envelope envelope)
#
xc::MOUSE_FIRST_DOWN (arg as { screen_point, mouse_button, ... } )
=>
if (xc::mousebutton_is_set (menu_mbs, mouse_button))
#
case (pop_menu (menu_rep, mouse_button, screen, icon, pos (WHERE_INFO arg), screen_point, from_mouse'))
#
THE v =>
{ make_thread "popup_menu pop menu" {.
#
put_in_mailslot (selection_channel, v);
};
();
};
NULL => ();
esac;
else
put_in_mailslot (m_slot, envelope);
fi;
_ => put_in_mailslot (m_slot, envelope);
esac;
loop ();
};
end;
wg::realize_widget widget
{ window,
window_size,
kidplug => xc::replace_mouse (kidplug, take_from_mailslot' m_slot)
};
();
}; # fun realize_widget
wg::make_widget
{ root_window,
realize_widget,
args => \\ () = { background => NULL },
size_preference_thunk_of
=>
wg::size_preference_thunk_of widget
};
}; # fun attach
fun attach_menu_to_widget (widget, mbuts, menu)
=
{ selection_slot = make_mailslot ();
#
( attach (selection_slot, widget, mbuts, menu, NULL, \\ _ = PUT_POPUP_MENU_ITEM_BENEATH_MOUSE 0),
take_from_mailslot' selection_slot
);
};
fun attach_labeled_menu_to_widget (widget, mbuts, label, menu)
=
{ selection_slot = make_mailslot ();
#
( attach (selection_slot, widget, mbuts, menu, THE label, \\ _ = PUT_POPUP_MENU_ITEM_BENEATH_MOUSE 0),
take_from_mailslot' selection_slot
);
};
fun attach_positioned_menu_to_widget (widget, mbuts, menu, pos)
=
{ selection_slot = make_mailslot ();
#
( attach (selection_slot, widget, mbuts, menu, NULL, pos),
take_from_mailslot' selection_slot
);
};
fun make_lowlevel_popup_menu (root_window, menu, label)
=
do_pop
where
xsession = wg::xsession_of root_window;
screen = wg::screen_of root_window;
font = xc::find_else_open_font xsession menu_font;
menu_rep = layout_menu (font, menu, label);
icon = has_submenu menu ?? THE (xc::make_readonly_pixmap_from_clientside_pixmap screen submenu_image)
:: NULL;
fun do_pop (mbut, menupt, screen_pt, m)
=
{ slot = make_mailslot ();
#
fun do_it ()
=
put_in_mailslot (slot, pop_menu (menu_rep, mbut, screen, icon, menupt, screen_pt, m));
make_thread "popup_menu popup_menu" do_it;
take_from_mailslot' slot;
};
end;
}; # package popup_menu
end;