The Trunk: Morphic-mt.1569.mcz

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
|

The Trunk: Morphic-mt.1569.mcz

commits-2
Marcel Taeumel uploaded a new version of Morphic to project The Trunk:
http://source.squeak.org/trunk/Morphic-mt.1569.mcz

==================== Summary ====================

Name: Morphic-mt.1569
Author: mt
Time: 14 October 2019, 7:33:22.288144 pm
UUID: ee898d43-7d6a-be4f-aa27-a6e194f9302a
Ancestors: Morphic-mt.1568

Cleans up pluggable (multi-column) lists and fixes some caching bugs. Adds a lookup cache between modelIndex and viewIndex for faster selections in filtered lists. Makes multi-column lists work again (?) with filtering etc. PluggableMultiColumnListMorphByItem should work, too.

LazyListMorph now supports #cellInset on all edges and #cellPositioning along the horizontal axis (i.e., #leftCenter, #center, #rightCenter).

PluggableMultiColumnListMorph uses multiple LazyListMorph instead of the MulticolumnLazyListMorph ... which is now deprecated. I think. That's why filtering and drawing works as in the regular PluggableListMorph.

Even if LazyListMorph does not have an actual LayoutPolicy, the callbacks #doLayoutIn: and #submorphBoundsForShrinkWrap are enough to do most things. Note that LazyListMorph calculates some "layout" in #maxWidth, #itemAt: and most of it via #drawOn:, which is not common in Morphic. But it helps with the performance, I guess.

=============== Diff against Morphic-mt.1568 ===============

Item was added:
+ (PackageInfo named: 'Morphic') preamble: 'PluggableListMorph allSubInstancesDo: [:m |
+ m listMorph cellInset: 3@0].'!

Item was removed:
- ----- Method: AlternatePluggableListMorphOfMany>>handlesMouseDown: (in category 'event handling') -----
- handlesMouseDown: evt
- ^ true!

Item was changed:
  ----- Method: AlternatePluggableListMorphOfMany>>itemSelectedAmongMultiple: (in category 'model access') -----
+ itemSelectedAmongMultiple: viewIndex
+ ^self listSelectionAt: (self modelIndexFor: viewIndex)!
- itemSelectedAmongMultiple: index
- ^self listSelectionAt: (self modelIndexFor: index)!

Item was removed:
- ----- Method: AlternatePluggableListMorphOfMany>>list: (in category 'initialization') -----
- list: listOfStrings
- self isThisEverCalled .
- scroller removeAllMorphs.
- list := listOfStrings ifNil: [Array new].
- list isEmpty ifTrue: [^ self selectedMorph: nil].
- super list: listOfStrings.
-
- "At this point first morph is sensitized, and all morphs share same handler."
- scroller firstSubmorph on: #mouseEnterDragging
- send: #mouseEnterDragging:onItem:
- to: self.
- scroller firstSubmorph on: #mouseUp
- send: #mouseUp:onItem:
- to: self.
- "This should add this behavior to the shared event handler thus affecting all items"!

Item was changed:
+ ----- Method: AlternatePluggableListMorphOfMany>>listSelectionAt: (in category 'model access') -----
- ----- Method: AlternatePluggableListMorphOfMany>>listSelectionAt: (in category 'drawing') -----
  listSelectionAt: index
  getSelectionListSelector ifNil:[^false].
  ^model perform: getSelectionListSelector with: index!

Item was changed:
+ ----- Method: AlternatePluggableListMorphOfMany>>listSelectionAt:put: (in category 'model access') -----
- ----- Method: AlternatePluggableListMorphOfMany>>listSelectionAt:put: (in category 'drawing') -----
  listSelectionAt: index put: value
  setSelectionListSelector ifNil:[^false].
  ^model perform: setSelectionListSelector with: index with: value!

Item was changed:
  ----- Method: AlternatePluggableListMorphOfMany>>mouseUp: (in category 'event handling') -----
  mouseUp: event
 
  event hand newKeyboardFocus: self.
+ hasFocus := true.
+ Cursor normal show.!
- hasFocus := true.!

Item was changed:
+ ----- Method: AlternatePluggableListMorphOfMany>>update: (in category 'updating') -----
- ----- Method: AlternatePluggableListMorphOfMany>>update: (in category 'event handling') -----
  update: aSymbol
+
+ aSymbol == #allSelections ifTrue: [
+ "Convenient - yet hard-coded - way to refresh all selections."
+ super update: getIndexSelector.
- aSymbol == #allSelections ifTrue:
- [self selectionIndex: self getCurrentSelectionIndex.
  ^ self changed].
+ aSymbol == getSelectionListSelector ifTrue: [
+ ^ self changed].
+
+ super update: aSymbol.!
- ^ super update: aSymbol!

Item was changed:
  Morph subclass: #LazyListMorph
+ instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex'
- instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth'
  classVariableNames: ''
  poolDictionaries: ''
  category: 'Morphic-Widgets'!
 
+ !LazyListMorph commentStamp: 'mt 10/13/2019 19:44' prior: 0!
- !LazyListMorph commentStamp: 'efc 8/6/2005 11:34' prior: 0!
  The morph that displays the list in a PluggableListMorph.  It is "lazy" because it will only request the list items that it actually needs to display.
 
+ I will cache the maximum width of my items in maxWidth to avoid this potentially expensive and frequent computation.
+
+ The following layout properties are supported:
+ - #cellPositioning: #leftCenter [default], #center, #rightCenter
+ - #cellInset: [default: 3@0 corner: 3@0]!
- I will cache the maximum width of my items in maxWidth to avoid this potentially expensive and frequent computation.!

Item was removed:
- ----- Method: LazyListMorph>>adjustHeight (in category 'drawing') -----
- adjustHeight
- "private.  Adjust our height to match the length of the underlying list"
- self height: (listItems size max: 1) * font height
- !

Item was removed:
- ----- Method: LazyListMorph>>adjustWidth (in category 'drawing') -----
- adjustWidth
- "private.  Adjust our height to match the length of the underlying list"
- self width: ((listSource width max: self hUnadjustedScrollRange) + 20).
- !

Item was changed:
  ----- Method: LazyListMorph>>bottomVisibleRowForCanvas: (in category 'drawing') -----
  bottomVisibleRowForCanvas: aCanvas
          "return the bottom visible row in aCanvas's clip rectangle"
+         ^self rowAtLocation: aCanvas clipRect bottomLeft - (0@1).
-         ^self rowAtLocation: aCanvas clipRect bottomLeft.
  !

Item was added:
+ ----- Method: LazyListMorph>>cellInset: (in category 'layout') -----
+ cellInset: inset
+ "Always store a rectangle to speed up drawing."
+
+ super cellInset: (inset isRectangle
+ ifTrue: [inset]
+ ifFalse: [inset asPoint corner: inset asPoint]).!

Item was added:
+ ----- Method: LazyListMorph>>columnIndex (in category 'accessing') -----
+ columnIndex
+ ^ columnIndex!

Item was added:
+ ----- Method: LazyListMorph>>columnIndex: (in category 'accessing') -----
+ columnIndex: anInteger
+ columnIndex := anInteger.!

Item was changed:
  ----- Method: LazyListMorph>>display:atRow:on: (in category 'drawing') -----
  display: item atRow: row on: canvas
  "display the given item at row row"
 
+ | drawBounds emphasized rowColor itemAsText alignment |
- | drawBounds emphasized rowColor itemAsText |
  itemAsText := item asStringOrText.
+ alignment := self cellPositioning.
 
  "If it is a text, we will only use the first character's emphasis."
  emphasized := itemAsText isText
  ifTrue: [font emphasized: (itemAsText emphasisAt: 1)]
  ifFalse: [font].
 
  rowColor := itemAsText isText
  ifTrue: [itemAsText colorAt: 1 ifNone: [self colorForRow: row]]
  ifFalse: [self colorForRow: row].
 
+ drawBounds := self drawBoundsForRow: row.
+
+ alignment ~= #leftCenter ifTrue: [
+ | itemWidth |
+ itemWidth := self widthToDisplayItem: item. "includes left/right margins"
+ alignment == #center ifTrue: [
+ drawBounds := (self center x - (itemWidth / 2) floor) @ drawBounds top corner: (self center x + (itemWidth / 2) ceiling) @ drawBounds bottom].
+ alignment == #rightCenter ifTrue: [
+ drawBounds := (self right - itemWidth) @ drawBounds top corner: self right @ drawBounds bottom]].
- drawBounds := (self drawBoundsForRow: row) translateBy: (self hMargin @ 0).
- drawBounds := drawBounds intersect: self bounds.
 
  "Draw icon if existing. Adjust draw bounds in that case."
+ drawBounds := drawBounds translateBy: (self cellInset left @ 0).
  (self icon: row) ifNotNil: [ :icon || top |
  top := drawBounds top + ((drawBounds height - icon height) // 2).
  canvas translucentImage: icon at: drawBounds left @ top.
  drawBounds := drawBounds left: drawBounds left + icon width + 2 ].
 
  "We will only draw strings here."
+ drawBounds := drawBounds translateBy: (0 @ self cellInset top).
  canvas
  drawString: itemAsText asString
  in: drawBounds
  font: emphasized
  color: rowColor.
 
  "Draw filter matches if any."
  self
  displayFilterOn: canvas
  for: row
  in: drawBounds
  font: emphasized.!

Item was added:
+ ----- Method: LazyListMorph>>doLayoutIn: (in category 'layout') -----
+ doLayoutIn: layoutBounds
+ "Only consider #shrinkWrap. Other layout properties are implemented in #drawOn:."
+
+ self adjustLayoutBounds.
+ fullBounds := self outerBounds.!

Item was changed:
  ----- Method: LazyListMorph>>drawBoundsForRow: (in category 'list management') -----
  drawBoundsForRow: row
  "calculate the bounds that row should be drawn at.  This might be outside our bounds!!"
  | topLeft drawBounds |
+ topLeft := self topLeft x @ (self topLeft y + ((row - 1) * self rowHeight)).
+ drawBounds := topLeft extent: self width @ self rowHeight.
- topLeft := self topLeft x @ (self topLeft y + ((row - 1) * (font height))).
- drawBounds := topLeft extent: self width @ font height.
  ^drawBounds!

Item was changed:
  ----- Method: LazyListMorph>>drawOn: (in category 'drawing') -----
  drawOn: aCanvas
 
  | topRow bottomRow |
+ self getListSize = 0 ifTrue: [ ^self ].
- listItems ifEmpty: [ ^self ].
 
  self drawPreSelectionOn: aCanvas.
 
  topRow := self topVisibleRowForCanvas: aCanvas.
  bottomRow := self bottomVisibleRowForCanvas: aCanvas.
 
  "Draw multi-selection."
+ self listSource hasMultiSelection ifTrue: [
- listSource hasMultiSelection ifTrue: [
  topRow to: bottomRow do: [ :row |
+ (self listSource itemSelectedAmongMultiple: row) ifTrue: [
- (listSource itemSelectedAmongMultiple: row) ifTrue: [
  self drawBackgroundForMulti: row on: aCanvas ] ] ].
  self drawSelectionOn: aCanvas.
 
  "Draw hovered row if preference enabled."
  PluggableListMorph highlightHoveredRow ifTrue: [
+ self listSource hoverRow > 0 ifTrue: [
- listSource hoverRow > 0 ifTrue: [
  self highlightHoverRow: listSource hoverRow on: aCanvas ] ].
 
  "Draw all visible rows."
  topRow to: bottomRow do: [ :row |
  self display: (self item: row) atRow: row on: aCanvas ].
 
  "Finally, highlight drop row for drag/drop operations.."
+ self listSource potentialDropRow > 0 ifTrue: [
+ self highlightPotentialDropRow: self listSource potentialDropRow on: aCanvas ].!
- listSource potentialDropRow > 0 ifTrue: [
- self highlightPotentialDropRow: listSource potentialDropRow on: aCanvas ].!

Item was changed:
+ ----- Method: LazyListMorph>>filterOffsets: (in category 'list access - cached') -----
- ----- Method: LazyListMorph>>filterOffsets: (in category 'list access') -----
  filterOffsets: row
+ "Do inst-var access on listFilterOffsets here to initialize it as late as possible."
- "Get the character offsets for the matching filter term."
 
+ listFilterOffsets ifNil: [listFilterOffsets := Array new: self getListSize].
+
+ ^ (listFilterOffsets at: row) ifNil: [
+ | offsets |
+ offsets := self getFilterOffsets: row.
+ listFilterOffsets at: row put: offsets.
+ offsets]!
- | indexes |
- "Migrate old instances if necessary."
- listFilterOffsets ifNil: [listFilterOffsets := Array new: listItems size].
-
- row <= listFilterOffsets size ifFalse: [
- ^ self getFilterOffsets: row].
-
- (indexes := listFilterOffsets at: row) ifNil: [
- indexes := self getFilterOffsets: row.
- listFilterOffsets at: row put: indexes ].
-
- ^ indexes!

Item was changed:
+ ----- Method: LazyListMorph>>font (in category 'accessing') -----
- ----- Method: LazyListMorph>>font (in category 'drawing') -----
  font
  "return the font used for drawing.  The response is never nil"
  ^font!

Item was changed:
+ ----- Method: LazyListMorph>>font: (in category 'accessing') -----
- ----- Method: LazyListMorph>>font: (in category 'drawing') -----
  font: newFont
+
  font := (newFont ifNil: [ TextStyle default defaultFont ]).
+ listFilterOffsets := nil.
+
+ self layoutChanged.
- self adjustHeight.
- listFilterOffsets := Array new: self getListSize.
  self changed.!

Item was changed:
  ----- Method: LazyListMorph>>getFilterOffsets: (in category 'list access') -----
  getFilterOffsets: row
  "Calculate matching character indexes for the current filter term."
 
  | item filter offsets currentIndex sub emphasized |
+ filter := self listSource filterTerm.
- filter := listSource filterTerm.
  filter ifEmpty: [^ Array empty].
 
  item := (self item: row) asStringOrText.
 
  emphasized := item isText
  ifTrue: [font emphasized: (item emphasisAt: 1)]
  ifFalse: [font].
 
  item := item asString.
 
  offsets := OrderedCollection new.
 
  currentIndex := 1.
  [currentIndex > 0] whileTrue: [
  currentIndex := item findString: filter startingAt: currentIndex caseSensitive: false.
  currentIndex > 0 ifTrue: [ | left width |
  left := emphasized widthOfString: item from: 1 to: currentIndex-1.
  sub := item copyFrom: currentIndex to: currentIndex + filter size - 1.
  width := emphasized widthOfString: sub.
  offsets addLast: {(left to: left + width). sub}.
  currentIndex := currentIndex + 1] ].
  ^ offsets!

Item was changed:
  ----- Method: LazyListMorph>>getListIcon: (in category 'list access') -----
  getListIcon: row
- "Grab icon directly from the model."
 
+ ^ listSource iconAt: row column: self columnIndex
- ^ listSource iconAt: row
  !

Item was changed:
  ----- Method: LazyListMorph>>getListItem: (in category 'list access') -----
  getListItem: index
+
+ ^ listSource itemAt: index column: self columnIndex!
- "grab a list item directly from the model"
- ^listSource getListItem: index!

Item was changed:
  ----- Method: LazyListMorph>>getListSize (in category 'list access') -----
  getListSize
  "return the number of items in the list"
  listSource ifNil: [ ^0 ].
+ ^listSource listSize!
- ^listSource getListSize!

Item was removed:
- ----- Method: LazyListMorph>>hMargin (in category 'accessing') -----
- hMargin
-
- ^ 3!

Item was removed:
- ----- Method: LazyListMorph>>hUnadjustedScrollRange (in category 'scroll range') -----
- hUnadjustedScrollRange
- "Ok, this is a bit messed up. We need to return the width of the widest item in the list. If we grab every item in the list, it defeats the purpose of LazyListMorph. If we don't, then we don't know the size.
-
- This is a compromise -- find the widest of the first 30 items, then double it, This width will be updated as new items are installed, so it will always be correct for the visible items. If you know a better way, please chime in."
-
- | itemsToCheck item index |
- "Check for a cached value"
- maxWidth ifNotNil:[^maxWidth].
-
- "Compute from scratch"
- itemsToCheck := 30 min: (listItems size).
- maxWidth := 0.
-
- "Check the first few items to get a representative sample of the rest of the list."
- index := 1.
- [index < itemsToCheck] whileTrue:
- [ item := self getListItem: index. "Be careful not to actually install this item"
- maxWidth := maxWidth max: (self widthToDisplayItem: item).
- index:= index + 1.
- ].
-
- "Add some initial fudge if we didn't check all the items."
- (itemsToCheck < listItems size) ifTrue:[maxWidth := maxWidth*2].
-
- ^maxWidth
- !

Item was changed:
+ ----- Method: LazyListMorph>>icon: (in category 'list access - cached') -----
- ----- Method: LazyListMorph>>icon: (in category 'list access') -----
  icon: row
+ "Do inst-var access on listIcons here to initialize it as late as possible."
 
+ self listSource canHaveIcons ifFalse: [^ nil].
- | icon |
- listSource canHaveIcons ifFalse: [^ nil].
 
+ listIcons ifNil: [listIcons := Array new: self getListSize].
- listIcons ifNil: [listIcons := Array new: listItems size].
 
+ ^ (listIcons at: row) ifNil: [
+ | icon |
- row <= listIcons size ifFalse: [
- ^ self getListIcon: row].
-
- (icon := listIcons at: row) ifNil: [
  icon := (self getListIcon: row) ifNotNil: [:form | form scaleIconToDisplay].
+ listIcons at: row put: icon.
+ icon]!
- listIcons at: row put: icon ].
-
- ^ icon!

Item was changed:
  ----- Method: LazyListMorph>>initialize (in category 'initialization') -----
  initialize
+
  super initialize.
+
  self color: Color black.
+ self cellInset: 3@0.
+
  font := Preferences standardListFont.
+
+ listItems := nil.
+ listIcons := nil.
+ listFilterOffsets := nil.
+
- listItems := #().
- listIcons := #().
- listFilterOffsets := #().
  selectedRow := nil.
  selectedRows := PluggableSet integerSet.
+ preSelectedRow := nil.!
- preSelectedRow := nil.
- self adjustHeight.!

Item was changed:
+ ----- Method: LazyListMorph>>item: (in category 'list access - cached') -----
- ----- Method: LazyListMorph>>item: (in category 'list access') -----
  item: index
+ "Do inst-var access on listItems here to initialize it as late as possible."
+
+ listItems ifNil: [listItems := Array new: self getListSize].
+
+ ^ (listItems at: index) ifNil: [
+ | newItem itemWidth |
- "return the index-th item, using the 'listItems' cache"
- | newItem itemWidth |
- (index between: 1 and: listItems size)
- ifFalse: [ "there should have been an update, but there wasn't!!"  ^self getListItem: index].
- (listItems at: index) ifNil: [
  newItem := self getListItem: index.
+
  "Update the width cache."
+ maxWidth ifNotNil: [
- maxWidth ifNotNil:[
  itemWidth := self widthToDisplayItem: newItem.
+ itemWidth > maxWidth ifTrue: [
- itemWidth > maxWidth ifTrue:[
  maxWidth := itemWidth.
+ super layoutChanged]].
+
+ listItems at: index put: newItem.
+ newItem].!
- self adjustWidth.
- ]].
- listItems at: index put: newItem ].
- ^listItems at: index!

Item was added:
+ ----- Method: LazyListMorph>>layoutChanged (in category 'layout') -----
+ layoutChanged
+ "See #item:. We have to invalidate listItems or maxWidth will not be updated if you switch hResizing to #shrinkWrap."
+
+ listItems := nil.
+ maxWidth := nil.
+
+ super layoutChanged.!

Item was changed:
+ ----- Method: LazyListMorph>>listChanged (in category 'layout') -----
- ----- Method: LazyListMorph>>listChanged (in category 'list management') -----
  listChanged
  "set newList to be the list of strings to display"
+
+ listItems := nil.
+ listIcons := nil.
+ listFilterOffsets := nil.
+
- | size |
- size := self getListSize.
- listItems := Array new: size.
- listIcons := Array new: size.
- listFilterOffsets := Array new: size.
  maxWidth := nil.
+
  selectedRow := nil.
  selectedRows := PluggableSet integerSet.
  preSelectedRow := nil.
+
+ self layoutChanged.
- self adjustHeight.
- self adjustWidth.
  self changed.
  !

Item was added:
+ ----- Method: LazyListMorph>>listSource (in category 'accessing') -----
+ listSource
+ ^ listSource!

Item was changed:
+ ----- Method: LazyListMorph>>listSource: (in category 'accessing') -----
- ----- Method: LazyListMorph>>listSource: (in category 'initialization') -----
  listSource: aListSource
  "set the source of list items -- typically a PluggableListMorph"
+ listSource := aListSource.!
- listSource := aListSource.
- self listChanged!

Item was added:
+ ----- Method: LazyListMorph>>maxHeight (in category 'layout') -----
+ maxHeight
+
+ ^ (self getListSize max: 1) * (font height + self cellInset top + self cellInset bottom)!

Item was added:
+ ----- Method: LazyListMorph>>maxWidth (in category 'layout') -----
+ maxWidth
+ "Approximate the maximum width of this lazy list. Take first n items as a sample."
+
+ | threshold listSize |
+ maxWidth ifNotNil:[^maxWidth].
+
+ threshold := 30.
+ listSize := self getListSize.
+
+ maxWidth := 0.
+ 1 to: (threshold min: listSize) do: [:index |
+ maxWidth := maxWidth max: (self widthToDisplayItem: (self getListItem: index))].
+
+ ^ maxWidth
+ !

Item was removed:
- ----- Method: LazyListMorph>>resetFilterOffsets (in category 'list access') -----
- resetFilterOffsets
-
- listFilterOffsets := nil.!

Item was changed:
  ----- Method: LazyListMorph>>rowAtLocation: (in category 'list management') -----
  rowAtLocation: aPoint
  "return the number of the row at aPoint"
  | y |
  y := aPoint y.
+ y < self top ifTrue: [ ^ 1 min: self getListSize ].
+ ^((y - self top // self rowHeight) + 1) min: self getListSize max: 0!
- y < self top ifTrue: [ ^ 1 min: listItems size ].
- ^((y - self top // (font height)) + 1) min: listItems size max: 0!

Item was added:
+ ----- Method: LazyListMorph>>rowChanged:with: (in category 'updating') -----
+ rowChanged: oneRow with: anotherRow
+ "Speed up drawing. Merge consecutive rows because the default damage recorder might not merge these rectangles."
+
+ oneRow ifNil: [anotherRow ifNil: [^ self] ifNotNil: [^ self rowChanged: anotherRow]].
+ anotherRow ifNil: [^ self rowChanged: oneRow].
+
+ (oneRow - anotherRow) abs = 1
+ ifTrue: [
+ self invalidRect: ((self drawBoundsForRow: oneRow)
+ quickMerge: (self drawBoundsForRow: anotherRow))]
+ ifFalse: [
+ self invalidRect: (self drawBoundsForRow: oneRow).
+ self invalidRect: (self drawBoundsForRow: anotherRow)].!

Item was added:
+ ----- Method: LazyListMorph>>rowHeight (in category 'layout') -----
+ rowHeight
+
+ ^ font height + self cellInset top + self cellInset bottom!

Item was changed:
  ----- Method: LazyListMorph>>selectedRow: (in category 'list management') -----
  selectedRow: index
  " Select the index-th row. Clear the pre selection highlight. If nil, remove the current selection. "
+
+ self rowChanged: selectedRow with: index.
  selectedRow := index.
+ preSelectedRow := nil.!
- preSelectedRow := nil.
- self changed.!

Item was added:
+ ----- Method: LazyListMorph>>selection (in category 'list management') -----
+ selection
+
+ ^ self selectedRow ifNotNil: [:row |
+ (row between: 1 and: self getListSize)
+ ifTrue: [self item: row]
+ ifFalse: [nil]]!

Item was added:
+ ----- Method: LazyListMorph>>submorphBoundsForShrinkWrap (in category 'layout') -----
+ submorphBoundsForShrinkWrap
+ "Since we have no submorphs, we calculate those bounds here. Skip width calculation if we do not #shrinkWrap."
+
+ ^ self hResizing == #shrinkWrap
+ ifTrue: [(self topLeft extent: self maxWidth @ self maxHeight) insetBy: self cellInset]
+ ifFalse: [self topLeft extent: 0 @ (self maxHeight - self cellInset top - self cellInset bottom)]!

Item was changed:
+ ----- Method: LazyListMorph>>widthToDisplayItem: (in category 'layout') -----
- ----- Method: LazyListMorph>>widthToDisplayItem: (in category 'scroll range') -----
  widthToDisplayItem: item
+
+ | labelWidth iconWidth leftMargin rightMargin |
+ labelWidth := self font widthOfStringOrText: item asStringOrText.
+ iconWidth := self listSource canHaveIcons
+ ifTrue: [(16 * RealEstateAgent scaleFactor) truncated]
+ ifFalse: [0].
+ leftMargin := self cellInset left.
+ rightMargin := self cellInset right.
+ ^ leftMargin + iconWidth + labelWidth + rightMargin!
- ^ self font widthOfStringOrText: item asStringOrText!

Item was changed:
  ----- Method: MulticolumnLazyListMorph>>getListItem: (in category 'list access') -----
  getListItem: index
+ ^listSource getListItem: index!
- ^listSource getListRow: index!

Item was changed:
  ScrollPane subclass: #PluggableListMorph
+ instanceVariableNames: 'list fullList modelToView viewToModel getListSelector getListSizeSelector getListElementSelector getIndexSelector setIndexSelector keystrokeActionSelector autoDeselect lastKeystrokeTime lastKeystrokes lastClickTime doubleClickSelector handlesBasicKeys potentialDropRow hoverRow listMorph keystrokePreviewSelector priorSelection getIconSelector getHelpSelector'
+ classVariableNames: 'ClearFilterAutomatically ClearFilterDelay FilterableLists FlashOnErrors HighlightHoveredRow HighlightPreSelection MenuRequestUpdatesSelection'
- instanceVariableNames: 'list fullList getListSelector getListSizeSelector getListElementSelector getIndexSelector setIndexSelector keystrokeActionSelector autoDeselect lastKeystrokeTime lastKeystrokes lastClickTime doubleClickSelector handlesBasicKeys potentialDropRow hoverRow listMorph hScrollRangeCache keystrokePreviewSelector priorSelection getIconSelector getHelpSelector'
- classVariableNames: 'ClearFilterAutomatically FilterableLists HighlightHoveredRow MenuRequestUpdatesSelection'
  poolDictionaries: ''
  category: 'Morphic-Pluggable Widgets'!
 
+ !PluggableListMorph commentStamp: 'mt 10/12/2019 11:04' prior: 0!
+ I am a list widget that uses the change/update mechanism to display model data as a vertical arrangement of (maybe formatted) strings and icons in a scroll pane.
- !PluggableListMorph commentStamp: 'cmm 8/21/2011 23:37' prior: 0!
- When a PluggableListMorph is in focus, type in a letter (or several letters quickly) to go to the next item that begins with that letter (if FilterableLists is false).
 
+ When I am in keyboard focus, type in a letter (or several letters quickly) to go to the next item that begins with that letter. If you enabled the "filterable lists" preference, I will hide all items that do not match the filter.
+
+ Special keys (arrow up/down, page up/down, home, end) are also supported.!
- Special keys (up, down, home, etc.) are also supported.!

Item was added:
+ ----- Method: PluggableListMorph class>>clearFilterDelay (in category 'preferences') -----
+ clearFilterDelay
+ <preference: 'Filterable Lists Clear Delay'
+ category: 'scrolling'
+ description: 'Defines the maximum delay (in milliseconds) between keystrokes before the filter expression is cleared again.'
+ type: #Number>
+
+ ^ ClearFilterDelay ifNil: [500]!

Item was added:
+ ----- Method: PluggableListMorph class>>clearFilterDelay: (in category 'preferences') -----
+ clearFilterDelay: aNumber
+
+ ClearFilterDelay := aNumber.!

Item was added:
+ ----- Method: PluggableListMorph class>>flashOnErrors (in category 'preferences') -----
+ flashOnErrors
+
+ <preference: 'Flash on Errors in Lists'
+ category: #Morphic
+ description: 'If a user request cannot be fulfilled as expected, flash briefly to inform the user about it. While this can improve user feedback, it adds a small delay because of an explicit world refresh.'
+ type: #Boolean>
+
+ ^ FlashOnErrors ifNil: [false]!

Item was added:
+ ----- Method: PluggableListMorph class>>flashOnErrors: (in category 'preferences') -----
+ flashOnErrors: aBoolean
+
+ FlashOnErrors := aBoolean.!

Item was added:
+ ----- Method: PluggableListMorph class>>highlightPreSelection (in category 'preferences') -----
+ highlightPreSelection
+
+ <preference: 'Highlight Pre-Selection in Lists'
+ category: #Morphic
+ description: 'Indicate the attempt to change the selection in models like after a click or key press. If model updates can take long, such indication can improve user feedback. However, this adds a small delay because of an explicit world refresh.'
+ type: #Boolean>
+
+ ^ HighlightPreSelection ifNil: [false]!

Item was added:
+ ----- Method: PluggableListMorph class>>highlightPreSelection: (in category 'preferences') -----
+ highlightPreSelection: aBoolean
+
+ HighlightPreSelection := aBoolean.!

Item was changed:
  ----- Method: PluggableListMorph>>applyUserInterfaceTheme (in category 'updating') -----
  applyUserInterfaceTheme
 
+ super applyUserInterfaceTheme.
+ self listMorph listChanged.!
- super applyUserInterfaceTheme.!

Item was changed:
+ ----- Method: PluggableListMorph>>basicKeyPressed: (in category 'model access - keystroke') -----
+ basicKeyPressed: aChar
+
+ self listFilterAppend: aChar.!
- ----- Method: PluggableListMorph>>basicKeyPressed: (in category 'model access') -----
- basicKeyPressed: aChar
- | milliseconds slowKeyStroke listSize newSelectionIndex oldSelectionIndex startIndex |
- oldSelectionIndex := newSelectionIndex := self getCurrentSelectionIndex.
- listSize := self getListSize.
- listSize = 0 ifTrue: [ ^self flash ].
- milliseconds := Time millisecondClockValue.
- slowKeyStroke := (Time
- milliseconds: milliseconds
- since: lastKeystrokeTime) > (self filterableList ifTrue: [500] ifFalse: [ 300 ]).
- lastKeystrokeTime := milliseconds.
- slowKeyStroke
- ifTrue:
- [ self filterableList ifTrue: [ self hasFilter ifFalse: [ priorSelection := self modelIndexFor: self selectionIndex] ].
- "forget previous keystrokes and search in following elements"
- lastKeystrokes := aChar asLowercase asString.
- newSelectionIndex := newSelectionIndex \\ listSize + 1.
- self filterableList ifTrue: [ list := self getFullList ] ]
- ifFalse: [ "append quick keystrokes but don't move selection if it still matches"
- lastKeystrokes := lastKeystrokes , aChar asLowercase asString.
- newSelectionIndex := newSelectionIndex max: 1 ].
- "No change if model is locked"
- model okToChange ifFalse: [ ^ self ].
- self filterableList
- ifTrue:
- [ self
- filterList ;
- updateList.
- newSelectionIndex := self modelIndexFor: 1 ]
- ifFalse:
- [ startIndex := newSelectionIndex.
- listSize := self getListSize.
- [ (self getListItem: newSelectionIndex) asString withBlanksTrimmed asLowercase beginsWith: lastKeystrokes ] whileFalse:
- [ (newSelectionIndex := newSelectionIndex \\ listSize + 1) = startIndex ifTrue: [ ^ self flash"Not in list." ] ].
- newSelectionIndex = oldSelectionIndex ifTrue: [ ^ self flash ] ].
- (self hasFilter and: [(self getCurrentSelectionIndex = newSelectionIndex) not]) ifTrue:
- [self changeModelSelection: newSelectionIndex]!

Item was changed:
+ ----- Method: PluggableListMorph>>bottomVisibleRowIndex (in category 'accessing - items') -----
- ----- Method: PluggableListMorph>>bottomVisibleRowIndex (in category 'accessing') -----
  bottomVisibleRowIndex
  ^ self rowAtLocation: self bottomLeft+(3@3 negated)!

Item was changed:
+ ----- Method: PluggableListMorph>>canHaveIcons (in category 'list morph callbacks') -----
- ----- Method: PluggableListMorph>>canHaveIcons (in category 'testing') -----
  canHaveIcons
 
  ^ getIconSelector notNil!

Item was changed:
  ----- Method: PluggableListMorph>>changeModelSelection: (in category 'model access') -----
+ changeModelSelection: aModelIndex
+ " Change the model's selected item index to be aModelIndex. Optionally, do a pre-selection highlight, which requires an immediate re-draw of this widget."
- changeModelSelection: anInteger
- " Change the model's selected item index to be anInteger. Enable the pre selection highlight. Step the World forward to let the pre selection highlight take effect. "
 
+ self class highlightPreSelection ifTrue: [
+ self rowAboutToBecomeSelected: (self viewIndexFor: aModelIndex).
+ self refreshWorld].
- self rowAboutToBecomeSelected: (self uiIndexFor: anInteger).
- self refreshWorld.
  setIndexSelector ifNotNil: [
+ model perform: setIndexSelector with: aModelIndex ].!
- model perform: setIndexSelector with: anInteger ].!

Item was changed:
  ----- Method: PluggableListMorph>>copyListToClipboard (in category 'menus') -----
  copyListToClipboard
  "Copy my items to the clipboard as a multi-line string"
 
+ Clipboard clipboardText: (
+ String streamContents: [:stream |
+ 1 to: self listSize do: [:viewIndex |
+ stream nextPutAll: (self itemAt: viewIndex) asString; cr]]).!
- | stream |
- stream := WriteStream on: (String new: self getList size * 40).
- list do: [:ea | stream nextPutAll: ea asString] separatedBy: [stream nextPut: Character cr].
- Clipboard clipboardText: stream contents!

Item was added:
+ ----- Method: PluggableListMorph>>createListMorph (in category 'initialization') -----
+ createListMorph
+
+ ^ self listMorphClass new
+ listSource: self;
+ hResizing: #spaceFill;
+ vResizing: #shrinkWrap;
+ cellPositioning: #leftCenter;
+ setProperty: #indicateKeyboardFocus toValue: #never;
+ yourself.!

Item was removed:
- ----- Method: PluggableListMorph>>deriveHScrollRange (in category 'scroll cache') -----
- deriveHScrollRange
-
- |  unadjustedRange totalRange |
- (list isNil or: [list isEmpty])
- ifTrue:[hScrollRangeCache := Array with: 0 with: 0 with: 0 with: 0 with: 0 ]
- ifFalse:[
- unadjustedRange := self listMorph hUnadjustedScrollRange.
- totalRange := unadjustedRange + self hExtraScrollRange + self hMargin.
- hScrollRangeCache := Array
- with: totalRange
- with: unadjustedRange
- with: list size
- with: list first
- with: list last .
- ].
- !

Item was changed:
+ ----- Method: PluggableListMorph>>doubleClick: (in category 'event handling') -----
- ----- Method: PluggableListMorph>>doubleClick: (in category 'events') -----
  doubleClick: event
  | index |
  doubleClickSelector ifNil: [^super doubleClick: event].
  index := self rowAtLocation: event position.
  index = 0 ifTrue: [^super doubleClick: event].
  "selectedMorph ifNil: [self setSelectedMorph: aMorph]."
  ^ self model perform: doubleClickSelector!

Item was removed:
- ----- Method: PluggableListMorph>>doubleClick:onItem: (in category 'obsolete') -----
- doubleClick: event onItem: aMorph
- self removeObsoleteEventHandlers.!

Item was added:
+ ----- Method: PluggableListMorph>>doubleClickSelector (in category 'accessing - selectors') -----
+ doubleClickSelector
+ ^ doubleClickSelector!

Item was changed:
+ ----- Method: PluggableListMorph>>doubleClickSelector: (in category 'accessing - selectors') -----
- ----- Method: PluggableListMorph>>doubleClickSelector: (in category 'initialization') -----
  doubleClickSelector: aSymbol
  doubleClickSelector := aSymbol!

Item was removed:
- ----- Method: PluggableListMorph>>extent: (in category 'geometry') -----
- extent: newExtent
- super extent: newExtent.
-
- "Change listMorph's bounds to the new width. It is either the size
- of the widest list item, or the size of self, whatever is bigger"
- self listMorph width: (self width max: listMorph hUnadjustedScrollRange).
- !

Item was removed:
- ----- Method: PluggableListMorph>>filterList (in category 'filtering') -----
- filterList
- self hasFilter
- ifTrue:
- [ | frontMatching substringMatching newList |
- self indicateFiltered.
- frontMatching := OrderedCollection new.
- substringMatching := OrderedCollection new.
- list withIndexDo:
- [ : each : n | | foundPos |
- foundPos := each asString
- findString: lastKeystrokes
- startingAt: 1
- caseSensitive: false.
- foundPos = 1
- ifTrue: [ frontMatching add: each ]
- ifFalse:
- [ foundPos = 0 ifFalse: [ substringMatching add: each ] ] ].
- newList := frontMatching , substringMatching.
- (newList isEmpty not or: [ self allowEmptyFilterResult ])
- ifTrue: [ list := newList ]
- ifFalse:
- [ lastKeystrokes := lastKeystrokes allButLast: 1.
- self
- flash ;
- filterList ] ]
- ifFalse: [ self indicateUnfiltered ]!

Item was removed:
- ----- Method: PluggableListMorph>>filterList: (in category 'filtering') -----
- filterList: aString
- "Manually set the list filter."
-
- lastKeystrokes := aString.
- self filterList.
- self updateList.
- self changeModelSelection: (list ifEmpty: [0] ifNotEmpty: [self modelIndexFor: 1]).!

Item was added:
+ ----- Method: PluggableListMorph>>filterList:matching: (in category 'filtering') -----
+ filterList: someItems matching: aPattern
+ "Filter someStrings according to aPattern. Prepend best matches in the result. Update the model-to-view map."
+
+ | frontMatching substringMatching tmp |
+ aPattern ifEmpty: [ ^ someItems ].
+ someItems ifEmpty: [ ^ someItems ].
+
+ frontMatching := OrderedCollection new.
+ substringMatching := OrderedCollection new.
+
+ modelToView := Dictionary new.
+ viewToModel := Dictionary new.
+ tmp := OrderedCollection new.
+
+ someItems doWithIndex:
+ [ :each :n | | foundPos |
+ foundPos := self filterListItem: each matching: aPattern.
+ foundPos = 1
+ ifTrue:
+ [ frontMatching add: each.
+ modelToView at: n put: frontMatching size.
+ viewToModel at: frontMatching size put: n ]
+ ifFalse:
+ [ foundPos > 1 ifTrue:
+ [ substringMatching add: each.
+ tmp add: n; add:  substringMatching size ] ] ].
+
+ tmp pairsDo: [:modelIndex :viewIndex |
+ modelToView at: modelIndex put: viewIndex + frontMatching size.
+ viewToModel at: viewIndex + frontMatching size put: modelIndex].
+
+ ^ frontMatching, substringMatching!

Item was added:
+ ----- Method: PluggableListMorph>>filterListItem:matching: (in category 'filtering') -----
+ filterListItem: anObject matching: aPattern
+
+ ^ anObject asString
+ findString: aPattern
+ startingAt: 1
+ caseSensitive: false!

Item was added:
+ ----- Method: PluggableListMorph>>filterTerm: (in category 'filtering') -----
+ filterTerm: aString
+
+ lastKeystrokes = aString ifTrue: [^ self].
+ lastKeystrokes := aString.
+ self updateListFilter.!

Item was added:
+ ----- Method: PluggableListMorph>>flash (in category 'drawing') -----
+ flash
+
+ self class flashOnErrors ifTrue: [super flash].!

Item was added:
+ ----- Method: PluggableListMorph>>fullListSize (in category 'list morph callbacks') -----
+ fullListSize
+ "Number of items in the unfiltered list."
+
+ ^ self getFullList size!

Item was added:
+ ----- Method: PluggableListMorph>>getFilteredList (in category 'model access - cached') -----
+ getFilteredList
+ "Apply the current filter to the list. Maybe shorten the filter term if there are no matches."
+
+ | fullList filteredList |
+ fullList := self getFullList.
+
+ self hasFilter ifFalse: [^ fullList].
+ fullList ifEmpty: [^ fullList].
+
+ filteredList := self filterList: fullList matching: lastKeystrokes.
+
+ (filteredList isEmpty not or: [ self allowEmptyFilterResult ])
+ ifFalse:
+ [ "Remove the last character and try filtering again."
+ lastKeystrokes := lastKeystrokes allButLast: 1.
+ ^ self
+ flash;
+ getFilteredList ].
+
+ ^ filteredList!

Item was changed:
+ ----- Method: PluggableListMorph>>getFullList (in category 'model access - cached') -----
- ----- Method: PluggableListMorph>>getFullList (in category 'model access') -----
  getFullList
+ "The full, unfiltered list. Cached result, see #updateList. Prefer getListSelector over getListElementSelector if both a set."
+
+ ^ fullList ifNil: [
+ fullList := getListSelector
+ ifNotNil: [:sel | model perform: sel]
+ ifNil: [(getListSizeSelector notNil and: [getListElementSelector notNil])
+ ifTrue: [ (1 to: self getListSize) collect: [:index | self getListItem: index]]
+ ifFalse: [#() "We cannot get the full list."]]]!
- "The full, unfiltered list."
- ^ fullList ifNil: [fullList := model perform: getListSelector]!

Item was changed:
+ ----- Method: PluggableListMorph>>getHelpSelector (in category 'accessing - selectors') -----
- ----- Method: PluggableListMorph>>getHelpSelector (in category 'accessing') -----
  getHelpSelector
 
  ^ getHelpSelector!

Item was changed:
+ ----- Method: PluggableListMorph>>getHelpSelector: (in category 'accessing - selectors') -----
- ----- Method: PluggableListMorph>>getHelpSelector: (in category 'accessing') -----
  getHelpSelector: aSelector
  "Get help for list entries."
 
  getHelpSelector := aSelector.!

Item was added:
+ ----- Method: PluggableListMorph>>getIconSelector (in category 'accessing - selectors') -----
+ getIconSelector
+ ^ getIconSelector!

Item was changed:
+ ----- Method: PluggableListMorph>>getIconSelector: (in category 'accessing - selectors') -----
- ----- Method: PluggableListMorph>>getIconSelector: (in category 'initialization') -----
  getIconSelector: aSymbol
  getIconSelector := aSymbol!

Item was changed:
+ ----- Method: PluggableListMorph>>getList (in category 'model access - cached') -----
- ----- Method: PluggableListMorph>>getList (in category 'model access') -----
  getList
+ "Answer the (maybe filtered) list to be displayed. Cached result, see #updateList."
+
+ ^ list ifNil: [
+ list := self filterableList
+ ifTrue: [self getFilteredList]
+ ifFalse: [self getFullList].
+ self updateListMorph.
+ list]!
- "Answer the list to be displayed.  Caches the returned list in the 'list' ivar"
- getListSelector == nil ifTrue: [ ^ Array empty ].
- list := self getFullList.
- self filterableList ifTrue: [ self filterList ].
- ^ list ifNil: [ Array empty ]!

Item was added:
+ ----- Method: PluggableListMorph>>getListElementSelector (in category 'accessing - selectors') -----
+ getListElementSelector
+ "specify a selector that can be used to obtain a single element in the underlying list"
+ ^ getListElementSelector!

Item was changed:
+ ----- Method: PluggableListMorph>>getListElementSelector: (in category 'accessing - selectors') -----
- ----- Method: PluggableListMorph>>getListElementSelector: (in category 'initialization') -----
  getListElementSelector: aSymbol
  "specify a selector that can be used to obtain a single element in the underlying list"
+
+ getListElementSelector = aSymbol ifTrue: [^self].
- getListElementSelector == aSymbol ifTrue:[^self].
  getListElementSelector := aSymbol.
+ self updateList.!
- list := nil.  "this cache will not be updated if getListElementSelector has been specified, so go ahead and remove it"!

Item was changed:
  ----- Method: PluggableListMorph>>getListItem: (in category 'model access') -----
+ getListItem: modelIndex
+
+ ^ getListElementSelector
+ ifNotNil: [:sel | model perform: sel with: modelIndex ]
+ ifNil: [self getFullList at: modelIndex]!
- getListItem: index
- "get the index-th item in the displayed list"
- getListElementSelector ifNotNil: [ ^(model perform: getListElementSelector with: index) asStringOrText ].
- list ifNotNil: [ ^list at: index ].
- ^self getList at: index!

Item was changed:
+ ----- Method: PluggableListMorph>>getListSelector (in category 'accessing - selectors') -----
- ----- Method: PluggableListMorph>>getListSelector (in category 'selection') -----
  getListSelector
  ^ getListSelector!

Item was changed:
+ ----- Method: PluggableListMorph>>getListSelector: (in category 'accessing - selectors') -----
+ getListSelector: aSymbol
- ----- Method: PluggableListMorph>>getListSelector: (in category 'initialization') -----
- getListSelector: sel
  "Set the receiver's getListSelector as indicated, and trigger a recomputation of the list"
+
+ getListSelector = aSymbol ifTrue: [^ self].
+ getListSelector := aSymbol.
+ self updateList.!
- self
- setGetListSelector: sel ;
- changed ;
- updateList!

Item was changed:
  ----- Method: PluggableListMorph>>getListSize (in category 'model access') -----
  getListSize
  "return the current number of items in the displayed list"
+
+ ^ getListSizeSelector
+ ifNotNil: [:sel | model perform: sel]
+ ifNil: [self fullListSize]!
- getListSizeSelector ifNotNil: [ ^model perform: getListSizeSelector ].
- ^self getList size!

Item was added:
+ ----- Method: PluggableListMorph>>getListSizeSelector (in category 'accessing - selectors') -----
+ getListSizeSelector
+ ^ getListSizeSelector!

Item was changed:
+ ----- Method: PluggableListMorph>>getListSizeSelector: (in category 'accessing - selectors') -----
- ----- Method: PluggableListMorph>>getListSizeSelector: (in category 'initialization') -----
  getListSizeSelector: aSymbol
  "specify a selector that can be used to specify the list's size"
+
+ getListSizeSelector = aSymbol ifTrue: [^ self].
+ getListSizeSelector := aSymbol.
+ self updateList.!
- getListSizeSelector := aSymbol!

Item was added:
+ ----- Method: PluggableListMorph>>hScrollBarPolicy: (in category 'accessing') -----
+ hScrollBarPolicy: aSymbol
+ "The lazy list morph will never wrap its rows if they do not fit. Instead, they are just clipped. So, #spaceFill is fine if the horizontal scroll bar should never be visible."
+
+ self checkScrollBarPolicy: aSymbol.
+
+ aSymbol ~= #never
+ ifTrue: [self listMorph hResizing: #shrinkWrap]
+ ifFalse: [self listMorph hResizing: #spaceFill].
+
+ ^ super hScrollBarPolicy: aSymbol!

Item was removed:
- ----- Method: PluggableListMorph>>hTotalScrollRange (in category 'scroll cache') -----
- hTotalScrollRange
- "Return the entire scrolling range."
-
- self resetHScrollRangeIfNecessary.
-
- ^hScrollRangeCache first
- !

Item was removed:
- ----- Method: PluggableListMorph>>hUnadjustedScrollRange (in category 'scroll cache') -----
- hUnadjustedScrollRange
- "Override because our lazy list approximates the width for performance reasons."
-
- self resetHScrollRangeIfNecessary.
-
- ^hScrollRangeCache second
- !

Item was removed:
- ----- Method: PluggableListMorph>>handleBasicKeys: (in category 'events') -----
- handleBasicKeys: aBoolean
- "set whether the list morph should handle basic keys like arrow keys, or whether everything should be passed to the model"
- handlesBasicKeys := aBoolean!

Item was changed:
+ ----- Method: PluggableListMorph>>handlesBasicKeys (in category 'accessing') -----
- ----- Method: PluggableListMorph>>handlesBasicKeys (in category 'events') -----
  handlesBasicKeys
  " if ya don't want the list to automatically handle non-modifier key
  (excluding shift key) input, return false"
  ^ handlesBasicKeys ifNil: [ true ]!

Item was added:
+ ----- Method: PluggableListMorph>>handlesBasicKeys: (in category 'accessing') -----
+ handlesBasicKeys: aBoolean
+ "set whether the list morph should handle basic keys like arrow keys, or whether everything should be passed to the model"
+ handlesBasicKeys := aBoolean!

Item was changed:
  ----- Method: PluggableListMorph>>hasFilter (in category 'filtering') -----
  hasFilter
+ ^ self filterTerm notEmpty!
- ^ lastKeystrokes isEmptyOrNil not!

Item was changed:
+ ----- Method: PluggableListMorph>>hasMultiSelection (in category 'list morph callbacks') -----
- ----- Method: PluggableListMorph>>hasMultiSelection (in category 'testing') -----
  hasMultiSelection
 
  ^ false!

Item was changed:
+ ----- Method: PluggableListMorph>>highlightSelector (in category 'accessing - selectors') -----
- ----- Method: PluggableListMorph>>highlightSelector (in category 'accessing') -----
  highlightSelector
  ^self valueOfProperty: #highlightSelector!

Item was changed:
+ ----- Method: PluggableListMorph>>highlightSelector: (in category 'accessing - selectors') -----
- ----- Method: PluggableListMorph>>highlightSelector: (in category 'accessing') -----
  highlightSelector: aSelector
+
+ self highlightSelector = aSelector ifTrue: [^ self].
+ self setProperty: #highlightSelector toValue: aSelector.!
- self setProperty: #highlightSelector toValue: aSelector.
- self updateList!

Item was changed:
+ ----- Method: PluggableListMorph>>hoverItem (in category 'accessing - items') -----
- ----- Method: PluggableListMorph>>hoverItem (in category 'accessing') -----
  hoverItem
 
  ^ self hoverRow = 0
  ifTrue: [nil]
+ ifFalse: [self itemAt: self hoverRow]!
- ifFalse: [self getListItem: self hoverRow]!

Item was changed:
  ----- Method: PluggableListMorph>>hoverRow: (in category 'accessing') -----
+ hoverRow: viewIndex
- hoverRow: anInteger
 
+ hoverRow = viewIndex ifTrue: [^ self].
- hoverRow = anInteger ifTrue: [^ self].
 
+ self listMorph rowChanged: hoverRow with: viewIndex.
+ hoverRow := viewIndex.
- hoverRow ifNotNil: [self listMorph rowChanged: hoverRow].
- hoverRow := anInteger.
- hoverRow ifNotNil: [self listMorph rowChanged: hoverRow].
 
  self wantsBalloon ifTrue: [
  self activeHand
  removePendingBalloonFor: self;
  triggerBalloonFor: self after: self balloonHelpDelayTime].!

Item was changed:
+ ----- Method: PluggableListMorph>>iconAt: (in category 'list morph callbacks') -----
+ iconAt: viewIndex
- ----- Method: PluggableListMorph>>iconAt: (in category 'model access') -----
- iconAt: anInteger
 
+ ^ getIconSelector ifNotNil: [model perform: getIconSelector with: (self modelIndexFor: viewIndex)]!
- | index |
- index := (self hasFilter and: [list notNil])
- ifTrue: [self getFullList indexOf: (list at: anInteger ifAbsent: [^nil])]
- ifFalse: [anInteger].
- ^ getIconSelector ifNotNil: [model perform: getIconSelector with: index]!

Item was added:
+ ----- Method: PluggableListMorph>>iconAt:column: (in category 'list morph callbacks') -----
+ iconAt: viewIndex column: columnIndex
+
+ ^ self iconAt: viewIndex!

Item was changed:
  ----- Method: PluggableListMorph>>initialize (in category 'initialization') -----
  initialize
+
+ listMorph := self createListMorph.
  super initialize.
+
+ self scroller
+ layoutPolicy: TableLayout new;
+ addMorph: listMorph.
 
+ self minimumWidth: (self font widthOf: $m) * 5.
+
+ !
- self minimumWidth: (self font widthOf: $m) * 5.!

Item was added:
+ ----- Method: PluggableListMorph>>itemAt: (in category 'list morph callbacks') -----
+ itemAt: viewIndex
+ "Callback from list morph."
+
+ ^ self getList at: viewIndex!

Item was added:
+ ----- Method: PluggableListMorph>>itemAt:column: (in category 'list morph callbacks') -----
+ itemAt: viewIndex column: columnIndex
+
+ ^ self itemAt: viewIndex!

Item was changed:
+ ----- Method: PluggableListMorph>>itemFromPoint: (in category 'accessing - items') -----
- ----- Method: PluggableListMorph>>itemFromPoint: (in category 'accessing') -----
  itemFromPoint: aPoint
  "Return the list element (morph) at the given point or nil if outside"
  | ptY |
  scroller hasSubmorphs ifFalse:[^nil].
  (scroller fullBounds containsPoint: aPoint) ifFalse:[^nil].
  ptY := (scroller firstSubmorph point: aPoint from: self) y.
  "note: following assumes that submorphs are vertical, non-overlapping, and ordered"
  scroller firstSubmorph top > ptY ifTrue:[^nil].
  scroller lastSubmorph bottom < ptY ifTrue:[^nil].
  "now use binary search"
  ^scroller
  findSubmorphBinary:[:item|
  (item top <= ptY and:[item bottom >= ptY])
  ifTrue:[0] "found"
  ifFalse:[ (item top + item bottom // 2) > ptY ifTrue:[-1] ifFalse:[1]]]!

Item was changed:
+ ----- Method: PluggableListMorph>>itemSelectedAmongMultiple: (in category 'list morph callbacks') -----
+ itemSelectedAmongMultiple: viewIndex
- ----- Method: PluggableListMorph>>itemSelectedAmongMultiple: (in category 'model access') -----
- itemSelectedAmongMultiple: index
  "return whether the index-th row is selected.  Always false in PluggableListMorph, but sometimes true in PluggableListMorphOfMany"
  ^false!

Item was changed:
  ----- Method: PluggableListMorph>>keyboardFocusChange: (in category 'event handling') -----
  keyboardFocusChange: aBoolean
+ "Clear the hover effect and maybe the current list filter if we lose keyboard focus."
+
+ aBoolean ifFalse:
+ [self hoverRow: nil.
- "The message is sent to a morph when its keyboard focus changes.
- The given argument indicates that the receiver is gaining (versus losing) the keyboard focus.
- In this case, all we need to do is to redraw border feedback"
- aBoolean ifFalse: [
- self hoverRow: nil.
  self clearFilterAutomatically ifTrue:
+ [self removeFilter]].
- [ self hasFilter ifTrue:
- [ self
- removeFilter ;
- updateList ] ] ].
 
  super keyboardFocusChange: aBoolean.!

Item was added:
+ ----- Method: PluggableListMorph>>keystrokeActionSelector (in category 'accessing - selectors') -----
+ keystrokeActionSelector
+ ^ keystrokeActionSelector!

Item was changed:
+ ----- Method: PluggableListMorph>>keystrokeActionSelector: (in category 'accessing - selectors') -----
- ----- Method: PluggableListMorph>>keystrokeActionSelector: (in category 'initialization') -----
  keystrokeActionSelector: keyActionSel
  "Set the keystroke action selector as specified"
 
  keystrokeActionSelector := keyActionSel!

Item was changed:
+ ----- Method: PluggableListMorph>>keystrokePreviewSelector (in category 'accessing - selectors') -----
- ----- Method: PluggableListMorph>>keystrokePreviewSelector (in category 'selection') -----
  keystrokePreviewSelector
  ^ keystrokePreviewSelector!

Item was changed:
+ ----- Method: PluggableListMorph>>keystrokePreviewSelector: (in category 'accessing - selectors') -----
- ----- Method: PluggableListMorph>>keystrokePreviewSelector: (in category 'selection') -----
  keystrokePreviewSelector: sel
  "The method on the model that will be given first view of any keystroke events.  For access via scripting"
 
  keystrokePreviewSelector := sel!

Item was removed:
- ----- Method: PluggableListMorph>>list: (in category 'initialization') -----
- list: listOfStrings  
- "lex doesn't think this is used any longer, but is not yet brave enough to remove it.  It should be removed eventually"
-
-
- "Set the receiver's list as specified"
-
- | morphList h index converter aSelector textColor font loc |
- self isThisEverCalled.
- scroller removeAllMorphs.
- list := listOfStrings ifNil: [Array new].
- list isEmpty ifTrue: [self setScrollDeltas.  ^ self selectedMorph: nil].
- "NOTE: we will want a quick StringMorph init message, possibly even
- combined with event install and positioning"
- font ifNil: [font := Preferences standardListFont].
- converter := self valueOfProperty: #itemConversionMethod.
- converter ifNil: [converter := #asStringOrText].
- textColor := self valueOfProperty: #textColor.
- morphList := list collect: [:each | | stringMorph item |
- item := each.
- item := item perform: converter.
- stringMorph := item isText
- ifTrue: [StringMorph contents: item font: font emphasis: (item emphasisAt: 1)]
- ifFalse: [StringMorph contents: item font: font].
- textColor ifNotNil: [ stringMorph color: textColor ].
- stringMorph
- ].
-
- (aSelector := self valueOfProperty: #balloonTextSelectorForSubMorphs)
- ifNotNil:
- [morphList do: [:m | m balloonTextSelector: aSelector]].
-
- self highlightSelector ifNotNil:
- [model perform: self highlightSelector with: list with: morphList].
-
- "Lay items out vertically and install them in the scroller"
- h := morphList first height "self listItemHeight".
- loc := 0@0.
- morphList do: [:m | m bounds: (loc extent: 9999@h).  loc := loc + (0@h)].
- scroller addAllMorphs: morphList.
-
- index := self getCurrentSelectionIndex.
- self selectedMorph: ((index = 0 or: [index > morphList size]) ifTrue: [nil] ifFalse: [morphList at: index]).
- self setScrollDeltas.
- scrollBar setValue: 0.0!

Item was added:
+ ----- Method: PluggableListMorph>>listFilterAppend: (in category 'filtering') -----
+ listFilterAppend: aChar
+ "Update the list filter or change selection to match the current filter expression. The given character will be appended to the current filter term."
+
+ | milliseconds slowKeyStroke newModelIndex oldModelIndex |
+ model okToChange ifFalse: [ ^ self ].
+
+ newModelIndex := oldModelIndex := self getCurrentSelectionIndex.
+ milliseconds := Time millisecondClockValue.
+ slowKeyStroke := (Time
+ milliseconds: milliseconds
+ since: lastKeystrokeTime) > self class clearFilterDelay.
+ slowKeyStroke := slowKeyStroke or:
+ "For unfiltered lists, ff the users hits the same key repeatedly, support navigation regardless of the configured delay, as quickly as they want."
+ [ self filterableList not and: [ lastKeystrokes size = 1 and: [ lastKeystrokes first = aChar ] ] ] .
+ lastKeystrokeTime := milliseconds.
+
+ "1) Record the key press."
+ slowKeyStroke
+ ifTrue:
+ [ "Fresh filter expression *and* filterable lists? Keep track of that for escaping the filter."
+ (self filterableList and: [self hasFilter not]) ifTrue:
+ [ priorSelection := self modelIndexFor: self selectionIndex ].
+ "Forget previous keystrokes and search in following elements."
+ lastKeystrokes := aChar asLowercase asString.
+ self filterableList ifFalse:
+ [ newModelIndex := self modelIndexFor: ((self nextSelectionIndexFrom: self selectionIndex) max: 1) ] ]
+ ifFalse:
+ [ "Append quick keystrokes but don't move selection if it still matches."
+ lastKeystrokes := lastKeystrokes , aChar asLowercase asString.
+ self filterableList ifFalse:
+ [ newModelIndex := self modelIndexFor: ((self nextSelectionIndexFrom: self selectionIndex-1) max: 1) ] ].
+
+ self filterableList ifTrue:
+ [ self updateListFilter.
+ newModelIndex := self modelIndexFor: 1 ].
+
+ (self hasFilter and: [(self getCurrentSelectionIndex = newModelIndex) not]) ifTrue:
+ [ self changeModelSelection: newModelIndex ].!

Item was added:
+ ----- Method: PluggableListMorph>>listFilterSet: (in category 'filtering') -----
+ listFilterSet: aString
+ "Set the filter term and select the first match."
+
+ self filterTerm: aString.
+ self changeModelSelection: (self listSize = 0 ifTrue: [0] ifFalse: [self modelIndexFor: 1]).!

Item was changed:
  ----- Method: PluggableListMorph>>listMorph (in category 'accessing') -----
  listMorph
- listMorph ifNil: [
- "create this lazily, in case the morph is legacy"
- listMorph := self listMorphClass new.
- listMorph listSource: self.
- listMorph width: self scroller width.
- listMorph color: self textColor ].
 
+ ^ listMorph!
- listMorph owner ~~ self scroller ifTrue: [
- "list morph needs to be installed.  Again, it's done this way to accomodate legacy PluggableListMorphs"
- self scroller removeAllMorphs.
- self scroller addMorph: listMorph ].
-
- ^listMorph!

Item was added:
+ ----- Method: PluggableListMorph>>listSize (in category 'list morph callbacks') -----
+ listSize
+ "Number of items in the (filtered) list."
+
+ ^ self getList size!

Item was changed:
  ----- Method: PluggableListMorph>>maximumSelection (in category 'selection') -----
  maximumSelection
+ "Return the highest ui index that can be selected."
+
+ ^ self listSize!
- ^ self getListSize!

Item was changed:
+ ----- Method: PluggableListMorph>>modelIndexFor: (in category 'filtering') -----
- ----- Method: PluggableListMorph>>modelIndexFor: (in category 'selection') -----
  modelIndexFor: selectionIndex
+ "The model does not know anything about the receiver's filtering. So if I am filtered, we must determine the correct index by scanning the full list in the model -- or use the lookup cache."
+
+ self hasFilter ifFalse: [^ selectionIndex]. "No lookup needed."
+ self filterableList ifFalse: [^ selectionIndex]. "No lookup needed."
+ selectionIndex = 0 ifTrue: [^ 0]. "Nothing selected."
+
+ ^ viewToModel at: selectionIndex ifAbsent: [0]!
- "The model does not know anything about the receiver's filtering, so if I am filtered, we must determine the correct index by scanning the full list in the model."
- ^ (selectionIndex > 0 and: [ self hasFilter ])
- ifTrue:
- [ selectionIndex > list size
- ifTrue: [ 0 ]
- ifFalse: [ self getFullList indexOf: (self getListItem: selectionIndex) ] ]
- ifFalse: [ selectionIndex ]!

Item was changed:
+ ----- Method: PluggableListMorph>>modifierKeyPressed: (in category 'model access - keystroke') -----
- ----- Method: PluggableListMorph>>modifierKeyPressed: (in category 'model access') -----
  modifierKeyPressed: aChar
+
+ keystrokeActionSelector ifNil: [^ self].
+
+ model
+ perform: keystrokeActionSelector
+ withEnoughArguments: {
+ aChar.
+ self}.!
- | args |
- keystrokeActionSelector isNil ifTrue: [^nil].
- args := keystrokeActionSelector numArgs.
- args = 1 ifTrue: [^model perform: keystrokeActionSelector with: aChar].
- args = 2
- ifTrue:
- [^model
- perform: keystrokeActionSelector
- with: aChar
- with: self].
- ^self error: 'keystrokeActionSelector must be a 1- or 2-keyword symbol'!

Item was changed:
+ ----- Method: PluggableListMorph>>mouseDown: (in category 'event handling') -----
- ----- Method: PluggableListMorph>>mouseDown: (in category 'events') -----
  mouseDown: evt
  | selectors row |
  row := self rowAtLocation: evt position.
 
  evt yellowButtonPressed  "First check for option (menu) click"
  ifTrue: [
  (self class menuRequestUpdatesSelection and: [model okToChange]) ifTrue: [
  "Models depend on the correct selection:"
+ self selectionIndex = row
- self selectionIndex = (self modelIndexFor: row)
  ifFalse: [self changeModelSelection: (self modelIndexFor: row)]].
 
  ^ self yellowButtonActivity: evt shiftPressed].
  row = 0  ifTrue: [^super mouseDown: evt].
  "self dragEnabled ifTrue: [aMorph highlightForMouseDown]."
  selectors := Array
  with: #click:
  with: (doubleClickSelector ifNotNil:[#doubleClick:])
  with: nil
  with: (self dragEnabled ifTrue:[#startDrag:] ifFalse:[nil]).
  evt hand waitForClicksOrDrag: self event: evt selectors: selectors threshold: HandMorph dragThreshold "pixels".!

Item was removed:
- ----- Method: PluggableListMorph>>mouseDown:onItem: (in category 'obsolete') -----
- mouseDown: event onItem: aMorph
- self removeObsoleteEventHandlers.!

Item was changed:
+ ----- Method: PluggableListMorph>>mouseEnter: (in category 'event handling') -----
- ----- Method: PluggableListMorph>>mouseEnter: (in category 'events') -----
  mouseEnter: event
 
  super mouseEnter: event.
  Preferences mouseOverForKeyboardFocus
  ifTrue: [event hand newKeyboardFocus: self].!

Item was changed:
+ ----- Method: PluggableListMorph>>mouseEnterDragging: (in category 'event handling') -----
- ----- Method: PluggableListMorph>>mouseEnterDragging: (in category 'events') -----
  mouseEnterDragging: evt
 
  (evt hand hasSubmorphs and:[self dropEnabled]) ifFalse: ["no d&d"
  ^super mouseEnterDragging: evt].
 
  (self wantsDroppedMorph: evt hand firstSubmorph event: evt )
  ifTrue:[
  potentialDropRow := self rowAtLocation: evt position.
  evt hand newMouseFocus: self.
  self changed.
  "above is ugly but necessary for now"
  ].
  !

Item was removed:
- ----- Method: PluggableListMorph>>mouseEnterDragging:onItem: (in category 'obsolete') -----
- mouseEnterDragging: anEvent onItem: aMorph
- self removeObsoleteEventHandlers.!

Item was changed:
+ ----- Method: PluggableListMorph>>mouseLeave: (in category 'event handling') -----
- ----- Method: PluggableListMorph>>mouseLeave: (in category 'events') -----
  mouseLeave: event
 
  super mouseLeave: event.
  self hoverRow: nil.
 
  Preferences mouseOverForKeyboardFocus
  ifTrue: [event hand releaseKeyboardFocus: self].!

Item was changed:
+ ----- Method: PluggableListMorph>>mouseLeaveDragging: (in category 'event handling') -----
- ----- Method: PluggableListMorph>>mouseLeaveDragging: (in category 'events') -----
  mouseLeaveDragging: anEvent
 
  self hoverRow: nil.
  (self dropEnabled and:[anEvent hand hasSubmorphs]) ifFalse: ["no d&d"
  ^ super mouseLeaveDragging: anEvent].
  self resetPotentialDropRow.
  anEvent hand releaseMouseFocus: self.
  "above is ugly but necessary for now"
  !

Item was removed:
- ----- Method: PluggableListMorph>>mouseLeaveDragging:onItem: (in category 'obsolete') -----
- mouseLeaveDragging: anEvent onItem: aMorph
- self removeObsoleteEventHandlers.!

Item was changed:
+ ----- Method: PluggableListMorph>>mouseMove: (in category 'event handling') -----
- ----- Method: PluggableListMorph>>mouseMove: (in category 'events') -----
  mouseMove: evt
 
  (self dropEnabled and:[evt hand hasSubmorphs])
  ifFalse:[^super mouseMove: evt].
  potentialDropRow ifNotNil:[
  potentialDropRow = (self rowAtLocation: evt position)
  ifTrue:[^self].
  ].
  self mouseLeaveDragging: evt.
  (self containsPoint: evt position)
  ifTrue:[self mouseEnterDragging: evt].!

Item was changed:
+ ----- Method: PluggableListMorph>>mouseUp: (in category 'event handling') -----
- ----- Method: PluggableListMorph>>mouseUp: (in category 'events') -----
  mouseUp: event
+
- "The mouse came up within the list; take appropriate action"
  | row |
+ model okToChange ifFalse: [^ self].
+
  row := self rowAtLocation: event position.
- "aMorph ifNotNil: [aMorph highlightForMouseDown: false]."
- model okToChange
- ifFalse: [^ self].
- "No change if model is locked"
  row = self selectionIndex
+ ifTrue: [(autoDeselect ifNil: [true]) ifTrue: [row = 0 ifFalse: [self changeModelSelection: 0] ]]
- ifTrue: [(autoDeselect ifNil: [true]) ifTrue:[row = 0 ifFalse: [self changeModelSelection: 0] ]]
  ifFalse: [self changeModelSelection: (self modelIndexFor: row)].
+
  event hand newKeyboardFocus: self.
  hasFocus := true.
+ Cursor normal show.!
- Cursor normal show!

Item was removed:
- ----- Method: PluggableListMorph>>mouseUp:onItem: (in category 'obsolete') -----
- mouseUp: event onItem: aMorph
- self removeObsoleteEventHandlers.!

Item was added:
+ ----- Method: PluggableListMorph>>nextSelectionIndexFrom: (in category 'filtering') -----
+ nextSelectionIndexFrom: start
+ "Return the next selection index that matches the current filter term."
+
+ | newViewIndex oldViewIndex startIndex |
+ oldViewIndex := newViewIndex := start \\ (self listSize max: 1) + 1.
+ startIndex := newViewIndex.
+
+ [ (self itemAt: newViewIndex) asString withBlanksTrimmed asLowercase
+ beginsWith: lastKeystrokes ] whileFalse:
+ [ (newViewIndex := newViewIndex \\ (self listSize max: 1) + 1) = startIndex ifTrue:
+ [ self flash. ^ 0 "Not in list." ] ].
+
+ oldViewIndex = newViewIndex ifTrue: [ self flash ].
+
+ ^ newViewIndex!

Item was changed:
  ----- Method: PluggableListMorph>>numSelectionsInView (in category 'scrolling') -----
  numSelectionsInView
+ "Overwritten to map submorphCount to the (filtered) list size. There is only one submorph: the lazy list morph."
- "Answer the scroller's height based on the average number of submorphs."
 
+ ^ scroller numberOfItemsPotentiallyInViewWith: self listSize!
- (scroller submorphCount > 0) ifFalse:[ ^0 ].
-
- "ugly hack, due to code smell.
- PluggableListMorph added another level of indirection,
- There is always only one submorph - a LazyListMorph which holds the actual list,
- but TransformMorph doesn't know that and we are left with a breach of interface.
-
- see vUnadjustedScrollRange for another bad example."
-
- ^scroller numberOfItemsPotentiallyInViewWith: (scroller
- submorphs last getListSize).!

Item was changed:
  ----- Method: PluggableListMorph>>on:list:selected:changeSelected:menu:keystroke: (in category 'initialization') -----
  on: anObject list: getListSel selected: getSelectionSel changeSelected: setSelectionSel menu: getMenuSel keystroke: keyActionSel
+
  self model: anObject.
+
  getListSelector := getListSel.
  getIndexSelector := getSelectionSel.
  setIndexSelector := setSelectionSel.
  getMenuSelector := getMenuSel.
  keystrokeActionSelector := keyActionSel.
+
  autoDeselect := true.
+
  self updateList.
+ self updateListSelection.
+
+ self initForKeystrokes.!
- self selectionIndex: self getCurrentSelectionIndex.
- self initForKeystrokes!

Item was changed:
  ----- Method: PluggableListMorph>>potentialDropItem (in category 'drag and drop') -----
  potentialDropItem
  "return the item that the most recent drop hovered over, or nil if there is no potential drop target"
  self potentialDropRow = 0 ifTrue: [ ^self ].
+ ^self itemAt: self potentialDropRow!
- ^self getListItem: self potentialDropRow!

Item was changed:
+ ----- Method: PluggableListMorph>>previewKeystroke: (in category 'model access - keystroke') -----
+ previewKeystroke: keystrokeEvent
- ----- Method: PluggableListMorph>>previewKeystroke: (in category 'model access') -----
- previewKeystroke: event
  "Let the model decide if it's going to handle the event for us"
 
+ keystrokePreviewSelector ifNil: [^ false].
+
+ ^ model
+ perform: keystrokePreviewSelector
+ withEnoughArguments: {
+ keystrokeEvent.
+ self}!
- ^ keystrokePreviewSelector
- ifNil: [ false ]
- ifNotNil: [ model perform: keystrokePreviewSelector with: event ]!

Item was changed:
  ----- Method: PluggableListMorph>>removeFilter (in category 'filtering') -----
  removeFilter
+
+ self filterTerm: String empty.!
- lastKeystrokes := String empty.
- list := nil!

Item was removed:
- ----- Method: PluggableListMorph>>removeObsoleteEventHandlers (in category 'obsolete') -----
- removeObsoleteEventHandlers
- scroller submorphs do:[:m|
- m eventHandler: nil; highlightForMouseDown: false; resetExtension].!

Item was removed:
- ----- Method: PluggableListMorph>>resetHScrollRange (in category 'scroll cache') -----
- resetHScrollRange
-
- hScrollRangeCache := nil.
- self deriveHScrollRange.
- !

Item was removed:
- ----- Method: PluggableListMorph>>resetHScrollRangeIfNecessary (in category 'scroll cache') -----
- resetHScrollRangeIfNecessary
-
- hScrollRangeCache ifNil: [ ^self deriveHScrollRange ].
-
- (list isNil or: [list isEmpty])
- ifTrue:[^hScrollRangeCache := Array with: 0 with: 0 with: 0 with: 0 with: 0].
-
- "Make a guess as to whether the scroll ranges need updating based on whether the size, first item, or last item of the list has changed"
- (
- (hScrollRangeCache third == list size) and: [
- (hScrollRangeCache fourth == list first) and: [
- (hScrollRangeCache fifth == list last)
- ]])
- ifFalse:[self deriveHScrollRange].
-
- !

Item was changed:
+ ----- Method: PluggableListMorph>>rowAtLocation: (in category 'accessing - items') -----
- ----- Method: PluggableListMorph>>rowAtLocation: (in category 'accessing') -----
  rowAtLocation: aPoint
  "Return the row at the given point or 0 if outside"
  | pointInListMorphCoords |
  pointInListMorphCoords := (self scroller transformFrom: self) transform: aPoint.
  ^self listMorph rowAtLocation: pointInListMorphCoords.!

Item was changed:
  ----- Method: PluggableListMorph>>scrollSelectionIntoView (in category 'selection') -----
  scrollSelectionIntoView
  "make sure that the current selection is visible"
+
  | row |
+ (row := self selectionIndex) = 0
+ ifFalse: [self scrollToShow: (self listMorph drawBoundsForRow: row)]!
- row := self uiIndexFor: self getCurrentSelectionIndex.
- row = 0 ifTrue: [ ^ self ].
- self scrollToShow: (self listMorph drawBoundsForRow: row)!

Item was changed:
  ----- Method: PluggableListMorph>>selection (in category 'selection') -----
  selection
+
+ ^ self getList
+ at: self selectionIndex
+ ifAbsent: [nil]!
- self selectionIndex = 0 ifTrue: [ ^nil ].
- list ifNotNil: [ ^list at: self selectionIndex ].
- ^ self getListItem: self selectionIndex!

Item was changed:
  ----- Method: PluggableListMorph>>selectionIndex: (in category 'selection') -----
+ selectionIndex: viewIndex
- selectionIndex: index
  "Called internally to select the index-th item."
+
- | row |
  self unhighlightSelection.
+ self listMorph selectedRow: (viewIndex min: self listSize).
- row := index ifNil: [ 0 ].
- row := row min: self getListSize.  "make sure we don't select past the end"
- self listMorph selectedRow: row.
  self highlightSelection.
+
  self scrollSelectionIntoView.!

Item was removed:
- ----- Method: PluggableListMorph>>setGetListSelector: (in category 'selection') -----
- setGetListSelector: sel
- "Set the the receiver's getListSelector as indicated.  For access via scripting"
-
- getListSelector := sel!

Item was changed:
+ ----- Method: PluggableListMorph>>specialKeyPressed: (in category 'model access - keystroke') -----
- ----- Method: PluggableListMorph>>specialKeyPressed: (in category 'model access') -----
  specialKeyPressed: asciiValue
  "A special key with the given ascii-value was pressed; dispatch it"
  | oldSelection nextSelection max howManyItemsShowing |
  (#(8 13) includes: asciiValue) ifTrue:
  [ "backspace key - clear the filter, restore the list with the selection"
  model okToChange ifFalse: [^ self].
  self removeFilter.
  priorSelection ifNotNil:
  [ | prior |
  prior := priorSelection.
  priorSelection := self getCurrentSelectionIndex.
  asciiValue = 8 ifTrue: [ self changeModelSelection: prior ] ].
+ ^ self ].
- ^ self updateList ].
  asciiValue = 27 ifTrue:
  [" escape key"
  ^ ActiveEvent shiftPressed
  ifTrue:
  [ActiveWorld putUpWorldMenuFromEscapeKey]
  ifFalse:
  [self yellowButtonActivity: false]].
 
  max := self maximumSelection.
  max > 0 ifFalse: [^ self].
  nextSelection := oldSelection := self selectionIndex.
  asciiValue = 31 ifTrue:
  [" down arrow"
  nextSelection := oldSelection + 1.
  nextSelection > max ifTrue: [nextSelection := 1]].
  asciiValue = 30 ifTrue:
  [" up arrow"
  nextSelection := oldSelection - 1.
  nextSelection < 1 ifTrue: [nextSelection := max]].
  asciiValue = 1 ifTrue:
  [" home"
  nextSelection := 1].
  asciiValue = 4 ifTrue:
  [" end"
  nextSelection := max].
  howManyItemsShowing := self numSelectionsInView.
  asciiValue = 11 ifTrue:
  [" page up"
  nextSelection := 1 max: oldSelection - howManyItemsShowing].
  asciiValue = 12 ifTrue:
  [" page down"
  nextSelection := oldSelection + howManyItemsShowing min: max].
  model okToChange ifFalse: [^ self].
  "No change if model is locked"
  oldSelection = nextSelection ifTrue: [^ self flash].
  ^ self changeModelSelection: (self modelIndexFor: nextSelection)!

Item was changed:
  ----- Method: PluggableListMorph>>startDrag: (in category 'drag and drop') -----
  startDrag: evt
 
  | item itemMorph |
  evt hand hasSubmorphs ifTrue: [^ self].
  self model okToChange ifFalse: [^ self].
 
  "Ensure selection to save additional click."
+ (self rowAtLocation: evt position) in: [:clickedRow |
+ self selectionIndex = clickedRow
+ ifFalse: [self changeModelSelection: (self modelIndexFor: clickedRow)]].
- (self modelIndexFor: (self rowAtLocation: evt position)) in: [:evtIndex |
- self selectionIndex = evtIndex ifFalse: [self changeModelSelection: evtIndex]].
 
  item := self selection ifNil: [^ self].
  itemMorph := StringMorph contents: item asStringOrText.
 
  [ "Initiate drag."
  (self model dragPassengerFor: itemMorph inMorph: self) ifNotNil: [:passenger | | ddm |
  ddm := (self valueOfProperty: #dragTransferClass ifAbsent: [TransferMorph]) withPassenger: passenger from: self.
  ddm dragTransferType: (self model dragTransferTypeForMorph: self).
  ddm updateFromUserInputEvent: evt.
  self model dragStartedFor: itemMorph transferMorph: ddm.
  evt hand grabMorph: ddm]
  ] ensure: [
  Cursor normal show.
  evt hand releaseMouseFocus: self].!

Item was removed:
- ----- Method: PluggableListMorph>>startDrag:onItem: (in category 'obsolete') -----
- startDrag: evt onItem: itemMorph
- self removeObsoleteEventHandlers.!

Item was changed:
+ ----- Method: PluggableListMorph>>topVisibleRowIndex (in category 'accessing - items') -----
- ----- Method: PluggableListMorph>>topVisibleRowIndex (in category 'accessing') -----
  topVisibleRowIndex
  ^ self rowAtLocation: self topLeft+(3@3)!

Item was removed:
- ----- Method: PluggableListMorph>>uiIndexFor: (in category 'selection') -----
- uiIndexFor: modelIndex
- "The model does not know anything about the receiver's filtering.  Answer the index in my (possibly filtered) list for modelIndex."
- (modelIndex > 0 and: [ self hasFilter ])
- ifTrue:
- [ | selectedItem |
- selectedItem := self getFullList at: modelIndex.
- (list ifNil: [ self getList ]) withIndexDo:
- [ : eachMatchingItem : n | eachMatchingItem = selectedItem ifTrue: [ ^ n ] ].
- ^ 0 ]
- ifFalse: [ ^ modelIndex ]!

Item was changed:
  ----- Method: PluggableListMorph>>update: (in category 'updating') -----
  update: aSymbol
+
+ aSymbol == getListSelector ifTrue: [^ self updateList].
+ aSymbol == getIndexSelector ifTrue: [^ self updateListSelection].
+
+ "The following selectors are rarely used."
+ aSymbol == getListSizeSelector ifTrue: [^ self updateList].
+ aSymbol == getListElementSelector ifTrue: [^ self updateList].!
- "Refer to the comment in View|update:."
- aSymbol == getListSelector ifTrue:
- [ self updateList.
- ^ self ].
- aSymbol == getIndexSelector ifTrue:
- [ | uiIndex modelIndex |
- uiIndex := self uiIndexFor: (modelIndex := self getCurrentSelectionIndex).
- self selectionIndex:
- (uiIndex = 0
- ifTrue:
- [ "The filter is preventing us from selecting the item we want - remove it."
- (modelIndex > 0 and: [list notNil and: [list size > 0]]) ifTrue: [ self removeFilter; updateList ].
- modelIndex ]
- ifFalse: [ uiIndex ]).
- ^ self ]!

Item was changed:
  ----- Method: PluggableListMorph>>updateList (in category 'updating') -----
  updateList
 
+ self updateList: nil.!
- | index |
- fullList := nil.
- self listMorph listChanged.
- index := self getCurrentSelectionIndex.
- self resetPotentialDropRow.
- self selectionIndex: (self uiIndexFor: index).
- !

Item was added:
+ ----- Method: PluggableListMorph>>updateList: (in category 'updating') -----
+ updateList: modelList
+ "Keeps the current filter as it is."
+
+ fullList := modelList.
+ self resetPotentialDropRow.
+ self updateListFilter.!

Item was added:
+ ----- Method: PluggableListMorph>>updateListFilter (in category 'updating') -----
+ updateListFilter
+
+ | selection |
+ selection := self selection.
+
+ list := nil.
+ modelToView := nil.
+ viewToModel := nil.
+
+ self getList.
+
+ "Try to restore the last selection."
+ selection ifNotNil: [self selection: selection].!

Item was added:
+ ----- Method: PluggableListMorph>>updateListMorph (in category 'updating') -----
+ updateListMorph
+ "Tell my list morph that the data has changed. This is an extra hook for subclasses."
+
+ self listMorph listChanged.!

Item was added:
+ ----- Method: PluggableListMorph>>updateListSelection (in category 'updating') -----
+ updateListSelection
+
+ self updateListSelection: self getCurrentSelectionIndex.!

Item was added:
+ ----- Method: PluggableListMorph>>updateListSelection: (in category 'updating') -----
+ updateListSelection: modelIndex
+ "Model changed the selection. Invalidate the filtered list if the selection index cannot be found."
+
+ | viewIndex |
+ modelIndex = 0 ifTrue: [
+ ^ self selectionIndex: 0].
+
+ "The the model is inconsistent. Cannot fix here."
+ modelIndex > self fullListSize ifTrue: [
+ ^ self selectionIndex: 0].
+
+ "Lookup the view index."
+ viewIndex := self viewIndexFor: modelIndex.
+
+ "The filter is preventing us from selecting the item we want - remove it."
+ viewIndex = 0 ifTrue: [
+ self removeFilter.
+ viewIndex := modelIndex].
+
+ self selectionIndex: viewIndex.!

Item was changed:
  ----- Method: PluggableListMorph>>userString (in category 'debug and other') -----
  userString
  "Do I have a text string to be searched on?"
 
  ^ String streamContents: [:strm |
+ self getFullList do: [:item |
- 1 to: self getListSize do: [:i |
  "must use asStringOrText because that's what the drawing uses, too"
+ strm nextPutAll: item asStringOrText; cr]]!
- strm nextPutAll: (self getListItem: i) asStringOrText; cr]]!

Item was changed:
  ----- Method: PluggableListMorph>>verifyContents (in category 'updating') -----
  verifyContents
  "Verify the contents of the receiver, reconstituting if necessary.  Called whenever window is reactivated, to react to possible structural changes.  Also called periodically in morphic if the smartUpdating preference is true"
+
+ | currentList modelList modelIndex |
+ self flag: #performance. "mt: We do have changed/update. Why can't the tools communicate through an appropriate notifier such as the SystemChangeNotifier?"
+
+ "1) Is the list still up to date?"
+ currentList := fullList. fullList := nil.
+ modelList := self getFullList.
+ modelList = currentList ifTrue: [^ self].
+ self updateList: modelList.
+
+ "2) Is the selection still up to date?"
+ modelIndex := self getCurrentSelectionIndex.
+ (self modelIndexFor: self selectionIndex) = modelIndex ifTrue: [^ self].
+ self updateListSelection: modelIndex.!
- | newList existingSelection anIndex oldList |
- oldList := list ifNil: [ #() ].
- newList := self getList.
- oldList = newList ifTrue: [ ^ self ].
- existingSelection :=  oldList at: self selectionIndex ifAbsent: [ nil ].
- self updateList.
- (existingSelection notNil and: [(anIndex := self getFullList indexOf: existingSelection asStringOrText ifAbsent: [nil]) notNil])
- ifTrue:
- [model noteSelectionIndex: anIndex for: getListSelector.
- self selectionIndex: anIndex]
- ifFalse:
- [self changeModelSelection: 0]!

Item was added:
+ ----- Method: PluggableListMorph>>viewIndexFor: (in category 'filtering') -----
+ viewIndexFor: modelIndex
+ "The model does not know anything about the receiver's filtering. So if I am filtered, we must determine the correct index by scanning the filtered list in the view -- or use the lookup cache."
+
+ self hasFilter ifFalse: [^ modelIndex]. "No lookup needed."
+ self filterableList ifFalse: [^ modelIndex]. "No lookup needed."
+ modelIndex = 0 ifTrue: [^ 0]. "Nothing selected."
+
+ ^ modelToView at: modelIndex ifAbsent: [0]!

Item was changed:
  ----- Method: PluggableListMorph>>visibleList (in category 'accessing') -----
  visibleList
+
+ ^ (self topVisibleRowIndex to: (self bottomVisibleRowIndex min: self listSize))
+ collect: [:viewIndex | self itemAt: viewIndex].!
- ^ list isEmptyOrNil
- ifTrue: [ Array empty ]
- ifFalse:
- [ list
- copyFrom: self topVisibleRowIndex
- to: (self bottomVisibleRowIndex min: list size) ]!

Item was changed:
  PluggableListMorph subclass: #PluggableListMorphByItem
+ instanceVariableNames: ''
- instanceVariableNames: 'itemList'
  classVariableNames: ''
  poolDictionaries: ''
  category: 'Morphic-Pluggable Widgets'!
+
+ !PluggableListMorphByItem commentStamp: 'mt 10/12/2019 12:25' prior: 0!
+ Like PluggableListMorph but maps #setIndexSelector and #getIndexSelector to actual items instead of indexes.!

Item was changed:
  ----- Method: PluggableListMorphByItem>>changeModelSelection: (in category 'model access') -----
  changeModelSelection: anInteger
  "Change the model's selected item to be the one at the given index."
 
- | item |
  setIndexSelector ifNotNil: [
+ model
+ perform: setIndexSelector
+ with: (self getFullList at: anInteger ifAbsent: [nil])].!
- item := (anInteger = 0 ifTrue: [nil] ifFalse: [itemList at: anInteger]).
- model perform: setIndexSelector with: item].
- self update: getIndexSelector.
- !

Item was changed:
  ----- Method: PluggableListMorphByItem>>getCurrentSelectionIndex (in category 'model access') -----
  getCurrentSelectionIndex
  "Answer the index of the current selection."
+
+ ^ getIndexSelector
+ ifNil: [0]
+ ifNotNil: [self getFullList indexOf: (model perform: getIndexSelector)]!
- | item |
- getIndexSelector == nil ifTrue: [^ 0].
- item := model perform: getIndexSelector.
- ^ list findFirst: [ :x | x = item]
- !

Item was removed:
- ----- Method: PluggableListMorphByItem>>getList (in category 'model access') -----
- getList
- "cache the raw items in itemList"
- itemList := getListSelector ifNil: [ #() ] ifNotNil: [ model perform: getListSelector ].
- ^super getList!

Item was removed:
- ----- Method: PluggableListMorphByItem>>list: (in category 'initialization') -----
- list: arrayOfStrings
- "Set the receivers items to be the given list of strings."
- "Note: the instance variable 'items' holds the original list.
- The instance variable 'list' is a paragraph constructed from
- this list."
- "NOTE: this is no longer true; list is a real list, and itemList is no longer used.  And this method shouldn't be called, incidentally."
- self isThisEverCalled .
- itemList := arrayOfStrings.
- ^ super list: arrayOfStrings!

Item was changed:
  ----- Method: PluggableListMorphOfMany>>basicKeyPressed: (in category 'model access') -----
  basicKeyPressed: aCharacter
+ "Maps [space] key to be a special key."
+
-
  aCharacter = Character space
  ifTrue: [self specialKeyPressed: aCharacter asciiValue]
  ifFalse: [super basicKeyPressed: aCharacter].!

Item was changed:
  ----- Method: PluggableListMorphOfMany>>itemSelectedAmongMultiple: (in category 'model access') -----
+ itemSelectedAmongMultiple: viewIndex
+ ^self listSelectionAt: (self modelIndexFor: viewIndex)!
- itemSelectedAmongMultiple: index
- ^self listSelectionAt: (self modelIndexFor: index)!

Item was removed:
- ----- Method: PluggableListMorphOfMany>>list: (in category 'initialization') -----
- list: listOfStrings
- scroller removeAllMorphs.
- list := listOfStrings ifNil: [Array new].
- list isEmpty ifTrue: [^ self selectedMorph: nil].
- super list: listOfStrings.
-
- "At this point first morph is sensitized, and all morphs share same handler."
- scroller firstSubmorph on: #mouseEnterDragging
- send: #mouseEnterDragging:onItem:
- to: self.
- scroller firstSubmorph on: #mouseUp
- send: #mouseUp:onItem:
- to: self.
- "This should add this behavior to the shared event handler thus affecting all items"!

Item was changed:
+ ----- Method: PluggableListMorphOfMany>>listSelectionAt: (in category 'model access') -----
- ----- Method: PluggableListMorphOfMany>>listSelectionAt: (in category 'drawing') -----
  listSelectionAt: index
  getSelectionListSelector ifNil:[^false].
  ^model perform: getSelectionListSelector with: index!

Item was changed:
+ ----- Method: PluggableListMorphOfMany>>listSelectionAt:put: (in category 'model access') -----
- ----- Method: PluggableListMorphOfMany>>listSelectionAt:put: (in category 'drawing') -----
  listSelectionAt: index put: value
  setSelectionListSelector ifNil:[^false].
  ^model perform: setSelectionListSelector with: index with: value!

Item was changed:
  ----- Method: PluggableListMorphOfMany>>mouseUp: (in category 'event handling') -----
  mouseUp: event
 
  dragOnOrOff := nil.  "So improperly started drags will have not effect"
+
  event hand newKeyboardFocus: self.
+ hasFocus := true.
+ Cursor normal show.!
- hasFocus := true.!

Item was changed:
  ----- Method: PluggableListMorphOfMany>>on:list:primarySelection:changePrimarySelection:listSelection:changeListSelection:menu:keystroke: (in category 'initialization') -----
  on: anObject list: listSel primarySelection: getSelectionSel changePrimarySelection: setSelectionSel listSelection: getListSel changeListSelection: setListSel menu: getMenuSel keystroke: keyActionSel
  "setup a whole load of pluggability options"
+
  getSelectionListSelector := getListSel.
  setSelectionListSelector := setListSel.
+ super
+ on: anObject
+ list: listSel
+ selected: getSelectionSel
+ changeSelected: setSelectionSel
+ menu: getMenuSel
+ keystroke: keyActionSel.!
- super on: anObject list: listSel selected: getSelectionSel changeSelected: setSelectionSel menu: getMenuSel keystroke: keyActionSel
- !

Item was changed:
  ----- Method: PluggableListMorphOfMany>>specialKeyPressed: (in category 'model access') -----
  specialKeyPressed: asciiValue
+ "Toggle the selection on [space]."
+
-
  asciiValue = Character space asciiValue
  ifTrue: [ | index |
  index :=  self getCurrentSelectionIndex.
  self
  listSelectionAt: index
  put: ((self listSelectionAt: index) not).
  ^ self].
 
  super specialKeyPressed: asciiValue.!

Item was changed:
  ----- Method: PluggableListMorphOfMany>>update: (in category 'updating') -----
  update: aSymbol
+
+ aSymbol == #allSelections ifTrue: [
+ "Convenient - yet hard-coded - way to refresh all selections."
+ super update: getIndexSelector.
- aSymbol == #allSelections ifTrue:
- [self selectionIndex: self getCurrentSelectionIndex.
  ^ self changed].
+ aSymbol == getSelectionListSelector ifTrue: [
+ ^ self changed].
+
+ super update: aSymbol.!
- ^ super update: aSymbol!

Item was changed:
  PluggableListMorph subclass: #PluggableMultiColumnListMorph
+ instanceVariableNames: 'listMorphs'
- instanceVariableNames: 'lists'
  classVariableNames: ''
  poolDictionaries: ''
  category: 'Morphic-Pluggable Widgets'!
 
  !PluggableMultiColumnListMorph commentStamp: '<historical>' prior: 0!
  This morph can be used to show a list having multiple columns,  The columns are self width sized to make the largest entry in each list fit.  In some cases the pane may then be too narrow.
 
  Use it like a regular PluggableListMorph except pass in an array of lists instead of a single list.
 
  There are base assumptions made here that each list in the array of lists is the same size.
 
  Also, the highlight color for the selection is easy to modify in the #highlightSelection method.  I used blue
  when testing just to see it work.!

Item was removed:
- ----- Method: PluggableMultiColumnListMorph>>basicKeyPressed: (in category 'model access') -----
- basicKeyPressed: aChar
- "net supported for multi-column lists; which column should be used?!!  The issue is that the base class implementation uses getList expecting a single collectino to come back instead of several of them"
- ^self!

Item was removed:
- ----- Method: PluggableMultiColumnListMorph>>calculateColumnOffsetsFrom: (in category 'initialization') -----
- calculateColumnOffsetsFrom: maxWidths
- | offsets previous current |
- offsets := Array new: maxWidths size.
- 1
- to: offsets size
- do: [:indx | offsets at: indx put: (maxWidths at: indx)
- + 10].
- 2
- to: offsets size
- do: [:indx |
- previous := offsets at: indx - 1.
- current := offsets at: indx.
- current := previous + current.
- offsets at: indx put: current].
- ^offsets
- !

Item was removed:
- ----- Method: PluggableMultiColumnListMorph>>calculateColumnWidthsFrom: (in category 'initialization') -----
- calculateColumnWidthsFrom: arrayOfMorphs
- | maxWidths |
- maxWidths := Array new: arrayOfMorphs size - 1.
- 1
- to: maxWidths size
- do: [:idx | maxWidths at: idx put: 0].
- 1
- to: maxWidths size
- do: [:idx | (arrayOfMorphs at: idx)
- do: [:mitem | mitem width
- > (maxWidths at: idx)
- ifTrue: [maxWidths at: idx put: mitem width]]].
- ^maxWidths!

Item was changed:
+ ----- Method: PluggableMultiColumnListMorph>>charactersOccluded (in category 'geometry') -----
- ----- Method: PluggableMultiColumnListMorph>>charactersOccluded (in category 'private') -----
  charactersOccluded
  "Not meaningful in multi-column lists, since they should truncate their column widths according to how much space is needed vs. available to show a bit of each."
  ^ 0!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>columnCount (in category 'accessing') -----
+ columnCount
+ "Even an empty column is a column."
+
+ ^ self getFullList size max: 1!

Item was removed:
- ----- Method: PluggableMultiColumnListMorph>>createMorphicListsFrom: (in category 'initialization') -----
- createMorphicListsFrom: arrayOfLists
- | array |
-
- array := Array new: arrayOfLists size.
- 1 to: arrayOfLists size do: [:arrayIndex |
- array at: arrayIndex put: (
- (arrayOfLists at: arrayIndex) collect: [:item | item isText
- ifTrue: [StringMorph
- contents: item
- font: self font
- emphasis: (item emphasisAt: 1)]
- ifFalse: [StringMorph contents: item font: self font]])
- ].
- ^array!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterList:matching: (in category 'filtering') -----
+ filterList: columns matching: aPattern
+ "A matching row has a match in at least one column."
+
+ | frontMatching substringMatching rowCount columnCount tmp |
+ aPattern ifEmpty: [^ columns].
+ columns ifEmpty: [^ columns].
+
+ rowCount := columns first size.
+ rowCount = 0 ifTrue: [^ columns].
+ columnCount := columns size.
+
+ frontMatching := Array new: columnCount.
+ 1 to: columnCount do: [:c | frontMatching at: c put: OrderedCollection new].
+ substringMatching := Array new: columnCount.
+ 1 to: columnCount do: [:c | substringMatching at: c put: OrderedCollection new].
+
+ modelToView := Dictionary new.
+ viewToModel := Dictionary new.
+ tmp := OrderedCollection new.
+
+ 1 to: rowCount do: [:rowIndex |
+ | match foundPos |
+ match := false.
+ foundPos := 0.
+ 1 to: columnCount do: [:colIndex |
+ match := match or: [(foundPos := (self
+ filterListItem: ((columns at: colIndex) at: rowIndex)
+ matching: aPattern)+colIndex) > colIndex]].
+ match & (foundPos = 2) "means front match in first column"
+ ifTrue: [
+ 1 to: columnCount do: [:colIndex |
+ (frontMatching at: colIndex) add: ((columns at: colIndex) at: rowIndex)].
+ modelToView at: rowIndex put: frontMatching first size.
+ viewToModel at: frontMatching first size put: rowIndex]
+ ifFalse: [match ifTrue: [
+ 1 to: columnCount do: [:colIndex |
+ (substringMatching at: colIndex) add: ((columns at: colIndex) at: rowIndex)].
+ tmp add: rowIndex; add: substringMatching first size]]
+ ].
+
+ tmp pairsDo: [:modelIndex :viewIndex |
+ modelToView at: modelIndex put: viewIndex + frontMatching first size.
+ viewToModel at: viewIndex + frontMatching first size put: modelIndex].
+
+ ^ (1 to: columnCount) collect: [:colIndex |
+ (frontMatching at: colIndex), (substringMatching at: colIndex)]
+
+
+
+
+
+
+
+
+
+
+
+
+
+ !

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>font: (in category 'initialization') -----
+ font: aFontOrNil
+
+ listMorphs do: [:lm | lm font: aFontOrNil].
+ super font: aFontOrNil.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>fullListSize (in category 'list morph callbacks') -----
+ fullListSize
+ "return the current number of items in the displayed list"
+
+ ^ self getFullList
+ ifEmpty: [0]
+ ifNotEmpty: [:columns | columns first size]!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>getFilteredList (in category 'filtering') -----
+ getFilteredList
+ "Apply the current filter to the list. Maybe shorten the filter term if there are no matches."
+
+ | fullList filteredList |
+ fullList := self getFullList.
+
+ self hasFilter ifFalse: [^ fullList].
+ fullList ifEmpty: [^ fullList].
+ fullList first ifEmpty: [^ fullList].
+
+ filteredList := self filterList: fullList matching: lastKeystrokes.
+
+ (filteredList first isEmpty not or: [ self allowEmptyFilterResult ])
+ ifFalse:
+ [ "Remove the last character and try filtering again."
+ lastKeystrokes := lastKeystrokes allButLast: 1.
+ ^ self
+ flash;
+ getFilteredList ].
+
+ ^ filteredList!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>getFullList (in category 'model access - cached') -----
+ getFullList
+ "The full list arranges all items column-first."
+
+ fullList ifNotNil: [^ fullList].
+
+ fullList := getListSelector
+ ifNotNil: [:selector | "A) Fetch the list column-first from the model."
+ model perform: selector]
+ ifNil: [
+ (getListSizeSelector notNil and: [getListElementSelector notNil])
+ ifFalse: ["X) We cannot fetch the list from the model. Make it empty."
+ #()]
+ ifTrue: [ "B) Fetch the list row-first from the model:"
+ | listSize |
+ listSize := self getListSize.
+ listSize = 0 ifTrue: [#() "Empty list"] ifFalse: [
+ | firstRow columns |
+ firstRow := self getListItem: 1.
+ columns := Array new: firstRow size.
+ 1 to: columns size do: [:columnIndex |
+ "Initialize all columns."
+ columns at: columnIndex put: (Array new: listSize).
+ "Put the first row in."
+ (columns at: columnIndex) at: 1 put: (firstRow at: columnIndex)].
+ "Put all other rows in."
+ 2 to: listSize do: [:rowIndex | (self getListItem: rowIndex)
+ doWithIndex: [:item :columnIndex |
+ (columns at: columnIndex) at: rowIndex put: item]].
+ columns]]].
+
+ self updateColumns.
+
+ ^ fullList!

Item was removed:
- ----- Method: PluggableMultiColumnListMorph>>getList (in category 'model access') -----
- getList
- "fetch and answer the lists to be displayed"
- getListSelector == nil ifTrue: [^ #()].
- list := model perform: getListSelector.
- list == nil ifTrue: [^ #()].
- list := list collect: [ :column | column collect: [ :item | item asStringOrText ] ].
- ^ list!

Item was changed:
+ ----- Method: PluggableMultiColumnListMorph>>getListItem: (in category 'model access') -----
+ getListItem: modelIndex
+ "Return a row full of items."
+
+ ^ getListElementSelector
+ ifNotNil: [:sel | model perform: sel with: modelIndex ]
+ ifNil: [self getFullList collect: [:column | column at: modelIndex]]!
- ----- Method: PluggableMultiColumnListMorph>>getListItem: (in category 'selection') -----
- getListItem: index
- "get the index-th item in the displayed list"
- getListElementSelector ifNotNil: [ ^(model perform: getListElementSelector with: index) asStringOrText ].
- list ifNotNil: [ ^list first at: index ]. "this is a very trivial fix for the issue of having rows ofdata in the multiple columns. It is *not* a robust solution"
- ^self getList at: index!

Item was removed:
- ----- Method: PluggableMultiColumnListMorph>>getListRow: (in category 'accessing') -----
- getListRow: row
- "return the strings that should appear in the requested row"
- getListElementSelector ifNotNil: [ ^model perform: getListElementSelector with: row ].
- ^self getList collect: [ :l | l at: row ]!

Item was removed:
- ----- Method: PluggableMultiColumnListMorph>>getListSize (in category 'accessing') -----
- getListSize
- | l |
- getListSizeSelector ifNotNil: [ ^model perform: getListSizeSelector ].
-
- l := self getList.
- l isEmpty ifTrue: [ ^ 0 ].
- ^l first size!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>hScrollBarPolicy: (in category 'accessing') -----
+ hScrollBarPolicy: aSymbol
+ "The lazy list morph will never wrap its rows if they do not fit. Instead, they are just clipped. So, #spaceFill is fine if the horizontal scroll bar should never be visible."
+
+ self checkScrollBarPolicy: aSymbol.
+
+ aSymbol ~= #never
+ ifTrue: [listMorphs do: [:lm | lm hResizing: #shrinkWrap]]
+ ifFalse: [listMorphs do: [:lm | lm hResizing: #spaceFill]].
+
+ ^ super hScrollBarPolicy: aSymbol!

Item was removed:
- ----- Method: PluggableMultiColumnListMorph>>highlightSelection (in category 'selection') -----
- highlightSelection
- ^self!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>hoverRow: (in category 'accessing') -----
+ hoverRow: viewIndex
+
+ hoverRow = viewIndex ifTrue: [^ self].
+ listMorphs do: [:listMorph |
+ listMorph rowChanged: hoverRow with: viewIndex].
+ super hoverRow: viewIndex.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>iconAt: (in category 'list morph callbacks') -----
+ iconAt: rowIndex
+
+ ^ self iconAt: rowIndex column: 1!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>iconAt:column: (in category 'list morph callbacks') -----
+ iconAt: rowIndex column: columnIndex
+
+ getIconSelector ifNil: [^ nil].
+
+ getIconSelector numArgs = 1 ifTrue: [
+ "For unspecific icon selectors only icons for the first column."
+ ^ columnIndex = 1
+ ifTrue: [model perform: getIconSelector with: rowIndex]
+ ifFalse: [nil]].
+
+ ^ model
+ perform: getIconSelector
+ with: (self modelIndexFor: rowIndex)
+ with: columnIndex!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>initialize (in category 'initialization') -----
+ initialize
+
+ listMorphs := #().
+ super initialize.
+
+ self scroller
+ listDirection: #leftToRight;
+ cellPositioning: #topLeft.
+
+ listMorphs := OrderedCollection with: listMorph.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>itemAt: (in category 'list morph callbacks') -----
+ itemAt: rowIndex
+
+ ^ self itemAt: rowIndex column: 1!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>itemAt:column: (in category 'list morph callbacks') -----
+ itemAt: rowIndex column: columnIndex
+
+ ^ (self getList at: columnIndex) at: rowIndex!

Item was removed:
- ----- Method: PluggableMultiColumnListMorph>>layoutMorphicLists: (in category 'initialization') -----
- layoutMorphicLists: arrayOfMorphs
- | maxWidths offsets locs h |
- maxWidths := self calculateColumnWidthsFrom: arrayOfMorphs.
- offsets := self calculateColumnOffsetsFrom: maxWidths.
- locs := Array new: arrayOfMorphs size.
- locs at: 1 put: 0 @ 0.
- 2
- to: locs size
- do: [:indx | locs at: indx put: (offsets at: indx - 1)
- @ 0].
- h := arrayOfMorphs first first height.
- 1
- to: arrayOfMorphs size
- do: [:indx | (arrayOfMorphs at: indx)
- do: [:morphItem |
- morphItem
- bounds: ((locs at: indx)
- extent: 9999 @ h).
- locs at: indx put: (locs at: indx)
- + (0 @ h)]]!

Item was removed:
- ----- Method: PluggableMultiColumnListMorph>>list: (in category 'initialization') -----
- list: arrayOfLists
- | listOfStrings |
- lists := arrayOfLists.
- scroller removeAllMorphs.
- listOfStrings := arrayOfLists == nil
- ifTrue: [Array new]
- ifFalse: [
- arrayOfLists isEmpty ifFalse: [
- arrayOfLists at: 1]].
- list := listOfStrings
- ifNil: [Array new].
- self listMorph listChanged..
-
- self setScrollDeltas.
- scrollBar setValue: 0.0!

Item was removed:
- ----- Method: PluggableMultiColumnListMorph>>listMorphClass (in category 'private') -----
- listMorphClass
- ^MulticolumnLazyListMorph!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>listSize (in category 'list morph callbacks') -----
+ listSize
+
+ ^ self visibleRowCount!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>rowAboutToBecomeSelected: (in category 'selection') -----
+ rowAboutToBecomeSelected: anInteger
+
+ listMorphs do: [:listMorph | listMorph preSelectedRow: anInteger].
+ super rowAboutToBecomeSelected: anInteger.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>rowAtLocation: (in category 'accessing - items') -----
+ rowAtLocation: aPoint
+ "Return the row at the given point or 0 if outside"
+
+ | pointInListMorphCoords rowIndex |
+ pointInListMorphCoords := (self scroller transformFrom: self) transform: aPoint.
+
+ listMorphs do: [:listMorph |
+ rowIndex := listMorph rowAtLocation: pointInListMorphCoords.
+ rowIndex > 0 ifTrue: [^ rowIndex]].
+
+ ^ 0
+ !

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>rowCount (in category 'accessing') -----
+ rowCount
+
+ ^ self getFullList
+ ifEmpty: [0]
+ ifNotEmpty: [:columns | columns first size]!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>selection (in category 'selection') -----
+ selection
+ "Collect the selected row from all columns as array."
+
+ ^ self getList collect: [:column |
+ column
+ at: self selectionIndex
+ ifAbsent: [nil]]!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>selection: (in category 'selection') -----
+ selection: someObjects
+
+ | found |
+ someObjects size ~= self columnCount ifTrue: [^ self].
+
+ 1 to: self listSize do: [:row |
+ found := true.
+ self getList doWithIndex: [:items :column |
+ found := found and: [(items at: row) = (someObjects at: column)]].
+ found ifTrue: [^ self selectionIndex: row]].!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>selectionIndex: (in category 'selection') -----
+ selectionIndex: viewIndex
+
+ listMorphs do: [:listMorph | listMorph selectedRow: (viewIndex min: self listSize)].
+ super selectionIndex: viewIndex.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>setListParameters (in category 'initialization') -----
+ setListParameters
+
+ listMorphs ifEmpty: [^ super setListParameters].
+
+ listMorphs do: [:lm | listMorph := lm. super setListParameters].
+ listMorph := listMorphs first.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>textColor: (in category 'initialization') -----
+ textColor: aColor
+
+ listMorphs do: [:listMorph | listMorph color: aColor].
+ super textColor: aColor.!

Item was removed:
- ----- Method: PluggableMultiColumnListMorph>>unhighlightSelection (in category 'selection') -----
- unhighlightSelection
- ^self!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>updateColumns (in category 'updating') -----
+ updateColumns
+ "The number of columns must match the number of list morphs."
+
+ self columnCount = listMorphs size ifTrue: [^ self].
+
+ [self columnCount < listMorphs size]
+ whileTrue: [
+ listMorphs removeLast delete].
+
+ [self columnCount > listMorphs size]
+ whileTrue: [
+ listMorphs addLast: self createListMorph.
+ self scroller addMorphBack: listMorphs last].
+
+ listMorphs doWithIndex: [:ea :col | ea columnIndex: col].
+ self setListParameters.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>updateListMorph (in category 'updating') -----
+ updateListMorph
+ "We have to notify all columns."
+
+ listMorphs do: #listChanged.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>userString (in category 'debug and other') -----
+ userString
+ "Do I have a text string to be searched on?"
+
+ ^ String streamContents: [:strm |
+ 1 to: self rowCount do: [:row |
+ 1 to: self columnCount do: [:col |
+ strm nextPutAll: ((self getFullList at: col) at: row) asStringOrText; tab].
+ strm cr]]!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>verifyContents (in category 'updating') -----
+ verifyContents
+ "Verify the contents of the receiver, reconstituting if necessary.  Called whenever window is reactivated, to react to possible structural changes.  Also called periodically in morphic if the smartUpdating preference is true"
+
+ | changed currentList modelList modelIndex |
+ self flag: #performance. "mt: We do have changed/update. Why can't the tools communicate through an appropriate notifier such as the SystemChangeNotifier?"
+
+ "1) Is the list still up to date?"
+ currentList := fullList. fullList := nil.
+ modelList := self getFullList.
+ changed := false.
+ modelList doWithIndex: [:column :index |
+ changed := changed or: [(currentList at: index) ~= column]].
+ changed ifFalse: [^ self].
+ self updateList: modelList.
+
+ "2) Is the selection still up to date?"
+ modelIndex := self getCurrentSelectionIndex.
+ (self modelIndexFor: self selectionIndex) = modelIndex ifTrue: [^ self].
+ self updateListSelection: modelIndex.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>visibleRowCount (in category 'accessing') -----
+ visibleRowCount
+ "Take the size of the first column. We treat all columns equally."
+
+ ^ self getList
+ ifEmpty: [0]
+ ifNotEmpty: [:columns | columns first size]!

Item was changed:
  PluggableMultiColumnListMorph subclass: #PluggableMultiColumnListMorphByItem
+ instanceVariableNames: ''
- instanceVariableNames: 'itemList'
  classVariableNames: ''
  poolDictionaries: ''
  category: 'Morphic-Pluggable Widgets'!
+
+ !PluggableMultiColumnListMorphByItem commentStamp: 'mt 10/12/2019 12:25' prior: 0!
+ Like PluggableMultiColumnListMorph but maps #setIndexSelector and #getIndexSelector to actual items instead of indexes.!

Item was changed:
  ----- Method: PluggableMultiColumnListMorphByItem>>changeModelSelection: (in category 'model access') -----
+ changeModelSelection: modelIndex
- changeModelSelection: anInteger
  "Change the model's selected item to be the one at the given index."
+
+ setIndexSelector ifNotNil: [
+ model
+ perform: setIndexSelector
+ with: (modelIndex = 0 ifTrue: [#()] ifFalse: [self getFullList collect: [:column | column at: modelIndex]])].!
- | item |
- setIndexSelector
- ifNotNil: [item := anInteger = 0
- ifFalse: [list first at: anInteger].
- model perform: setIndexSelector with: item].
- self update: getIndexSelector!

Item was changed:
  ----- Method: PluggableMultiColumnListMorphByItem>>getCurrentSelectionIndex (in category 'model access') -----
  getCurrentSelectionIndex
+ "Answer the index of the current selection. Similar to #selection: but with the full list instead of the (maybe) filtered list."
- "Answer the index of the current selection."
- | item |
- getIndexSelector == nil
- ifTrue: [^ 0].
- item := model perform: getIndexSelector.
 
+ getIndexSelector ifNil: [^ 0].
+
+ (model perform: getIndexSelector) in: [:row |
+ row ifNil: [^ 0].
+ row ifEmpty: [^ 0].
+
+ 1 to: self fullListSize do: [:rowIndex |
+ | match |
+ match := true.
+ self getFullList doWithIndex: [:column :columnIndex |
+ match := match and: [(column at: rowIndex) = (row at: columnIndex)]].
+ match ifTrue: [^ rowIndex]]].
+
+ ^ 0!
- ^ list first
- findFirst: [:x | x  = item]!

Item was removed:
- ----- Method: PluggableMultiColumnListMorphByItem>>list: (in category 'initialization') -----
- list: arrayOfStrings
- "Set the receivers items to be the given list of strings."
- "Note: the instance variable 'items' holds the original list.  
- The instance variable 'list' is a paragraph constructed from  
- this list."
- "NO LONGER TRUE.  list is a real list, and listItems is obsolete."
- self isThisEverCalled .
- itemList := arrayOfStrings first.
- ^ super list: arrayOfStrings!

Item was changed:
  ----- Method: ScrollPane>>offsetToShow: (in category 'scrolling') -----
  offsetToShow: aRectangle
  "Calculate the offset necessary to show the rectangle."
 
  | offset scrollRange target |
+ self fullBounds. "We need updated bounds."
  offset := scroller offset.
  scrollRange := self hTotalScrollRange @ self vTotalScrollRange.
 
  "Normalize the incoming rectangle."
  target :=
  (scroller width < aRectangle width
  ifTrue: [offset x < aRectangle left "Coming from left?"
  ifTrue: [aRectangle right - scroller width]
  ifFalse: [aRectangle left]]
  ifFalse: [aRectangle left])
  @
  (scroller height < aRectangle height
  ifTrue: [offset y < aRectangle top "Coming from top?"
  ifTrue: [aRectangle bottom - scroller height]
  ifFalse: [aRectangle top]]
  ifFalse: [aRectangle top])
  corner:
  (scroller width < aRectangle width
  ifTrue: [offset x + scroller width > aRectangle right "Coming from right?"
  ifTrue: [aRectangle left + scroller width]
  ifFalse: [aRectangle right]]
  ifFalse: [aRectangle right])
  @
  (scroller height < aRectangle height
  ifTrue: [offset y + scroller height > aRectangle bottom "Coming from bottom?"
  ifTrue: [aRectangle top + scroller height]
  ifFalse: [aRectangle bottom]]
  ifFalse: [aRectangle bottom]).
 
  "Vertical Scrolling"
  target top < offset y
  ifTrue: [offset := offset x @ target top].
  target bottom > (offset y + scroller height)
  ifTrue: [offset := offset x @ (target bottom - scroller height)].
 
  "Horizontal Scrolling"
  target left < offset x
  ifTrue: [offset := target left @ offset y].
  target right > (offset x + scroller width)
  ifTrue: [offset := (target right - scroller width) @ offset y].
 
  ^ (offset min: scrollRange - scroller extent) max: 0@0!

Item was changed:
+ (PackageInfo named: 'Morphic') postscript: 'PluggableListMorph allSubInstancesDo: [:m |
+ m scroller layoutPolicy: TableLayout new.
+ m listMorph
+ cellPositioning: #leftCenter;
+ cellInset: 3@0;
+ vResizing: #shrinkWrap;
+ removeProperty: #errorOnDraw. "Just in case."
+ m updateList.
+ m hScrollBarPolicy: #never].
+ '!
- (PackageInfo named: 'Morphic') postscript: '"Reset all existing text fields because their scrollers have now an actual TableLayout."
- PluggableTextMorph allSubInstancesDo: [:ea |
- ea scroller layoutPolicy: TableLayout new.
- ea textMorph vResizing: #shrinkWrap.
- ea wrapFlag
- ifTrue: [ea wrapFlag: true]
- ifFalse: [ea wrapFlag: false; hideScrollBarsIndefinitely]].'!