WindowSensor.st
author claus
Tue, 28 Feb 1995 22:51:15 +0100
changeset 110 60c08d82e209
parent 103 6156d12a414d
child 115 1d93fd8c5371
permissions -rw-r--r--
*** empty log message ***

"
 COPYRIGHT (c) 1993 by Claus Gittinger
	      All Rights Reserved

 This software is furnished under a license and may be used
 only in accordance with the terms of that license and with the
 inclusion of the above copyright notice.   This software may not
 be provided or otherwise made available to, or used by, any
 other person.  No title to or ownership of the software is
 hereby transferred.
"

Object subclass:#WindowSensor
	 instanceVariableNames:'eventSemaphore damage mouseAndKeyboard 
				compressMotionEvents ignoreUserInput
				buttonState exposeEventSemaphore 
				catchExpose gotExpose gotOtherEvent
				translateKeyboardEvents'
	 classVariableNames:'ControlCEnabled'
	 poolDictionaries:''
	 category:'Interface-Support'
!

WindowSensor comment:'
COPYRIGHT (c) 1993 by Claus Gittinger
	      All Rights Reserved

$Header: /cvs/stx/stx/libview/WindowSensor.st,v 1.18 1995-02-28 21:51:15 claus Exp $
'!

!WindowSensor class methodsFor:'documentation'!

copyright
"
 COPYRIGHT (c) 1993 by Claus Gittinger
	      All Rights Reserved

 This software is furnished under a license and may be used
 only in accordance with the terms of that license and with the
 inclusion of the above copyright notice.   This software may not
 be provided or otherwise made available to, or used by, any
 other person.  No title to or ownership of the software is
 hereby transferred.
"
!

version
"
$Header: /cvs/stx/stx/libview/WindowSensor.st,v 1.18 1995-02-28 21:51:15 claus Exp $
"
!

documentation
"
    Instances of this class keep track of events and damage areas for a group of 
    views. All incoming expose rectangles and events (from Workstation) are 
    collected here, until someone gets a chance to handle them. 
    When adding an expose rectangle, WindowSensor tries to merge the rectangle 
    with the list of existing damages to minimize redrawing.

    instance variables:
	eventSemaphore          <Semaphore>     the semaphore to be signalled when an event
						(or damage) arrives

	damage                  <Collection>    collection of damage events

	mouseAndKeyboard        <Collection>    collection of user events

	compressMotionEvents    <Boolean>       if true, multiple motion events are
						compressed to one event. If false, each
						event is handled individual.
						(should be set to false when doing free-hand drawing)

	ignoreUserInput         <Boolean>       if true, key & button events are ignored
						(usually set to true by WindowGroup, while a
						 modalbox covers a view)

	buttonState                             the current state (currently unused)

	exposeEventSemaphore    <Semaphore>     X-special: semaphore to be signalled when
						expose event arrives after a copyArea.

	catchExpose             <Boolean>       true, while waiting for an expose event 
						(after a copyArea)

	gotExpose               <Boolean>       set to true, when an expose event arrives
						(after a copyarea)

	gotOtherEvent           <Boolean>       set to true if other events arrive while
						waiting for expose (after a copyarea).

	translateKeyboardEvents <Boolean>       if true, keyboard events are translated via
						the devices leyboardMap; if false, they
						are reported as raw-keys. Default is true.

    class variables:
	ControlCEnabled         <Boolean>       if true (which is the default) Control-C
						will interrupt the process handling the
						view.
						For secure stand-alone applications,
						this can be set to false, in which case 
						Control-C does NOT interrupt the process.
"
! !

!WindowSensor class methodsFor:'initialization'!

initialize
    ControlCEnabled := true.

!

disableControlC
    "disable Control-C processing. If enabled,
     pressing CNTL-C in a view will interrupt it and bring
     its process into the debugger (actually raising signal).
     Otherwise, CNTL-C is sent to the view like any other key.
    "

    ControlCEnabled := false
!

enableControlC
    "enable Control-C processing. If enabled,
     pressing CNTL-C in a view will interrupt it and bring
     its process into the debugger (actually raising signal).
     Otherwise, CNTL-C is sent to the view like any other key.
    "

    ControlCEnabled := true
! !

!WindowSensor class methodsFor:'instance creation'!

new
    ^ self basicNew initialize
! !

!WindowSensor methodsFor:'damage'!

addDamage: aRectangle view:aView
    "Add aRectangle to the damage list.
     We try to merge incoming rectangles with the existing damage by discarding
     incoming rectangles which are contained in the existing damage or replacing
     any existing damage rectangle with incoming rectangles that completely contain it.
     There could be much more optimization here (for example joining rectangles)..."

    |count temp index newEvent 
     sz "{ Class: SmallInteger }" |

    sz := damage size.
    sz == 0 ifTrue: [
	newEvent := WindowEvent damageFor:aView rectangle:aRectangle.
	damage := OrderedCollection with:newEvent.
	^ self
    ].

    "
     first look, if this rectangle is already in the expose list;
     if so, dont add to queue
    "
    damage do: [:aDamage | 
	aDamage notNil ifTrue:[
	    aDamage isDamage ifTrue:[
		aDamage view == aView ifTrue:[
		    ((aDamage rectangle) contains:aRectangle) ifTrue: [^self]
		]
	    ]
	].
    ].

    "
     then look, if new rectangle contains any in the expose list;
     if so, remove the old damage
    "
    count := 0.
    1 to:sz do:[:i |
	|aDamage|

	aDamage := damage at:i.
	aDamage notNil ifTrue:[
	    aDamage isDamage ifTrue:[
		(aDamage view) == aView ifTrue:[
		    (aRectangle contains:(aDamage rectangle)) ifTrue: [ 
			damage at:i put:nil.
			count := count + 1
		    ]
		]
	    ]
	]
    ].

    count > 10 ifTrue: [
	temp := OrderedCollection new:(sz - count + 1).
	index := 1.
	damage do:[:aDamage |
	    aDamage notNil ifTrue: [
		temp add: aDamage.
	    ]
	].
	damage := temp
    ].
    newEvent := WindowEvent damageFor:aView rectangle:aRectangle.
    damage add:newEvent.
!

nextDamage
    "retrieve the next damage rectangle or nil, if there is none"

    |d|

    [d isNil] whileTrue:[
	damage size == 0 ifTrue:[^ nil].
	d := damage removeFirst.
    ].
    ^ d
!

nextEvent
    "retrieve the next event or nil, if there is none"

    |e|

    [e isNil] whileTrue:[
	mouseAndKeyboard size == 0 ifTrue:[^ nil].
	e := mouseAndKeyboard removeFirst.
    ].
    ^ e
! !

!WindowSensor methodsFor:'special'!

catchExpose
    "start catching noExpose events (must be done BEFORE a bitblt,
     to prepare for the exposeEventSemaphore to be signalled when 
     the noExpose event arrives)."

    gotExpose := false.
    gotOtherEvent := false.
    catchExpose := true.
    exposeEventSemaphore := Semaphore new.
!

waitForExposeFor:aView
    "wait until a noExpose arrives (after a bitblt)"

    Processor activeProcessIsSystemProcess ifTrue:[
	"
	 cannot really suspend, if its a systemProcess
	"
	[gotExpose] whileFalse:[
	    aView device dispatchExposeEventFor:aView id.
	    Processor yield.
	]
    ] ifFalse:[
	[gotExpose] whileFalse:[
	    "
	     just in case we have a (network or software) problem ...
	     explanation: it may happen, that an expose event is totally
	     lost - for example, if the network breaks down.
	     To not block forever, we wait with a timeout, to get out of here
	     if the event does not arrive after 15 seconds.
	    "
	    (exposeEventSemaphore waitWithTimeout:5) ifFalse:[
		aView device synchronizeOutput.
		'WSENSOR: late expose event' errorPrintNL.
		(exposeEventSemaphore waitWithTimeout:10) ifFalse:[
		    'WSENSOR: lost expose event again - ignore' errorPrintNL.
		].
		"
		 you can put a comment around the following line, if you dont
		 like the message ...
		"
		gotExpose := true.
		^ self
	    ]
	].

	"
	 other incoming events have been ignored during the wait.
	 Now handle those ...
	"
	gotOtherEvent ifTrue:[
	    eventSemaphore signal
	].
    ].
    catchExpose := false
!

flushUserEvents
    "throw away all pending user events"

    (mouseAndKeyboard isNil or:[mouseAndKeyboard size > 0]) ifTrue:[
	mouseAndKeyboard := OrderedCollection new
    ].
!

flushExposeEvents
    "throw away all pending expose events; this
     can be done after a full redraw (or in views, which are
     doing full redraws anly)"

    (damage isNil or:[damage size > 0]) ifTrue:[
	damage := OrderedCollection new
    ].
!

flushExposeEventsFor:aView
    "throw away all pending expose events for aView; 
     this can be done after a full redraw 
     (or in views, which are always doing full redraws -
      instead of drawing the clip-area only)"

    damage notNil ifTrue:[
	1 to:damage size do:[:index |
	    |aDamage|

	    aDamage := damage at:index.
	    aDamage notNil ifTrue:[
		aDamage view == aView ifTrue:[
		    damage at:index put:nil
		]
	    ]
	]
    ].
!

flushUserEventsFor:aView
    "throw away all pending user events for aView" 

    mouseAndKeyboard notNil ifTrue:[
	1 to:mouseAndKeyboard size do:[:i |
	    |anEvent|

	    anEvent := mouseAndKeyboard at:i.
	    anEvent notNil ifTrue:[
		anEvent view == aView ifTrue:[
		    mouseAndKeyboard at:i put:nil
		]
	    ]
	]
    ].
!

flushEventsFor:aView
    "throw away all events for aView"

    self flushExposeEventsFor:aView.
    self flushUserEventsFor:aView.
!

pushUserEvent:anEvent
    "manually put an event into the queue - this allows
     simulation of events (implementation of recorders & playback)."

    mouseAndKeyboard addLast:anEvent.
    self notifyEventArrival
!

pushUserEvent:aSelector for:aView withArguments:arguments
    "manually put an event into the queue - this allows
     simulation of events (implementation of recorders & playback)."

    self pushEvent:(WindowEvent 
		     for:aView
		     type:aSelector
		     arguments:arguments).

    "
     |b|
     b := Button label:'test'.
     b open.
     (Delay forSeconds:5) wait.
     b sensor pushEvent:#pointerEnter:x:y: for:b withArguments:#(0 1 1).
     (Delay forSeconds:1) wait.
     b sensor pushEvent:#buttonPress:x:y: for:b withArguments:#(1 1 1).
     (Delay forSeconds:2) wait.
     b sensor pushEvent:#buttonRelease:x:y: for:b withArguments:#(1 1 1).
     (Delay forSeconds:1) wait.
     b sensor pushEvent:#pointerLeave: for:b withArguments:#(0).
    "
!

forwardKeyEventsTo:aView
    "remove all keyboard events and send them to aSensor instead"

"/ 'fwd' printNL.
    1 to:mouseAndKeyboard size do:[:i |
	|anEvent|

	anEvent := mouseAndKeyboard at:i.
	anEvent notNil ifTrue:[
	    anEvent isKeyEvent ifTrue:[
		anEvent view:aView.
		aView sensor pushUserEvent:anEvent.
"/ anEvent type printNL.
		mouseAndKeyboard at:i put:nil
	    ]
	]
    ].
! !

!WindowSensor methodsFor:'queries '!

hasDamageFor:aView 
    "return true, if any exposure events are pending for aView"

    damage size ~~ 0 ifTrue:[
	damage do:[:aDamage |
	    aDamage notNil ifTrue:[
		aDamage view == aView ifTrue:[^ true].
	    ].
	]
    ].
    ^ false
!

hasDamage 
    "return true, if any exposure events are pending"

    ^ damage size ~~ 0
!

hasEvents 
    "return true, if any mouse/keyboard events are pending"

    ^ mouseAndKeyboard size ~~ 0
!

eventPending
    "return true, if either damage or events are pending"

    mouseAndKeyboard size ~~ 0 ifTrue:[^ true].
    ^ damage size ~~ 0
!

hasButtonMotionEventsFor:aView 
    "return true, if any buttonMotion events are pending"

    mouseAndKeyboard size ~~ 0 ifTrue:[
	mouseAndKeyboard do:[:anEvent |
	    anEvent notNil ifTrue:[
		anEvent view == aView ifTrue:[
		    anEvent type == #buttonMotion:x:y: ifTrue:[^ true].
		]
	    ].
	]
    ].
    ^ aView device eventPending:#buttonMotion for:aView id
! !

!WindowSensor methodsFor:'event processing'!

notifyEventArrival
    "an event arrived - if there is an eventSemaphore,
     signal it, to wake up any controller process"

    catchExpose == true ifTrue:[
	"
	 dont wake up, if we are currently waiting for an expose
	 but remember arrival of something.
	"
	gotOtherEvent := true.
	^ self
    ].
    eventSemaphore notNil ifTrue:[
	eventSemaphore signal
    ]
!

exposeX:left y:top width:width height:height view:aView
    "an expose event arrived - this is sent from the device (Display)"

    self addDamage:(Rectangle left:left top:top width:width height:height) view:aView.
    self notifyEventArrival
!

noExposeView:aView
    "an noexpose event arrived - this is sent from the device (Display)"

    gotExpose := true.
    exposeEventSemaphore notNil ifTrue:[
	exposeEventSemaphore signal
    ]
!

graphicExposeX:left y:top width:width height:height view:aView
    self addDamage:(left @ top extent:width @ height) view:aView.
!

buttonMotion:state x:x y:y view:aView
    "mouse was moved - this is sent from the device (Display)"

    |args|

    ignoreUserInput == true ifTrue:[
	^ self
    ].
    args := Array with:state with:x with:y.

    compressMotionEvents ifTrue:[
	"
	 merge with last motion
	"
	mouseAndKeyboard reverseDo:[:ev |
	    ev notNil ifTrue:[
		((ev type == #buttonMotion:x:y:) 
		and:[(ev view == aView)
		and:[(ev arguments at:1) == state]]) ifTrue:[
		    ev arguments:args.
		    ^ self
		]
	    ]
	]
    ].
    mouseAndKeyboard
	addLast:(WindowEvent
		     for:aView
		     type:#buttonMotion:x:y:
		     arguments:args).
    self notifyEventArrival
!

buttonPress:button x:x y:y view:aView
    "mouse button was pressed - this is sent from the device (Display)"

    ignoreUserInput == true ifTrue:[
	^ self
    ].
    mouseAndKeyboard 
	addLast:(WindowEvent 
		     for:aView
		     type:#buttonPress:x:y:
		     arguments:(Array with:button with:x with:y)).
    self notifyEventArrival
!

buttonRelease:button x:x y:y view:aView
    "mouse button was released- this is sent from the device (Display)"

    ignoreUserInput == true ifTrue:[
	^ self
    ].
    mouseAndKeyboard
	addLast:(WindowEvent
		     for:aView 
		     type:#buttonRelease:x:y:
		     arguments:(Array with:button with:x with:y)).
    self notifyEventArrival
!

buttonMultiPress:button x:x y:y view:aView
    "mouse button was pressed - this is sent from the device (Display)"

    ignoreUserInput == true ifTrue:[
	^ self
    ].
    mouseAndKeyboard
	 addLast:(WindowEvent
		      for:aView
		      type:#buttonMultiPress:x:y:
		      arguments:(Array with:button with:x with:y)).
    self notifyEventArrival
!

buttonShiftPress:button x:x y:y view:aView
    "mouse button was pressed - this is sent from the device (Display)"

    ignoreUserInput == true ifTrue:[
	^ self
    ].
    mouseAndKeyboard
	addLast:(WindowEvent
		     for:aView
		     type:#buttonShiftPress:x:y:
		     arguments:(Array with:button with:x with:y)).
    self notifyEventArrival
!

keyPress:key x:x y:y view:aView
    "key was pressed - this is sent from the device (Display).
     beside the keyboard translation, CntlC processing is done here."

    |xlatedKey group process|

    translateKeyboardEvents ifTrue:[
	xlatedKey := aView device translateKey:key.
    ] ifFalse:[
	xlatedKey := key.
    ].

    (xlatedKey == #CtrlV) ifTrue:[
	'Smalltalk/X ' errorPrint. 
	Smalltalk versionString errorPrint. ' of ' errorPrint.
	Smalltalk versionDate errorPrintNL.
	Smalltalk copyrightString errorPrintNL.
    ].

    ((xlatedKey == #UserInterrupt) and:[ControlCEnabled]) ifTrue:[
	"
	 Special handling for Ctrl-C: interrupt the underlying process.

	 cannot halt here (this would stop the event-dispatcher),
	 but instead interrupt the underlying process and have it
	 perform the userInterrupt in the interrupt-method.
	"
	group := aView windowGroup.
	group notNil ifTrue:[
	    process := group process.
	    process notNil ifTrue:[
		process interruptWith:[process userInterrupt]
	    ]
	].
	^ self
    ].

    ignoreUserInput == true ifTrue:[
	^ self
    ].
    mouseAndKeyboard
	addLast:(WindowEvent
		     for:aView
		     type:#keyPress:x:y:
		     arguments:(Array with:xlatedKey with:x with:y)).
    self notifyEventArrival
!

keyRelease:key x:x y:y view:aView
    "key was released - this is sent from the device (Display)."

    |xlatedKey|

    ignoreUserInput == true ifTrue:[
	^ self
    ].
    translateKeyboardEvents ifTrue:[
	xlatedKey := aView device translateKey:key.
    ] ifFalse:[
	xlatedKey := key.
    ].
    mouseAndKeyboard
	addLast:(WindowEvent
		     for:aView
		     type:#keyRelease:x:y:
		     arguments:(Array with:xlatedKey with:x with:y)).
    self notifyEventArrival
!

pointerEnter:state x:x y:y view:aView
    "mouse cursor was moved into the view - this is sent from the device (Display)"

    mouseAndKeyboard
	addLast:(WindowEvent
		  for:aView
		  type:#pointerEnter:x:y:
		  arguments:(Array with:state with:x with:y)).
    self notifyEventArrival
!

pointerLeave:state view:aView
    "mouse cursor was moved out of the view - this is sent from the device (Display)"

    mouseAndKeyboard
	 addLast:(WindowEvent
		     for:aView
		     type:#pointerLeave: 
		     arguments:(Array with:state)).
    self notifyEventArrival
!

configureX:x y:y width:w height:h view:aView
    damage
	addLast:(WindowEvent
		     for:aView
		     type:#configureX:y:width:height:
		     arguments:(Array with:x with:y with:w with:h)).
    self notifyEventArrival
!

coveredBy:sibling view:aView
    "aView was covered by one of its siblings"

    damage
	 addLast:(WindowEvent
		     for:aView
		     type:#coveredBy:
		     arguments:(Array with:sibling)).
    self notifyEventArrival
!

focusInView:aView
    "view got input focus - this is sent from the device (Display)"

    mouseAndKeyboard
	 addLast:(WindowEvent
		     for:aView
		     type:#focusIn).
    self notifyEventArrival
!

focusOutView:aView
    "view lost input focus - this is sent from the device (Display)"

    mouseAndKeyboard
	 addLast:(WindowEvent
		     for:aView
		     type:#focusOut).
    self notifyEventArrival
!

mappedView:aView
    "view was mapped (from window manager)"

    damage
	 addLast:(WindowEvent
		     for:aView
		     type:#mapped).
    self notifyEventArrival
!

unmappedView:aView
    "view was unmapped (from window manager)"

    damage
	 addLast:(WindowEvent
		     for:aView
		     type:#unmapped).
    self notifyEventArrival
!

terminateView:aView
    "view should terminate (from window manager)"

    self flushEventsFor:aView.
    damage
	 addLast:(WindowEvent
		     for:aView
		     type:#terminate).
    self notifyEventArrival
!

saveAndTerminateView:aView
    "view should save & terminate (from window manager)"

    self flushEventsFor:aView.
    damage
	 addLast:(WindowEvent
		     for:aView
		     type:#saveAndTerminate).
    self notifyEventArrival
!

destroyedView:aView
    "view was destroyed (from window manager)"

    "at this time, the view is already gone; remove
     all pending events for this one ..."

    self flushEventsFor:aView.
    damage
	 addLast:(WindowEvent
		     for:aView
		     type:#destroyed).
    self notifyEventArrival
! !

!WindowSensor methodsFor:'initialization'!

initialize
    "initialize the event queues to empty"

    damage := OrderedCollection new.
    mouseAndKeyboard := OrderedCollection new.
    gotExpose := true.
    catchExpose := false.
    compressMotionEvents := true.
    ignoreUserInput := false.
    translateKeyboardEvents := true
!

reinitialize
    "reinitialize the event queues to empty; leave other setup as-is"

    self flushUserEvents.
    self flushExposeEvents.
    gotExpose := true.
    catchExpose := false.
! !

!WindowSensor methodsFor:'accessing'!

ignoreUserInput:aBoolean
    ignoreUserInput := aBoolean
!

ignoreUserInput
    ^ ignoreUserInput
!

eventSemaphore:aSemaphore
    "set the semaphore used to signal event arrival"

    eventSemaphore := aSemaphore
!

eventSemaphore
    "return the semaphore used to signal event arrival"

    ^ eventSemaphore
!

compressMotionEvents:aBoolean
    compressMotionEvents := aBoolean
! !