The Trunk: Morphic-mt.1582.mcz

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

The Trunk: Morphic-mt.1582.mcz

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

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

Name: Morphic-mt.1582
Author: mt
Time: 23 October 2019, 3:31:24.67861 pm
UUID: 82af72b9-4198-4bd2-8e7e-e8733d9018c7
Ancestors: Morphic-mt.1581

Adds things for column-specific list filtering:

- backgroundColor for LazyListMorph
- optional filter-term indication for LazyListMorph
- column highlights for multi-column lists
- Character tab to cycle between columns

Note that I opted to not use the [space] key for column toggling because (a) we have to widget-focus cycling via [tab] at the moment and (b) the space character might be a valuable filter term.

=============== Diff against Morphic-mt.1581 ===============

Item was changed:
  Morph subclass: #LazyListMorph
+ instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex iconExtent backgroundColor showFilter'
- instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex iconExtent'
  classVariableNames: ''
  poolDictionaries: ''
  category: 'Morphic-Widgets'!
 
  !LazyListMorph commentStamp: 'mt 10/13/2019 19:44' 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]!

Item was added:
+ ----- Method: LazyListMorph>>backgroundColor (in category 'accessing') -----
+ backgroundColor
+ "Since #color is this morph's default text color, this extra property is used for the actual background color. Supports nil."
+
+ ^ backgroundColor!

Item was added:
+ ----- Method: LazyListMorph>>backgroundColor: (in category 'accessing') -----
+ backgroundColor: aColor
+
+ backgroundColor = aColor ifTrue: [^ self].
+ backgroundColor := aColor.
+
+ self changed.
+ "Invalidate owner because we want to fill the vertical axis in the viewport entirely."
+ self owner ifNotNil: [:o | o changed].!

Item was changed:
  ----- Method: LazyListMorph>>displayFilterOn:for:in:font: (in category 'drawing') -----
  displayFilterOn: canvas for: row in: drawBounds font: font
  "Draw filter matches if any."
 
  | fillStyle fillHeight |
+ self showFilter ifFalse: [^ self].
- listSource filterableList ifFalse: [^ self].
 
  fillHeight := font height.
  fillStyle := self filterColor isColor
  ifTrue: [SolidFillStyle color: self filterColor]
  ifFalse: [self filterColor].
  fillStyle isGradientFill ifTrue: [
  fillStyle origin: drawBounds topLeft.
  fillStyle direction: 0@ fillHeight].
 
  (self filterOffsets: row) do: [:offset |
  | highlightRectangle |
  highlightRectangle := ((drawBounds left + offset first first) @ drawBounds top
  corner: (drawBounds left + offset first last) @ (drawBounds top + fillHeight)).
  canvas
  frameAndFillRoundRect: (highlightRectangle outsetBy: 1@0)
  radius: (3 * RealEstateAgent scaleFactor) truncated
  fillStyle: fillStyle
  borderWidth: (1 * RealEstateAgent scaleFactor) truncated
  borderColor: fillStyle asColor twiceDarker.
  canvas
  drawString: offset second
  in: highlightRectangle
  font: font
  color: self filterTextColor].!

Item was changed:
  ----- Method: LazyListMorph>>drawOn: (in category 'drawing') -----
  drawOn: aCanvas
 
  | topRow bottomRow |
+ self backgroundColor ifNotNil: [:color |
+ aCanvas fillRectangle: (self topLeft corner: self right @ ((self owner ifNil: [self]) bottom)) color: color].
+
  self getListSize = 0 ifTrue: [ ^self ].
 
  self drawPreSelectionOn: aCanvas.
 
  topRow := self topVisibleRowForCanvas: aCanvas.
  bottomRow := self bottomVisibleRowForCanvas: aCanvas.
 
  "Draw multi-selection."
  self listSource hasMultiSelection ifTrue: [
  topRow to: bottomRow do: [ :row |
  (self 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: [
  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 ].!

Item was added:
+ ----- Method: LazyListMorph>>showFilter (in category 'accessing') -----
+ showFilter
+
+ ^ (showFilter ~~ false and: [listSource filterableList])!

Item was added:
+ ----- Method: LazyListMorph>>showFilter: (in category 'accessing') -----
+ showFilter: aBoolean
+
+ showFilter = aBoolean ifTrue: [^ self].
+ showFilter := aBoolean.
+ self changed.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterColumnColor (in category 'filtering') -----
+ filterColumnColor
+
+ ^ (Color gray: 0.85) alpha: 0.4!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterColumnIndex (in category 'filtering') -----
+ filterColumnIndex
+ "Which column to apply the filter to?"
+
+ | i |
+ i := 0.
+ self listMorphs
+ detect: [:m | i := i + 1. m backgroundColor notNil]
+ ifNone: [i := 0].
+ ^ i!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterList:columnIndex:matching: (in category 'filtering') -----
+ filterList: columns columnIndex: index 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 := self
+ filterListItem: ((columns at: index) at: rowIndex)
+ matching: aPattern.
+ foundPos = 1
+ 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: [foundPos > 1 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 changed:
  ----- 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].
 
+ "Enable column-specific filtering."
+ self filterColumnIndex in: [:index |
+ index > 0 ifTrue: [^ self filterList: columns columnIndex: index matching: aPattern]].
+
  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>>highlightNextColumn (in category 'filtering') -----
+ highlightNextColumn
+
+ | i currentColumn nextColumn |
+ i := self filterColumnIndex.
+ i = 0 ifTrue: [self listMorphs do: [:m | m showFilter: false]].
+
+ currentColumn := self listMorphs at: (i max: 1).
+ nextColumn := self listMorphs at: i \\ self listMorphs size + 1.
+
+ currentColumn
+ showFilter: false;
+ backgroundColor: nil.
+
+ nextColumn
+ showFilter: true;
+ backgroundColor: self filterColumnColor.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>highlightNoColumn (in category 'filtering') -----
+ highlightNoColumn
+
+ self listMorphs do: [:m |
+ m showFilter: true; backgroundColor: nil].!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>removeFilter (in category 'filtering') -----
+ removeFilter
+
+ self highlightNoColumn.
+ super removeFilter.
+ !

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>specialKeyPressed: (in category 'filtering') -----
+ specialKeyPressed: asciiValue
+ "Use the [Tab] key to filter specific columns."
+
+ ^ asciiValue = Character tab asciiValue
+ ifTrue: [self highlightNextColumn]
+ ifFalse: [super specialKeyPressed: asciiValue].!

Item was changed:
  ----- Method: PluggableMultiColumnListMorph>>updateColumns (in category 'updating') -----
  updateColumns
  "The number of columns must match the number of list morphs."
 
  | columnsChanged |
  columnsChanged := self columnCount ~= listMorphs size.
 
  [self columnCount < listMorphs size]
  whileTrue: [
  listMorphs removeLast delete].
 
  [self columnCount > listMorphs size]
  whileTrue: [
  listMorphs addLast: self createListMorph.
  self scroller addMorphBack: listMorphs last].
 
  listMorphs doWithIndex: [:listMorph :columnIndex |
  listMorph
  columnIndex: columnIndex;
+ color: self textColor;
  cellPositioning: (self cellPositioningAtColumn: columnIndex);
  cellInset: (self cellInsetAtColumn: columnIndex);
  hResizing: (self hResizingAtColumn: columnIndex);
  spaceFillWeight: (self spaceFillWeightAtColumn: columnIndex)].
 
  columnsChanged ifTrue: [self setListParameters].!


Reply | Threaded
Open this post in threaded view
|

Re: The Trunk: Morphic-mt.1582.mcz

marcel.taeumel

Best,
Marcel

Am 23.10.2019 15:31:47 schrieb [hidden email] <[hidden email]>:

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

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

Name: Morphic-mt.1582
Author: mt
Time: 23 October 2019, 3:31:24.67861 pm
UUID: 82af72b9-4198-4bd2-8e7e-e8733d9018c7
Ancestors: Morphic-mt.1581

Adds things for column-specific list filtering:

- backgroundColor for LazyListMorph
- optional filter-term indication for LazyListMorph
- column highlights for multi-column lists
- Character tab to cycle between columns

Note that I opted to not use the [space] key for column toggling because (a) we have to widget-focus cycling via [tab] at the moment and (b) the space character might be a valuable filter term.

=============== Diff against Morphic-mt.1581 ===============

Item was changed:
Morph subclass: #LazyListMorph
+ instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex iconExtent backgroundColor showFilter'
- instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex iconExtent'
classVariableNames: ''
poolDictionaries: ''
category: 'Morphic-Widgets'!

!LazyListMorph commentStamp: 'mt 10/13/2019 19:44' 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]!

Item was added:
+ ----- Method: LazyListMorph>>backgroundColor (in category 'accessing') -----
+ backgroundColor
+ "Since #color is this morph's default text color, this extra property is used for the actual background color. Supports nil."
+
+ ^ backgroundColor!

Item was added:
+ ----- Method: LazyListMorph>>backgroundColor: (in category 'accessing') -----
+ backgroundColor: aColor
+
+ backgroundColor = aColor ifTrue: [^ self].
+ backgroundColor := aColor.
+
+ self changed.
+ "Invalidate owner because we want to fill the vertical axis in the viewport entirely."
+ self owner ifNotNil: [:o | o changed].!

Item was changed:
----- Method: LazyListMorph>>displayFilterOn:for:in:font: (in category 'drawing') -----
displayFilterOn: canvas for: row in: drawBounds font: font
"Draw filter matches if any."

| fillStyle fillHeight |
+ self showFilter ifFalse: [^ self].
- listSource filterableList ifFalse: [^ self].

fillHeight := font height.
fillStyle := self filterColor isColor
ifTrue: [SolidFillStyle color: self filterColor]
ifFalse: [self filterColor].
fillStyle isGradientFill ifTrue: [
fillStyle origin: drawBounds topLeft.
fillStyle direction: 0@ fillHeight].

(self filterOffsets: row) do: [:offset |
| highlightRectangle |
highlightRectangle := ((drawBounds left + offset first first) @ drawBounds top
corner: (drawBounds left + offset first last) @ (drawBounds top + fillHeight)).
canvas
frameAndFillRoundRect: (highlightRectangle outsetBy: 1@0)
radius: (3 * RealEstateAgent scaleFactor) truncated
fillStyle: fillStyle
borderWidth: (1 * RealEstateAgent scaleFactor) truncated
borderColor: fillStyle asColor twiceDarker.
canvas
drawString: offset second
in: highlightRectangle
font: font
color: self filterTextColor].!

Item was changed:
----- Method: LazyListMorph>>drawOn: (in category 'drawing') -----
drawOn: aCanvas

| topRow bottomRow |
+ self backgroundColor ifNotNil: [:color |
+ aCanvas fillRectangle: (self topLeft corner: self right @ ((self owner ifNil: [self]) bottom)) color: color].
+
self getListSize = 0 ifTrue: [ ^self ].

self drawPreSelectionOn: aCanvas.

topRow := self topVisibleRowForCanvas: aCanvas.
bottomRow := self bottomVisibleRowForCanvas: aCanvas.

"Draw multi-selection."
self listSource hasMultiSelection ifTrue: [
topRow to: bottomRow do: [ :row |
(self 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: [
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 ].!

Item was added:
+ ----- Method: LazyListMorph>>showFilter (in category 'accessing') -----
+ showFilter
+
+ ^ (showFilter ~~ false and: [listSource filterableList])!

Item was added:
+ ----- Method: LazyListMorph>>showFilter: (in category 'accessing') -----
+ showFilter: aBoolean
+
+ showFilter = aBoolean ifTrue: [^ self].
+ showFilter := aBoolean.
+ self changed.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterColumnColor (in category 'filtering') -----
+ filterColumnColor
+
+ ^ (Color gray: 0.85) alpha: 0.4!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterColumnIndex (in category 'filtering') -----
+ filterColumnIndex
+ "Which column to apply the filter to?"
+
+ | i |
+ i := 0.
+ self listMorphs
+ detect: [:m | i := i + 1. m backgroundColor notNil]
+ ifNone: [i := 0].
+ ^ i!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterList:columnIndex:matching: (in category 'filtering') -----
+ filterList: columns columnIndex: index 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 := self
+ filterListItem: ((columns at: index) at: rowIndex)
+ matching: aPattern.
+ foundPos = 1
+ 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: [foundPos > 1 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 changed:
----- 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].

+ "Enable column-specific filtering."
+ self filterColumnIndex in: [:index |
+ index > 0 ifTrue: [^ self filterList: columns columnIndex: index matching: aPattern]].
+
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>>highlightNextColumn (in category 'filtering') -----
+ highlightNextColumn
+
+ | i currentColumn nextColumn |
+ i := self filterColumnIndex.
+ i = 0 ifTrue: [self listMorphs do: [:m | m showFilter: false]].
+
+ currentColumn := self listMorphs at: (i max: 1).
+ nextColumn := self listMorphs at: i \\ self listMorphs size + 1.
+
+ currentColumn
+ showFilter: false;
+ backgroundColor: nil.
+
+ nextColumn
+ showFilter: true;
+ backgroundColor: self filterColumnColor.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>highlightNoColumn (in category 'filtering') -----
+ highlightNoColumn
+
+ self listMorphs do: [:m |
+ m showFilter: true; backgroundColor: nil].!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>removeFilter (in category 'filtering') -----
+ removeFilter
+
+ self highlightNoColumn.
+ super removeFilter.
+ !

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>specialKeyPressed: (in category 'filtering') -----
+ specialKeyPressed: asciiValue
+ "Use the [Tab] key to filter specific columns."
+
+ ^ asciiValue = Character tab asciiValue
+ ifTrue: [self highlightNextColumn]
+ ifFalse: [super specialKeyPressed: asciiValue].!

Item was changed:
----- Method: PluggableMultiColumnListMorph>>updateColumns (in category 'updating') -----
updateColumns
"The number of columns must match the number of list morphs."

| columnsChanged |
columnsChanged := self columnCount ~= listMorphs size.

[self columnCount < listmorphs="">
whileTrue: [
listMorphs removeLast delete].

[self columnCount > listMorphs size]
whileTrue: [
listMorphs addLast: self createListMorph.
self scroller addMorphBack: listMorphs last].

listMorphs doWithIndex: [:listMorph :columnIndex |
listMorph
columnIndex: columnIndex;
+ color: self textColor;
cellPositioning: (self cellPositioningAtColumn: columnIndex);
cellInset: (self cellInsetAtColumn: columnIndex);
hResizing: (self hResizingAtColumn: columnIndex);
spaceFillWeight: (self spaceFillWeightAtColumn: columnIndex)].

columnsChanged ifTrue: [self setListParameters].!




Reply | Threaded
Open this post in threaded view
|

Re: The Trunk: Morphic-mt.1582.mcz

Chris Muller-3
In reply to this post by commits-2
Very nice, looking forward to trying it out.

Thanks!

On Wed, Oct 23, 2019 at 8:31 AM <[hidden email]> wrote:
Marcel Taeumel uploaded a new version of Morphic to project The Trunk:
http://source.squeak.org/trunk/Morphic-mt.1582.mcz

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

Name: Morphic-mt.1582
Author: mt
Time: 23 October 2019, 3:31:24.67861 pm
UUID: 82af72b9-4198-4bd2-8e7e-e8733d9018c7
Ancestors: Morphic-mt.1581

Adds things for column-specific list filtering:

- backgroundColor for LazyListMorph
- optional filter-term indication for LazyListMorph
- column highlights for multi-column lists
- Character tab to cycle between columns

Note that I opted to not use the [space] key for column toggling because (a) we have to widget-focus cycling via [tab] at the moment and (b) the space character might be a valuable filter term.

=============== Diff against Morphic-mt.1581 ===============

Item was changed:
  Morph subclass: #LazyListMorph
+       instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex iconExtent backgroundColor showFilter'
-       instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex iconExtent'
        classVariableNames: ''
        poolDictionaries: ''
        category: 'Morphic-Widgets'!

  !LazyListMorph commentStamp: 'mt 10/13/2019 19:44' 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]!

Item was added:
+ ----- Method: LazyListMorph>>backgroundColor (in category 'accessing') -----
+ backgroundColor
+       "Since #color is this morph's default text color, this extra property is used for the actual background color. Supports nil."
+       
+       ^ backgroundColor!

Item was added:
+ ----- Method: LazyListMorph>>backgroundColor: (in category 'accessing') -----
+ backgroundColor: aColor
+
+       backgroundColor = aColor ifTrue: [^ self].
+       backgroundColor := aColor.
+
+       self changed.
+       "Invalidate owner because we want to fill the vertical axis in the viewport entirely."
+       self owner ifNotNil: [:o | o changed].!

Item was changed:
  ----- Method: LazyListMorph>>displayFilterOn:for:in:font: (in category 'drawing') -----
  displayFilterOn: canvas for: row in: drawBounds font: font
        "Draw filter matches if any."

        | fillStyle fillHeight |
+       self showFilter ifFalse: [^ self].
-       listSource filterableList ifFalse: [^ self].

        fillHeight := font height.
        fillStyle := self filterColor isColor
                ifTrue: [SolidFillStyle color: self filterColor]
                ifFalse: [self filterColor].
        fillStyle isGradientFill ifTrue: [
                fillStyle origin: drawBounds topLeft.
                fillStyle direction: 0@ fillHeight].

        (self filterOffsets: row) do: [:offset |
                | highlightRectangle |
                highlightRectangle := ((drawBounds left + offset first first) @ drawBounds top
                        corner: (drawBounds left + offset first last) @ (drawBounds top + fillHeight)).
                canvas
                        frameAndFillRoundRect: (highlightRectangle outsetBy: 1@0)
                        radius: (3 * RealEstateAgent scaleFactor) truncated
                        fillStyle: fillStyle
                        borderWidth: (1 * RealEstateAgent scaleFactor) truncated
                        borderColor: fillStyle asColor twiceDarker.
                canvas
                        drawString: offset second
                        in: highlightRectangle
                        font: font
                        color: self filterTextColor].!

Item was changed:
  ----- Method: LazyListMorph>>drawOn: (in category 'drawing') -----
  drawOn: aCanvas

        | topRow bottomRow |
+       self backgroundColor ifNotNil: [:color |
+               aCanvas fillRectangle: (self topLeft corner: self right @ ((self owner ifNil: [self]) bottom)) color: color].
+
        self getListSize = 0 ifTrue: [ ^self ].

        self drawPreSelectionOn: aCanvas.

        topRow := self topVisibleRowForCanvas: aCanvas.
        bottomRow := self bottomVisibleRowForCanvas: aCanvas.

        "Draw multi-selection."
        self listSource hasMultiSelection ifTrue: [
                topRow to: bottomRow do: [ :row |
                        (self 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: [
                        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 ].!

Item was added:
+ ----- Method: LazyListMorph>>showFilter (in category 'accessing') -----
+ showFilter
+
+       ^ (showFilter ~~ false and: [listSource filterableList])!

Item was added:
+ ----- Method: LazyListMorph>>showFilter: (in category 'accessing') -----
+ showFilter: aBoolean
+
+       showFilter = aBoolean ifTrue: [^ self].
+       showFilter := aBoolean.
+       self changed.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterColumnColor (in category 'filtering') -----
+ filterColumnColor
+
+       ^ (Color gray: 0.85) alpha: 0.4!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterColumnIndex (in category 'filtering') -----
+ filterColumnIndex
+       "Which column to apply the filter to?"
+
+       | i |
+       i := 0.
+       self listMorphs
+               detect: [:m | i := i + 1. m backgroundColor notNil]
+               ifNone: [i := 0].
+       ^ i!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterList:columnIndex:matching: (in category 'filtering') -----
+ filterList: columns columnIndex: index 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 := self
+                                               filterListItem: ((columns at: index) at: rowIndex)
+                                               matching: aPattern.
+               foundPos = 1
+                       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: [foundPos > 1 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 changed:
  ----- 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].

+       "Enable column-specific filtering."
+       self filterColumnIndex in: [:index |
+               index > 0 ifTrue: [^ self filterList: columns columnIndex: index matching: aPattern]].
+       
        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>>highlightNextColumn (in category 'filtering') -----
+ highlightNextColumn
+
+       | i currentColumn nextColumn |
+       i := self filterColumnIndex.
+       i = 0 ifTrue: [self listMorphs do: [:m | m showFilter: false]].
+
+       currentColumn := self listMorphs at: (i max: 1).
+       nextColumn := self listMorphs at: i \\ self listMorphs size + 1.
+       
+       currentColumn
+               showFilter: false;
+               backgroundColor: nil.
+               
+       nextColumn
+               showFilter: true;
+               backgroundColor: self filterColumnColor.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>highlightNoColumn (in category 'filtering') -----
+ highlightNoColumn
+
+       self listMorphs do: [:m |
+               m showFilter: true; backgroundColor: nil].!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>removeFilter (in category 'filtering') -----
+ removeFilter
+
+       self highlightNoColumn.
+       super removeFilter.
+ !

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>specialKeyPressed: (in category 'filtering') -----
+ specialKeyPressed: asciiValue
+       "Use the [Tab] key to filter specific columns."
+       
+       ^ asciiValue = Character tab asciiValue
+               ifTrue: [self highlightNextColumn]
+               ifFalse: [super specialKeyPressed: asciiValue].!

Item was changed:
  ----- Method: PluggableMultiColumnListMorph>>updateColumns (in category 'updating') -----
  updateColumns
        "The number of columns must match the number of list morphs."

        | columnsChanged |
        columnsChanged := self columnCount ~= listMorphs size.

        [self columnCount < listMorphs size]
                whileTrue: [
                        listMorphs removeLast delete].

        [self columnCount > listMorphs size]
                whileTrue: [
                        listMorphs addLast: self createListMorph.
                        self scroller addMorphBack: listMorphs last].

        listMorphs doWithIndex: [:listMorph :columnIndex |
                listMorph
                        columnIndex: columnIndex;
+                       color: self textColor;
                        cellPositioning: (self cellPositioningAtColumn: columnIndex);
                        cellInset: (self cellInsetAtColumn: columnIndex);
                        hResizing: (self hResizingAtColumn: columnIndex);
                        spaceFillWeight: (self spaceFillWeightAtColumn: columnIndex)].

        columnsChanged ifTrue: [self setListParameters].!




Reply | Threaded
Open this post in threaded view
|

Re: The Trunk: Morphic-mt.1582.mcz

Christoph Thiede

Looks great! Some ideas:


- I find it confusing that filters from columns that are not currently selected are not displayed, but still active. In this example, I filtered the left column for "tex" and then selected the right one. Now there is no visual indication of the left filter:




Would it be possible to display such filters as by giving them a grey background color?


- If I pressed Tab again on the screenshot above, I would like no column to be selected again. Why not implement tab cycling along the series "all columns, col1, col2, ..., coln, all columns"?

- Wouldn't it be more convenient if you could also use the cursor to focus a column? :)


Best,

Christoph


Von: Squeak-dev <[hidden email]> im Auftrag von Chris Muller <[hidden email]>
Gesendet: Freitag, 25. Oktober 2019 01:36:31
An: squeak dev
Cc: [hidden email]
Betreff: Re: [squeak-dev] The Trunk: Morphic-mt.1582.mcz
 
Very nice, looking forward to trying it out.

Thanks!

On Wed, Oct 23, 2019 at 8:31 AM <[hidden email]> wrote:
Marcel Taeumel uploaded a new version of Morphic to project The Trunk:
http://source.squeak.org/trunk/Morphic-mt.1582.mcz

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

Name: Morphic-mt.1582
Author: mt
Time: 23 October 2019, 3:31:24.67861 pm
UUID: 82af72b9-4198-4bd2-8e7e-e8733d9018c7
Ancestors: Morphic-mt.1581

Adds things for column-specific list filtering:

- backgroundColor for LazyListMorph
- optional filter-term indication for LazyListMorph
- column highlights for multi-column lists
- Character tab to cycle between columns

Note that I opted to not use the [space] key for column toggling because (a) we have to widget-focus cycling via [tab] at the moment and (b) the space character might be a valuable filter term.

=============== Diff against Morphic-mt.1581 ===============

Item was changed:
  Morph subclass: #LazyListMorph
+       instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex iconExtent backgroundColor showFilter'
-       instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex iconExtent'
        classVariableNames: ''
        poolDictionaries: ''
        category: 'Morphic-Widgets'!

  !LazyListMorph commentStamp: 'mt 10/13/2019 19:44' 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]!

Item was added:
+ ----- Method: LazyListMorph>>backgroundColor (in category 'accessing') -----
+ backgroundColor
+       "Since #color is this morph's default text color, this extra property is used for the actual background color. Supports nil."
+       
+       ^ backgroundColor!

Item was added:
+ ----- Method: LazyListMorph>>backgroundColor: (in category 'accessing') -----
+ backgroundColor: aColor
+
+       backgroundColor = aColor ifTrue: [^ self].
+       backgroundColor := aColor.
+
+       self changed.
+       "Invalidate owner because we want to fill the vertical axis in the viewport entirely."
+       self owner ifNotNil: [:o | o changed].!

Item was changed:
  ----- Method: LazyListMorph>>displayFilterOn:for:in:font: (in category 'drawing') -----
  displayFilterOn: canvas for: row in: drawBounds font: font
        "Draw filter matches if any."

        | fillStyle fillHeight |
+       self showFilter ifFalse: [^ self].
-       listSource filterableList ifFalse: [^ self].

        fillHeight := font height.
        fillStyle := self filterColor isColor
                ifTrue: [SolidFillStyle color: self filterColor]
                ifFalse: [self filterColor].
        fillStyle isGradientFill ifTrue: [
                fillStyle origin: drawBounds topLeft.
                fillStyle direction: 0@ fillHeight].

        (self filterOffsets: row) do: [:offset |
                | highlightRectangle |
                highlightRectangle := ((drawBounds left + offset first first) @ drawBounds top
                        corner: (drawBounds left + offset first last) @ (drawBounds top + fillHeight)).
                canvas
                        frameAndFillRoundRect: (highlightRectangle outsetBy: 1@0)
                        radius: (3 * RealEstateAgent scaleFactor) truncated
                        fillStyle: fillStyle
                        borderWidth: (1 * RealEstateAgent scaleFactor) truncated
                        borderColor: fillStyle asColor twiceDarker.
                canvas
                        drawString: offset second
                        in: highlightRectangle
                        font: font
                        color: self filterTextColor].!

Item was changed:
  ----- Method: LazyListMorph>>drawOn: (in category 'drawing') -----
  drawOn: aCanvas

        | topRow bottomRow |
+       self backgroundColor ifNotNil: [:color |
+               aCanvas fillRectangle: (self topLeft corner: self right @ ((self owner ifNil: [self]) bottom)) color: color].
+
        self getListSize = 0 ifTrue: [ ^self ].

        self drawPreSelectionOn: aCanvas.

        topRow := self topVisibleRowForCanvas: aCanvas.
        bottomRow := self bottomVisibleRowForCanvas: aCanvas.

        "Draw multi-selection."
        self listSource hasMultiSelection ifTrue: [
                topRow to: bottomRow do: [ :row |
                        (self 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: [
                        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 ].!

Item was added:
+ ----- Method: LazyListMorph>>showFilter (in category 'accessing') -----
+ showFilter
+
+       ^ (showFilter ~~ false and: [listSource filterableList])!

Item was added:
+ ----- Method: LazyListMorph>>showFilter: (in category 'accessing') -----
+ showFilter: aBoolean
+
+       showFilter = aBoolean ifTrue: [^ self].
+       showFilter := aBoolean.
+       self changed.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterColumnColor (in category 'filtering') -----
+ filterColumnColor
+
+       ^ (Color gray: 0.85) alpha: 0.4!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterColumnIndex (in category 'filtering') -----
+ filterColumnIndex
+       "Which column to apply the filter to?"
+
+       | i |
+       i := 0.
+       self listMorphs
+               detect: [:m | i := i + 1. m backgroundColor notNil]
+               ifNone: [i := 0].
+       ^ i!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterList:columnIndex:matching: (in category 'filtering') -----
+ filterList: columns columnIndex: index 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 := self
+                                               filterListItem: ((columns at: index) at: rowIndex)
+                                               matching: aPattern.
+               foundPos = 1
+                       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: [foundPos > 1 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 changed:
  ----- 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].

+       "Enable column-specific filtering."
+       self filterColumnIndex in: [:index |
+               index > 0 ifTrue: [^ self filterList: columns columnIndex: index matching: aPattern]].
+       
        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>>highlightNextColumn (in category 'filtering') -----
+ highlightNextColumn
+
+       | i currentColumn nextColumn |
+       i := self filterColumnIndex.
+       i = 0 ifTrue: [self listMorphs do: [:m | m showFilter: false]].
+
+       currentColumn := self listMorphs at: (i max: 1).
+       nextColumn := self listMorphs at: i \\ self listMorphs size + 1.
+       
+       currentColumn
+               showFilter: false;
+               backgroundColor: nil.
+               
+       nextColumn
+               showFilter: true;
+               backgroundColor: self filterColumnColor.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>highlightNoColumn (in category 'filtering') -----
+ highlightNoColumn
+
+       self listMorphs do: [:m |
+               m showFilter: true; backgroundColor: nil].!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>removeFilter (in category 'filtering') -----
+ removeFilter
+
+       self highlightNoColumn.
+       super removeFilter.
+ !

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>specialKeyPressed: (in category 'filtering') -----
+ specialKeyPressed: asciiValue
+       "Use the [Tab] key to filter specific columns."
+       
+       ^ asciiValue = Character tab asciiValue
+               ifTrue: [self highlightNextColumn]
+               ifFalse: [super specialKeyPressed: asciiValue].!

Item was changed:
  ----- Method: PluggableMultiColumnListMorph>>updateColumns (in category 'updating') -----
  updateColumns
        "The number of columns must match the number of list morphs."

        | columnsChanged |
        columnsChanged := self columnCount ~= listMorphs size.

        [self columnCount < listMorphs size]
                whileTrue: [
                        listMorphs removeLast delete].

        [self columnCount > listMorphs size]
                whileTrue: [
                        listMorphs addLast: self createListMorph.
                        self scroller addMorphBack: listMorphs last].

        listMorphs doWithIndex: [:listMorph :columnIndex |
                listMorph
                        columnIndex: columnIndex;
+                       color: self textColor;
                        cellPositioning: (self cellPositioningAtColumn: columnIndex);
                        cellInset: (self cellInsetAtColumn: columnIndex);
                        hResizing: (self hResizingAtColumn: columnIndex);
                        spaceFillWeight: (self spaceFillWeightAtColumn: columnIndex)].

        columnsChanged ifTrue: [self setListParameters].!




Reply | Threaded
Open this post in threaded view
|

Re: The Trunk: Morphic-mt.1582.mcz

marcel.taeumel
Hi Christoph.

I find it confusing that filters from columns that are not currently selected are not displayed, but still active.

Columns have no filter (term). The entire list has the filter. Your notion of a column "being active" is interesting. The filter changes only when typing letters. The user can hit tab to either configure that next filter action or look at the matching terms in a specific column. There is no new "being active" mode. It's still just "all items" or "filtered items". It could well be that two different columns would yield the same filter result.

Hmm... I would rather not "over-design" that feature. :-) At the moment, column cycling does not reset the filter and the active column will show the matching items. A simple extension would be to always show the matching terms for all columns. I disabled that to make the active column pop out a little bit more.

Would it be possible to display such filters as by giving them a grey background color?

That would require a new color (or property) to be spread across all existing UI themes. I suppose. While that would be possible, I don't think it would improve things substantially. Just my two cents. :-)

Wouldn't it be more convenient if you could also use the cursor to focus a column? :)

I suppose not. You are talking about actual selection. There is no actual selection of the column. Let's better not mix those things up unless we can really provide column selection for the model. So make it visually and interactively different from actual selection. For now. :-)

It's no spreadsheet. I suppose the number of columns is usually small. Cycle-through should work.

 I would like no column to be selected again.

Sounds like a good idea. Would you also reset the current filter if you cycle through columns?

Looks great! Some ideas.

Keep them coming. :-) Watch out for trade-offs vs. coherent/sound new features. Column filtering is definitely a trade-off not to be pushed too far. Try looking at other widget libraries in other systems to learn about what users expect these days...

Best,
Marcel

Am 02.11.2019 00:43:13 schrieb Thiede, Christoph <[hidden email]>:

Looks great! Some ideas:


- I find it confusing that filters from columns that are not currently selected are not displayed, but still active. In this example, I filtered the left column for "tex" and then selected the right one. Now there is no visual indication of the left filter:




Would it be possible to display such filters as by giving them a grey background color?


- If I pressed Tab again on the screenshot above, I would like no column to be selected again. Why not implement tab cycling along the series "all columns, col1, col2, ..., coln, all columns"?

- Wouldn't it be more convenient if you could also use the cursor to focus a column? :)


Best,

Christoph


Von: Squeak-dev <[hidden email]> im Auftrag von Chris Muller <[hidden email]>
Gesendet: Freitag, 25. Oktober 2019 01:36:31
An: squeak dev
Cc: [hidden email]
Betreff: Re: [squeak-dev] The Trunk: Morphic-mt.1582.mcz
 
Very nice, looking forward to trying it out.

Thanks!

On Wed, Oct 23, 2019 at 8:31 AM <[hidden email]> wrote:
Marcel Taeumel uploaded a new version of Morphic to project The Trunk:
http://source.squeak.org/trunk/Morphic-mt.1582.mcz

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

Name: Morphic-mt.1582
Author: mt
Time: 23 October 2019, 3:31:24.67861 pm
UUID: 82af72b9-4198-4bd2-8e7e-e8733d9018c7
Ancestors: Morphic-mt.1581

Adds things for column-specific list filtering:

- backgroundColor for LazyListMorph
- optional filter-term indication for LazyListMorph
- column highlights for multi-column lists
- Character tab to cycle between columns

Note that I opted to not use the [space] key for column toggling because (a) we have to widget-focus cycling via [tab] at the moment and (b) the space character might be a valuable filter term.

=============== Diff against Morphic-mt.1581 ===============

Item was changed:
  Morph subclass: #LazyListMorph
+       instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex iconExtent backgroundColor showFilter'
-       instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex iconExtent'
        classVariableNames: ''
        poolDictionaries: ''
        category: 'Morphic-Widgets'!

  !LazyListMorph commentStamp: 'mt 10/13/2019 19:44' 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]!

Item was added:
+ ----- Method: LazyListMorph>>backgroundColor (in category 'accessing') -----
+ backgroundColor
+       "Since #color is this morph's default text color, this extra property is used for the actual background color. Supports nil."
+       
+       ^ backgroundColor!

Item was added:
+ ----- Method: LazyListMorph>>backgroundColor: (in category 'accessing') -----
+ backgroundColor: aColor
+
+       backgroundColor = aColor ifTrue: [^ self].
+       backgroundColor := aColor.
+
+       self changed.
+       "Invalidate owner because we want to fill the vertical axis in the viewport entirely."
+       self owner ifNotNil: [:o | o changed].!

Item was changed:
  ----- Method: LazyListMorph>>displayFilterOn:for:in:font: (in category 'drawing') -----
  displayFilterOn: canvas for: row in: drawBounds font: font
        "Draw filter matches if any."

        | fillStyle fillHeight |
+       self showFilter ifFalse: [^ self].
-       listSource filterableList ifFalse: [^ self].

        fillHeight := font height.
        fillStyle := self filterColor isColor
                ifTrue: [SolidFillStyle color: self filterColor]
                ifFalse: [self filterColor].
        fillStyle isGradientFill ifTrue: [
                fillStyle origin: drawBounds topLeft.
                fillStyle direction: 0@ fillHeight].

        (self filterOffsets: row) do: [:offset |
                | highlightRectangle |
                highlightRectangle := ((drawBounds left + offset first first) @ drawBounds top
                        corner: (drawBounds left + offset first last) @ (drawBounds top + fillHeight)).
                canvas
                        frameAndFillRoundRect: (highlightRectangle outsetBy: 1@0)
                        radius: (3 * RealEstateAgent scaleFactor) truncated
                        fillStyle: fillStyle
                        borderWidth: (1 * RealEstateAgent scaleFactor) truncated
                        borderColor: fillStyle asColor twiceDarker.
                canvas
                        drawString: offset second
                        in: highlightRectangle
                        font: font
                        color: self filterTextColor].!

Item was changed:
  ----- Method: LazyListMorph>>drawOn: (in category 'drawing') -----
  drawOn: aCanvas

        | topRow bottomRow |
+       self backgroundColor ifNotNil: [:color |
+               aCanvas fillRectangle: (self topLeft corner: self right @ ((self owner ifNil: [self]) bottom)) color: color].
+
        self getListSize = 0 ifTrue: [ ^self ].

        self drawPreSelectionOn: aCanvas.

        topRow := self topVisibleRowForCanvas: aCanvas.
        bottomRow := self bottomVisibleRowForCanvas: aCanvas.

        "Draw multi-selection."
        self listSource hasMultiSelection ifTrue: [
                topRow to: bottomRow do: [ :row |
                        (self 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: [
                        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 ].!

Item was added:
+ ----- Method: LazyListMorph>>showFilter (in category 'accessing') -----
+ showFilter
+
+       ^ (showFilter ~~ false and: [listSource filterableList])!

Item was added:
+ ----- Method: LazyListMorph>>showFilter: (in category 'accessing') -----
+ showFilter: aBoolean
+
+       showFilter = aBoolean ifTrue: [^ self].
+       showFilter := aBoolean.
+       self changed.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterColumnColor (in category 'filtering') -----
+ filterColumnColor
+
+       ^ (Color gray: 0.85) alpha: 0.4!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterColumnIndex (in category 'filtering') -----
+ filterColumnIndex
+       "Which column to apply the filter to?"
+
+       | i |
+       i := 0.
+       self listMorphs
+               detect: [:m | i := i + 1. m backgroundColor notNil]
+               ifNone: [i := 0].
+       ^ i!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterList:columnIndex:matching: (in category 'filtering') -----
+ filterList: columns columnIndex: index 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 := self
+                                               filterListItem: ((columns at: index) at: rowIndex)
+                                               matching: aPattern.
+               foundPos = 1
+                       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: [foundPos > 1 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 changed:
  ----- 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].

+       "Enable column-specific filtering."
+       self filterColumnIndex in: [:index |
+               index > 0 ifTrue: [^ self filterList: columns columnIndex: index matching: aPattern]].
+       
        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>>highlightNextColumn (in category 'filtering') -----
+ highlightNextColumn
+
+       | i currentColumn nextColumn |
+       i := self filterColumnIndex.
+       i = 0 ifTrue: [self listMorphs do: [:m | m showFilter: false]].
+
+       currentColumn := self listMorphs at: (i max: 1).
+       nextColumn := self listMorphs at: i \\ self listMorphs size + 1.
+       
+       currentColumn
+               showFilter: false;
+               backgroundColor: nil.
+               
+       nextColumn
+               showFilter: true;
+               backgroundColor: self filterColumnColor.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>highlightNoColumn (in category 'filtering') -----
+ highlightNoColumn
+
+       self listMorphs do: [:m |
+               m showFilter: true; backgroundColor: nil].!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>removeFilter (in category 'filtering') -----
+ removeFilter
+
+       self highlightNoColumn.
+       super removeFilter.
+ !

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>specialKeyPressed: (in category 'filtering') -----
+ specialKeyPressed: asciiValue
+       "Use the [Tab] key to filter specific columns."
+       
+       ^ asciiValue = Character tab asciiValue
+               ifTrue: [self highlightNextColumn]
+               ifFalse: [super specialKeyPressed: asciiValue].!

Item was changed:
  ----- Method: PluggableMultiColumnListMorph>>updateColumns (in category 'updating') -----
  updateColumns
        "The number of columns must match the number of list morphs."

        | columnsChanged |
        columnsChanged := self columnCount ~= listMorphs size.

        [self columnCount < listMorphs size]
                whileTrue: [
                        listMorphs removeLast delete].

        [self columnCount > listMorphs size]
                whileTrue: [
                        listMorphs addLast: self createListMorph.
                        self scroller addMorphBack: listMorphs last].

        listMorphs doWithIndex: [:listMorph :columnIndex |
                listMorph
                        columnIndex: columnIndex;
+                       color: self textColor;
                        cellPositioning: (self cellPositioningAtColumn: columnIndex);
                        cellInset: (self cellInsetAtColumn: columnIndex);
                        hResizing: (self hResizingAtColumn: columnIndex);
                        spaceFillWeight: (self spaceFillWeightAtColumn: columnIndex)].

        columnsChanged ifTrue: [self setListParameters].!




Reply | Threaded
Open this post in threaded view
|

Re: The Trunk: Morphic-mt.1582.mcz

Christoph Thiede

Hi Marcel,


yes, you're absolutely right, over-designing should be strictly avoided :)


I admit I didn't yet study your whole code, so my comments only represent opinions from an end-user. From my point of view, it is just a bit weird that the list contains filters you cannot see directly. Maybe it would be better to reset the filter if the column selection was changed? Or alternatively, directly apply the latest filter to the next selected column?


Try looking at other widget libraries in other systems to learn about what users expect these days...

Any special recommendations? :-) While Squeak does not always have the most fancy look, I believe I never met another UI that provides similar productivity ... :D

Best,
Christoph


Von: Squeak-dev <[hidden email]> im Auftrag von Taeumel, Marcel
Gesendet: Samstag, 2. November 2019 13:26:01
An: John Pfersich via Squeak-dev
Cc: [hidden email]
Betreff: Re: [squeak-dev] The Trunk: Morphic-mt.1582.mcz
 
Hi Christoph.

I find it confusing that filters from columns that are not currently selected are not displayed, but still active.

Columns have no filter (term). The entire list has the filter. Your notion of a column "being active" is interesting. The filter changes only when typing letters. The user can hit tab to either configure that next filter action or look at the matching terms in a specific column. There is no new "being active" mode. It's still just "all items" or "filtered items". It could well be that two different columns would yield the same filter result.

Hmm... I would rather not "over-design" that feature. :-) At the moment, column cycling does not reset the filter and the active column will show the matching items. A simple extension would be to always show the matching terms for all columns. I disabled that to make the active column pop out a little bit more.

Would it be possible to display such filters as by giving them a grey background color?

That would require a new color (or property) to be spread across all existing UI themes. I suppose. While that would be possible, I don't think it would improve things substantially. Just my two cents. :-)

Wouldn't it be more convenient if you could also use the cursor to focus a column? :)

I suppose not. You are talking about actual selection. There is no actual selection of the column. Let's better not mix those things up unless we can really provide column selection for the model. So make it visually and interactively different from actual selection. For now. :-)

It's no spreadsheet. I suppose the number of columns is usually small. Cycle-through should work.

 I would like no column to be selected again.

Sounds like a good idea. Would you also reset the current filter if you cycle through columns?

Looks great! Some ideas.

Keep them coming. :-) Watch out for trade-offs vs. coherent/sound new features. Column filtering is definitely a trade-off not to be pushed too far. Try looking at other widget libraries in other systems to learn about what users expect these days...

Best,
Marcel

Am 02.11.2019 00:43:13 schrieb Thiede, Christoph <[hidden email]>:

Looks great! Some ideas:


- I find it confusing that filters from columns that are not currently selected are not displayed, but still active. In this example, I filtered the left column for "tex" and then selected the right one. Now there is no visual indication of the left filter:




Would it be possible to display such filters as by giving them a grey background color?


- If I pressed Tab again on the screenshot above, I would like no column to be selected again. Why not implement tab cycling along the series "all columns, col1, col2, ..., coln, all columns"?

- Wouldn't it be more convenient if you could also use the cursor to focus a column? :)


Best,

Christoph


Von: Squeak-dev <[hidden email]> im Auftrag von Chris Muller <[hidden email]>
Gesendet: Freitag, 25. Oktober 2019 01:36:31
An: squeak dev
Cc: [hidden email]
Betreff: Re: [squeak-dev] The Trunk: Morphic-mt.1582.mcz
 
Very nice, looking forward to trying it out.

Thanks!

On Wed, Oct 23, 2019 at 8:31 AM <[hidden email]> wrote:
Marcel Taeumel uploaded a new version of Morphic to project The Trunk:
http://source.squeak.org/trunk/Morphic-mt.1582.mcz

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

Name: Morphic-mt.1582
Author: mt
Time: 23 October 2019, 3:31:24.67861 pm
UUID: 82af72b9-4198-4bd2-8e7e-e8733d9018c7
Ancestors: Morphic-mt.1581

Adds things for column-specific list filtering:

- backgroundColor for LazyListMorph
- optional filter-term indication for LazyListMorph
- column highlights for multi-column lists
- Character tab to cycle between columns

Note that I opted to not use the [space] key for column toggling because (a) we have to widget-focus cycling via [tab] at the moment and (b) the space character might be a valuable filter term.

=============== Diff against Morphic-mt.1581 ===============

Item was changed:
  Morph subclass: #LazyListMorph
+       instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex iconExtent backgroundColor showFilter'
-       instanceVariableNames: 'listItems listIcons listFilterOffsets font selectedRow selectedRows preSelectedRow listSource maxWidth columnIndex iconExtent'
        classVariableNames: ''
        poolDictionaries: ''
        category: 'Morphic-Widgets'!

  !LazyListMorph commentStamp: 'mt 10/13/2019 19:44' 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]!

Item was added:
+ ----- Method: LazyListMorph>>backgroundColor (in category 'accessing') -----
+ backgroundColor
+       "Since #color is this morph's default text color, this extra property is used for the actual background color. Supports nil."
+       
+       ^ backgroundColor!

Item was added:
+ ----- Method: LazyListMorph>>backgroundColor: (in category 'accessing') -----
+ backgroundColor: aColor
+
+       backgroundColor = aColor ifTrue: [^ self].
+       backgroundColor := aColor.
+
+       self changed.
+       "Invalidate owner because we want to fill the vertical axis in the viewport entirely."
+       self owner ifNotNil: [:o | o changed].!

Item was changed:
  ----- Method: LazyListMorph>>displayFilterOn:for:in:font: (in category 'drawing') -----
  displayFilterOn: canvas for: row in: drawBounds font: font
        "Draw filter matches if any."

        | fillStyle fillHeight |
+       self showFilter ifFalse: [^ self].
-       listSource filterableList ifFalse: [^ self].

        fillHeight := font height.
        fillStyle := self filterColor isColor
                ifTrue: [SolidFillStyle color: self filterColor]
                ifFalse: [self filterColor].
        fillStyle isGradientFill ifTrue: [
                fillStyle origin: drawBounds topLeft.
                fillStyle direction: 0@ fillHeight].

        (self filterOffsets: row) do: [:offset |
                | highlightRectangle |
                highlightRectangle := ((drawBounds left + offset first first) @ drawBounds top
                        corner: (drawBounds left + offset first last) @ (drawBounds top + fillHeight)).
                canvas
                        frameAndFillRoundRect: (highlightRectangle outsetBy: 1@0)
                        radius: (3 * RealEstateAgent scaleFactor) truncated
                        fillStyle: fillStyle
                        borderWidth: (1 * RealEstateAgent scaleFactor) truncated
                        borderColor: fillStyle asColor twiceDarker.
                canvas
                        drawString: offset second
                        in: highlightRectangle
                        font: font
                        color: self filterTextColor].!

Item was changed:
  ----- Method: LazyListMorph>>drawOn: (in category 'drawing') -----
  drawOn: aCanvas

        | topRow bottomRow |
+       self backgroundColor ifNotNil: [:color |
+               aCanvas fillRectangle: (self topLeft corner: self right @ ((self owner ifNil: [self]) bottom)) color: color].
+
        self getListSize = 0 ifTrue: [ ^self ].

        self drawPreSelectionOn: aCanvas.

        topRow := self topVisibleRowForCanvas: aCanvas.
        bottomRow := self bottomVisibleRowForCanvas: aCanvas.

        "Draw multi-selection."
        self listSource hasMultiSelection ifTrue: [
                topRow to: bottomRow do: [ :row |
                        (self 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: [
                        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 ].!

Item was added:
+ ----- Method: LazyListMorph>>showFilter (in category 'accessing') -----
+ showFilter
+
+       ^ (showFilter ~~ false and: [listSource filterableList])!

Item was added:
+ ----- Method: LazyListMorph>>showFilter: (in category 'accessing') -----
+ showFilter: aBoolean
+
+       showFilter = aBoolean ifTrue: [^ self].
+       showFilter := aBoolean.
+       self changed.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterColumnColor (in category 'filtering') -----
+ filterColumnColor
+
+       ^ (Color gray: 0.85) alpha: 0.4!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterColumnIndex (in category 'filtering') -----
+ filterColumnIndex
+       "Which column to apply the filter to?"
+
+       | i |
+       i := 0.
+       self listMorphs
+               detect: [:m | i := i + 1. m backgroundColor notNil]
+               ifNone: [i := 0].
+       ^ i!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>filterList:columnIndex:matching: (in category 'filtering') -----
+ filterList: columns columnIndex: index 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 := self
+                                               filterListItem: ((columns at: index) at: rowIndex)
+                                               matching: aPattern.
+               foundPos = 1
+                       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: [foundPos > 1 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 changed:
  ----- 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].

+       "Enable column-specific filtering."
+       self filterColumnIndex in: [:index |
+               index > 0 ifTrue: [^ self filterList: columns columnIndex: index matching: aPattern]].
+       
        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>>highlightNextColumn (in category 'filtering') -----
+ highlightNextColumn
+
+       | i currentColumn nextColumn |
+       i := self filterColumnIndex.
+       i = 0 ifTrue: [self listMorphs do: [:m | m showFilter: false]].
+
+       currentColumn := self listMorphs at: (i max: 1).
+       nextColumn := self listMorphs at: i \\ self listMorphs size + 1.
+       
+       currentColumn
+               showFilter: false;
+               backgroundColor: nil.
+               
+       nextColumn
+               showFilter: true;
+               backgroundColor: self filterColumnColor.!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>highlightNoColumn (in category 'filtering') -----
+ highlightNoColumn
+
+       self listMorphs do: [:m |
+               m showFilter: true; backgroundColor: nil].!

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>removeFilter (in category 'filtering') -----
+ removeFilter
+
+       self highlightNoColumn.
+       super removeFilter.
+ !

Item was added:
+ ----- Method: PluggableMultiColumnListMorph>>specialKeyPressed: (in category 'filtering') -----
+ specialKeyPressed: asciiValue
+       "Use the [Tab] key to filter specific columns."
+       
+       ^ asciiValue = Character tab asciiValue
+               ifTrue: [self highlightNextColumn]
+               ifFalse: [super specialKeyPressed: asciiValue].!

Item was changed:
  ----- Method: PluggableMultiColumnListMorph>>updateColumns (in category 'updating') -----
  updateColumns
        "The number of columns must match the number of list morphs."

        | columnsChanged |
        columnsChanged := self columnCount ~= listMorphs size.

        [self columnCount < listMorphs size]
                whileTrue: [
                        listMorphs removeLast delete].

        [self columnCount > listMorphs size]
                whileTrue: [
                        listMorphs addLast: self createListMorph.
                        self scroller addMorphBack: listMorphs last].

        listMorphs doWithIndex: [:listMorph :columnIndex |
                listMorph
                        columnIndex: columnIndex;
+                       color: self textColor;
                        cellPositioning: (self cellPositioningAtColumn: columnIndex);
                        cellInset: (self cellInsetAtColumn: columnIndex);
                        hResizing: (self hResizingAtColumn: columnIndex);
                        spaceFillWeight: (self spaceFillWeightAtColumn: columnIndex)].

        columnsChanged ifTrue: [self setListParameters].!