Tools__ChangeList.st
changeset 13835 7dfb57f5f5e7
parent 13513 97b651d76972
child 13927 78883c6c8297
--- a/Tools__ChangeList.st	Wed Feb 05 19:58:07 2014 +0100
+++ b/Tools__ChangeList.st	Wed Feb 05 19:58:09 2014 +0100
@@ -28,9 +28,9 @@
 "{ NameSpace: Tools }"
 
 BrowserListWithFilter subclass:#ChangeList
-	instanceVariableNames:'listHolder showRemovedHolder showSameHolder
-		showConflictsOnlyHolder allowRemoveHolder allowAcceptHolder
-		applyAction'
+	instanceVariableNames:'listHolder showRemovedHolder showSameHolder showTimestampHolder
+		allowRemoveHolder allowAcceptHolder scrollToBottom applyAction
+		showConflictsOnlyHolder'
 	classVariableNames:'LastSelectionConditionString'
 	poolDictionaries:''
 	category:'Interface-Browsers-ChangeSet'
@@ -132,54 +132,171 @@
 
     <resource: #canvas>
 
-    ^ 
+    ^
      #(FullSpec
-        name: windowSpec
-        window: 
+	name: windowSpec
+	window:
        (WindowSpec
-          label: 'Change List'
-          name: 'Change List'
-          min: (Point 10 10)
-          bounds: (Rectangle 0 0 300 300)
-        )
-        component: 
+	  label: 'Change List'
+	  name: 'Change List'
+	  min: (Point 10 10)
+	  bounds: (Rectangle 0 0 300 300)
+	)
+	component:
        (SpecCollection
-          collection: (
-           (SelectionInListModelViewSpec
-              name: 'List'
-              layout: (LayoutFrame 0 0 0 0 0 1 0 1)
-              model: selectionHolder
-              menu: menuHolderWithShowFilter
-              hasHorizontalScrollBar: true
-              hasVerticalScrollBar: true
-              listModel: listHolder
-              multipleSelectOk: true
-              useIndex: false
-              highlightMode: line
-              doubleClickSelector: selectionDoubleclicked
-              postBuildCallback: postBuildListView:
-            )
-           (InputFieldSpec
-              name: 'Filter'
-              layout: (LayoutFrame 0 0 0 0 0 1 26 0)
-              initiallyInvisible: true
-              model: filterPatternHolder
-              immediateAccept: true
-              acceptOnLeave: false
-              acceptOnReturn: false
-              acceptOnTab: false
-              acceptOnPointerLeave: false
-              emptyFieldReplacementText: 'Search Filter...'
-              usePreferredHeight: true
-              useDynamicPreferredHeight: true
-              postBuildCallback: postBuildFilterView:
-            )
-           )
-         
-        )
+	  collection: (
+	   (InputFieldSpec
+	      name: 'Filter'
+	      layout: (LayoutFrame 0 0 0 0 0 1 25 0)
+	      initiallyInvisible: true
+	      model: filterPatternHolder
+	      immediateAccept: true
+	      acceptOnLeave: false
+	      acceptOnReturn: false
+	      acceptOnTab: false
+	      acceptOnPointerLeave: false
+	      emptyFieldReplacementText: 'Search Filter...'
+	      postBuildCallback: postBuildFilterView:
+	    )
+	   (DataSetSpec
+	      name: 'List'
+	      layout: (LayoutFrame 0 0 0 0 0 1 0 1)
+	      model: selectionHolder
+	      menu: menuHolderWithShowFilter
+	      hasHorizontalScrollBar: true
+	      hasVerticalScrollBar: true
+	      dataList: listHolder
+	      useIndex: false
+	      doubleClickSelector: selectionDoubleclicked
+	      columnHolder: listColumns
+	      showLabels: false
+	      multipleSelectOk: true
+	      postBuildCallback: postBuildListView:
+	      properties:
+	     (PropertyListDictionary
+		startDragSelector: nil
+		dropObjectSelector: dragObjects:
+		canDropSelector: canDrop:
+		dropSelector: dropObjects:
+	      )
+	    )
+	   )
+
+	)
+      )
+! !
+
+!ChangeList class methodsFor:'list specs'!
+
+listColumnSpec
+    "This resource specification was automatically generated
+     by the DataSetBuilder of ST/X."
+
+    "Do not manually edit this!! If it is corrupted,
+     the DataSetBuilder may not be able to read the specification."
+
+    "
+     DataSetBuilder new openOnClass:Tools::ChangeList andSelector:#listColumnSpec
+    "
+
+    <resource: #tableColumns>
+
+    ^#(
+      (DataSetColumnSpec
+	 label: 'Removed'
+	 id: 'removed'
+	 labelButtonType: Button
+	 width: 20
+	 minWidth: 20
+	 editorType: CheckToggle
+	 rendererType: CheckToggle
+	 model: notRemoved
+	 menuFromApplication: false
+	 printSelector: notRemoved
+	 isResizeable: false
+	 showRowSeparator: false
+	 showSelectionHighLighted: false
+	 showColSeparator: false
+       )
+      (DataSetColumnSpec
+	 label: 'Delta'
+	 id: 'delta'
+	 labelButtonType: Button
+	 width: 20
+	 minWidth: 20
+	 menuFromApplication: false
+	 printSelector: iconDelta
+	 canSelect: false
+	 isResizeable: false
+	 showRowSeparator: false
+	 showSelectionHighLighted: false
+	 showColSeparator: false
+       )
+      (DataSetColumnSpec
+	 label: 'Change'
+	 id: change
+	 labelAlignment: left
+	 labelButtonType: Button
+	 menuFromApplication: false
+	 printSelector: label
+	 canSelect: false
+	 showRowSeparator: false
+	 showColSeparator: false
+       )
+      (DataSetColumnSpec
+	 label: 'Class'
+	 id: 'className'
+	 labelAlignment: left
+	 activeHelpKey: ''
+	 activeHelpKeyForLabel: ''
+	 labelButtonType: Button
+	 usePreferredWidth: true
+	 model: className
+	 menuFromApplication: false
+	 canSelect: false
+	 showRowSeparator: false
+	 showColSeparator: false
+       )
+      (DataSetColumnSpec
+	 label: 'Selector'
+	 id: 'selector'
+	 labelAlignment: left
+	 activeHelpKey: ''
+	 activeHelpKeyForLabel: ''
+	 labelButtonType: Button
+	 usePreferredWidth: true
+	 model: selector
+	 menuFromApplication: false
+	 canSelect: false
+	 showRowSeparator: false
+	 showColSeparator: false
+       )
+      (DataSetColumnSpec
+	 label: 'Category'
+	 id: category
+	 labelAlignment: left
+	 labelButtonType: Button
+	 usePreferredWidth: true
+	 model: category
+	 menuFromApplication: false
+	 canSelect: false
+	 showRowSeparator: false
+	 showColSeparator: false
+       )
+      (DataSetColumnSpec
+	 label: 'Time Stamp'
+	 id: timeStamp
+	 labelAlignment: left
+	 labelButtonType: Button
+	 usePreferredWidth: true
+	 model: timeStamp
+	 menuFromApplication: false
+	 canSelect: false
+	 showRowSeparator: false
+	 showColSeparator: false
+       )
       )
 
-    "Modified: / 29-11-2011 / 15:52:16 / Jan Vrany <jan.vrany@fit.cvut.cz>"
 ! !
 
 !ChangeList class methodsFor:'menu specs'!
@@ -325,17 +442,17 @@
     ^ #(
         #allowAcceptHolder
         #allowRemoveHolder
-
         #inGeneratorHolder
         #menuHolder
         #outGeneratorHolder
         #selectionHolder
+        #showFilterHolder
         #showRemovedHolder
         #showSameHolder
+        #showTimestampHolder
         #showConflictsOnlyHolder
       ).
 
-    "Modified: / 24-01-2012 / 19:54:54 / Jan Vrany <jan.vrany@fit.cvut.cz>"
 ! !
 
 !ChangeList methodsFor:'accessing'!
@@ -377,13 +494,31 @@
     "Created: / 27-12-2011 / 14:18:25 / cg"
 !
 
+scrollToBottom:aBoolean
+    "If set to true, list will automaticallu scroll to bottom
+     ehen the list is updated and clears this flag.
+
+     This is required as the list is updated asynchronously"
+
+    scrollToBottom := aBoolean.
+
+    "Modified (comment): / 30-03-2012 / 17:05:08 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+!
+
 selection
 
-    ^self selectionHolder value
+    ^(self selectionHolder value ? #()) reject:[:e|e isNil].
 
     "Created: / 05-12-2009 / 14:48:29 / Jan Vrany <jan.vrany@fit.cvut.cz>"
 !
 
+selection: selection
+
+    self selectionHolder value: selection
+
+    "Created: / 30-03-2012 / 12:29:51 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+!
+
 selectionIndices
     |sel|
 
@@ -469,6 +604,19 @@
     ].
 !
 
+listColumns
+
+    |holder|
+    (holder := builder bindingAt:#listColumns) isNil ifTrue:[
+	builder aspectAt:#listColumns put:(holder := List new).
+	self listColumnShow: #delta.
+	self listColumnShow: #change.
+    ].
+    ^ holder
+
+    "Created: / 26-07-2012 / 18:03:27 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+!
+
 listHolder
     listHolder isNil ifTrue:[
         listHolder := ValueHolder new.
@@ -563,6 +711,35 @@
     oldValue ~~ newValue ifTrue:[
         self update:#value with:newValue from:showSameHolder.
     ].
+!
+
+showTimestampHolder
+    "return/create the 'showTimestampHolder' value holder (automatically generated)"
+
+    showTimestampHolder isNil ifTrue:[
+	showTimestampHolder := ValueHolder new.
+	showTimestampHolder addDependent:self.
+    ].
+    ^ showTimestampHolder
+!
+
+showTimestampHolder:something
+    "set the 'showTimestampHolder' value holder (automatically generated)"
+
+    |oldValue newValue|
+
+    showTimestampHolder notNil ifTrue:[
+	oldValue := showTimestampHolder value.
+	showTimestampHolder removeDependent:self.
+    ].
+    showTimestampHolder := something.
+    showTimestampHolder notNil ifTrue:[
+	showTimestampHolder addDependent:self.
+    ].
+    newValue := showTimestampHolder value.
+    oldValue ~~ newValue ifTrue:[
+	self update:#value with:newValue from:showTimestampHolder.
+    ].
 ! !
 
 !ChangeList methodsFor:'change & update'!
@@ -573,12 +750,24 @@
 
 update: aspect with: param from: sender
 
+    sender == allowRemoveHolder ifTrue:[
+        self listColumn: #removed visible: allowRemoveHolder value.
+        ^self.
+    ].
+
+    sender == showTimestampHolder ifTrue:[
+        self listColumn: #timeStamp visible: showTimestampHolder value.
+        ^self.
+    ].
+
+
     sender == selectionHolder ifTrue:[
         self selectionChanged.
         ^ self
     ].
     sender == showSameHolder ifTrue:[
-        self updateList
+        self updateList.
+        ^self.
     ].
     sender == showRemovedHolder ifTrue:[
         self updateList
@@ -591,6 +780,38 @@
 
     "Created: / 24-10-2009 / 19:47:14 / Jan Vrany <jan.vrany@fit.cvut.cz>"
     "Modified (format): / 27-12-2011 / 14:20:30 / cg"
+    "Modified: / 26-07-2012 / 18:44:11 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+! !
+
+!ChangeList methodsFor:'drag & drop'!
+
+canDrop: aDropContext
+
+    ^(self inGeneratorHolder value isKindOf: Iterator) not
+	and:[aDropContext dropObjects allSatisfy:[:obj|(obj theObject isKindOf: Change)]].
+
+    "Created: / 01-08-2012 / 17:53:54 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+!
+
+dragObjects: aDropSource
+
+    ^self selection collect:[:item|(DropObject new: item change) displayObject: (item labelAndIcon)]
+
+    "Created: / 01-08-2012 / 17:50:01 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+!
+
+dropObjects:aDropContext
+    "drop manager wants to drop.
+     This is ony sent, if #canDrop: returned true.
+     Must be redefined in order for drop to work."
+
+    | changes |
+
+    changes := self inGeneratorHolder value.
+    aDropContext dropObjects do:[:obj|changes add: obj theObject].
+    self inGeneratorHolder changed: #value
+
+    "Modified: / 01-08-2012 / 18:15:39 / Jan Vrany <jan.vrany@fit.cvut.cz>"
 ! !
 
 !ChangeList methodsFor:'event processing'!
@@ -634,11 +855,9 @@
 !
 
 keyPress:key x:x y:y view:aView
-    <resource: #keyboard (#Ctrls #Accept)>
-
     (key == #Accept or:[ key == #Ctrls]) ifTrue:[
-        self listMenuApply.
-        ^ self
+	self listMenuApply.
+	^ self
     ].
 
     "Created: / 08-02-2012 / 14:42:18 / cg"
@@ -675,15 +894,15 @@
 !ChangeList methodsFor:'initialization'!
 
 initialize
+
     super initialize.
-
     menuHolder := [self menuFor: #listMenu].
+    scrollToBottom := false.
     applyAction isNil ifTrue:[
-        applyAction := [:change | change apply ].
+	applyAction := [:change | change apply ].
     ].
 
     "Created: / 29-10-2010 / 12:50:32 / Jan Vrany <jan.vrany@fit.cvut.cz>"
-    "Modified: / 10-09-2012 / 13:57:20 / cg"
 ! !
 
 !ChangeList methodsFor:'menu actions'!
@@ -897,6 +1116,62 @@
     ^ nil.
 !
 
+listColumn: columnId visible: visible
+    visible ifTrue:[
+        self listColumnShow: columnId
+    ] ifFalse:[
+        self listColumnHide: columnId
+    ]
+
+    "Created: / 26-07-2012 / 18:20:22 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+!
+
+listColumnHide: columnId
+    self listColumns do:[:spec|
+	spec id = columnId ifTrue:[
+	    self listColumns remove: spec.
+	    listView notNil ifTrue:[
+		listView invalidate.
+	    ].
+	    ^self
+	].
+    ]
+
+    "Created: / 26-07-2012 / 18:19:38 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+!
+
+listColumnShow: columnId
+
+    | columns |
+    columns := self listColumns.
+
+    columns do:[:spec|
+	spec id = columnId ifTrue:[
+	    ^self.
+	]
+    ].
+
+    self class listColumnSpec do:[:specArray|
+	| spec |
+
+	spec := specArray decodeAsLiteralArray.
+	spec id = columnId ifTrue:[
+	    columnId == #removed ifTrue:[
+		columns addFirst: spec.
+	    ] ifFalse:[
+		columns add: spec.
+	    ].
+	    listView notNil ifTrue:[
+		listView invalidate.
+	    ].
+	    ^self.
+	]
+
+    ].
+
+    "Created: / 26-07-2012 / 17:56:50 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+!
+
 listEntryFor:chg 
     ^ListEntry change:chg application: self
 
@@ -921,35 +1196,47 @@
 !
 
 updateList
-    |inGenerator changeset newList|
-
+    |inGenerator changeset firstLineShown oldSel newList newSel |
     self withWaitCursorDo:[
         inGenerator := self inGeneratorHolder value.
-        changeset := inGenerator ifNil:[ #() ] ifNotNil:[ inGenerator ].
-        self showConflictsOnlyHolder value ifTrue:[
-            newList := changeset select: [:chg | 
-                           chg isConflict
-                       ].
-        ] ifFalse:[
-            | showRemoved showSame |
+        listView notNil ifTrue:[
+            ((listView isKindOf: ScrollableView) not or:[listView scrolledView notNil]) ifTrue:[
+                firstLineShown := listView firstLineShown
+            ].
+        ].
+        oldSel := self selection.
 
-            showRemoved := self showRemovedHolder value.
-            showSame := self showSameHolder value.
-
-            newList := changeset select: [:chg | 
-                           (showRemoved or:[ chg removed not ])
-                               and:[showSame or:[chg delta ~~ #=]]
-                       ].
-        ].
+        changeset := inGenerator isNil ifTrue:[ #() ] ifFalse:[ inGenerator ].
+        newList := changeset
+                    select:
+                        [:chg |
+                        (self showRemovedHolder value or:[ chg removed not ])
+                            and:[self showSameHolder value or:[chg delta ~~ #=]]
+                        ].
         newList := self filterList: newList.
         newList := newList collect:[:chg | self listEntryFor:chg ].
         self listHolder value ~= newList ifTrue:[
             self listHolder value: newList.
+            ((newList size ~~ 0) and:[scrollToBottom]) ifTrue:[
+                self selection: { newList last }
+            ] ifFalse:[
+                oldSel notEmptyOrNil ifTrue:[
+                    newSel := OrderedCollection new: oldSel size.
+                    oldSel := oldSel reject:[:e|e isNil].
+                    oldSel := oldSel collect:[:e|e change].
+                    newList do:[:e|(oldSel includes:e change) ifTrue:[newSel add:e]].
+                    self selection: newSel.
+                ].
+                (listView notNil and:[firstLineShown notNil]) ifTrue:[
+                    listView scrollToLine: (newList size min: firstLineShown).
+                ].
+            ].
+            scrollToBottom := false.
+
         ]
     ]
-
-    "Modified: / 29-11-2011 / 15:38:59 / Jan Vrany <jan.vrany@fit.cvut.cz>"
     "Modified: / 28-12-2011 / 15:46:15 / cg"
+    "Modified: / 01-08-2012 / 18:10:52 / Jan Vrany <jan.vrany@fit.cvut.cz>"
 ! !
 
 !ChangeList methodsFor:'queries'!
@@ -981,6 +1268,13 @@
         ].
     ].
     ^ true
+!
+
+showColumn: columnSymbol
+
+    ^self showColumnSpecHolder value includes: columnSymbol.
+
+    "Created: / 03-04-2012 / 11:28:37 / Jan Vrany <jan.vrany@fit.cvut.cz>"
 ! !
 
 !ChangeList::ListEntry class methodsFor:'instance creation'!
@@ -1040,10 +1334,17 @@
 changeSource
     "the changes source"
 
-    ^change changeSource
+    "/ Hack to make sure code is displayed in similar way
+    "/ (i.e., with no namespace pragma)
+    ^ (change isClassDefinitionChange and:[change isPrivateClassDefinitionChange]) ifTrue:[
+        change definitionStringInNamespace: nil.
+    ] ifFalse:[
+        change changeSource.
+    ].
 
     "Created: / 19-07-2011 / 12:03:18 / Jan Vrany <jan.vrany@fit.cvut.cz>"
     "Modified (comment): / 25-07-2012 / 17:39:39 / cg"
+    "Modified: / 14-11-2013 / 14:55:30 / Jan Vrany <jan.vrany@fit.cvut.cz>"
 !
 
 children
@@ -1112,6 +1413,23 @@
     | label |
 
     label := change displayString.
+
+    "Hack: if the printString contains multiple lines, then
+     if there are just 2 lines, then merge them, otherwise add
+     ... to the first line"
+    (label includes: Character cr) ifTrue:[
+        | firstCR secondCR |
+        firstCR := label indexOf: Character cr.
+        firstCR ~~ label size ifTrue:[
+            secondCR := label indexOf: Character cr startingAt: firstCR + 1.
+        ].
+        secondCR isNil ifTrue:[
+            "/2 lines only
+            label := label copy at: firstCR put: Character space.
+        ] ifFalse:[
+            label := (label copyTo: firstCR - 1) , '...'
+        ].
+    ].
     self removed ifTrue:[label := label asText colorizeAllWith: Color gray].
     ^label
 
@@ -1121,8 +1439,28 @@
     "Modified (format): / 27-07-2012 / 21:26:34 / cg"
 !
 
+labelAndIcon
+    ^(LabelAndIcon label: self label icon: self iconDelta)
+
+    "Created: / 01-08-2012 / 18:14:43 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+!
+
+notRemoved
+    ^ self removed not
+
+    "Created: / 26-07-2012 / 18:31:48 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+!
+
+notRemoved: aBoolean
+    ^ self removed: aBoolean not
+
+    "Created: / 26-07-2012 / 18:31:56 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+!
+
 removed
-    ^ change removed
+    ^ change removed == true
+
+    "Modified: / 26-07-2012 / 18:30:17 / Jan Vrany <jan.vrany@fit.cvut.cz>"
 !
 
 removed:aBoolean
@@ -1134,31 +1472,50 @@
     ^change source
 !
 
+timeStamp
+    ^change timeOfChangeIfKnown
+
+    "Created: / 26-07-2012 / 18:38:39 / Jan Vrany <jan.vrany@fit.cvut.cz>"
+!
+
 x
     ^ x
 ! !
 
 !ChangeList::ListEntry methodsFor:'displaying'!
 
-displayLabel:aLabel h:lH on:aGC x:newX y:y h:h 
-    | cx icon |
+displayLabel:aLabel h:lH on:aGC x:newX y:y h:h
 
+    | list cx icon colW colS |
+    list := self application.
     cx := x := newX.
-    self application allowRemoveHolder value ifTrue:[
-        (icon := self iconRemoved) notNil ifTrue:[
-            icon displayOn: aGC x: cx y: y + (h / 2) - (icon height / 2) 
-        ].
-        cx := cx + 22."experimental value - this looks good"
+    (list allowRemoveHolder value and:[list showRemovedHolder value]) ifTrue: [
+	(icon := self iconRemoved) notNil ifTrue:[
+	    icon displayOn: aGC x: cx y: y + (h / 2) - (icon height / 2)
+	].
+	cx := cx + 22."experimental value - this looks good"
     ].
     (icon := self iconDelta) notNil ifTrue:[
-        icon displayOn: aGC x: cx y: y + (h / 2) - (icon height / 2) 
+	icon displayOn: aGC x: cx y: y + (h / 2) - (icon height / 2)
     ].
-    cx := cx + 16."12 + 2px gap"                
+    cx := cx + 16."12 + 2px gap"
+
+    super displayLabel:aLabel h:lH on:aGC x:cx y:y h:h.
 
-    super displayLabel:aLabel h:lH on:aGC x:cx y:y h:h
+"/    "Now, display additional columns..."
+"/    (list showColumn: #timestamp) ifTrue:[
+"/        cx := cx + (aLabel widthOn: aGC) + 5."px - padding"
+"/        colS := change timeOfChangeIfKnown notNil
+"/                    ifTrue:[change timeOfChangeIfKnown printString]
+"/                    ifFalse:['???'].
+"/        [ (colW := colS widthOn: aGC) > (aGC width - cx - 5) ] whileTrue:[
+"/            colS = '...' ifTrue:[ ^ self ].
+"/            colS := '...' , (colS copyFrom: 6).
+"/        ].
+"/        super displayLabel:colS h:lH on:aGC x: (aGC width - 5 - colW) y:y h:h.
+"/    ].
 
-    "Modified: / 24-01-2012 / 21:44:21 / Jan Vrany <jan.vrany@fit.cvut.cz>"
-    "Modified: / 27-07-2012 / 21:26:23 / cg"
+    "Modified: / 27-07-2012 / 17:13:24 / Jan Vrany <jan.vrany@fit.cvut.cz>"
 !
 
 iconDelta
@@ -1200,14 +1557,14 @@
 !ChangeList class methodsFor:'documentation'!
 
 version
-    ^ '$Header: /cvs/stx/stx/libtool/Tools__ChangeList.st,v 1.26 2013-09-05 23:18:30 cg Exp $'
+    ^ '$Header: /cvs/stx/stx/libtool/Tools__ChangeList.st,v 1.27 2014-02-05 18:58:09 cg Exp $'
 !
 
 version_CVS
-    ^ '$Header: /cvs/stx/stx/libtool/Tools__ChangeList.st,v 1.26 2013-09-05 23:18:30 cg Exp $'
+    ^ '$Header: /cvs/stx/stx/libtool/Tools__ChangeList.st,v 1.27 2014-02-05 18:58:09 cg Exp $'
 !
 
 version_SVN
-    ^ '$Id: Tools__ChangeList.st,v 1.26 2013-09-05 23:18:30 cg Exp $'
+    ^ '$Id: Tools__ChangeList.st,v 1.27 2014-02-05 18:58:09 cg Exp $'
 ! !