SelectionInListModelView.st
author ca
Thu, 24 Oct 2002 15:35:19 +0200
changeset 2324 ada66df6ac14
parent 2323 8257059da044
child 2330 7d83a6a7728b
permissions -rw-r--r--
support of highlighting items under mouse

"
 COPYRIGHT (c) 1999 by eXept Software AG
              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.
"


"{ Package: 'stx:libwidg2' }"

ListModelView subclass:#SelectionInListModelView
	instanceVariableNames:'selection multipleSelectOk selectOnButtomMenu actionBlock
		doubleClickActionBlock selectConditionBlock clickLine
		highlightMode useIndex ignoreReselect toggleSelect hilightFgColor
		hilightBgColor hilightLevel hilightFrameColor hilightStyle
		dragAccessPoint dropTarget dropSource editorView openEditorAction
		closeEditorAction highlightEnterItem enterItem'
	classVariableNames:'DefaultHilightStyle DefaultHilightBackgroundColor
		DefaultHilightForegroundColor DefaultHilightLevel
		DefaultHilightFrameColor'
	poolDictionaries:''
	category:'Views-Lists'
!

!SelectionInListModelView class methodsFor:'documentation'!

copyright
"
 COPYRIGHT (c) 1999 by eXept Software AG
              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.
"

!

documentation
"
    SelectionInListModelView is mostly like SelectionInListView,
    but derives from the ListModelView and thus the list is kept
    by the model.

    [Instance variables:]

        selection               <misc>       the current selection. nil, a number or collection of numbers
        multipleSelectOk        <Boolean>    allow/disallow multiple selections( default:false )
        selectOnButtomMenu      <Boolean>    enable/disable selection will change on menu pressed

        actionBlock             <Block>      action evaluated on single click
        doubleClickActionBlock  <Block>      action evaluated on double click
        selectConditionBlock

        clickLine               <Number>     clicked line during button motion
        useIndex                <Boolean>    representation of the model selection

        ignoreReselect          <Boolean>    if set, a click on an already selected entry is ignored
        toggleSelect            <Boolean>    a click on an entry unselects it and vice versa

        highlightMode           <Symbol>     how to draw the selection
        hilightFgColor          <Color>      foregroundColor of highlighted items       
        hilightBgColor          <Color>      backgroundColor of highlighted items
        hilightLevel            <Integer>    level to draw selections (i.e. for 3D effect)
        hilightFrameColor       <Color>      rectangle around highlighted items
        hilightStyle            <Boolean>    actions on widget are enabled/disabled

        dragAccessPoint         <Point>      point where the drag operation starts from
        dropTarget              <DropTarget> keeps information about the drop operation
        dropSource              <DropSource> keeps information about the drag operation

        editorView              <View>       editor on current selected item
        openEditorAction        <Action>     action to get an editor on the current selection from user
        closeEditorAction       <Action>     action invoked before the editor is closed.

        enterItem               <Item/nil>   item over which the mouse pointer is located
                                             or nil
        highlightEnterItem      <Boolean>    enable or disable highlight of enterItem

    [author:]
        Claus Atzkern

    [see also:]

        ListModelView
        HierarchicalListView
"
!

examples
"
                                                                        [exBegin]
    |top list view|

    list := List new.

    1 to:100 do:[:i| list add:('element: ', i printString) ].
    top  := StandardSystemView new; extent:300@300.
    view := ScrollableView for:SelectionInListModelView miniScroller:true
                        origin:0.0@0.0 corner:1.0@1.0 in:top.
    view list:list.
    top  open.
                                                                        [exEnd]


                                                                        [exBegin]
    |top list view|

    list := List new.

    1 to:100 do:[:i| list add:('element: ', i printString) ].
    top  := StandardSystemView new; extent:300@300.
    view := ScrollableView for:SelectionInListModelView miniScroller:true
                        origin:0.0@0.0 corner:1.0@1.0 in:top.
    view list:list.

    view openEditorAction:[:ln :aGC| |field|
        field := EditField new.
        field level:0.
        field acceptOnLostFocus:true.
        field acceptAction:[:x| list at:ln put:(field contents) ].
        field font:(aGC font).
        field contents:(list at:ln).
        field
    ].
    top open.
                                                                        [exEnd]

                                                                        [exBegin]
    |top list view item|

    list := HierarchicalList new.
    item := HierarchicalItem::Example labeled:'Test'.
    item expand.
    list showRoot:false.
    list root:item.

    top  := StandardSystemView new; extent:300@300.
    view := ScrollableView for:SelectionInListModelView miniScroller:true
                        origin:0.0@0.0 corner:1.0@1.0 in:top.

    view list:list.
    view doubleClickAction:[:i| (list at:i) toggleExpand ].
    top  open.
                                                                        [exEnd]

"
! !

!SelectionInListModelView class methodsFor:'defaults'!

updateStyleCache
    "extract values from the styleSheet and cache them in class variables"

    <resource: #style   (
                        #'selection.hilightForegroundColor' #'selection.hilightBackgroundColor'
                        #'selection.hilightFrameColor'      #'selection.hilightLevel'
                        #'selection.foregroundColor'        #'selection.backgroundColor'
                        #'selection.shadowColor'            #'selection.lightColor'
                        #'selection.font'                   #'selection.hilightStyle'
                        #'text.foregroundColor'
                        )>

    DefaultHilightForegroundColor  := StyleSheet colorAt:'selection.hilightForegroundColor'.
    DefaultHilightBackgroundColor  := StyleSheet colorAt:'selection.hilightBackgroundColor'.
    DefaultHilightFrameColor       := StyleSheet colorAt:'selection.hilightFrameColor'.
    DefaultHilightLevel            := StyleSheet at:'selection.hilightLevel' default:0.
    DefaultHilightStyle            := StyleSheet at:'selection.hilightStyle' default:(StyleSheet name).
    DefaultForegroundColor         := StyleSheet colorAt:'selection.foregroundColor'.
    DefaultBackgroundColor         := StyleSheet colorAt:'selection.backgroundColor'.
    DefaultShadowColor             := StyleSheet colorAt:'selection.shadowColor'.
    DefaultLightColor              := StyleSheet colorAt:'selection.lightColor'.

    DefaultForegroundColor isNil ifTrue:[
        DefaultForegroundColor := StyleSheet colorAt:'text.foregroundColor' default:Black
    ].
    "
     self updateStyleCache
    "


! !

!SelectionInListModelView methodsFor:'accessing'!

list:aList
    "get the status of <showRoot> from the list
    "
    self deselectWithoutRedraw.
  ^ super list:aList
! !

!SelectionInListModelView methodsFor:'accessing editor'!

closeEditor
    "close the current editor
    "
    |editor|

    (editor := editorView) notNil ifTrue:[
        editorView := nil.
        closeEditorAction notNil ifTrue:[
            closeEditorAction value:editor.
        ].
        editor destroy.
    ].
!

closeEditorAction
    "get the block which is evaluated before the editor is closed; the argument
     to the action is the editor.
    "
    ^ closeEditorAction
!

closeEditorAction:anOneArgAction
    "get the block which is evaluated before the editor is closed; the argument
     to the action is the editor.
    "
    closeEditorAction := anOneArgAction.
!

openEditor
    "opens the editor on the current selection;
     returns the editorView or nil if no openEditorAction is defined
     or no single selection exists ...
    "
    |numArgs lnNr|

    self closeEditor.
    shown ifFalse:[^ nil].
    openEditorAction isNil ifTrue:[^ nil].
    lnNr := self selectedIndex.
    lnNr == 0 ifTrue:[^ nil].

    self makeSelectionVisible.

    numArgs := openEditorAction numArgs.

    numArgs == 0 ifTrue:[
        editorView := openEditorAction value
    ] ifFalse:[
        numArgs == 1 ifTrue:[editorView := openEditorAction value:lnNr]
                    ifFalse:[editorView := openEditorAction value:lnNr value:self]
    ].
    editorView isNil ifTrue:[^ nil].

    editorView superView isNil ifTrue:[
        self addSubView:editorView
    ].
    self computeEditorLayout.
    editorView realize.
    self windowGroup focusView:editorView.
  ^ editorView
!

openEditorAction
    "get the block which is evaluated to get the editor which is set for the
     line; the arguments to the block is the line number and the widget itself
    "
    ^ openEditorAction

!

openEditorAction:aTwoArgAction
    "set the block which is evaluated to get the editor which is set for the
     line; the arguments to the block is the line number and the widget itself
    "
    openEditorAction := aTwoArgAction.
! !

!SelectionInListModelView methodsFor:'accessing-actions'!

action
    "get the action block to be performed on select
    "
    ^ actionBlock
!

action:aOneArgAction
    "set the action block to be performed on select
    "
    actionBlock := aOneArgAction


!

doubleClickAction
    "get the action block to be performed on doubleclick
    "
    ^ doubleClickActionBlock

!

doubleClickAction:aOneArgAction
    "set the action block to be performed on doubleclick
    "
    doubleClickActionBlock := aOneArgAction

!

selectConditionBlock
    "set the conditionBlock; this block is evaluated before a selection
     change is performed; the change will not be done, if the evaluation
     returns false
    "
    ^ selectConditionBlock

!

selectConditionBlock:aOneArgBlock
    "set the conditionBlock; this block is evaluated before a selection
     change is performed; the change will not be done, if the evaluation
     returns false
    "
    selectConditionBlock := aOneArgBlock.



! !

!SelectionInListModelView methodsFor:'accessing-behavior'!

highlightEnterItem
    "enable or disable to highlight the item over which the mouse pointer is located
    "
    ^ highlightEnterItem
!

highlightEnterItem:aBool
    "enable or disable to highlight the item over which the mouse pointer is located
    "
    highlightEnterItem ~~ aBool ifTrue:[
        highlightEnterItem := aBool.
        self pointerEntersItem:nil.
    ].
!

highlightMode
    "get the mode how to draw a selected line:
        #line           draw whole line selected
        #label          draw label selected
    "
    ^ highlightMode


!

highlightMode:aMode
    "set the mode how to draw a selected line:
        #line           draw whole line selected
        #label          draw label selected
    "
    highlightMode := aMode.
!

ignoreReselect
    "get the ignoreReselect flag - see method #ignoreReselect: for more details
    "
    ^ ignoreReselect
!

ignoreReselect:aBoolean
    "set/clear the ignoreReselect flag - 
     if set, a click on an already selected entry is ignored.
     Otherwise the notification is done, even if no
     change in the selection occurs.
     (for example, in browser to update a method).
     Setting ignoreReselect to false makes sense if data is shown
     which may change by itself (i.e. without the user doing anything)
     For example, the inspector uses this, and redisplays the value,
     if the selection is the same.
     The default is true, meaning that a click on an already selected
     does not lead to a notification via the actionBlock/change mechanism.
    "
    ignoreReselect := aBoolean

!

multipleSelectOk
    "allow/disallow multiple selections; the default is false
    "
    ^ multipleSelectOk


!

multipleSelectOk:aState
    "allow/disallow multiple selections; the default is false
    "
    multipleSelectOk := aState ? false.
!

selectOnButtomMenu
    "define the button-menu-press behavior; if true the line under the mouse
     will be selected before the menu is opened. Otherwise the menu is opened
     on the current selection.
    "
    ^ selectOnButtomMenu
!

selectOnButtomMenu:aBoolean
    "define the button-menu-press behavior; if true the line under the mouse
     will be selected before the menu is opened. Otherwise the menu is opened
     on the current selection.
    "
    selectOnButtomMenu := aBoolean.
!

toggleSelect
    "get the toggleSelect flag - see method #toggleSelect: for more details
    "
    ^ toggleSelect
!

toggleSelect:aBoolean
    "turn on/off toggle select. If true, clicking on a selected entry
     unselects it and vice versa. The default is false, which means
     that clicking on an already selected entry does not change its
     select status (see also ignoreReselect:).
    "
    toggleSelect := aBoolean.


!

useIndex
    "set/clear the useIndex flag.
     the selection writen to the model are the indices into the list
     or the elements selected.
    "
    ^ useIndex


!

useIndex:aBoolean
    "set/clear the useIndex flag.
     the selection writen to the model are the indices into the list
     or the elements selected.
    "
    useIndex := aBoolean ? true


! !

!SelectionInListModelView methodsFor:'change & update'!

argForChangeMessage
    "return the argument for a selectionChange;
     depending on the setting of useIndex, this is either the numeric
     index of the selection or the value (i.e. the string)
    "
    selection isNil ifTrue:[
        multipleSelectOk ifTrue:[^ #()].
        ^ useIndex ifTrue:[0] ifFalse:[nil]
    ].
    useIndex ifTrue:[^ selection].

    multipleSelectOk ifFalse:[
        ^ list at:selection ifAbsent:nil
    ].
    ^ selection collect:[:nr| list at:nr ifAbsent:nil ]
!

contentsChanged
    super contentsChanged.
    self  computeEditorLayout.
!

lineChangedAt:aLnNr with:arg
    super lineChangedAt:aLnNr with:arg.

    (editorView notNil and:[aLnNr == self selectedIndex]) ifTrue:[
        self computeEditorLayout.
    ]
!

listSizeChanged:aLnNr nLines:aDeltaLines
    "update selection
    "
    |changed cnts|

    super listSizeChanged:aLnNr nLines:aDeltaLines.

    selection isNil ifTrue:[^ self].

    list size == 0 ifTrue:[
        selection := nil.
      ^ self selectionChanged
    ].

    multipleSelectOk ifFalse:[
        selection < aLnNr ifTrue:[^ self].

        selection := selection + aDeltaLines.

        (aDeltaLines < 0 and:[selection < aLnNr]) ifTrue:[
            selection := nil.
          ^ self selectionChanged
        ]
    ] ifTrue:[
        changed := false.

        aDeltaLines < 0  ifFalse:[
            selection keysAndValuesDo:[:i :ln|
                ln >= aLnNr ifTrue:[
                    changed := true.
                    selection at:i put:(ln + aDeltaLines)
                ]
            ]
        ] ifTrue:[
            cnts := 0.

            selection keysAndValuesDo:[:i :ln||new|
                ln >= aLnNr ifTrue:[
                    changed := true.

                    (new := ln + aDeltaLines) < aLnNr ifTrue:[
                        cnts := cnts + 1.
                        new  := nil
                    ].
                    selection at:i put:new
                ]
            ].
            cnts ~~ 0 ifTrue:[
                cnts == selection size ifTrue:[
                    selection := nil
                ] ifFalse:[
                    selection := selection select:[:ln| ln notNil]
                ].
              ^ self selectionChanged.
            ].
        ].
        changed ifFalse:[^ self].
    ].

    (useIndex and:[model notNil]) ifTrue:[
        model setValue:(self argForChangeMessage)
    ].


!

originChanged:delta
    "setup the origin of the editing view
    "
    self computeEditorLayout.
    super originChanged:delta.


!

selectionChanged
    "selection has changed. Call actionblock and/or send changeMessage if defined
    "
    |arg|

    self closeEditor.

    (model isNil and:[actionBlock isNil]) ifTrue:[
        ^ self
    ].

    arg := self argForChangeMessage.

    model notNil ifTrue:[
        model removeDependent:self.
        "/ change models value to force a change notification: reselect mode
        arg = model value ifTrue:[
            model setValue:(arg isNil ifTrue:[0] ifFalse:[nil]).
        ].
        self sendChangeMessage:#value: with:arg.
        model notNil ifTrue:[  "/ argggh could be nilled
            model addDependent:self.
        ]
    ].

    actionBlock notNil ifTrue:[
        actionBlock valueWithOptionalArgument:arg
    ].
!

updateFromModel
    "update selection from the model
    "
    |value nsel sensor|

    model isNil ifTrue:[
        ^ self
    ].
    (     multipleSelectOk
     and:[clickLine notNil
     and:[(sensor := self sensor) notNil
     and:[sensor leftButtonPressed]]]
    ) ifTrue:[
        "running in button motion; discard change notification
        "
        ^ self
    ].

    value := model value.

    (useIndex or:[value isNil or:[value isNumber]]) ifFalse:[
        multipleSelectOk ifFalse:[
            value := list identityIndexOf:value
        ] ifTrue:[
            value isEmpty ifTrue:[
                value := nil
            ] ifFalse:[
                nsel := OrderedCollection new.
                value do:[:e||i|(i := list identityIndexOf:e) ~~ 0 ifTrue:[nsel add:i]].
                value := nsel notEmpty ifTrue:[nsel] ifFalse:[nil]
            ]
        ].
    ].
    self setSelection:value.
! !

!SelectionInListModelView methodsFor:'drag & drop'!

canDrag
    "returns true if dragging is enabled
    "
    ^ dropSource notNil

!

dropSource
    "returns the dropSource or nil
    "
    ^ dropSource

!

dropSource:aDropSourceOrNil
    "set the dropSource or nil
    "
    dropSource := aDropSourceOrNil.

!

dropTarget
    "returns the dropTarget or nil
    "
    ^ dropTarget


!

dropTarget:aDropTragetOrNil
    "set the dropTarget or nil
    "
    dropTarget := aDropTragetOrNil.

!

startDragAt:aPoint
    "start drag at a point
    "
    dropSource notNil ifTrue:[
        dropSource startDragSelector notNil ifTrue:[
            dropSource startDragIn:self at:aPoint
        ] ifFalse:[
            DragAndDropManager new startDragFrom:self
                                      dropSource:dropSource
                                          offset:#center
        ]
    ]

! !

!SelectionInListModelView methodsFor:'drawing'!

drawFrom:start to:stop x:x y:y w:w
    "draw the lines between start to stop without clearing the background
    "
    highlightMode notNil ifTrue:[
        self selectionDo:[:lnNr|
            (lnNr between:start and:stop) ifTrue:[
                self drawSelectionFrameAt:lnNr x:x w:w
            ]
        ]
    ].
    self drawElementsFrom:start to:stop x:x y:y w:w.


!

drawLabelAt:anIndex x:x y:y h:h
    "draw the label at position x/y without clearing the background
    "
    |item y0 x1|

    editorView notNil ifTrue:[
        "/ there is an open editor for the line; thus no redraw for the label (hidden by editor)
        self selectedIndex == anIndex ifTrue:[^ self].
    ].

    item := list at:anIndex ifAbsent:nil.

    item notNil ifTrue:[
        (highlightMode notNil and:[self isInSelection:anIndex]) ifTrue:[
            self paint:hilightFgColor on:hilightBgColor
        ] ifFalse:[
            enterItem == item ifTrue:[
                self paint:hilightBgColor on:bgColor.
            ] ifFalse:[
                self paint:fgColor on:bgColor.
            ].
        ].
        self displayElement:item x:x y:y h:h.

        enterItem == item ifTrue:[
            y0 := y + h - 2.
            x1 := x + (item widthOn:self).

            self displayLineFromX:x y:y0 toX:x1 y:y0.
        ].
    ]
!

drawSelectionFrameAt:lnNr x:x w:w
    "draw the background and foreground of the selection frame
     at a lineNr.
    "
    |item
     x0 "{ Class:SmallInteger }"
     x1 "{ Class: SmallInteger }"
     y0 "{ Class:SmallInteger }"
     y1 "{ Class:SmallInteger }"
     hL "{ Class:SmallInteger }"
     wL "{ Class:SmallInteger }"
    |
    editorView notNil ifTrue:[
        "/ there is an open editor; do not redraw selected
        ^ self
    ].

    (item := list at:lnNr ifAbsent:nil) isNil ifTrue:[
        "/ list might change during drawing; item no longer visible
        ^ self
    ].

    "/ CLEAR THE BACKGROUND

    y0 := self yVisibleOfLine:lnNr.
    y1 := self yVisibleOfLine:(lnNr + 1).
    hL := y1 - y0.

    highlightMode == #line ifTrue:[
        x0 := x.
        x1 := x0 + w.
    ] ifFalse:[
        highlightMode == #label ifFalse:[^ self].
        x0 := (self xVisibleOfItem:item) - (textStartLeft // 2).
        x1 := x0 + (item widthOn:self) + textStartLeft.
    ].
    wL := x1 - x0.

    wL > 0 ifFalse:[^ self].

    self paint:hilightBgColor.
    self fillRectangleX:x0 y:y0 width:wL height:hL.

    "/ DRAW THE FRAME

    hilightFrameColor notNil ifTrue:[
        hilightLevel == 0 ifTrue:[
            self paint:hilightFrameColor.

            highlightMode == #line ifTrue:[
                self displayLineFromX:x0 y:y0 toX:x1 y:y0.
                y1 := y0 + hL - 1.
                self displayLineFromX:x0 y:y1 toX:x1 y:y1.
            ] ifFalse:[
                self displayRectangleX:x0 y:y0 width:wL height:hL
            ].
            ^ self.
        ]
    ] ifFalse:[
        hilightStyle == #motif ifTrue:[
            self paint:bgColor.
            y1 := y0 + 1.
            highlightMode == #line ifTrue:[
                self displayLineFromX:x0 y:y1 toX:x1 y:y1.
                y1 := y0 + hL - 2.
                self displayLineFromX:x0 y:y1 toX:x1 y:y1.
            ] ifFalse:[
                self displayRectangleX:x0 + 1 y:y1 width:wL - 2 height:hL - 2
            ]
        ]
    ].

    hilightLevel ~~ 0 ifTrue:[
        "/ draw edge
        highlightMode == #line ifTrue:[
            x0 := margin.
            wL := width - x0 - x0.
        ].
        self drawEdgesForX:x0 y:y0 width:wL height:hL level:hilightLevel.
    ]

!

invalidateSelectionAt:aLineNr
    "redraw a line which changed its selection status.
     optimized when drawing only the label.
    "
    |item x|

    editorView notNil ifTrue:[
        "/ there is an open editor; do not redraw selected
        ^ self
    ].

    (shown and:[aLineNr notNil and:[highlightMode notNil]]) ifFalse:[
        ^ self
    ].

    highlightMode == #label ifTrue:[
        item := list at:aLineNr ifAbsent:nil.

        item isNil ifTrue:[
            ^ self
        ].
        x := (self xVisibleOfItem:item) - (textStartLeft // 2)
    ] ifFalse:[
        x := 0.
    ].
    self invalidateLineAt:aLineNr fromX:x
! !

!SelectionInListModelView methodsFor:'event handling'!

buttonMotion:buttonMask x:x y:y
    "mouse-move while button was pressed - handle selection changes
    "
    |dragPoint lnNr d0 nsel viewOrgY yAbsLine item
     step  "{ Class:SmallInteger }"
    |
    enabled ifFalse:[^ self].

    clickLine isNil ifTrue:[
        highlightEnterItem ifTrue:[
            self sensor anyButtonPressed ifFalse:[
                lnNr := self yVisibleToLineNr:y.

                lnNr notNil ifTrue:[ item := list at:lnNr ifAbsent:nil ]
                           ifFalse:[ item := nil ].

                self pointerEntersItem:item.
            ]
        ].    
        ^ self
    ].

    self hasSelection ifFalse:[^ self].            

    dragAccessPoint notNil ifTrue:[
        dragPoint := x @ y.

        (dragAccessPoint dist:dragPoint) > 5.0 ifTrue:[
            dragAccessPoint := nil.
            self startDragAt:dragPoint
        ].
        ^ self
    ].

    (multipleSelectOk and:[self sensor leftButtonPressed]) ifFalse:[
        ^ self
    ].

    (lnNr := self yVisibleToLineNr:y) isNil ifTrue:[
        lnNr := list size
    ] ifFalse:[
        lnNr := lnNr max:1
    ].
    lnNr == clickLine ifTrue:[^ self].

    autoScroll ifTrue:[
        "/ if moved outside of view, start autoscroll
        (y < 0) ifTrue:[
            ^ self startAutoScroll:[self scrollUp] distance:y.
        ].
        (y > height) ifTrue:[
            ^ self startAutoScroll:[self scrollDown] distance:(y - height).
        ].
    ].

    "move inside - stop autoscroll if any"
    self stopAutoScroll.

    nsel := OrderedCollection new.
    step := lnNr > clickLine ifTrue:[1] ifFalse:[-1].

    clickLine to:lnNr by:step do:[:i|
        ((self isInSelection:i) or:[self canSelectIndex:i forAdd:true]) ifTrue:[nsel add:i].
    ].
    (self selectWithoutScroll:nsel redraw:true) ifFalse:[^ self].

    viewOrgY := viewOrigin y.
    yAbsLine := self yAbsoluteOfLine:lnNr.

    yAbsLine < viewOrgY ifTrue:[
        self scrollTo:( viewOrigin x @ yAbsLine) redraw:true
    ] ifFalse:[
        yAbsLine - viewOrgY > height ifTrue:[
            d0 := yAbsLine - height.
            self scrollTo:( viewOrigin x @ d0) redraw:true.
        ].
    ].
    self repairDamage.
    self selectionChanged.
!

buttonMultiPress:button x:x y:y
    "button was pressed multiple - handle a doubleClick action
    "
    clickLine := nil.
    dragAccessPoint := nil.

    enabled ifFalse:[^ self].

    ((button == 1) or:[button == #select]) ifFalse:[
        ^ super buttonMultiPress:button x:x y:y
    ].
    self doubleClicked
!

buttonPress:button x:x y:y
    "a button was pressed - handle selection here
    "
    |sensor nsel start step item menu appl isInSelection lineNr|

    clickLine       := nil.
    dragAccessPoint := nil.

    self pointerEntersItem:nil.

    enabled ifFalse:[^ self].

    sensor := self sensor.
    sensor notNil ifTrue:[ lineNr := self yVisibleToLineNr:y ]
                 ifFalse:[ lineNr := nil ].

    ((button == 2) or:[button == #menu]) ifTrue:[
        selectOnButtomMenu ifTrue:[
            (     lineNr notNil
             and:[lineNr ~~ self selectedIndex
             and:[self canSelectIndex:lineNr forAdd:false ]]
            ) ifTrue:[
                (self selectWithoutScroll:lineNr redraw:true) ifTrue:[
                    self selectionChanged
                ].
                item := self selectedElement.
            ] ifFalse:[
                item := nil
            ]
        ] ifFalse:[
            item := self selectedElement.
        ].

        item notNil ifTrue:[
            self makeSelectionVisible.

            menu := item perform:#middleButtonMenu ifNotUnderstood:nil.

            menu notNil ifTrue:[
                menu isCollection ifTrue:[
                    menu := Menu new fromLiteralArrayEncoding:menu.
                    appl := self application.

                    appl notNil ifTrue:[
                        menu findGuiResourcesIn:appl.
                        "/ menu receiver:appl  -- now done in findGuiResources ...
                    ] ifFalse:[
                        menu receiver:item
                    ]
                ].
                menu startUp.
              ^ self
            ]
        ].
        super buttonPress:button x:x y:y.
      ^ self
    ].

    (clickLine := lineNr) isNil ifTrue:[ ^ self ].

    isInSelection := self isInSelection:clickLine.

    multipleSelectOk ifTrue:[
        sensor ctrlDown ifTrue:[
            (isInSelection or:[self canSelectIndex:clickLine forAdd:true]) ifTrue:[
                self toggleSelection:clickLine.
                self selectionChanged
            ].
            ^ self
        ].

        (self canSelectIndex:clickLine forAdd:sensor shiftDown) ifFalse:[^ self].

        (sensor shiftDown and:[(start := self firstInSelection) notNil]) ifTrue:[
            step := clickLine < start ifTrue:[-1] ifFalse:[1].
            nsel := OrderedCollection new.

            start to:clickLine by:step do:[:i|
                ((self isInSelection:i) or:[self canSelectIndex:i forAdd:true]) ifTrue:[nsel add:i]
            ].

            (self selectWithoutScroll:nsel redraw:true) ifTrue:[
                self selectionChanged
            ].
            ^ self
        ]
    ] ifFalse:[
        (self canSelectIndex:clickLine forAdd:false) ifFalse:[^ self]
    ].

    (isInSelection and:[self canDrag]) ifTrue:[
        dragAccessPoint := x @ y.
    ] ifFalse:[
        self buttonPressOrReleaseAtLine:clickLine x:x y:y
    ].
!

buttonPressOrReleaseAtLine:aLnNr x:x y:y
    "handle a button press or release at a line
    "
    aLnNr == self selectedIndex ifTrue:[
        editorView notNil ifTrue:[^ self].

        openEditorAction notNil ifTrue:[
            self openEditorAtX:x y:y.
            editorView notNil ifTrue:[^ self].
        ].

        ignoreReselect ifFalse:[
            (toggleSelect and:[self sensor ctrlDown]) ifTrue:[
                self selection:nil
            ] ifFalse:[
                self selectionChanged
            ].
        ].
    ] ifFalse:[
        (self selectWithoutScroll:aLnNr redraw:true) ifTrue:[
            self selectionChanged
        ]
    ].
!

buttonRelease:button x:x y:y
    "a button was released
    "
    |dragPoint clickLnNr|

    dragPoint := dragAccessPoint.
    clickLnNr := clickLine.
    clickLine := dragAccessPoint := nil.

    self stopAutoScroll.

    (enabled and:[dragPoint notNil and:[clickLnNr notNil]]) ifTrue:[
        self buttonPressOrReleaseAtLine:clickLnNr x:(dragPoint x) y:(dragPoint y).
    ].
!

characterPress:aKey x:x y:y
    " a character is pressed - lookup and change selection
    "
    |lnNr size idx sensor stp to1 fr2|

    (enabled and:[(size := self size) > 1]) ifFalse:[
        ^ self
    ].
    lnNr := self firstInSelection ? 0.

    ((sensor := self sensor) notNil and:[sensor shiftDown]) ifTrue:[
        stp := -1.              "/ search backward
        to1 :=  1.
        fr2 := size.
    ] ifFalse:[
        stp := 1.               "/ search forward
        to1 := size.
        fr2 := 1.
    ].

    idx := self findLineFrom:lnNr+stp to:to1 by:stp startingWithCharacter:aKey.

    idx == 0 ifTrue:[
        idx := self findLineFrom:fr2 to:lnNr-stp by:stp startingWithCharacter:aKey
    ].
    idx ~~ 0 ifTrue:[^ self selection:idx]
!

characterSearchItemStringAt:anIndex
    |item|

    item := list at:anIndex ifAbsent:nil.
    item isNil ifTrue:[^ item]. 

    ^ item perform:#string ifNotUnderstood:nil.

!

containerChangedSize
    "/ stupid kludge for motif-style (which draws a frame at the right-edge)
    "/ cg: I think it should not (see normal SelectionInListView, which does only
    "/     draw the frame at the top and bottom, but NOT at the left and right

    selection notNil ifTrue:[
        (hilightFrameColor notNil 
        or:[hilightStyle == #motif
        or:[hilightLevel ~~ 0]]) ifTrue:[
           "/ invalidate the right-edge
            self invalidate:(((width-3) @ 0) corner:((width-1) @ (height-1))).
        ]
    ].

    super containerChangedSize.
!

doubleClicked
    "handle a double click
    "
    (doubleClickActionBlock notNil and:[self numberOfSelections == 1]) ifTrue:[
        (doubleClickActionBlock numArgs == 1) ifTrue:[
            doubleClickActionBlock value:(self selectedIndex)
        ] ifFalse:[
            doubleClickActionBlock value
        ]
    ]

!

findLineFrom:aStart to:aStop by:aStep startingWithCharacter:aCharacter
    "find a line starting with a character
    "
    |item char lbl cmp
     size     "{ Class:SmallInteger }"
     start    "{ Class:SmallInteger }"
     stop     "{ Class:SmallInteger }"
    |
    (size := list size) ~~ 0 ifTrue:[
        aStep > 0 ifTrue:[
            aStart > aStop ifTrue:[^ 0].
        ] ifFalse:[
            (aStep == 0 or:[aStop > aStart]) ifTrue:[^ 0]
        ].

        start := aStart < 0 ifTrue:[1] ifFalse:[aStart min:size].
        stop  := aStop  < 0 ifTrue:[1] ifFalse:[aStop  min:size].
        char  := aCharacter asUppercase.

        start to:stop by:aStep do:[:anIndex|
            (self canSelectIndex:anIndex forAdd:false) ifTrue:[
                lbl := self characterSearchItemStringAt:anIndex.
                lbl notNil ifTrue:[
                    cmp := lbl string at:1 ifAbsent:nil.

                    cmp notNil ifTrue:[
                        (char == cmp or:[char == cmp asUppercase]) ifTrue:[
                            ^ anIndex
                        ]
                    ]
                ]
            ]
        ]
    ].
    ^ 0
!

keyPress:aKey x:x y:y
    "a key was pressed - handle page-keys here
    "
    <resource: #keyboard( #Return #CursorUp #CursorDown )>

    |sensor n size lineNr delta|

    enabled ifFalse:[
        ^ super keyPress:aKey x:x y:y
    ].
    aKey == #Return ifTrue:[
        self numberOfSelections == 1 ifTrue:[self doubleClicked].
      ^ self
    ].

    aKey isCharacter ifTrue:[
        ^ self characterPress:aKey x:x y:y
    ].

    (aKey == #CursorUp or:[aKey == #CursorDown]) ifFalse:[
        ^ super keyPress:aKey x:x y:y
    ].

    (size := self size) == 0 ifTrue:[
        ^ self
    ].

    lineNr := self selectedIndex.
    sensor := self sensor.


    sensor notNil ifTrue:[
        n := (1 + (sensor compressKeyPressEventsWithKey:aKey)) \\ size.
        n == 0 ifTrue:[n := 1].
    ] ifFalse:[
        n := 1
    ].

    aKey == #CursorUp ifTrue:[
        lineNr == 0 ifTrue:[lineNr := size + 1].
        (n := lineNr - n) <= 0 ifTrue:[n := size + n].
        lineNr > size ifTrue:[lineNr := size].
        delta := -1.
    ] ifFalse:[
        (n := lineNr + n) > size ifTrue:[n := n - size].

        lineNr == 0 ifTrue:[lineNr := 1].
        delta := 1.
    ].

    "/ TODO: care for shift-Down (add-to-selection)
    [
        self canSelectIndex:n forAdd:false
    ] whileFalse:[
        n := n + delta.
        n > size ifTrue:[n := 1].
        n == 0 ifTrue:[n := size].
        n == lineNr ifTrue:[^ self]
    ].

    self selection:n
!

pointerEntersItem:anItemOrNil
    "the pointer moves over an item or nil
    "
    |lnNr x newItem|

    (shown and:[list size ~~ 0]) ifFalse:[
        enterItem := nil.
      ^ self.  
    ].

    highlightEnterItem ifTrue:[ newItem := anItemOrNil ]
                      ifFalse:[ newItem := nil ].

    anItemOrNil == enterItem ifTrue:[ ^ self ].

    2 timesRepeat:[
        enterItem notNil ifTrue:[
            lnNr := list identityIndexOf:enterItem.
            lnNr notNil ifTrue:[
                x := self xVisibleOfItem:enterItem.
                self invalidateLineAt:lnNr fromX:x
            ].
        ].
        "/ set the new item
        enterItem := anItemOrNil.
    ].
!

pointerLeave:state
    self pointerEntersItem:nil.
    super pointerLeave:state.
!

sizeChanged:how
    |selectionWasVisible|

    selectionWasVisible := false.

    "/ stupid kludge for motif-style (which draws a frame at the right-edge)
    "/ cg: I think it should not (see normal SelectionInListView, which does only
    "/     draw the frame at the top and bottom, but NOT at the left and right
    selection notNil ifTrue:[
        selectionWasVisible := self isSelectionVisibleIn:(previousExtent ? self extent).

        (hilightFrameColor notNil 
        or:[hilightStyle == #motif
        or:[hilightLevel ~~ 0]]) ifTrue:[
           "/ invalidate the right-edge
            self invalidate:(((width-3) @ 0) corner:((width-1) @ (height-1))).
        ].
    ].
    super sizeChanged:how.

    selectionWasVisible ifTrue:[
        self makeSelectionVisible
    ].
! !

!SelectionInListModelView methodsFor:'focus handling'!

wantsFocusWithPointerEnter
    "return true, if I want the focus when
     the mouse pointer enters"

    |pref|

    pref := UserPreferences current focusFollowsMouse.
    (pref ~~ false
    and:[(styleSheet at:#'selection.requestFocusOnPointerEnter' default:true)
    ]) ifTrue:[
        list size > 0 ifTrue:[
            ^ true
        ]
    ].
    ^ false
! !

!SelectionInListModelView methodsFor:'initialize / release'!

fetchResources
    "fetch device colors and ..., to avoid reallocation at redraw time;
     *** called after a create or snapin to fetch all device resources
    "

    super fetchResources.

    hilightFgColor    := self colorOnDevice:hilightFgColor.
    hilightBgColor    := self colorOnDevice:hilightBgColor.
    hilightFrameColor := self colorOnDevice:hilightFrameColor.
!

initStyle
    "setup viewStyle specifics
    "
    super initStyle.

    hilightFrameColor   := nil.
    hilightStyle        := DefaultHilightStyle.
    highlightMode       := #label.
    textStartLeft       := 4.
    selectOnButtomMenu  := styleSheet at:#'selection.selectOnButtomMenu' default:false.

    super font:(styleSheet fontAt:#'selection.font').

    device hasGrayscales ifTrue:[
        "
         must get rid of these hard codings
        "
        (hilightStyle == #next) ifTrue:[
            hilightFgColor := fgColor.
            hilightBgColor := White.
            hilightFrameColor := fgColor
        ] ifFalse:[
            (hilightStyle == #motif) ifTrue:[
                fgColor := White.
                bgColor := Grey.
                viewBackground := bgColor.
                hilightFgColor := bgColor.
                hilightBgColor := fgColor.
            ] ifFalse:[
                (hilightStyle == #openwin) ifTrue:[
                    hilightFgColor := fgColor.
                    hilightBgColor := Color grey.
                ]
            ]
        ]
    ].

    hilightFgColor isNil ifTrue:[
        hilightFgColor := bgColor.
    ].
    hilightBgColor isNil ifTrue:[
        hilightBgColor := fgColor.
    ].
    DefaultForegroundColor notNil ifTrue:[
        fgColor := DefaultForegroundColor
    ].
    DefaultBackgroundColor notNil ifTrue:[
        bgColor := viewBackground := DefaultBackgroundColor
    ].

    DefaultHilightForegroundColor notNil ifTrue:[
        hilightFgColor := DefaultHilightForegroundColor
    ].
    DefaultHilightBackgroundColor notNil ifTrue:[
        hilightBgColor := DefaultHilightBackgroundColor
    ].
    DefaultHilightFrameColor notNil ifTrue:[
        hilightFrameColor := DefaultHilightFrameColor
    ].

    hilightLevel := DefaultHilightLevel ? 0.
    lineSpacing  := (hilightLevel abs > 0) ifTrue:[3] ifFalse:[2].

    hilightFgColor isNil ifTrue:[
        hilightFgColor := bgColor.
        hilightBgColor := fgColor
    ].
!

initialize
    "setup default attributes/behavior
    "
    multipleSelectOk   := false.
    useIndex           := true.
    ignoreReselect     := true.
    toggleSelect       := false.
    highlightEnterItem := false.

    super initialize.
!

mapped
    "get selection from model; scroll to selection
    "
    super mapped.
    self makeSelectionVisible.
!

realize
    "get selection from model; scroll to selection
    "
    self updateFromModel.
    super realize.
! !

!SelectionInListModelView methodsFor:'private editor'!

computeEditorLayout
    "update the layout of the editor
    "
    |lnNr item y x h|

    editorView isNil ifTrue:[
        ^ self
    ].

    (    (lnNr := self selectedIndex) == 0
     or:[(item := list at:lnNr ifAbsent:nil) isNil]
    ) ifTrue:[
        "/ there is no more single selection; thus close the editor
        ^ self closeEditor
    ].

    y := self yVisibleOfLine:lnNr.
    x := (self xVisibleOfItem:item) - (textStartLeft // 2).
    h := self yVisibleOfLine:(lnNr + 1).

    editorView layout:( Rectangle left:x top:y right:(width - 1 - margin) bottom:(h - 1) ).
!

openEditorAtX:x y:y
    "opens an editor on the current single selection
    "
    |item lnNr x0 y0 editor|

    self closeEditor.
    shown ifFalse:[^ self ].
    openEditorAction isNil ifTrue:[^ self].

    lnNr := self selectedIndex.
    lnNr == 0 ifTrue:[^ self].

    item := list at:lnNr ifAbsent:nil.
    item isNil ifTrue:[^ self].

    x < (self xVisibleOfItem:item) ifTrue:[
        "/ not part of the selection frame; ignorre
        ^ self
    ].
    editor := self openEditor.
    editor isNil ifTrue:[^ self].

    y0 := (y - editor origin y) max:0.
    x0 := (x - editor origin x) max:0.

    "/ simulate clicking into the editor
    self sensor pushEvent:(WindowEvent buttonPress:#select x:x0 y:y0 view:editor).
    self sensor pushEvent:(WindowEvent buttonRelease:#select x:x0 y:y0 view:editor).

    "/ to clear the selection
    self invalidateLineAt:lnNr.
! !

!SelectionInListModelView methodsFor:'protocol'!

drawElementsFrom:start to:stop x:x y:y w:w
    "draw the items between start to stop without clearing the background
    "
    |y0 "{ Class:SmallInteger }"
     y1 "{ Class:SmallInteger }"
     x0 "{ Class:SmallInteger }"
    |
    x0 := textStartLeft - viewOrigin x.
    y1 := y.

    start to:stop do:[:i|
        y0 := y1.
        y1 := self yVisibleOfLine:(i + 1).
        self drawLabelAt:i x:x0 y:y0 h:(y1 - y0)
    ].


!

lostSynchronisation
    "called when the changes derived from the model are faster than the handling
    "
    self deselectWithoutRedraw.
    super lostSynchronisation.
!

xVisibleOfItem:anItem
    "returns the visible x of the labeled text
    "
    ^ textStartLeft - viewOrigin x


! !

!SelectionInListModelView methodsFor:'selection'!

deselect
    "clear selection
    "
    self selection:nil

!

firstInSelection
    "returns line number of first element selected or nil
    "
    multipleSelectOk ifFalse:[
        ^ selection
    ].
    ^ selection isNil ifTrue:[nil] ifFalse:[selection at:1]
!

hasSelection
    "returns true if a selection exists
    "
    ^ selection notNil

!

isInSelection:aNumber
    "return true, if line, aNumber is in the selection
    "
    multipleSelectOk ifFalse:[
        ^ aNumber == selection
    ].
    ^ selection isNil ifTrue:[false]
                     ifFalse:[selection includesIdentical:aNumber]
!

isSelectionVisible
    "returns true if any selected element is visible"

    ^ self isSelectionVisibleIn:self extent
!

isSelectionVisibleIn:anExtentPoint
    "returns true if any selected element is visible"

    self selectionDo:[:aLineNr|
        (self isLineVisible:aLineNr in:anExtentPoint) ifTrue:[^ true].
    ].
    ^ false
!

numberOfSelections
    "return the number of selected items
    "
    selection notNil ifTrue:[
        ^ multipleSelectOk ifFalse:[1] ifTrue:[selection size]
    ].
    ^ 0

!

selectElement:anElement
    "select the element. Scroll to make the new selection visible.
     Model and/or actionBlock notification IS done.
    "
    self selectElement:anElement ifAbsent:[self deselect]
!

selectElement:anElement ifAbsent:exceptionalValue
    |idx|

    idx := list identityIndexOf:anElement.
    idx == 0 ifTrue:[
        ^ exceptionalValue value
    ].
    self selection:idx
!

selectedElement
    "return the single selected item or nil
    "
    |index|

    (index := self selectedIndex) ~~ 0 ifTrue:[
        ^ list at:index ifAbsent:nil
    ].
    ^ nil
!

selectedIndex
    "returns the index of the selected line or 0. If more
     lines are selected, 0 is returned
    "
    self numberOfSelections == 1 ifTrue:[
        ^ self firstInSelection
    ].
    ^ 0
!

selection
    "return the selection index or collection of indices 
     in case of multiple selection enabled
    "
    ^ selection

!

selection:something
    "select something or deselect if the argument is nil;
     scroll to make the selected line visible.
     The model and/or actionBlock IS notified.
    "
    (self selectWithoutScroll:something redraw:true) ifTrue:[
        self makeSelectionVisible.
        self selectionChanged
    ].

!

selectionDo:aBlock
    "perform aBlock for each nr in the selection.
     For single selection, it is called once for the items nr.
     For multiple selections, it is called for each.
    "
    selection notNil ifTrue:[
        multipleSelectOk ifTrue:[
            selection do:aBlock
        ] ifFalse:[
            aBlock value:selection
        ].
    ].


!

setSelection:something
    "select something or deselect if the argument is nil;
     scroll to make the selected line visible.
     *** No model and/or actionBlock notification is done here.
    "
    (self selectWithoutScroll:something redraw:true) ifTrue:[
        self makeSelectionVisible
    ]

! !

!SelectionInListModelView methodsFor:'selection private'!

canSelectIndex:anIndex
    "return true, if the object identified by its index is selectable
    "
    ^ self canSelectIndex:anIndex forAdd:false
!

canSelectIndex:anIndex forAdd:isForAdd
    "return true, if the object identified by its index is selectable
    "
    |item isOk|

    selectConditionBlock notNil ifTrue:[
        selectConditionBlock numArgs == 2 ifTrue:[
            isOk := selectConditionBlock value:anIndex value:isForAdd.
        ] ifFalse:[
            isOk := selectConditionBlock value:anIndex.
        ].
        isOk ifFalse:[
            ^ false
        ]
    ].

    item := list at:anIndex ifAbsent:nil.
    item isNil ifTrue:[^ false].
    item isHierarchicalItem ifTrue:[
      ^ item isSelectable
    ].
    ^ true
!

deselectWithoutRedraw
    "clear the selection without redraw and scrolling;
     the model and/or actionBlock is notified
    "
    (self selectWithoutScroll:nil redraw:false) ifTrue:[
        self selectionChanged
    ]
!

makeSelectionVisible
    "scroll to make the selection line visible
    "
    |lineNr|

    lineNr := self firstInSelection.

    lineNr notNil ifTrue:[
        self scrollToLine:lineNr
    ]

!

selectWithoutScroll:something redraw:doRedraw
    "change selection to something; returns true if selection changed
    "
    |oldSelect|

    selection = something ifTrue:[^ false].

    oldSelect := selection.

    (something isNil or:[something == 0]) ifTrue:[
        selection := nil
    ] ifFalse:[
        something isNumber ifTrue:[
            selection := multipleSelectOk ifTrue:[Array with:something]
                                         ifFalse:[something]
        ] ifFalse:[
            something size == 0 ifTrue:[
                selection := nil
            ] ifFalse:[
                selection := multipleSelectOk ifTrue:[something]
                                             ifFalse:[something at:1]
            ]
        ]
    ].
    selection = oldSelect ifTrue:[^ false].
    self closeEditor.

    (doRedraw and:[shown]) ifFalse:[
        ^ true
    ].

    multipleSelectOk ifFalse:[
        oldSelect notNil ifTrue:[self invalidateSelectionAt:oldSelect].
        selection notNil ifTrue:[self invalidateSelectionAt:selection].
    ] ifTrue:[
        (selection notNil and:[oldSelect notNil]) ifTrue:[
            selection do:[:i|(oldSelect includesIdentical:i) ifFalse:[self invalidateSelectionAt:i]].
            oldSelect do:[:i|(selection includesIdentical:i) ifFalse:[self invalidateSelectionAt:i]].
        ] ifFalse:[
            oldSelect isNil ifTrue:[oldSelect := selection].
            oldSelect do:[:i|self invalidateSelectionAt:i]
        ]
    ].
    ^ true
!

toggleSelection:aNumber
    "add a number to the selection. No scrolling is done.
     *** No model and/or actionBlock notification is done here.
    "
    |oldSelect doRemove|

    multipleSelectOk ifFalse:[
        aNumber == selection ifTrue:[                   "/ remove from selection
            selection := nil.
        ] ifFalse:[                                     "/ add to selection
            oldSelect := selection.
            selection := aNumber.

            oldSelect notNil ifTrue:[
                self invalidateSelectionAt:oldSelect
            ]
        ].
        self invalidateSelectionAt:aNumber.
      ^ self
    ].

    selection isNil ifTrue:[                            "/ add to empty selection
        selection := OrderedCollection with:aNumber.
        self invalidateSelectionAt:aNumber.
      ^ self
    ].

    doRemove := selection includesIdentical:aNumber.

    (doRemove and:[selection size == 1]) ifTrue:[       "/ remove selection
        selection := nil.
        self invalidateSelectionAt:aNumber.
      ^ self
    ].

    selection := selection asOrderedCollection.

    doRemove ifTrue:[
        selection removeIdentical:aNumber.
    ] ifFalse:[
        selection add:aNumber.
    ].
    self invalidateSelectionAt:aNumber.
! !

!SelectionInListModelView class methodsFor:'documentation'!

version
    ^ '$Header: /cvs/stx/stx/libwidg2/SelectionInListModelView.st,v 1.61 2002-10-24 13:35:19 ca Exp $'
! !