Chris Muller uploaded a new version of ToolBuilder-MVC to project Squeak 4.6:
http://source.squeak.org/squeak46/ToolBuilder-MVC-mt.42.mcz
==================== Summary ====================
Name: ToolBuilder-MVC-mt.42
Author: mt
Time: 3 May 2015, 3:14:38.18 pm
UUID: f1b2eb4e-2140-c445-a31f-c4c95ad44334
Ancestors: ToolBuilder-MVC-mt.41
A simple implementation for spacers.
==================== Snapshot ====================
SystemOrganization addCategory: #'ToolBuilder-MVC'!
ToolBuilder subclass: #MVCToolBuilder
instanceVariableNames: 'panes topSize widgets parentMenu'
classVariableNames: ''
poolDictionaries: ''
category: 'ToolBuilder-MVC'!
!MVCToolBuilder commentStamp: 'ar 2/11/2005 15:02' prior: 0!
The MVC tool builder.!
----- Method: MVCToolBuilder class>>isActiveBuilder (in category 'accessing') -----
isActiveBuilder
"Answer whether I am the currently active builder"
"This is really a way of answering whether 'Smalltalk isMVC'"
ScheduledControllers ifNil:[^false].
^(ScheduledControllers activeControllerProcess ifNil:[^false]) isTerminated not!
----- Method: MVCToolBuilder>>asWindow: (in category 'private') -----
asWindow: aRectangle
| outer |
outer := parent window ifNil: [topSize].
^(aRectangle origin * outer extent) truncated
corner: (aRectangle corner * outer extent) truncated!
----- Method: MVCToolBuilder>>buildPluggableAlternateMultiSelectionList: (in category 'widgets optional') -----
buildPluggableAlternateMultiSelectionList: aSpec
"Trim selector to one argument to match list expectations."
aSpec setIndex: (aSpec setIndex first: (aSpec setIndex findString: ':')) asSymbol.
^ self buildPluggableList: aSpec.!
----- Method: MVCToolBuilder>>buildPluggableButton: (in category 'widgets required') -----
buildPluggableButton: aSpec
| widget label state |
label := aSpec label.
state := aSpec state.
widget := PluggableButtonView on: aSpec model
getState: (state isSymbol ifTrue:[state])
action: aSpec action
label: (label isSymbol ifTrue:[label]).
self register: widget id: aSpec name.
label ifNotNil: [label isSymbol
ifTrue: [widget label: (aSpec model perform: label)]
ifFalse: [widget label: label]].
self setFrame: aSpec frame in: widget.
parent ifNotNil: [parent addSubView: widget].
^widget!
----- Method: MVCToolBuilder>>buildPluggableList: (in category 'widgets required') -----
buildPluggableList: aSpec
| widget listClass getIndex setIndex |
aSpec getSelected ifNil:[
listClass := PluggableListView.
getIndex := aSpec getIndex.
setIndex := aSpec setIndex.
] ifNotNil:[
listClass := PluggableListViewByItem.
getIndex := aSpec getSelected.
setIndex := aSpec setSelected.
].
widget := listClass on: aSpec model
list: aSpec list
selected: getIndex
changeSelected: setIndex
menu: aSpec menu
keystroke: aSpec keyPress.
self register: widget id: aSpec name.
self setFrame: aSpec frame in: widget.
parent ifNotNil:[parent addSubView: widget].
panes ifNotNil:[
aSpec list ifNotNil:[panes add: aSpec list].
].
^widget!
----- Method: MVCToolBuilder>>buildPluggableMenu: (in category 'widgets required') -----
buildPluggableMenu: menuSpec
"Just a very simple mapping to selection menu. It assumes that all item specs have the same receiver."
| menu |
menu := CustomMenu new.
menuSpec items do: [:ea |
menu
add: ea label
target: ea action receiver
selector: ea action selector
argumentList: ea action arguments.
ea separator ifTrue: [menu addLine]].
^ menu!
----- Method: MVCToolBuilder>>buildPluggableMenuItem: (in category 'widgets required') -----
buildPluggableMenuItem: itemSpec
^ itemSpec label!
----- Method: MVCToolBuilder>>buildPluggablePanel: (in category 'widgets required') -----
buildPluggablePanel: aSpec
| widget children |
widget := View new model: aSpec model.
widget borderWidth: 1.
self register: widget id: aSpec name.
children := aSpec children.
children isSymbol ifTrue:[
"@@@@ FIXME: PluggablePanes need to remember their getChildrenSelector"
"widget getChildrenSelector: children.
widget update: children."
children := #().
].
self setFrame: aSpec frame in: widget.
self buildAll: children in: widget.
parent ifNotNil:[parent addSubView: widget].
self setLayout: aSpec layout in: widget.
^widget!
----- Method: MVCToolBuilder>>buildPluggableSpacer: (in category 'widgets required') -----
buildPluggableSpacer: aSpec
| widget |
widget := View new.
self register: widget id: aSpec name.
widget borderWidth: 0.
widget backgroundColor: aSpec color.
widget window: (widget window topLeft extent: aSpec extent).
self setFrame: aSpec frame in: widget.
parent ifNotNil:[parent addSubView: widget].
^widget!
----- Method: MVCToolBuilder>>buildPluggableText: (in category 'widgets required') -----
buildPluggableText: aSpec
| widget |
widget := PluggableTextView on: aSpec model
text: aSpec getText
accept: aSpec setText
readSelection: aSpec selection
menu: aSpec menu.
self register: widget id: aSpec name.
self setFrame: aSpec frame in: widget.
parent ifNotNil:[parent addSubView: widget].
panes ifNotNil:[
aSpec getText ifNotNil:[panes add: aSpec getText].
].
^widget!
----- Method: MVCToolBuilder>>buildPluggableTree: (in category 'widgets required') -----
buildPluggableTree: aSpec
self shouldBeImplemented.!
----- Method: MVCToolBuilder>>buildPluggableWindow: (in category 'widgets required') -----
buildPluggableWindow: aSpec
| widget children label |
topSize := 0@0 corner: 640@480.
aSpec layout == #proportional ifFalse:[
"This needs to be implemented - probably by adding a single pane and then the rest"
^self error: 'Not implemented'.
].
widget := StandardSystemView new.
self register: widget id: aSpec name.
widget model: aSpec model.
label := aSpec label.
label isSymbol ifTrue: [label := aSpec model perform: label].
label isNil ifFalse: [widget setLabel: label].
children := aSpec children.
children isSymbol ifTrue:[
"This isn't implemented by StandardSystemView, so we fake it"
children := widget model perform: children.
].
aSpec extent ifNotNil:[topSize := 0@0 extent: aSpec extent].
widget window: topSize.
panes := OrderedCollection new.
self buildAll: children in: widget.
widget setUpdatablePanesFrom: panes.
^widget!
----- Method: MVCToolBuilder>>close: (in category 'opening') -----
close: aWidget
"Close a previously opened widget"
aWidget controller closeAndUnschedule.!
----- Method: MVCToolBuilder>>open: (in category 'opening') -----
open: anObject
"Build and open the object. Answer the widget opened."
| window |
window := (anObject isKindOf: View)
ifTrue: [anObject]
ifFalse: [self build: anObject].
(window isKindOf: PopUpMenu)
ifTrue: [window invokeOn: nil].
(window isKindOf: View)
ifTrue: [window controller open].
^window!
----- Method: MVCToolBuilder>>open:label: (in category 'opening') -----
open: anObject label: aString
"Build an open the object, labeling it appropriately. Answer the widget opened."
| window |
window := self build: anObject.
window label: aString.
window controller open.
^window!
----- Method: MVCToolBuilder>>openDebugger: (in category 'opening') -----
openDebugger: anObject
"Build and open the object. Answer the widget opened."
| window |
window := self build: anObject.
window controller openNoTerminate.
^window!
----- Method: MVCToolBuilder>>openDebugger:label: (in category 'opening') -----
openDebugger: anObject label: aString
"Build an open the object, labeling it appropriately. Answer the widget opened."
| window |
window := self build: anObject.
window label: aString.
window controller openNoTerminate.
^window!
----- Method: MVCToolBuilder>>openDebugger:label:closing: (in category 'opening') -----
openDebugger: anObject label: aString closing: topView
"Build an open the object, labeling it appropriately. Answer the widget opened."
| window |
topView controller controlTerminate.
topView deEmphasize; erase.
"a few hacks to get the scroll selection artifacts out when we got here by clicking in the list"
" topView subViewWantingControl ifNotNil: [
topView subViewWantingControl controller controlTerminate
]."
topView controller status: #closed.
window := self build: anObject.
window label: aString.
window controller openNoTerminate.
topView controller closeAndUnscheduleNoErase.
Processor terminateActive.
^window!
----- Method: MVCToolBuilder>>positionSubviewsWithin: (in category 'private') -----
positionSubviewsWithin: widget
"Translate subviews to position the viewport of each subView relative to
the widget window origin. If subviews are repositioned, as in a row of button
views arranged within a view, then the transformations will later be rescaled
to fit the subviews within the widget window."
widget subViews ifNotNilDo: [:subViews |
subViews isEmpty ifFalse: [ | translation |
translation := widget window origin - subViews first window origin.
subViews do: [:v |
v setTransformation: (v transformation translateBy: translation)]]].
!
----- Method: MVCToolBuilder>>register:id: (in category 'private') -----
register: widget id: id
id ifNil:[^self].
widgets ifNil:[widgets := Dictionary new].
widgets at: id put: widget.!
----- Method: MVCToolBuilder>>runModal: (in category 'opening') -----
runModal: aWidget
"Run the (previously opened) widget modally, e.g.,
do not return control to the sender before the user has responded."
!
----- Method: MVCToolBuilder>>setFrame:in: (in category 'private') -----
setFrame: fractionsRectangleOrLayoutFrame in: widget
| win |
fractionsRectangleOrLayoutFrame ifNil: [^nil].
win := fractionsRectangleOrLayoutFrame isRectangle
ifTrue: [self asWindow: fractionsRectangleOrLayoutFrame]
ifFalse: [fractionsRectangleOrLayoutFrame layout: nil in: topSize]. "assume LayoutFrame"
widget window: win.!
----- Method: MVCToolBuilder>>setLayout:in: (in category 'private') -----
setLayout: layout in: widget
"Arrange subview horizontally or vertically according to layout directive.
If the subview dimensions were specified with layout frames rather than explicit
rectangle sizes, then their window horizontal or vertical dimensions will be resized
as needed to fit within the widget extent."
self positionSubviewsWithin: widget.
layout == #proportional ifTrue:[^self].
layout == #horizontal ifTrue:[
| prev subViewWidth widgetWidth xScale |
subViewWidth := (widget subViews collect: [:e | e window extent x]) sum.
widgetWidth := widget window extent x.
xScale := widgetWidth / subViewWidth. "to adjust corner of prev prior to align:"
prev := nil.
widget subViews do:[:next| | newWindowWidth newCorner |
prev ifNotNil:[ "resize prev window prior to aligning next"
xScale < 1 ifTrue: [ "proportional placement spec requires resizing"
newWindowWidth := (prev window extent x * xScale) truncated.
newCorner := (prev window origin x + newWindowWidth)@(prev window corner y).
prev setWindow: (prev window origin corner: newCorner)].
next align: next viewport topLeft with: prev viewport topRight.
].
prev := next.
].
^self].
layout == #vertical ifTrue:[
| prev subViewHeight widgetHeight yScale |
subViewHeight := (widget subViews collect: [:e | e window extent y]) sum.
widgetHeight := widget window extent y.
yScale := widgetHeight / subViewHeight. "to adjust corner of prev prior to align:"
prev := nil.
widget subViews do:[:next| | newWindowHeight newCorner |
prev ifNotNil:[ "resize prev window prior to aligning next"
yScale < 1 ifTrue: [ "proportional placement spec requires resizing"
newWindowHeight := (prev window extent y * yScale) truncated.
newCorner := (prev window corner x)@(prev window origin y + newWindowHeight).
prev setWindow: (prev window origin corner: newCorner)].
next align: next viewport topLeft with: prev viewport bottomLeft.
].
prev := next.
].
^self].
^self error: 'Unknown layout: ', layout.!
----- Method: MVCToolBuilder>>widgetAt:ifAbsent: (in category 'private') -----
widgetAt: id ifAbsent: aBlock
widgets ifNil:[^aBlock value].
^widgets at: id ifAbsent: aBlock!
UIManager subclass: #MVCUIManager
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'ToolBuilder-MVC'!
!MVCUIManager commentStamp: 'dtl 5/2/2010 16:06' prior: 0!
MVCUIManager is a UIManager that implements user interface requests for an MVC user interface.!
----- Method: MVCUIManager class>>isActiveManager (in category 'accessing') -----
isActiveManager
"Answer whether I should act as the active ui manager"
"This is really a way of answering whether 'Smalltalk isMVC'"
ScheduledControllers ifNil:[^false].
^ScheduledControllers activeControllerProcess == Processor activeProcess!
----- Method: MVCUIManager>>chooseDirectory:from: (in category 'ui requests') -----
chooseDirectory: label from: dir
"Let the user choose a directory"
^self notYetImplemented!
----- Method: MVCUIManager>>chooseFileMatching:label: (in category 'ui requests') -----
chooseFileMatching: patterns label: labelString
"Let the user choose a file matching the given patterns"
^self notYetImplemented!
----- Method: MVCUIManager>>chooseFont:for:setSelector:getSelector: (in category 'ui requests') -----
chooseFont: aPrompt for: aTarget setSelector: setSelector getSelector: getSelector
"MVC Only!! prompt for a font and if one is provided, send it to aTarget using a message with selector aSelector."
| aMenu aChoice aStyle namesAndSizes aFont |
aMenu := CustomMenu new.
TextStyle actualTextStyles keysSortedSafely do:
[:styleName |
aMenu add: styleName action: styleName].
aChoice := aMenu startUpWithCaption: aPrompt.
aChoice ifNil: [^ self].
aMenu := CustomMenu new.
aStyle := TextStyle named: aChoice.
(namesAndSizes := aStyle fontNamesWithPointSizes) do:
[:aString | aMenu add: aString action: aString].
aChoice := aMenu startUpWithCaption: nil.
aChoice ifNil: [^ self].
aFont := aStyle fontAt: (namesAndSizes indexOf: aChoice).
aTarget perform: setSelector with: aFont!
----- Method: MVCUIManager>>chooseFrom:lines:title: (in category 'ui requests') -----
chooseFrom: aList lines: linesArray title: aString
"Choose an item from the given list. Answer the index of the selected item."
| menu |
menu := PopUpMenu labelArray: aList lines: linesArray.
^aString isEmpty ifTrue:[menu startUp] ifFalse:[menu startUpWithCaption: aString]!
----- Method: MVCUIManager>>chooseFrom:values:lines:title: (in category 'ui requests') -----
chooseFrom: labelList values: valueList lines: linesArray title: aString
"Choose an item from the given list. Answer the selected item."
| menu |
menu := SelectionMenu labels: labelList lines: linesArray selections: valueList.
^aString isEmpty ifTrue:[menu startUp] ifFalse:[menu startUpWithCaption: aString]!
----- Method: MVCUIManager>>confirm: (in category 'ui requests') -----
confirm: queryString
"Put up a yes/no menu with caption queryString. Answer true if the
response is yes, false if no. This is a modal question--the user must
respond yes or no."
^PopUpMenu confirm: queryString!
----- Method: MVCUIManager>>confirm:orCancel: (in category 'ui requests') -----
confirm: aString orCancel: cancelBlock
"Put up a yes/no/cancel menu with caption aString. Answer true if
the response is yes, false if no. If cancel is chosen, evaluate
cancelBlock. This is a modal question--the user must respond yes or no."
^PopUpMenu confirm: aString orCancel: cancelBlock!
----- Method: MVCUIManager>>confirm:orCancel:title: (in category 'ui requests') -----
confirm: aString orCancel: cancelBlock title: titleString
"Put up a yes/no/cancel menu with caption aString, and titleString to label the dialog.
Answer true if the response is yes, false if no. If cancel is chosen, evaluate cancelBlock.
This is a modal question--the user must respond yes or no."
^ PopUpMenu
confirm: (self dialogStringFromQuery: aString withTitle: titleString)
orCancel: cancelBlock!
----- Method: MVCUIManager>>confirm:title: (in category 'ui requests') -----
confirm: queryString title: titleString
"Put up a yes/no menu with caption queryString, and titleString to label the dialog.
Answer true if the response is yes, false if no. This is a modal question--the user
must respond yes or no."
^PopUpMenu confirm: (self dialogStringFromQuery: queryString withTitle: titleString)
!
----- Method: MVCUIManager>>confirm:title:trueChoice:falseChoice: (in category 'ui requests') -----
confirm: queryString title: titleString trueChoice: trueChoice falseChoice: falseChoice
"Put up a yes/no menu with caption queryString, and titleString to label the dialog.
The actual wording for the two choices will be as provided in the trueChoice and
falseChoice parameters. Answer true if the response is the true-choice, false if it
is the false-choice. This is a modal question -- the user must respond one way or
the other."
^ PopUpMenu
confirm: (self dialogStringFromQuery: queryString withTitle: titleString)
trueChoice: trueChoice
falseChoice: falseChoice!
----- Method: MVCUIManager>>confirm:trueChoice:falseChoice: (in category 'ui requests') -----
confirm: queryString trueChoice: trueChoice falseChoice: falseChoice
"Put up a yes/no menu with caption queryString. The actual wording for the two choices will be as provided in the trueChoice and falseChoice parameters. Answer true if the response is the true-choice, false if it's the false-choice.
This is a modal question -- the user must respond one way or the other."
^PopUpMenu confirm: queryString trueChoice: trueChoice falseChoice: falseChoice!
----- Method: MVCUIManager>>dialogStringFromQuery:withTitle: (in category 'private') -----
dialogStringFromQuery: queryString withTitle: titleString
"In Morphic, it is common to display a simple dialog that has a title bar. In MVC, simple
dialogs are simple. If the UIManager requests a dialog with title, add the title as a simply
decorated line at the top of the dialog text."
^String streamContents: [ :strm |
strm nextPutAll: '- ';
nextPutAll: titleString;
nextPutAll: ' -';
nextPut: Character cr;
nextPutAll: queryString ]
!
----- Method: MVCUIManager>>displayProgress:at:from:to:during: (in category 'ui requests') -----
displayProgress: titleString at: aPoint from: minVal to: maxVal during: workBlock
"Display titleString as a caption over a progress bar while workBlock is evaluated."
| delta savedArea captionText textFrame barFrame outerFrame result range lastW |
barFrame := aPoint - (75@10) corner: aPoint + (75@10).
captionText := DisplayText text: titleString asText allBold.
captionText
foregroundColor: Color black
backgroundColor: Color white.
textFrame := captionText boundingBox insetBy: -4.
textFrame := textFrame align: textFrame bottomCenter
with: barFrame topCenter + (0@2).
outerFrame := barFrame merge: textFrame.
delta := outerFrame amountToTranslateWithin: Display boundingBox.
barFrame := barFrame translateBy: delta.
textFrame := textFrame translateBy: delta.
outerFrame := outerFrame translateBy: delta.
savedArea := Form fromDisplay: outerFrame.
Display fillBlack: barFrame; fillWhite: (barFrame insetBy: 2).
Display fillBlack: textFrame; fillWhite: (textFrame insetBy: 2).
captionText displayOn: Display at: textFrame topLeft + (4@4).
range := maxVal = minVal ifTrue: [1] ifFalse: [maxVal - minVal]. "Avoid div by 0"
lastW := 0.
[result := workBlock value: "Supply the bar-update block for evaluation in the work block"
[:barVal |
| w |
w := ((barFrame width-4) asFloat * ((barVal-minVal) asFloat / range min: 1.0)) asInteger.
w ~= lastW ifTrue: [
Display fillGray: (barFrame topLeft + (2@2) extent: w@16).
lastW := w]]]
ensure: [savedArea displayOn: Display at: outerFrame topLeft].
^result!
----- Method: MVCUIManager>>edit:label:accept: (in category 'ui requests') -----
edit: aText label: labelString accept: anAction
"Open an editor on the given string/text"
Workspace new
acceptContents: aText;
acceptAction: anAction;
openLabel: labelString
!
----- Method: MVCUIManager>>inform: (in category 'ui requests') -----
inform: aString
"Display a message for the user to read and then dismiss"
^PopUpMenu inform: aString!
----- Method: MVCUIManager>>informUser:during: (in category 'ui requests') -----
informUser: aString during: aBlock
"Display a message above (or below if insufficient room) the cursor
during execution of the given block.
UIManager default informUser: 'Just a sec!!' during: [(Delay forSeconds: 1) wait].
"
(SelectionMenu labels: '')
displayAt: Sensor cursorPoint
withCaption: aString
during: aBlock!
----- Method: MVCUIManager>>informUserDuring: (in category 'ui requests') -----
informUserDuring: aBlock
"Display a message above (or below if insufficient room) the cursor
during execution of the given block.
UIManager default informUserDuring:[:bar|
#(one two three) do:[:info|
bar value: info.
(Delay forSeconds: 1) wait]]"
aBlock value:[:string| Transcript cr; show: string].!
----- Method: MVCUIManager>>multiLineRequest:centerAt:initialAnswer:answerHeight: (in category 'ui requests') -----
multiLineRequest: queryString centerAt: aPoint initialAnswer: defaultAnswer answerHeight: answerHeight
"Create a multi-line instance of me whose question is queryString with
the given initial answer. Invoke it centered at the given point, and
answer the string the user accepts. Answer nil if the user cancels. An
empty string returned means that the ussr cleared the editing area and
then hit 'accept'. Because multiple lines are invited, we ask that the user
use the ENTER key, or (in morphic anyway) hit the 'accept' button, to
submit; that way, the return key can be typed to move to the next line."
^FillInTheBlank multiLineRequest: queryString centerAt: aPoint initialAnswer: defaultAnswer answerHeight: answerHeight!
----- Method: MVCUIManager>>newDisplayDepthNoRestore: (in category 'display') -----
newDisplayDepthNoRestore: pixelSize
"Change depths. Check if there is enough space!! , di"
| area need |
pixelSize = Display depth ifTrue: [^ self "no change"].
pixelSize abs < Display depth ifFalse:
["Make sure there is enough space"
area := Display boundingBox area. "pixels"
ScheduledControllers scheduledWindowControllers do:
[:aController | "This should be refined..."
aController view cacheBitsAsTwoTone ifFalse:
[area := area + aController view windowBox area]].
need := (area * (pixelSize abs - Display depth) // 8) "new bytes needed"
+ Smalltalk lowSpaceThreshold.
(Smalltalk garbageCollectMost <= need
and: [Smalltalk garbageCollect <= need])
ifTrue: [self error: 'Insufficient free space']].
Display setExtent: Display extent depth: pixelSize.
ScheduledControllers updateGray.
DisplayScreen startUp!
----- Method: MVCUIManager>>request:initialAnswer: (in category 'ui requests') -----
request: queryString initialAnswer: defaultAnswer
"Create an instance of me whose question is queryString with the given
initial answer. Invoke it centered at the given point, and answer the
string the user accepts. Answer the empty string if the user cancels."
^FillInTheBlank request: queryString initialAnswer: defaultAnswer !
----- Method: MVCUIManager>>request:initialAnswer:centerAt: (in category 'ui requests') -----
request: queryString initialAnswer: defaultAnswer centerAt: aPoint
"Create an instance of me whose question is queryString with the given
initial answer. Invoke it centered at the given point, and answer the
string the user accepts. Answer the empty string if the user cancels."
^ FillInTheBlank request: queryString initialAnswer: defaultAnswer centerAt: aPoint !
----- Method: MVCUIManager>>requestPassword: (in category 'ui requests') -----
requestPassword: queryString
"Create an instance of me whose question is queryString. Invoke it centered
at the cursor, and answer the string the user accepts. Answer the empty
string if the user cancels."
^FillInTheBlank requestPassword: queryString!
----- Method: MVCUIManager>>restoreDisplay (in category 'display') -----
restoreDisplay
"Restore the bits on Display"
Project current ifNotNil:[:p| p invalidate; restore].!
----- Method: MVCUIManager>>restoreDisplayAfter: (in category 'display') -----
restoreDisplayAfter: aBlock
"Evaluate the block, wait for a mouse click, and then restore the screen."
aBlock value.
Sensor waitButton.
self restoreDisplay!
Chris Muller uploaded a new version of UpdateStream to project Squeak 4.6:
http://source.squeak.org/squeak46/UpdateStream-nice.4.mcz
==================== Summary ====================
Name: UpdateStream-nice.4
Author: nice
Time: 7 December 2013, 12:08:41.719 am
UUID: 5fcdedce-88aa-469a-bf8b-32820f051c4f
Ancestors: UpdateStream-fbs.3
Move some updateStream hooks in UpdateStream package
This does not make the package properly removeable, because those hooks often are hardcoded
So after removal, there will be some unimplemented sends
==================== Snapshot ====================
SystemOrganization addCategory: #UpdateStream!
----- Method: ServerDirectory class>>convertGroupNames (in category '*UpdateStream-server groups') -----
convertGroupNames
"ServerDirectory convertGroupNames"
self servers do: [:each | each convertGroupName]!
----- Method: ServerDirectory class>>groupNames (in category '*UpdateStream-server groups') -----
groupNames
"Return the names of all registered groups of servers, including individual servers not in any group."
"ServerDirectory groupNames"
| names |
names := Set new.
self servers do: [:server |
names add: server groupName].
^names asSortedArray
!
----- Method: ServerDirectory class>>serverInGroupNamed: (in category '*UpdateStream-server groups') -----
serverInGroupNamed: groupName
"Return the first (available) server in the group of this name."
| servers |
servers := self serversInGroupNamed: groupName.
servers isEmpty
ifTrue: [self error: 'No server found in group "' , groupName asString , '".'].
^servers first!
----- Method: ServerDirectory class>>serversInGroupNamed: (in category '*UpdateStream-server groups') -----
serversInGroupNamed: nameString
"Return the servers in the group of this name."
"ServerDirectory serversInGroupNamed: 'Squeak Public Updates' "
^self servers values select: [:server |
nameString = server groupName].
!
----- Method: ServerDirectory>>checkNames: (in category '*UpdateStream-updating') -----
checkNames: list
"Look at these names for update and see if they are OK"
list do: [:local |
(local count: [:char | char == $.]) > 1 ifTrue: [
self inform: 'File name ',local,'
may not have more than one period'.
^ false].
local size > 26 ifTrue: ["allows for 5 digit update numbers"
self inform: 'File name ',local,'
is too long. Please rename it.'.
^ false].
(local at: 1) isDigit ifTrue: [
self inform: 'File name ',local,'
may not begin with a number'.
^ false].
(local findDelimiters: '%/* ' startingAt: 1) <= local size ifTrue: [
self inform: 'File name ',local,'
may not contain % / * or space'.
^ false]].
^ true
!
----- Method: ServerDirectory>>checkServersWithPrefix:andParseListInto: (in category '*UpdateStream-updating') -----
checkServersWithPrefix: prefix andParseListInto: listBlock
"Check that all servers are up and have the latest Updates.list.
Warn user when can't write to a server that can still be read.
The contents of updates.list is parsed into {{vers. {fileNames*}}*},
and returned via the listBlock."
| serverList updateLists listContents maxSize outOfDateServers |
serverList := self serversInGroup.
serverList isEmpty
ifTrue: [^Array new].
updateLists := Dictionary new.
serverList do: [:updateServer |
[listContents := updateServer getFileNamed: prefix , 'updates.list'.
updateLists at: updateServer put: listContents]
on: Error
do: [:ex |
UIManager default chooseFrom: #('Cancel entire update')
title: 'Server ', updateServer moniker,
' is unavailable.\Please consider phoning the administator.\' withCRs, listContents.
^Array new]].
maxSize := (updateLists collect: [:each | each size]) max.
outOfDateServers := updateLists keys select: [:updateServer |
(updateLists at: updateServer) size < maxSize].
outOfDateServers do: [:updateServer |
(self outOfDate: updateServer) ifTrue: [^Array new]].
listBlock value: (UpdateStreamDownloader default parseListContents: listContents).
serverList removeAll: outOfDateServers.
^serverList
!
----- Method: ServerDirectory>>closeGroup (in category '*UpdateStream-server groups') -----
closeGroup
"Close connection with all servers in the group."
self serversInGroup do: [:aDir | aDir quit].
!
----- Method: ServerDirectory>>convertGroupName (in category '*UpdateStream-server groups') -----
convertGroupName
group
ifNotNil: [self groupName: self groupName]!
----- Method: ServerDirectory>>copyUpdatesNumbered:toVersion: (in category '*UpdateStream-updating') -----
copyUpdatesNumbered: selectList toVersion: otherVersion
"Into the section of updates.list corresponding to otherVersion,
copy all the fileNames from this version matching the selectList."
"
(ServerDirectory serverInGroupNamed: 'Disney Internal Updates*')
copyUpdatesNumbered: #(4411 4412) to version: 'Squeak3.1beta'.
"
| myServers updateStrm indexPrefix version versIndex lastNum otherVersIndex additions outOfOrder listContents |
self openGroup.
indexPrefix := (self groupName includes: $*)
ifTrue: [(self groupName findTokens: ' ') first] "special for internal updates"
ifFalse: ['']. "normal"
myServers := self checkServersWithPrefix: indexPrefix
andParseListInto: [:x | listContents := x].
myServers size = 0 ifTrue: [self closeGroup. ^ self].
version := SystemVersion current version.
versIndex := (listContents collect: [:pair | pair first]) indexOf: version.
versIndex = 0 ifTrue:
[self inform: 'There is no section in updates.list for your version'.
self closeGroup. ^ nil]. "abort"
otherVersIndex := (listContents collect: [:pair | pair first]) indexOf: otherVersion.
otherVersIndex = 0 ifTrue:
[self inform: 'There is no section in updates.list for the target version'.
self closeGroup. ^ nil]. "abort"
versIndex < listContents size ifTrue:
[(self confirm: 'This system, ', version ,
' is not the latest version.\OK to copy updates from that old version?' withCRs)
ifFalse: [self closeGroup. ^ nil]]. "abort"
"Append all fileNames in my list that are not in the export list"
additions := OrderedCollection new.
outOfOrder := OrderedCollection new.
lastNum := (listContents at: otherVersIndex) last isEmpty
ifTrue: [0] "no checking if the current list is empty"
ifFalse: [(listContents at: otherVersIndex) last last initialIntegerOrNil].
(listContents at: versIndex) last do:
[:fileName | | seq | seq := fileName initialIntegerOrNil.
(selectList includes: seq) ifTrue:
[seq > lastNum
ifTrue: [additions addLast: fileName]
ifFalse: [outOfOrder addLast: seq]]].
outOfOrder isEmpty ifFalse:
[UIManager default inform: 'Updates numbered ' , outOfOrder asArray printString,
' are out of order.\ The last update in ' withCRs,
otherVersion, ' is ', lastNum printString,
'.\No update will take place.' withCRs.
self closeGroup. ^ nil]. "abort"
"Save old copy of updates.list on local disk"
FileDirectory default deleteFileNamed: indexPrefix , 'updates.list.bk'.
UpdateStreamDownloader default writeList: listContents toStream: (FileStream fileNamed: indexPrefix , 'updates.list.bk').
"Write a new copy of updates.list on all servers..."
listContents at: otherVersIndex put:
{otherVersion. (listContents at: otherVersIndex) last , additions}.
updateStrm := ReadStream on:
(String streamContents: [:s | Utilities writeList: listContents toStream: s]).
myServers do:
[:aServer |
updateStrm reset.
aServer putFile: updateStrm named: indexPrefix , 'updates.list' retry: true.
Transcript show: 'Update succeeded on server ', aServer moniker; cr].
self closeGroup.
Transcript cr; show: 'Be sure to test your new update!!'; cr.
!
----- Method: ServerDirectory>>exportUpdatesExcept: (in category '*UpdateStream-updating') -----
exportUpdatesExcept: skipList
"Into the section of updates.list corresponding to this version,
copy all the fileNames in the named updates.list for this group
that are more recently numbered."
"
(ServerDirectory serverInGroupNamed: 'Disney Internal Updates*')
exportUpdatesExcept: #(3959).
"
| myServers updateStrm response indexPrefix version versIndex lastNum expContents expVersIndex additions listContents |
self openGroup.
indexPrefix := (self groupName includes: $*)
ifTrue: [(self groupName findTokens: ' ') first] "special for internal updates"
ifFalse: ['']. "normal"
myServers := self checkServersWithPrefix: indexPrefix
andParseListInto: [:x | listContents := x].
myServers size = 0 ifTrue: [self closeGroup. ^ self].
version := SystemVersion current version.
versIndex := (listContents collect: [:pair | pair first]) indexOf: version.
versIndex = 0 ifTrue:
[self inform: 'There is no section in updates.list for your version'.
self closeGroup. ^ nil]. "abort"
versIndex < listContents size ifTrue:
[response := UIManager default
chooseFrom: #('Make update from an older version' 'Cancel update')
title: 'This system, ', SystemVersion current version,
' is not the latest version'.
response = 1 ifFalse: [self closeGroup. ^ nil]]. "abort"
"Get the old export updates.list."
expContents := UpdateStreamDownloader default parseListContents:
(myServers first getFileNamed: 'updates.list').
expVersIndex := (expContents collect: [:pair | pair first]) indexOf: version.
expVersIndex = 0 ifTrue:
[self inform: 'There is no section in updates.list for your version'.
self closeGroup. ^ nil]. "abort"
lastNum := (expContents at: expVersIndex) last isEmpty
ifTrue: [0] "no checking if the current list is empty"
ifFalse: [(expContents at: expVersIndex) last last initialIntegerOrNil].
"Save old copy of updates.list on local disk"
FileDirectory default deleteFileNamed: 'updates.list.bk'.
UpdateStreamDownloader default writeList: expContents toStream: (FileStream fileNamed: 'updates.list.bk').
"Append all fileNames in my list that are not in the export list"
additions := OrderedCollection new.
(listContents at: versIndex) last do:
[:fileName | | seq | seq := fileName initialIntegerOrNil.
(seq > lastNum and: [(skipList includes: seq) not]) ifTrue:
[additions addLast: fileName]].
expContents at: expVersIndex put:
{version. (expContents at: expVersIndex) last , additions}.
(self confirm: 'Do you really want to export ' , additions size printString , ' recent updates?')
ifFalse: [self closeGroup. ^ nil]. "abort"
"Write a new copy of updates.list on all servers..."
updateStrm := ReadStream on:
(String streamContents: [:s | Utilities writeList: expContents toStream: s]).
myServers do:
[:aServer |
updateStrm reset.
aServer putFile: updateStrm named: 'updates.list' retry: true.
Transcript show: 'Update succeeded on server ', aServer moniker; cr].
self closeGroup.
Transcript cr; show: 'Be sure to test your new update!!'; cr.
!
----- Method: ServerDirectory>>openGroup (in category '*UpdateStream-server groups') -----
openGroup
"Open all servers in the group. Don't forget to close later."
self serversInGroup do: [:aDir | aDir wakeUp].
!
----- Method: ServerDirectory>>outOfDate: (in category '*UpdateStream-updating') -----
outOfDate: aServer
"Inform the user that this server does not have a current version of 'Updates.list' Return true if the user does not want any updates to happen."
| response |
response := UIManager default chooseFrom: #('Install on others' 'Cancel entire update')
title: 'The server ', aServer moniker, ' is not up to date.
Please store the missing updates maually.'.
^ response ~= 1!
----- Method: ServerDirectory>>putUpdate: (in category '*UpdateStream-updating') -----
putUpdate: fileStrm
"Put this file out as an Update on the servers of my group. Each version of the system may have its own set of update files, or they may all share the same files. 'updates.list' holds the master list. Each update is a fileIn whose name begins with a number. See Utilities class readServerUpdatesThrough:saveLocally:updateImage:.
When two sets of updates are stored on the same directory, one of them has a * in its
serverUrls description. When that is true, the first word of the description is put on
the front of 'updates.list', and that index file is used."
| myServers updateStrm newName response localName seq indexPrefix listContents version versIndex lastNum stripped |
localName := fileStrm localName.
fileStrm size = 0 ifTrue:
[^ self inform: 'That file has zero bytes!! May have a new name.'].
(fileStrm contentsOfEntireFile includes: Character linefeed)
ifTrue: [self notifyWithLabel: 'That file contains linefeeds. Proceed if...
you know that this is okay (e.g. the file contains raw binary data).'].
fileStrm reset.
(self checkNames: {localName}) ifFalse: [^ nil]. "illegal characters"
response := UIManager default chooseFrom: #('Install update' 'Cancel update')
title: 'Do you really want to broadcast the file ', localName,
'\to every Squeak user who updates from ' withCRs, self groupName, '?'.
response = 1 ifFalse: [^ nil]. "abort"
self openGroup.
indexPrefix := (self groupName includes: $*)
ifTrue: [(self groupName findTokens: ' ') first] "special for internal updates"
ifFalse: ['']. "normal"
myServers := self checkServersWithPrefix: indexPrefix
andParseListInto: [:x | listContents := x].
myServers size = 0 ifTrue: [self closeGroup. ^ self].
version := SystemVersion current version.
versIndex := (listContents collect: [:pair | pair first]) indexOf: version.
versIndex = 0 ifTrue:
[self inform: 'There is no section in updates.list for your version'.
self closeGroup. ^ nil]. "abort"
"A few affirmations..."
versIndex < listContents size ifTrue:
[(self confirm: 'This system, ', version ,
' is not the latest version.\Make update for an older version?' withCRs)
ifFalse: [self closeGroup. ^ nil]]. "abort"
(listContents at: versIndex) last isEmpty ifTrue:
[(self confirm: 'Please confirm that you mean to issue the first update for ' ,
version , '\(otherwise something is wrong).' withCRs)
ifFalse: [self closeGroup. ^ nil]].
"We now determine next update number to be max of entire index"
lastNum := listContents inject: 0 into:
[:max :pair | pair last isEmpty
ifTrue: [max]
ifFalse: [max max: pair last last initialIntegerOrNil]].
"Save old copy of updates.list on local disk"
FileDirectory default deleteFileNamed: indexPrefix , 'updates.list.bk'.
UpdateStreamDownloader default writeList: listContents toStream: (FileStream fileNamed: indexPrefix , 'updates.list.bk').
"append name to updates with new sequence number"
seq := (lastNum + 1) printString padded: #left to: 4 with: $0.
"strip off any old seq number"
stripped := localName copyFrom: (localName findFirst: [:c | c isDigit not]) to: localName size.
newName := seq , stripped.
listContents at: versIndex put:
{version. (listContents at: versIndex) last copyWith: newName}.
"Write a new copy on all servers..."
updateStrm := ReadStream on:
(String streamContents: [:s | Utilities writeList: listContents toStream: s]).
myServers do:
[:aServer |
fileStrm reset. "reopen"
aServer putFile: fileStrm named: newName retry: true.
updateStrm reset.
aServer putFile: updateStrm named: indexPrefix , 'updates.list' retry: true.
Transcript show: 'Update succeeded on server ', aServer moniker; cr].
self closeGroup.
Transcript cr; show: 'Be sure to test your new update!!'; cr.
"rename the file locally (may fail)"
fileStrm directory rename: localName toBe: newName.
!
----- Method: ServerDirectory>>putUpdateMulti:fromDirectory: (in category '*UpdateStream-updating') -----
putUpdateMulti: list fromDirectory: updateDirectory
"Put these files out as an Update on the servers of my group. List is an array of local file names with or without number prefixes. Each version of the system has its own set of update files. 'updates.list' holds the master list. Each update is a fileIn whose name begins with a number. See Utilities class absorbUpdatesFromServer."
| myServers updateStrm lastNum response newNames numStr indexPrefix version versIndex listContents |
(self checkNames: (list collect: "Check the names without their numbers"
[:each | each copyFrom: (each findFirst: [:c | c isDigit not]) to: each size]))
ifFalse: [^ nil].
response := UIManager default chooseFrom: #('Install update' 'Cancel update')
title: 'Do you really want to broadcast ', list size printString, ' updates',
'\to every Squeak user who updates from ' withCRs, self groupName, '?'.
response = 1 ifFalse: [^ nil]. "abort"
self openGroup.
indexPrefix := (self groupName includes: $*)
ifTrue: [(self groupName findTokens: ' ') first] "special for internal updates"
ifFalse: ['']. "normal"
myServers := self checkServersWithPrefix: indexPrefix
andParseListInto: [:x | listContents := x].
myServers size = 0 ifTrue: [self closeGroup. ^ self].
version := SystemVersion current version.
versIndex := (listContents collect: [:pair | pair first]) indexOf: version.
versIndex = 0 ifTrue:
[self inform: 'There is no section in updates.list for your version'.
self closeGroup. ^ nil]. "abort"
lastNum := (listContents at: versIndex) last last initialIntegerOrNil.
versIndex < listContents size ifTrue:
[response := UIManager default chooseFrom: #('Make update for an older version' 'Cancel update')
title: 'This system, ', SystemVersion current version,
' is not the latest version'.
response = 1 ifFalse: [self closeGroup. ^ nil].
numStr := UIManager default
request: 'Please confirm or change the starting update number'
initialAnswer: (lastNum+1) printString.
lastNum := numStr asNumber - 1]. "abort"
"Save old copy of updates.list on local disk"
FileDirectory default deleteFileNamed: indexPrefix , 'updates.list.bk'.
UpdateStreamDownloader default writeList: listContents toStream: (FileStream fileNamed: indexPrefix , 'updates.list.bk').
"Append names to updates with new sequence numbers"
newNames := list with: (lastNum+1 to: lastNum+list size) collect:
[:each :num | | stripped seq | seq := num printString padded: #left to: 4 with: $0.
"strip off any old seq number"
stripped := each copyFrom: (each findFirst: [:c | c isDigit not]) to: each size.
seq , stripped].
listContents at: versIndex put:
{version. (listContents at: versIndex) second , newNames}.
"Write a new copy on all servers..."
updateStrm := ReadStream on:
(String streamContents: [:s | Utilities writeList: listContents toStream: s]).
myServers do:
[:aServer |
list doWithIndex: [:local :ind | | file |
file := updateDirectory oldFileNamed: local.
aServer putFile: file named: (newNames at: ind) retry: true.
file close].
updateStrm reset.
aServer putFile: updateStrm named: indexPrefix , 'updates.list' retry: true.
Transcript show: 'Update succeeded on server ', aServer moniker; cr].
self closeGroup.
Transcript cr; show: 'Be sure to test your new update!!'; cr.
"rename the file locally"
list with: newNames do:
[:local :newName | updateDirectory rename: local toBe: newName].
!
----- Method: ServerDirectory>>serversInGroup (in category '*UpdateStream-server groups') -----
serversInGroup
^self groupName
ifNil: [Array with: self]
ifNotNil: [self class serversInGroupNamed: self groupName]!
----- Method: ServerDirectory>>updateInstallVersion: (in category '*UpdateStream-updating') -----
updateInstallVersion: newVersion
"For each server group, ask whether we want to put the new version marker (eg 'Squeak2.3') at the end of the file. Current version of Squeak must be the old one when this is done.
ServerDirectory new updateInstallVersion: 'Squeak9.9test'
"
| myServers updateStrm names choice indexPrefix listContents version versIndex |
[names := ServerDirectory groupNames asSortedArray.
choice := UIManager default chooseFrom: names values: names.
choice == nil]
whileFalse:
[indexPrefix := (choice endsWith: '*')
ifTrue: [(choice findTokens: ' ') first] "special for internal updates"
ifFalse: ['']. "normal"
myServers := (ServerDirectory serverInGroupNamed: choice)
checkServersWithPrefix: indexPrefix
andParseListInto: [:x | listContents := x].
myServers size = 0 ifTrue: [^ self].
version := SystemVersion current version.
versIndex := (listContents collect: [:pair | pair first]) indexOf: version.
versIndex = 0 ifTrue:
[^ self inform: 'There is no section in updates.list for your version']. "abort"
"Append new version to updates following my version"
listContents := listContents copyReplaceFrom: versIndex+1 to: versIndex with: {{newVersion. {}}}.
updateStrm := ReadStream on:
(String streamContents: [:s | UpdateStreamDownloader default writeList: listContents toStream: s]).
myServers do:
[:aServer | updateStrm reset.
aServer putFile: updateStrm named: indexPrefix ,'updates.list'.
Transcript cr; show: indexPrefix ,'updates.list written on server ', aServer moniker].
self closeGroup]!
----- Method: ImageReadWriter class>>formFromServerFile: (in category '*UpdateStream') -----
formFromServerFile: fileName
"Answer a ColorForm stored on the file with the given name. Meant to be called from during the getting of updates from the server. That assures that (UpdateStreamDownloader default serverUrls) returns the right group of servers."
| urls |
urls := UpdateStreamDownloader default serverUrls collect:
[:url | url, fileName]. " fileName starts with: 'updates/' "
urls do: [:aURL | | form doc |
(fileName findTokens: '.') last asLowercase = 'gif' ifTrue: [
form := HTTPSocket httpGif: aURL.
form = (ColorForm extent: 20@20 depth: 8)
ifTrue: [self inform: 'The file ',aURL,' is ill formed.'].
^ form].
(fileName findTokens: '.') last asLowercase = 'bmp' ifTrue: [
doc := HTTPSocket httpGet: aURL accept: 'image/bmp'.
form := Form fromBMPFile: doc.
doc close.
form ifNil: [self inform: 'The file ',aURL,' is ill formed.'. ^ Form new]
ifNotNil: [^ form]].
self inform: 'File ', fileName, 'does not end with .gif or .bmp'].
self inform: 'That file not found on any server we know'.!
Object subclass: #UpdateStreamDownloader
instanceVariableNames: ''
classVariableNames: 'PromptForUpdateServer UpdateDownloader UpdateSavesFile UpdateUrlLists'
poolDictionaries: ''
category: 'UpdateStream'!
----- Method: UpdateStreamDownloader class>>applyUpdatesFromDisk (in category 'fetching updates') -----
applyUpdatesFromDisk
"UpdateStreamDownloader applyUpdatesFromDisk"
"compute highest update number"
| updateDirectory updateNumbers |
updateDirectory := self getUpdateDirectoryOrNil.
updateDirectory
ifNil: [^ self].
updateNumbers := updateDirectory fileNames
collect: [:fn | fn initialIntegerOrNil]
thenSelect: [:fn | fn notNil].
self
applyUpdatesFromDiskToUpdateNumber: (updateNumbers
inject: 0
into: [:max :num | max max: num])
stopIfGap: false!
----- Method: UpdateStreamDownloader class>>applyUpdatesFromDiskToUpdateNumber:stopIfGap: (in category 'fetching updates') -----
applyUpdatesFromDiskToUpdateNumber: lastUpdateNumber stopIfGap: stopIfGapFlag
"To use this mechanism, be sure all updates you want to have considered
are in a folder named 'updates' which resides in the same directory as
your image. Having done that, simply evaluate:
UpdateStreamDownloader applyUpdatesFromDiskToUpdateNumber: 1234 stopIfGap: false
and all numbered updates <= lastUpdateNumber not yet in the image will
be loaded in numerical order."
| previousHighest currentUpdateNumber done fileNames aMessage updateDirectory loaded |
updateDirectory := self getUpdateDirectoryOrNil.
updateDirectory ifNil: [^ self].
previousHighest := SystemVersion current highestUpdate.
currentUpdateNumber := previousHighest.
done := false.
loaded := 0.
[done]
whileFalse: [currentUpdateNumber := currentUpdateNumber + 1.
currentUpdateNumber > lastUpdateNumber
ifTrue: [done := true]
ifFalse: [fileNames := updateDirectory fileNamesMatching: currentUpdateNumber printString , '*'.
fileNames size > 1
ifTrue: [^ self inform: 'ambiguity -- two files both start with ' , currentUpdateNumber printString , '
(at this point it is probably best to remedy
the situation on disk, then try again.)'].
fileNames size = 0
ifTrue: [Transcript cr; show: 'gap in updates from disk for update number '; print: currentUpdateNumber; show: ' found...'.
done := stopIfGapFlag]
ifFalse: [ChangeSet
newChangesFromStream: (updateDirectory readOnlyFileNamed: fileNames first)
named: fileNames first.
SystemVersion current registerUpdate: currentUpdateNumber.
loaded := loaded + 1]]].
aMessage := loaded = 0
ifTrue: ['No new updates found.']
ifFalse: [loaded printString , ' update(s) loaded.'].
self inform: aMessage , '
Highest numbered update is now ' , (currentUpdateNumber - 1) printString , '.'!
----- Method: UpdateStreamDownloader class>>assureAbsenceOfUnstableUpdateStream (in category 'server urls') -----
assureAbsenceOfUnstableUpdateStream
"Check to see if the unstable Updates stream is in the list; if it is, *remove* it. This is the *opposite* of #assureAvailabilityOfUnstableUpdateStream"
UpdateUrlLists ifNil: [UpdateUrlLists := OrderedCollection new].
UpdateUrlLists := UpdateUrlLists select:
[:pair | pair first ~= 'Unstable Updates*']
"UpdateStreamDownloader assureAbsenceOfUnstableUpdateStream"!
----- Method: UpdateStreamDownloader class>>assureAvailabilityOfSqueakPublicUpdateStream (in category 'server urls') -----
assureAvailabilityOfSqueakPublicUpdateStream
"Check to see if the Squeak public Updates stream is in the list; if not, add it"
UpdateUrlLists ifNil: [UpdateUrlLists := OrderedCollection new].
UpdateUrlLists do:
[:pair | (pair first = 'Squeak Public Updates') ifTrue: [^ self]].
UpdateUrlLists addFirst: #('Squeak Public Updates' #('ftp.squeak.org/'))
"UpdateStreamDownloader assureAvailabilityOfSqueakPublicUpdateStream"!
----- Method: UpdateStreamDownloader class>>assureAvailabilityOfUnstableUpdateStream (in category 'server urls') -----
assureAvailabilityOfUnstableUpdateStream
"Check to see if the unstable Updates stream is in the list; if not, add it"
UpdateUrlLists ifNil: [UpdateUrlLists := OrderedCollection new].
UpdateUrlLists do:
[:pair | (pair first = 'Unstable Updates*') ifTrue: [^ self]].
UpdateUrlLists addFirst: #('Unstable Updates*' #('squeak.cs.uiuc.edu/Squeak2.0/' 'update.squeakfoundation.org/external/'))
"UpdateStreamDownloader assureAvailabilityOfUnstableUpdateStream"!
----- Method: UpdateStreamDownloader class>>broadcastUpdatesFrom:to:except: (in category 'fetching updates') -----
broadcastUpdatesFrom: n1 to: n2 except: skipList
"
Note: This method takes its list of files from the directory named 'updates',
which will have been created and filled by, eg,
UpdateStreamDownloader readServerUpdatesSaveLocally: true updateImage: true.
These can then be rebroadcast to any server using, eg,
UpdateStreamDownloader broadcastUpdatesFrom: 1 to: 9999 except: #(223 224).
If the files are already on the server, and it is only a matter
of copying them to the index for a different version, then use...
(ServerDirectory serverInGroupNamed: 'SqC Internal Updates*')
exportUpdatesExcept: #().
"
| fileNames fileNamesInOrder names choice file updateDirectory |
updateDirectory := FileDirectory default directoryNamed: 'updates'.
fileNames := updateDirectory fileNames select:
[:n | n first isDigit
and: [(n initialIntegerOrNil between: n1 and: n2)
and: [(skipList includes: n initialIntegerOrNil) not]]].
(file := fileNames select: [:n | (n occurrencesOf: $.) > 1]) size > 0
ifTrue: [self halt: file first , ' has multiple periods'].
fileNamesInOrder := fileNames asSortedCollection:
[:a :b | a initialIntegerOrNil < b initialIntegerOrNil].
names := ServerDirectory groupNames asSortedArray.
choice := UIManager default chooseFrom: names values: names.
choice == nil ifTrue: [^ self].
(ServerDirectory serverInGroupNamed: choice)
putUpdateMulti: fileNamesInOrder fromDirectory: updateDirectory
!
----- Method: UpdateStreamDownloader class>>chooseUpdateList (in category 'fetching updates') -----
chooseUpdateList
"When there is more than one set of update servers, let the user choose which we will update from. Put it at the front of the list. Return false if the user aborted. If the preference #promptForUpdateServer is false, then suppress that prompt, in effect using the same server choice that was used the previous time (a convenience for those of us who always answer the same thing to the prompt.)"
| index him |
((UpdateUrlLists size > 1) and: [self promptForUpdateServer])
ifTrue:
[index := UIManager default
chooseFrom: (UpdateUrlLists collect: [:each | each first])
lines: #()
title: 'Choose a group of servers\from which to fetch updates.' translated withCRs.
index > 0 ifTrue:
[him := UpdateUrlLists at: index.
UpdateUrlLists removeAt: index.
UpdateUrlLists addFirst: him].
^ index > 0].
^ true!
----- Method: UpdateStreamDownloader class>>default (in category 'accessing') -----
default
"Answer the default downloader. Currently, all methods are at class side, so it'll be ourself"
^self!
----- Method: UpdateStreamDownloader class>>extractThisVersion: (in category 'fetching updates') -----
extractThisVersion: list
"Pull out the part of the list that applies to this version."
| listContents version versIndex |
listContents := self parseListContents: list.
version := SystemVersion current version.
versIndex := (listContents collect: [:pair | pair first]) indexOf: version.
versIndex = 0 ifTrue: [^ Array new]. "abort"
^ (listContents at: versIndex) last!
----- Method: UpdateStreamDownloader class>>fileInFromUpdatesFolder: (in category 'fetching updates') -----
fileInFromUpdatesFolder: numberList
"File in a series of updates with the given updates numbers, from the updates folder in the default directory. The file-ins are done in numeric order, even if numberList was not sorted upon entry.
This is useful for test-driving the retrofitting of a possibly discontinguous list of updates from an alpha version back to a stable release.
UpdateStreamDownloader fileInFromUpdatesFolder: #(4745 4746 4747 4748 4749 4750 4751 4752 4754 4755 4761 4762 4767 4769).
"
| fileNames fileNamesInOrder file updateDirectory |
updateDirectory := FileDirectory default directoryNamed: 'updates'.
fileNames := updateDirectory fileNames select:
[:n | n first isDigit
and: [numberList includes: n initialIntegerOrNil]].
(file := fileNames select: [:n | (n occurrencesOf: $.) > 1]) size > 0
ifTrue: [self error: file first , ' has multiple periods'].
fileNamesInOrder := fileNames asSortedCollection:
[:a :b | a initialIntegerOrNil < b initialIntegerOrNil].
fileNamesInOrder do:
[:aFileName | (updateDirectory readOnlyFileNamed: aFileName) fileIntoNewChangeSet]!
----- Method: UpdateStreamDownloader class>>getUpdateDirectoryOrNil (in category 'fetching updates') -----
getUpdateDirectoryOrNil
^ (FileDirectory default directoryNames includes: 'updates')
ifTrue: [FileDirectory default directoryNamed: 'updates']
ifFalse: [self inform: 'Error: cannot find "updates" folder'.
nil]!
----- Method: UpdateStreamDownloader class>>lastUpdateNum: (in category 'fetching updates') -----
lastUpdateNum: updatesFileStrm
"Look in the Updates file and see what the last sequence number is. Warn the user if the version it is under is not this image's version."
| verIndex seqIndex char ver seqNum |
verIndex := seqIndex := 0. "last # starting a line and last digit starting a line"
seqNum := 0.
updatesFileStrm reset; ascii.
[char := updatesFileStrm next.
updatesFileStrm atEnd] whileFalse: [
char == Character cr ifTrue: [
updatesFileStrm peek == $# ifTrue: [verIndex := updatesFileStrm position +1.
seqIndex = 0 ifFalse: ["See if last num of old version if biggest so far"
updatesFileStrm position: seqIndex.
ver := SmallInteger readFrom: updatesFileStrm.
seqNum := seqNum max: ver.
updatesFileStrm position: verIndex-1]].
updatesFileStrm peek isDigit ifTrue: [seqIndex := updatesFileStrm position]]].
seqIndex = 0 ifFalse: ["See if last num of old version if biggest so far"
updatesFileStrm position: seqIndex.
ver := SmallInteger readFrom: updatesFileStrm.
seqNum := seqNum max: ver.
updatesFileStrm setToEnd].
^ seqNum!
----- Method: UpdateStreamDownloader class>>newUpdatesOn:special:throughNumber: (in category 'fetching updates') -----
newUpdatesOn: serverList special: indexPrefix throughNumber: aNumber
"Return a list of fully formed URLs of update files we do not yet have. Go to the listed servers and look at the file 'updates.list' for the names of the last N update files. We look backwards for the first one we have, and make the list from there. tk 9/10/97
No updates numbered higher than aNumber (if it is not nil) are returned "
| existing out maxNumber |
maxNumber := aNumber ifNil: [99999].
out := OrderedCollection new.
existing := SystemVersion current updates.
serverList do: [:server | | raw doc list char |
doc := HTTPClient httpGet: 'http://' , server,indexPrefix,'updates.list'.
"test here for server being up"
doc class == RWBinaryOrTextStream ifTrue:
[raw := doc reset; contents. "one file name per line"
list := self extractThisVersion: raw.
list reverseDo: [:fileName | | ff itsNumber |
ff := (fileName findTokens: '/') last. "allow subdirectories"
itsNumber := ff initialIntegerOrNil.
(existing includes: itsNumber)
ifFalse:
[
(itsNumber == nil or: [itsNumber <= maxNumber])
ifTrue:
[out addFirst: 'http://' , server, fileName]]
ifTrue: [^ out]].
((out size > 0) or: [char := doc reset; skipSeparators; next.
(char == $*) | (char == $#)]) ifTrue:
[^ out "we have our list"]]. "else got error msg instead of file"
"Server was down, try next one"].
self inform: 'All code update servers seem to be unavailable'.
^ out!
----- Method: UpdateStreamDownloader class>>objectStrmFromUpdates: (in category 'fetching updates') -----
objectStrmFromUpdates: fileName
"Go to the known servers and look for this file in the updates folder. It is an auxillery file, like .morph or a .gif. Return a RWBinaryOrTextStream on it. Meant to be called from during the getting of updates from the server. That assures that (UpdateStreamDownloader serverUrls) returns the right group of servers."
Cursor wait showWhile:
[ | urls |
urls := UpdateStreamDownloader serverUrls collect: [:url | url, 'updates/', fileName].
urls do: [:aUrl | | doc |
doc := HTTPSocket httpGet: aUrl accept: 'application/octet-stream'.
"test here for server being up"
doc class == RWBinaryOrTextStream ifTrue: [^ doc reset]]].
self inform: 'All update servers are unavailable, or bad file name'.
^ nil!
----- Method: UpdateStreamDownloader class>>parseListContents: (in category 'fetching updates') -----
parseListContents: listContents
| sections vers strm line fileNames |
"Parse the contents of updates.list into {{vers. {fileNames*}}*}, and return it."
sections := OrderedCollection new.
fileNames := OrderedCollection new: 1000.
vers := nil.
strm := ReadStream on: listContents.
[strm atEnd] whileFalse:
[line := strm nextLine.
line size > 0 ifTrue:
[line first = $#
ifTrue: [vers ifNotNil: [sections addLast: {vers. fileNames asArray}].
"Start a new section"
vers := line allButFirst.
fileNames resetTo: 1]
ifFalse: [line first = $* ifFalse: [fileNames addLast: line]]]].
vers ifNotNil: [sections addLast: {vers. fileNames asArray}].
^ sections asArray
" TEST:
| list |
list := UpdateStreamDownloader parseListContents: (FileStream oldFileNamed: 'updates.list') contentsOfEntireFile.
list = (UpdateStreamDownloader parseListContents: (String streamContents: [:s | UpdateStreamDownloader writeList: list toStream: s]))
ifFalse: [self error: 'test failed']
ifTrue: [self inform: 'test OK']
"!
----- Method: UpdateStreamDownloader class>>position:atVersion: (in category 'fetching updates') -----
position: updateStrm atVersion: version
"Set the stream to the end of the last line of updates names for this version. Usually the end of the file. We will add a new update name. Return the contents of the rest of the file."
| char foundIt where data |
updateStrm reset; ascii.
foundIt := false.
[char := updateStrm next.
updateStrm atEnd] whileFalse: [
(char == Character cr or: [char == Character lf]) ifTrue: [
updateStrm peek == $# ifTrue: [
foundIt ifTrue: ["Next section"
where := updateStrm position.
data := updateStrm upTo: (255 asCharacter).
updateStrm position: where.
^ data]. "won't be found -- copy all the way to the end"
updateStrm next.
(updateStrm nextMatchAll: version) ifTrue: [
(updateStrm atEnd or: [(updateStrm peek = Character cr) |
(updateStrm peek = Character lf)]) ifTrue: [
foundIt := true
]]]]].
foundIt ifTrue: [
updateStrm setToEnd.
^ ''].
self error: 'The current version does not have a section in the Updates file'.
!
----- Method: UpdateStreamDownloader class>>promptForUpdateServer (in category 'preferences') -----
promptForUpdateServer
<preference: 'Prompt for update server'
category: 'updates'
description: 'If false, the prompt for server choice when updating code from the server is suppressed. Set this to false to leave the server choice unchanged from update to update.'
type: #Boolean>
^PromptForUpdateServer ifNil: [ false ]!
----- Method: UpdateStreamDownloader class>>promptForUpdateServer: (in category 'preferences') -----
promptForUpdateServer: aBoolean
PromptForUpdateServer := aBoolean!
----- Method: UpdateStreamDownloader class>>readNextUpdateFromServer (in category 'fetching updates') -----
readNextUpdateFromServer
"UpdateStreamDownloader readNextUpdateFromServer"
self updateFromServerThroughUpdateNumber: (ChangeSet highestNumberedChangeSet + 1)!
----- Method: UpdateStreamDownloader class>>readNextUpdatesFromDisk: (in category 'fetching updates') -----
readNextUpdatesFromDisk: n
"Read the updates up through the current highest-update-number plus n. Thus,
UpdateStreamDownloader readNextUpdatesFromDisk: 7
will read the next seven updates from disk"
self applyUpdatesFromDiskToUpdateNumber: ChangeSet highestNumberedChangeSet + n
stopIfGap: false!
----- Method: UpdateStreamDownloader class>>readServer:special:updatesThrough:saveLocally:updateImage: (in category 'fetching updates') -----
readServer: serverList special: indexPrefix updatesThrough: maxNumber saveLocally: saveLocally updateImage: updateImage
"Scan the update server(s) for unassimilated updates. If maxNumber is not nil, it represents the highest-numbered update to load. This makes it possible to update only up to a particular point. If saveLocally is true, then save local copies of the update files on disc. If updateImage is true, then absorb the updates into the current image."
"UpdateStreamDownloader readServer: UpdateStreamDownloader serverUrls updatesThrough: 828 saveLocally: true updateImage: true"
| str urls failed loaded |
Cursor wait showWhile: [ | docQueue docQueueSema |
urls := self newUpdatesOn: (serverList collect: [:url | url, 'updates/'])
special: indexPrefix
throughNumber: maxNumber.
loaded := 0.
failed := nil.
"send downloaded documents throuh this queue"
docQueue := SharedQueue new.
"this semaphore keeps too many documents from beeing queueed up at a time"
docQueueSema := Semaphore new.
5 timesRepeat: [ docQueueSema signal ].
"fork a process to download the updates"
self retrieveUrls: urls ontoQueue: docQueue withWaitSema: docQueueSema.
"process downloaded updates in the foreground"
'Processing updates' displayProgressFrom: 0 to: urls size during: [:bar | | nextDoc this updateName |
[ this := docQueue next.
nextDoc := docQueue next.
nextDoc = #failed ifTrue: [ failed := this ].
(failed isNil and: [ nextDoc ~= #finished ])
] whileTrue: [
failed ifNil: [
nextDoc reset; text.
nextDoc size = 0 ifTrue: [ failed := this ]. ].
failed ifNil: [
nextDoc peek asciiValue = 4 "pure object file"
ifTrue: [failed := this]]. "Must be fileIn, not pure object file"
failed ifNil: [
"(this endsWith: '.html') ifTrue: [doc := doc asHtml]."
"HTML source code not supported here yet"
updateImage
ifTrue: [
updateName := (this findTokens: '/') last.
ChangeSet newChangesFromStream: nextDoc named: updateName.
SystemVersion current registerUpdate: updateName initialIntegerOrNil].
saveLocally ifTrue:
[self saveUpdate: nextDoc onFile: (this findTokens: '/') last]. "if wanted"
loaded := loaded + 1.
bar value: loaded].
docQueueSema signal].
]].
failed ~~ nil & (urls size - loaded > 0) ifTrue: [
str := loaded printString ,' new update file(s) processed.'.
str := str, '\Could not load ' withCRs,
(urls size - loaded) printString ,' update file(s).',
'\Starting with "' withCRs, failed, '".'.
self inform: str].
^ Array with: failed with: loaded
!
----- Method: UpdateStreamDownloader class>>readServerUpdatesSaveLocally:updateImage: (in category 'fetching updates') -----
readServerUpdatesSaveLocally: saveLocally updateImage: updateImage
^ self readServerUpdatesThrough: nil saveLocally: saveLocally updateImage: updateImage!
----- Method: UpdateStreamDownloader class>>readServerUpdatesThrough:saveLocally:updateImage: (in category 'fetching updates') -----
readServerUpdatesThrough: maxNumber saveLocally: saveLocally updateImage: updateImage
"Scan the update server(s) for unassimilated updates. If maxNumber is not nil, it represents the highest-numbered update to load. This makes it possible to update only up to a particular point. If saveLocally is true, then save local copies of the update files on disc. If updateImage is true, then absorb the updates into the current image.
A file on the server called updates.list has the names of the last N update files. We look backwards for the first one we do not have, and start there"
"* To add a new update: Name it starting with a new two-digit code.
* Do not use %, /, *, space, or more than one period in the name of an update file.
* The update name does not need to have any relation to the version name.
* Figure out which versions of the system the update makes sense for.
* Add the name of the file to each version's category below.
* Put this file and the update file on all of the servers.
*
* To make a new version of the system: Pick a name for it (no restrictions)
* Put # and exactly that name on a new line at the end of this file.
* During the release process, fill in exactly that name in the dialog box.
* Put this file on the server."
"When two sets of updates need to use the same directory, one of them has a * in its
serverUrls description. When that is true, the first word of the description is put on
the front of 'updates.list', and that is the index file used."
"UpdateStreamDownloader readServerUpdatesThrough: 3922 saveLocally: true updateImage: true"
| failed loaded str res servers triple tryAgain indexPrefix |
UpdateStreamDownloader chooseUpdateList ifFalse: [^ self]. "ask the user which kind of updates"
servers := UpdateStreamDownloader serverUrls copy.
indexPrefix := (UpdateStreamDownloader updateUrlLists first first includes: $*)
ifTrue: [(UpdateStreamDownloader updateUrlLists first first findTokens: ' ') first]
"special for internal updates"
ifFalse: ['']. "normal"
[servers isEmpty] whileFalse: [
triple := self readServer: servers special: indexPrefix
updatesThrough: maxNumber
saveLocally: saveLocally updateImage: updateImage.
"report to user"
failed := triple first.
loaded := triple second.
tryAgain := false.
failed ifNil: ["is OK"
loaded = 0 ifTrue: ["found no updates"
servers size > 1 ifTrue: ["not the last server"
res := UIManager default
chooseFrom: #('Stop looking' 'Try next server')
title:
'No new updates on the server
', servers first, '
Would you like to try the next server?
(Normally, all servers are identical, but sometimes a
server won''t let us store new files, and gets out of date.)'
.
res = 2 ifFalse: [^ self]
ifTrue: [servers := servers allButFirst. "try the next server"
tryAgain := true]]]].
tryAgain ifFalse: [
str := loaded printString ,' new update file(s) processed.'.
^ self inform: str].
].!
----- Method: UpdateStreamDownloader class>>retrieveUrls:ontoQueue:withWaitSema: (in category 'fetching updates') -----
retrieveUrls: urls ontoQueue: queue withWaitSema: waitSema
"download the given list of URLs. The queue will be loaded alternately
with url's and with the retrieved contents. If a download fails, the
contents will be #failed. If all goes well, a special pair with an empty
URL and the contents #finished will be put on the queue. waitSema is
waited on every time before a new document is downloaded; this keeps
the downloader from getting too far ahead of the main process"
"kill the existing downloader if there is one"
| updateCounter |
UpdateDownloader
ifNotNil: [UpdateDownloader terminate].
updateCounter := 0.
"fork a new downloading process"
UpdateDownloader := [
'Downloading updates' displayProgressFrom: 0 to: urls size during: [:bar |
urls
do: [:url | | front canPeek doc |
waitSema wait.
queue nextPut: url.
doc := HTTPClient httpGet: url.
doc isString
ifTrue: [queue nextPut: #failed.
UpdateDownloader := nil.
Processor activeProcess terminate]
ifFalse: [canPeek := 120 min: doc size.
front := doc next: canPeek. doc skip: -1 * canPeek.
(front beginsWith: '<!!DOCTYPE') ifTrue: [
(front includesSubString: 'Not Found') ifTrue: [
queue nextPut: #failed.
UpdateDownloader := nil.
Processor activeProcess terminate]]].
UpdateDownloader ifNotNil: [queue nextPut: doc. updateCounter := updateCounter + 1. bar value: updateCounter]]].
queue nextPut: ''.
queue nextPut: #finished.
UpdateDownloader := nil] newProcess.
UpdateDownloader priority: Processor userInterruptPriority.
"start the process running"
UpdateDownloader resume!
----- Method: UpdateStreamDownloader class>>saveUpdate:onFile: (in category 'fetching updates') -----
saveUpdate: doc onFile: fileName
"Save the update on a local file. With or without the update number on the front, depending on the preference #updateRemoveSequenceNum"
| file fName pos updateDirectory |
(FileDirectory default directoryNames includes: 'updates') ifFalse:
[FileDirectory default createDirectory: 'updates'].
updateDirectory := FileDirectory default directoryNamed: 'updates'.
fName := fileName.
self updateRemoveSequenceNum ifTrue:
[pos := fName findFirst: [:c | c isDigit not].
fName := fName copyFrom: pos to: fName size].
doc reset; ascii.
(updateDirectory fileExists: fName) ifFalse:
[file := updateDirectory newFileNamed: fName.
file nextPutAll: doc contents.
file close].
!
----- Method: UpdateStreamDownloader class>>serverUrls (in category 'server urls') -----
serverUrls
"Return the current list of server URLs. For code updates. Format of UpdateUrlLists is
#( ('squeak updates' ('url1' 'url2'))
('some other updates' ('url3' 'url4')))"
| list |
list := UpdateUrlLists first last.
"If there is a dead server, return a copy with that server last"
Socket deadServer ifNotNil: [
list clone withIndexDo: [:aName :ind |
(aName beginsWith: Socket deadServer) ifTrue: [
list := list asOrderedCollection. "and it's a copy"
list removeAt: ind.
list addLast: aName]]
].
^ list asArray!
----- Method: UpdateStreamDownloader class>>setUpdateServer: (in category 'fetching updates') -----
setUpdateServer: groupName
"UpdateStreamDownloader setUpdateServer: 'Squeakland' "
| entry index |
entry := UpdateUrlLists detect: [:each | each first = groupName] ifNone: [^self].
index := UpdateUrlLists indexOf: entry.
UpdateUrlLists removeAt: index.
UpdateUrlLists addFirst: entry!
----- Method: UpdateStreamDownloader class>>summariesForUpdates:through: (in category 'fetching updates') -----
summariesForUpdates: startNumber through: stopNumber
"Answer the concatenation of summary strings for updates numbered in the given range"
^ String streamContents: [:aStream |
((ChangeSet changeSetsNamedSuchThat:
[:aName | aName first isDigit
and: [aName initialIntegerOrNil >= startNumber
and: [aName initialIntegerOrNil <= stopNumber]]]) asSortedCollection:
[:a :b | a name < b name]) do:
[:aChangeSet | aStream cr; nextPutAll: aChangeSet summaryString]]
"UpdateStreamDownloader summariesForUpdates: 4899 through: 4903"
!
----- Method: UpdateStreamDownloader class>>updateComment (in category 'fetching updates') -----
updateComment
"The following used to be at the beginning of the update file.
Now it is here to simplify parsing the file...
* To add a new update: Name it starting with a new four-digit code.
* Do not use %, /, *, space, or more than one period in the name of an update file.
* The update name does not need to have any relation to the version name.
* Figure out which versions of the system the update makes sense for.
* Add the name of the file to each version's category below.
* Put this file and the update file on all of the servers.
*
* To make a new version of the system: Pick a name for it (no restrictions)
* Put # and exactly that name on a new line at the end of this file.
* During the release process, fill in exactly that name in the dialog box.
* Put a copy of updates.list on the server.
*
* Special file with a different name for Disney Internal Updates.
* No need to move or rename files to release them to external updates.
"!
----- Method: UpdateStreamDownloader class>>updateFromServer (in category 'fetching updates') -----
updateFromServer
"Update the image by loading all pending updates from the server. Also save local copies of the update files if the #updateSavesFile preference is set to true"
self readServerUpdatesSaveLocally: self updateSavesFile updateImage: true!
----- Method: UpdateStreamDownloader class>>updateFromServerThroughUpdateNumber: (in category 'fetching updates') -----
updateFromServerThroughUpdateNumber: aNumber
"Update the image by loading all pending updates from the server. Also save local copies of the update files if the #updateSavesFile preference is set to true"
self readServerUpdatesThrough: aNumber saveLocally: self updateSavesFile updateImage: true!
----- Method: UpdateStreamDownloader class>>updateRemoveSequenceNum (in category 'preferences') -----
updateRemoveSequenceNum
^false!
----- Method: UpdateStreamDownloader class>>updateSavesFile (in category 'preferences') -----
updateSavesFile
<preference: 'Update saves files'
category: 'updates'
description: 'If true, then when an update is loaded from the server, a copy of it will automatically be saved on a local file as well.'
type: #Boolean>
^UpdateSavesFile ifNil: [ false ]!
----- Method: UpdateStreamDownloader class>>updateSavesFile: (in category 'preferences') -----
updateSavesFile: aBoolean
UpdateSavesFile := aBoolean!
----- Method: UpdateStreamDownloader class>>updateUrlLists (in category 'server urls') -----
updateUrlLists
UpdateUrlLists ifNil: [UpdateUrlLists := OrderedCollection new].
^ UpdateUrlLists!
----- Method: UpdateStreamDownloader class>>writeList:toStream: (in category 'fetching updates') -----
writeList: listContents toStream: strm
"Write a parsed updates.list out as text.
This is the inverse of parseListContents:"
strm reset.
listContents do:
[:pair | | version fileNames |
version := pair first. fileNames := pair last.
strm nextPut: $#; nextPutAll: version; cr.
fileNames do: [:fileName | strm nextPutAll: fileName; cr]].
strm close!
----- Method: UpdateStreamDownloader class>>zapUpdateDownloader (in category 'fetching updates') -----
zapUpdateDownloader
UpdateDownloader ifNotNil: [UpdateDownloader terminate].
UpdateDownloader := nil.!
----- Method: AutoStart class>>checkForUpdates (in category '*UpdateStream') -----
checkForUpdates
| availableUpdate updateServer |
World ifNotNil:
[ World install.
ActiveHand position: 100 @ 100 ].
HTTPClient isRunningInBrowser ifFalse: [ ^ self processUpdates ].
availableUpdate := (Smalltalk namedArguments
at: 'UPDATE'
ifAbsent: [ '' ]) asInteger.
availableUpdate ifNil: [ ^ false ].
updateServer := Smalltalk namedArguments
at: 'UPDATESERVER'
ifAbsent:
[ Smalltalk namedArguments
at: 'UPDATE_SERVER'
ifAbsent: [ 'Squeakland' ] ].
UpdateStreamDownloader default setUpdateServer: updateServer.
^ SystemVersion checkAndApplyUpdates: availableUpdate!
----- Method: FileList>>putUpdate: (in category '*UpdateStream') -----
putUpdate: fullFileName
"Put this file out as an Update on the servers."
| names choice |
self canDiscardEdits ifFalse: [^ self changed: #flash].
names := ServerDirectory groupNames asSortedArray.
choice := UIManager default chooseFrom: names values: names.
choice == nil ifTrue: [^ self].
(ServerDirectory serverInGroupNamed: choice) putUpdate:
(directory oldFileNamed: fullFileName).
self volumeListIndex: volListIndex.
!
----- Method: FileList>>serviceBroadcastUpdate (in category '*UpdateStream') -----
serviceBroadcastUpdate
"Answer a service for broadcasting a file as an update"
^ SimpleServiceEntry
provider: self
label: 'broadcast as update'
selector: #putUpdate:
description: 'broadcast file as update'
buttonLabel: 'broadcast'!
----- Method: FilePackage class>>conflictsWithUpdatedMethods: (in category '*UpdateStream-instance creation') -----
conflictsWithUpdatedMethods: fullName
| conflicts changeList |
conflicts := (self fromFileNamed: fullName) conflictsWithUpdatedMethods.
conflicts isEmpty ifTrue: [^ self].
changeList := ChangeList new.
changeList
changes: conflicts
file: (FileDirectory default readOnlyFileNamed: fullName) close.
ChangeList
open: changeList
name: 'Conflicts for ', (FileDirectory localNameFor: fullName)
multiSelect: true.!
----- Method: FilePackage class>>fileReaderServicesForFile:suffix: (in category '*UpdateStream-reader service') -----
fileReaderServicesForFile: fullName suffix: suffix
^(suffix = 'st') | (suffix = 'cs') | (suffix = '*')
ifTrue: [self services]
ifFalse: [#()]!
----- Method: FilePackage class>>serviceConflictsWithUpdatedMethods (in category '*UpdateStream-reader service') -----
serviceConflictsWithUpdatedMethods
^ SimpleServiceEntry
provider: self
label: 'conflicts with updated methods'
selector: #conflictsWithUpdatedMethods:
description: 'check for conflicts with more recently updated methods in the image, showing the conflicts in a transcript window'
buttonLabel: 'conflicts'!
----- Method: FilePackage class>>services (in category '*UpdateStream-reader service') -----
services
^ Array with: self serviceConflictsWithUpdatedMethods!
----- Method: FilePackage>>checkForMoreRecentUpdateThanChangeSet:pseudoClass:selector: (in category '*UpdateStream-conflict checker') -----
checkForMoreRecentUpdateThanChangeSet: updateNumberChangeSet pseudoClass: pseudoClass selector: selector
"Returns the source code for a conflict if a conflict is found, otherwise returns nil."
| classOrMeta allChangeSets moreRecentChangeSets conflictingChangeSets changeRecordSource classAndMethodPrintString |
classAndMethodPrintString := pseudoClass name, (pseudoClass hasMetaclass ifTrue: [' class'] ifFalse: ['']), '>>', selector asString.
changeRecordSource := pseudoClass sourceCode at: selector.
changeRecordSource isText
ifTrue: [changeRecordSource := Text
fromString: 'method: ', classAndMethodPrintString, ' was removed']
ifFalse: [changeRecordSource stamp isEmptyOrNil ifTrue:
[self notify: 'Warning: ', classAndMethodPrintString, ' in ', self packageName, ' has no timestamp/initials!!']].
pseudoClass exists ifFalse:
[(self classes at: pseudoClass name) hasDefinition
ifTrue: [^ nil "a method was added for a newly defined class; not a conflict"]
ifFalse: [self class logCr; log: 'CONFLICT found for ', classAndMethodPrintString, '... class ', pseudoClass name asString, ' does not exist in the image and is not defined in the file'.
^ changeRecordSource]].
classOrMeta := pseudoClass realClass.
"Only printout the replacing methods here, but we still check for removed methods too in the rest of this method."
(self class verboseConflicts and: [classOrMeta includesSelector: selector])
ifTrue: [self class logCr; log: '...checking ', classOrMeta asString, '>>', selector asString].
allChangeSets := ChangesOrganizer allChangeSets.
moreRecentChangeSets := allChangeSets
copyFrom: (allChangeSets indexOf: updateNumberChangeSet)
to: (allChangeSets size).
conflictingChangeSets := (moreRecentChangeSets select:
[:cs | (cs atSelector: selector class: classOrMeta) ~~ #none]).
conflictingChangeSets isEmpty ifTrue: [^ nil].
self class logCr; log: 'CONFLICT found for ', classAndMethodPrintString,
(' with newer changeset' asPluralBasedOn: conflictingChangeSets).
conflictingChangeSets do: [:cs | self class log: ' ', cs name].
^ changeRecordSource
!
----- Method: FilePackage>>conflictsWithUpdatedMethods (in category '*UpdateStream-conflict checker') -----
conflictsWithUpdatedMethods
"Check this package for conflicts with methods in the image which are in newer updates."
| localFileName stream updateNumberString updateNumber imageUpdateNumber updateNumberChangeSet conflicts fileStream |
localFileName := FileDirectory localNameFor: fullName.
stream := ReadStream on: sourceSystem.
stream upToAll: 'latest update: #'.
updateNumberString := stream upTo: $].
stream close.
fileStream := FileStream readOnlyFileNamed: fullName.
(fileStream contentsOfEntireFile includes: Character linefeed)
ifTrue: [self notifyWithLabel: 'The changeset file ', localFileName, ' contains linefeeds. Proceed if...
you know that this is okay (e.g. the file contains raw binary data).'].
fileStream close.
updateNumberString isEmpty ifFalse: "remove prepended junk, if any"
[updateNumberString := (updateNumberString findTokens: Character space) last].
updateNumberString asInteger ifNil:
[(self confirm: 'Error: ', localFileName, ' has no valid Latest Update number in its header.
Do you want to enter an update number for this file?')
ifFalse: [^ self]
ifTrue: [updateNumberString := UIManager default
request: 'Please enter the estimated update number (e.g. 4332).']].
updateNumberString asInteger ifNil: [self inform: 'Conflict check cancelled.'. ^ self].
updateNumber := updateNumberString asInteger.
imageUpdateNumber := SystemVersion current highestUpdate.
updateNumber > imageUpdateNumber ifTrue:
[(self confirm: 'Warning: The update number for this file (#', updateNumberString, ')
is greater than the highest update number for this image (#', imageUpdateNumber asString, ').
This probably means you need to update your image.
Should we proceed anyway as if the file update number is #', imageUpdateNumber asString, '?')
ifTrue:
[updateNumber := imageUpdateNumber.
updateNumberString := imageUpdateNumber asString]
ifFalse: [^ self]].
updateNumberChangeSet := self findUpdateChangeSetMatching: updateNumber.
updateNumberChangeSet ifNil: [^ self].
Smalltalk isMorphic ifTrue: [self currentWorld findATranscript: self currentEvent].
self class logCr; logCr; log: 'Checking ', localFileName, ' (#', updateNumberString, ') for method conflicts with changesets after ', updateNumberChangeSet name, ' ...'.
conflicts := OrderedCollection new.
self classes do: [:pseudoClass |
(Array with: pseudoClass with: pseudoClass metaClass) do: [:classOrMeta |
classOrMeta selectorsDo: [:selector | | conflict |
conflict := self
checkForMoreRecentUpdateThanChangeSet: updateNumberChangeSet
pseudoClass: classOrMeta
selector: selector.
conflict ifNotNil: [conflicts add: conflict].
].
].
].
self class logCr; log: conflicts size asString, (' conflict' asPluralBasedOn: conflicts), ' found.'; logCr.
self class closeLog.
^ conflicts!
----- Method: FilePackage>>findUpdateChangeSetMatching: (in category '*UpdateStream-conflict checker') -----
findUpdateChangeSetMatching: updateNumber
"Find update-changeset beginning with updateNumber, or reasonably close."
"This is to account for the fact that many changeset files are output from final releases, but may be tested for conflicts in a following alpha image, which will often not include that particular update-changeset from the final release but will contain ones near it. For example, if the file updateNumber is 5180 (from 3.5 final), but the image has no update-changeset beginning with 5180 because it's a 3.6alpha image (which starts at 5181), it will try up to 5190 and down to 5170 for a close match."
| updateNumberChangeSet updateNumberToTry |
updateNumberToTry := updateNumber.
updateNumberChangeSet := nil.
[updateNumberChangeSet isNil and: [updateNumberToTry notNil]] whileTrue:
[updateNumberChangeSet := ChangesOrganizer allChangeSets
detect: [:cs | (cs name beginsWith: updateNumberToTry asString)
and: [(cs name at: (updateNumberToTry asString size + 1)) isDigit not]]
ifNone: [nil].
updateNumberToTry >= updateNumber ifTrue:
[updateNumberToTry < (updateNumber + 10)
ifTrue: [updateNumberToTry := updateNumberToTry + 1]
ifFalse: [updateNumberToTry := updateNumber]].
updateNumberToTry <= updateNumber ifTrue:
[updateNumberToTry > (updateNumber - 10)
ifTrue: [updateNumberToTry := updateNumberToTry - 1]
ifFalse: [updateNumberToTry := nil "we're done trying"]].
].
updateNumberChangeSet ifNil:
[(self confirm: 'Warning: No changeset beginning with ',
updateNumber asString, ' (within +/- 10) was found in the image.
You must have changesets going back this far in your image
in order to accurately check for conflicts.
Proceed anyway?')
ifTrue: [updateNumberChangeSet := ChangesOrganizer allChangeSets first]].
^ updateNumberChangeSet!
----- Method: MCConfigurationBrowser>>post (in category '*UpdateStream') -----
post
"Take the current configuration and post an update"
| name update managers names choice |
(self checkRepositories and: [self checkDependencies]) ifFalse: [^self].
name := UIManager default
request: 'Update name (.cs) will be appended):'
initialAnswer: self configuration suggestedNameOfNextVersion.
name isEmpty ifTrue:[^self].
self configuration name: name.
update := MCPseudoFileStream on: (String new: 100).
update localName: name, '.cs'.
update nextPutAll: '"Change Set: ', name.
update cr; nextPutAll: 'Date: ', Date today printString.
update cr; nextPutAll: 'Author: Posted by Monticello'.
update cr; cr; nextPutAll: 'This is a configuration map created by Monticello."'.
update cr; cr; nextPutAll: '(MCConfiguration fromArray: #'.
self configuration fileOutOn: update.
update nextPutAll: ') upgrade.'.
update position: 0.
managers := Smalltalk at: #UpdateManager ifPresent:[:mgr| mgr allRegisteredManagers].
managers ifNil:[managers := #()].
managers size > 0 ifTrue:[
| servers index |
servers := ServerDirectory groupNames asSortedArray.
names := (managers collect:[:each| each packageVersion]), servers.
index := UIManager default chooseFrom: names lines: {managers size}.
index = 0 ifTrue:[^self].
index <= managers size ifTrue:[
| mgr |
mgr := managers at: index.
^mgr publishUpdate: update.
].
choice := names at: index.
] ifFalse:[
names := ServerDirectory groupNames asSortedArray.
choice := UIManager default chooseFrom: names values: names.
choice == nil ifTrue: [^ self].
].
(ServerDirectory serverInGroupNamed: choice) putUpdate: update.!
----- Method: SystemVersion class>>checkAndApplyUpdates: (in category '*UpdateStream') -----
checkAndApplyUpdates: availableUpdate
"SystemVersion checkAndApplyUpdates: nil"
^(availableUpdate isNil
or: [availableUpdate > SystemVersion current highestUpdate])
ifTrue: [
(self confirm: 'There are updates available. Do you want to install them now?')
ifFalse: [^false].
UpdateStreamDownloader default
readServerUpdatesThrough: availableUpdate
saveLocally: false
updateImage: true.
Smalltalk snapshot: true andQuit: false.
true]
ifFalse: [false]!
Chris Muller uploaded a new version of Services-Base to project Squeak 4.6:
http://source.squeak.org/squeak46/Services-Base-mt.55.mcz
==================== Summary ====================
Name: Services-Base-mt.55
Author: mt
Time: 12 April 2015, 9:07:10.492 pm
UUID: 3b374e3f-27ec-ff4f-9af9-89da39d9d38b
Ancestors: Services-Base-topa.54
MVC compatibility.
==================== Snapshot ====================
SystemOrganization addCategory: #'Services-Base'!
SystemOrganization addCategory: #'Services-Base-Providers'!
SystemOrganization addCategory: #'Services-Base-Requestors'!
SystemOrganization addCategory: #'Services-Base-GUI'!
Preferences subclass: #ServicePreferences
instanceVariableNames: ''
classVariableNames: 'ServiceDictionaryOfPreferences'
poolDictionaries: ''
category: 'Services-Base-GUI'!
!ServicePreferences commentStamp: 'rr 7/10/2006 15:36' prior: 0!
I store the preferences related to the servicse framework. The preferences are editable via the Services Browser, based on Hernan Tylim's Preference Browser.
The main preference categories for services are:
-- keyboard shortcuts -- : several text preferences, one per keyboard shortcuts. To edit them, enter a service identifier (equal to the method name under which it is defined in its ServiceProvider), and accept with alt-s or enter
-- menu contents -- : All the service categories in the image have a text preference under here. To edit it, enter the services identifiers you wish to put in this category, separating them with a single space character. The order is important: it defines the order of the items in menus.
-- settings -- : general boolean preferences.
Then there is a preference category for each provider in the image. Under each, you will find:
A boolean preference for each service in the image. If it is false, the service will not appear in menus.
The text preference for each service category defined by the service provider. This is the same as the one appearing in the menu contents preference category.!
----- Method: ServicePreferences class>>compileAccessMethodForPreference: (in category 'accessing') -----
compileAccessMethodForPreference: aPreference
"do nothing"!
----- Method: ServicePreferences class>>dictionaryOfPreferences (in category 'accessing') -----
dictionaryOfPreferences
ServiceDictionaryOfPreferences
ifNil: [ServiceDictionaryOfPreferences := IdentityDictionary new].
^ ServiceDictionaryOfPreferences !
----- Method: ServicePreferences class>>dictionaryOfPreferences: (in category 'accessing') -----
dictionaryOfPreferences: aDictionary
ServiceDictionaryOfPreferences := aDictionary!
----- Method: ServicePreferences class>>replayPreferences: (in category 'replaying') -----
replayPreferences: preferences
| s |
s := SortedCollection new
sortBlock: [:a :b | a last < b last].
s addAll: preferences;
reSort.
s
do: [:e | | v |
v := self valueOfPreference: e first ifAbsent: ''.
self setPreference: e first toValue: (v
ifEmpty: ['']
ifNotEmpty: [v , ' '])
, e second]!
----- Method: ServicePreferences class>>wipe (in category 'accessing') -----
wipe
self dictionaryOfPreferences: nil!
----- Method: PasteUpMorph>>openWorldMenu (in category '*services-base') -----
openWorldMenu
| menu |
menu := (TheWorldMenu new adaptToWorld: self) buildWorldMenu.
menu addTitle: Preferences desktopMenuTitle translated.
menu openInHand!
----- Method: PasteUpMorph>>requestor (in category '*services-base') -----
requestor
"returns the focused window's requestor"
^ Requestor default!
----- Method: PasteUpMorph>>topRequestor (in category '*services-base') -----
topRequestor
"returns the focused window's requestor"
^ SystemWindow topWindow requestor!
----- Method: PasteUpMorph>>worldMenu (in category '*services-base') -----
worldMenu
^ TheWorldMenu new adaptToWorld: self!
----- Method: Model>>requestor (in category '*services-base') -----
requestor
^ Requestor default!
----- Method: StringHolder>>codePaneMenuServices: (in category '*services-base') -----
codePaneMenuServices: aMenu
<codePaneMenu>
<menuPriority: 150>
ServiceGui browser: self codePaneMenu: aMenu.
^ Preferences useOnlyServicesInMenu ifTrue: [nil] ifFalse: [aMenu]!
----- Method: StringHolder>>requestor (in category '*services-base') -----
requestor
^ (TextRequestor new) model: self; yourself!
----- Method: StringHolder>>selectedInterval (in category '*services-base') -----
selectedInterval
^self codeTextMorph selectionInterval!
----- Method: Collection>>chooseOne: (in category '*services-base') -----
chooseOne: caption
"pops up a menu asking for one of the elements in the collection.
If none is chosen, raises a ServiceCancelled notification"
| m |
m := MenuMorph entitled: caption.
self do:
[:ea |
m
add: ea
target: [:n | ^ n]
selector: #value:
argument: ea].
m invokeModal.
ServiceCancelled signal!
----- Method: PreferenceBrowserMorph class>>updateBrowsers (in category '*services-base') -----
updateBrowsers
(self allInstances select: [:e | e visible])
do: [:each |
(each findDeepSubmorphThat:[:m | m isKindOf:PluggableListMorph]
ifAbsent:[^ self]) verifyContents].!
PreferenceBrowserMorph subclass: #ServiceBrowserMorph
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base-GUI'!
!ServiceBrowserMorph commentStamp: 'rr 7/10/2006 15:28' prior: 0!
I subclass the PreferenceBrowserMorph to adapt the interface to services. So far the changes are minimal.!
----- Method: ServiceBrowserMorph>>newButtonRow (in category 'as yet unclassified') -----
newButtonRow
^BorderedMorph new
color: Color transparent;
cellInset: 2;
layoutInset: 2;
layoutPolicy: TableLayout new;
listDirection: #leftToRight;
listCentering: #topLeft;
cellPositioning: #topLeft;
on: #mouseEnter send: #paneTransition: to: self;
on: #mouseLeave send: #paneTransition: to: self;
"addMorphBack: self defaultButton;
addMorphBack: self newSeparator;
addMorphBack: self saveButton;
addMorphBack: self loadButton;
addMorphBack: self newSeparator;
addMorphBack: self saveToDiskButton;
addMorphBack: self loadFromDiskButton;
addMorphBack: self newSeparator;
addMorphBack: self newTransparentFiller;
addMorphBack: self helpButton;"
yourself.!
----- Method: SystemWindow class>>topWindow (in category '*services-base') -----
topWindow
^ TopWindow!
----- Method: SystemWindow>>requestor (in category '*services-base') -----
requestor
^[model requestor]
on: Error
do: [Transcript show: 'no requestor for : ', model class name. Requestor default] !
----- Method: SystemWindow>>topWindow (in category '*services-base') -----
topWindow
^ TopWindow!
----- Method: BlockContext>>valueWithRequestor: (in category '*services-base') -----
valueWithRequestor: aRequestor
"To do later: make the fillInTheBlank display more informative captions.
Include the description of the service, and maybe record steps"
^ self numArgs isZero
ifTrue: [self value]
ifFalse: [self value: aRequestor]!
----- Method: Association>>serviceUpdate (in category '*services-base-preferences') -----
serviceUpdate
self key service perform: self value!
----- Method: PreferenceBrowser class>>openForServices (in category '*services-base') -----
openForServices
"PreferenceBrowser openForServices"
| browser |
browser := self new.
browser initializeForServices.
(ServiceBrowserMorph withModel: browser)
openInWorld.
^browser. !
----- Method: PreferenceBrowser>>initializeForServices (in category '*services-base') -----
initializeForServices
preferences := ServicePreferences.
title := 'Services Browser'!
----- Method: MessageSet>>browseReference: (in category '*services-base') -----
browseReference: ref
self okToChange ifTrue: [
self initializeMessageList: (OrderedCollection with: ref).
self changed: #messageList.
self messageListIndex: 1.
] !
----- Method: MessageSet>>selectReference: (in category '*services-base') -----
selectReference: ref
self okToChange ifTrue: [self messageListIndex: (self messageList indexOf: ref)]!
----- Method: String>>service (in category '*services-base') -----
service
^ self serviceOrNil ifNil: [ServiceCategory new id: self asSymbol]!
----- Method: String>>serviceOrNil (in category '*services-base') -----
serviceOrNil
^ ServiceRegistry current serviceWithId: self asSymbol!
Object subclass: #BasicRequestor
instanceVariableNames: 'caption answer'
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base'!
!BasicRequestor commentStamp: 'rr 7/10/2006 14:44' prior: 0!
This class is the root of the Requestor hierarchy.
Requestors are interfaces between services and the system. ServiceActions are given an instance
of a Requestor, and they ask it for the data they need. The requestor is determined by the model of the application. A class used as a model can implement the #requestor message to return the most suited requestor. A requestor knows how to query its model and the user if needed.
Requestor are defined in hierarchies so that the protocol they rely on (methods starting with 'get') can be easily reused.!
----- Method: BasicRequestor>>caption: (in category 'generic requests') -----
caption: aString
caption := aString!
----- Method: BasicRequestor>>get: (in category 'executing') -----
get: aString
self caption: aString.
^ self getSymbol!
----- Method: BasicRequestor>>getString (in category 'generic requests') -----
getString
| result |
result := UIManager default request:caption initialAnswer:answer contents.
self newCaption.
result isEmpty |result isNil ifTrue:[ServiceCancelled signal].
^ result!
----- Method: BasicRequestor>>getStringCollection (in category 'generic requests') -----
getStringCollection
caption := caption, Character cr asString, 'Separate items with space'.
^ (self getString findTokens: ' ') collect: [:each | each copyWithoutAll: ' ' ]!
----- Method: BasicRequestor>>getSymbol (in category 'generic requests') -----
getSymbol
^ self getString asSymbol!
----- Method: BasicRequestor>>getSymbolCollection (in category 'generic requests') -----
getSymbolCollection
^[self getStringCollection collect: [:each | each asSymbol]]
on: ServiceCancelled
do: [#()]!
----- Method: BasicRequestor>>initialize (in category 'initialize-release') -----
initialize
self newCaption!
----- Method: BasicRequestor>>newCaption (in category 'generic requests') -----
newCaption
caption := 'Enter text'.
answer := String new writeStream.!
BasicRequestor subclass: #Requestor
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base-Requestors'!
!Requestor commentStamp: 'rr 7/10/2006 15:19' prior: 0!
I am an implementation of BasicRequestor with some requests already implemented.!
----- Method: Requestor class>>default (in category 'as yet unclassified') -----
default
"returns a default requestor"
^ self new!
----- Method: Requestor>>getClass (in category 'requests') -----
getClass
^Smalltalk at: self getSymbol!
----- Method: Requestor>>getClassCollection (in category 'requests') -----
getClassCollection
^ self getSymbolCollection collect: [:className | Smalltalk at: className]!
----- Method: Requestor>>getMethodBody (in category 'requests') -----
getMethodBody
| m |
m := FillInTheBlankMorph new.
m setQuery: 'Please enter the full body of the method you want to define'
initialAnswer: self class sourceCodeTemplate
answerExtent: 500@250
acceptOnCR: false.
World addMorph: m centeredNear: World activeHand position.
^ m getUserResponse.!
----- Method: Requestor>>getSelection (in category 'requests') -----
getSelection
"Sorry to feedle with fillInTheBlankMorph innards, but I had to"
| text m |
text := (MethodReference class: self getClass selector: self getSelector) sourceCode.
m := FillInTheBlankMorph new.
m setQuery: 'Highlight a part of the source code, and accept' initialAnswer: text
answerExtent: 500@250
acceptOnCR: true.
World addMorph: m centeredNear: World activeHand position.
m getUserResponse.
^ m selection!
----- Method: Requestor>>getSelector (in category 'services requests') -----
getSelector
^ self caption: 'enter selector'; getSymbol!
Requestor subclass: #TextRequestor
instanceVariableNames: 'model'
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base-Requestors'!
!TextRequestor commentStamp: 'rr 7/10/2006 15:20' prior: 0!
A requestor for text areas, able for example to fetch the current selected text.!
TextRequestor subclass: #BrowserRequestor
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base-Requestors'!
!BrowserRequestor commentStamp: 'rr 7/10/2006 15:24' prior: 0!
I am a requestor specialized to fetch information in a Browser.
I can ask a browser its selected class and selected method for example.
If the RB is installed too, I can also fetch ast nodes in the browser's selected
method.
I am the default requestor for CodeHolder and it's subclasses.
To be integrated with services, alternative browsers, such as the OmniBrowser and Whisker should define a specialized requestor subclassing this one. A few core messages would need to be redefined, such as getClass, getMessage ... to be adapted to the browser's data structures.
Only a few of them have to be overridden, the majority of the requests rely on a few base ones.!
----- Method: BrowserRequestor>>browser: (in category 'initialize-release') -----
browser: b
self model: b!
----- Method: BrowserRequestor>>getBrowser (in category 'requests') -----
getBrowser
^ self getModel!
----- Method: BrowserRequestor>>getClass (in category 'requests') -----
getClass
^ self getBrowser selectedClassOrMetaClass!
----- Method: BrowserRequestor>>getInitializingExpressionForTheNewParameter (in category 'requests') -----
getInitializingExpressionForTheNewParameter
^ UIManager default request: 'enter default parameter code'
initialAnswer: '42'!
----- Method: BrowserRequestor>>getNewSelectorName (in category 'requests') -----
getNewSelectorName
^ UIManager default request: 'enter the new selector name'
initialAnswer: self getSelector!
----- Method: BrowserRequestor>>getNewVariableName (in category 'requests') -----
getNewVariableName
^ UIManager default request: 'Enter the new variable name' translated initialAnswer: 'foo'!
----- Method: BrowserRequestor>>getPackage (in category 'requests') -----
getPackage
self getSelector ifNil: [
^ PackageInfo named:(
self getClass ifNil: [self getSystemCategory]
ifNotNil: [:c | c category copyUpTo: $-])].
^ PackageOrganizer default
packageOfMethod:
(MethodReference class: self getClass
selector: self getSelector)
ifNone: [PackageInfo named: (self getClass category copyUpTo: $-)] !
----- Method: BrowserRequestor>>getPackageForCategory (in category 'requests') -----
getPackageForCategory
"answers a packageinfo for the current class category"
^ PackageInfo named: self getClass category!
----- Method: BrowserRequestor>>getPackageForCategoryName (in category 'requests') -----
getPackageForCategoryName
"answers a packageinfo for the current class category"
^ self getPackageForCategory packageName!
----- Method: BrowserRequestor>>getPackageName (in category 'requests') -----
getPackageName
^ self getPackage packageName!
----- Method: BrowserRequestor>>getPackageProvider (in category 'requests') -----
getPackageProvider
| provs classes |
provs := ServiceProvider registeredProviders.
classes := self getPackage classes.
^ classes detect: [:e | provs includes: e] ifNone: [ServiceProvider newProviderFor: self getPackageName]!
----- Method: BrowserRequestor>>getSelection (in category 'requests') -----
getSelection
self getBrowser selectedInterval ifEmpty: [^super getSelection].
^ self getBrowser selectedInterval!
----- Method: BrowserRequestor>>getSelector (in category 'requests') -----
getSelector
| s |
s := self getBrowser selectedMessageName.
^ s ifNil: [super getSelector] ifNotNil: [s]!
----- Method: BrowserRequestor>>getSelectorCollection (in category 'requests') -----
getSelectorCollection
self caption: 'enter selector list'.
^ self getSymbolCollection !
----- Method: BrowserRequestor>>getSelectorName (in category 'requests') -----
getSelectorName
^ self getBrowser selectedMessageName!
----- Method: BrowserRequestor>>getSystemCategory (in category 'requests') -----
getSystemCategory
^ self getBrowser selectedSystemCategory!
----- Method: TextRequestor>>getCurrentText (in category 'request') -----
getCurrentText
"returns the unnacepted text in the text morph"
^ self getModel codeTextMorph text!
----- Method: TextRequestor>>getModel (in category 'request') -----
getModel
^ model first!
----- Method: TextRequestor>>model: (in category 'accessing') -----
model: aModel
model := WeakArray with: aModel!
----- Method: Object>>requestor (in category '*services-base') -----
requestor
"returns the focused window's requestor"
"SystemWindow focusedWindow ifNotNilDo: [:w | ^ w requestor]."
"triggers an infinite loop"
^ Requestor default!
Object subclass: #ServiceAction
instanceVariableNames: 'condition action requestor label shortLabel description id provider enabled'
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base'!
!ServiceAction commentStamp: 'rr 7/10/2006 14:58' prior: 0!
ServiceAction are executable objects in various contexts.
They can be displayed as buttons or menu items or bounded to keyboard shortcuts.
ServiceActions are defined in methods in an instance of a ServiceProvider class (in the 'services' method category), using the following template:
serviceIdentifierAndMethodName
^ ServiceAction
text: 'Menu item text'
button: 'Button text'
description: 'Longer text that appears in help balloons'
action: [:r | "Code block fetching data from the requestor instance, r, that is passed to the block"]
or, alternatively:
serviceIdentifierAndMethodName
^ ServiceAction
text: 'Menu item text'
button: 'Button text'
description: 'Longer text that appears in help balloons'
action: [:r | "Code block fetching data from the requestor instance, r, that is passed to the block"]
condition: [:r | "second block returning true if the service can be used at the time being, false otherwise. Data can still be fetched from the requestor instance"]
The method name in which the service is defined becomes its identifier. To build the hierarchy of services and to assign them to shortcuts, you will need to type this names in the relevant fields of the Services Browser.
Services are arranged in a hierarchy. and bound to keyboard shortcuts using the ServicesBrowser.
!
----- Method: ServiceAction class>>id:text:button:description:action: (in category 'instance creation') -----
id: aSymbol text: aStringOrBlock button: buttonString description: aString action: aBlock
^ self id: aSymbol
text: aStringOrBlock
button: buttonString
description: aString
action: aBlock
condition: [:r | true]!
----- Method: ServiceAction class>>id:text:button:description:action:condition: (in category 'instance creation') -----
id: aSymbol text: aStringOrBlock button: buttonString description: aString action: aBlock condition: cBlock
^ (self new)
id: aSymbol;
text: aStringOrBlock;
buttonLabel: buttonString;
description: aString;
action: aBlock;
condition: cBlock;
yourself!
----- Method: ServiceAction class>>initialize (in category 'class initialization') -----
initialize
#(
(inlineServicesInMenu true 'Inline the services the squeak menus')
(useOnlyServicesInMenu false 'Use only services and not regular menu items')
(useServicesInBrowserButtonBar false 'Use a service-based button bar'))
do: [:tr |
Preferences
addPreference: tr first
categories: #(#services)
default: tr second
balloonHelp: tr third]
!
----- Method: ServiceAction class>>text:button:description:action: (in category 'instance creation') -----
text: aStringOrBlock button: buttonString description: aString action: aBlock
"use when id can be automatically generated"
^ self id: nil
text: aStringOrBlock
button: buttonString
description: aString
action: aBlock
condition: [:r | true]!
----- Method: ServiceAction class>>text:button:description:action:condition: (in category 'instance creation') -----
text: aStringOrBlock button: buttonString description: aString action: aBlock condition: cBlock
"use when id can be generated"
^ self
id: nil
text: aStringOrBlock
button: buttonString
description: aString
action: aBlock
condition: cBlock!
----- Method: ServiceAction class>>text:description:action: (in category 'instance creation') -----
text: textString description: aString action: aBlock
"use when id can be generated"
^ self id: nil text: textString button: textString description: aString action: aBlock!
----- Method: ServiceAction>>action: (in category 'accessing') -----
action: aBlock
action := aBlock!
----- Method: ServiceAction>>addPreference:category:selector: (in category 'preferences') -----
addPreference: name category: cat selector: sel
ServicePreferences
addPreference: name
categories: {cat asSymbol. self providerCategory}
default: ''
balloonHelp:self description
projectLocal:false
changeInformee: self id -> sel
changeSelector: #serviceUpdate
type: #String!
----- Method: ServiceAction>>buttonLabel (in category 'accessing') -----
buttonLabel
^ shortLabel
ifNil: [self text]
ifNotNil: [shortLabel ifEmpty: [self text] ifNotEmpty: [shortLabel]]!
----- Method: ServiceAction>>buttonLabel: (in category 'accessing') -----
buttonLabel: anObject
shortLabel := anObject!
----- Method: ServiceAction>>categories (in category 'accessing') -----
categories
^ ServiceRegistry current categories select: [:e | e services includes: self]!
----- Method: ServiceAction>>condExecuteWith: (in category 'executing') -----
condExecuteWith: aRequestor
self requestor: aRequestor.
self executeCondition
ifTrue: [self execute]
ifFalse: [Beeper beep]!
----- Method: ServiceAction>>condition: (in category 'accessing') -----
condition: aBlock
condition := aBlock!
----- Method: ServiceAction>>description (in category 'accessing') -----
description
^ description ifNil: [self text] ifNotNil: [description]!
----- Method: ServiceAction>>description: (in category 'accessing') -----
description: anObject
description := anObject select: [:each | (each = Character cr) not]
thenCollect: [:each | each = Character tab ifTrue: [Character space]
ifFalse: [each]].!
----- Method: ServiceAction>>execute (in category 'executing') -----
execute
^ action clone valueWithRequestor: World topRequestor!
----- Method: ServiceAction>>executeCondition (in category 'executing') -----
executeCondition
^ [condition clone valueWithRequestor: World topRequestor]
on: Error
do: [false]!
----- Method: ServiceAction>>id (in category 'accessing') -----
id
^id!
----- Method: ServiceAction>>id: (in category 'accessing') -----
id: aSymbol
id := aSymbol!
----- Method: ServiceAction>>initialize (in category 'initialize-release') -----
initialize
self
action: [].
self
condition: [true].
self text: 'no op'.
self requestor: Requestor new.
self id: #none.
enabled := true!
----- Method: ServiceAction>>insertPreferences (in category 'preferences') -----
insertPreferences
ServicePreferences
addPreference: self id
categories: (Array with: self providerCategory)
default: true
balloonHelp: self description
projectLocal: false
changeInformee: self id -> #updateEnable
changeSelector: #serviceUpdate
type: #Boolean!
----- Method: ServiceAction>>isCategory (in category 'testing') -----
isCategory
^ false!
----- Method: ServiceAction>>isEnabled (in category 'testing') -----
isEnabled
^ enabled!
----- Method: ServiceAction>>menuLabel (in category 'accessing') -----
menuLabel
| l sh |
l := self text.
l size > 50 ifTrue: [l := (l first: 47), '...'].
sh := self shortcut.
sh := (sh isNil or: [sh isEmpty]) ifTrue: [''] ifFalse: [' (', sh, ')'].
^ l capitalized, sh!
----- Method: ServiceAction>>menuLabelNumbered: (in category 'accessing') -----
menuLabelNumbered: index
| shorterLabel shortCut serviceNumberString |
shorterLabel := self text.
shorterLabel size > 50 ifTrue: [ shorterLabel := (shorterLabel first: 47) , '...' ].
shortCut := self shortcut.
shortCut := (shortCut isNil or: [ shortCut isEmpty ])
ifTrue: [ String empty ]
ifFalse: [ ' (' , shortCut , ')' ].
serviceNumberString := index isZero
ifTrue: [ String empty ]
ifFalse: [ index asString , '.' ].
^ serviceNumberString , shorterLabel , shortCut!
----- Method: ServiceAction>>perform:orSendTo: (in category 'executing') -----
perform: selector orSendTo: otherTarget
^ self perform: selector!
----- Method: ServiceAction>>preferences (in category 'preferences') -----
preferences
^ {ServicePreferences preferenceAt: self shortcutPreference} select: [:e | e notNil]!
----- Method: ServiceAction>>printOn: (in category 'printing') -----
printOn: aStream
super printOn: aStream.
aStream
space ;
nextPutAll: id asString!
----- Method: ServiceAction>>provider (in category 'accessing') -----
provider
^ provider
ifNil: [nil]
ifNotNil: [provider new]!
----- Method: ServiceAction>>provider: (in category 'accessing') -----
provider: p
provider := p!
----- Method: ServiceAction>>providerCategory (in category 'preferences') -----
providerCategory
^ provider name!
----- Method: ServiceAction>>requestor (in category 'accessing') -----
requestor
^requestor!
----- Method: ServiceAction>>requestor: (in category 'accessing') -----
requestor: anObject
requestor := anObject!
----- Method: ServiceAction>>shortcut (in category 'preferences') -----
shortcut
^ ServicePreferences valueOfPreference: self shortcutPreference!
----- Method: ServiceAction>>shortcutPreference (in category 'preferences') -----
shortcutPreference
^ ('Shortcut for ', self id, ':') asSymbol!
----- Method: ServiceAction>>text (in category 'accessing') -----
text
^label isBlock ifTrue: [label value: requestor] ifFalse: [label]!
----- Method: ServiceAction>>text: (in category 'accessing') -----
text: aString
label := aString!
----- Method: ServiceAction>>updateEnable (in category 'preferences') -----
updateEnable
enabled := ServicePreferences
valueOfPreference: self id
ifAbsent: [true]!
----- Method: ServiceAction>>updateShortcut (in category 'updating') -----
updateShortcut
(self systemNavigation allImplementorsOf: #processService:newShortcut:)
do: [:ref | | cls |
cls := ref actualClass.
cls isMeta ifTrue: [cls soleInstance processService: self newShortcut: self shortcut]].
ServiceRegistry ifInteractiveDo: [self provider savePreferencesFor: self]!
ServiceAction subclass: #ServiceCategory
instanceVariableNames: 'services'
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base'!
!ServiceCategory commentStamp: 'rr 7/10/2006 15:06' prior: 0!
I represent a category of services that can be added to a menu.
I can be displayed as a menu or button bar containing my services.
I am also a subclass of ServiceAction, so I can form a subcategory of another service category.
Like services, I am created in methods of a ServiceProvider, in the 'services' method protocol.
The template to create a service category is the following:
methodNameAndServiceCategoryId
^ ServiceCategory
text: 'Menu text'
button: 'Button text'
description: 'Longer descriptive text appearing in help balloons'
To put services in a service category, you have to use the Service Browser, located in the word menu, under the 'Preferences and Services' menu item.
In it, you can look up for the name of your category, and enter service identifiers as children
of the category in the associatedd text field, separating them with spaces.!
----- Method: ServiceCategory class>>text:button:description: (in category 'instance creation') -----
text: aStringOrBlock button: buttonString description: aString
"use when id can be generated"
^ self id: nil text: aStringOrBlock button: buttonString description: aString action: [] !
----- Method: ServiceCategory>>childrenPreferences (in category 'preferences') -----
childrenPreferences
^ ('Items in ', self id, ':') asSymbol!
----- Method: ServiceCategory>>enabledServices (in category 'accessing') -----
enabledServices
^ services
select: [:e | e isEnabled]!
----- Method: ServiceCategory>>execute (in category 'executing') -----
execute
"displays the subservices as a submenu"
ServiceGui openMenuFor: self!
----- Method: ServiceCategory>>externalPreferences (in category 'preferences') -----
externalPreferences
| p |
p := ServicePreferences valueOfPreference: self childrenPreferences ifAbsent: [''].
^ (p findTokens: ' ') collect: [:e | e service]!
----- Method: ServiceCategory>>initialize (in category 'initialize-release') -----
initialize
services := OrderedCollection new.
super initialize.
!
----- Method: ServiceCategory>>insertPreferences (in category 'preferences') -----
insertPreferences
super insertPreferences.
ServicePreferences
addPreference: self childrenPreferences
categories: {
(#'-- menu contents --').
(self providerCategory)}
default: ''
balloonHelp: self description
projectLocal: false
changeInformee: self id -> #updateChildren
changeSelector: #serviceUpdate
type: #String!
----- Method: ServiceCategory>>isCategory (in category 'testing') -----
isCategory
^ true!
----- Method: ServiceCategory>>newChildren (in category 'preferences') -----
newChildren
| s |
s := ServicePreferences valueOfPreference: self childrenPreferences.
^ (s findTokens: ' ') collect: [:str | str serviceOrNil]!
----- Method: ServiceCategory>>newChildrenValid (in category 'preferences') -----
newChildrenValid
| s |
s := ServicePreferences valueOfPreference: self childrenPreferences.
^ (s findTokens: ' ') allSatisfy: [:str |
str serviceOrNil
ifNil: [ServiceRegistry ifInteractiveDo:
[self inform: str, ' is not a valid service name'].
false]
ifNotNil: [true]]!
----- Method: ServiceCategory>>prefServices (in category 'preferences') -----
prefServices
| s |
s := ServicePreferences valueOfPreference: self childrenPreferences.
^ (s findTokens: ' ') collect: [:str | str service]!
----- Method: ServiceCategory>>replaceChildren (in category 'preferences') -----
replaceChildren
ServiceRegistry ifInteractiveDo: [services
do: [:s | s provider
ifNotNil: [:p | p class removeSelector: (self id , s id) asSymbol]]].
services := self newChildren.
services
do: [:e |
(ServicePreferences preferenceAt: e shortcutPreference)
ifNotNil: [:p | p categoryList: {'-- keyboard shortcuts --'. self id asString}].
ServiceRegistry
ifInteractiveDo: [self provider savePreferencesFor: self]]!
----- Method: ServiceCategory>>requestor: (in category 'accessing') -----
requestor: aRequestor
super requestor: aRequestor.
self services do: [:s | s requestor: aRequestor]!
----- Method: ServiceCategory>>services (in category 'accessing') -----
services
^services!
----- Method: ServiceCategory>>updateChildren (in category 'preferences') -----
updateChildren
self newChildrenValid
ifTrue: [self replaceChildren].
"PreferenceBrowserMorph updateBrowsers."
ServiceGui updateBar: self!
Object subclass: #ServiceGui
instanceVariableNames: 'menu bar service n'
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base-GUI'!
ServiceGui class
instanceVariableNames: 'bars'!
!ServiceGui commentStamp: 'rr 7/10/2006 15:29' prior: 0!
I abstract all the UI-related behaviors for the services framework.
In the future I could be changed to be compatible with ToolBuilder!
ServiceGui class
instanceVariableNames: 'bars'!
----- Method: ServiceGui class>>bars (in category 'registering button bars') -----
bars
^ bars!
----- Method: ServiceGui class>>browser:classCategoryMenu: (in category 'hooks') -----
browser: b classCategoryMenu: aMenu
^ (self new for:b id:#browserClassCategoryMenu) inlineInMenu:aMenu!
----- Method: ServiceGui class>>browser:classMenu: (in category 'hooks') -----
browser: b classMenu: aMenu
^ (self new for:b id:#browserClassMenu) inlineInMenu:aMenu!
----- Method: ServiceGui class>>browser:codePaneMenu: (in category 'hooks') -----
browser: b codePaneMenu: aMenu
^(self new for: b id: #browserCodePaneMenu) inlineInMenu: aMenu!
----- Method: ServiceGui class>>browser:messageCategoryMenu: (in category 'hooks') -----
browser: b messageCategoryMenu: aMenu
^ (self new for:b id:#browserMethodCategoryMenu) inlineInMenu:aMenu!
----- Method: ServiceGui class>>browser:messageListMenu: (in category 'hooks') -----
browser: aBrowser messageListMenu: aMenu
^ (self new
for: aBrowser
id: #browserMethodMenu) inlineInMenu: aMenu!
----- Method: ServiceGui class>>browserButtonRow: (in category 'hooks') -----
browserButtonRow: aBrowser
^ (self new for: aBrowser id: #browserButtonBar) buildButtonBar !
----- Method: ServiceGui class>>browserButtonRow:inlinedIn: (in category 'hooks') -----
browserButtonRow: aBrowser inlinedIn: row
| bar |
self buttonBarServices
ifTrue: [bar := (self new for: aBrowser id: #browserButtonBar) buildButtonBar.
row addMorphBack: bar].
^ row!
----- Method: ServiceGui class>>buttonBarServices (in category 'preferences') -----
buttonBarServices
^ ServicePreferences valueOfPreference: #useServicesInBrowserButtonBar !
----- Method: ServiceGui class>>initialize (in category 'registering button bars') -----
initialize
bars := OrderedCollection new.
(TheWorldMenu respondsTo: #registerOpenCommand:)
ifTrue: [TheWorldMenu unregisterOpenCommand: 'Services Browser'.
TheWorldMenu registerOpenCommand: {'Services Browser'. {PreferenceBrowser. #openForServices}}]!
----- Method: ServiceGui class>>inlineServices (in category 'preferences') -----
inlineServices
^ ServicePreferences valueOfPreference: #inlineServicesInMenu !
----- Method: ServiceGui class>>onlyServices (in category 'preferences') -----
onlyServices
^ ServicePreferences valueOfPreference: #useOnlyServicesInMenu!
----- Method: ServiceGui class>>openMenuFor: (in category 'opening menus') -----
openMenuFor: aServiceCategory
(self new menuFor: aServiceCategory) invokeModal!
----- Method: ServiceGui class>>registerBar:for: (in category 'registering button bars') -----
registerBar: aBar for: service
self bars removeAllSuchThat: [:a | a value isNil].
self bars add: (WeakValueAssociation key: service value: aBar).!
----- Method: ServiceGui class>>updateBar: (in category 'registering button bars') -----
updateBar: cat
self bars
select: [:assoc | (assoc key id = cat id) & assoc value notNil]
thenDo: [:assoc | | newBar |
cat requestor: assoc key requestor.
newBar := self new buttonBarFor: cat.
assoc value removeAllMorphs.
newBar submorphsDo: [:m | assoc value addMorphBack: m]]!
----- Method: ServiceGui class>>updateBars (in category 'registering button bars') -----
updateBars
self bars do: [:assoc | | oldCat cat newBar bar |
(bar := assoc value) ifNotNil: [
oldCat := assoc key.
cat := oldCat id service.
cat requestor: oldCat requestor.
newBar := self new buttonBarFor: cat.
bar removeAllMorphs.
newBar submorphsDo: [:m | bar addMorphBack: m]].
]!
----- Method: ServiceGui class>>updateMenu:forModel:selector: (in category 'hooks') -----
updateMenu: aMenu forModel: aModel selector: selector
('codePane*' match: selector) ifTrue: [
(self new for: aModel id: #codeSelectionRefactorings) inlineInMenu: aMenu].
^ aMenu
!
----- Method: ServiceGui class>>worldMenu: (in category 'hooks') -----
worldMenu: aMenu
^ (self new for: aMenu id: #world) inlineInMenu: aMenu!
----- Method: ServiceGui>>bar (in category 'accessing') -----
bar
^ bar!
----- Method: ServiceGui>>buildButtonBar (in category 'building') -----
buildButtonBar
bar := self buttonBarFor: service.
self class registerBar: bar for: service.
^ bar!
----- Method: ServiceGui>>buttonBarFor: (in category 'servicecategory') -----
buttonBarFor: aServiceCategory
self styleBar: self bar.
aServiceCategory enabledServices
do: [:each | self bar
addMorphBack: (self buttonFor: each)].
^ self bar!
----- Method: ServiceGui>>buttonFor: (in category 'services') -----
buttonFor: aService
^ aService isCategory ifTrue: [self buttonForCategory: aService]
ifFalse: [self buttonForAction: aService]!
----- Method: ServiceGui>>buttonForAction: (in category 'serviceactions') -----
buttonForAction: aService
"see getstate for availability?"
| aButton |
aButton := PluggableButtonMorph
on: aService
getState: nil
action: #execute.
self styleButton: aButton.
aButton
label: aService buttonLabel;
setBalloonText: aService description.
^aButton!
----- Method: ServiceGui>>buttonForCategory: (in category 'servicecategory') -----
buttonForCategory: aService
"see getstate for availability?"
| aButton |
aButton := PluggableButtonMorph
on: [:button | aService requestor: button requestor.
self class openMenuFor: aService]
getState: nil
action: #value:.
aButton arguments: (Array with: aButton).
self styleButton: aButton.
aButton
label: aService buttonLabel.
^aButton!
----- Method: ServiceGui>>for:id: (in category 'initialization') -----
for: caller id: id
service := id service.
caller ifNotNil: [service requestor: caller requestor]!
----- Method: ServiceGui>>initialize (in category 'initialization') -----
initialize
super initialize.
menu := OrderedCollection new.
bar := AlignmentMorph newRow.
n := OrderedCollection with: 0!
----- Method: ServiceGui>>inlineInMenu: (in category 'building') -----
inlineInMenu: aMenu
^ self class inlineServices
ifTrue: [self inlineInMenu: aMenu for: service]
ifFalse: [aMenu]!
----- Method: ServiceGui>>inlineInMenu:for: (in category 'servicecategory') -----
inlineInMenu: aMenu for: aServiceCategory
menu addLast: aMenu.
aServiceCategory enabledServices
do: [:each | self menuItemFor: each].
^ self popMenu!
----- Method: ServiceGui>>menu (in category 'accessing') -----
menu
^ menu last!
----- Method: ServiceGui>>menuFor: (in category 'servicecategory') -----
menuFor: aServiceCategory
| submenu |
submenu := self subMenuFor: aServiceCategory.
^ submenu
addTitle: (aServiceCategory menuLabel)!
----- Method: ServiceGui>>menuItemFor: (in category 'services') -----
menuItemFor: aService
[aService isCategory ifTrue: [self menuItemForCategory: aService]
ifFalse: [self menuItemForAction: aService]]
on: Error
do: [:er | (self confirm: 'menuItemFor: error. debug?') ifTrue: [er signal]]!
----- Method: ServiceGui>>menuItemForAction: (in category 'serviceactions') -----
menuItemForAction: aServiceAction
"Returns a menuItem triggering self"
self menu
add: (aServiceAction menuLabelNumbered: self n)
target: aServiceAction
selector: #execute.
Smalltalk isMorphic ifTrue: [
self menu lastItem isEnabled: aServiceAction executeCondition.
self menu balloonTextForLastItem: aServiceAction description]!
----- Method: ServiceGui>>menuItemForCategory: (in category 'servicecategory') -----
menuItemForCategory: aServiceCategory
"Returns a menuItem triggering self"
| submenu |
submenu := self subMenuFor: aServiceCategory.
self menu add: (aServiceCategory menuLabelNumbered: self n) subMenu: submenu!
----- Method: ServiceGui>>n (in category 'servicecategory') -----
n
^ n last!
----- Method: ServiceGui>>n: (in category 'servicecategory') -----
n: nn
n removeLast.
n addLast: nn!
----- Method: ServiceGui>>popMenu (in category 'servicecategory') -----
popMenu
| aMenu |
aMenu := menu removeLast.
n removeLast.
self styleMenu: aMenu.
^ aMenu!
----- Method: ServiceGui>>pushMenu (in category 'servicecategory') -----
pushMenu
menu addLast: MenuMorph new.
n addLast: 0!
----- Method: ServiceGui>>styleBar: (in category 'styling') -----
styleBar: aBar
aBar setNameTo: 'button bar'.
aBar beSticky;
hResizing: #spaceFill;
wrapCentering: #center;
cellPositioning: #leftCenter;
clipSubmorphs: true;
cellInset: 0;
color: Preferences defaultWindowColor.!
----- Method: ServiceGui>>styleButton: (in category 'styling') -----
styleButton: aButton
aButton color: Color transparent;
onColor: Color transparent offColor: Color transparent;
borderStyle: (BorderStyle width: 1 color: Color gray);
askBeforeChanging: true;
clipSubmorphs: true;
hResizing: #spaceFill;
vResizing: #spaceFill.
^ self!
----- Method: ServiceGui>>styleMenu: (in category 'styling') -----
styleMenu: aMenu
"gradient, etc ..?"
"aMenu color: Color white;
borderStyle: (BorderStyle width: 1 color: Color gray);
clipSubmorphs: true;
addDropShadow;
shadowColor: (TranslucentColor
r: 0.0
g: 0.0
b: 0.0
alpha: 0.666);
shadowOffset: 1 @ 1"!
----- Method: ServiceGui>>subMenuFor: (in category 'servicecategory') -----
subMenuFor: aServiceCategory
self pushMenu.
aServiceCategory enabledServices
ifEmpty: [self menuItemFor: ServiceAction new].
aServiceCategory enabledServices
doWithIndex: [:each :i | self n: i. self menuItemFor: each].
^ self popMenu!
Object subclass: #ServiceProvider
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base'!
!ServiceProvider commentStamp: 'rr 7/10/2006 15:08' prior: 0!
A ServiceProvider references services that are relevant to a given application.
Each application that wishes to use the Services framework must subclass a ServiceProvider.
This class must define a 'services' method category.
Each method implemented in this category will be automatically called by the framework.
Each of these method should be a unary message (taking no argument), and return a fully initialised instance of ServiceAction or ServiceCategory. There are three possible patterns:
1)
serviceIdentifierAndMethodName
^ ServiceAction
text: 'Menu item text'
button: 'Button text'
description: 'Longer text that appears in help balloons'
action: [:r | "Code block fetching data from the requestor instance, r, that is passed to the block"]
2)
serviceIdentifierAndMethodName
^ ServiceAction
text: 'Menu item text'
button: 'Button text'
description: 'Longer text that appears in help balloons'
action: [:r | "Code block fetching data from the requestor instance, r, that is passed to the block"]
condition: [:r | "second block returning true if the service can be used at the time being, false otherwise. Data can still be fetched from the requestor instance"]
3)
methodNameAndServiceCategoryId
^ ServiceCategory
text: 'Menu text'
button: 'Button text'
description: 'Longer descriptive text appearing in help balloons'
The organisation of services into categories, and the services bound to keyboard shortcuts are
specified using the Services Browser (see the comment on the class ServicesPreferences for more details). When editing preferences, they are saved as methods on the ServiceProvider, all defined
in the 'saved preferences' method category. Each of thesse methods stores preferences that the provider can replay.
!
ServiceProvider subclass: #BrowserProvider
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base-Providers'!
!BrowserProvider commentStamp: 'rr 7/10/2006 15:17' prior: 0!
I define the default categories of services dealing with browsing:
- the class category menu (service identifier: browserClassCategoryMenu)
- the class menu (browserClassMenu)
- the method category menu (browserMethodCategoryMenu)
- the browser method menu (browserMethodMenu)
- the browser button bar (browserButtonBar)
- the browser code pane/selection menu (browserCodePaneMenu)!
----- Method: BrowserProvider class>>initialize (in category 'initialize-release') -----
initialize
ServiceRegistry current buildProvider: self new!
----- Method: BrowserProvider>>browser (in category 'services') -----
browser
^ ServiceCategory text: 'Browser'
button: 'browser'
description: 'The browser menus'!
----- Method: BrowserProvider>>browserButtonBar (in category 'services') -----
browserButtonBar
^ ServiceCategory
text:'button bar'
button:'button'
description:'the browser button bar'!
----- Method: BrowserProvider>>browserClassCategoryMenu (in category 'services') -----
browserClassCategoryMenu
^ ServiceCategory
text:'Class Category'
button:'class cat'
description:'The browser class category menu'!
----- Method: BrowserProvider>>browserClassMenu (in category 'services') -----
browserClassMenu
^ ServiceCategory
text:'Class'
button:'class'
description:'The browser class menu'!
----- Method: BrowserProvider>>browserClassMenushortcut (in category 'saved preferences') -----
browserClassMenushortcut
^ #(#'Shortcut for browserClassMenu:' '' 1000 )!
----- Method: BrowserProvider>>browserCodePaneMenu (in category 'services') -----
browserCodePaneMenu
^ ServiceCategory text: 'Code Pane'
button: 'pane'
description: 'The browser code pane menu'!
----- Method: BrowserProvider>>browserMethodCategoryMenu (in category 'services') -----
browserMethodCategoryMenu
^ ServiceCategory
text:'Method Category'
button:'method cat'
description:'The browser method menu'!
----- Method: BrowserProvider>>browserMethodMenu (in category 'services') -----
browserMethodMenu
^ ServiceCategory
text:'Method'
button:'method'
description:'The browser method menu'!
----- Method: BrowserProvider>>browserMethodMenushortcut (in category 'saved preferences') -----
browserMethodMenushortcut
^ #(#'Shortcut for browserMethodMenu:' '' 1000 )!
----- Method: ServiceProvider class>>newProviderFor: (in category 'provider creation') -----
newProviderFor: packageName
| cls clsName |
clsName := ((packageName copyWithout: $-) , 'ServiceProvider') asSymbol.
cls := self subclass: clsName
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: packageName.
cls class compile: 'initialize
ServiceRegistry buildProvider: self new' classified: 'initialization'.
^ cls!
----- Method: ServiceProvider class>>registeredProviders (in category 'accessing') -----
registeredProviders
^ self allSubclasses collect: [:each | each new]!
----- Method: ServiceProvider>>performAndSetId: (in category 'accessing') -----
performAndSetId: aSymbol
| service |
service := self perform: aSymbol.
service id: aSymbol.
^service!
----- Method: ServiceProvider>>registeredServices (in category 'accessing') -----
registeredServices
^ self services collect: [:each | self performAndSetId: each]!
----- Method: ServiceProvider>>replayPreferences (in category 'persistence') -----
replayPreferences
ServicePreferences replayPreferences: self savedPreferences!
----- Method: ServiceProvider>>savePreferencesFor: (in category 'persistence') -----
savePreferencesFor: aService
"pref := ServicePreferences preferenceAt: aService shortcutPreference.
strm := WriteStream with: ''.
strm nextPutAll: aService id;
nextPutAll: 'shortcut';
cr;
tab;
nextPutAll: '^ ';
nextPutAll: {pref name. pref preferenceValue. 1000} storeString.
self class compileSilently: strm contents classified: 'saved preferences'."
aService isCategory
ifTrue: [aService externalPreferences
doWithIndex: [:e :i | | strm |
strm := WriteStream with: aService id asString.
strm nextPutAll: e id asString;
cr;
tab;
nextPutAll: '^ ';
nextPutAll: {aService childrenPreferences. e id. i} storeString.
e provider class compileSilently: strm contents classified: 'saved preferences']]!
----- Method: ServiceProvider>>savedPreferences (in category 'persistence') -----
savedPreferences
^ (self class organization listAtCategoryNamed: #'saved preferences')
collect: [:e | self perform: e]!
----- Method: ServiceProvider>>services (in category 'accessing') -----
services
^ self class organization listAtCategoryNamed: #services!
ServiceProvider subclass: #WorldMenuProvider
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base-Providers'!
!WorldMenuProvider commentStamp: 'rr 7/10/2006 15:19' prior: 0!
I define services and categories:
- The world menu category (identifier: world), where services and categories can be put to be displayed in the world menu.
- The preferencesMenu category, where services about services and preferences can be put
- th open menu!
----- Method: WorldMenuProvider class>>initialize (in category 'initialize-release') -----
initialize
ServiceRegistry current buildProvider: self new!
----- Method: WorldMenuProvider>>browserMethodMenucreateNewService (in category 'saved preferences') -----
browserMethodMenucreateNewService
^ #(#'Items in browserMethodMenu:' #createNewService 1 )!
----- Method: WorldMenuProvider>>closeTopWindow (in category 'services') -----
closeTopWindow
^ ServiceAction
text: 'Close top window'
button: 'close window'
description: 'Closes the focused window'
action: [:r | SystemWindow topWindow delete]!
----- Method: WorldMenuProvider>>convertOpenCommand: (in category 'service registering') -----
convertOpenCommand: array
| description |
description := array size > 2
ifTrue: [array third]
ifFalse: ['none available'].
^ServiceAction
id: array first asSymbol
text: array first
button: array first
description: description
action: [array second first perform: array second second]!
----- Method: WorldMenuProvider>>createNewService (in category 'services') -----
createNewService
^ ServiceAction
text: 'Create new service'
button: 'new service'
description: 'Define a new service provided by this package'
action: [:r | | s p |
s := r caption: 'enter service identifier'; getSymbol.
p := r getPackageProvider.
p compile: s, '
^ ServiceAction
"Open the service browser to set the menu position and the keyboard shortcut"
text: ''fill menu label''
button: ''short button text''
description: ''longer text for balloon help''
action: [:r | "action block"]
condition: [:r | "optional condition block"]' classified: 'services'.
r getBrowser browseReference: (MethodReference class: p selector: s)]!
----- Method: WorldMenuProvider>>helpOnServices (in category 'services') -----
helpOnServices
^ ServiceAction
text: 'Help on Services'
button: 'services help'
description: 'Introductory text about services'
action: [StringHolder new contents: self servicesHelpText; openLabel: 'Introduction to Services'].!
----- Method: WorldMenuProvider>>nextWindow (in category 'services') -----
nextWindow
^ ServiceAction text: 'Switch to next window' button: 'next window' description: 'Switches to the next window' action: [:r | SystemWindow sendTopWindowToBack]!
----- Method: WorldMenuProvider>>openMenu (in category 'services') -----
openMenu
^ ServiceCategory text: 'Open' button: 'open' description: 'The open menu'!
----- Method: WorldMenuProvider>>preferencesBrowser (in category 'services') -----
preferencesBrowser
^ ServiceAction text: 'Preference Browser' button: 'pref. browser' description: 'Open the preference browser to edit various Squeak settings' action: [PreferenceBrowser open].!
----- Method: WorldMenuProvider>>preferencesMenu (in category 'services') -----
preferencesMenu
^ ServiceCategory text: 'Preferences & Services' button: 'preferences' description: 'Menu related to editing preferences'!
----- Method: WorldMenuProvider>>preferencesMenuhelpOnServices (in category 'saved preferences') -----
preferencesMenuhelpOnServices
^ #(#'Items in preferencesMenu:' #helpOnServices 3 )!
----- Method: WorldMenuProvider>>preferencesMenupreferencesBrowser (in category 'saved preferences') -----
preferencesMenupreferencesBrowser
^ #(#'Items in preferencesMenu:' #preferencesBrowser 1 )!
----- Method: WorldMenuProvider>>preferencesMenurebuildRegistry (in category 'saved preferences') -----
preferencesMenurebuildRegistry
^ #(#'Items in preferencesMenu:' #rebuildRegistry 4 )!
----- Method: WorldMenuProvider>>preferencesMenuservicesBrowser (in category 'saved preferences') -----
preferencesMenuservicesBrowser
^ #(#'Items in preferencesMenu:' #servicesBrowser 2 )!
----- Method: WorldMenuProvider>>preferencesMenushortcut (in category 'saved preferences') -----
preferencesMenushortcut
^ #(#'Shortcut for preferencesMenu:' '' 1000 )!
----- Method: WorldMenuProvider>>rebuildRegistry (in category 'services') -----
rebuildRegistry
^ ServiceAction text: 'Rebuild service registry' button: 'rebuild registry' description: 'Rebuilds the service registry to scan for newly defined services' action: [ServiceRegistry rebuild].!
----- Method: WorldMenuProvider>>servicesBrowser (in category 'services') -----
servicesBrowser
^ ServiceAction text: 'Services Browser' button: 'services' description: 'Open a preference browser to edit several Squeak menus' action: [PreferenceBrowser openForServices].!
----- Method: WorldMenuProvider>>servicesHelpText (in category 'accessing') -----
servicesHelpText
^ '
This is an overview of the main concepts of the services framework. More details are available in class comments. The aim is to help you defining services step by step. The three main classes are:
-ServiceAction
-ServiceCategory
-ServiceProvider
Alongside them, a tool to use is the Services Browser. It can be found in the world menu, under the ''Preferences & Services'' menu heading (in which you found this text).
ServiceAction are executable objects in various contexts.
They can be displayed as buttons or menu items or bounded to keyboard shortcuts.
ServiceCategory are categories of services. They are also services, so a ServiceCategory can be included in another, forming a tree of Services. ServiceCategories can be displayed with menus, or button bars.
A ServiceProvider references services that are relevant to a given application.
Each application that wishes to use the Services framework must subclass a ServiceProvider.
This class must define a ''services'' method category.
Each method implemented in this category will be automatically called by the framework.
Each of these method should be a unary message (taking no argument), and return a fully initialised instance of ServiceAction or ServiceCategory. There are three possible patterns:
1)
serviceIdentifierAndMethodName
^ ServiceAction
text: ''Menu item text''
button:''Button text''
description: ''Longer text that appears in help balloons''
action: [:r | "Code block fetching data from the requestor instance, r, that is passed to the block"]
2)
serviceIdentifierAndMethodName
^ ServiceAction
text: ''Menu item text''
button: ''Button text''
description: ''Longer text that appears in help balloons''
action: [:r | "Code block fetching data from the requestor instance, r, that is passed to the block"]
condition: [:r | "second block returning true if the service can be used at the time being, false otherwise. Data can still be fetched from the requestor instance"]
3)
methodNameAndServiceCategoryId
^ ServiceCategory
text: ''Menu text''
button: ''Button text''
description: ''Longer descriptive text appearing in help balloons''
The block given to the ServiceActions can take an instance of the Requestor class as parameter. You can fetch data from these. The generic format is to call methods starting with ''get'' on the requestor, like getClass, getMessageName for services related to the browser.
The organisation of services into categories, and the services bound to keyboard shortcuts are
specified using the Services Browser, based on the Preference Browser by Hernan Tylim. When editing preferences, they are saved as methods on the ServiceProvider, all defined in the ''saved preferences'' method category.
When opening the Services Browser you see a list of preference categories on the left, and the preferences inside this category on the right. The main preference categories for services are:
-- keyboard shortcuts -- : several text preferences, one per keyboard shortcuts. To edit them, enter a service identifier (equal to the method name under which it is defined in its ServiceProvider), and accept with alt-s or enter
-- menu contents -- : All the service categories in the image have a text preference under here. To edit it, enter the services identifiers you wish to put in this category, separating them with a single space character. The order is important: it defines the order of the items in menus.
-- settings -- : general boolean preferences.
Then there is a preference category for each provider in the image. Under each, you will find:
A boolean preference for each service in the image. If it is false, the service will not appear in menus.
The text preference for each service category defined by the service provider. This is the same as the one appearing in the menu contents preference category.
Some identifiers of categories already appearing in the UI are:
- world : the world menu
- preferencesMenu
- browserClasssCategoryMenu
- browserClassMenu
- browserMethodCategoryMenu
- browserMethodMenu
- browserCodePaneMenu
- browserButtonBar
After editing these preferences to match the services and categories you defined for your application, you should be done.
Romain Robbes'!
----- Method: WorldMenuProvider>>world (in category 'services') -----
world
^ ServiceCategory text: 'World' button: 'world' description: 'The world menu'!
----- Method: WorldMenuProvider>>worldpreferencesMenu (in category 'saved preferences') -----
worldpreferencesMenu
^ #(#'Items in world:' #preferencesMenu 1 )!
----- Method: WorldMenuProvider>>worldshortcut (in category 'saved preferences') -----
worldshortcut
^ #(#'Shortcut for world:' '' 1000 )!
Object subclass: #ServiceRegistry
instanceVariableNames: 'services interactive'
classVariableNames: 'Current'
poolDictionaries: ''
category: 'Services-Base'!
!ServiceRegistry commentStamp: 'rr 7/10/2006 15:10' prior: 0!
The ServiceRegistry is the repository in which services are stored. They are stored in
a dictionary, and keyed by their identifier (which is the name of the method they were defined in).
The registry handles the intialization, building and referencing processes as well.!
----- Method: ServiceRegistry class>>current (in category 'as yet unclassified') -----
current
^ Current ifNil: [Current := self new]!
----- Method: ServiceRegistry class>>ifInteractiveDo: (in category 'as yet unclassified') -----
ifInteractiveDo: aBlock
self current isInteractive ifTrue: [aBlock value]!
----- Method: ServiceRegistry class>>initialize (in category 'as yet unclassified') -----
initialize
self rebuild.
SystemChangeNotifier uniqueInstance
notify: self
ofSystemChangesOfItem: #method
using: #methodChanged:
!
----- Method: ServiceRegistry class>>methodChanged: (in category 'as yet unclassified') -----
methodChanged: event
self ifInteractiveDo: [
| cls |
cls := event itemClass.
((event changeKind = #removed) not & (cls inheritsFrom: ServiceProvider) and: [cls new services includes: event itemSelector])
ifTrue: [[self current addService: (cls new performAndSetId: event itemSelector)
provider: cls]
on: Error do: [self inform: 'Service format seems to be incorrect']]]!
----- Method: ServiceRegistry class>>rebuild (in category 'as yet unclassified') -----
rebuild
| old |
old := Current.
[Current := self new.
Current build]
on: Error
do: [:err | (self confirm: 'An error occured during build.
Debug it?')
ifTrue: [err signal].
Current := old]!
----- Method: ServiceRegistry>>addService:provider: (in category 'building') -----
addService: aService provider: p
services at:aService id put:aService.
aService provider: p.
aService insertPreferences
!
----- Method: ServiceRegistry>>beNotInteractiveDuring: (in category 'building') -----
beNotInteractiveDuring: aBlock
interactive := false.
aBlock value.
interactive := true!
----- Method: ServiceRegistry>>build (in category 'building') -----
build
"ServicePreferences wipe."
self
beNotInteractiveDuring: [
| pr |
ServiceProvider registeredProviders
do: [:p | p registeredServices
do: [:each | self addService: each provider: p class]].
pr := ServiceProvider registeredProviders
gather: [:p | p savedPreferences].
ServicePreferences replayPreferences: pr.
].
ServiceGui updateBars.
ServiceShortcuts setPreferences!
----- Method: ServiceRegistry>>buildProvider: (in category 'building') -----
buildProvider: p
self beNotInteractiveDuring: [
p registeredServices do: [:each | self addService: each provider: p class].
p replayPreferences]
!
----- Method: ServiceRegistry>>categories (in category 'accessing') -----
categories
^ self serviceCollection select: [:s | s isCategory]!
----- Method: ServiceRegistry>>initialize (in category 'initialize-release') -----
initialize
services := Dictionary new.
interactive := true!
----- Method: ServiceRegistry>>isInteractive (in category 'accessing') -----
isInteractive
^ interactive!
----- Method: ServiceRegistry>>serviceCollection (in category 'accessing') -----
serviceCollection
^ services asArray!
----- Method: ServiceRegistry>>serviceWithId: (in category 'accessing') -----
serviceWithId: aSymbol
^ services at: aSymbol
ifAbsent: [nil]!
----- Method: ServiceRegistry>>services (in category 'accessing') -----
services
^ self serviceCollection reject: [:s | s isCategory]!
Object subclass: #ServiceShortcuts
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base'!
ServiceShortcuts class
instanceVariableNames: 'map'!
!ServiceShortcuts commentStamp: 'rr 7/10/2006 15:14' prior: 0!
A data structures implementing a simple form of keyboard shortucts is defined on the class side.
Available keyboard shortcuts are:
command-0 to command-9 (command is also called alt on some systems).
control-0 to control-0
command-control-0 to command-control-9 (command is also alt)
control-command-left arrow
control-command-up arrow
control-command-right arrow
control-command-down arrow
Using the Services Browser (see class ServicePreferences), these shortcuts can be bound to service identifiers.!
ServiceShortcuts class
instanceVariableNames: 'map'!
----- Method: ServiceShortcuts class>>arrowShortcut:event: (in category 'as yet unclassified') -----
arrowShortcut: str event: event
| key s |
key := event keyCharacter caseOf: {
[Character arrowDown] -> ['down'].
[Character arrowUp] -> ['up'].
[Character arrowLeft] -> ['left'].
[Character arrowRight] -> ['right']}.
s := self map
at: str , key
ifAbsent: [^ self].
s serviceOrNil
ifNotNil: [:sv | sv execute.
event wasHandled: true]!
----- Method: ServiceShortcuts class>>changeShortcut:to: (in category 'as yet unclassified') -----
changeShortcut: shortcut to: aString
aString isBlock ifTrue: [^self map at: shortcut put: aString].
(aString beginsWith: '[') ifTrue: [^self map at: shortcut put: aString].
aString isEmpty ifTrue: [self map removeKey: shortcut ifAbsent: []]
ifFalse: [self map at: shortcut put: aString]!
----- Method: ServiceShortcuts class>>handleKeystroke: (in category 'as yet unclassified') -----
handleKeystroke: event
[event isKeystroke
ifTrue: [self process: event]]
on: Error
do: [:e | (self confirm: 'shortcut error. debug?') ifTrue: [e signal]]!
----- Method: ServiceShortcuts class>>insertPrefShortcut: (in category 'as yet unclassified') -----
insertPrefShortcut: short
ServicePreferences
addPreference: short
categories: #('-- keyboard shortcuts --' )
default: ''
balloonHelp: 'enter a service id to bind it to this shortcut'
projectLocal: false
changeInformee: [self
changeShortcut: short
to: (ServicePreferences valueOfPreference: short)]
changeSelector: #value
type: #String!
----- Method: ServiceShortcuts class>>map (in category 'as yet unclassified') -----
map
^ map ifNil: [map := Dictionary new]!
----- Method: ServiceShortcuts class>>process: (in category 'as yet unclassified') -----
process: event
event keyCharacter isDigit
ifTrue: [event commandKeyPressed & event controlKeyPressed
ifTrue: [^ self shortcut: 'ctrl-cmd-' event: event].
event commandKeyPressed
ifTrue: [^ self shortcut: 'cmd-' event: event].
event controlKeyPressed
ifTrue: [^ self shortcut: 'ctrl-' event: event]].
({Character arrowUp. Character arrowDown. Character arrowLeft. Character arrowRight} includes: event keyCharacter)
ifTrue: [event commandKeyPressed & event controlKeyPressed
ifTrue: [^ self arrowShortcut: 'ctrl-cmd-' event: event].
]!
----- Method: ServiceShortcuts class>>setPreferences (in category 'as yet unclassified') -----
setPreferences
| mm |
mm := self map copy.
(0 to: 9)
do: [:i | #('ctrl-' 'cmd-' 'ctrl-cmd-' )
do: [:str |
| short |
short := (str , i asString) asSymbol.
self insertPrefShortcut: short]].
#(#up #down #left #right )
do: [:s |
self insertPrefShortcut: ('ctrl-cmd-' , s) asSymbol.].
mm
keysAndValuesDo: [:k :v | ServicePreferences setPreference: k toValue: v].
((Array new: 3) at: 1 put: ((Array new: 3) at: 1 put: #inlineServicesInMenu;
at: 2 put: true;
at: 3 put: 'Inline services within squeak menus';
yourself);
at: 2 put: ((Array new: 3) at: 1 put: #useOnlyServicesInMenu;
at: 2 put: false;
at: 3 put: 'Use only services and not regular menu items';
yourself);
at: 3 put: ((Array new: 3) at: 1 put: #useServicesInBrowserButtonBar;
at: 2 put: true;
at: 3 put: 'Use a service-based button bar';
yourself);
yourself)
do: [:tr | ServicePreferences
addPreference: tr first
categories: #('-- settings --' )
default: tr second
balloonHelp: tr third]!
----- Method: ServiceShortcuts class>>shortcut:event: (in category 'as yet unclassified') -----
shortcut: str event: event
| s |
Transcript cr.
s := self map
at: str , event keyCharacter asString
ifAbsent: [^ self].
(s beginsWith: '[') ifTrue: [^ (Compiler evaluateUnloggedForSelf: s) value].
s serviceOrNil
ifNotNil: [:sv | sv execute.
event wasHandled: true]!
----- Method: FillInTheBlankMorph>>selection (in category '*services-base') -----
selection
"answers what is actually selected in the morph"
^ textPane selectionInterval!
----- Method: BlockClosure>>valueWithRequestor: (in category '*services-base') -----
valueWithRequestor: aRequestor
"To do later: make the fillInTheBlank display more informative captions.
Include the description of the service, and maybe record steps"
^ self numArgs isZero
ifTrue: [self value]
ifFalse: [self value: aRequestor]!
----- Method: SequenceableCollection>>startsWith: (in category '*services-base') -----
startsWith: start
| comp |
self deprecated: 'Use #beginsWith:'.
self size < start size ifTrue: [^ false].
comp := true.
(self first: start size) with: start
do: [:ea :ea2 | ea = ea2 ifFalse: [comp := false]].
^ comp!
----- Method: Morph>>requestor (in category '*services-base') -----
requestor
^ owner ifNil: [super requestor] ifNotNil: [owner requestor]!
Warning subclass: #ServiceCancelled
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Services-Base'!
!ServiceCancelled commentStamp: 'rr 4/1/2004 18:24' prior: 0!
Exception raised when a service is cancelled, to inform the user.!
----- Method: ServiceCancelled>>defaultAction (in category 'handling') -----
defaultAction
Transcript cr; show: 'service has been cancelled'!
----- Method: ServiceCancelled>>messageText (in category 'accessing') -----
messageText
^ 'Service has been cancelled'!
----- Method: CodeHolder>>messageListMenuServices: (in category '*services-base') -----
messageListMenuServices: aMenu
<messageListMenu>
<menuPriority: 150>
ServiceGui browser: self messageListMenu: aMenu.
^ Preferences useOnlyServicesInMenu ifTrue: [nil] ifFalse: [aMenu]!
----- Method: CodeHolder>>requestor (in category '*services-base') -----
requestor
^ (BrowserRequestor new) browser: self; yourself!
----- Method: Browser>>browseReference: (in category '*services-base') -----
browseReference: ref
self okToChange ifTrue: [
self selectCategoryForClass: ref actualClass theNonMetaClass.
self selectClass: ref actualClass theNonMetaClass .
ref actualClass isMeta ifTrue: [self indicateClassMessages].
self changed: #classSelectionChanged.
self selectMessageCategoryNamed: ref category.
self selectedMessageName: ref methodSymbol.
]!
----- Method: Browser>>classCategoryMenuServices: (in category '*services-base') -----
classCategoryMenuServices: aMenu
<systemCategoryMenu>
<menuPriority: 150>
ServiceGui browser: self classCategoryMenu: aMenu.
^ Preferences useOnlyServicesInMenu ifTrue: [nil] ifFalse: [aMenu]!
----- Method: Browser>>classListMenuServices: (in category '*services-base') -----
classListMenuServices: aMenu
<classListMenu>
<menuPriority: 150>
ServiceGui browser: self classMenu: aMenu.
^ Preferences useOnlyServicesInMenu ifTrue: [nil] ifFalse: [aMenu]!
----- Method: Browser>>messageCategoryMenuServices: (in category '*services-base') -----
messageCategoryMenuServices: aMenu
<messageCategoryMenu>
<menuPriority: 150>
ServiceGui browser: self messageCategoryMenu: aMenu.
^ Preferences useOnlyServicesInMenu ifTrue: [nil] ifFalse: [aMenu]!
----- Method: Browser>>methodReference (in category '*services-base') -----
methodReference
| cls sel |
cls := self selectedClassOrMetaClass.
sel := self selectedMessageName.
cls isNil | sel isNil ifTrue: [^nil].
^ MethodReference class: cls selector: sel!
----- Method: Browser>>optionalButtonRow (in category '*services-base') -----
optionalButtonRow
^ServiceGui browserButtonRow: self inlinedIn: super optionalButtonRow!
----- Method: Browser>>selectReference: (in category '*services-base') -----
selectReference: ref
self browseReference: ref!
Chris Muller uploaded a new version of VersionNumber to project Squeak 4.6:
http://source.squeak.org/squeak46/VersionNumber-cmm.4.mcz
==================== Summary ====================
Name: VersionNumber-cmm.4
Author: cmm
Time: 9 June 2012, 1:50:58.564 pm
UUID: 68fb1f05-d3e2-4c9b-9234-20a9bed166dc
Ancestors: VersionNumber-nice.3
Strengthen VersionNumber>>#=.
==================== Snapshot ====================
SystemOrganization addCategory: #VersionNumber!
Magnitude subclass: #VersionNumber
instanceVariableNames: 'numbers'
classVariableNames: ''
poolDictionaries: ''
category: 'VersionNumber'!
!VersionNumber commentStamp: '<historical>' prior: 0!
I am a version number. My representation allows me to handle an entire tree of versions. Once created, an instance should not change (note: VersionNumbers could be canonicalized like Symbols, but are not currently).
I am a magnitude so that you can see if one version preceeds another (only if the two versions are in the same branch).
'2.1' asVersion < '2.2.1' asVersion "true"
'2.3' asVersion < '2.2.1' asVersion "error different branches"
'2.3' asVersion inSameBranchAs: '2.2.1' asVersion "false, why the previous one failed."
'2.1' asVersion = '2.1' asVersion "true, obviously"
To get the next version number in the same branch:
'2.3.4' asVersion next "2.3.5"
To get the next version number, starting a new branch:
'2.3.4' asVersion branchNext "2.3.4.1"
To get the common base version of any two version numbers (useful for merging):
'2.3.8' asVersion commonBase: '2.3.4.1' asVersion "2.3.4"!
----- Method: VersionNumber class>>first (in category 'as yet unclassified') -----
first
^self fromCollection: #(1)!
----- Method: VersionNumber class>>fromCollection: (in category 'as yet unclassified') -----
fromCollection: aCollection
^self new
initializeNumbers: aCollection;
yourself!
----- Method: VersionNumber class>>fromString: (in category 'as yet unclassified') -----
fromString: aString
^self fromCollection:
((aString findTokens: '.') collect: [:ea | ea asNumber ])
!
----- Method: VersionNumber>>< (in category 'comparing') -----
< another
"Answer whether the receiver is less than the argument."
| tmp |
(self inSameBranchAs: another) ifFalse:
[^self error: 'Receiver and argument in different branches'].
tmp := another numbers.
(tmp size = numbers size) ifTrue:
[1 to: numbers size do:
[ :in | (numbers at: in) < (tmp at: in) ifTrue: [^true]].
^false].
^numbers size < tmp size
!
----- Method: VersionNumber>>= (in category 'comparing') -----
= aVersion
self == aVersion ifTrue: [ ^ true ].
aVersion species = self species ifFalse: [ ^ false ].
^ numbers = aVersion numbers!
----- Method: VersionNumber>>branchNext (in category 'accessing') -----
branchNext
^self class fromCollection: (numbers, (Array with: 1))!
----- Method: VersionNumber>>commonBase: (in category 'accessing') -----
commonBase: aVersion
| smallNums largeNums cutoff |
(aVersion numbers size <= numbers size)
ifTrue: [smallNums := aVersion numbers. largeNums := numbers]
ifFalse: [smallNums := numbers. largeNums := aVersion numbers].
cutoff := (1 to: smallNums size)
detect: [ :in | ((smallNums at: in) ~= (largeNums at: in))]
ifNone: [^self class fromCollection: smallNums].
^self class fromCollection:
((numbers copyFrom: 1 to: (cutoff - 1)),
(Array with: ((smallNums at: cutoff) min: (largeNums at: cutoff))))
!
----- Method: VersionNumber>>hash (in category 'comparing') -----
hash
^numbers hash!
----- Method: VersionNumber>>inSameBranchAs: (in category 'testing') -----
inSameBranchAs: aVersion
| less more |
(aVersion numbers size <= numbers size)
ifTrue: [less := aVersion numbers. more := numbers]
ifFalse: [less := numbers. more := aVersion numbers].
1 to: (less size - 1) do: [ :in | ((less at: in) = (more at: in)) ifFalse: [^false]].
^less size = more size or:
[(less at: less size) <= (more at: less size)]
!
----- Method: VersionNumber>>initializeNumbers: (in category 'initialization') -----
initializeNumbers: aCollection
aCollection do: [ :ea |
ea <= 0 ifTrue:
[^self error: 'VersionNumbers cannot contain zero or negative numbers']].
numbers := aCollection asArray!
----- Method: VersionNumber>>next (in category 'accessing') -----
next
| tmp |
tmp := numbers copy.
tmp at: numbers size put: (numbers last + 1).
^self class fromCollection: tmp!
----- Method: VersionNumber>>numbers (in category 'accessing') -----
numbers
"Answer a copy (to discourage people from directly changing a version number).
VersionNumbers should never change, instead, instantiate a new instance."
^numbers copy!
----- Method: VersionNumber>>previous (in category 'accessing') -----
previous
| tmp |
numbers last = 1 ifTrue:
[^self class fromCollection: (numbers allButLast)].
tmp := numbers copy.
tmp at: numbers size put: (numbers last - 1).
^self class fromCollection: tmp
!
----- Method: VersionNumber>>printOn: (in category 'printing') -----
printOn: strm
self storeOn: strm!
----- Method: VersionNumber>>storeOn: (in category 'printing') -----
storeOn: strm
strm nextPut: $'.
self versionStringOn: strm.
strm nextPutAll: ''' asVersion'.!
----- Method: VersionNumber>>versionString (in category 'printing') -----
versionString
^String streamContents: [ :strm | self versionStringOn: strm ]!
----- Method: VersionNumber>>versionStringOn: (in category 'printing') -----
versionStringOn: strm
| first |
first := true.
numbers do: [ :ea |
first ifFalse: [strm nextPut: $.].
first := false.
ea printOn: strm]
!
Object subclass: #VersionHistory
instanceVariableNames: 'versions'
classVariableNames: ''
poolDictionaries: ''
category: 'VersionNumber'!
!VersionHistory commentStamp: '<historical>' prior: 0!
I am a version history. A version history is a collection of VersionNumbers that together form a tree of versions. I enforce rules about how versions are added and removed from the history.
To add a new version to a VersionHistory based on an existing version:
VersionHistory startingAt1 addNewVersionBasedOn: '1' asVersion; yourself
If you add 2 new versions based on the same version, a branch will be started:
VersionHistory startingAt1
addNewVersionBasedOn: '1' asVersion;
addNewVersionBasedOn: '1' asVersion;
yourself
To remove a single version (note: only versions at the tip of a branch, or at the base of the trunk (if it has only one successor) can be individually removed):
VersionHistory startingAt1
addNewVersionBasedOn: '1' asVersion;
addNewVersionBasedOn: '1' asVersion;
remove: '1.1' asVersion;
yourself
To remove an entire branch:
VersionHistory startingAt1
addNewVersionBasedOn: '1' asVersion;
addNewVersionBasedOn: '1' asVersion;
addNewVersionBasedOn: '1.1' asVersion;
addNewVersionBasedOn: '1.2' asVersion;
removeBranch: '1.1' asVersion;
yourself
To remove a portion of the trunk:
VersionHistory startingAt1
addNewVersionBasedOn: '1' asVersion;
addNewVersionBasedOn: '2' asVersion;
addNewVersionBasedOn: '3' asVersion;
addNewVersionBasedOn: '3' asVersion;
removeTrunk: '2' asVersion;
yourself
To get a string description of a version history:
VersionHistory startingAt1
addNewVersionBasedOn: '1' asVersion;
addNewVersionBasedOn: '2' asVersion;
addNewVersionBasedOn: '3' asVersion;
addNewVersionBasedOn: '3' asVersion;
treeString
Also, the following methods are useful for accessing the versions:
#firstVersion
#versionBefore:
#versionsAfter:
#mainLineStartingAt:
#allVersionsAfter:
#allVersionsBefore:
!
----- Method: VersionHistory class>>startingAt1 (in category 'as yet unclassified') -----
startingAt1
^self startingAt: '1' asVersion!
----- Method: VersionHistory class>>startingAt: (in category 'as yet unclassified') -----
startingAt: aVersion
^self new
initializeVersionsAt: aVersion;
yourself!
----- Method: VersionHistory>>addNewVersionBasedOn: (in category 'adding') -----
addNewVersionBasedOn: aVersion
| tmp |
(versions includes: aVersion) ifFalse: [^self error: 'Version is not in this history'].
tmp := aVersion next.
(versions includes: tmp) ifFalse:
[versions add: tmp.
^tmp].
tmp := aVersion.
[versions includes: (tmp := tmp branchNext)] whileTrue.
versions add: tmp.
^tmp
!
----- Method: VersionHistory>>allVersionsAfter: (in category 'accessing') -----
allVersionsAfter: aVersion
"Answer all the versions based on aVersion."
| answer |
answer := Set new.
versions do: [ :ea |
((ea inSameBranchAs: aVersion) and:
[ea > aVersion]) ifTrue: [answer add: ea]].
^answer!
----- Method: VersionHistory>>allVersionsBefore: (in category 'accessing') -----
allVersionsBefore: aVersion
"Answer all versions that came before aVersion"
| answer |
answer := Set new.
versions do: [ :ea |
((ea inSameBranchAs: aVersion) and:
[ea < aVersion]) ifTrue: [answer add: ea]].
^answer!
----- Method: VersionHistory>>canRemove: (in category 'testing') -----
canRemove: aVersion
| hasPriors followers |
(versions includes: aVersion) ifFalse: [^false].
hasPriors := (self versionBefore: aVersion) notNil.
followers := self versionsAfter: aVersion.
"Don't allow versions in the middle to be extracted"
(hasPriors and: [followers size > 0]) ifTrue: [^false].
"Don't allow versions with more than one follower to be extracted"
(hasPriors not and: [followers size > 1]) ifTrue: [^false].
^true
!
----- Method: VersionHistory>>firstVersion (in category 'accessing') -----
firstVersion
"Answer the first version in the entire version history"
^versions inject: versions anyOne into: [ :x :ea |
(x inSameBranchAs: ea)
ifTrue: [(x < ea) ifTrue: [x] ifFalse: [ea]]
ifFalse: [ea]]!
----- Method: VersionHistory>>includesVersion: (in category 'testing') -----
includesVersion: aVersion
^versions includes: aVersion!
----- Method: VersionHistory>>initializeVersionsAt: (in category 'initialization') -----
initializeVersionsAt: aVersion
versions := Set new.
versions add: aVersion.!
----- Method: VersionHistory>>mainLineStartingAt: (in category 'accessing') -----
mainLineStartingAt: aVersion
"Answer all versions based on aVersion that are not branches (they have
the same number of digits with the same values, except the last value is
greater than the last value of aVersion)."
| answer tmp |
answer := OrderedCollection new.
tmp := aVersion.
[versions includes: tmp]
whileTrue:
[answer add: tmp.
tmp := tmp next].
^answer
!
----- Method: VersionHistory>>remove: (in category 'removing') -----
remove: aVersion
"Remove aVersion from this version history."
^self remove: aVersion ifAbsent: [self error: 'version not found'].!
----- Method: VersionHistory>>remove:ifAbsent: (in category 'removing') -----
remove: aVersion ifAbsent: aBlock
"Remove aVersion from this version history."
(versions includes: aVersion) ifFalse: [^aBlock value].
(self canRemove: aVersion) ifFalse:
[^self error: 'Only versions at the beginning or end with no more than one follower may be removed'].
versions remove: aVersion.!
----- Method: VersionHistory>>removeBranch: (in category 'removing') -----
removeBranch: aVersion
"Remove aVersion and all of it's successors, providing that
aVersion is not the first version."
(self versionBefore: aVersion)
ifNil: [^self error: 'version is the first version in the history'].
versions removeAll: (self allVersionsAfter: aVersion).
versions remove: aVersion.!
----- Method: VersionHistory>>removeTrunk: (in category 'removing') -----
removeTrunk: aVersion
"Remove aVersion and all of it's predecessors, providing there
are no other branches stemming from the trunk. Note, a trunk is defined
as all versions, starting with the first version, that have only one successor."
| tmp |
(self versionsAfter: aVersion) size > 1
ifTrue: [^self error: 'version is at a fork'].
tmp := self allVersionsBefore: aVersion.
(tmp anySatisfy: [ :ea | (self versionsAfter: ea) size > 1 ])
ifTrue: [^self error: 'not a trunk, other branches detected'].
versions removeAll: tmp.
versions remove: aVersion.!
----- Method: VersionHistory>>treeString (in category 'printing') -----
treeString
"Answer a string that show the entire version history with
each branch starting on a new line"
^self treeStringStartingAt: self firstVersion!
----- Method: VersionHistory>>treeStringOn:startingAt: (in category 'printing') -----
treeStringOn: strm startingAt: aVersion
| tmp |
tmp := self mainLineStartingAt: aVersion.
tmp do: [ :ea | ea versionStringOn: strm. strm space; space ].
strm cr.
tmp do:
[ :ea |
(versions includes: ea branchNext)
ifTrue: [self treeStringOn: strm startingAt: ea branchNext]].!
----- Method: VersionHistory>>treeStringStartingAt: (in category 'printing') -----
treeStringStartingAt: aVersion
| strm |
strm := WriteStream on: ''.
self treeStringOn: strm startingAt: aVersion.
^strm contents!
----- Method: VersionHistory>>versionBefore: (in category 'accessing') -----
versionBefore: aVersion
"Answer the version immediately preceeding aVersion."
| tmp |
(aVersion > '1' asVersion) ifFalse: [^nil].
(versions includes: (tmp := aVersion previous)) ifFalse: [^nil].
^tmp!
----- Method: VersionHistory>>versionsAfter: (in category 'accessing') -----
versionsAfter: aVersion
"Answer all the versions immediately following aVersion."
| answer tmp |
answer := Set new.
tmp := aVersion next.
(versions includes: aVersion next) ifTrue: [answer add: tmp].
tmp := aVersion.
[versions includes: (tmp := tmp branchNext)] whileTrue:
[answer add: tmp].
^answer!
----- Method: String>>asVersion (in category '*versionnumber') -----
asVersion
"Answer a VersionNumber"
^VersionNumber fromString: self!