[comp.windows.news] KeySee 1.1: A few bug fixes, a few new features

sjs@jcricket.ctt.bellcore.com (Stan Switzer) (10/26/88)

Thanks to a lot of good net feedback I have made a number
of improvements to the original KeySee.

Since KeySee is now about a month old and hasn't changed any in about
half of that time, I figured there'd be no harm in posting this
(hopefully final) newer version: KeySee 1.1.

New features:
  1) You can change focus mode from cursor-follows-mouse to
click-to-type with a button.
  2) Middle and right mouse buttons map to control and shift.  A
trivial change allows you to substitute "meta" for one or the other.
  3) Pressing L5 and L7 with your mouse now does the "expected" thing.
  4) Eliminate certain key "pumping" problems.
  5) If KeySee is too slow for your taste, look at the documentation
on "SpeedHacks".
  6) Generous internal and functional documentation has been added.
Readers interested in more detail may enquire within.

Notes:
  1) If somehow the keyboard gets stuck in Caps mode or Control mode,
remebmer that how can hit "L1" to clear the problem.
  2) If keyboard input seems to be going to the wrong window, make
sure that you are'nt in click-to-type mode.

Enjoy,

Stan Switzer  sjs@ctt.bellcore.com  "Thou shalt not have no idea"

P.S.: Ignore bogus reply path, use signature address only.

----------------------------------------------------------
#!/usr/NeWS/bin/psh
%
% KeySee: Display the keyboard and simulate keyboard input.
%
% Copyright (C) 1988 by Stan Switzer. All rights reserved.
% This program is provided for unrestricted use, provided that this 
% copyright message is preserved. There is no warranty, and no author 
% or distributer accepts responsibility for any damage caused by this 
% program.
%
%	Mon KeySee, mon key do.
%
% Stan Switzer   sjs@ctt.bellcore.com

% ---------------------------------------
% Sept. 29, 1988  Stan Switzer
%
% One good hack deserves another.  Ever since Don Hopkins posted his
% MouSee I have been toying with the idea of writing something that
% does for the keyboard what MouSee does for the Mouse.  This is the
% result.
%
% The original version of this program simply displayed the status of
% the keyboard.  It soon occured to me that it could just as easily
% simulate keyboard input.  Not long afterward I added the on/off switch
% and made it load "liteitem.ps" if it wasn't already loaded.
% Josh Siegel suggested binding control and shift to the other mouse
% keys, which turned out to be quite practical.  Another suggestion was
% to display the keycaps according to the current bindings (and related
% ideas), which did not turn out to be practical.  Perhaps someone else
% will take up the cause.
%
% Thanks to comp.windows.news readers who put up with (numerous) early
% versions of this program, and who provided valuable feedback for new
% features.
%
% The following features of the program may need some explanation:
%   + KeySee assumes you have a Sun-3 style keyboard.  This is true
%     of all Sun-3 and Sun-4 "kb" devices, but not the 386i.
%     It is relatively simple to accomodate other keyboard layouts.
%   + The On/Off button determines whether keyboard monitoring is
%     enabled.
%   + The Cursor/Click button controls the keyboard focus mode:
%     "cursor follows mouse" or "click to type."  Note that the button
%     changes even if you change focus mode by other means!
%   + Ctl, Left Shift, and Left (meta) are "sticky."  One press turns
%     it on, another turns it off.
%   + The Adjust button is "Ctl" and the Menu button is "Shift" except
%     that they are not sticky.
%   + When the monitor is "Off," the keyboard doesn't highlight mouse
%     operations.  This is a feature, not a bug.  The cute thing here is
%     when you press "A" with your mouse, it doesn't gray out because
%     you pressed it (like a button would) but because it really thinks
%     you pressed the "A" key.  If the monitor is off, it just doesn't
%     know.  It would be easy to "fix" but I'm not sure it's broken.
%     If enough people feel strongly about it I'll change it.
%   + Note what happens if you press L5 or L7 with your mouse!  For this
%     to work it is important that simulated events have proper screen
%     coordinates (/XLocation and /YLocation values).
%   + Note what happens when you press the *real* L1 key.  Apparently the
%     kernel hides the L1 down in case you also hit "A" and then gives you
%     *both* a down and up event when (if!) the key comes up again.
%   + Keys will not simulate down or up events if they are already in the
%     result state.  This is to prevent key "pumping" where you manage to
%     create more down events than up.  This is particularly important in
%     the case of shift keys.
%   + Because KeySee operates as close to the raw keyboard as possible,
%     simulated events look very real -- even to the extent that KeySee
%     shifts work with real keys and real shifts work with KeySee key
%     hits.  About the only difference is that the /KeyState interest field
%     isn't (cannot be) set.  Apparently /KeyState isn't really used for
%     much.  Perhaps someone can explain SCANNER in user_edit_template(UI.ps)
%     to me.  Possibly /KeyState would be useful for mouse events so that
%     you detect shifted mouse buttons, etc.  Better would be to use
%     the {shift|caps|meta|control}_keys_down entries in UI_private.
%   + Serious NeWS hacks might want to look at some of the comments in here.
%     There are some good tricks (many learned from Don Hopkins's code and
%     quite a few learned the old-fashioned way) and important observations
%     documented here. The following techniques are illustrated here:
%     - Dictionaries as interest /Names; event handling.
%     - Raw keyboard input handling.
%     - Managing canvases with LOTS of items.
%     - Playing games with the icon.
%     - Tricking items into drawing themselves somewhere besides where they
%	really live (i.e.: in the icon!)
%     - Subclassing of items and windows.  I strongly suggest subclassing
%       windows rather than "defing" things in to a vanilla window.
%     - How I learned to stop worrying and love polymorphism
%       (Dr. Strangehack)
%   + KeySee is a bit slow on a 3/50.  Especially if you run two or more!
%
% Oct. 1, 1988  SJS
%
%   + Simplified mouse menu and adjust button handling.  Changing the menu
%     key to the meta will now only require an extremely trival change.
%     Search for "meta" below to find the code.
%   + Despite precautions taken to avoid the situation it is sometimes
%     possible to "pump" a key (it isn't easy, mind you, just possible)
%     if you should find keys inexplicably stuck in caps or control mode,
%     hit "L1" to reset the keystate.
%   + An interesting and perplexing problem occurs when KeySee is run in
%     color (non-retained) and it is partially obscured by another canvas.
%     When you press the L5 button and the key comes up, it attempts to
%     redraw itself to the "up" color, but the canvas is clipped to the
%     damage path, rendering the redraw ineffective.  There doesn't seem
%     to be any good solution to this problem.
%
% Oct. 4, 1988  SJS
%
%   + Stopped dummy (spacer) buttons from sending events.  Previously, they
%     sent bogus but apparently harmless events with "null" as the name.
%
% Oct. 6, 1988  SJS
%
%   + Added "xor" style highlighting code.  If you find KeySee too slow,
%     find "/SpeedHacks? false def" and change "false" to "true".
%
% Oct. 11, 1988	SJS
%   + Changed cycle items to use prevailing background and text colors
%     to be sure that they can be seen when the background is black.
%     There's no accounting for taste.
%
% Oct. 26, 1988 SJS
%   + As there have been no changes to KeySee for nearly a month,
%     it, in its present state, shall be called KeySee 1.1 and
%     released for (I hope) one final time.
%

systemdict /Item known not { (NeWS/liteitem.ps) run } if

/KeyItem LabeledItem dictbegin
    /ItemDownColor   .5 .5 .5 rgbcolor def
    /ItemRadius	     4		       def
    /Station	     null	       def
    /StickyKey?	     false	       def
    /ItemValue	     false	       def
dictend classbegin
    /new { % label canvas width height => instance
	() /Center nullproc 6 3 roll /new super send begin
	    ItemLabel /OU eq {  % over/under key label
		/LowerLabel exch def
		/UpperLabel exch def
	    } if
	    currentdict
	end
    } def
    /SpeedHacks? false def	% enable speed hacks?
    /ItemLabelFont   /Times-Roman findfont 10 scalefont def
    /setvalue { % bool => -   -- true == down, false == up
	/ItemValue exch store
	ItemValue ItemPaintedValue ne {
	    SpeedHacks? {
		gsave ItemCanvas setcanvas HiLightKey grestore
		/ItemPaintedValue ItemValue def
	    } {
		/paint self send
	    } ifelse
	} if
    } def
    /JustSetvalue { % bool => -   -- paint but don't draw it
	/ItemValue exch store
    } def
    /PaintIfNeeded { % - => -	  -- draw it if needed
	ItemValue ItemPaintedValue ne {/paint self send} if
    } def
    /KS 2 def	% Key Separation
    /ItemShape {  % bool => -   -- shape of item (false->inset)
	{ ItemRadius KS 2 idiv dup ItemWidth KS sub ItemHeight KS sub }
	{ ItemRadius .5 sub KS 2 idiv .5 add dup 
	    ItemWidth KS sub 1 sub ItemHeight KS sub 1 sub }
	ifelse
	rrectpath
    } def
    /PaintItem {
	true ItemShape
	gsave
	ItemFillColor ItemValue null ne {
	    SpeedHacks? not ItemValue and { pop ItemDownColor } if
	} if
	setcolor fill
	grestore
	ItemBorderColor setcolor stroke
	ShowLabel
	SpeedHacks? ItemValue and { HiLightKey } if
    } def
    /HiLightKey {
	gsave false ItemShape 0 setgray 5 setrasteropcode fill grestore
    } def
    /OU { % over-under proc
	% if the Label is a sym, it is executed passing "true" to draw it
	%	and "false" to return the width and height.
	% Hack: we pretend that the labels have no width and then
	%       cshow them.
	{ % draw it
	    0 currentfont fontdescent 1 add 2 idiv rmoveto
	    gsave 0 currentfont fontheight rmoveto UpperLabel cshow grestore
	    LowerLabel cshow
	} { % size it
	    0 currentfont fontheight 2 mul
	} ifelse
    } def
    /reshape { % x y w h
	/reshape super send
	LabelSize % w h
	ItemHeight exch sub 2 div /LabelY exch def
	ItemWidth exch sub 2 div /LabelX exch def
    } def
    /SetStation { % stationcode => -
	16#6F00 add		% This is magic
	/Station exch def
    } def
    /ClientDown { true FakeKey } def
    /ClientUp { false FakeKey } def
    /FakeKey {	% bool(true==down) => -    -- fake a key transition
	StickyKey? {
	    { ItemValue not ReallyFakeKey } if
	} {
	    ReallyFakeKey
	} ifelse
    } def
    /ReallyFakeKey { % bool(true==down) -- bypass sticky goo
	dup ItemValue ne exch /ItemValue exch store
	{
	    createevent dup begin
		/Name Station def
		/Action ItemValue /DownTransition /UpTransition ifelse def
		gsave framebuffer setcanvas currentcursorlocation
		    /YLocation exch def /XLocation exch def
		grestore
	    end sendevent
	} if
    } def
    /SetSticky { /StickyKey? exch def } def
	
classend def

/DummyKeyItem KeyItem [ ]   % just uses up space
classbegin
    /new { % canvas width height => instance
	() 4 1 roll
	/new super send
    } def
    /ReallyFakeKey { pop } def	% don't send key events
    /PaintItem nullproc def
    /SetStation { pop } def
classend def

% really a cycle item but reaponds to some "KeyItem" messages:
/CycleKey CycleItem dictbegin
    /Station	     null   def
dictend classbegin
    /new { %  choices notify can w h => instance
	/cycle 6 1 roll /Right 5 1 roll
	/new super send
    } def
    /SetStation { pop } def
    /PaintIfNeeded nullproc def
classend def

/KeeSee DefaultWindow dictbegin
    /Items	       null 	def
    /ItemList	       null	def
    /TmpDict	       null	def
    /Watcher	       null	def
    /IconKey	       null	def
    /ItemProc	       null	def
    /EventProc	       null	def
dictbegin
dictend classbegin

    /new {
	/new super send begin
	    /PaintClient {
		ClientFillColor fillcanvas
		ClientCanvas setcanvas
		ItemList { paintitems } forall
	    } def
	    /TmpDict 20 dict def
	    currentdict
	end
    } def
    /FrameLabel (Key See) def
    /IconLabel FrameLabel def
    /KeyWidth 24 def	% Width (&Height) of Std Key
    /Border 4 def   	% border around keyboard proper

    /Key { % (Label) WidthFactor => item
	KeyWidth mul 
	ClientCanvas exch KeyWidth /new KeyItem send
	pause
    } def
    /Dummy { % WidthFactor => item
	KeyWidth mul ClientCanvas exch KeyWidth /new DummyKeyItem send
    } def
    /Sticky { % item => item
	true /SetSticky 2 index send
    } def
    
    /CreateClientCanvas {
	/CreateClientCanvas super send
   
	% various items:
	/Items dictbegin
	    (A) 0 get 1 (Z) 0 get { % alpha keys
		1 string dup 0 4 -1 roll put
		dup 1 string copy cvn exch 1 Key def
	    } for
	    /D1 (!)(1)/OU 1 Key def
	    /D2 (@)(2)/OU 1 Key def
	    /D3 (#)(3)/OU 1 Key def
	    /D4 ($)(4)/OU 1 Key def
	    /D5 (%)(5)/OU 1 Key def
	    /D6 (^)(6)/OU 1 Key def
	    /D7 (&)(7)/OU 1 Key def
	    /D8 (*)(8)/OU 1 Key def
	    /D9 (\()(9)/OU 1 Key def
	    /D0 (\))(0)/OU 1 Key def
	    /Caps (Caps) 1.25 Key def
	    /Left (Left) 1.5 Key Sticky def
	    /Space () 9 Key def
	    /SPC (Space) 2.25 Key def 0 0 /move SPC send
	    /Right (Right) 1.5 Key def
	    /Alt (Alt) 1.75 Key def
	    /LShift (Shift) 2.25 Key Sticky def
	    /RShift (Shift) 1.75 Key def
	    /LF (LF) 1 Key def
	    /L-C (<)(,)/OU 1 Key def
	    /G-P (>)(.)/OU 1 Key def
	    /Q-S (?)(/)/OU 1 Key def
	    /Ctl (Ctl) 1.75 Key Sticky def
	    /C-S (:)(;)/OU 1 Key def
	    /Q-Q (")(')/OU 1 Key def
	    /Ret (Return) 2.25 Key def
	    /Tab (Tab) 1.5 Key def
	    /O-B ({)([)/OU 1 Key def
	    /C-B (})(])/OU 1 Key def
	    /Del (Del) 1.5 Key def
	    /Esc (Esc) 1 Key def
	    /U-D (_)(-)/OU 1 Key def
	    /P-E (+)(=)/OU 1 Key def
	    /V-B (|)(\\)/OU 1 Key def
	    /T-Q (~)(`)/OU 1 Key def
	    /F1 (F1) 1 Key def
	    /F2 (F2) 1 Key def
	    /F3 (F3) 2 Key def
	    /F4 (F4) 2 Key def
	    /F5 (F5) 2 Key def
	    /F6 (F6) 2 Key def
	    /F7 (F7) 2 Key def
	    /F8 (F8) 1 Key def
	    /F9 (F9) 1 Key def
	    /BS (BS) 1 Key def
	    /L1 (L1) 1 Key def /L2 (L2) 1 Key def /X1 .5 Dummy def
	    /L3 (L3) 1 Key def /L4 (L4) 1 Key def /X2 .5 Dummy def
	    /L5 (L5) 1 Key def /L6 (L6) 1 Key def /X3 .5 Dummy def
	    /L7 (L7) 1 Key def /L8 (L8) 1 Key def /X4 .5 Dummy def
	    /L9 (L9) 1 Key def /L10 (L10) 1 Key def /X5 .5 Dummy def
	    /X6 .5 Dummy def /R1 (R1) 1 Key def
	        /R2 (R2) 1 Key def /R3 (R3) 1 Key def
	    /X7 .5 Dummy def /R4 (R4) 1 Key def
	        /R5 (R5) 1 Key def /R6 (R6) 1 Key def
	    /X8 .5 Dummy def /R7 (R7) 1 Key def
	        /R8 (R8) 1 Key def /R9 (R9) 1 Key def
	    /X9 .5 Dummy def /R10 (R10) 1 Key def
	        /R11 (R11) 1 Key def /R12 (R12) 1 Key def
	    /X10 .5 Dummy def /R13 (R13) 1 Key def
	        /R14 (R14) 1 Key def /R15 (R15) 1 Key def
	    /X11 .5 Dummy def
	    /OnOff [ (On) (Off) ]
	        { ItemValue 0 eq /watch /stopwatch ifelse ThisWindow send }
	        ClientCanvas KeyWidth dup 2.5 mul exch /new CycleKey send def
	    textcolor ClientFillColor {
		/ItemFillColor exch def
		/ItemTextColor exch def
	    } OnOff send
	    /Mode [ (Cursor) (Click) ]
		{ ItemValue 0 eq /CursorFocus /ClickFocus ifelse setfocusmode }
	        ClientCanvas KeyWidth dup 3 mul exch /new CycleKey send def
	    textcolor ClientFillColor {
		/ItemFillColor exch def
		/ItemTextColor exch def
	    } Mode send
	    Mode /ItemValue
	        UI_private /FocusMode get /CursorFocus eq 0 1 ifelse put
	dictend store

	% Display order
	/ItemList Items begin [ % Key rows from bottom to top
	    [ OnOff 119 Caps Left Space Right 19 Alt X11 Mode ] Station
	    [ 95 L9 97 L10 X5 99 LShift Z X C
		V B N M L-C G-P Q-S RShift LF
	        X10 112 R13 R14 R15 ] Station
	    [ 72 L7 L8 X4 76 Ctl A S D F G H J K L C-S Q-Q 89 Ret
	        X9 91 R10 R11 R12 ] Station
	    [ 49 L5 51 L6 X3 53 Tab Q W E R T Y U I O P O-B C-B Del
	        X8 68 R7 R8 R9 ] Station
	    [ 25 L3 L4 X2 29 Esc D1 D2 D3 D4 D5 D6 D7 D8 D9 D0
		U-D P-E 88 V-B 42 T-Q
	        X7 45 R4 R5 R6 ] Station
	    [ 1 L1 3 L2 X1 5 F1 F2 8 F3 10 F4 12 F5
		14 F6 16 F7 F8 F9 43 BS
		X6 21 R1 R2 R3 ] Station
	] end store
        /ItemProc Items forkitems store
    } def

    /IconWidth 64 def /IconHeight 48 def

    /PaintIconKey { 	% paints IconKey centered in icon
	% a tasty hack!
	IconKey null ne {
	    { ItemHeight ItemWidth } IconKey send
	    IconWidth exch sub 2 idiv exch
	    IconHeight exch sub 2 idiv 6 add
	    gsave translate  /PaintItem IconKey send  grestore
	} if
    } def
    
    /PaintIcon { gsave IconCanvas setcanvas
	IconFillColor fillcanvas IconBorderColor strokecanvas
	IconTextColor setcolor
	PaintIconKey
	PaintIconLabel
    grestore } def

    /flipiconic {
	/flipiconic super send
	Iconic? {
	    painticon    % update icon image
	} {  % we don't paint items when they are not displayed, so update.
	    ItemList { { /PaintIfNeeded exch send } forall } forall
	} ifelse
    } def
	
    /SetIconKey { gsave IconCanvas setcanvas
	IconKey null ne {	   % erase previous key image
	    { ItemWidth ItemHeight } IconKey send
	    IconWidth 2 index sub 2 idiv
	    IconHeight 2 index sub 2 idiv 6 add
	    moveto rect
	    IconFillColor setshade fill
	} if
	JustSetIconKey
	IconTextColor setcolor PaintIconKey
    grestore } def
    
    /JustSetIconKey {
	Items begin
	    dup Space eq {  % normal space bar is too big!
		/ItemValue get SPC dup /ItemValue 4 -1 roll put
	    } if
	end
	/IconKey exch def
    } def

    /eventmonitor { % start eventmonitor
	EventProc null ne { EventProc killprocess } if
	/EventProc {
	    createevent dup begin
		/Name  /SetFocusPolicy def 	% focus mode change
		/Priority 10 def
	    end expressinterest
	    createevent dup begin
		/Name [ MenuButton AdjustButton ] def
		/Action [ /UpTransition /DownTransition ] def
		/Canvas ClientCanvas def
		% evidence suggests that mouse events are exclusive whether
		%     you want it or not!
		/Exclusivity true def
	    end expressinterest
	    {
		awaitevent begin
		    % invoke name w/ Action as param:
		    %     "0 pop" keeps "self send" from being expunged
		    Action Name self 0 pop send
		end
	    } loop
	} fork def
    } def

    /FakeKey {	% action key => -
	Items exch get	 % action keyitem
	exch /DownTransition eq exch % bool keyitem
	/ReallyFakeKey exch send     % bypass sticky key handling
    } def
	    
    AdjustButton { /Ctl FakeKey } def  % action => -
    MenuButton  { /LShift FakeKey } def % action => - % if you prefer "shift"
    % MenuButton  { /Left FakeKey } def % action => - % if you prefer "meta"

    /SetFocusPolicy { % action => -
	% action is new mode
	/CursorFocus eq 0 1 ifelse /setvalue Items /Mode get send
    } def

    /stopwatch {    % stop event loop
	Watcher null ne { Watcher killprocess } if
    } def
    /watch {	% start event loop
	stopwatch
	/Watcher {
	    createevent dup begin	
		/Name dictbegin	% dict: keycode => item
		    Items {
			exch pop dup /Station get
			dup null eq { pop pop } { exch def } ifelse
		    } forall
		dictend def
		/Priority 10 def
	    end expressinterest
	    {
		awaitevent begin
		    % Note: Name is key item because of interest /Name dict
		    
		    pause % perhaps this will let us do real work first
		    
		    % "self /foo exch send" keeps the method compiler
		    % from removing self send.  This is important to that
		    % when (Just)SetIconKey is invoked it will end up
		    % setting IconKey in the object, not in the event.
		    % failing to do this results in having the interest
		    % (which is apparently reused for all keyboard events)
		    % referencing an item and consequently its parent (the
		    % ClientCanvas), causing the canvas to just stick around
		    % forever.  Probably defing bogus entries in events should
		    % be an error.  This was no fun to find.
		    
		    Action /DownTransition eq
		    Iconic? {
			/JustSetvalue Name send
			Name self /SetIconKey exch send
		    } {
			/setvalue Name send
			Name self /JustSetIconKey exch send
		    } ifelse
		end
	    } loop
	} fork def
    } def

    /Station { % [ KeyItems-and-indexes ] => [ KeyItems ]
	% sets the station codes in the array's items.
	mark exch 0 exch { % [ item item ... n currentitem
	    dup type /integertype eq {
		exch pop
	    } {
		2 copy /SetStation exch send
		exch 1 add
	    } ifelse
	} forall
	pop ]
    } def

    /ShapeClientCanvas {
	% This is a real good way to position items!
	/ShapeClientCanvas super send
	Recalc	% recalc layout params
	ClientCanvas setcanvas

	% now, move the items to their rightful places
	TmpDict begin
	    /SepX 0 def /SepY 0 def
	    /Y Border def
            ItemList {
		/X Border def
		/MaxH 0 def
		{
		    X Y /move 3 index send
		    /ItemHeight 1 index send dup MaxH gt
			{ /MaxH exch def } { pop } ifelse
		    /ItemWidth exch send X add SepX add /X exch store
		} forall
		/Y Y MaxH SepY add add def
	    } forall
	end
	Watcher null eq { watch } if
	EventProc null eq { eventmonitor } if
    } def

    /placeit {	% one click placement and sizing
	gsave fboverlay setcanvas getclick grestore
	BorderLeft BorderRight add 21 KeyWidth mul add Border 2 mul add
	BorderTop BorderBottom add 6 KeyWidth mul add Border 2 mul add
	3 -1 roll 1 index sub 3 1 roll % % ulx uly w h => llx lly w h
	reshape
    } def

    /Recalc { % - => -  -- recalculates various layout parameters
	% for when I decide to handle resizing!
    } def

classend def

/win framebuffer /new KeeSee send def
/placeit win send
/map win send

% ----- Anything after this line is not part of the program -----