don@BRILLIG.UMD.EDU (Don Hopkins) (07/08/87)
Date: Wed, 1 Jul 87 23:04:47 EDT From: Don Hopkins <don@brillig.umd.edu> To: NeWS-makers@brillig.umd.edu Subject: Pie Menus for NeWS This is my initial implementation of Pie Menus for NeWS, in PostScript. The choices of a Pie Menu are positioned in a circle around the cursor, so that the direction of movement makes the selection. There is an inactive region in the menu center where the cursor starts out, and the regions corresponding to the choices are wedge shaped, like the slices of a pie: each slice is adjacent to the cursor, but in a different direction. Moving the cursor further out from the menu center increases the angular precision. Pie Menu choices may be positioned in intuitivly correct directions, with opposite choices in opposite directions, and other natural arrangments. Pie Menus are easy to learn, using "muscle memory", because you remember directions, not order. Because you don't need to look at the menu to choose a direction, you can mouse ahead dependably with menus you're familiar with. More information about Pie Menus if forthcoming, but right now I want to get the initial implementation out so that people can use them. Just load the following file into a NeWS server with psh, and try them out. They replace the default menu class, so that subsequently made menus will be Pie Menus. I welcome your questions, comments, and suggestions. -Don Spoken: Don Hopkins Net: don@brillig.umd.edu, ...!seismo!mimsy!don Mail: 5819 Ruatan St., College Park, Md. 20740, USA ---Clip-here--8X-------------------------------------------------------- %! %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % @(#)piemenu.ps % % Pie menu class implementation. % Copyright (C) 1987. % By Don Hopkins. % All rights reserved. % % Simple Simon popped a Pie Men- % u upon the screen; % With directional selection, % all is peachy keen! % % Pie Menus are provided for UNRESTRICTED use provided that this % copyright message is preserved on all copies and derivitive works. % This is provided without any warranty. No author or distributor % accepts any responsibility whatsoever to any person or any entity % with respect to any loss or damage caused or alleged to be caused % directly or indirectly by this program. This includes, but is not % limited to, any interruption of service, loss of business, loss of % information, loss of anticipated profits, core dumps, abuses of the % virtual memory system, or any consequential or incidental damages % resulting from the use of this program. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % May 28 1987 Don Hopkins % First cut, based on LitePullRightMenu. % % May 30 1987 Don Hopkins % Uses "Thing"s from liteitem.ps for key labels. A thing can be a % string, or a keyword. The string is shown in MenuFont. The % keyword can be either the name of an icon in icondict, or bound % on the dict stack to an executable function. The function takes % a boolean as input; if true, it draws itsself; if false, it % returns its width and height. % Better label positioning scheme: top or bottom justify labels at % at the very bottom or top of the menu, and left or right justify % labels on the right or left sides of the menu. The points % relative to which the labels are justified are positioned at % evenly spaced angles in a circle around the menu center. The % instance variable PieInitialAngle is the angle of the first % point. LabelRadius is the distance from the menu center to each % point, calculated as: % LabelMinRadius + LabelRadiusPerKey * <the number of menu keys> % If the menu can't be centered on the location of the button % event that invoked it, then warp the cursor to the menu center % plus how much it has moved since the button down event, so that % pop up menus near the screen edge and static menus work % correctly. But ARRRGH FOO: setcursorlocation is broken!!! It % moves the cursor, but next time you move the mouse, the cursor % pops back to where it used to be! The Sun X server used to have % the same problem with XWarpMouse. Makes you wonder. Well, % anyway, I commented it out, because it's more confusing with % setcursorlocation broken than it is not warping at all. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Things to do: % % Use blockinputqueue to avoid missing button up events that happen % immediatly after button down events, when popping up the first menu. % I tried it, putting it in the beginning of /show, and making a /fork % method that went /fork super send, and then unblockinputqueue, but % still it sometimes misses them, especially when something is slowing % the system down. But only on the root menu ... % % Uncomment setcursorlocation code that moves mouse to menu center % when setcursorlocation is fixed. % % Don't bother putting up a menu (or even moving the menu to be % completely on screen), if the button event that would make % the selection is already in the input queue. % % Teach it to use items as menu keys. Create PieItems like buttons, % cycles, sliders and pull-out menus based on the distance, % etc... % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% systemdict /Item known not { (NeWS/liteitem.ps) run } if systemdict /LiteMenu known not { (NeWS/litemenu.ps) run } if systemdict begin %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Utilities % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Coerce an angle to be >=0 and <360. % Note: mod returns integers, so's no good. /NormalAngle { % angle => angle dup 0 lt { dup 360 sub 360 idiv 360 mul sub } if dup 360 ge { dup 360 idiv 360 mul sub } if } def % From demomenu.ps % Fake method to send to a menu that returns a copy of the menu in the % new menu style. Recursivly changes all sub-menus. One thing to look % out for is that it does not change variables bound to the sub-menus % that were changed, so setting /rootmenu to the result of sending % /flipstyle to rootmenu will give you a new root menu, with a new % terminal sub-menu, but /terminalmenu will still be bound to the old % one, so sending messages to terminalmenu will not change the % terminal menu you get under the new rootmenu. But sending /flipstyle % to terminalwindow would not update the terminal menu under rootmenu. % So get your changes in before you flip styles! /flipstyle { % - => newmenu 0 1 MenuActions length 1 sub { MenuActions 1 index get % i ithAction dup type /dicttype eq { /flipstyle exch send % i menu' MenuActions 3 1 roll put % - } {pop pop} ifelse } for MenuKeys MenuActions /new DefaultMenu send } def % Override flipdefaultmenustyle, a function invoked from the user % interface menu. /flipdefaultmenustyle { % - => - (Flips default menu style) /DefaultMenu DefaultMenu SunViewMenu eq {PieMenu} {SunViewMenu} ifelse store } def %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % PieMenu class % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% /PieMenu LiteMenu %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Instance variables % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% dictbegin % The slice currently painted. /PaintedValue null def % Info about MenuFont. /FontAscent null def /FontHeight null def % Inner radius around which labels are positioned. Based LabelMinRadius, % LabelRadiusPerKey, and the length of MenuKeys. /LabelRadius null def % Minimum radius for label positioning. /LabelMinRadius 40 def % Increase LabelRadius by this much per key. /LabelRadiusPerKey 3 def % Direction in which the keys are laid out around the circle. /Clockwise true def % Pie menu outer radius. Based on LabelRadius and the bounding boxes of % the Key Things. /PieRadius null def % The angle at which the first key is placed. /PieInitialAngle 90 def % The number of degrees a slice takes up. Based on length of MenuKeys. /PieSliceWidth null def % The current direction in degrees of the cursor from the menu center. /PieDirection null def % The current distance of the cursor from the menu center. /PieDistance null def % Angle used in loops. /ThisAngle null def % Ammount to move the menu so that it fits entirely on the screen. /DeltaX null def /DeltaY null def dictend %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Class variables % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% classbegin % Highlight: true strokes, false fills. /StrokeSelection false def % Width of border just inside PieRadius perimiter. /Border 3 def % Gap between outermost label edge and border. /Gap 9 def % Radius of numb hole in menu center that makes no menu selection. /NumbRadius 10 def % Fudge factors for menu positioning. /MouseXDelta 0 def /MouseYDelta -3 def % Draw lines delimiting slices. /SliceLines false def % Draw arrows in the directions of slices. /SliceArrows true def % Drill a hole through the menu center, as big as NumbRadius. /NumbHole true def % Save the bits so pop-up is fast. /RetainCanvas? true def %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Class methods % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Calculate and set the menu FontAscent, FontHeight, PieSliceWidth, % LabelRadius, PieRadius, MenuWidth, and MenuHeight. Shape the canvas % and set the cursor. /layout { gsave MenuFont setfont initmatrix MenuCanvas setcanvas /FontAscent currentfont fontascent store /FontHeight currentfont fontheight store /PieSliceWidth 360 MenuKeys length 1 max div store /LabelRadius LabelMinRadius LabelRadiusPerKey MenuKeys length mul add store /ThisAngle PieInitialAngle store /PieRadius 0 MenuKeys { % maxradius key MenuFont ThingSize % maxradius w h ThisAngle cos abs .01 lt { exch 2 div exch } { 2 div } ifelse ThisAngle sin abs LabelRadius mul add exch % maxradius y w ThisAngle cos abs LabelRadius mul add % maxradius y x dup mul exch dup mul add % maxradius r max % maxradius /ThisAngle ThisAngle PieSliceWidth Clockwise {sub} {add} ifelse store } forall sqrt Gap add Border add store /MenuWidth PieRadius dup add store /MenuHeight PieRadius dup add store framebuffer setcanvas PieRadius dup dup 0 360 arc NumbHole { PieRadius dup NumbRadius 1 sub 0 360 arc } if MenuCanvas eoreshapecanvas /beye /beye_m MenuCanvas setstandardcursor % So retained canvases don't have their old image upon popup: MenuCanvas setcanvas MenuFillColor fillcanvas grestore } def % Make sure nothing's highlighted if there's a retained canvas. Fork % an event manager, make a canvas, and layout the menu as needed. If % ShowAtMouse? is true, move the menu so it's centered on the mouse, % and calculate any movement (DeltaX, DeltaY) needed to force it % to be completely on the screen. Move the menu to the right place on % screen, and set the canvas up. Move the cursor by the same ammount % that the menu had to be moved (DeltaX, DeltaY). (This is % commented out because setcursorlocation is broken.) Reset the menu % value, and fork a menu event manager. /show { PaintedValue null ne MenuCanvas null ne and MenuWidth null ne and { % /paint self send % God! Fix the paint stuff to be more atomic!! gsave MenuFont setfont initmatrix MenuCanvas setcanvas PaintedValue PaintSlice grestore } if /PaintedValue null store MenuEventMgr null ne {MenuEventMgr waitprocess pop} if MenuCanvas null eq {/MenuCanvas ParentCanvas newcanvas def} if MenuWidth null eq {/layout self send} if ShowAtMouse? { gsave framebuffer setcanvas CurrentEvent begin XLocation YLocation end /MenuY exch PieRadius sub MouseYDelta add store /MenuX exch PieRadius sub MouseXDelta add store % Correct for menu being beyond framebuffer. clippath pathbbox /DeltaY exch MenuHeight sub MenuY min 0 max MenuY sub store /DeltaX exch MenuWidth sub MenuX min 0 max MenuX sub store pop pop grestore } { /DeltaX 0 store /DeltaY 0 store } ifelse /MenuX MenuX DeltaX add store /MenuY MenuY DeltaY add store MenuCanvas savebehindcanvas MenuCanvas setcanvas MenuX MenuY movecanvas MenuCanvas canvastotop % Arrgh! setcursorlocation is broken! % gsave % fb setcanvas % currentcursorlocation % exch DeltaX add exch DeltaY add setcursorlocation % grestore MenuCanvas mapcanvas % note that this causes brain damage, thus % causes paint to be called. /MenuValue null def /fork self send } def % Paint the menu. Draw a Border width border inside PieRadius, and a % circle of radius NumbRadius at the menu center, in the % MenuBorderColor. Draw the keys at points on a circle around the % center of the menu. PieInitialAngle is the angle of the first point. % The points are PieSliceWidth degrees apart, at a distance of % LabelRadius. The keys may be Things (from liteitem.ps), which % ThingSize and ShowThing take. If the angle of a point is 90 or 270, % then the center of the bottom or top edge of the Thing is positioned % at the point. If the angle is to the left (>90 and <270) or right % (<90 or >270), then the center of the right or left edge of the % Thing is positioned at the point. If SliceLines is true, lines are % drawn at the edges of the slices. If SliceArrows is true, arrows % are drawn from the menu center pointing in the direction of each % slice. /paint { gsave MenuFont setfont initmatrix MenuCanvas setcanvas MenuFillColor fillcanvas PieRadius dup translate newpath 0 0 PieRadius Border sub 0 360 arc 0 0 PieRadius 0 360 arc MenuBorderColor setcolor eofill newpath 0 0 NumbRadius 0 360 arc fill /ThisAngle PieInitialAngle NormalAngle store MenuKeys { % thing MenuTextColor 1 index % thing color thing MenuFont ThingSize % thing color w h ThisAngle cos abs .01 lt { ThisAngle 180 lt { pop -2 div 0 } { neg exch -2 div exch } ifelse } { ThisAngle 90 gt ThisAngle 270 lt and { -2 div exch neg exch } { -2 div exch pop 0 exch } ifelse } ifelse % thing color dx dy ThisAngle sin LabelRadius mul add exch % thing color y dx ThisAngle cos LabelRadius mul add exch % thing color x y MenuFont ShowThing SliceLines { gsave newpath ThisAngle PieSliceWidth 2 div sub rotate NumbRadius 0 moveto PieRadius 1 sub 0 lineto MenuBorderColor setcolor stroke grestore } if SliceArrows { gsave newpath ThisAngle rotate NumbRadius 0 moveto LabelRadius .5 mul 0 lineto currentpoint LabelRadius .4 mul LabelRadius .04 mul lineto moveto LabelRadius .4 mul LabelRadius -.04 mul lineto MenuBorderColor setcolor stroke grestore } if /ThisAngle ThisAngle PieSliceWidth Clockwise {sub} {add} ifelse NormalAngle def } forall grestore } def % Handle drag events. If there's not a child menu up, then track the % mouse movement, updating the menu value according the the event % location; if it has changed, then update the highlighting. /DragProc { ChildMenu null eq { gsave MenuFont setfont initmatrix MenuCanvas setcanvas PieRadius dup translate CurrentEvent begin XLocation YLocation end % x y SetMenuValue MenuValue PaintedValue ne { PaintMenuValue } if grestore } if } def % Handle enter canvas events. Just call DragProc to keep the menu % value updated. /EnterProc { DragProc } def % Handle exit canvas events. Same as above. Here we keep tracking even % when you're off the menu edge (due to expressing interest in events % on the framebuffer overlay canvas). But if it really turns you on, % going off the edge could mean no selection (like when you're within % the numb radius - look at SetMenuValue), or select the slice, or pop % up a submenu, or drag the menu around, or give more info about the % slice, or whatever. /ExitProc { DragProc } def % Calculate and set the menu value from the cursor x y location. % Updates /PieDistance and /PieDirection instance variables. /SetMenuValue { % x y => - (Sets /MenuValue) /PieDistance 2 index dup mul 2 index dup mul add sqrt def exch atan /PieDirection exch def /MenuValue PieDistance NumbRadius le % It could be that when the cursor is out past the menu radius, % nothing is selected. But I don't do it that way, because it wins % to be able to get arbitrarily more precision by moving out further. % PieDistance PieRadius gt or { null } { PieInitialAngle PieSliceWidth 2 div Clockwise { add PieDirection sub } { sub PieDirection add } ifelse NormalAngle PieSliceWidth idiv } ifelse def } def % Update the highlighted slice to show the current menu value. /PaintMenuValue { % - => - (Hilite current item, un-hilite prev one.) PaintedValue PaintSlice MenuValue PaintSlice /PaintedValue MenuValue store } def % Paint highlighting on a menu slice. If it's null, then do nothing. % Draw an arrow, and a box around the key. /PaintSlice { % key => - dup null ne { % key gsave MenuCanvas setcanvas PieRadius dup translate % Draw an arrow pointing out in the direction of the slice. PieInitialAngle % key PieInitialAngle 1 index PieSliceWidth mul % key PieInitialAngle width Clockwise { sub } { add } ifelse % key angle /ThisAngle exch NormalAngle store % key ThisAngle rotate newpath NumbRadius 0 moveto LabelRadius .4 mul LabelRadius .1 mul lineto LabelRadius .6 mul 0 lineto LabelRadius .4 mul LabelRadius -.1 mul lineto closepath % Highlight the key Thing. LabelRadius 0 translate ThisAngle neg rotate % Factor this stuff out of here! MenuKeys exch get % thing MenuFont ThingSize % w h 2 copy % w h w h ThisAngle cos abs .01 lt { ThisAngle 180 lt { pop -2 div 0 } { neg exch -2 div exch } ifelse } { ThisAngle 90 gt ThisAngle 270 lt and { -2 div exch neg exch } { -2 div exch pop 0 exch } ifelse } ifelse % w h dx dy % Fudge 1 add % exch 1 sub exch 4 2 roll % dx dy h w -4 2 6 2 roll insetrrect rrectpath % overlayerase StrokeSelection {stroke} {fill} ifelse grestore } {pop} ifelse % } def % Handle button up events. If we have children, then let the leaf % child menu handle the button up event. Otherwise, we handle it: If % it's a menu dictionary, then make it the child menu and show it. % Otherwise, execute the associated menu action, and send a /popdown % message to the root parent menu. /UpProc { % If we have a child menu, let the leaf do the UpProc. ChildMenu null ne { /UpProc /leafmenu self send send } { % No child menus. Handle it. DragProc MenuValue getmenuaction dup type /dicttype eq { /ChildMenu exch store ChildMenu /ParentMenu self put /show ChildMenu send } { % Do our stuff /domenu self send % Find the parent menu self { dup /ParentMenu get dup null eq { pop exit } { exch pop } ifelse } loop % ^?^? (toodles [ex-tm]!) /popdown exch send } ifelse } ifelse } def % Handle button down events. Remember we got a down event so /DownProc { /GotDown true store } def % Handle damage events. Gotta make sure the highlighted slice is % re-highlighted. /DamageProc { MenuFont setfont initmatrix MenuCanvas setcanvas damagepath clipcanvas /paint self send PaintedValue PaintSlice newpath clipcanvas } def % Construct menu event interests. Only the first menu in a chain of % nested menus listens for mouse up events. It forwards them on to the % leaf child menu. I'm not sure if this is the right way to do it, % though. The interests for button and drag events are on the frame % buffer overlay canvas, instead of null, or menucanvas. This is % because we want to keep tracking when the mouse is out of the % canvas. Using fboverlay seems to be more politically correct than % using null. % % Foo on political correctness. Using fboverlay does not seem to work! % Living in sin, by using null for now. /makeinterests { /MenuInterests [ ParentMenu null eq { MenuButton /UpProc UpTransition null eventmgrinterest } if MenuButton /DownProc DownTransition null eventmgrinterest MouseDragged /DragProc null null eventmgrinterest /EnterEvent /EnterProc null MenuCanvas eventmgrinterest /ExitEvent /ExitProc null MenuCanvas eventmgrinterest /Damaged /DamageProc null MenuCanvas eventmgrinterest % Kludge to refresh messed up retained menu canvases. Ssssh! Don't tell anyone. PointButton /DamageProc UpTransition MenuCanvas eventmgrinterest ] def } def classend def %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Death linear menus! /DefaultMenu PieMenu store % This only sort of works. Read the flipstyle comments. systemdict /rootmenu known { rootmenu type /dicttype eq { /rootmenu /flipstyle rootmenu send store } if } if end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%