class: ListView
authorClaus Gittinger <cg@exept.de>
Fri, 06 Sep 2013 14:24:18 +0200
changeset 4747 3e369a9683d8
parent 4746 39a5681e71ca
child 4748 3f24bdf8a355
class: ListView class definition added:10 methods comment/format in: #list: changed:6 methods optimizations for the case when the list is computed on the fly (via a VirtualArray), and it is expensive to compute all of the text. For example, if you have a virtual size of 100000 or more lines, which are generated by reading a file or generating a hex dump.
ListView.st
--- a/ListView.st	Thu Sep 05 15:38:16 2013 +0200
+++ b/ListView.st	Fri Sep 06 14:24:18 2013 +0200
@@ -18,7 +18,10 @@
 		fontIsFixedWidth fontWidth autoScroll autoScrollBlock
 		autoScrollDeltaT wordCheck includesNonStrings widthOfWidestLine
 		listMsg viewOrigin listChannel backgroundAlreadyClearedColor
-		scrollWhenUpdating scrollLocked lineEndCRLF highlightAreas'
+		scrollWhenUpdating scrollLocked lineEndCRLF highlightAreas
+		compareModelOnUpdate expandTabsWhenUpdating
+		checkLineEndConventionWhenUpdating
+		numberOfLinesForWidthOfContentsComputation'
 	classVariableNames:'DefaultForegroundColor DefaultBackgroundColor DefaultTabPositions
 		UserDefaultTabPositions DefaultLeftMargin DefaultTopMargin'
 	poolDictionaries:''
@@ -459,6 +462,98 @@
 
 !ListView methodsFor:'accessing-behavior'!
 
+checkLineEndConventionWhenUpdating
+    "return the line-end convention check when updating behavior.
+     If true (the default), the first line of the list given is checked for having a
+     cr-lf line end (which is a DOS convention), and the lineEndCRLF flag is set dynamically.
+     If false, the lineEndCRLF remains as specified by the user.
+     You may want to disable this flag if it is very expensive to generate a line
+     (although, only the very first line is checked, anyway)"
+
+    ^ checkLineEndConventionWhenUpdating
+!
+
+checkLineEndConventionWhenUpdating:aBoolean
+    "define the line-end convention check when updating behavior.
+     If true (the default), the first line of the list given is checked for having a
+     cr-lf line end (which is a DOS convention), and the lineEndCRLF flag is set dynamically.
+     If false, the lineEndCRLF remains as specified by the user.
+     You may want to disable this flag if it is very expensive to generate a line
+     (although, only the very first line is checked, anyway)"
+
+    checkLineEndConventionWhenUpdating := aBoolean
+!
+
+compareModelWhenUpdating
+    "return the compare when updating behavior.
+     If true (the default), the list of lines as given due to a model update 
+     is processed and compared against the currently shown text. 
+     If they are the same, no action is taken.
+     This behavior is ok in 99.99% of all applications.
+     However, you may turn this off iff:
+        - it is very expensive to process the list (for example, because the list
+          is defined by a virtual array, which computes the lines dynamically, on
+          the fly).
+     One use where this flag should be turned off is in the hex-memory display,
+     which is able to simulate texts with millions of lines, but they are actually
+     simulated by generating the presented lines dynamically, as they are displayed."
+
+    ^ compareModelOnUpdate
+!
+
+compareModelWhenUpdating:aBoolean
+    "define the compare when updating behavior.
+     If true (the default), the list of lines as given due to a model update 
+     is processed and compared against the currently shown text. 
+     If they are the same, no action is taken.
+     This behavior is ok in 99.99% of all applications.
+     However, you may turn this off iff:
+        - it is very expensive to process the list (for example, because the list
+          is defined by a virtual array, which computes the lines dynamically, on
+          the fly).
+     One use where this flag should be turned off is in the hex-memory display,
+     which is able to simulate texts with millions of lines, but they are actually
+     simulated by generating the presented lines dynamically, as they are displayed."
+
+    compareModelOnUpdate := aBoolean
+!
+
+expandTabsWhenUpdating
+    "return the tab expansion behavior.
+     If true (the default), the list of lines as given via #list: or 
+     due to a model update is processed and lines are replaced by lines with
+     tabs expanded.
+     This behavior is ok in 99.99% of all applications.
+     However, you may turn this off iff:
+        - you are certain, that no tabs are in the passed in list
+        - it is very expensive to process the list (for example, because the list
+          is defined by a virtual array, which computes the lines dynamically, on
+          the fly).
+     One use where this flag should be turned off is in the hex-memory display,
+     which is able to simulate texts with millions of lines, but they are actually
+     simulated by generating the presented lines dynamically, as they are displayed."
+
+    ^ expandTabsWhenUpdating
+!
+
+expandTabsWhenUpdating:aBoolean
+    "define the tab expansion behavior.
+     If true (the default), the list of lines as given via #list: or 
+     due to a model update is processed and lines are replaced by lines with
+     tabs expanded.
+     This behavior is ok in 99.99% of all applications.
+     However, you may turn this off iff:
+        - you are certain, that no tabs are in the passed in list
+        - it is very expensive to process the list (for example, because the list
+          is defined by a virtual array, which computes the lines dynamically, on
+          the fly).
+     One use where this flag should be turned off is in the hex-memory display,
+     which is able to simulate texts with millions of lines, but they are actually
+     simulated by generating the presented lines dynamically, as they are displayed."
+
+    expandTabsWhenUpdating := aBoolean
+!
+
 lineEndCRLF
     "answer true, if CRLF is used for the line end.
      This is true for DOS/Windows files.
@@ -469,6 +564,26 @@
     "Created: / 04-07-2006 / 19:05:01 / fm"
 !
 
+numberOfLinesForWidthOfContentsComputation
+    "return the number of lines to consider in the widthOfContents computation,
+     which is needed by the scrollBar interface.
+     If nil (the default), all lines are processed and the width of the longest line is taken.
+     You may want to change this to 1 if it is guaranteed that all linesa are of the same width
+     (for example, when it is very expensive to generate all lines"
+
+    ^ numberOfLinesForWidthOfContentsComputation
+!
+
+numberOfLinesForWidthOfContentsComputation:anIntegerOrNil
+    "set the number of lines to consider in the widthOfContents computation,
+     which is needed by the scrollBar interface.
+     If nil (the default), all lines are processed and the width of the longest line is taken.
+     You may want to change this to 1 if it is guaranteed that all linesa are of the same width
+     (for example, when it is very expensive to generate all lines"
+
+    numberOfLinesForWidthOfContentsComputation := anIntegerOrNil
+!
+
 scrollWhenUpdating
     "return the scroll behavior, when I get a new text
      (via the model or the #contents/#list)
@@ -828,12 +943,28 @@
      (remembered to optimize later redraws)."
 
     self
-	list:aCollection expandTabs:expand scanForNonStrings:scan includesNonStrings:true
+        list:aCollection expandTabs:expand scanForNonStrings:scan includesNonStrings:nil
 
     "Modified: 5.6.1997 / 12:40:35 / cg"
 !
 
-list:aCollection expandTabs:expand scanForNonStrings:scan includesNonStrings:nonStrings
+list:aCollection expandTabs:expand scanForNonStrings:scan includesNonStrings:nonStringsIfNoScan
+    "set the contents (a collection of strings)
+     and scroll as specified in scrollWhenUpdating (default:top-left).
+     If expand is true, tabs are expanded (to spaces).
+     If scan is true, scan the passed list for nonStrings;
+     otherwise, take the information from the nonStrings arg.
+     (the nonStrings information is remembered to optimize later redraws & height computations)."
+
+    self 
+        list:aCollection 
+        expandTabs:expand 
+        scanForNonStrings:scan 
+        includesNonStrings:nonStringsIfNoScan 
+        redraw:true
+!
+
+list:aCollection expandTabs:expand scanForNonStrings:scan includesNonStrings:nonStringsIfNoScan redraw:doRedraw
     "set the contents (a collection of strings)
      and scroll as specified in scrollWhenUpdating (default:top-left).
      If expand is true, tabs are expanded (to spaces).
@@ -844,47 +975,57 @@
     |oldFirst oldLeft nonStringsBefore fontHeightBefore
      scrollToEnd scrollToTop newLeftOffset wText same firstLine|
 
-    "/ see if there is a change at all.
-    "/ use to compare using =, but thats not enough in case of emphasis change.
-    aCollection size == list size ifTrue:[
-        same := true.
-        aCollection size > 0 ifTrue:[
-            aCollection with:list do:[:eachNewLine :eachOldLine |
-                (eachNewLine == eachOldLine)
-"/                ((eachNewLine species == eachOldLine species)
-"/                and:[eachNewLine = eachOldLine])
-                ifFalse:[
-                    same := false.
+    "/ cg: what is the point in comparing here?
+    "/ I think, if there is something to optimize,
+    "/ the caller should do so (moved to getListFromModel).
+    "/ notice, that it may be very expensive to ask aCollection for each line
+    "/ for example, iff the lines are generated on the fly by an algorithm
+    false ifTrue:[
+        "/ see if there is a change at all.
+        "/ use to compare using =, but that's not enough in case of emphasis change.
+        aCollection size == list size ifTrue:[
+            same := true.
+            aCollection size > 0 ifTrue:[
+                aCollection with:list do:[:eachNewLine :eachOldLine |
+                    (eachNewLine == eachOldLine)
+                    ifFalse:[
+                        same := false.
+                    ]
                 ]
-            ]
+            ].
+            same ifTrue:[^ self].
         ].
-        same ifTrue:[^ self].
     ].
 
     scrollToTop := scrollWhenUpdating == #begin or:[scrollWhenUpdating == #beginOfText].
     scrollToEnd := scrollWhenUpdating == #end or:[scrollWhenUpdating == #endOfText].
 
-    (aCollection isNil and:[list isNil]) ifTrue:[
+    (aCollection isEmptyOrNil and:[list isEmptyOrNil]) ifTrue:[
         "no contents change"
-        scrollToTop ifTrue:[
-            self scrollToTop.
-        ] ifFalse:[
-            scrollToEnd ifTrue:[
-                self scrollToBottom.
-            ]
+        list := aCollection.
+        scrollLocked ifFalse:[
+            scrollToTop ifTrue:[
+                self scrollToTop.
+            ] ifFalse:[
+                scrollToEnd ifTrue:[
+                    self scrollToBottom.
+                ]
+            ].
+            self scrollToLeft.
         ].
-        self scrollToLeft.
         ^ self
     ].
 
-    "Check if the we use DOS/Windows line end conventin with CR LF.
-     The LF has already been consumed by the conversion to a StringCollection,
-     now check for and remove the trailing left over CRs"
-
-    lineEndCRLF := (aCollection size > 0
-                    and:[(firstLine := aCollection at:1) isString
-                    and:[firstLine notEmpty
-                    and:[firstLine string endsWith:Character return]]]).
+    checkLineEndConventionWhenUpdating ifTrue:[
+        "Check if the we use DOS/Windows line end convention with CR LF.
+         The LF has already been consumed by the conversion to a StringCollection,
+         now check for and remove the trailing left over CRs"
+
+        lineEndCRLF := (aCollection size > 0
+                        and:[(firstLine := aCollection at:1) isString
+                        and:[firstLine notEmpty
+                        and:[firstLine string endsWith:Character return]]]).
+    ].
     lineEndCRLF ifTrue:[
         list := aCollection
                     collect:[:eachLineWithCROrNil |
@@ -908,7 +1049,7 @@
             scan ifTrue:[
                 includesNonStrings := list contains:[:e | e isString not].
             ] ifFalse:[
-                includesNonStrings := nonStrings
+                includesNonStrings := nonStringsIfNoScan ? nonStringsBefore
             ]
         ].
     ].
@@ -924,47 +1065,45 @@
         self computeNumberOfLinesShown.
     ].
 
-    newLeftOffset := viewOrigin x.
-    scrollToTop ifTrue:[
-        firstLineShown := 1.
-        newLeftOffset := 0.
-    ] ifFalse:[
-        scrollToEnd ifTrue:[
-            firstLineShown := (list size - nFullLinesShown + 1) max:1.
+    scrollLocked ifFalse:[
+        newLeftOffset := viewOrigin x.
+        scrollToTop ifTrue:[
+            firstLineShown := 1.
             newLeftOffset := 0.
-        ]
-    ].
-    newLeftOffset > 0 ifTrue:[
-        wText := self widthOfContents.
-        (viewOrigin x + self innerWidth) > wText ifTrue:[
-            newLeftOffset := (wText - self innerWidth) max:0.
+        ] ifFalse:[
+            scrollToEnd ifTrue:[
+                firstLineShown := (list size - nFullLinesShown + 1) max:1.
+                newLeftOffset := 0.
+            ]
         ].
-    ].
-    newLeftOffset ~= oldLeft ifTrue:[
-        viewOrigin := newLeftOffset @ viewOrigin y.
+        newLeftOffset > 0 ifTrue:[
+            wText := self widthOfContents.
+            (viewOrigin x + self innerWidth) > wText ifTrue:[
+                newLeftOffset := (wText - self innerWidth) max:0.
+            ].
+        ].
+        newLeftOffset ~= oldLeft ifTrue:[
+            viewOrigin := newLeftOffset @ viewOrigin y.
+        ].
     ].
 
     realized ifTrue:[
         self contentsChanged.
-        "
-         don't use scroll here to avoid double redraw
-        "
-        viewOrigin := viewOrigin isNil ifTrue:[0@0] ifFalse:[(viewOrigin x) @ 0].
-        transformation := nil.
-
-        oldFirst ~~ firstLineShown ifTrue:[
-            self originChanged:0 @ ((oldFirst - 1) * fontHeight negated).
+        scrollLocked ifFalse:[
+            "
+             don't use scroll here to avoid double redraw
+            "
+            viewOrigin := viewOrigin isNil ifTrue:[0@0] ifFalse:[(viewOrigin x) @ 0].
+            transformation := nil.
+
+            oldFirst ~~ firstLineShown ifTrue:[
+                self originChanged:0 @ ((oldFirst - 1) * fontHeight negated).
+            ].
         ].
-        shown ifTrue:[
-self invalidate.
-"/            self redrawFromVisibleLine:1 to:nLinesShown.
-
-"/            fontHeightBefore > fontHeight ifTrue:[
-"/                (self listLineIsVisible:(self size)) ifTrue:[
-"/"/                    self clearRectangle:((margin @ (self yOfVisibleLine:nLinesShown+1))
-"/"/                                        corner:(width-margin) @ (height-margin)).
-"/                ].
-"/            ]
+        doRedraw ifTrue:[
+            shown ifTrue:[
+                self invalidate.
+            ]
         ]
     ]
 
@@ -1152,58 +1291,98 @@
      This can be used to update a self-changing list
      (for example: a file list being shown, without disturbing the user too much)"
 
-    |oldFirst nonStringsBefore|
-
-    (aCollection isNil and:[list isNil]) ifTrue:[
-        "no change"
-        ^ self
-    ].
-
-    list := aCollection.
-
-    nonStringsBefore := includesNonStrings.
-    includesNonStrings := false.
-
-    list notNil ifTrue:[
-        expandTabs ifTrue:[
-            self expandTabs
-        ] ifFalse:[
-            includesNonStrings := (list findFirst:[:e | e isString not]) ~~ 0.
-        ].
-    ].
-    (includesNonStrings ~~ nonStringsBefore) ifTrue:[
-        self getFontParameters.
-        self computeNumberOfLinesShown.
+    self 
+        setList:aCollection expandTabs:expandTabs scanForNonStrings:true includesNonStrings:nil
+        redraw:doRedraw
+!
+
+setList:aCollection expandTabs:expandTabs scanForNonStrings:scan includesNonStrings:nonStringsIfNoScan redraw:doRedraw
+    "set the contents (a collection of strings);
+     do not change position (i.e. do not scroll).
+     This can be used to update a self-changing list
+     (for example: a file list being shown, without disturbing the user too much).
+    TODO: this stinks: most of the code is the same as in #list:expandTabs:...
+          needs a refactoring"
+
+    |prev|
+
+    prev := scrollLocked.
+    [
+        scrollLocked := false.
+        self 
+            list:aCollection 
+            expandTabs:expandTabs 
+            scanForNonStrings:scan 
+            includesNonStrings:nonStringsIfNoScan 
+            redraw:doRedraw
+    ] ensure:[
+        scrollLocked := prev
     ].
-
-    "/ new: reposition horizontally if too big
-    widthOfWidestLine := nil.   "/ i.e. unknown
-    innerWidth >= self widthOfContents ifTrue:[
-        viewOrigin := 0 @ viewOrigin y.
-    ].
-    self contentsChanged.
-
-    "/ new: reposition vertically if too big
-    (firstLineShown + nFullLinesShown) > self size ifTrue:[
-        oldFirst := firstLineShown.
-        firstLineShown := self size - nFullLinesShown + 1.
-        firstLineShown < 1 ifTrue:[firstLineShown := 1].
-        oldFirst ~= firstLineShown ifTrue:[
-            viewOrigin y:(firstLineShown - 1 * fontHeight).
-            self originChanged:0 @ ((oldFirst - 1) negated * fontHeight).
-            shown ifTrue:[
-                self clearView.
-            ]
-        ]
-    ].
-
-    (shown and:[doRedraw]) ifTrue:[
-          self redrawFromVisibleLine:1 to:nLinesShown
-    ]
-
-    "Modified: / 18.12.1995 / 23:27:54 / stefan"
-    "Created: / 22.4.1998 / 11:11:51 / cg"
-    "Modified: / 26.7.1998 / 13:46:49 / cg"
+"/
+"/    
+"/"/                scrollLocked ifTrue:[
+"/"/                    self setList:newText expandTabs:expandTabsWhenUpdating
+"/"/                ] ifFalse:[
+"/                    self list:newText expandTabs:expandTabsWhenUpdating scanForNonStrings:expandTabsWhenUpdating
+"/"/                ]
+"/
+"/    |oldFirst nonStringsBefore|
+"/
+"/    (aCollection isNil and:[list isNil]) ifTrue:[
+"/        "no change"
+"/        ^ self
+"/    ].
+"/
+"/    list := aCollection.
+"/
+"/    nonStringsBefore := includesNonStrings.
+"/    includesNonStrings := false.
+"/
+"/    list notNil ifTrue:[
+"/        expandTabs ifTrue:[
+"/            self expandTabs
+"/        ] ifFalse:[
+"/            scan ifTrue:[
+"/                includesNonStrings := (list findFirst:[:e | e isString not]) ~~ 0.
+"/            ] ifFalse:[
+"/                includesNonStrings := nonStringsIfNoScan ? nonStringsBefore
+"/            ]
+"/        ].
+"/    ].
+"/    (includesNonStrings ~~ nonStringsBefore) ifTrue:[
+"/        self getFontParameters.
+"/        self computeNumberOfLinesShown.
+"/    ].
+"/
+"/    "/ new: reposition horizontally if too big
+"/    widthOfWidestLine := nil.   "/ i.e. unknown
+"/    innerWidth >= self widthOfContents ifTrue:[
+"/        viewOrigin := 0 @ viewOrigin y.
+"/    ].
+"/    self contentsChanged.
+"/
+"/    "/ new: reposition vertically if too big
+"/    (firstLineShown + nFullLinesShown) > self size ifTrue:[
+"/        oldFirst := firstLineShown.
+"/        firstLineShown := self size - nFullLinesShown + 1.
+"/        firstLineShown < 1 ifTrue:[firstLineShown := 1].
+"/        oldFirst ~= firstLineShown ifTrue:[
+"/            viewOrigin y:((firstLineShown - 1) * fontHeight).
+"/            self originChanged:0 @ ((oldFirst - 1) negated * fontHeight).
+"/            shown ifTrue:[
+"/                self clearView.
+"/            ]
+"/        ]
+"/    ].
+"/
+"/    (shown and:[doRedraw]) ifTrue:[
+"/        self invalidate
+"/        "/ self redrawFromVisibleLine:1 to:nLinesShown
+"/    ]
+"/
+"/    "Modified: / 18.12.1995 / 23:27:54 / stefan"
+"/    "Created: / 22.4.1998 / 11:11:51 / cg"
+"/    "Modified: / 26.7.1998 / 13:46:49 / cg"
 !
 
 size
@@ -2273,10 +2452,15 @@
     partialLines := true.
     tabPositions := UserDefaultTabPositions ? DefaultTabPositions.
     includesNonStrings := false.
+    numberOfLinesForWidthOfContentsComputation := nil."/ i.e. all
     self getFontParameters.
     self initializeWordCheckAction.
+
     scrollWhenUpdating := #keep. "/ #beginOfText.
-
+    expandTabsWhenUpdating := true.
+    compareModelOnUpdate := true.
+    checkLineEndConventionWhenUpdating := true.
+    scrollLocked := false.
     autoScroll := true.
 
     "Modified: / 03-07-2006 / 17:03:59 / cg"
@@ -2623,29 +2807,48 @@
     "ask my model (if any) for the text via the listMsg.
      If there is no listMessage, try aspect for backward compatibility."
 
-    |text msg|
+    |newText msg|
 
     model notNil ifTrue:[
         msg := listMsg ? aspectMsg.
 
         msg notNil ifTrue:[
-            text := model perform:msg.
+            newText := model perform:msg.
             "/ cg: this makes many optimizations (virtualArray) useless;
-            "/ I do not think that this is a good idea!!
+            "/ I do not think that this is a good idea:
             "/     text notNil ifTrue:[
             "/ so I changed it to:
-            (text notNil and:[text isString]) ifTrue:[
-                text := text asStringCollection.
+            (newText notNil and:[newText isString]) ifTrue:[
+                newText := newText asStringCollection.
+            ].
+
+            compareModelOnUpdate ifTrue:[
+                "/ see if there is a change at all.
+                "/ use to compare using =, but that's not enough in case of emphasis change.
+                newText size == list size ifTrue:[
+                    |same|
+
+                    same := true.
+                    newText size > 0 ifTrue:[
+                        newText with:list do:[:eachNewLine :eachOldLine |
+                            (eachNewLine == eachOldLine) ifFalse:[
+                                same := false.
+                            ]
+                        ]
+                    ].
+                    same ifTrue:[^ self].
+                ].
             ].
 
             "/ SV: this compare does not work, if model uses (i.e. updates)
             "/ the same stringCollection as the view!!
             true "text ~= list" ifTrue:[
-                scrollLocked == true ifTrue:[
-                    self setList:text
-                ] ifFalse:[
-                    self list:text
-                ]
+                "/ changed #list to care for scrollLocked
+"/                scrollLocked ifTrue:[
+"/                    self setList:newText expandTabs:expandTabsWhenUpdating
+"/                ] ifFalse:[
+                    self list:newText expandTabs:expandTabsWhenUpdating scanForNonStrings:expandTabsWhenUpdating
+"/                ]
             ].
         ].
     ].
@@ -2657,16 +2860,16 @@
 getListFromModelScroll:aBoolean
     "ask my model (if any) for the text via the listMsg.
      If there is no listMessage, try aspect for backward compatibility.
-     Scrolling is suppressed here"
+     If aBoolean is false, scrolling is suppressed here"
 
     |prev|
 
     prev := scrollLocked.
-    scrollLocked := true.
+    scrollLocked := aBoolean not.
     [
-	self getListFromModel
+        self getListFromModel
     ] ensure:[
-	scrollLocked := prev.
+        scrollLocked := prev.
     ].
 !
 
@@ -4931,10 +5134,10 @@
 !ListView class methodsFor:'documentation'!
 
 version
-    ^ '$Header: /cvs/stx/stx/libwidg/ListView.st,v 1.378 2013-09-03 20:06:14 cg Exp $'
+    ^ '$Header: /cvs/stx/stx/libwidg/ListView.st,v 1.379 2013-09-06 12:24:18 cg Exp $'
 !
 
 version_CVS
-    ^ '$Header: /cvs/stx/stx/libwidg/ListView.st,v 1.378 2013-09-03 20:06:14 cg Exp $'
+    ^ '$Header: /cvs/stx/stx/libwidg/ListView.st,v 1.379 2013-09-06 12:24:18 cg Exp $'
 ! !