diff --git a/microwitch.image b/microwitch.image index 5c0d3e9..085e83a 100644 Binary files a/microwitch.image and b/microwitch.image differ diff --git a/src/fix errors when no serial ports.cs b/src/fix errors when no serial ports.cs new file mode 100644 index 0000000..be828d6 --- /dev/null +++ b/src/fix errors when no serial ports.cs @@ -0,0 +1 @@ +'From MIT Squeak 0.9.4 (June 1, 2003) [No updates present.] on 29 March 2021 at 4:45:32 pm'! !MicroPythonBoard methodsFor: 'serial port'! portNames Smalltalk isUnix ifTrue: [^ self unixPortNames]. Smalltalk isWindows ifTrue: [^ self winPortNames]. ScratchPlugin serialPortOpsAvailable ifFalse: [^ (1 to: 32) collect: [:i | 'COM' , i printString]]. ^ SerialPort2 portNames reject: [:n | (n asLowercase includesSubString: 'modem') or: [n asLowercase includesSubString: 'pda-sync']]! ! !MicroPythonBoard methodsFor: 'serial port'! winPortNames | str | str := MicrowitchPlugin enumerateComPorts. str ifNil: [^ #()]. ^ str lines! ! \ No newline at end of file diff --git a/src/microwitch.changes b/src/microwitch.changes index 0f8e34a..1f0a09b 100644 --- a/src/microwitch.changes +++ b/src/microwitch.changes @@ -6,4 +6,4 @@ ! ! !ScratchCommentMorph methodsFor: 'private' stamp: 'jm 3/29/2009 10:25'! commentsBeforeMe: blocksInStack "Return a collection of comment morphs in my owner that are attached to blocks before me in the given list of blocks. Assumes anchor is not nil." | allComments result | owner ifNil: [^ #()]. allComments _ owner submorphs select: [:m | m isKindOf: self class]. result _ OrderedCollection new. blocksInStack do: [:b | b == anchor ifTrue: [^ result asArray]. allComments do: [:c | c anchor = b ifTrue: [result add: c]]]. ^ result asArray ! ! !ScratchCommentMorph methodsFor: 'private' stamp: 'ee 5/7/2008 16:20'! defaultWidth ^ 112 ! ! !ScratchCommentMorph methodsFor: 'private' stamp: 'jens 3/30/2009 21:56'! detach | oldAnchor | oldAnchor _ anchor. anchor _ nil. connector ifNotNil: [ connector delete. connector _ nil]. oldAnchor ifNotNil: [ oldAnchor topBlock commentLayoutChanged]. ! ! !ScratchCommentMorph methodsFor: 'private' stamp: 'jm 7/8/2008 19:11'! labelWhenClosed "Answer the label string to used when the comment is closed." | maxW firstLine ellipses s w | maxW _ self width - 22. firstLine _ commentMorph lines at: 1. (labelMorph stringWidth: firstLine) < maxW ifTrue: [^ firstLine]. ellipses _ ScratchTranslator ellipsesSuffix asUTF32. 1 to: firstLine size do: [:i | s _ firstLine copyFrom: 1 to: i. w _ labelMorph stringWidth: s, ellipses. w > maxW ifTrue: [ ^ (firstLine copyFrom: 1 to: i - 1), ellipses]]. ^ firstLine ! ! !ScratchCommentMorph methodsFor: 'private' stamp: 'jens 3/30/2009 22:21'! layoutChanged super layoutChanged. anchor ifNotNil: [anchor topBlock commentLayoutChanged]. ! ! !ScratchCommentMorph methodsFor: 'private' stamp: 'jm 3/29/2009 11:14'! refreshConnector | y | owner ifNil: [^ self]. connector ifNil: [ connector _ ScratchConnectorMorph new color: self topBarColor]. connector owner isNil ifTrue: [ owner addMorph: connector. connector goBehind]. y _ self top + 7. ScratchTranslator isRTL ifTrue: [ connector from: (anchor left + 1) @ y to: self right @ y] ifFalse: [ connector from: (anchor right - 1) @ y to: self left @ y]. ! ! !ScratchCommentMorph methodsFor: 'private' stamp: 'jens 3/30/2009 20:38'! resetAnchorOffset anchorOffset _ 5. self updatePosition. ! ! !ScratchCommentMorph methodsFor: 'private' stamp: 'jm 3/29/2009 13:28'! toggleShowing | colors | isShowing _ isShowing not. colors _ showHideMorph form colors. isShowing ifTrue: [ showHideMorph form: (ScratchFrameMorph skinAt: #arrowOpenComment). showHideMorph form colors: colors. (self submorphs at: 2) addMorphBack: commentMorph. (self submorphs at: 2) addMorphBack: resizeMorph. commentMorph startStepping. labelMorph contents: ''. self comeToFront. self color: self commentMorphColor] ifFalse: [ showHideMorph form: (ScratchFrameMorph skinAt: #arrowClosedComment). showHideMorph form colors: colors. commentMorph delete. resizeMorph delete. labelMorph contents: self labelWhenClosed. self color: self topBarColor]. anchor ifNotNil: [anchor topBlock commentLayoutChanged]. ! ! !ScratchCommentMorph methodsFor: 'private' stamp: 'ee 5/14/2008 09:56'! topBarColor ^ Color r: 1 g: 1 b: (165/255) ! ! !ScratchCommentMorph methodsFor: 'private' stamp: 'jens 3/4/2009 00:39'! tupleSequence "Answer an array with a single tuple describing this comment." anchor isNil ifTrue: [ ^ Array with: (Array with: #scratchComment with: commentMorph contents with: isShowing with: self width)] ifFalse: [ ^ Array with: (Array with: #scratchComment with: commentMorph contents with: isShowing with: self width with: anchor blockID)] ! ! !ScratchCommentMorph methodsFor: 'private' stamp: 'jens 3/30/2009 21:51'! updateAnchorOffset "Update the horizontal offset from my anchor block." | extraSpace blocksInStack rects allObstacles touching before after collapsed | anchor ifNil: [^ self]. self resetAnchorOffset. isShowing ifFalse: [^ self]. extraSpace _ 5. blocksInStack _ anchor topBlock allMorphs reversed select: [:m | (m isKindOf: BlockMorph) and: [m isReporter not]]. before _ self commentsBeforeMe: blocksInStack. after _ self commentsAfterMe: blocksInStack. collapsed _ after select: [:c | c isShowing not]. before, collapsed do: [: c | c resetAnchorOffset]. before do: [:c | c updateAnchorOffset]. allObstacles _ blocksInStack, before, collapsed. rects _ allObstacles collect: [:m | (m isKindOf: BlockMorph) ifTrue: [m bounds origin extent: (m bounds extent - (0@4))] ifFalse: [m bounds]]. [touching _ rects detect: [:r | (r intersects: self bounds)] ifNone: [nil]. touching isNil] whileFalse: [ ScratchTranslator isRTL ifTrue: [self right: (self right min: (touching left - extraSpace))] ifFalse: [self left: (self left max: (touching right + extraSpace))]]. ScratchTranslator isRTL ifTrue: [anchorOffset _ anchor left - self right] ifFalse: [anchorOffset _ self left - anchor right]. self updatePosition. ! ! !ScratchCommentMorph methodsFor: 'private' stamp: 'jm 3/30/2009 21:47'! updatePosition | vOffset | anchor ifNil: [^ self]. vOffset _ (anchor isKindOf: HatBlockMorph) ifTrue: [19] ifFalse: [3]. ScratchTranslator isRTL ifTrue: [self position: (anchor left - self width - anchorOffset) @ (anchor top + vOffset)] ifFalse: [self position: (anchor right + anchorOffset) @ (anchor top + vOffset)]. self refreshConnector. oldAnchorPos _ anchor position. ! ! !ScratchConnectorMorph methodsFor: 'as yet unclassified' stamp: 'jens 1/27/2009 19:53'! drawOn: aCanvas (point1 isNil | point2 isNil) ifTrue: [^ self]. aCanvas line: point1 to: point2 width: 1 color: color ! ! !ScratchConnectorMorph methodsFor: 'as yet unclassified' stamp: 'jens 3/23/2009 20:41'! from: p1 to: p2 | left right top bottom | ((point1 = p1) & (point2 = p2)) ifTrue: [^ self]. point1 _ p1. point2 _ p2. left _ point1 x min: point2 x. right _ point1 x max: point2 x. top _ point1 y min: point2 y. bottom _ point1 y max: point2 y. self position: left @ top. self extent: ((right - left) @ ((bottom - top) max: 1)). ! ! !ScratchConnectorMorph methodsFor: 'as yet unclassified' stamp: 'jens 1/27/2009 19:53'! rootForGrabOf: aMorph "prevent the receiver from being picked up" ^ nil ! ! I represent a Scratch event. I have a name and an optional argument ! !ScratchEvent methodsFor: 'accessing' stamp: 'jm 8/10/2004 12:50'! argument ^ argument ! ! !ScratchEvent methodsFor: 'accessing' stamp: 'jm 8/10/2004 12:50'! name ^ name ! ! !ScratchEvent methodsFor: 'accessing' stamp: 'jm 3/23/2005 09:00'! name: aString argument: anObjectOrNil "Set my event name and argument." name _ aString. argument _ anObjectOrNil. ! ! I am a File Chooser dialog box for Scratch. My submorphs vary according to my function. For example, I may show a project thumbnail, I may have a typein field to allow the user to enter a new file name, or I may have buttons to create or paint a new sprite. You can also use me to select an existing or create a new file or folder. Some examples: ScratchFileChooserDialog chooseFile ScratchFileChooserDialog chooseFile: FileDirectory default enableNew: false ! !ScratchFileChooserDialog methodsFor: 'initialization' stamp: 'jm 5/8/2009 10:03'! addShortcutButtons "Add shortcut buttons for my type to the shortcutColumn." | spacer | spacer _ Morph new extent: 5@5; color: Color transparent. shortcutColumn removeAllMorphs. shortcutColumn addMorphBack: (self shortcutButtonLabel: 'Computer' action: #myComp icon: #folderDiscsIcon). shortcutColumn addMorphBack: spacer fullCopy. shortcutColumn addMorphBack: (self shortcutButtonLabel: self labelForHomeFolder action: #myHome icon: #folderHouseIcon). shortcutColumn addMorphBack: spacer fullCopy. shortcutColumn addMorphBack: (self shortcutButtonLabel: 'Desktop' action: #myDesktop icon: #folderIcon). shortcutColumn addMorphBack: spacer fullCopy. #background = self type ifTrue: [ shortcutColumn addMorphBack: (self shortcutButtonLabel: 'Backgrounds' action: #scratchBackgrounds icon: #folderCatIcon)]. #costume = self type ifTrue: [ shortcutColumn addMorphBack: (self shortcutButtonLabel: 'Costumes' action: #scratchCostumes icon: #folderCatIcon)]. #project = self type ifTrue: [ shortcutColumn addMorphBack: (self shortcutButtonLabel: 'Examples' action: #sampleProjects icon: #folderCatIcon). shortcutColumn addMorphBack: spacer fullCopy. shortcutColumn addMorphBack: (self shortcutButtonLabel: 'My Projects' action: #userProjects icon: #folderIcon)]. #sound = self type ifTrue: [ shortcutColumn addMorphBack: (self shortcutButtonLabel: 'Sounds' action: #scratchSounds icon: #folderCatIcon)]. ! ! !ScratchFileChooserDialog methodsFor: 'initialization' stamp: 'ee 3/24/2009 18:53'! createScratchFileChooserFor: aScratchFrameMorph saving: savingFlag "Create a Scratch file chooser dialog box with a project thumbnail and info box." | labelFont contentsFont commentFont thumbnailHolder | scratchFrame _ aScratchFrameMorph. readingScratchFile _ savingFlag not. list _ ScratchFilePicker new extensions: #(scratch sb). self removeAllMorphs. bottomSpacer delete. bottomSpacer _ nil. mainColumn addMorphBack: list. savingFlag ifFalse: [ self title: 'Open Project'. list scratchInfoClient: self]. labelFont _ (ScratchFrameMorph getFont: #FileChooserLabel). contentsFont _ (ScratchFrameMorph getFont: #FileChooserContents). commentFont _ (ScratchFrameMorph getFont: #FileChooserComment). savingFlag ifTrue: [ self title: 'Save Project'. newFileTitle _ StringMorph contents: ('New Filename:' localized, ' ') font: labelFont. newFileTitle color: (Color gray: 0.3). newFileName _ StringFieldMorph new contents: scratchFrame projectName; client: self; font: contentsFont; color: (Color r: (211/255) g: (214/255) b: (216/255)); width: 180. tabFields add: newFileName. newTitleBin addMorphBack: newFileTitle; addMorphBack: (Morph new extent: (5@5); color: Color transparent); addMorphBack: newFileName; addMorphBack: (AlignmentMorph newSpacer: Color transparent). ScratchTranslator isRTL ifTrue: [newTitleBin submorphs reversed do: [:m | m delete. newTitleBin addMorphBack: m]]]. mainColumn addMorphBack: (Morph new extent: (5@9); color: Color transparent); addMorphBack: newTitleBin. thumbnailHolder _ AlignmentMorph newColumn centering: #center; color: Color transparent. thumbnailFrameMorph _ ImageFrameMorph new initFromForm: (ScratchFrameMorph skinAt: #dialogThumbnailFrame). thumbnailFrameMorph extent: (170@130). thumbnailHolder addMorph: thumbnailFrameMorph. fileInfoColumn addMorphBack: thumbnailHolder; addMorphBack: (Morph new extent: (5@6); color: Color transparent). "spacer" thumbnailMorph _ ImageMorph new form: (Form extent: 160@120 depth: 1). thumbnailFrameMorph addMorphFront: (thumbnailMorph position: ((thumbnailFrameMorph position) + (5@5))). authorLabelMorph _ StringMorph contents: 'Project author:' localized font: labelFont. authorLabelMorph color: (Color gray: 0.3). fileInfoColumn addMorphBack: authorLabelMorph. savingFlag ifTrue: [authorMorph _ StringFieldMorph new useStringFieldFrame; contents: ''; font: contentsFont. tabFields add: authorMorph] ifFalse: [fileInfoColumn addMorphBack: (Morph new extent: (5@6); color: Color transparent). "spacer" authorMorph _ StringFieldMorph new color: Color transparent; borderWidth: 0; contents: ''; isEditable: false; font: contentsFont]. fileInfoColumn addMorphBack: authorMorph; addMorphBack: (Morph new extent: (5@6); color: Color transparent). "spacer" commentLabelMorph _ StringMorph contents: 'About this project:' localized font: labelFont. commentLabelMorph color: authorLabelMorph color. fileInfoColumn addMorphBack: commentLabelMorph. commentMorph _ ScrollingStringMorph new borderWidth: 0; contents: ''; font: commentFont; extent: (210@110). savingFlag ifTrue: [commentMorph backForm: (ScratchFrameMorph skinAt: #stringFieldFrame). tabFields add: commentMorph] ifFalse: [commentMorph isEditable: false]. fileInfoColumn addMorphBack: commentMorph. fileInfoColumn addMorphBack: buttonRow. self addMorphBack: shortcutColumn; addMorphBack: mainColumn; addMorphBack: fileInfoColumn. savingFlag ifTrue: [ self scratchInfo: scratchFrame projectInfo. thumbnailMorph form: scratchFrame workPane thumbnailForm. "default author field to login name if known; else author" (aScratchFrameMorph loginName size > 0) ifTrue: [authorMorph contents: aScratchFrameMorph loginName] ifFalse: [authorMorph contents: aScratchFrameMorph author]]. ! ! !ScratchFileChooserDialog methodsFor: 'initialization' stamp: 'ee 4/30/2009 11:52'! getUserResponse "Wait for the user to respond, then answer the full path name of the chosen file or #cancelled if the user cancels the operation. If opening a remote file for reading, answer a HTTPFetcher on the remote file." "Details: This is invoked synchronously from the caller. In order to keep processing inputs and updating the screen while waiting for the user to respond, this method has its own version of the World's event loop." | w | self openInWorld. w _ self world. w activeHand newKeyboardFocus: (tabFields at: 1). self centerOnScreen. newFileName ifNotNil: [w activeHand newKeyboardFocus: newFileName]. list getDirectoryContents. response _ #cancelled. "default response" done _ false. [done or: [list isFinalSelection]] whileFalse: [w doOneCycle]. self delete. w doOneCycle. "erase myself from the screen" ((response = #cancelled) and: [list isFinalSelection not]) ifTrue: [^ #cancelled]. list selectedFile ifNil: [^ #cancelled]. (thumbnailMorph notNil & readingScratchFile not) ifTrue: [ "save info in project" scratchFrame author: authorMorph contents withBlanksTrimmed. scratchFrame projectComment: commentMorph contents]. (list currentDirectory isKindOf: ScratchServerDirectory) ifTrue: [^ list projectFetcher] ifFalse: [^ list currentDirectory fullNameFor: list selectedFile]. ! ! !ScratchFileChooserDialog methodsFor: 'initialization' stamp: 'ee 6/29/2008 13:28'! initialize "Create the file chooser dialog box" super initialize. choosingFolder _ false. scratchFrame _ nil. readingScratchFile _ false. newTitleBin _ AlignmentMorph newRow centering: #center; color: Color transparent. buttonRow hResizing: #spaceFill. self withButtonsForYes: false no: false okay: true cancel: true. ! ! !ScratchFileChooserDialog methodsFor: 'accessing' stamp: 'jm 5/12/2006 13:41'! choosingFolder: aBoolean choosingFolder _ aBoolean. ! ! !ScratchFileChooserDialog methodsFor: 'accessing' stamp: 'ee 7/3/2008 14:33'! createFileChooserLayout: allowNewFile "Create the file chooser dialog box." list _ ScratchFilePicker new. self removeAllMorphs. bottomSpacer delete. bottomSpacer _ nil. mainColumn addMorphBack: list. self title: 'Open'. allowNewFile ifTrue: [ self title: 'Save As'. newFileTitle _ StringMorph new contents: 'New Filename:' localized, ' '; color: (Color gray: 0.3); font: (ScratchFrameMorph getFont: #FileChooserNewFileTitle). newFileName _ StringFieldMorph new font: (ScratchFrameMorph getFont: #FileChooserNewFilename); color: (Color r: (211/255) g: (214/255) b: (216/255)); width: 180. newTitleBin addMorphBack: newFileTitle; addMorphBack: (Morph new extent: (5@5); color: Color transparent); addMorphBack: newFileName; addMorphBack: (AlignmentMorph newSpacer: Color transparent). ScratchTranslator isRTL ifTrue: [newTitleBin submorphs reversed do: [:m | m delete. newTitleBin addMorphBack: m]]]. mainColumn addMorphBack: newTitleBin; addMorphBack: buttonRow. self addMorphBack: shortcutColumn; addMorphBack: mainColumn; addMorphBack: fileInfoColumn. ! ! !ScratchFileChooserDialog methodsFor: 'accessing' stamp: 'jm 5/12/2006 11:38'! defaultName: aString "Set the default file name." newFileName ifNotNil: [newFileName contents: aString]. ! ! !ScratchFileChooserDialog methodsFor: 'accessing' stamp: 'jm 5/14/2005 14:16'! extensions: anArray list extensions: anArray. ! ! !ScratchFileChooserDialog methodsFor: 'accessing' stamp: 'ee 6/29/2008 12:45'! listExtent: anExtent list extent: anExtent. self changed. ! ! !ScratchFileChooserDialog methodsFor: 'accessing' stamp: 'jm 5/20/2009 14:04'! redirectSavesToSampleFolder "Check to see if we are about to save into the Sample projects directory. If so, change the default location to the user's project folder." (ScratchFileChooserDialog lastFolderIsSampleProjectsFolder) ifTrue: [ self setDirectory: ScratchFileChooserDialog userScratchProjectsDir]. ! ! !ScratchFileChooserDialog methodsFor: 'accessing' stamp: 'jm 5/20/2005 15:31'! scratchFrame: aScratchFrameMorph scratchFrame _ aScratchFrameMorph. ! ! !ScratchFileChooserDialog methodsFor: 'accessing' stamp: 'jm 5/15/2005 00:02'! setDirectory: anObject "Set the currently selected directory. The argument may be either a FileDirectory or a string." (anObject isKindOf: FileDirectory) ifTrue: [list currentDirectory: anObject] ifFalse: [list currentDirectory: (list currentDirectory directoryNamed: anObject)]. ! ! !ScratchFileChooserDialog methodsFor: 'accessing' stamp: 'jm 5/20/2005 16:31'! showThumbnails: aBoolean list showThumbnails: aBoolean. ! ! !ScratchFileChooserDialog methodsFor: 'accessing' stamp: 'tis 12/10/2006 17:43'! type "Return the type of thing being opened/saved in the file dialog box, in order to include the appropriate shortcuts. Possible types include #costume #background #sound #sprite #stageShot #project" ^ type ! ! !ScratchFileChooserDialog methodsFor: 'accessing' stamp: 'jm 5/21/2009 10:29'! type: t "Set the type of thing being opened/saved in the file dialog box, in order to include the appropriate shortcuts. Then add relevant shortcut buttons and set the directory. Types include: #background #costume #list #project #projectSummary #scriptSnapshot #sound #sprite #stageShot" type _ t. self addShortcutButtons. self setDirectory: (ScratchFileChooserDialog getLastFolderForType: type). ! ! !ScratchFileChooserDialog methodsFor: 'interaction' stamp: 'ee 4/30/2009 11:52'! getUserResponseForFolder "Wait for the user to respond, then answer the full path name of the chosen directory or #cancelled if the user cancels the operation. To make a FileDirectory from the response string use the method: FileDirectory on: ." "Details: This is invoked synchronously from the caller. In order to keep processing inputs and updating the screen while waiting for the user to respond, this method has its own version of the World's event loop." | w | self openInWorld. w _ self world. w activeHand newKeyboardFocus: (tabFields at: 1). self extent: self extent. "force layout" self position: w center - (self extent // 2) + (0@5). "center on screen but disregard the shadow on the bottom" list getDirectoryContents. response _ #cancelled. "default response" done _ false. [done "or: [list model isFinalSelection]"] whileFalse: [w doOneCycle]. self delete. w doOneCycle. "erase myself from the screen" response = #cancelled ifTrue: [^ #cancelled] ifFalse: [^ list currentDirectory]. ! ! !ScratchFileChooserDialog methodsFor: 'interaction' stamp: 'ee 4/30/2009 11:52'! getUserResponseForNewFile "Wait for the user to respond, then answer the full path name of the new file or #cancelled if the user cancels the operation." "Details: This is invoked synchronously from the caller. In order to keep processing inputs and updating the screen while waiting for the user to respond, this method has its own version of the World's event loop." | w fn | self openInWorld. w _ self world. w activeHand newKeyboardFocus: (tabFields at: 1). self extent: self extent. "force layout" self position: w center - (self extent // 2) + (0@5). "center on screen but disregard the shadow on the bottom" newFileName ifNotNil: [w activeHand newKeyboardFocus: newFileName]. list getDirectoryContents. [true] whileTrue: [ done _ false. [done] whileFalse: [w doOneCycle]. response = #cancelled ifTrue: [^ #cancelled]. thumbnailMorph ifNotNil: [ "save info in project" scratchFrame author: authorMorph contents withBlanksTrimmed. scratchFrame projectComment: commentMorph contents]. fn _ newFileName contents withBlanksTrimmed. fn size > 0 ifTrue: [ fn _ fn collect: [:ch | ('\/:' includes: ch) ifTrue: [$-] ifFalse: [ch]]. "replace directory delimiters with dashes" ^ list currentDirectory pathName, FileDirectory pathNameDelimiter asString, fn]. newFileTitle color: Color red. self openInWorld. w activeHand newKeyboardFocus: newFileName]. ! ! !ScratchFileChooserDialog methodsFor: 'interaction' stamp: 'tis 12/16/2006 14:33'! yes "Yes button was pressed." | entry | entry _ list selectedEntryOrNil. (entry notNil and: [entry entryIsDirectory]) ifTrue: [ (newFileName isNil or: [newFileName contents size = 0]) ifTrue: [ self setDirectory: entry entryName. choosingFolder ifTrue: [super yes]. ^ self]]. ScratchFileChooserDialog setLastFolderTo: list currentDirectory forType: self type. ^ super yes ! ! !ScratchFileChooserDialog methodsFor: 'shortcuts' stamp: 'jm 3/26/2009 09:26'! labelForHomeFolder "Answer the name to use for the home folder. This is the user name unless the home folder location has been overridden by an entry in the Scratch.ini file." | home delimiter | UserHomeFolder notNil ifTrue: [^ 'Home' localized]. home _ ScratchPlugin primGetFolderPathOrNil: 1. delimiter _ FileDirectory pathNameDelimiter asString. ^ UTF8 withAll: (home findTokens: delimiter) last ! ! !ScratchFileChooserDialog methodsFor: 'shortcuts' stamp: 'jm 9/27/2007 15:33'! myComp "My Computer button was pressed." list currentDirectory: (Smalltalk isMacOSX ifTrue: [FileDirectory on: '/Volumes'] ifFalse: [FileDirectory on: '']) ! ! !ScratchFileChooserDialog methodsFor: 'shortcuts' stamp: 'jm 5/20/2009 14:02'! myDesktop "My desktop button was pressed." list currentDirectory: (FileDirectory on: (ScratchPlugin primGetFolderPath: 2)) ! ! !ScratchFileChooserDialog methodsFor: 'shortcuts' stamp: 'jm 2/22/2009 22:23'! myHome "My Home button was pressed." list currentDirectory: self class homeDir. ! ! !ScratchFileChooserDialog methodsFor: 'shortcuts' stamp: 'jm 2/22/2009 23:35'! sampleProjects "Sample Projects button was pressed." (FileDirectory default directoryExists: 'Projects') ifTrue: [ list currentDirectory: (FileDirectory default directoryNamed: 'Projects')]. ! ! !ScratchFileChooserDialog methodsFor: 'shortcuts' stamp: 'tis 12/16/2006 14:58'! scratchBackgrounds "Scratch Backgrounds button was pressed." | backgrounds | backgrounds _ ScratchFileChooserDialog getDefaultFolderForType: #background. backgrounds ifNotNil: [list currentDirectory: backgrounds]. ! ! !ScratchFileChooserDialog methodsFor: 'shortcuts' stamp: 'tis 12/16/2006 14:58'! scratchCostumes "Scratch Costumes button was pressed." | costumes | costumes _ ScratchFileChooserDialog getDefaultFolderForType: #costume. costumes ifNotNil: [list currentDirectory: costumes]. ! ! !ScratchFileChooserDialog methodsFor: 'shortcuts' stamp: 'tis 12/16/2006 14:58'! scratchSounds "Scratch Sounds button was pressed." | sounds | sounds _ ScratchFileChooserDialog getDefaultFolderForType: #sound. sounds ifNotNil: [list currentDirectory: sounds]. ! ! !ScratchFileChooserDialog methodsFor: 'shortcuts' stamp: 'jm 2/24/2009 17:52'! userProjects "My Projects button was pressed." list currentDirectory: self class userScratchProjectsDir. ! ! !ScratchFileChooserDialog methodsFor: 'other' stamp: 'jm 7/26/2006 11:22'! delete super delete. (list isKindOf: ScratchFilePicker) ifTrue: [list stopPlayingSound]. ! ! !ScratchFileChooserDialog methodsFor: 'other' stamp: 'jm 2/28/2009 14:52'! scratchInfo: infoDict "Update the Scratch project thumbnail and info display." | s | infoDict ifNil: [ "clear thumbnail and info" thumbnailMorph form: (Form extent: thumbnailMorph extent depth: 1) fillWhite. authorMorph contents: ''. commentMorph contents: ''. ^ self]. ((infoDict includesKey: 'thumbnail') and: [thumbnailMorph notNil]) ifTrue: [thumbnailMorph form: (infoDict at: 'thumbnail')] ifFalse: [thumbnailMorph form: (Form extent: thumbnailMorph extent depth: 1) fillBlack]. authorMorph contents: ''. ((infoDict includesKey: 'author') and: [authorMorph notNil]) ifTrue: [ authorMorph contents: (infoDict at: 'author')]. s _ ''. readingScratchFile ifTrue: [ (infoDict includesKey: 'comment') ifTrue: [s _ infoDict at: 'comment']] ifFalse: [ s _ scratchFrame projectCommentOrTemplate]. commentMorph contents: s; changed. ! ! !ScratchFileChooserDialog class methodsFor: 'class initialization' stamp: 'jm 5/8/2009 10:05'! clearFolderCache "Clear all folder caches." "ScratchFileChooserDialog clearFolderCache" UserHomeFolder _ nil. LastFolderForType _ Dictionary new. ! ! !ScratchFileChooserDialog class methodsFor: 'class initialization' stamp: 'jm 5/8/2009 09:52'! initialize self clearFolderCache. ! ! !ScratchFileChooserDialog class methodsFor: 'instance creation' stamp: 'ee 6/29/2008 12:49'! chooseExistingFileType: type extensions: anArrayOrNil title: titleString | m | ScratchFileChooserDialog deleteDuplicates. m _ self new createFileChooserLayout: false; type: type; extensions: anArrayOrNil; title: titleString. ^ m getUserResponse ! ! !ScratchFileChooserDialog class methodsFor: 'instance creation' stamp: 'ee 6/29/2008 12:49'! chooseFolder: aDirectory | m | ScratchFileChooserDialog deleteDuplicates. m _ self new createFileChooserLayout: false; choosingFolder: true; setDirectory: aDirectory; "initial directory" extensions: '!!'; "do not show files in the directories" title: 'Choose a folder'. ^ m getUserResponseForFolder ! ! !ScratchFileChooserDialog class methodsFor: 'instance creation' stamp: 'jm 5/11/2009 20:11'! chooseImageFileType: type title: aString "ScratchFileChooserDialog chooseImageFileType: #costume title: 'Costume'" | m | ScratchFileChooserDialog deleteDuplicates. m _ self new createFileChooserLayout: false; showThumbnails: true; type: type; extensions: #(gif jpeg jpg bmp png); title: aString; listExtent: 550@300. "allow sprites to be imported into the paint editor:" aString = 'Import Image' ifTrue: [ m extensions: #(gif jpeg jpg bmp png sprite)]. ^ m getUserResponse ! ! !ScratchFileChooserDialog class methodsFor: 'instance creation' stamp: 'ee 1/30/2009 11:56'! chooseNewFileDefault: defaultName title: titleString type: type | m | ScratchFileChooserDialog deleteDuplicates. m _ self new createFileChooserLayout: true; type: type; defaultName: defaultName; title: titleString; listExtent: 400@280. ^ m getUserResponseForNewFile ! ! !ScratchFileChooserDialog class methodsFor: 'instance creation' stamp: 'ee 1/30/2009 11:45'! chooseSpriteCostumeFor: aScratchFrameMorph "ScratchFileChooserDialog chooseSpriteCostumeFor: nil" | m | ScratchFileChooserDialog deleteDuplicates. m _ self new createFileChooserLayout: false; title: 'New Sprite'; showThumbnails: true; type: #costume; extensions: #(gif jpeg jpg bmp png sprite); scratchFrame: aScratchFrameMorph; listExtent: 550@300. ^ m getUserResponse ! ! !ScratchFileChooserDialog class methodsFor: 'instance creation' stamp: 'tis 12/18/2006 14:50'! deleteDuplicates "If another file chooser dialog box is already opened, delete it" World submorphs do: [:i | (i isKindOf: ScratchFileChooserDialog) ifTrue: [i delete]]. ! ! !ScratchFileChooserDialog class methodsFor: 'instance creation' stamp: 'ee 6/29/2008 11:55'! openScratchFileFor: aScratchFrameMorph "Choose a Scratch project file for reading. Answer: nil, if no file is selected the full file name, if a local file is selected a ByteArray containing the project data, if a remote file is selected. As the user selects files in the file picker, preview the project thumbnail and info string for the selected project. If the file 'servers.txt' is found in the Scratch folder, then allow browsing and selecting of Scratch projects on a remote HTTP server." "ScratchFileChooserDialog openScratchFileFor: nil" | m response s | ScratchFileChooserDialog deleteDuplicates. m _ self new createScratchFileChooserFor: aScratchFrameMorph saving: false; type: #project. response _ m getUserResponse. response = #cancelled ifTrue: [^ nil]. (response isKindOf: String) ifTrue: [^ response]. "answer the full name of a local file" "get contents of a remote file, giving the user the option of aborting" self assert: [response isKindOf: HTTPFetcher]. self waitForCompletionOrCancelOfFetch: response. response succeeded ifFalse: [response stopDownload. ^ nil]. s _ (response path findTokens: '/') last. aScratchFrameMorph projectName: (aScratchFrameMorph nameFromFileName: s). ^ response bodyData ! ! !ScratchFileChooserDialog class methodsFor: 'instance creation' stamp: 'jm 5/11/2009 10:50'! saveScratchFileFor: aScratchFrameMorph "Choose a file for saving the current Scratch project file. Display the thumbnail and info string for the current project and allow the info string to be edited. Answer the full name of the file in which to save the project or #cancelled if the operation is cancelled." "ScratchFileChooserDialog saveScratchFileFor: nil" | m result | ScratchFileChooserDialog deleteDuplicates. m _ self new createScratchFileChooserFor: aScratchFrameMorph saving: true; type: #project; redirectSavesToSampleFolder. result _ m getUserResponseForNewFile. result = #cancelled ifTrue: [^ result]. (result asLowercase endsWith: '.sb') ifFalse: [result _ result, '.sb']. ^ result ! ! !ScratchFileChooserDialog class methodsFor: 'accessing' stamp: 'jm 5/8/2009 09:49'! getDefaultFolderForType: type | mediaDir | (type = #project) ifTrue: [^ self userScratchProjectsDir]. (FileDirectory default directoryExists: 'Media') ifTrue: [ mediaDir _ FileDirectory default directoryNamed: 'Media'. #background = type ifTrue: [ (mediaDir directoryExists: 'Backgrounds') ifTrue: [ ^ mediaDir directoryNamed: 'Backgrounds']]. (#(costume sprite) includes: type) ifTrue: [ (mediaDir directoryExists: 'Costumes') ifTrue: [ ^ mediaDir directoryNamed: 'Costumes']]. #sound = type ifTrue: [ (mediaDir directoryExists: 'Sounds') ifTrue: [ ^ mediaDir directoryNamed: 'Sounds']]]. ^ self homeDir ! ! !ScratchFileChooserDialog class methodsFor: 'accessing' stamp: 'jm 5/8/2009 09:36'! getLastFolderForType: type "Return the last used folder for the given type. If this is the first time the type has been used, return the default folder for that type." | dir | dir _ LastFolderForType at: type ifAbsent: [nil]. dir ifNotNil: [ (dir isKindOf: FileDirectory) ifTrue: [(dir parentDirectory directoryExists: dir pathName) ifTrue: [^ dir]]]. ^ ScratchFileChooserDialog getDefaultFolderForType: type ! ! !ScratchFileChooserDialog class methodsFor: 'accessing' stamp: 'jm 5/20/2009 14:28'! homeDir "Return the home directory for this user. By default, this is either provided by the OS via primGetFolderPath: but it can be overridden by adding a 'homedir=path' entry to the Scratch.ini folder." | homeDir | UserHomeFolder ifNotNil: [^ UserHomeFolder]. "provided by Scratch.ini" "try in order: documents folder, user home folder, Scratch folder" homeDir _ FileDirectory on: (ScratchPlugin primGetFolderPath: 3). "documents" (FileDirectory default directoryExists: homeDir pathName) ifFalse: [ homeDir _ FileDirectory on: (ScratchPlugin primGetFolderPath: 1)]. "home" (FileDirectory default directoryExists: homeDir pathName) ifFalse: [ homeDir _ FileDirectory default]. "Scratch folder (last resort)" ^ homeDir ! ! !ScratchFileChooserDialog class methodsFor: 'accessing' stamp: 'jm 5/20/2009 16:52'! lastFolderIsSampleProjectsFolder "Return true if the last projects folder is the sample projects folder." | lastDirPath sampleProjectDirPath | lastDirPath _ (self getLastFolderForType: #project) pathName. sampleProjectDirPath _ (FileDirectory default directoryNamed: 'Projects') pathName. ^ lastDirPath beginsWith: sampleProjectDirPath ! ! !ScratchFileChooserDialog class methodsFor: 'accessing' stamp: 'jm 5/8/2009 09:55'! setHomeDir: aStringOrNil "Set the home directory for this user. If the user name contains an asterisk, replace it with the name of logged in user." "self setHomeDir: '/Users/*/Documents'" | path root | UserHomeFolder _ nil. aStringOrNil ifNotNil: [ path _ self replaceAsteriskWithUserName: aStringOrNil. root _ FileDirectory on: ''. (root directoryExists: path) ifTrue: [ UserHomeFolder _ root on: path]]. ! ! !ScratchFileChooserDialog class methodsFor: 'accessing' stamp: 'jm 5/8/2009 09:37'! setLastFolderTo: dir forType: type "Remember the given dir as the last folder used for the given type of file." (dir isKindOf: ScratchServerDirectory) ifTrue: [^ self]. LastFolderForType at: type put: dir. ! ! !ScratchFileChooserDialog class methodsFor: 'accessing' stamp: 'jm 5/20/2009 18:41'! userScratchProjectsDir "Return the path to the user's 'Scratch' project folder, usually located inside the user's 'Documents' folder. If the folder does not already exists, attempt to create it. If the .ini file specifies an alternate home directory, create the folder there. If the directory can't be created, return the user's home folder." | scratchFolderName homeDir | scratchFolderName _ 'Scratch Projects'. "if this were localized a user could get multiple project folders for different languages..." homeDir _ self homeDir. "try to create Scratch Projects folder in the user's homeDir" (homeDir directoryExists: scratchFolderName) ifFalse: [ [homeDir createDirectory: scratchFolderName] ifError: []]. ^ (homeDir directoryExists: scratchFolderName) ifTrue: [homeDir directoryNamed: scratchFolderName] ifFalse: [homeDir] ! ! !ScratchFileChooserDialog class methodsFor: 'utilities' stamp: 'ee 8/8/2008 11:33'! confirmFileOverwriteIfExisting: aFilename "If the given file exists, ask the user if they want to overwrite it or pick a different file name." | response fName | fName _ aFilename. (fName endsWith: '.sb') ifFalse: [fName _ fName, '.sb']. (FileDirectory default fileExists: fName) ifFalse: [^ aFilename]. response _ DialogBoxMorph askWithCancel: 'The file name already exists. Overwrite existing file?'. response = #cancelled ifTrue: [^ #cancelled]. response ifTrue: [^ fName] ifFalse: [^ false]. ! ! !ScratchFileChooserDialog class methodsFor: 'utilities' stamp: 'jm 2/22/2009 23:35'! replaceAsteriskWithUserName: aString "If the given string includes an asterisk, replace it with the name of the current user. Otherwise, return the string." "self replaceAsteriskWithUserName: '/Users/*/Documents'" | i home delimiter userName | (i _ aString indexOf: $*) = 0 ifTrue: [^ aString]. home _ ScratchPlugin primGetFolderPathOrNil: 1. home ifNil: [^ aString]. delimiter _ FileDirectory pathNameDelimiter asString. userName _ (home findTokens: delimiter) last. ^ (aString copyFrom: 1 to: i - 1), userName, (aString copyFrom: i + 1 to: aString size). ! ! !ScratchFileChooserDialog class methodsFor: 'utilities' stamp: 'jm 1/15/2006 09:24'! waitForCompletionOrCancelOfFetch: anHTTPFetcher "Put up dialog box until the given fetch completes, fails, or is cancelled by the user." "self waitForCompletionOrCancelOfFetch: nil" | dialogBox total | dialogBox _ DialogBoxMorph new title: 'Downloading...'; withButtonsForYes: false no: false okay: false cancel: true; extent: 200@150; percentDone: 0. dialogBox center: World center. dialogBox openInWorld. total _ nil. [dialogBox isDone not & anHTTPFetcher inProgress] whileTrue: [ Delay waitMSecs: 100. total ifNil: [total _ anHTTPFetcher contentLength]. total ifNotNil: [ dialogBox percentDone: (100 * anHTTPFetcher bytesDownloaded) // total]. World doOneCycle]. "wait for user to press a button" anHTTPFetcher succeeded ifFalse: [ dialogBox message: 'Failed: ', anHTTPFetcher failureReason. [dialogBox isDone not] whileTrue: [World doOneCycle]]. dialogBox delete. World doOneCycle. ! ! !ScratchFileGetter methodsFor: 'all' stamp: 'jm 1/3/2005 18:04'! getFileNamed: aString "Get the contents of the Scratch file in the current directory." | fullURL | fullURL _ aString. ((aString size < 5) or: [(aString copyFrom: 1 to: 5) asLowercase ~= 'http:']) ifTrue: [ fullURL _ 'http://', server, '/', baseDir. subdirs do: [:s | fullURL _ fullURL, '/', s]. fullURL _ fullURL, '/', aString]. ^ HTTPSocket httpGet: fullURL ! ! !ScratchFileGetter methodsFor: 'all' stamp: 'jm 10/7/2004 21:50'! getHtml "Get the contents of the page for base URL with the list of subdirectories concatenated onto it." | fullURL | fullURL _ 'http://', server, '/', baseDir. subdirs do: [:s | fullURL _ fullURL, '/', s]. ^ HTTPSocket httpGet: fullURL ! ! !ScratchFileGetter methodsFor: 'all' stamp: 'jm 10/7/2004 21:49'! getScratchFileFromBaseURL: baseURLString "Answer a ByteArray containing a Scratch file from the given base URL or nil if the user doesn't select any file. The use is presented with a sequence of menus that can be used to navigate to a particular Scratch project." | urlParts | urlParts _ HtmlChunker parseURL: baseURLString. server _ urlParts at: #server. baseDir _ urlParts at: #locator. subdirs _ #(). ^ self invokeMenu ! ! !ScratchFileGetter methodsFor: 'all' stamp: 'tis 12/11/2006 16:38'! invokeMenu "Answer a ByteArray containing a Scratch file from the base URL or nil if the user doesn't select any file." | htmlString refs files links menu choice dir newKey | htmlString _ self getHtml contents. refs _ HtmlChunker linksFrom: htmlString. refs size = 0 ifTrue: [^ nil]. refs _ refs asArray collect: [:assoc | newKey _ assoc key collect: [:ch | (ch asciiValue = 10) | (ch asciiValue = 13) ifTrue: [Character space] ifFalse: [ch]]. newKey -> assoc value]. files _ refs select: [:assoc | (assoc value asLowercase endsWith: '.scratch') | (assoc value asLowercase endsWith: '.sb')]. links _ refs select: [:assoc | ((assoc value asLowercase endsWith: '.scratch') not) & ((assoc value asLowercase endsWith: '.sb') not)]. menu _ CustomMenu new. links do: [:assoc | (assoc value includes: $:) ifTrue: [menu add: assoc key action: (#fullUrl -> assoc value)] ifFalse: [menu add: assoc key action: (#subdir -> assoc value)]]. menu addLine. files do: [:assoc | menu add: assoc key action: (#file -> assoc value)]. choice _ menu startUp. choice ifNil: [^ nil]. choice key = # file ifTrue: [^ self getFileNamed: choice value]. choice key = # fullUrl ifTrue: [^ self getScratchFileFromBaseURL: choice value]. choice key = # subdir ifTrue: [ dir _ choice value. (dir beginsWith: '/') ifTrue: [ baseDir _ dir copyFrom: 2 to: dir size. subdirs _ #()] ifFalse: [ subdirs _ subdirs copyWith: dir]. ^ self invokeMenu]. ! ! Allows the user to navigate through files and folders to select a file. ! !ScratchFilePicker methodsFor: 'initialization' stamp: 'ee 6/19/2008 11:50'! buildContentsPane "Build a scroll pane to hold the directory contents." contentsPaneMorph _ ScrollFrameMorph2 new color: self color; contents: (Morph new color: self color); showHorizontalScrollbar: false; hBarInset: 18; vBarInset: 18. self addMorphBack: contentsPaneMorph. ! ! !ScratchFilePicker methodsFor: 'initialization' stamp: 'ee 2/5/2009 16:22'! initialize super initialize. self initFromForm: (ScratchFrameMorph skinAt: #filePickerFrame). fetchInProgress _ false. showThumbnails _ false. finalSelection _ false. feedbackMorph _ StringMorph new. "used to show a remote directory fetch in progress" freezeFocus _ false. self buildButtons. self buildContentsPane. self extent: 380@310. self currentDirectory: FileDirectory default. ! ! !ScratchFilePicker methodsFor: 'accessing' stamp: 'tis 12/6/2006 17:24'! contentsPaneMorph ^ contentsPaneMorph ! ! !ScratchFilePicker methodsFor: 'accessing' stamp: 'jm 3/9/2005 11:43'! currentDirectory "Answer the directory that is currently being viewed." ^ currentDir ! ! !ScratchFilePicker methodsFor: 'accessing' stamp: 'ee 7/3/2008 15:05'! currentDirectory: aDirectoryOrString "Set my current directory to the given FileDirectory or path String." | s truncated w eWidth | (currentDir isKindOf: ScratchServerDirectory) ifTrue: [ fetchInProgress _ false. currentDir stopFetching]. (aDirectoryOrString isKindOf: String) ifTrue: [currentDir _ FileDirectory on: aDirectoryOrString] ifFalse: [currentDir _ aDirectoryOrString]. currentDir pathParts isEmpty ifTrue: [s _ '/'] ifFalse: [s _ currentDir pathParts last]. "trim directory name to fit button, if necessary" truncated _ false. eWidth _ (ScratchTranslator stringExtent: '...' font: directoryBarMorph label font) x. w _ 190 - eWidth. [((ScratchTranslator stringExtent: s font: directoryBarMorph label font) x) > w] whileTrue: [ truncated _ true. s _ s copyFrom: 1 to: s size - 1]. truncated ifTrue: [s _ s, '...']. s = '/' ifTrue: [s _ 'Computer']. directoryBarMorph label: (UTF8 withAll: s) font: (ScratchFrameMorph getFont: #FilePickerDirectoryName). directoryBarMorph width: contentsPaneMorph width - 160. directoryBarArrowMorph right: directoryBarMorph right - 9. lastUpMSecs _ 0. self getDirectoryContents. ! ! !ScratchFilePicker methodsFor: 'accessing' stamp: 'jm 3/9/2005 11:50'! extensions: anArrayOfStringsOrNil "Set the set of extensions used to select which files to display. For example, extensions might be set to #(gif bmp png jpg) to show only files containing images. No filtering is done if extensions is nil, as it is by default." extensions _ anArrayOfStringsOrNil. extensions ifNotNil: [ extensions _ extensions asArray collect: [:s | s asString asLowercase]]. ! ! !ScratchFilePicker methodsFor: 'accessing' stamp: 'md 3/10/2005 14:37'! isFinalSelection ^ finalSelection! ! !ScratchFilePicker methodsFor: 'accessing' stamp: 'jm 5/22/2005 21:12'! projectFetcher | fullPath | fullPath _ currentDir path, self selectedFile, '.scratch'. scratchProjectFetcher ifNotNil: [ "try to use the existing fetcher" (scratchProjectFetcher serverName = currentDir serverName and: [scratchProjectFetcher path = fullPath]) ifTrue: [^ scratchProjectFetcher] ifFalse: [ scratchProjectFetcher stopDownload. scratchProjectFetcher _ nil]]. "make a new fetcher" ^ HTTPFetcher new startDownload: fullPath fromServer: currentDir serverName. ! ! !ScratchFilePicker methodsFor: 'accessing' stamp: 'jm 5/20/2005 16:18'! scratchInfoClient: anObject "Set the object to be informed when a Scratch project file with an info dictionary is selected. The client will be sent the message #scratchInfo: once each time such a file is selected, allowing the project thumbnail and other project information to be displayed." scratchInfoClient _ anObject. ! ! !ScratchFilePicker methodsFor: 'accessing' stamp: 'jm 3/9/2005 11:54'! selectedFile "Answer the local name of the currently selected file or nil if there is no file selected." contentsPaneMorph contents submorphsDo: [:m | (m isHighlit & m entryIsDirectory not) ifTrue: [^ m entryName]]. ^ nil ! ! !ScratchFilePicker methodsFor: 'accessing' stamp: 'jm 3/9/2005 18:04'! showThumbnails: aBoolean "If true, generate and layout my contents as thumbnails." showThumbnails _ aBoolean. ! ! !ScratchFilePicker methodsFor: 'accessing' stamp: 'jm 1/2/2007 18:50'! startPlayingSound: fullName "Attempt to play the sound with given name." self stopPlayingSound. (fullName asLowercase endsWith: '.mp3') ifTrue: [sound _ [StreamingMP3Sound onFileNamed: fullName] ifError: [nil]] ifFalse: [sound _ [SampledSound fromFileNamed: fullName] ifError: [nil]]. sound ifNotNil: [ (sound respondsTo: #volume:) ifTrue: [sound volume: 1.0]. sound play]. ! ! !ScratchFilePicker methodsFor: 'accessing' stamp: 'jm 1/2/2007 18:40'! stopPlayingSound "If I am playing a sound, stop it." sound ifNotNil: [sound pause]. sound _ nil. ! ! !ScratchFilePicker methodsFor: 'geometry' stamp: 'ee 6/28/2008 18:36'! extent: aPoint "Resize my contents pane when I am resized." super extent: aPoint. contentsPaneMorph ifNotNil: [ contentsPaneMorph bounds: ((self topLeft + (9@50)) corner: (self bottomRight - 7)). topBarMorph width: contentsPaneMorph width]. ! ! !ScratchFilePicker methodsFor: 'event handling' stamp: 'jm 3/6/2005 12:12'! handlesMouseDown: evt ^ true ! ! !ScratchFilePicker methodsFor: 'event handling' stamp: 'jm 5/18/2005 12:53'! highlightEntryAt: aPoint "Highlight the entry at the given point, if any, and unhighlight all other entries. Answer true if the selected entry was already highlit, which means this is the second click on that entry." | secondClick | secondClick _ false. contentsPaneMorph contents submorphsDo: [:m | (m isKindOf: ScratchFilePickerEntry) ifTrue: [ (m containsPoint: aPoint) ifTrue: [secondClick _ m isHighlit. m highlight: true] ifFalse: [m highlight: false]]]. ^ secondClick ! ! !ScratchFilePicker methodsFor: 'event handling' stamp: 'jm 5/28/2009 21:21'! keyStroke: evt "Handle up/down arrow keys." | ch m entry | freezeFocus ifTrue: [^ self]. ch _ evt keyValue. ch = 9 ifTrue: [ "tab" (m _ self ownerThatIsA: DialogBoxMorph) ifNotNil: [m tabToNextField: evt]. ^ self]. ch = 27 ifTrue: [ "escape key" (m _ self ownerThatIsA: DialogBoxMorph) ifNotNil: [m escapeKeyPressed: evt]. ^ self]. ((ch = 1) | (ch = 11)) ifTrue: [ "home or page up" self selectedEntryOrNil ifNotNil: [self selectedEntryOrNil highlight: false]. ^ self highlightAndScrollToEntry: contentsPaneMorph contents submorphs first]. ((ch = 4) | (ch = 12)) ifTrue: [ "end or page down" self selectedEntryOrNil ifNotNil: [self selectedEntryOrNil highlight: false]. ^ self highlightAndScrollToEntry: contentsPaneMorph contents submorphs last]. ((ch = 8) | (ch = 127)) ifTrue: [ "delete key" self deleteDirectory]. ((ch = 10) | (ch = 13)) ifTrue: [ "cr, lf, or enter key" (entry _ self selectedEntryOrNil) ifNil: [^ self]. (entry entryIsDirectory and: [(owner isKindOf: ScratchFileChooserDialog)]) ifTrue: [self currentDirectory: (currentDir directoryNamed: entry entryName)] ifFalse: [(self ownerThatIsA: ScratchFileChooserDialog) yes]. ^ self]. (ch = 28) ifTrue: [^ self changeSelectionIndexBy: -1]. "left" (ch = 29) ifTrue: [^ self changeSelectionIndexBy: 1]. "right" (ch = 30) ifTrue: [ "up" showThumbnails ifTrue: [^ self changeSelectionIndexBy: -4] ifFalse: [^ self changeSelectionIndexBy: -1]]. (ch = 31) ifTrue: [ "down" showThumbnails ifTrue: [^ self changeSelectionIndexBy: 4] ifFalse: [^ self changeSelectionIndexBy: 1]]. ((ch between: $a asciiValue and: $z asciiValue) or: [ch between: $A asciiValue and: $Z asciiValue]) ifTrue: [^ self scrollToFileStartingWith: ch asCharacter asLowercase]. ! ! !ScratchFilePicker methodsFor: 'event handling' stamp: 'jm 12/16/2006 11:46'! mouseDown: evt "Record whether the mouse went down in the currently highlit entry (wasSelected) and if it should be considered a double-click. See mouseUp: for details." evt hand newKeyboardFocus: self. wasSelected _ self highlightEntryAt: evt cursorPoint. isDoubleClick _ (wasSelected and: [lastUpMSecs notNil and: [(Time millisecondClockValue - lastUpMSecs) < 1000]]). ! ! !ScratchFilePicker methodsFor: 'event handling' stamp: 'jm 5/20/2005 19:44'! mouseMove: evt "If the user drags away from the original selection and comes back, don't consider it a virtual double-click." (self highlightEntryAt: evt cursorPoint) ifFalse: [isDoubleClick _ false]. scratchInfoClient ifNotNil: [self reportScratchProjectInfo]. ! ! !ScratchFilePicker methodsFor: 'event handling' stamp: 'jm 1/2/2007 18:43'! mouseUp: evt "Handle a mouse up. If the mouse went down on an entry that was already selected, open that entry. (Like a double-click, but not sensitive to timing.) If the entry 'double-clicked' was a directory, open that directory. If it was a file, set finalSelection to true to cause the dialog to open the file." | singleClickOpensDirs playSelectedSound entry ext | singleClickOpensDirs _ false. playSelectedSound _ true. lastUpMSecs _ Time millisecondClockValue. (sound notNil and: [sound isPlaying]) ifTrue: [ self stopPlayingSound. isDoubleClick _ false. wasSelected ifTrue: [playSelectedSound _ false]]. entry _ contentsPaneMorph contents submorphs detect: [:m | m containsPoint: evt cursorPoint] ifNone: [^ self]. entry entryIsDirectory ifTrue: [ singleClickOpensDirs | isDoubleClick ifTrue: [ self currentDirectory: (currentDir directoryNamed: entry entryName)]. ^ self]. ext _ FileDirectory extensionFor: entry entryName asLowercase. playSelectedSound _ (playSelectedSound & extensions notNil) and: [extensions includes: 'wav']. playSelectedSound & isDoubleClick not ifTrue: [ (extensions includes: ext) ifTrue: [ self startPlayingSound: (currentDir fullNameFor: entry entryName)]]. finalSelection _ isDoubleClick. finalSelection ifTrue: [(self ownerThatIsA: ScratchFileChooserDialog) yes]. ! ! !ScratchFilePicker methodsFor: 'stepping' stamp: 'jm 5/22/2005 20:50'! step "If I am showing thumbnails, poll my contents for thumbnails that are ready for display." showThumbnails ifTrue: [ Delay waitMSecs: 10. contentsPaneMorph contents submorphsDo: [:m | m thumbnailReady ifTrue: [m clearThumbnailReady]]]. fetchInProgress ifTrue: [ currentDir fetchInProgress ifFalse: [ self getFetchedServerDirectoryContents. fetchInProgress _ false]]. scratchProjectFetcher ifNotNil: [ self checkForScratchInfoFetchDone]. ! ! !ScratchFilePicker methodsFor: 'stepping' stamp: 'jm 4/4/2005 22:43'! stepTime ^ 50 ! ! !ScratchFilePicker methodsFor: 'private-thumbnails' stamp: 'jm 4/4/2005 22:20'! startThumbnailFetchProcess "Start a background process to fetch thumbnails. This process runs at lower priority than the UI thread. The step method looks for thumbnails that have become ready and updates the display." thumbnailFetchProcess ifNotNil: [thumbnailFetchProcess terminate]. thumbnailFetchProcess _ [self thumbnailFetchLoop] newProcess. thumbnailFetchProcess priority: Processor userBackgroundPriority. thumbnailFetchProcess resume. ! ! !ScratchFilePicker methodsFor: 'private-thumbnails' stamp: 'jm 4/4/2005 21:14'! stopThumbnailFetchProcess thumbnailFetchProcess ifNotNil: [ thumbnailFetchProcess terminate. thumbnailFetchProcess _ nil]. ! ! !ScratchFilePicker methodsFor: 'private-thumbnails' stamp: 'jm 12/7/2005 12:38'! thumbnailFetchLoop contentsPaneMorph contents submorphs do: [:m | self isInWorld ifFalse: [^ self]. m computeThumbnail. Delay waitMSecs: 100]. thumbnailFetchProcess _ nil. Processor terminateActive. "stop myself--nothing after here will execute" ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 3/17/2009 22:00'! addDriveShortcuts: menu "Add shortcuts to the file picker menu." | drives visible | "Include drives" menu addLine. (FileDirectory default isKindOf: WinFileDirectory) ifTrue: [ "Windows" drives _ (FileDirectory on: '') directoryNames. drives _ drives copyWithout: 'Network'. drives _ drives copyWithout: 'automount/Servers'. (visible _ ScratchFrameMorph visibleDrives) ifNotNil: [ drives _ drives select: [:drive | visible includes: drive asUppercase]]. drives do: [:drive | menu add: drive action: drive]] ifFalse: [ "Mac OS X" drives _ (FileDirectory on: '/Volumes') directoryNames. (visible _ ScratchFrameMorph visibleDrives) ifNotNil: [ drives _ drives select: [:drive | visible includes: drive asUppercase]]. drives _ drives do: [:drive | menu add: drive action: '/Volumes/', drive]]. ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'tis 12/18/2006 16:38'! addShortcut: dir named: name toMenu: menu "Adds one of the Scratch default folders to the file picker menu, if it is not the current directory." dir ifNil: [^ self]. (currentDir isKindOf: ScratchServerDirectory) "keep this check separate since a ScratchServerDirectory does not respond to pathName" ifTrue: [^ menu add: name action: dir pathName]. (dir pathName ~= currentDir pathName) ifTrue: [^ menu add: name action: dir pathName]. ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'ee 7/3/2008 16:21'! buildButtons "Build my directory bar, parent buttons, and new folder button." | b f | topBarMorph _ AlignmentMorph newRow hResizing: #spaceFill; vResizing: #rigid; centering: #center; color: Color transparent. directoryBarArrowMorph _ ImageMorph new form: (ScratchFrameMorph skinAt: #directoryBarArrow). directoryBarMorph _ ResizableToggleButton2 new offForm: (ScratchFrameMorph skinAt: #directoryBar) onForm: (ScratchFrameMorph skinAt: #directoryBar); padding: 3@5. directoryBarMorph target: self; actionSelector: #directoryMenu; actWhen: #buttonUp. topBarMorph addMorphBack: (Morph new extent: (5@5); color: Color transparent); addMorphBack: directoryBarMorph. b _ ToggleButton onForm: (ScratchFrameMorph skinAt: #parentDirectoryButtonOn) offForm: (ScratchFrameMorph skinAt: #parentDirectoryButton). b target: self; actionSelector: #showParentDirectory; actWhen: #buttonUp; isMomentary: true. topBarMorph addMorphBack: (Morph new extent: (5@5); color: Color transparent); addMorphBack: (b position: self position + (252@16)). f _ ToggleButton onForm: (ScratchFrameMorph skinAt: #newFolderIconSelected) offForm: (ScratchFrameMorph skinAt: #newFolderIcon). f target: self; actionSelector: #newDirectory; actWhen: #buttonUp; isMomentary: true. topBarMorph addMorphBack: (Morph new extent: (5@5); color: Color transparent); addMorphBack: (f position: self position + (274@16)). directoryBarMorph label: (UTF8 withAll: '') font: (ScratchFrameMorph getFont: #FilePickerDirectoryName); leftJustifyInset: 9. self addMorphFront: (directoryBarArrowMorph position: self topLeft + ((b left - 32)@((50 - directoryBarArrowMorph height) // 2))). self addMorphBack: (topBarMorph position: self topLeft + (5@5)). ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 1/2/2007 18:41'! changeSelectionIndexBy: anInteger | entries e newI | self stopPlayingSound. entries _ contentsPaneMorph contents submorphs. entries size = 0 ifTrue: [^ self]. 1 to: entries size do: [:i | e _ entries at: i. e isHighlit ifTrue: [ e highlight: false. newI _ i + anInteger. newI > entries size ifTrue: [newI _ (i + 1) min: entries size]. newI < 1 ifTrue: [newI _ (i - 1) max: 1]. self highlightAndScrollToEntry: (entries at: newI). ^ self]]. entries first highlight: true. scratchInfoClient ifNotNil: [self reportScratchProjectInfo]. ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 7/30/2008 17:09'! checkForScratchInfoFetchDone "Try to retrieve info for the currently selected Scratch project and, if successful, report it to our client." "Details: The fetcher is allowed to keep running even after the info dictionary is retrieved. Thus if the user decides to open this project, we'll have a head start on fetching it." | data s version infoSize infoDict | scratchProjectFetcher ifNil: [^ self]. data _ scratchProjectFetcher bodyData. data size >= 14 ifTrue: [ s _ ReadStream on: data. version _ ObjStream scratchFileVersionFrom: (s next: 10) asString. (version = 1) | (version = 2) ifTrue: [ infoSize _ s uint32. infoSize <= (s size - s position) ifTrue: [ infoDict _ [ObjStream new readObjFrom: s] ifError: [Dictionary new]. scratchInfoClient scratchInfo: infoDict]] ifFalse: [ scratchInfoClient scratchInfo: Dictionary new]]. ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 12/22/2006 21:52'! deleteDirectory "Delete the directory or file that is currently highlit." | entries selectedDir dirEntries response | entries _ contentsPaneMorph contents submorphs. selectedDir _ entries detect: [:e | e isHighlit & e entryIsDirectory] ifNone: [^ self]. dirEntries _ (self currentDirectory directoryNamed: selectedDir entryName) entries. dirEntries size > 0 ifTrue: [^ self]. "do nothing if selectedDir is not empty" freezeFocus _ true. response _ DialogBoxMorph ask: 'Are you sure you want to delete the empty folder ''' , selectedDir entryName , '''?'. freezeFocus _ false. response ifTrue: [ [self currentDirectory deleteDirectory: e entryName] ifError: [^ DialogBoxMorph warn: 'Could not delete folder.']. ^ self currentDirectory: self currentDirectory]. ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 2/22/2009 22:40'! directoryMenu "Present a drop-down menu of all directories in my current path." | menu pathParts prefix n serverEntries choice s serverPath | self navigationForbidden ifTrue: [^ self]. menu _ CustomMenu new. pathParts _ currentDir pathParts. prefix _ ''. n _ 1. pathParts do: [:part | menu add: prefix asUTF8, part action: n. prefix _ prefix ,' '. n _ n + 1]. pathParts isEmpty ifTrue: [menu add: 'Computer' action: (FileDirectory on: '') pathName]. self addDriveShortcuts: menu. "if opening a Scratch project, allow access to servers" scratchInfoClient ifNotNil: [ serverEntries _ ScratchFrameMorph scratchServers. serverEntries size > 0 ifTrue: [ menu addLine. serverEntries do: [:entry | menu add: entry first action: n. n _ n + 1]]]. choice _ menu startUp: nil withCaption: nil at: (directoryBarMorph right - 117)@(directoryBarMorph top + 8). choice ifNil: [^ self]. (choice isKindOf: String) ifTrue: [ choice = 'Computer' ifTrue: [^ self currentDirectory: ''] ifFalse: [^ self currentDirectory: choice contents]]. choice > pathParts size ifTrue: [ entry _ serverEntries at: choice - pathParts size. ^ self currentDirectory: (ScratchServerDirectory new serverName: (entry at: 2); path: (entry at: 3))]. s _ WriteStream on: String new. 1 to: choice do: [:i | s nextPutAll: (pathParts at: i). i < choice ifTrue: [s nextPut: currentDir pathNameDelimiter]]. (currentDir isKindOf: ScratchServerDirectory) ifTrue: [ serverPath _ '/', s contents. (serverPath endsWith: '/') ifFalse: [serverPath _ serverPath, '/']. self currentDirectory: (currentDir copy path: serverPath)] ifFalse: [ self currentDirectory: s contents]. ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 1/2/2007 17:11'! getDirectoryContents "Generate and layout the morphs in my contents pane from the files and folder in the current directory." feedbackMorph delete. (currentDir isKindOf: FileDirectory) ifTrue: [^ self getLocalDirectoryContents]. "remote case: start fetching directory contents and give feedback that fetch is started" feedbackMorph contents: 'Reading from ', currentDir serverName, '...'; position: self topLeft + (20@60). contentsPaneMorph contents removeAllMorphs. contentsPaneMorph vScrollRelative: 0. self addMorphFront: feedbackMorph. fetchInProgress _ true. ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'tis 12/11/2006 16:39'! getFetchedServerDirectoryContents "Generate and layout the morphs in my contents pane for the files and folder in the recently server directory contents. If the fetch generated an error, display the error." "Assume that the page morph exists already." | page m morphsToAdd x y nm isDir | page _ contentsPaneMorph contents. currentDir fetchSucceeded ifFalse: [ m _ StringMorph contents: 'Failed: ', currentDir fetchFailureReason. page addMorph: (m position: page topLeft + (10@30)). ^ self]. morphsToAdd _ OrderedCollection new: 1000. x _ page left + 7. y _ page top + 5. currentDir allNames do: [:n | nm _ n. isDir _ true. (n asLowercase endsWith: '.scratch') ifTrue: [ nm _ n copyFrom: 1 to: n size - '.scratch' size. isDir _ false]. (n asLowercase endsWith: '.sb') ifTrue: [ nm _ n copyFrom: 1 to: n size - '.sb' size. isDir _ false]. m _ ScratchFilePickerEntry new name: nm dir: currentDir isDirectory: isDir; width: self width - 60; color: self color. morphsToAdd add: (m position: x@y). y _ y + m height]. feedbackMorph delete. page removeAllMorphs. page addAllMorphs: morphsToAdd. self changeSelectionIndexBy: 1. ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 10/29/2007 09:36'! getLocalDirectoryContents "Generate and layout the morphs in my contents pane from the files and folder in the current local directory." | allNames fileNames dirNames ext page | "make an alphabetized list of all files and directory names" fileNames _ currentDir fileNames. extensions ifNotNil: [ "filter out files without a matching extension" fileNames _ fileNames select: [:n | extensions includes: (FileDirectory extensionFor: n) asLowercase]]. fileNames _ fileNames sort: [:n1 :n2 | n1 caseInsensitiveLessOrEqual: n2]. dirNames _ self getLocalDirectoryNames. dirNames _ dirNames sort: [:n1 :n2 | n1 caseInsensitiveLessOrEqual: n2]. allNames _ dirNames , fileNames. allNames _ allNames reject: [:n | n endsWith: '_th.gif']. "suppress Scratch project thumbnails" allNames _ allNames reject: [:n | n beginsWith: '.']. "suppress hidden files on Mac OS X and Unix" allNames _ allNames reject: [:n | ext _ (FileDirectory extensionFor: n) asLowercase. #(app dll exe ini image changes) includes: ext]. allNames _ allNames reject: [:n | ScratchPlugin isHidden: (currentDir fullNameFor: n)]. "suppress hidden files/folders on Win32" currentDir pathName = FileDirectory default pathName ifTrue: [ allNames _ allNames reject: [:fn | #(help icons 'license.txt' locale plugins 'scratch.app' ) includes: fn asLowercase]]. showThumbnails ifTrue: [page _ self thumbnailStylePageFor: allNames] ifFalse: [page _ self listStylePageFor: allNames]. contentsPaneMorph contents: page. showThumbnails ifTrue: [self startThumbnailFetchProcess]. self changeSelectionIndexBy: 1. ! ! !ScratchFilePicker methodsFor: 'private'! getLocalDirectoryNames "Answer the directories in currentDir. Suppress hidden volumes." | dirNames visible | dirNames _ currentDir directoryNames. (visible _ ScratchFrameMorph visibleDrives) ifNil: [^ dirNames]. Smalltalk isMacOSX ifTrue: [currentDir pathName = '/Volumes' ifFalse: [^ dirNames]] ifFalse: [currentDir pathName = '' ifFalse: [^ dirNames]]. ^ dirNames select: [:dir | visible includes: dir asUppercase] ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 7/30/2008 17:10'! getScratchInfoFromFile "Answer the project info dictionary for the currently selected file. Answer the empty dictionary if no file is selected or if the file does not include a project info dictionary (e.g. if it is in an older Scratch file format)." | result fullName f version buf | result _ Dictionary new. self selectedFile ifNil: [^ result]. fullName _ currentDir fullNameFor: self selectedFile. (FileDirectory default fileExists: fullName) ifFalse: [^ result]. [f _ (FileStream readOnlyFileNamed: fullName) binary] ifError: [^ result]. [ version _ ObjStream scratchFileVersionFrom: (f next: 10) asString. (version = 1) | (version = 2) ifTrue: [ buf _ f next: f uint32. result _ ObjStream new readObjFrom: (ReadStream on: buf)]. ] ifError: []. f close. ^ result ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'tis 12/7/2006 18:50'! highlightAndScrollToEntry: e e highlight: true. contentsPaneMorph scrollSubmorphIntoView: e. scratchInfoClient ifNotNil: [self reportScratchProjectInfo]. ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'ee 6/27/2008 15:39'! listStylePageFor: allNames "Answer a new contents page as a column of list style entries." | page dirNames morphsToAdd x y m | page _ Morph new color: self color; width: self width - 20. dirNames _ currentDir directoryNames asSet. morphsToAdd _ OrderedCollection new: 1000. x _ page left + 7. y _ page top + 5. allNames do: [:n | m _ ScratchFilePickerEntry new name: n dir: currentDir isDirectory: (dirNames includes: n); width: self width - 60; color: self color; borderWidth: 0; useRoundedCorners. morphsToAdd add: (m position: x@y). y _ y + m height]. page addAllMorphs: morphsToAdd. ^ page ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 3/25/2009 17:17'! navigationForbidden "Answer true if navigating the file hierarch is forbidden because there is a visibledrives entry in the Scratch.ini file." | thisDrive pathParts | ScratchFrameMorph visibleDrives ifNil: [^ false]. thisDrive _ ''. pathParts _ currentDir pathParts. pathParts size > 0 ifTrue: [thisDrive _ pathParts first asUppercase]. Smalltalk isMacOSX ifTrue: [ ((thisDrive = 'volumes') and: [pathParts size > 1]) ifTrue: [ thisDrive _ pathParts second asUppercase]]. (ScratchFrameMorph visibleDrives includes: thisDrive) ifTrue: [^ false]. self beep. ^ true ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'ee 6/27/2008 17:45'! newDirectory "Create a new directory." | name | name _ StringDialog askWithCancel: 'New folder name:'. name = '' ifTrue: [^ self]. [self currentDirectory createDirectory: name] ifError: [:err :rcvr | ^ DialogBoxMorph warn: 'Could not create folder.']. self currentDirectory: self currentDirectory. ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 1/2/2007 16:42'! reportScratchProjectInfo "Try to retrieve info for the currently selected Scratch project and, if successful, report it to our client." | fullPath | scratchInfoClient ifNil: [^ self]. scratchProjectFetcher ifNotNil: [ scratchProjectFetcher stopDownload. scratchProjectFetcher _ nil]. self selectedFile ifNil: [scratchInfoClient scratchInfo: nil. ^ self]. (currentDir isKindOf: FileDirectory) ifTrue: [ ^ scratchInfoClient scratchInfo: self getScratchInfoFromFile]. "directory is on a server: start a fetcher to get the project info" fullPath _ currentDir path, self selectedFile, '.scratch'. scratchProjectFetcher ifNil: [scratchProjectFetcher _ HTTPFetcher new]. ((scratchProjectFetcher serverName ~= currentDir serverName) | (scratchProjectFetcher path ~= fullPath)) ifTrue: [ "start fetching from the new path" scratchProjectFetcher startDownload: fullPath fromServer: currentDir serverName]. Delay waitMSecs: 50. self checkForScratchInfoFetchDone. ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 1/2/2007 18:40'! scrollToFileStartingWith: aCharacter | entries possibilities currentlyHighlightedIndex | self stopPlayingSound. entries _ contentsPaneMorph contents submorphs. entries size = 0 ifTrue: [^ self]. "get all folders and files starting with aCharacter" possibilities _ entries select: [:e | e entryName asLowercase beginsWith: (String with: aCharacter)]. possibilities isEmpty ifTrue: [^ self]. "Find the currentlyHighlightedIndex" 1 to: entries size do: [:i | e _ entries at: i. e isHighlit ifTrue: [ e highlight: false. currentlyHighlightedIndex _ i]]. "Find the next file or folder (after the currentlyHighlightedIndex) starting with aCharacter" currentlyHighlightedIndex ifNotNil: [ (currentlyHighlightedIndex + 1) to: entries size do: [:i | e _ entries at: i. (e entryName asLowercase beginsWith: (String with: aCharacter)) ifTrue: [ ^ self highlightAndScrollToEntry: e]]]. "If all else fails, highlight the first file or folder starting with aCharacter" ^ self highlightAndScrollToEntry: possibilities first ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 6/23/2005 16:19'! selectedEntryOrNil ^ contentsPaneMorph contents submorphs detect: [:m | m isHighlit] ifNone: [^ nil] ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'jm 2/22/2009 22:40'! showParentDirectory "Go to my parent directory." self navigationForbidden ifTrue: [^ self]. self currentDirectory: currentDir parentDirectory. ! ! !ScratchFilePicker methodsFor: 'private' stamp: 'ee 6/27/2008 18:37'! thumbnailStylePageFor: allNames "Answer a new contents page as tableau of thumbnails." | page dirNames morphsToAdd leftMargin rightMargin x y m thumbnailCache f | page _ Morph new color: self color; width: self width - 20. dirNames _ currentDir directoryNames asSet. thumbnailCache _ ThumbnailCache new directory: currentDir. thumbnailCache readThumbnailFile; updateThumbnails. morphsToAdd _ OrderedCollection new: 1000. leftMargin _ page left + 7. rightMargin _ page width - 75. x _ leftMargin. y _ page top + 5. allNames do: [:n | m _ ScratchFilePickerImageEntry new name: n dir: currentDir isDirectory: (dirNames includes: n). m borderWidth: 0; useRoundedCorners. f _ thumbnailCache thumbnailFor: n. f ifNotNil: [m thumbnailForm: f]. morphsToAdd add: (m position: x@y). x _ x + m width. x > rightMargin ifTrue: [ x _ leftMargin. y _ y + m height]]. page addAllMorphs: morphsToAdd. ^ page ! ! Displays one line representing a file or folder within a ScratchFilePicker. ! !ScratchFilePickerEntry methodsFor: 'initialization' stamp: 'ee 6/27/2008 15:58'! name: aString dir: owningDirectory isDirectory: dirFlag | icon m n | directory _ owningDirectory. entryName _ aString. entryIsDirectory _ dirFlag. entryIsDirectory ifTrue: [icon _ ScratchFrameMorph skinAt: #folderIcon] ifFalse: [icon _ ScratchFrameMorph skinAt: #fileIcon]. n _ dirFlag ifTrue: [entryName] ifFalse: [FileDirectory baseNameFor: entryName]. nameMorph _ StringMorph contents: (UTF8 withAll: n) font: (ScratchFrameMorph getFont: #FilePickerEntry). self addMorph: (nameMorph position: self position + (28@3)). m _ ImageMorph new form: icon. self addMorph: (m position: self position + (6@((nameMorph height - 6) // 2))). highlit _ false. self extent: 200@(nameMorph height + 6). ! ! !ScratchFilePickerEntry methodsFor: 'accessing' stamp: 'jm 3/8/2005 09:12'! entryIsDirectory ^ entryIsDirectory ! ! !ScratchFilePickerEntry methodsFor: 'accessing' stamp: 'jm 3/8/2005 09:14'! entryName ^ entryName ! ! !ScratchFilePickerEntry methodsFor: 'accessing' stamp: 'ee 7/3/2008 15:15'! highlight: aBoolean "Show or hide my highlight." aBoolean ifFalse: [ highlit _ false. self color: owner color. nameMorph font: (ScratchFrameMorph getFont: #FilePickerEntry); color: Color black. borderWidth _ 0. ^ self]. highlit _ true. "self color: owner color darker darker darker. borderWidth _ 2. borderColor _ (Color r: (96/255) g: (136/255) b: (182/255))." self color: (Color r: (96/255) g: (136/255) b: (182/255)). nameMorph font: (ScratchFrameMorph getFont: #FilePickerEntryHighlighted); color: Color white. ! ! !ScratchFilePickerEntry methodsFor: 'accessing' stamp: 'ee 6/27/2008 18:35'! isHighlit "Answer true if I am highlit." highlit ifNil: [^ false] ifNotNil: [^ highlit] ! ! !ScratchFilePickerEntry methodsFor: 'accessing' stamp: 'jm 4/4/2005 22:31'! thumbnailReady "I don't have a thumbnail." ^ false ! ! I represent an image file, which I can display as thumbnail. Details: The image file is read and the thumbnail computed by a background process. A step method in the UI process polls to find out when the thumbnail is ready for display, then installs it. This avoids potential race conditions with the UI thread's damage reporting and redisplay mechanism. ! !ScratchFilePickerImageEntry methodsFor: 'initialization' stamp: 'ee 6/28/2008 16:13'! name: aString dir: owningDirectory isDirectory: dirFlag | n truncated w eWidth | self color: Color transparent. self extent: 125@110. directory _ owningDirectory. entryName _ aString. entryIsDirectory _ dirFlag. thumbnailReady _ false. thumbnailMorph _ ImageMorph new. entryIsDirectory ifTrue: [thumbnailMorph form: (ScratchFrameMorph skinAt: #bigFolderIcon)] ifFalse: [thumbnailMorph form: ((Form extent: 4@4 depth: 8) fillColor: Color transparent)]. thumbnailMorph top: self bottom - thumbnailMorph height - 23. thumbnailMorph left: self left + ((self width - thumbnailMorph width) // 2). n _ dirFlag ifTrue: [entryName] ifFalse: [FileDirectory baseNameFor: entryName]. nameMorph _ StringMorph contents: (UTF8 withAll: n) font: (ScratchFrameMorph getFont: #FilePickerEntry). "trim file name to fit button, if necessary" truncated _ false. eWidth _ (ScratchTranslator stringExtent: '...' font: (ScratchFrameMorph getFont: #FilePickerEntryHighlighted)) x. w _ 100 - eWidth. [((ScratchTranslator stringExtent: n font: nameMorph font) x) > w] whileTrue: [ truncated _ true. n _ n copyFrom: 1 to: n size - 1]. truncated ifTrue: [n _ n, '...']. nameMorph contents: (UTF8 withAll: n). nameMorph bottom: self bottom - 2. self isHighlit ifTrue: [nameMorph left: self left + ((self width - nameMorph width - eWidth) // 2)] ifFalse: [nameMorph left: self left + ((self width - nameMorph width) // 2)]. self addMorph: nameMorph. self addMorph: thumbnailMorph. ! ! !ScratchFilePickerImageEntry methodsFor: 'accessing' stamp: 'jm 4/5/2005 08:06'! clearThumbnailReady "Clear my thumbnailReady flag and report a change. Because it contains a 'self changed', this method should be called from the main UI thread, not from the background process to avoid potential race conditions." thumbnailMorph top: self bottom - thumbnailForm height - 23. thumbnailMorph left: self left + ((self width - thumbnailForm width) // 2). thumbnailMorph form: thumbnailForm. thumbnailReady _ false. self changed. ! ! !ScratchFilePickerImageEntry methodsFor: 'accessing' stamp: 'jm 7/18/2006 12:49'! computeThumbnail "Compute my thumbnail, but don't do a 'self changed' because I may be run from a background thread." | fName fContents f scale | entryIsDirectory ifTrue: [^ self]. thumbnailForm ifNotNil: [^ self]. [ fName _ directory fullNameFor: entryName. fContents _ (FileStream readOnlyFileNamed: fName) binary contentsOfEntireFile. f _ Form fromBinaryStream: (ReadStream on: fContents) ] ifError: [^ self]. f depth < 16 ifTrue: [f _ f asFormOfDepth: 16]. scale _ (80.0 / f width) min: (60.0 / f height). thumbnailForm _ f magnifyBy: scale. thumbnailReady _ true. ! ! !ScratchFilePickerImageEntry methodsFor: 'accessing' stamp: 'ee 7/3/2008 15:15'! highlight: aBoolean "Show or hide my highlight." aBoolean ifFalse: [ self color: owner color. nameMorph font: (ScratchFrameMorph getFont: #FilePickerEntry); color: Color black. highlit _ false. borderWidth _ 0. nameMorph left: self left + ((self width - nameMorph width) // 2). ^ self]. "self color: owner color darker darker darker. borderWidth _ 2. borderColor _ (Color r: (96/255) g: (136/255) b: (182/255))." self color: (Color r: (96/255) g: (136/255) b: (182/255)). nameMorph font: (ScratchFrameMorph getFont: #FilePickerEntryHighlighted); color: Color white. nameMorph left: self left + ((self width - nameMorph width) // 2). highlit _ true. ! ! !ScratchFilePickerImageEntry methodsFor: 'accessing' stamp: 'jm 7/18/2006 12:51'! thumbnailForm: aForm "Set my thumbnail form." thumbnailForm _ aForm. thumbnailMorph top: self bottom - thumbnailForm height - 23. thumbnailMorph left: self left + ((self width - thumbnailForm width) // 2). thumbnailMorph form: thumbnailForm. ! ! !ScratchFilePickerImageEntry methodsFor: 'accessing' stamp: 'jm 4/4/2005 21:08'! thumbnailReady "Answer true if my thumbnail has been computed but I have not yet done a change update." ^ thumbnailReady ! ! I am the top level user interface for Scratch. I tile the screen with a toolbar, a work pane (for content), a viewer pane, and a script editor pane. I can resize myself to fill the entire Squeak window. I keep a list of Scratch processes (threads) and run each one to the its next stopping point when I am stepped each screen update cycle. ! !ScratchFrameMorph methodsFor: 'intialization' stamp: 'jm 9/23/2009 14:36'! addShortcutButtonsTo: rowMorph | buttonSpecs b | buttonSpecs _ #( "name tool tip selector" (language 'Set language' languageMenu:) (save 'Save this project' saveScratchProjectNoDialog) ). buttonSpecs do: [:spec | b _ ToggleButton onForm: (ScratchFrameMorph skinAt: (spec at: 1), 'ButtonOver') offForm: (ScratchFrameMorph skinAt: (spec at: 1), 'Button') overForm: (ScratchFrameMorph skinAt: (spec at: 1), 'ButtonOver'). b target: self; actionSelector: (spec at: 3); setBalloonText: (spec at: 2) localized; actWhen: #buttonUp; isMomentary: true. ('language' = (spec at: 1)) ifTrue: [ "language special case" b arguments: (Array with: b)]. ('save' = (spec at: 1)) ifTrue: [ "spacer" rowMorph addMorphBack: (Morph new extent: (10@5); color: Color transparent)]. rowMorph addMorphBack: b]. rowMorph addMorphBack: (Morph new extent: (20@5); color: Color transparent). ! ! !ScratchFrameMorph methodsFor: 'intialization' stamp: 'ee 1/27/2009 16:44'! createBasicPanes "Create and add my palette (viewer), script editor, stage, and library panes." topPane _ ImageFrameMorph new initFromForm: (ScratchFrameMorph skinAt: 'topPane'). viewerPane _ ScratchViewerMorph new rebuildCategorySelectors. scriptsPane _ ScratchScriptEditorMorph new. stageFrame _ ImageFrameMorph new initFromForm: (ScratchFrameMorph skinAt: 'stagePane'). titlePane _ ImageFrameMorph new initFromForm: (ScratchFrameMorph skinAt: 'titlePane'). workPane _ ScratchStageMorph new extent: WorkpaneExtent. libraryPane _ ScratchLibraryMorph new. "make panes sticky so clicking on them doesn't pick up entire frame" self addMorph: (topPane isSticky: true); addMorph: (viewerPane isSticky: true); addMorph: (scriptsPane isSticky: true); addMorph: (stageFrame isSticky: true); addMorph: (workPane isSticky: true); addMorph: (titlePane isSticky: true); addMorph: (libraryPane isSticky: true). self createReadoutPane. workPane comeToFront. ! ! !ScratchFrameMorph methodsFor: 'intialization' stamp: 'ee 1/27/2009 13:21'! createLogo "Create and the Scratch logo." logoMorph _ SketchMorph withForm: (ScratchFrameMorph skinAt: #scratchLogo). logoMorph position: topPane position + (12@8). topPane addMorph: logoMorph. ! ! !ScratchFrameMorph methodsFor: 'intialization' stamp: 'jm 9/23/2009 13:23'! createMenuPanel "Create and add a panel containing the menus and close button." | menuSpecs m | "create panel" menuPanel _ AlignmentMorph new color: Color transparent; centering: #center; inset: 0; height: 0. "will grow as needed" self addShortcutButtonsTo: menuPanel. "menuSpecs defines the menus" menuSpecs _ #( "name selector" (File fileMenu:) (Edit editMenu:) (Help helpMenu:) ). menuSpecs do: [:spec | m _ ScratchMenuTitleMorph new contents: (spec at: 1) localized; target: self selector: (spec at: 2). menuPanel addMorphBack: m. #helpMenu: = (spec at: 2) ifFalse: [ menuPanel addMorphBack: (Morph new color: Color transparent; extent: 12@5)]]. topPane addMorph: menuPanel. ! ! !ScratchFrameMorph methodsFor: 'intialization' stamp: 'ee 1/29/2009 14:21'! createReadoutPane "Create and add my presentation mode button, new sprite buttongs, and mouse readout pane." | xyReadout | readoutPane _ ImageFrameMorph new initFromForm: (ScratchFrameMorph skinAt: #mouseReadoutPane). xyReadout _ self makeXYReadout. readoutPane addMorph: xyReadout. "make pane sticky so clicking on it doesn't pick up entire frame" self addMorph: (readoutPane isSticky: true). ! ! !ScratchFrameMorph methodsFor: 'intialization' stamp: 'ee 3/17/2009 16:36'! createStageButtonsPanel "Create and add a panel containing the project title, green flag, and stop buttons." | buttonSpecs bName button | "create panel" stageButtonsPanel _ AlignmentMorph new color: Color transparent; centering: #center; height: 20. projectTitleMorph _ StringMorph new forceUnicodeRendering: true; contents: ''; font: (ScratchFrameMorph getFont: #FrameMorphProjectTitle). stageButtonsPanel addMorphBack: projectTitleMorph; addMorphBack: (AlignmentMorph newSpacer: Color transparent). "buttonSpecs defines the toolbar buttons; first is icon name, second is selector" buttonSpecs _ #( "name selector tool tip" (go shoutGo 'Start green flag scripts') (stop stopAll 'Stop everything')). buttonSpecs do: [:spec | bName _ spec first. button _ ToggleButton onForm: (ScratchFrameMorph skinAt: (bName, 'ButtonGrayPressed') asSymbol) offForm: (ScratchFrameMorph skinAt: (bName, 'ButtonGray') asSymbol) overForm: (ScratchFrameMorph skinAt: (bName, 'ButtonGrayPressed') asSymbol). button target: self; actionSelector: (spec at: 2); isMomentary: true; setProperty: #balloonText toValue: (spec at: 3) localized. stageButtonsPanel addMorphBack: button. bName = #go ifTrue: [ flagButton _ button. stageButtonsPanel addMorphBack: (Morph new color: Color transparent; extent: 2@5)]]. titlePane addMorph: stageButtonsPanel. ! ! !ScratchFrameMorph methodsFor: 'intialization' stamp: 'jm 4/14/2009 08:59'! createToolbar "Create and add the toolbar." | buttonSpecs bName button | toolbarPanel _ AlignmentMorph new hResizing: #shrinkWrap; vResizing: #shrinkWrap; color: Color transparent. buttonSpecs _ #( "name selector" "tooltip" (copy copyTool 'Duplicate') (delete cutTool 'Delete') (zoomIn zoomInTool 'Grow sprite') (zoomOut zoomOutTool 'Shrink sprite') ). buttonSpecs do: [:spec | bName _ spec at: 1. button _ ToggleButton onForm: (ScratchFrameMorph skinAt: (bName, 'ButtonPressed') asSymbol) offForm: (ScratchFrameMorph skinAt: (bName, 'Button') asSymbol) overForm: (ScratchFrameMorph skinAt: (bName, 'ButtonOver') asSymbol). button target: self; actionSelector: (spec at: 2); isMomentary: true; setProperty: #balloonText toValue: (spec at: 3) localized. toolbarPanel addMorphBack: button]. self addMorph: toolbarPanel. ! ! !ScratchFrameMorph methodsFor: 'intialization' stamp: 'jm 4/14/2009 08:57'! createViewModeButtonsPanel | specs bName button | viewModeButtonsPanel _ AlignmentMorph newRow hResizing: #shrinkWrap; vResizing: #shrinkWrap; color: Color transparent. viewModeButtons _ OrderedCollection new. specs _ OrderedCollection new. specs add: #(quarter enterQuarterMode 'Switch to small stage'). specs add: #(normal enterNormalMode 'Switch to full stage'). specs add: #(presentation enterPresentationMode 'Switch to presentation mode'). specs do: [:spec | bName _ spec first. button _ ToggleButton new onForm: (ScratchFrameMorph skinAt: bName, 'ViewModeOn') offForm: (ScratchFrameMorph skinAt: bName, 'ViewMode') overForm: (ScratchFrameMorph skinAt: bName, 'ViewModeOver'). button target: self; actionSelector: (spec at: 2); alphaOn: true; setProperty: #balloonText toValue: (spec at: 3) localized. viewModeButtonsPanel addMorphBack: button; addMorphBack: (Morph new extent: 1@5; color: Color transparent). viewModeButtons add: button]. self addMorph: viewModeButtonsPanel. ! ! !ScratchFrameMorph methodsFor: 'intialization' stamp: 'jm 4/9/2009 10:19'! initialize super initialize. fillScreenFlag _ false. paintingInProgress _ false. projectInfo _ Dictionary new. watcherPositions _ Dictionary new. justSaved _ false. author _ ''. loginName _ ''. loginPassword _ ''. viewMode _ #normal. self createBasicPanes. self createLogo. self createMenuPanel. self createViewModeButtonsPanel. self createStageButtonsPanel. self createToolbar. self extent: 1000@600. ! ! !ScratchFrameMorph methodsFor: 'intialization' stamp: 'jm 5/4/2009 11:15'! makeXYReadout "Make and answer an x-y readout." | normalFont boldFont panel spaceWidth labelX readoutX labelY readoutY | normalFont _ (ScratchFrameMorph getFont: #XYReadout). boldFont _ (ScratchFrameMorph getFont: #XYReadoutBold). (ScratchTranslator renderScale ~= 1) ifTrue: [ "force fonts to be fixed size:" normalFont _ StrikeFont osFontName: normalFont name size: normalFont pointSize / ScratchTranslator renderScale asFloat. boldFont _ StrikeFont osFontName: boldFont name size: boldFont pointSize / ScratchTranslator renderScale asFloat]. panel _ Morph new color: (Color r: 0.753 g: 0.764 b: 0.776). ScratchTranslator isRTL ifTrue: [labelX _ StringMorph new font: normalFont; contents: ':x' asUTF8] ifFalse: [labelX _ StringMorph new font: normalFont; contents: 'x:' asUTF8]. readoutX _ UpdatingStringMorph new target: self; getSelector: #mouseX; forceUnicodeRendering: true; font: boldFont; stepTime: 150; growable: false. readoutX width: (readoutX stringWidth: '-1000'). ScratchTranslator isRTL ifTrue: [labelY _ labelX fullCopy contents: ':y' asUTF8] ifFalse: [labelY _ labelX fullCopy contents: 'y:' asUTF8]. readoutY _ readoutX fullCopy getSelector: #mouseY. spaceWidth _ ((readoutX stringWidth: ' ') * 0.8) asInteger. ScratchTranslator isRTL ifTrue: [readoutY rightJustify: true. panel addMorph: (readoutY position: 0@0). panel addMorph: (labelY position: ((readoutY topRight) + (spaceWidth@0))).] ifFalse: [panel addMorph: (labelX position: 0@0). panel addMorph: (readoutX position: ((labelX topRight) + (spaceWidth@0)))]. ScratchTranslator isRTL ifTrue: [readoutX rightJustify: true. panel addMorph: (readoutX position: (labelY right@labelY top) + (spaceWidth@0)). panel addMorph: (labelX position: ((readoutX topRight) + (spaceWidth@0)))] ifFalse: [panel addMorph: (labelY position: (labelX right + readoutX width + 8)@(labelX top)). panel addMorph: (readoutY position: ((labelY topRight) + (spaceWidth@0)))]. ScratchTranslator isRTL ifTrue: [panel extent: ((labelX right) max: (labelY right))@(labelY bottom)] ifFalse: [panel extent: ((readoutX right) max: (readoutY right))@(labelY bottom)]. ^ panel ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 3/24/2007 09:34'! author ^ author ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 3/24/2007 09:35'! author: aString "This is the author used in notes and save as, which is different from the user name used when uploading to the website." author _ aString. ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 6/3/2009 16:44'! importSpriteOrProject: fileNameOrData "Read the sprite or project file and merge into the current project." | data f importedStage defaultForm defaultSound oldName oldPosition | data _ fileNameOrData. (data isKindOf: String) ifTrue: [ "read the contents of a local file" (FileDirectory default fileExists: fileNameOrData) ifFalse: [^ self]. f _ (FileStream readOnlyFileNamed: fileNameOrData) binary. f ifNil: [^ self]. data _ f contentsOfEntireFile]. [importedStage _ self extractProjectFrom: data] ifError: [^ self]. "fix references to old stage" importedStage allMorphsDo: [:m | (m isKindOf: WatcherMorph) ifTrue: [m mapReceiver: importedStage to: workPane]. (m isKindOf: ScriptableScratchMorph) ifTrue: [ m blocksBin submorphs do: [:stack | (stack isKindOf: BlockMorph) ifTrue: [ stack blockSequence do: [:b | b mapReceiver: importedStage to: workPane]]]]]. "add global variables from importated stage to my stage" importedStage varNames do: [:v | workPane addVariable: v value: (importedStage getVar: v)]. "add imported stage scripts" importedStage blocksBin submorphs do: [:stack | (stack isKindOf: BlockMorph) ifTrue: [workPane addStack: stack fullCopy]]. "add imported background costumes and scripts to my stage, filtering out default items" defaultForm _ workPane defaultImageMedia form hibernate. defaultSound _ SoundMedia new sound. importedStage media do: [:media | (media isImage and: [media form hibernate bits ~= defaultForm bits]) ifTrue: [workPane addMediaItem: media]. (media isSound and: [media sound samples ~= defaultSound samples]) ifTrue: [workPane addMediaItem: media]]. importedStage submorphs do: [:m | (m isKindOf: ScratchSpriteMorph) ifTrue: [ oldName _ m objName. oldPosition _ m position - m owner position + (47@55). "jm: I am not sure why this offset is needed. It's the rotation center of the default costume..." self addAndView: m. "assigns a new name" m objName: oldName. m position: workPane topLeft + oldPosition]]. workPane layoutChanged. ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'tis 11/8/2006 13:34'! libraryPane ^ libraryPane ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 3/24/2007 09:27'! loginName ^ loginName ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 3/24/2007 09:30'! loginName: aString loginName _ aString. ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 3/24/2007 09:27'! loginPassword ^ loginPassword ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 3/24/2007 09:30'! loginPassword: aString loginPassword _ aString. ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 5/2/2005 17:22'! paintingInProgress "Answer true if the paint editor is in use." ^ paintingInProgress ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 5/2/2005 17:06'! paintingInProgress: aBoolean paintingInProgress _ aBoolean. ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 2/28/2009 14:29'! projectComment ^ projectInfo at: 'comment' ifAbsent: [''] ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 3/2/2009 12:56'! projectCommentOrTemplate | s | s _ projectInfo at: 'comment' ifAbsent: ['']. s size = 0 ifTrue: [s _ DefaultNotes]. ^ s ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 10/29/2005 10:26'! projectInfo "Answer the project info dictionary." ^ projectInfo ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'ee 6/2/2009 18:55'! projectName ^ self nameFromFileName: projectName ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 10/26/2008 17:44'! projectName: aString projectName _ aString. projectTitleMorph contents: aString. self fixLayout. ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 6/23/2004 09:54'! scratchObjects "Answer a collection of all the scratch objects in the work pane." ^ self workPane submorphs select: [:m | m isKindOf: ScriptableScratchMorph] ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 7/20/2003 12:13'! scriptsPane ^ scriptsPane ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'ee 12/30/2008 13:47'! viewMode ^ viewMode ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 7/20/2003 12:08'! viewerPane ^ viewerPane ! ! !ScratchFrameMorph methodsFor: 'accessing' stamp: 'jm 7/20/2003 12:08'! workPane ^ workPane ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 8/27/2009 14:05'! aboutScratch | dialogBox | dialogBox _ DialogBoxMorph new title: 'About Scratch'; withButtonsForYes: false no: false okay: true cancel: false. dialogBox message: 'Based on Scratch from the MIT Media Lab, v', Version, ' Copyright © 2009 Massachusetts Institute of Technology. All rights reserved. Developed by the Lifelong Kindergarten Group at the MIT Media Lab, with support from the National Science Foundation, Microsoft, Intel, Nokia, and MIT Media Lab research consortia. http://info.scratch.mit.edu/Source_Code ' font: (ScratchFrameMorph getFont: #AboutScratch). dialogBox getUserResponse. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 6/5/2009 14:17'! addServerCommandsTo: menu "Add Scratch server commands to the given menu." | disable endCmd | disable _ true. "make this true to disable this feature" disable ifTrue: [^ self]. menu addLine. (workPane scratchServer notNil and: [workPane scratchServer sessionInProgress]) ifTrue: [ menu add: 'Show IP Address' action: #showNetworkAddress. endCmd _ workPane scratchServer isHosting ifTrue: ['Stop Hosting Mesh'] ifFalse: ['Leave Mesh']. menu add: endCmd action: #exitScratchSession] ifFalse: [ menu add: 'Host Mesh' action: #startHostingScratchSession. menu add: 'Join Mesh' action: #joinScratchSession]. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 12/8/2008 12:03'! addSpriteMorph | result f m el | self world activeHand toolType: nil. self paintingInProgress ifTrue: [^ self beep]. result _ ScratchFileChooserDialog chooseSpriteCostumeFor: self. result = #cancelled ifTrue: [^ self]. (result asLowercase endsWith: '.sprite') ifTrue: [^ self importSpriteOrProject: result]. [f _ Form fromFileNamed: result] ifError: [^ self]. el _ ImageMedia new form: (ScratchFrameMorph scaledFormForPaintEditor: f). m _ ScratchSpriteMorph new soleCostume: el. el mediaName: (m unusedMediaNameFromBaseName: (FileDirectory localNameFor: result)). self addAndView: m. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 3/16/2007 19:12'! allProjectMedia "Answer a collection of all media items in the current project." | result | result _ OrderedCollection new. (workPane submorphs copyWith: workPane) do: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [ result addAll: m media]]. ^ result ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 10/17/2007 12:29'! canonicalizeImagesQuality: qualityOrNil saveOriginal: saveFlag | count unique match | count _ 0. unique _ OrderedCollection new: 1000. self allProjectMedia do: [:m | m isImage ifTrue: [ match _ unique detect: [:u | u form equals: m form] ifNone: [nil]. match ifNil: [ qualityOrNil ifNotNil: [ (m jpegCompressIfPossibleQuality: qualityOrNil saveOriginal: saveFlag) ifTrue: [count _ count + 1]]. unique add: m] ifNotNil: [ m shareFormWith: match]]]. ^ count ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 10/17/2007 13:47'! canonicalizeSoundsBits: bitsPerSample saveOriginal: saveFlag | count unique match | count _ 0. unique _ OrderedCollection new: 1000. self allProjectMedia do: [:m | m isSound ifTrue: [ match _ unique detect: [:u | u sound equals: m sound] ifNone: [nil]. match ifNil: [ bitsPerSample ifNotNil: [ (m compressBitsPerSample: bitsPerSample saveOriginal: saveFlag) ifTrue: [count _ count + 1]]. unique add: m] ifNotNil: [ m shareSoundWith: match]]]. bitsPerSample notNil & saveFlag not ifTrue: [ "uncompress compressed sounds so the result can be heard" self allProjectMedia do: [:m | m isSound ifTrue: [ m decompress]]]. ^ count ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 7/9/2008 18:19'! compressImages | s q count | s _ StringDialog askWithCancel: 'JPEG Quality (10-100)?' initialAnswer: '70'. s size = 0 ifTrue: [^ self]. q _ [s asNumber] ifError: [nil]. q ifNil: [^ self]. q _ (q within: 10 and: 101) truncated. q > 100 ifTrue: [q _ nil]. "just canonicalize, don't compress" count _ self canonicalizeImagesQuality: q saveOriginal: false. scriptsPane categoryChanged: 'Costumes'. DialogBoxMorph inform: 'Images compressed' withDetails: count printString. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'ee 8/8/2008 13:16'! compressSounds | menu bitsPerSample count | menu _ CustomMenu new title: 'Sound quality:'. menu add: 'High (biggest)' action: 5. menu add: 'Normal' action: 4. menu add: 'Low' action: 3. menu add: 'Lowest (smallest)' action: 2. menu addLine. menu add: 'cancel' action: nil. menu localize. (bitsPerSample _ menu startUp) ifNil: [^ self]. count _ self canonicalizeSoundsBits: bitsPerSample saveOriginal: false. scriptsPane categoryChanged: 'Sounds'. DialogBoxMorph inform: 'Sounds compressed' withDetails: count printString. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 7/13/2008 10:27'! developersMenu "Present the Scratch developer's menu." | menu | self world activeHand toolType: nil. Cursor normal show. menu _ CustomMenu new. fillScreenFlag ifTrue: [menu add: 'turn fill screen off' action: #fillScreenOff] ifFalse: [menu add: 'turn fill screen on' action: #fillScreenOn]. UseErrorCatcher ifTrue: [menu add: 'turn error catching off' action: #toggleErrorCatcher] ifFalse: [menu add: 'turn error catching on' action: #toggleErrorCatcher]. menu addLine. menu add: 'save image for end-user' action: #saveImageForEndUser. menu invokeOn: self. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 5/20/2009 16:58'! editMenu: aMenuTitleMorph | menu | menu _ CustomMenu new. menu add: 'Undelete' action: #undoTool. menu addLine. ScratchProcess blockHighlightMSecs <= 1 ifTrue: [menu add: 'Start Single Stepping' action: #toggleSingleStepping] ifFalse: [menu add: 'Stop Single Stepping' action: #toggleSingleStepping]. menu add: 'Set Single Stepping' action: #setSingleStepping. menu addLine. menu add: 'Compress Sounds' action: #compressSounds. menu add: 'Compress Images' action: #compressImages. menu addLine. workPane showMotorBlocks ifTrue: [menu add: 'Hide Motor Blocks' action: #hideMotorBlocks] ifFalse: [menu add: 'Show Motor Blocks' action: #showMotorBlocks]. menu localize. #(3 4 5) do: [:n | menu labels at: n put: ((menu labels at: n) copyFrom: 1 to: (menu labels at: n) size - 1), ScratchTranslator ellipsesSuffix]. menu invokeOn: self at: aMenuTitleMorph bottomLeft + (0@10). ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 10/11/2006 17:56'! editNotes (ScratchNotesDialog editNotesFor: self) getUserResponse. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 8/26/2008 11:50'! enableRemoteSensors "Start running the Scratch server, allowing Scratch and other applications to interact with this Scratch remotely." | server | workPane scratchServer ifNil: [ server _ ScratchServer new userName: 'Scratch'. server stage: workPane. workPane scratchServer: server]. workPane scratchServer startHosting. DialogBoxMorph inform: 'Remote sensor connections enabled' localized. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 2/27/2009 09:04'! exitScratchSession "Close all connections to remote collaborators." workPane scratchServer ifNil: [^ self]. workPane scratchServer endScratchSession. workPane scratchServer: nil. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 10/28/2008 14:53'! exportSprite scriptsPane target exportObject. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 8/27/2009 14:12'! fileMenu: aMenuTitleMorph | menu | menu _ CustomMenu new. menu add: 'New' action: #newScratchProject. menu add: 'Open' action: #openScratchProject. menu add: 'Save' action: #saveScratchProjectNoDialog. menu add: 'Save As' action: #saveScratchProject. menu addLine. menu add: 'Import Project' action: #importScratchProject. menu add: 'Export Sprite' action: #exportSprite. menu addLine. menu add: 'Project Notes' action: #editNotes. Sensor shiftPressed ifTrue: [ "developer menu" menu addLine. menu add: 'Write Project Summary' action: #writeSummaryFile. menu add: 'Write Multiple Project Summaries' action: #writeMultipleSummaries. menu addLine. fillScreenFlag ifTrue: [ menu add: 'Exit User Mode' action: #fillScreenOff] ifFalse: [ menu add: 'Enter User Mode' action: #fillScreenOn. menu add: 'Save Image in User Mode' action: #saveImageForEndUser]]. menu addLine. menu add: 'Quit' action: #quitScratch. menu localize. #(2 4 5 6 7) do: [:n | menu labels at: n put: ((menu labels at: n) copyFrom: 1 to: (menu labels at: n) size - 1), ScratchTranslator ellipsesSuffix]. menu invokeOn: self at: aMenuTitleMorph bottomLeft + (0@10). ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 4/14/2009 09:56'! fillScreenOff "Stop filling the entire screen. Useful during development." Smalltalk fullScreenMode: false. World restoreDisplay. fillScreenFlag _ false. self isSticky: false. self extent: Display extent - 50. UseErrorCatcher _ false. Preferences disable: #noviceMode. Preferences enable: #warnIfNoSourcesFile. Preferences enable: #warnIfNoChangesFile. Preferences insertionPointColor: (Color r: 0.4 g: 1.0 b: 0.0). Preferences textHighlightColor: (Color r: 0.4 g: 1.0 b: 0.0). ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 4/14/2009 09:55'! fillScreenOn "Start filling the entire screen and being sticky. Also configure a few other things for the end user such as turning off halos and the control menu (noviceMode) and making sure that error catching is enabled." TakeOverScreen ifTrue: [ Smalltalk fullScreenMode: true. World restoreDisplay]. fillScreenFlag _ true. self position: 0@0. self isSticky: true. self comeToFront. UseErrorCatcher _ true. Sensor useOSEvents: true. Preferences enable: #noviceMode. Preferences disable: #warnIfNoSourcesFile. Preferences disable: #warnIfNoChangesFile. Preferences insertionPointColor: (Color r: 0.353 g: 0.607 b: 0.788). Preferences textHighlightColor: (Color r: 0.353 g: 0.607 b: 0.788). self updateProjectName. self step. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 5/13/2009 14:37'! getLoginName "Ask the user for their name name and record their answer in 'loginName'." | s | s _ StringDialog askWithCancel: 'User name:' initialAnswer: loginName. s size = 0 ifTrue: [^ '']. loginName _ s. ^ s ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'ee 4/28/2009 19:34'! helpMenu: aMenuTitleMorph | menu | menu _ CustomMenu new. menu add: 'Help Page' action: #launchHelpPage. menu add: 'Help Screens' action: #launchAllHelpScreens. menu addLine. menu add: 'About Scratch' action: #aboutScratch. menu localize. #(1 2 3) do: [:n | menu labels at: n put: ((menu labels at: n) copyFrom: 1 to: (menu labels at: n) size - 1), ScratchTranslator ellipsesSuffix]. menu invokeOn: self at: aMenuTitleMorph bottomLeft + (0@10). ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 2/17/2009 11:33'! hideMotorBlocks workPane showMotorBlocks: false. viewerPane refresh. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 5/14/2009 08:11'! joinScratchSession "Join another Scratch user or a Scratch-compatible remote application." | server addrString ok | server _ ScratchServer new. server stage: workPane. workPane scratchServer: server. addrString _ StringDialog askWithCancel: 'IP address:'. addrString size = 0 ifTrue: [^ self]. ok _ workPane scratchServer joinSessionAt: addrString. ok ifFalse: [DialogBoxMorph warn: 'Could not connect to ', addrString]. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'ee 4/29/2009 13:18'! languageMenu: aToggleButtonMorph "Present a menu of possible languages for blocks." | bullet menu choice | ScratchTranslator canRenderUnicode ifFalse: [ "try to find a Unicode plugin in case this is the first use after startup" ScratchTranslator detectRenderPlugin]. bullet _ UTF8 withAll: '• '. menu _ CustomMenu new. ScratchTranslator languageNames do: [:lang | ((ScratchTranslator isoCodeForName: lang) = (ScratchTranslator currentLanguage)) ifTrue: [menu add: (bullet, lang) action: lang] ifFalse: [menu add: lang action: lang]]. choice _ menu startUp: nil withCaption: nil at: aToggleButtonMorph bottomLeft + (0@10). choice ifNil: [^ self]. self stopAll. self setLanguage: choice. self recordLanguage: (ScratchTranslator currentLanguage). ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 2/23/2009 07:07'! launchAllHelpScreens self launchHelpFile: 'allscreens.html' ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'ee 4/16/2009 12:45'! launchHelpFile: aFilename | helpDir subDir | (FileDirectory default directoryNames includes: 'Help') ifFalse: [^ self beep]. "no help folder" self stopAll. helpDir _ FileDirectory default directoryNamed: 'Help'. "use the English subfolder by default if it exists" (helpDir directoryNames includes: 'en') ifTrue: [subDir _ helpDir directoryNamed: 'en']. "use subfolder for the current language if it exists" (helpDir directoryNames includes: ScratchTranslator currentLanguage) ifTrue: [ subDir _ helpDir directoryNamed: ScratchTranslator currentLanguage]. subDir ifNotNil: [helpDir _ subDir]. (helpDir fileExists: aFilename) ifTrue: [Smalltalk isMacOSX ifTrue: [ScratchPlugin primOpenURL: 'file://', (helpDir pathName, FileDirectory slash, aFilename) encodeForHTTP] ifFalse: [ScratchPlugin primOpenURL: helpDir pathName, FileDirectory slash, aFilename]] ifFalse: [ DialogBoxMorph inform: 'Scratch help file not found.' localized]. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'ee 4/15/2009 11:55'! launchHelpPage self launchHelpFile: 'index.html'. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 12/8/2008 12:04'! newScratchProject "Make a new, blank Scratch project." | response newProject sprite | self closeMediaEditorsAndDialogs ifFalse: [^ self]. self stopAll. (justSaved or: [self projectIsEmpty]) ifFalse: [ "ask the user if they want to save the current project" response _ DialogBoxMorph askWithCancel: 'Save the current project?'. response = #cancelled ifTrue: [^ self]. response ifTrue: [self saveScratchProjectNoDialog. justSaved ifFalse: [^ self]]]. projectDirectory _ ScratchFileChooserDialog getDefaultFolderForType: #project. projectName _ ''. projectInfo _ Dictionary new. newProject _ ScratchStageMorph new. sprite _ ScratchFrameMorph defaultSprite fullCopy. sprite position: (241@181) - sprite extent. newProject addMorph: sprite. self installNewProject: newProject. self initializeWatcherPositions. justSaved _ true. "self enterNormalMode." ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 12/8/2008 12:05'! paintSpriteMorph | m | m _ ScratchSpriteMorph new soleCostume: ImageMedia new. self addAndView: m. m editDrawingOldCostumeName: m costume mediaName deleteOnCancel: true. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 10/27/2008 13:49'! pressGreenFlagButton "Simulate pressing the green flag button when enter key is pressed." flagButton on. World displayWorld. Delay waitMSecs: 100. flagButton off. World displayWorld. self shoutGo. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 3/26/2009 20:48'! quitScratch "Quit from Scratch. Ask the user if they want to save, first." | response | self closeMediaEditorsAndDialogs ifFalse: [^ self]. self stopAll. (justSaved or: [self projectIsEmpty]) ifFalse: [ response _ ScratchCloseDialog new getUserResponse. response = #cancelled ifTrue: [^ self]. response ifTrue: [ self saveScratchProjectNoDialog. justSaved ifFalse: [^ self]]]. Smalltalk snapshot: false andQuit: true. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 6/28/2008 08:48'! renderingMenu ScratchTranslator renderingMenu. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 12/3/2007 21:34'! resaveAllProjects "Resave all the projects in the given directory (to recompress them)." | dir | dir _ ScratchFileChooserDialog chooseFolder: FileDirectory default. dir = #cancelled ifTrue: [^ self]. dir allFileNamesDo: [:fn | ((fn asLowercase endsWith: '.sb') and: [(fn endsWith: '-2.sb') not]) ifTrue: [ self openScratchProjectNamed: fn. World doOneCycleNoInput. " projectName _ (fn copyFrom: 1 to: fn size - 3), '-3.sb'." self writeScratchProject]]. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 6/2/2009 18:54'! saveImageForEndUser (self confirm: ' Close non-Scratch windows and save this image in end-user (fillScreen) mode?') ifFalse: [^ self]. ScratchFrameMorph isXO ifTrue: [Preferences useLargeFonts]. self setLanguage: 'en'. World submorphs do: [:m | (m isKindOf: SystemWindow) ifTrue: [m delete]]. self clearStage. Display newDepth: 32. self fillScreenOn. World doOneCycleNow. Smalltalk snapshot: true andQuit: true. self startup. Sensor useOSEvents: true. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'ee 11/10/2007 19:05'! setSingleStepping "Ask whether script should be single-stepped." | menu mSecs | menu _ CustomMenu new title: 'Single-step speed?'. menu add: 'Turbo speed' action: 0. menu add: 'Normal' action: 1. menu add: 'Flash blocks (fast)' action: 30. menu add: 'Flash blocks (slow)' action: 200. mSecs _ menu localize startUp. mSecs ifNil: [^ self]. ScratchProcess blockHighlightMSecs: mSecs. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 8/3/2008 13:52'! shoutGo "Broadcasts the start event to all objects and processes." self stopAll. workPane broadcastEventNamed: 'Scratch-StartClicked' with: 0. flagButton on. World displayWorldSafely. "force button flash" Delay waitMSecs: 20. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 3/26/2009 22:21'! showMotorBlocks workPane showMotorBlocks: true. viewerPane currentCategory: 'motion'. viewerPane pageViewer vScrollRelative: 1.0.! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 5/14/2009 10:33'! showNetworkAddress "Display my IP address. This is a temporary feature to allow connected multiple Scratch computers in a peer-to-peer configuration without the help of a presence server." | localAddr wanAddr msg | Socket initializeNetwork. localAddr _ NetNameResolver localHostAddress. wanAddr _ nil. " wanAddr _ ScratchServer getIPAddressFromServer." msg _ NetNameResolver stringFromAddress: localAddr. (wanAddr notNil and: [wanAddr ~= localAddr]) ifTrue: [ msg _ msg, String cr, 'Internet: ', (NetNameResolver stringFromAddress: wanAddr)]. DialogBoxMorph inform: msg title: 'IP Address'. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 10/31/2006 20:59'! showSensorBoard | sb | sb _ workPane sensorBoard. sb position: self workPane position + 20. self workPane addMorph: sb. sb tryToOpenPort. World startSteppingSubmorphsOf: sb. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 5/14/2009 10:33'! startHostingScratchSession "Start running the Scratch server, allowing Scratch and other applications to interact with this Scratch remotely." | server | workPane scratchServer ifNil: [ server _ ScratchServer new. server stage: workPane. workPane scratchServer: server]. workPane scratchServer startHosting. self showNetworkAddress. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 11/14/2006 17:58'! stopAll "Tell my workPane to stop everything." | oldJustSaved | oldJustSaved _ justSaved. workPane stopAll. justSaved _ oldJustSaved. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 12/8/2008 12:05'! surpriseSpriteMorph | fileName f el m e | self world activeHand toolType: nil. self paintingInProgress ifTrue: [^ self beep]. fileName _ self nextSurpriseCostumeName. fileName ifNil: [ ^ self addAndView: ScratchFrameMorph defaultSprite fullCopy]. [f _ Form fromFileNamed: fileName] ifError: [^ self]. el _ ImageMedia new form: (ScratchFrameMorph scaledFormForPaintEditor: f). m _ ScratchSpriteMorph new soleCostume: el. el mediaName: (m unusedMediaNameFromBaseName: (FileDirectory localNameFor: fileName)). self addAndView: m. e _ (workPane extent - m extent) abs // 2. m referencePosition: ((e x negated) to: e x) atRandom @ ((e y negated) to: e y) atRandom. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 9/29/2003 23:19'! toggleErrorCatcher UseErrorCatcher _ UseErrorCatcher not. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 3/14/2005 15:13'! toggleSingleStepping "Toggle single stepping." ScratchProcess blockHighlightMSecs <= 1 ifTrue: [ScratchProcess blockHighlightMSecs: 60] ifFalse: [ScratchProcess blockHighlightMSecs: 1]. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 6/14/2007 18:44'! uniqueSummaryFileName | baseName fileName n | baseName _ self projectName. baseName size <= 1 ifTrue: [baseName _ 'newProject']. fileName _ baseName, '-summary.txt'. n _ 1. [FileDirectory default fileExists: fileName] whileTrue: [ fileName _ baseName, n printString, '-summary.txt'. n _ n + 1]. ^ fileName ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 6/14/2007 17:07'! writeMultipleSummaries "Write the summary for all Scratch projects in a given folder." | dir | dir _ ScratchFileChooserDialog chooseFolder: FileDirectory default. dir = #cancelled ifTrue: [^ self]. dir allFileNamesDo: [:fn | (fn asLowercase endsWith: '.scratch') | (fn asLowercase endsWith: '.sb') ifTrue: [ self openScratchProjectNamed: fn. World doOneCycleNoInput. self writeSummaryFile: fn]]. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 6/14/2007 17:06'! writeSummaryFile "Write a summary of this project to a file." self writeSummaryFile: ''. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 5/21/2009 10:28'! writeSummaryFile: fullFileName "Write a summary of this project to a file." | s sprites f fName | s _ WriteStream on: (String new: 10000). s nextPutAll: 'Project: ', self projectName; crlf. fullFileName size > 0 ifTrue: [s nextPutAll: 'Location: ', fullFileName; crlf]. (projectInfo includesKey: 'author') ifTrue: [ s nextPutAll: 'Author: ', (projectInfo at: 'author'); crlf]. (projectInfo includesKey: 'scratch-version') ifTrue: [ s nextPutAll: 'Scratch: ', (projectInfo at: 'scratch-version'); crlf]. (projectInfo includesKey: 'comment') ifTrue: [ s nextPutAll: 'Notes:'; crlf. (projectInfo at: 'comment') lines do: [:l | s nextPutAll: ' ', l; crlf]. s crlf]. (projectInfo includesKey: 'history') ifTrue: [ s nextPutAll: 'History:'; crlf. (projectInfo at: 'history') lines do: [:l | s nextPutAll: ' ', l; crlf]. s crlf]. self writeSummaryTotalsOn: s. s nextPutAll: '--------'; crlf. workPane printSummaryOn: s. sprites _ workPane submorphs select: [:m | m isKindOf: ScratchSpriteMorph]. sprites do: [:m | s skip: -2. "remove last crlf" s nextPutAll: '--------'; crlf. m printSummaryOn: s]. s nextPutAll: '--------'; crlf. ParagraphEditor clipboardTextPut: s contents asText. fName _ fullFileName. fullFileName size = 0 ifTrue: [ fName _ ScratchFileChooserDialog chooseNewFileDefault: self uniqueSummaryFileName title: 'File Name?' type: #projectSummary. fName = #cancelled ifTrue: [^ self]] ifFalse: [ fName _ self uniqueSummaryFileName]. f _ StandardFileStream newScratchFileNamed: fName. f ifNil: [^ self]. f nextPutAll: s contents. f close. ! ! !ScratchFrameMorph methodsFor: 'menu/button actions' stamp: 'jm 3/20/2007 09:50'! writeSummaryTotalsOn: aStream "Write the totals for this project on the given stream." | sprites uniqueCostumes uniqueSounds stackCount | sprites _ workPane submorphs select: [:m | m isKindOf: ScriptableScratchMorph]. sprites _ sprites asArray copyWith: workPane. uniqueCostumes _ IdentitySet new: 100. uniqueSounds _ IdentitySet new: 100. stackCount _ 0. sprites do: [:m | m media do: [:item | item isImage ifTrue: [uniqueCostumes add: item form]. item isSound ifTrue: [uniqueSounds add: item sound]]. stackCount _ stackCount + m blocksBin submorphCount]. aStream nextPutAll: 'Totals: '; crlf. aStream nextPutAll: ' Sprites: ', (sprites size - 1) printString; crlf. aStream nextPutAll: ' Stacks: ', stackCount printString; crlf. aStream nextPutAll: ' Unique costumes: ', uniqueCostumes size printString; crlf. aStream nextPutAll: ' Unique sounds: ', uniqueSounds size printString; crlf. ! ! !ScratchFrameMorph methodsFor: 'geometry' stamp: 'jm 10/26/2008 17:44'! extent: aPoint "Position all my submorphs whenever I get resized." super extent: aPoint. self fixLayout. ! ! !ScratchFrameMorph methodsFor: 'geometry' stamp: 'jm 4/22/2009 08:59'! fixLayout | stageExtent xyReadout w | stageExtent _ workPane isQuarterSize ifTrue: [workPane extent // 2] ifFalse: [workPane extent]. topPane position: self topLeft; width: self width; height: (menuPanel height + 0 max: logoMorph height + 10). stageFrame extent: stageExtent + (14@42); top: topPane bottom; right: self right. workPane position: stageFrame topLeft + (4@37). titlePane position: stageFrame topLeft + (0@1); width: stageFrame width - 6; height: 36. self fixProjectTitleMorphLayout. scriptsPane fixLayout. w _ (viewerPane catButtonsExtent x + 17) within: 40 and: (self width - (scriptsPane bareMinimumWidth + stageFrame width)). viewerPane position: topPane bottomLeft; width: w; height: self bottom - topPane bottom. scriptsPane position: viewerPane topRight; width: self width - (stageFrame width + viewerPane width); height: self bottom - topPane bottom; fixLayout. libraryPane position: stageFrame bottomLeft; width: (self right - scriptsPane right); height: self bottom - libraryPane top. menuPanel left: logoMorph right + 18; top: topPane top + ((topPane height - menuPanel height) // 2) + 2. viewModeButtonsPanel right: stageFrame right - 8; top: self top + 7. stageButtonsPanel position: (stageFrame left + 10)@(topPane bottom + 5); width: stageFrame width - 28; height: (workPane top - stageFrame top) - 8. xyReadout _ readoutPane submorphs at: 1. readoutPane width: xyReadout width + 23; height: xyReadout height + 15; position: stageFrame bottomRight - ((readoutPane width + 6)@3). xyReadout position: readoutPane position + (18@5). toolbarPanel left: (stageFrame left - 4 max: menuPanel right); top: self top + ((topPane height - toolbarPanel height) // 2) + 3. ((toolbarPanel right - 5) > viewModeButtonsPanel left) ifTrue: [toolbarPanel delete] ifFalse: [ (toolbarPanel owner = self) ifFalse: [ self addMorphFront: toolbarPanel]]. ! ! !ScratchFrameMorph methodsFor: 'geometry' stamp: 'jm 12/23/2008 13:50'! hidePalette: hidePalette "Hide or show the blocks palette." hidePalette = viewerPane owner isNil ifTrue: [^ self]. "no change" hidePalette ifTrue: [ viewerPane delete. scriptsPane initFrontFromForm: (ScratchFrameMorph skinAt: #blocksPaletteFrameTransparent2) topSectionHeight: 90; middleBarLeftMargin: 5 rightMargin: 0] ifFalse: [ self addMorph: viewerPane. scriptsPane initFrontFromForm: (ScratchFrameMorph skinAt: #scriptPaneFrameTransparent2) topSectionHeight: 90; middleBarLeftMargin: 0 rightMargin: 0]. scriptsPane color: (Color r: 149/255 g: 154/255 b: 159/255). ! ! !ScratchFrameMorph methodsFor: 'drawing' stamp: 'jm 12/9/2008 18:27'! areasRemainingToFill: aRectangle "Drawing optimization. Since I completely fill my bounds with opaque pixels, this method tells Morphic that it isn't necessary to draw any morphs covered by me." ^ aRectangle areasOutside: self bounds ! ! !ScratchFrameMorph methodsFor: 'drawing' stamp: 'jm 4/27/2005 17:29'! drawOn: aCanvas "Optimization: Don't draw myself at all since I am completely tiled." ! ! !ScratchFrameMorph methodsFor: 'drawing' stamp: 'jm 2/3/2009 19:17'! fullDrawOn: aCanvas "Draw my full Morphic structure on the given Canvas." "Optimization: if damage is entirely contained in a given pane, draw only that pane." | damageR stageR | damageR _ aCanvas clipRect. stageR _ workPane bounds. workPane isQuarterSize ifTrue: [ stageR _ workPane position extent: (workPane width // 2) @ (workPane height // 2)]. (stageR containsRect: damageR) ifTrue: [ workPane fullDrawOn: aCanvas. ^ self]. (scriptsPane bounds containsRect: damageR) ifTrue: [ scriptsPane fullDrawOn: aCanvas. ^ self]. (viewerPane bounds containsRect: damageR) ifTrue: [ viewerPane fullDrawOn: aCanvas. ^ self]. (readoutPane bounds containsRect: damageR) ifTrue: [ readoutPane fullDrawOn: aCanvas. ^ self]. (libraryPane bounds containsRect: damageR) ifTrue: [ libraryPane fullDrawOn: aCanvas. ^ self]. super fullDrawOn: aCanvas. ! ! !ScratchFrameMorph methodsFor: 'event handling' stamp: 'jm 3/16/2007 16:53'! handlesMouseDown: evt ^ true ! ! !ScratchFrameMorph methodsFor: 'event handling' stamp: 'jm 8/27/2009 14:15'! mouseDown: evt "Revert to normal cursor." evt hand toolType: nil. (evt cursorPoint y - self top) < topPane height ifTrue: [ fillScreenFlag ifFalse: [evt hand grabMorph: self]]. ! ! !ScratchFrameMorph methodsFor: 'event handling' stamp: 'jm 10/2/2006 15:48'! wantsKeyboardFocusFor: aSubmorph "Don't allow shift-click edit and select in random label strings." ^ false ! ! !ScratchFrameMorph methodsFor: 'stepping' stamp: 'jm 6/25/2009 17:13'! checkForWeDo "Check for WeDo, and show motor blocks if it is found." "Note: Polling on Vista can take several hundred milliseconds, so reduce polling to just a few times per minute." | now | now _ Time millisecondClockValue. (lastWeDoPoll isNil or: [lastWeDoPoll > now]) ifTrue: [lastWeDoPoll _ 0]. ((now - lastWeDoPoll) < 15000) ifTrue: [^ self]. "don't poll too often" lastWeDoPoll _ now. WeDoPlugin readInputs. WeDoPlugin isOpen ifTrue: [ workPane showMotorBlocks ifTrue: [^ self]. self showMotorBlocks. WeDoPlugin readInputs]. ! ! !ScratchFrameMorph methodsFor: 'stepping' stamp: 'jm 12/31/2006 15:59'! processDroppedFiles "Process any files that have been dropped onto me." | droppedFiles m dropPoint fName | droppedFiles _ FileStream droppedFiles. droppedFiles size = 0 ifTrue: [^ self]. (m _ self viewerPane target) ifNil: [^ self]. dropPoint _ droppedFiles first. (droppedFiles copyFrom: 2 to: droppedFiles size) do: [:file | file close. fName _ file fullName. ((fName asLowercase endsWith: '.scratch') | (fName asLowercase endsWith: '.sb')) ifTrue: [self openScratchDroppedProjectNamed: fName] ifFalse: [ (fName asLowercase endsWith: '.sprite') ifTrue: [self importSpriteOrProject: fName] ifFalse: [m importMedia: fName]]]. ! ! !ScratchFrameMorph methodsFor: 'stepping' stamp: 'jm 12/9/2008 23:01'! processKeyboardEvents | evt ch | World hands do: [:h | [(evt _ h nextUnclaimedKeystrokeOrNil) notNil] whileTrue: [ ch _ evt keyValue. evt commandKeyPressed ifTrue: [ch _ ch \\ 32]. "map cmd/alt keys to control keys" (ch = 3) | (ch = 13) ifTrue: [^ self pressGreenFlagButton]. ch = 15 ifTrue: [^ self openScratchProject]. ch = 17 ifTrue: [^ self quitScratch]. ch = 19 ifTrue: [^ self saveScratchProjectNoDialog]. ch = 27 ifTrue: [ TakeOverScreen ifTrue: [ Smalltalk fullScreenMode: false. Smalltalk fullScreenMode: true. World restoreDisplay]. ^ self]. workPane broadcastEventNamed: 'Scratch-KeyPressedEvent' with: evt]]. ! ! !ScratchFrameMorph methodsFor: 'stepping' stamp: 'jm 6/4/2009 12:33'! processWhenConditions "Trigger any 'when ' hats." | objList | true ifTrue: [^ self]. "disabled" objList _ workPane submorphs select: [:m | m isKindOf: ScriptableScratchMorph]. objList _ objList copyWith: workPane. objList do: [:obj | obj scripts do: [:hat | (hat isMemberOf: WhenHatBlockMorph) ifTrue: [ (hat hasRunningProcess not and: [hat evaluateCondition]) ifTrue: [ hat start; layoutChanged]]]]. ! ! !ScratchFrameMorph methodsFor: 'stepping' stamp: 'jm 5/20/2009 16:59'! step "Run each process until it gives up control, then filter out any processes that have terminated." | screenExtent oldJustSaved | fillScreenFlag ifTrue: [ screenExtent _ Display extent. ((self position = (0@0)) and: [self extent = screenExtent]) ifFalse: [ oldJustSaved _ justSaved. self position: 0@0. self extent: screenExtent. self enterQuarterModeIfSmallScreen. scriptsPane currentCategory: scriptsPane currentCategory. justSaved _ oldJustSaved. ^ self]]. workPane ifNotNil: [ ScriptableScratchMorph scratchOrigin: workPane center. viewerPane target isNil ifTrue: [workPane viewBlocksAndScripts] ifFalse: [viewerPane target isInWorld ifFalse: [workPane viewBlocksAndScripts]]]. Sensor processOSMenuEvents. paintingInProgress ifTrue: [^ self]. workPane scratchServer ifNotNil: [workPane scratchServer stepServer]. self checkForWeDo. self updateToolButtons. self processWhenConditions. self processKeyboardEvents. workPane stepProcesses. workPane scratchServer ifNotNil: [workPane scratchServer stepServer]. self processDroppedFiles. workPane processesToRun size > 0 ifTrue: [flagButton on] ifFalse: [flagButton off]. ! ! !ScratchFrameMorph methodsFor: 'stepping' stamp: 'jm 7/9/2003 12:02'! stepTime "Every screen update cycle." ^ 0 ! ! !ScratchFrameMorph methodsFor: 'stepping' stamp: 'jm 4/22/2009 08:36'! updateToolButtons "Update the highlighting of my tool buttons." | toolButtons currentTool | Sensor anyButtonPressed ifTrue: [^ self]. "don't update if mouse button pressed" toolButtons _ toolbarPanel submorphs select: [:m | (m isKindOf: ToggleButton) and: [m actionSelector endsWith: 'Tool']]. currentTool _ (World activeHand toolType ifNil: ['none']) asLowercase. toolButtons do: [:b | (b actionSelector asLowercase = currentTool) ifTrue: [b on] ifFalse: [b off]]. ! ! !ScratchFrameMorph methodsFor: 'dropping/grabbing' stamp: 'ee 5/14/2008 12:55'! wantsDroppedMorph: aMorph event: evt ^ (aMorph isKindOf: BlockMorph) or: [ (aMorph isKindOf: ScriptableScratchMorph) or: [ (aMorph isKindOf: ScratchCommentMorph)]]. ! ! !ScratchFrameMorph methodsFor: 'view mode' stamp: 'jm 7/25/2006 11:46'! closeDialogBoxes "Close all dialog boxes, including PaintEditors and SoundRecorders." World submorphs do: [:m | (m isKindOf: DialogBoxMorph) ifTrue: [m cancelled; delete]]. ! ! !ScratchFrameMorph methodsFor: 'view mode' stamp: 'jm 4/13/2009 21:05'! enterNormalMode "Go into normal (full-stage) mode." (viewMode = #normal) ifTrue: [ self updateViewModeButtons. ^ self]. viewMode _ #normal. workPane isQuarterSize: false. workPane isInWorld ifTrue: [self fixLayout] ifFalse: [self exitPresentationMode]. self updatePanes. self updateViewModeButtons. ! ! !ScratchFrameMorph methodsFor: 'view mode' stamp: 'jm 6/22/2009 12:54'! enterPresentationMode "Go into presentation mode." | presenter | ScratchPlugin pluginAvailable ifFalse: [ self updateViewModeButtons. ^ self beep]. (viewMode = #presentation) ifTrue: [^ self]. lastViewMode _ viewMode. viewMode _ #presentation. self closeDialogBoxes. workPane isQuarterSize: false. presenter _ ScratchPresenterMorph new frame: self. self delete. Display fillBlack. Smalltalk fullScreenMode: true. World restoreDisplay. Display fillBlack. World assuredCanvas. "re-allocate canvas after entering full-screen mode" ((Display width >= 965) & (Display height >= 750)) ifTrue: [presenter beDoubleSize]. presenter extent: Display extent. World addMorphFront: presenter. World startSteppingSubmorphsOf: presenter. World activeHand newKeyboardFocus: nil. self updatePenPositions. self updateViewModeButtons. World assuredCanvas. "re-allocate canvas after entering full-screen mode" World fullRepaintNeeded. World displayWorldSafely. ! ! !ScratchFrameMorph methodsFor: 'view mode' stamp: 'jm 4/13/2009 21:01'! enterQuarterMode "Go into quarter stage mode." (viewMode = #quarter) ifTrue: [ self updateViewModeButtons. ^ self]. viewMode _ #quarter. workPane isQuarterSize: true. workPane isInWorld ifTrue: [self fixLayout] ifFalse: [self exitPresentationMode]. self updatePanes. self updateViewModeButtons. ! ! !ScratchFrameMorph methodsFor: 'view mode' stamp: 'jm 6/22/2009 12:56'! exitPresentationMode "Exit presentation mode." TakeOverScreen ifFalse: [ Smalltalk fullScreenMode: false. World restoreDisplay]. ScriptableScratchMorph doubleSize: false. self addMorphFront: workPane. self fixLayout. World addMorphFront: self. World startSteppingSubmorphsOf: self. World fullRepaintNeeded. self updatePenPositions. lastViewMode = #normal ifTrue: [^ self enterNormalMode]. lastViewMode = #quarter ifTrue: [^ self enterQuarterMode]. ! ! !ScratchFrameMorph methodsFor: 'view mode' stamp: 'jm 3/2/2009 12:56'! projectComment: aString aString = DefaultNotes ifTrue: [ projectInfo removeKey: 'comment' ifAbsent: []. ^ self]. projectInfo at: 'comment' put: aString asString. ! ! !ScratchFrameMorph methodsFor: 'view mode' stamp: 'jm 11/15/2006 17:10'! updatePenPositions "Update the pen positions of my sprites when going between normal and presentation mode." | stage | stage _ self workPane. ScriptableScratchMorph scratchOrigin: stage center. stage submorphsDo: [:m | stage updatePenPositionFor: m]. ! ! !ScratchFrameMorph methodsFor: 'view mode' stamp: 'ee 12/5/2008 17:27'! updateViewModeButtons viewModeButtons do: [:b | b off]. viewModeButtons do: [:b | (b actionSelector = #enterQuarterMode and: [viewMode = #quarter]) ifTrue: [b on]. (b actionSelector = #enterNormalMode and: [viewMode = #normal]) ifTrue: [b on]. (b actionSelector = #enterPresentationMode and: [viewMode = #presentation]) ifTrue: [b on]]. ! ! !ScratchFrameMorph methodsFor: 'other' stamp: 'ee 4/23/2008 22:01'! addAndView: aSpriteMorph "Add given morph to the work pane and view it." | pos i p | aSpriteMorph center: workPane center. pos _ self scratchObjects collect: [:o | o referencePosition]. i _ 0. [pos includes: (p _ (10 * i) asPoint)] whileTrue: [i _ i + 1]. workPane addMorphFront: aSpriteMorph. aSpriteMorph objName: aSpriteMorph nextInstanceName. aSpriteMorph referencePosition: p. aSpriteMorph startStepping. workPane sprites addLast: aSpriteMorph. self view: aSpriteMorph tab: 'Scripts' category: 'motion'. ! ! !ScratchFrameMorph methodsFor: 'other' stamp: 'ee 7/1/2008 11:09'! closeMediaEditorsAndDialogs "Close any open paint or sound editors, asking the user first to avoid losing edits. Answer true if all are closed." | mList mHasCancel | mList _ PaintFrame allInstances select: [:m | m isInWorld]. mList size > 0 ifTrue: [ (DialogBoxMorph ask: 'Close paint editor?') ifFalse: [^ false]. mList do: [:m | m cancelled; delete]. paintingInProgress _ false]. mList _ ScratchSoundRecorderDialogMorph allInstances select: [:m | m isInWorld]. mList size > 0 ifTrue: [ (DialogBoxMorph ask: 'Close sound recorder?') ifFalse: [^ false]. mList do: [:m | m cancelled; delete]]. mList _ DialogBoxMorph allInstances select: [:m | m isInWorld]. mList size > 0 ifTrue: [ (DialogBoxMorph ask: 'Close dialog?') ifFalse: [^ false]. mList do: [:m | mHasCancel _ false. m buttons do: [:b | b action = #cancelled ifTrue: [mHasCancel _ true]]. mHasCancel ifTrue: [m cancelled; delete] ifFalse: [m no; delete]]]. DialogBoxMorph subclassesDo: [:c | mList _ c allInstances select: [:m | m isInWorld]. mList size > 0 ifTrue: [ (DialogBoxMorph ask: 'Close dialog?') ifFalse: [^ false]. mList do: [:m | mHasCancel _ false. m buttons do: [:b | b action = #cancelled ifTrue: [mHasCancel _ true]]. mHasCancel ifTrue: [m cancelled; delete] ifFalse: [m no; delete]]]]. "subclass of a subclass of DialogBoxMorph" mList _ NewVariableDialog allInstances select: [:m | m isInWorld]. mList size > 0 ifTrue: [ (DialogBoxMorph ask: 'Close dialog?') ifFalse: [^ false]. mList do: [:m | m cancelled; delete]]. ^ true ! ! !ScratchFrameMorph methodsFor: 'other' stamp: 'jm 2/24/2004 18:57'! delete World activeHand toolType: nil. super delete. ! ! !ScratchFrameMorph methodsFor: 'other' stamp: 'jm 10/21/2005 18:45'! mouseX ^ workPane mouseX ! ! !ScratchFrameMorph methodsFor: 'other' stamp: 'jm 10/21/2005 18:46'! mouseY ^ workPane mouseY ! ! !ScratchFrameMorph methodsFor: 'other' stamp: 'ee 11/4/2008 11:46'! newSound "Open the dialog to record a new sound." scriptsPane tabPane currentTab: 'Sounds'. viewerPane target recordSound.! ! !ScratchFrameMorph methodsFor: 'other' stamp: 'jm 8/2/2005 19:17'! openMIDI "Prompt the user to select a MIDI port number, then open it." workPane openMIDI. ! ! !ScratchFrameMorph methodsFor: 'other' stamp: 'jm 6/22/2009 12:46'! presentHelpScreen: aStringOrNil "Look for a help screen with the given name in the 'Help' folder. If found, present it to the user." | helpDir subDir fileNames helpFileName helpForm | aStringOrNil ifNil: [^ self beep]. (FileDirectory default directoryNames includes: 'Help') ifFalse: [^ self beep]. "no help folder" helpDir _ FileDirectory default directoryNamed: 'Help'. "use the English subfolder by default if it exists" (helpDir directoryNames includes: 'en') ifTrue: [subDir _ helpDir directoryNamed: 'en']. "use subfolder for the current language if it exists" (helpDir directoryNames includes: ScratchTranslator currentLanguage) ifTrue: [ subDir _ helpDir directoryNamed: ScratchTranslator currentLanguage]. subDir ifNotNil: [helpDir _ subDir]. fileNames _ helpDir fileNames collect: [:s | s asLowercase]. helpFileName _ nil. #(hlp gif png jpg bmp) do: [:ext | helpFileName ifNil: [ helpFileName _ aStringOrNil, '.', ext. (fileNames includes: helpFileName asLowercase) ifFalse: [helpFileName _ nil]]]. helpFileName ifNil: [^ self beep]. World doOneCycle. "update cursor before fetching helpForm" [helpForm _ Form fromFileNamed: (helpDir fullNameFor: helpFileName)] ifError: [^ self beep]. HelpDialog showForm: helpForm. ! ! !ScratchFrameMorph methodsFor: 'other' stamp: 'jm 11/13/2003 20:42'! projectDirectory projectDirectory ifNil: [^ FileDirectory default]. ^ projectDirectory ! ! !ScratchFrameMorph methodsFor: 'other' stamp: 'jm 11/14/2006 17:41'! projectModified "Record that the current project has changed since it was last saved." justSaved _ false. ! ! !ScratchFrameMorph methodsFor: 'other' stamp: 'ee 4/14/2008 15:01'! updateMediaCategoryFor: anObject "Update the media viewer for the given object's media category. Do nothing if the media category of the given object is not being viewed." scriptsPane target = anObject ifTrue: [ scriptsPane categoryChanged: 'Sounds'. scriptsPane categoryChanged: 'Costumes']. viewerPane target = anObject ifTrue: [ viewerPane categoryChanged: 'Sound']. ! ! !ScratchFrameMorph methodsFor: 'other' stamp: 'ee 11/4/2008 11:45'! view: aMorph tab: t category: c "Add given morph to the work pane and view it." scriptsPane target: aMorph. scriptsPane tabPane currentTab: t. viewerPane target: aMorph; currentCategory: c. ! ! !ScratchFrameMorph methodsFor: 'startup' stamp: 'jm 9/23/2009 13:34'! processSettingsFile "Process settings from the Scratch.ini file." | lang settings k | self class setVisibleDrives: nil. lang _ nil. ScratchFileChooserDialog clearFolderCache. "clear homeDir and last folder cache" settings _ self readSettingsFile. settings associationsDo: [:assoc | k _ assoc key. k = 'language' ifTrue: [lang _ assoc value]. k = 'home' ifTrue: [ScratchFileChooserDialog setHomeDir: assoc value]. k = 'visibledrives' ifTrue: [self class setVisibleDrives: assoc value]]. lang ifNil: [lang _ ScratchTranslator guessLanguage]. self setLanguage: lang. ! ! !ScratchFrameMorph methodsFor: 'startup' stamp: 'jm 3/2/2009 12:56'! readDefaultNotes "If there is a file named 'defaultNotes.txt' in the Scratch folder, read it in." | dir | DefaultNotes _ ''. dir _ FileDirectory default. (dir fileExists: 'defaultNotes.txt') ifTrue: [ DefaultNotes _ (FileStream oldFileNamed: 'defaultNotes.txt') contentsOfEntireFile]. ! ! !ScratchFrameMorph methodsFor: 'startup' stamp: 'jm 3/19/2009 10:06'! readSettingsFile "Read my settings file and answer a Dictionary of settings." "ScratchFrameMorph new readSettingsFile" | f dict s tokens k | f _ FileStream readOnlyFileNamedOrNil: 'Scratch.ini'. f ifNil: [^ Dictionary new]. dict _ Dictionary new. f contentsOfEntireFile lines do: [:line | s _ line collect: [:c | (c asciiValue < 32) ifTrue: [Character space] ifFalse: [c]]. tokens _ s findTokens: '='. k _ tokens first withBlanksTrimmed asLowercase. tokens size = 2 ifTrue: [dict at: k put: tokens second withBlanksTrimmed] ifFalse: [dict at: k put: '1']]. ^ dict ! ! !ScratchFrameMorph methodsFor: 'startup' stamp: 'ee 8/11/2008 22:09'! setDefaultSprite "Look for default sprite in Media directory. If none found, use the DefaultCatSprite" | d f data importedProject fName | DefaultSprite _ nil. "if dfault.sprite exists, use that" d _ ScratchFileChooserDialog getDefaultFolderForType: #costume. (d fileExists: 'default.sprite') ifTrue: [ f _ (FileStream readOnlyFileNamed: (d fullNameFor: 'default.sprite')) binary. f ifNotNil: [ data _ f contentsOfEntireFile. importedProject _ [self extractProjectFrom: data] ifError: [nil]. importedProject ifNil: [^ self]. importedProject submorphs do: [:m | (m isKindOf: ScratchSpriteMorph) ifTrue: [DefaultSprite _ m]. ^ self]]]. "if default image exists, use the image and add 'pop' sound" #(gif png jpg bmp) do: [:e | fName _ 'default.', e. (d fileExists: fName) ifTrue: [ DefaultSprite _ ScratchSpriteMorph new importMedia: (d fullNameFor: fName); addMediaItem: (SoundMedia new mediaName: 'pop' localized; sound: ScratchSpriteMorph popSound). ^ self]]. ! ! !ScratchFrameMorph methodsFor: 'startup' stamp: 'jm 9/23/2009 13:30'! startup | startupFileNames fileName arg presentationMode | HostSystemMenus startUp. HostSystemMenus menuBarControler reviseHostMenus. ScriptableScratchMorph randomInit. ScratchTranslator detectRenderPlugin. ScratchTranslator importLanguagesList. self processSettingsFile. self readDefaultNotes. self updateProjectName. shuffledCostumeNames _ nil. author _ ''. loginName _ ''. loginPassword _ ''. justSaved _ true. presentationMode _ false. startupFileNames _ InputSensor startupFileNames asOrderedCollection. 2 to: 10 do: [:i | arg _ Smalltalk getSystemAttribute: i. (arg notNil and: [arg size > 0]) ifTrue: [ startupFileNames addLast: (ScratchPlugin primShortToLongPath: arg)]]. startupFileNames do: [:n | (n asLowercase = 'presentation') ifTrue: [presentationMode _ true]. (n asLowercase = 'fullscreen') ifTrue: [TakeOverScreen _ true]]. TakeOverScreen ifTrue: [ Smalltalk fullScreenMode: true. World restoreDisplay]. self enterQuarterModeIfSmallScreen. fileName _ startupFileNames detect: [:fn | (fn asLowercase endsWith: '.sb') or: [fn asLowercase endsWith: '.scratch']] ifNone: [nil]. fileName ifNotNil: [ presentationMode ifTrue: [Display fillColor: Color black]. self openScratchProjectNamed: fileName. presentationMode ifTrue: [self enterPresentationMode; shoutGo]. ^ self]. viewerPane currentCategory: 'motion'. self setDefaultSprite. self newScratchProject. fileName _ startupFileNames detect: [:fn | fn asLowercase endsWith: '.sprite'] ifNone: [^ self]. "open a .sprite file" workPane submorphs do: [:m | (m isKindOf: ScratchSpriteMorph) ifTrue: [m deleteSprite]]. self importSpriteOrProject: fileName. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'jm 6/2/2009 18:54'! clearStage self stopAll. projectDirectory _ ScratchFileChooserDialog getDefaultFolderForType: #project. projectName _ ''. projectInfo _ Dictionary new. self installNewProject: ScratchStageMorph new. self initializeWatcherPositions. justSaved _ true. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'jm 7/30/2008 17:12'! extractInfoFrom: aByteArray "Answer a Scratch info dictionary from the given ByteArray. Answer an empty dictionary if it is an old project." | s version | s _ ReadStream on: aByteArray. version _ ObjStream scratchFileVersionFrom: (s next: 10) asString. (version = 1) | (version = 2) ifTrue: [ s skip: 4. "skip info header byte count" ^ ObjStream new readObjFrom: s showProgress: false] ifFalse: [^ Dictionary new]. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'jm 7/31/2008 20:03'! extractProjectFrom: aByteArray "Answer a Scratch project (i.e. a ScratchStageMorph possibly containing sprites) from the given ByteArray. Answer nil if the project cannot be unpacked." | s version proj | s _ ReadStream on: aByteArray. version _ ObjStream scratchFileVersionFrom: (s next: 10) asString. version = 0 ifTrue: [ s position: 0. proj _ ObjStream new readObjFrom: s showProgress: true]. (version = 1) | (version = 2) ifTrue: [ s skip: s uint32. "skip header" proj _ ObjStream new readObjFrom: s showProgress: true]. proj class = ScratchStageMorph ifFalse: [ version > 2 ifTrue: [self error: 'Project created by a later version of Scratch'] ifFalse: [self error: 'Problem reading project.']. ^ nil]. ScriptableScratchMorph buildBlockSpecDictionary. proj allMorphsDo: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [ "covert to new blocks" m convertStacksToTuples. m convertTuplesToStacks]]. ^ proj ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'jm 5/8/2007 11:04'! fixByteReversedSounds "See if the current project contains any byte-reversed copies of one of the standard sounds. If it does, byte-reverse all sounds. This is a workaround for a bug in Scratch 1.0 that did not upload sounds in big-endian order." | allSoundBuffers badMeowPrefix badPopPrefix foundBadSound | allSoundBuffers _ IdentitySet new. self allProjectMedia do: [:media | media isSound ifTrue: [ allSoundBuffers add: media sound samples]]. allSoundBuffers remove: ScriptableScratchMorph meowSound samples ifAbsent: []. allSoundBuffers remove: ScriptableScratchMorph popSound samples ifAbsent: []. badMeowPrefix _ ScriptableScratchMorph oldMeowPrefixReversed. badPopPrefix _ (ScriptableScratchMorph popSound samples copyFrom: 1 to: 100) reverseEndiannessStereo: false. foundBadSound _ false. allSoundBuffers do: [:buf | foundBadSound ifFalse: [ ((buf beginsWith: badMeowPrefix) or: [buf beginsWith: badPopPrefix]) ifTrue: [foundBadSound _ true]]]. foundBadSound ifTrue: [ allSoundBuffers do: [:buf | buf reverseEndiannessStereo: false]]. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'ee 6/12/2008 12:01'! importScratchProject "Allow the user to select a project to open, then merge that project's sprites with the current project." | response | self closeMediaEditorsAndDialogs ifFalse: [^ self]. self stopAll. response _ ScratchFileChooserDialog chooseExistingFileType: #project extensions: #(scratch sb) title: 'Import Project'. response ifNil: [^ self]. self importSpriteOrProject: response. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'jm 6/2/2009 18:57'! installNewProject: newWorkpane "Called after creating or reading a new project to clear the process scheduler, pick an object to view, clear the library thumbnails, and perform other housekeeping." | viewTarget sb | self stopAll. newWorkpane class = ScratchStageMorph ifFalse: [^ self inform: 'Incompatible Scratch file format']. "self exitScratchSession." workPane scratchServer ifNotNil: [ workPane scratchServer clearCaches. workPane scratchServer stage: newWorkpane. newWorkpane scratchServer: workPane scratchServer]. newWorkpane isQuarterSize: workPane isQuarterSize. newWorkpane bounds: workPane bounds. newWorkpane midiPortNum: workPane midiPortNum. workPane closeMIDI. "use the same sensorboard for the new project" sb _ workPane sensorBoard. newWorkpane submorphs do: [:m | (m isKindOf: SensorBoardMorph) ifTrue: [ sb position: m position. newWorkpane replaceSubmorph: m by: sb. sb tryToOpenPort]]. newWorkpane sensorBoard: sb. workPane owner replaceSubmorph: workPane by: newWorkpane. workPane _ newWorkpane. self fixByteReversedSounds. "fix sprite positions (backward compatability)" workPane submorphs do: [:m | (m isKindOf: WatcherMorph) ifTrue: [m convertFromOldWatcher]. (m respondsTo: #costume) ifTrue: [ m position: m position + m costume rotationCenter]. "fix up positions" m layoutChanged]. workPane layoutChanged. "reset timer" ScriptableScratchMorph resetTimer. "pick an object view, or view the background if there is no other" viewTarget _ workPane. workPane submorphs do: [:m | (m respondsTo: #scripts) ifTrue: [ m scripts size >= viewTarget scripts size ifTrue: [viewTarget _ m]]]. viewTarget viewBlocksAndScripts. "populate the sprites list if it is empty (backward compatability)" workPane sprites isEmpty ifTrue: [ workPane submorphs do: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [workPane sprites addLast: m]]]. scriptsPane tabPane currentTab: 'Scripts'. libraryPane clearLibrary. workPane clearPenTrails. self updateProjectName. ScratchProcess blockHighlightMSecs: 1. ScratchPrompterMorph clearLastAnswer. (projectInfo at: 'isHosting' ifAbsent: [false]) ifTrue: [ self enableRemoteSensors]. (projectInfo at: 'hasMotorBlocks' ifAbsent: [false]) ifTrue: [ self showMotorBlocks]. (projectInfo includesKey: 'penTrails') ifTrue: [ workPane penTrailsForm: (projectInfo at: 'penTrails')]. Clipboard _ nil. World cleanseStepList. "make sure garbage collect can clean up the old sprites" Smalltalk garbageCollect. "get rid of old sprite instances" self world ifNotNil: [self world startSteppingSubmorphsOf: self]. ScriptableScratchMorph scratchOrigin: workPane center. justSaved _ true. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'jm 8/11/2008 16:31'! nameFromFileName: fileName "Return the given Scratch file name without the trailing .sb or .scratch extension, if it has one. Ensure the the result is UTF8." | s | s _ fileName. (s asLowercase endsWith: '.scratch') ifTrue: [s _ s copyFrom: 1 to: s size - 8]. (s asLowercase endsWith: '.sb') ifTrue: [s _ s copyFrom: 1 to: s size - 3]. s isUnicode ifFalse: [s _ UTF8 withAll: s]. ^ s ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'ee 6/12/2008 12:02'! openScratchDroppedProjectNamed: fName "Open a Scratch project with the given name that was dropped on the Scratch window." | response | self closeMediaEditorsAndDialogs ifFalse: [^ self]. self stopAll. (justSaved or: [self projectIsEmpty]) ifFalse: [ "ask the user if they want to save the current project" response _ DialogBoxMorph askWithCancel: 'Save the current project?'. response = #cancelled ifTrue: [^ self]. response ifTrue: [self saveScratchProjectNoDialog]]. self openScratchProjectNamed: fName. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'ee 6/12/2008 12:02'! openScratchProject "Allow the user to select a project to open, then open that project." | response newProj | self closeMediaEditorsAndDialogs ifFalse: [^ self]. self stopAll. (justSaved or: [self projectIsEmpty]) ifFalse: [ "ask the user if they want to save the current project" response _ DialogBoxMorph askWithCancel: 'Save the current project?'. response = #cancelled ifTrue: [^ self]. response ifTrue: [self saveScratchProjectNoDialog]]. response _ ScratchFileChooserDialog openScratchFileFor: self. response = #cancelled ifTrue: [^ self]. (response isKindOf: String) ifTrue: [ "read the contents of a local file" ^ self openScratchProjectNamed: response]. (response isKindOf: ByteArray) ifTrue: [ [projectInfo _ self extractInfoFrom: response] ifError: [projectInfo _ Dictionary new]. [newProj _ self extractProjectFrom: response] ifError: [^ self]. self installNewProject: newProj. projectDirectory _ ScratchFileChooserDialog getDefaultFolderForType: #project]. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'jens 11/18/2008 10:16'! openScratchProjectNamed: fName "Open a Scratch project with the given name." | f projData newProj dir fn| self closeMediaEditorsAndDialogs ifFalse: [^ self]. fn _ fName. f _ FileStream readOnlyFileNamedOrNil: fn. f ifNil: ["try a different encoding, fixes a Firefox bug, -Jens" fn _ fName isoLatinToMac asUTF8. f _ FileStream readOnlyFileNamedOrNil: fn. f ifNil: [^ self inform: 'Could not read' withDetails: fName]]. [ projData _ f binary contentsOfEntireFile. newProj _ self extractProjectFrom: projData. projectInfo _ self extractInfoFrom: projData. ] ifError: [:err :rcvr | ^ self inform: 'Could not read project; file may be damaged' withDetails: '(', err, ')']. dir _ FileDirectory dirPathFor: fn. projectDirectory _ FileDirectory on: dir. ScratchFileChooserDialog setLastFolderTo: projectDirectory forType: #project. projectName _ FileDirectory localNameFor: fn. self installNewProject: newProj. self initializeWatcherPositions. viewerPane updateContents. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'ee 6/12/2008 12:02'! saveScratchProject | fName result | self closeMediaEditorsAndDialogs ifFalse: [^ self]. self stopAll. fName _ ScratchFileChooserDialog saveScratchFileFor: self. (fName size = 0 or: [fName = #cancelled]) ifTrue: [^ self]. [(result _ ScratchFileChooserDialog confirmFileOverwriteIfExisting: fName) = false] whileTrue: [ fName _ ScratchFileChooserDialog saveScratchFileFor: self. (fName size = 0 or: [fName = #cancelled]) ifTrue: [^ self]]. (result = #cancelled) ifTrue: [^ self]. self updateLastHistoryEntryIfNeeded. fName _ (self nameFromFileName: fName), '.sb'. projectDirectory _ FileDirectory on: (FileDirectory dirPathFor: fName). projectName _ FileDirectory localNameFor: fName. projectInfo at: 'author' put: author. self updateHistoryProjectName: projectName op: 'save'. self writeScratchProject. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'jm 5/11/2009 10:52'! saveScratchProjectNoDialog | fName dir | self closeMediaEditorsAndDialogs ifFalse: [^ self]. projectName ifNil: [projectName _ '']. fName _ self nameFromFileName: projectName. dir _ ScratchFileChooserDialog getLastFolderForType: #project. (fName size = 0 | (dir fileExists: fName , '.sb') not) ifTrue: [^ self saveScratchProject]. ScratchFileChooserDialog lastFolderIsSampleProjectsFolder ifTrue: [^ self saveScratchProject]. self updateLastHistoryEntryIfNeeded. projectName _ FileDirectory localNameFor: (fName, '.sb'). "ignore path, if any; save in the original project directory" projectDirectory _ dir. self updateHistoryProjectName: projectName op: 'save'. self writeScratchProject. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'jm 5/11/2005 11:56'! storeProjectInfoOn: aBinaryStream | s | projectInfo at: 'thumbnail' put: workPane thumbnailForm. s _ WriteStream on: (ByteArray new: 100000). ObjStream new storeObj: projectInfo on: s. aBinaryStream uint32: s size. aBinaryStream nextPutAll: s contents. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'jm 6/2/2009 18:59'! updateHistoryProjectName: projName op: operation "The given user is about to save or upload a project with the given name. Update the project history. operation is a string specifying the operation." | timestamp tab history platform osVersion | projectInfo removeKey: 'organization' ifAbsent: []. "obsolete" projectInfo at: 'scratch-version' put: Version. timestamp _ (Date today printFormat: #(3 2 1 $- 1 1)), ' ', Time now print24. tab _ String tab. history _ projectInfo at: 'history' ifAbsent: ['']. history _ history, timestamp, tab. history _ history, operation, tab, (self nameFromFileName: projName), tab, loginName, tab, author. history _ history, String cr. projectInfo at: 'history' put: history. "record other data" projectInfo at: 'scratch-version' put: Version. projectInfo at: 'language' put: ScratchTranslator currentLanguage. platform _ Smalltalk platformName. platform ifNil: [platform _ 'unknown']. 'linux' = platform ifTrue: [ Display extent = (1200@900) ifTrue: [platform _ 'XO']]. projectInfo at: 'platform' put: platform. osVersion _ Smalltalk osVersion. osVersion ifNil: [osVersion _ 'unknown']. projectInfo at: 'os-version' put: osVersion. (workPane scratchServer notNil and: [workPane scratchServer isHosting]) ifTrue: [projectInfo at: 'isHosting' put: true] ifFalse: [projectInfo removeKey: 'isHosting' ifAbsent: []]. (self allBlocksString includesSubString: 'motor') ifTrue: [projectInfo at: 'hasMotorBlocks' put: true] ifFalse: [projectInfo removeKey: 'hasMotorBlocks' ifAbsent: []]. workPane penTrailsForm ifNil: [projectInfo removeKey: 'penTrails' ifAbsent: []] ifNotNil: [projectInfo at: 'penTrails' put: workPane penTrailsForm]. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'jm 3/28/2007 12:43'! updateLastHistoryEntryIfNeeded "If the the last entry in this project's history is an old-sytle entry (i.e. one that does not include the project name and author) update it." | lines lastLine oldAuthor tab s | lines _ (projectInfo at: 'history' ifAbsent: ['']) lines. lines size = 0 ifTrue: [^ self]. lastLine _ lines at: lines size. (lastLine includes: Character tab) ifTrue: [^ self]. "last line is already a new-style entry" oldAuthor _ projectInfo at: 'author' ifAbsent: ['']. tab _ String tab. lastLine _ lastLine, tab, 'old', tab, projectName, tab, "blank scratchr name" tab, oldAuthor. lines at: lines size put: lastLine. s _ WriteStream on: (String new: 1000). lines do: [:entry | s nextPutAll: entry; cr]. projectInfo at: 'history' put: s contents. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'ee 11/4/2008 11:46'! writeScratchProject "Write this Scratch project to the file named projectFile in the project directory. Called by saveScratchProject." | oldScriptsTarget oldTab oldViewerCategory oldPosition saveError out | self stopAll. self world ifNotNil: [self world activeHand newKeyboardFocus: nil]. "terminates active editor" "share duplicate sounds and images" self canonicalizeSoundsBits: nil saveOriginal: false. self canonicalizeImagesQuality: nil saveOriginal: false. oldScriptsTarget _ scriptsPane target. oldTab _ scriptsPane tabPane currentTab. oldViewerCategory _ viewerPane currentCategory. scriptsPane target: nil. workPane updateSpritesList. oldPosition _ workPane position. workPane delete; position: 0@0. self updatePenPositions. ScriptableScratchMorph buildBlockSpecDictionary. workPane allMorphsDo: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [ m blocksBin allMorphsDo: [:b | (b isKindOf: BlockMorph) ifTrue: [b stop]]. m convertStacksToTuples]]. saveError _ nil. [ out _ FileStream newFileNamed: (projectDirectory unusedNameStartingWith: 'tmp'). out ifNil: [saveError _ 'Folder may be locked or read-only'] ifNotNil: [ out binary. out nextPutAll: 'ScratchV02' asByteArray. self storeProjectInfoOn: out. ObjStream new storeObj: workPane on: out. out close]. ] ifError: [:err :rcvr | out ifNotNil: [ [ out close. projectDirectory deleteFileNamed: out localName. ] ifError: []]. "clean up, ignoring any errors" saveError _ err]. workPane allMorphsDo: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [ m convertTuplesToStacks]]. self addMorph: (workPane position: oldPosition). oldScriptsTarget ifNil: [oldScriptsTarget _ workPane]. oldScriptsTarget viewBlocksAndScripts. scriptsPane tabPane currentTab: oldTab. viewerPane currentCategory: oldViewerCategory. self updatePenPositions. saveError ifNil: [ justSaved _ true. self updateProjectName. projectDirectory deleteFileNamed: projectName. [projectDirectory rename: out localName toBe: projectName] ifError: [^ self inform: 'Save failed' withDetails: 'Is the folder read-only?' localized]. projectDirectory setMacFileNamed: projectName type: 'STsb' creator: 'MITS'] ifNotNil: [ projectName _ ''. self inform: 'Save failed' withDetails: saveError]. ! ! !ScratchFrameMorph methodsFor: 'file read/write' stamp: 'ee 11/4/2008 11:47'! writeScratchProjectOld "Obsolete!! Uses old format!! Write this Scratch project to the file named projectFile in the project directory. Called by saveScratchProject." | oldScriptsTarget oldTab oldViewerCategory oldPosition saveError out | self world ifNotNil: [self world activeHand newKeyboardFocus: nil]. "terminates active editor" self stopAll. (projectDirectory isKindOf: String) ifTrue: [ projectDirectory _ FileDirectory on: projectDirectory]. ((FileDirectory default directoryExists: projectDirectory pathName) or: [FileDirectory root directoryNames includes: projectDirectory pathName]) ifFalse: [ projectDirectory _ FileDirectory default]. oldScriptsTarget _ scriptsPane target. oldTab _ scriptsPane tabPane currentTab. oldViewerCategory _ viewerPane currentCategory. scriptsPane target: nil. oldPosition _ workPane position. workPane delete; position: 0@0. ScriptableScratchMorph buildBlockSpecDictionary. workPane allMorphsDo: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [ m blocksBin allMorphsDo: [:b | (b isKindOf: BlockMorph) ifTrue: [b stop]]. "xxx m convertStacksToTuples xxx"]]. saveError _ nil. [ out _ (FileStream newFileNamed: (projectDirectory unusedNameStartingWith: 'tmp')) binary. ObjStream new storeObj: workPane on: out showProgress: true. out close. ] ifError: [:err :rcvr | saveError _ err]. workPane allMorphsDo: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [ m convertTuplesToStacks]]. workPane position: oldPosition. self addMorph: workPane. oldScriptsTarget ifNil: [oldScriptsTarget _ workPane]. oldScriptsTarget viewBlocksAndScripts. scriptsPane tabPane currentTab: oldTab. viewerPane currentCategory: oldViewerCategory. self updateProjectName. saveError ifNil: [ projectDirectory deleteFileNamed: projectName. projectDirectory rename: out localName toBe: projectName. projectDirectory setMacFileNamed: projectName type: 'STsb' creator: 'MITS'] ifNotNil: [ out close. projectDirectory deleteFileNamed: out localName. self inform: 'Save failed' withDetails: saveError]. ! ! !ScratchFrameMorph methodsFor: 'uploading' stamp: 'jm 2/27/2009 11:31'! allBlocksString | s stacks | s _ WriteStream on: (String new: 10000). ((Array with: workPane), self scratchObjects) do: [:obj | stacks _ obj blocksBin submorphs select: [:m | m isKindOf: BlockMorph]. stacks size > 0 ifTrue: [ s nextPutAll: 'All stacks for ', obj objName, ':'; cr; cr. stacks do: [:blocks | self printTupleList: blocks tupleSequence on: s. s cr; cr]. s cr]]. ^ s contents ! ! !ScratchFrameMorph methodsFor: 'uploading' stamp: 'jm 10/25/2007 14:35'! compressMediaForUpload "Compress my media prior to uploading this project." self canonicalizeSoundsBits: 4 saveOriginal: true. self canonicalizeImagesQuality: 90 saveOriginal: true. ! ! !ScratchFrameMorph methodsFor: 'uploading' stamp: 'jm 10/23/2007 23:29'! printTupleElement: el on: s (el isKindOf: Array) ifTrue: [self printTupleList: el on: s. ^ self]. (el isKindOf: Symbol) ifTrue: [s nextPutAll: el. ^ self]. (el isKindOf: String) ifTrue: [s nextPut: $". s nextPutAll: el. s nextPut: $". ^ self]. (el isKindOf: ScriptableScratchMorph) ifTrue: [s nextPutAll: el objName. ^ self]. s nextPutAll: el printString. ! ! !ScratchFrameMorph methodsFor: 'uploading' stamp: 'jm 10/23/2007 23:30'! printTupleList: anArray on: s s nextPut: $(. 1 to: anArray size do: [:i | self printTupleElement: (anArray at: i) on: s. i = anArray size ifFalse: [s space]]. s nextPut: $). ! ! !ScratchFrameMorph methodsFor: 'uploading' stamp: 'jm 3/28/2007 11:29'! removeLastHistoryEntry "Remove the last entry in the project history. This is done if an upload attempt fails or is cancelled." | lines s | lines _ (projectInfo at: 'history' ifAbsent: ['']) lines. lines size = 0 ifTrue: [^ self]. lines _ lines copyFrom: 1 to: lines size - 1. s _ WriteStream on: (String new: 1000). lines do: [:entry | s nextPutAll: entry; cr]. projectInfo at: 'history' put: s contents. ! ! !ScratchFrameMorph methodsFor: 'uploading' stamp: 'jm 10/18/2007 16:26'! revertToUncompressedMedia "Revert to uncomprssed media after uploading this project." self allProjectMedia do: [:m | m revertToUncompressed]. ! ! !ScratchFrameMorph methodsFor: 'uploading' stamp: 'jm 10/23/2007 23:34'! scriptsStringForUpload | s scripts | s _ WriteStream on: (String new: 10000). ((Array with: workPane), self scratchObjects) do: [:obj | scripts _ obj blocksBin submorphs select: [:m | m isKindOf: HatBlockMorph]. scripts size > 0 ifTrue: [ s nextPutAll: 'Scripts for ', obj objName, ':'; cr; cr. scripts do: [:hat | self printTupleList: hat tupleSequence on: s. s cr]. s cr]]. ^ s contents ! ! !ScratchFrameMorph methodsFor: 'uploading' stamp: 'ee 11/4/2008 11:46'! writeScratchProjectOn: aStream "Write this Scratch project in a serialized form on the given stream." | oldScriptsTarget oldTab oldViewerCategory oldPosition storeError | self stopAll. self world ifNotNil: [self world activeHand newKeyboardFocus: nil]. "terminates active editor" oldScriptsTarget _ scriptsPane target. oldTab _ scriptsPane tabPane currentTab. oldViewerCategory _ viewerPane currentCategory. scriptsPane target: nil. workPane updateSpritesList. oldPosition _ workPane position. workPane delete; position: 0@0. self updatePenPositions. ScriptableScratchMorph buildBlockSpecDictionary. workPane allMorphsDo: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [ m blocksBin allMorphsDo: [:b | (b isKindOf: BlockMorph) ifTrue: [b stop]]. m convertStacksToTuples]]. storeError _ nil. [ aStream nextPutAll: 'ScratchV02' asByteArray. self storeProjectInfoOn: aStream. ObjStream new storeObj: workPane on: aStream. ] ifError: [:err :rcvr | storeError _ err]. workPane allMorphsDo: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [ m convertTuplesToStacks]]. self addMorph: (workPane position: oldPosition). oldScriptsTarget ifNil: [oldScriptsTarget _ workPane]. oldScriptsTarget viewBlocksAndScripts. scriptsPane tabPane currentTab: oldTab. viewerPane currentCategory: oldViewerCategory. self updatePenPositions. storeError ifNotNil: [self error: storeError]. ! ! !ScratchFrameMorph methodsFor: 'watchers' stamp: 'jm 5/6/2008 18:13'! deleteWatchersForSprite: aSprite "The given sprite is being deleted. Delete all watchers associated with it." aSprite lists do: [:m | m delete]. workPane submorphsDo: [:m | (m isKindOf: WatcherMorph) ifTrue: [ (m target = aSprite) ifTrue: [m delete]]]. watcherPositions keys do: [:k | (k at: 1) = aSprite ifTrue: [watcherPositions removeKey: k]]. ! ! !ScratchFrameMorph methodsFor: 'watchers' stamp: 'jm 12/1/2007 23:22'! deleteWatchersForVar: varName ofSprite: aSprite "The given variable is being deleted. Delete all watchers associated with it." workPane submorphsDo: [:m | (m isKindOf: WatcherMorph) ifTrue: [ ((m target = aSprite) and: [(m getSelector = #getVar:) and: [m parameter = varName]]) ifTrue: [m delete]]]. ! ! !ScratchFrameMorph methodsFor: 'watchers' stamp: 'jm 8/25/2008 10:00'! deletingWatcher "The given watcher is being removed from the stage; remember it's last position." | palette w | watcherPositions do: [:rec | rec first ifNotNil: [ w _ rec first. w owner ifNil: [ rec at: 1 put: nil. rec at: 2 put: w position - workPane position. "record old position and layout style" rec at: 3 put: w layoutStyle. rec at: 4 put: w sliderRange]]]. palette _ viewerPane pageViewer contents. palette ifNotNil: [palette updateWatcherButtonsForFrame: self]. ! ! !ScratchFrameMorph methodsFor: 'watchers' stamp: 'jm 8/25/2008 09:56'! initializeWatcherPositions "If any watchers are on the stage, store their position. The dictionary, which is created lazily, is formatted in the following way: (, ) -> (, ) Or, more concisely: (sprite/nil,selectorAndArg)->(watcher/nil,position,style,range)." | p | watcherPositions _ Dictionary new. self scratchWatchers do: [:w | p _ w position - workPane position. watcherPositions at: {w getAssociatedSprite. w selectorAndArg} put: {w. p. w layoutStyle. w sliderRange}]. ! ! !ScratchFrameMorph methodsFor: 'watchers' stamp: 'ee 7/11/2008 22:26'! listWatchers "Answer a collection of all the list watchers in the work pane." ^ self workPane submorphs select: [:m | m isKindOf: ScratchListMorph] ! ! !ScratchFrameMorph methodsFor: 'watchers' stamp: 'tis 7/31/2006 09:08'! scratchWatchers "Answer a collection of all the scratch watchers in the work pane." ^ self workPane submorphs select: [:m | m isKindOf: WatcherMorph] ! ! !ScratchFrameMorph methodsFor: 'watchers' stamp: 'jm 8/25/2008 10:05'! showWatcher: watcher "Show the given watcher. Reuse it's old position if it was showing before. Otherwise, find a new position for it." | rec style range | rec _ watcherPositions at: {watcher getAssociatedSprite. watcher selectorAndArg} ifAbsent: [nil]. rec ifNil: [ watcher position: self unusedPositionForWatcher. style _ #small. range _ watcher sliderRange] ifNotNil: [ watcher position: workPane position + rec second. style _ rec third. range _ rec fourth]. watcherPositions at: {watcher getAssociatedSprite. watcher selectorAndArg} put: {watcher. (watcher position - workPane position). style. range}. watcher sliderRange: range. watcher layoutStyle: style. watcher updateTargetName. workPane addMorph: watcher. watcher world ifNotNil: [watcher world startSteppingSubmorphsOf: watcher]. ! ! !ScratchFrameMorph methodsFor: 'watchers' stamp: 'ee 6/28/2008 15:27'! unusedPositionForWatcher "Return an unused watcher position on the stage." | watchers positions rowH x y newX | watchers _ (watcherPositions collect: [:r | r first]) select: [:w | w notNil]. positions _ watchers collect: [:w | w position]. (watchers size > 0) ifTrue: [rowH _ (watchers at: 1) height] ifFalse: [rowH _ 25]. x _ workPane left + 10. y _ workPane top + 10. [positions includes: (x@y)] whileTrue: [ y _ y + rowH. (y > (workPane bottom - rowH)) ifTrue: [ "start a new column" newX _ 0. watchers do: [:w | w left < (x + 20) ifTrue: [newX _ newX max: w right + 4]]. newX > (workPane right - 20) ifTrue: [ ^ ((10 to: 400) atRandom) @ ((10 to: 330) atRandom)]. "no free location" x _ newX. y _ workPane top + 10]]. ^ x@y ! ! !ScratchFrameMorph methodsFor: 'watchers' stamp: 'jm 11/30/2007 19:35'! watcherForBlock: aBlockMorph "Answer a watcher for the given block if there is one currently showing on the stage." | pair | pair _ watcherPositions at: {aBlockMorph getAssociatedSprite. aBlockMorph selectorAndArg} ifAbsent: [^ nil]. ^ pair first ! ! !ScratchFrameMorph methodsFor: 'watchers' stamp: 'jm 5/6/2008 17:17'! watcherShowingFor: sprite selectorAndArg: selectorAndArg "Answer true if a watcher for the given sprite, selector, and argument is currently showing on the stage." | sel arg listM pair | sel _ selectorAndArg first. arg _ selectorAndArg second. #contentsOfList: = sel ifTrue: [ listM _ sprite lists at: arg ifAbsent: [^ false]. ^ listM owner notNil]. pair _ watcherPositions at: {sprite. selectorAndArg} ifAbsent: [^ false]. pair first ifNotNil: [ pair first owner ifNil: [pair at: 1 put: nil]]. ^ pair first notNil ! ! !ScratchFrameMorph methodsFor: 'tools (no longer used)' stamp: 'jm 2/13/2009 15:26'! copyTool | hand cursorForm offset | self paintingInProgress ifTrue: [^ self beep]. hand _ self world activeHand. hand toolType: 'CopyTool'. cursorForm _ ScratchFrameMorph skinAt: #copyCursor. offset _ 8@13. ScratchFrameMorph isXO ifTrue: [ cursorForm _ cursorForm magnifyBy: 1.5. offset _ (offset * 1.5) rounded]. hand showTemporaryCursor: cursorForm hotSpotOffset: offset. ! ! !ScratchFrameMorph methodsFor: 'tools (no longer used)' stamp: 'jm 2/13/2009 15:26'! cutTool | hand cursorForm offset | self paintingInProgress ifTrue: [^ self beep]. hand _ self world activeHand. hand toolType: 'CutTool'. cursorForm _ ScratchFrameMorph skinAt: #cutCursor. offset _ 8@8. ScratchFrameMorph isXO ifTrue: [ cursorForm _ cursorForm magnifyBy: 1.5. offset _ (offset * 1.5) rounded]. hand showTemporaryCursor: cursorForm hotSpotOffset: offset. ! ! !ScratchFrameMorph methodsFor: 'tools (no longer used)' stamp: 'jm 2/24/2004 18:58'! normalTool self paintingInProgress ifTrue: [^ self beep]. self world activeHand toolType: nil. ! ! !ScratchFrameMorph methodsFor: 'tools (no longer used)' stamp: 'jm 1/23/2007 12:51'! undoTool | m newOwner oldName | self paintingInProgress ifTrue: [^ self beep]. Clipboard ifNil: [^ self beep]. self activeHand toolType: nil. m _ Clipboard fullCopy. "Reset clipboard to empty since an undo just happened" (m isKindOf: BlockMorph) ifTrue: [ newOwner _ viewerPane target. newOwner ifNotNil: [m newScriptOwner: newOwner]. (viewerPane currentCategory = 'variables') ifTrue: [ "update 'variables' category if it is showing" viewerPane currentCategory: 'variables']]. (m isKindOf: ScratchSpriteMorph) ifTrue: [ "sprite; add to stage" m filterReset; show. m objName: Clipboard objName. Clipboard _ nil. oldName _ m objName. self addAndView: m. m objName: oldName. ^ self]. "blocks or anything else: attach to hand" self activeHand attachMorph: m. Clipboard _ nil. ! ! !ScratchFrameMorph methodsFor: 'tools (no longer used)' stamp: 'jm 2/13/2009 15:26'! zoomInTool | hand cursorForm offset | self paintingInProgress ifTrue: [^ self beep]. hand _ self world activeHand. hand toolType: 'ZoomInTool'. cursorForm _ ScratchFrameMorph skinAt: #zoomInCursor. offset _ 8@8. ScratchFrameMorph isXO ifTrue: [ cursorForm _ cursorForm magnifyBy: 1.5. offset _ (offset * 1.5) rounded]. hand showTemporaryCursor: cursorForm hotSpotOffset: offset. ! ! !ScratchFrameMorph methodsFor: 'tools (no longer used)' stamp: 'jm 2/13/2009 15:26'! zoomOutTool | hand cursorForm offset | self paintingInProgress ifTrue: [^ self beep]. hand _ self world activeHand. hand toolType: 'ZoomOutTool'. cursorForm _ ScratchFrameMorph skinAt: #zoomOutCursor. offset _ 8@8. ScratchFrameMorph isXO ifTrue: [ cursorForm _ cursorForm magnifyBy: 1.5. offset _ (offset * 1.5) rounded]. hand showTemporaryCursor: cursorForm hotSpotOffset: offset. ! ! !ScratchFrameMorph methodsFor: 'private' stamp: 'jm 4/22/2009 09:37'! enterQuarterModeIfSmallScreen (Display width >= 980) & (Display height >= 555) ifTrue: [^ self]. viewMode = #normal ifTrue: [self enterQuarterMode]. ! ! !ScratchFrameMorph methodsFor: 'private' stamp: 'ee 2/3/2009 13:26'! fixProjectTitleMorphLayout | s truncated eWidth w | projectName ifNotNil: [ s _ (self nameFromFileName: projectName). "trim project name to fit, if necessary" truncated _ false. eWidth _ (ScratchTranslator stringExtent: '...' font: projectTitleMorph font) x. w _ titlePane width - 100 - eWidth. [((ScratchTranslator stringExtent: s font: projectTitleMorph font) x) > w] whileTrue: [ truncated _ true. s _ s copyFrom: 1 to: s size - 1]. truncated ifTrue: [s _ s, '...']. projectTitleMorph contents: s]. ! ! !ScratchFrameMorph methodsFor: 'private' stamp: 'jm 1/4/2007 23:10'! nextSurpriseCostumeName "Answer a surprise costume name or nil if there are no costumes." "Details: Shuffle the list of available costume names and return them one at a time. When there are none left, generate a new shuffle. This avoids repeats." | dir ext | (shuffledCostumeNames isNil or: [shuffledCostumeNames size = 0]) ifTrue: [ shuffledCostumeNames _ OrderedCollection new: 1000. dir _ (FileDirectory default directoryNamed: 'Media') directoryNamed: 'Costumes'. dir allFileNamesDo: [:fn | (fn includesSubString: 'Letters') ifFalse: [ ext _ (FileDirectory extensionFor: fn) asLowercase. ((ext size > 0) and: [#(gif png jpg) includes: ext]) ifTrue: [shuffledCostumeNames add: fn]]]]. shuffledCostumeNames _ shuffledCostumeNames shuffledBy: Random new. shuffledCostumeNames size = 0 ifTrue: [^ nil] ifFalse: [^ shuffledCostumeNames removeFirst]. ! ! !ScratchFrameMorph methodsFor: 'private' stamp: 'jm 8/7/2008 13:32'! projectIsEmpty "Answer true if the current project has no scripts, no variables, no special costumes or sounds, and at most a single sprite." | allScriptables defaultCostumes defaultSnds | "at most one sprite in workpane?" workPane submorphs size > 1 ifTrue: [^ false]. workPane submorphs size = 1 ifTrue: [ (workPane submorphs first isKindOf: ScratchSpriteMorph) ifFalse: [^ false]]. allScriptables _ workPane submorphs copyWith: workPane. defaultCostumes _ Set with: ScriptableScratchMorph defaultBackgroundForm. defaultSnds _ Set with: ScriptableScratchMorph popSound with: ScriptableScratchMorph meowSound. ScratchFrameMorph defaultSprite ifNotNil: [ ScratchFrameMorph defaultSprite media do: [:media | media isImage ifTrue: [defaultCostumes add: media form]. media isSound ifTrue: [defaultSnds add: media sound]]]. allScriptables do: [:m | m blocksBin submorphs size > 0 ifTrue: [^ false]. "any stacks?" m varNames size > 1 ifTrue: [^ false]. "any variables?" m media do: [:media | (media isImage and: [(defaultCostumes includes: media form) not]) ifTrue: [^ false]. (media isSound and: [(defaultSnds includes: media sound) not]) ifTrue: [^ false]]]. ^ true ! ! !ScratchFrameMorph methodsFor: 'private' stamp: 'ee 11/4/2008 11:46'! rebuildUIForNewLanguage "Rebuild my UI after the language or font has been changed." World fullRepaintNeeded. viewerPane rebuildCategorySelectors. self updatePanes. self view: scriptsPane target tab: scriptsPane tabPane currentTab category: viewerPane currentCategory. ! ! !ScratchFrameMorph methodsFor: 'private' stamp: 'jm 5/5/2007 23:41'! recordLanguage: aString "Record my language in the settings file." "ScratchFrameMorph new recordLanguage: 'English'" | fName f sz settings all | fName _ FileDirectory default fullNameFor: 'Scratch.ini'. f _ FileStream concreteStream new open: fName forWrite: true. f ifNil: [^ self]. sz _ f size. settings _ (f next: sz) lines. settings _ settings reject: [:s | s asLowercase beginsWith: 'language=']. settings _ settings reject: [:s | all _ s asByteArray asSet. (all size = 1) and: [all asArray first = 0]]. settings _ settings copyWith: ('Language=', aString). f position: 0. settings do: [:s | f nextPutAll: s, String crlf]. [f position < sz] whileTrue: [f nextPut: 0 asCharacter]. f close. ! ! !ScratchFrameMorph methodsFor: 'private' stamp: 'ee 11/4/2008 11:45'! setLanguage: aString "Set my language and update my blocks." | tempJustSaved | tempJustSaved _ justSaved. (workPane submorphs copyWith: workPane) do: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [ m convertStacksToTuples]]. ScratchTranslator setLanguage: (ScratchTranslator isoCodeForName: aString). viewerPane rebuildCategorySelectors. (workPane submorphs copyWith: workPane) do: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [ m convertTuplesToStacks]]. self updatePanes. self view: scriptsPane target tab: scriptsPane tabPane currentTab category: viewerPane currentCategory. justSaved _ tempJustSaved. ! ! !ScratchFrameMorph methodsFor: 'private' stamp: 'jm 6/5/2009 21:12'! updatePanes | p | menuPanel delete. self createMenuPanel. toolbarPanel delete. self createToolbar. viewModeButtonsPanel delete. self createViewModeButtonsPanel. stageButtonsPanel delete. self createStageButtonsPanel. titlePane addMorph: stageButtonsPanel. scriptsPane tabPane delete. scriptsPane createTabPane. readoutPane delete. self createReadoutPane. workPane sensorBoard owner ifNil: [p _ nil] ifNotNil: [p _ workPane sensorBoard position]. workPane sensorBoard addReadouts. p ifNotNil:[ self showSensorBoard. workPane sensorBoard position: p]. libraryPane clearLibrary. self scratchWatchers do: [:w | w languageChanged]. self listWatchers do: [:w | w fixLayoutForNewLanguage]. World startSteppingSubmorphsOf: self. self fixLayout. scriptsPane fixLayout. self updateViewModeButtons. ! ! !ScratchFrameMorph methodsFor: 'private' stamp: 'jm 10/26/2008 17:44'! updateProjectName "Update the project name display in the Scratch title bar." | s | projectName ifNil: [projectName _ '']. projectTitleMorph contents: (self nameFromFileName: projectName). projectTitleMorph contents size > 0 ifTrue: [s _ projectTitleMorph contents, '- Scratch'] ifFalse: [s _ 'Scratch ', Version]. ScratchPlugin primSetWindowTitle: s. self fixLayout. ! ! !ScratchFrameMorph class methodsFor: 'class initialization' stamp: 'jm 9/23/2009 13:34'! initialize "self initialize" Clipboard _ nil. WorkpaneExtent _ 480@360. UseErrorCatcher _ true. DefaultNotes _ ''. self initFonts. ! ! !ScratchFrameMorph class methodsFor: 'scratch skin' stamp: 'ee 11/10/2008 13:30'! getFont: aSymbol "Get a font for a given purpose (specified by aSymbol) based on the current font setting." ^ self isXO ifTrue: [FontsXO at: aSymbol] ifFalse: [Fonts at: aSymbol]. ! ! !ScratchFrameMorph class methodsFor: 'scratch skin' stamp: 'jm 5/7/2009 11:42'! initFonts "self initFonts" | fontSpecs fonts fontsXO | fontSpecs _ #( (Arg Verdana 10 HelveticaMedium 14) (Label VerdanaBoldNarrowSpace 10 Helvetica 18) (MenuTitle VerdanaBold 14 ArialBold 16) (Button VerdanaBold 10 ArialBold 16) (Category VerdanaBold 10 ArialBold 16) (Tab VerdanaBold 11 ArialBold 16) (CommentBlock Verdana 10 Verdana 14) (TalkBubble VerdanaBold 12 VerdanaBold 18) (ToolTip Verdana 13 ArialBold 16) (ReporterToolTip Verdana 14 ArialBold 16) (XYReadout Verdana 10 Arial 14) (XYReadoutBold VerdanaBold 10 ArialBold 14) (CostumesPage VerdanaBold 11 ArialBold 14) (SoundsPage VerdanaBold 11 ArialBold 14) (ViewerPage VerdanaBold 11 ArialBold 14) (UpdatingStringField VerdanaBold 11 VerdanaBold 14) (Watcher VerdanaBold 10 ArialBold 14) (WatcherLarge VerdanaBold 14 VerdanaBold 14) (PaintUtilityButton VerdanaBold 10 ArialBold 16) (PaintSetRotationCenter VerdanaBold 11 ArialBold 14) "Library" (LibraryItemName VerdanaBold 9 ArialBold 14) (LibraryItemInfo Verdana 6 Verdana 10) (MediaItemInfo Verdana 9 Arial 14) "Dialog Boxes" (DialogBoxTitle VerdanaBold 14 VerdanaBold 16) (DialogBoxMessage VerdanaBold 13 VerdanaBold 16) (DialogBoxButton VerdanaBold 11 VerdanaBold 16) (ProjectNotes Verdana 10 Verdana 12) (LinkMorphDefault VerdanaBold 10 VerdanaBold 12) (ShareLink VerdanaBold 13 VerdanaBold 12) (SoundRecorderButton VerdanaBold 13 VerdanaBold 12) (SoundRecorderTimer NewYorkBold 10 NewYorkBold 12) (StringDialogTypeIn Verdana 12 Verdana 16) (NewVariableDialogBox Verdana 11 Verdana 14) (AboutScratch VerdanaBold 11 VerdanaBold 14) (UploadTagLabel VerdanaBold 10 VerdanaBold 12) (UploadTag Verdana 10 Verdana 12) (UploadDialogLabel VerdanaBold 10 VerdanaBold 12) (UploadDialogContents Verdana 10 Verdana 12) (UploadDialogComment Verdana 10 Verdana 12) "File Choosers" "This is the for the folder shortcuts in the file dialog" (FolderShortcut Verdana 11 Verdana 14) (FileChooserNewFileTitle VerdanaBold 10 VerdanaBold 14) (FileChooserNewFilename Verdana 10 Verdana 12) (FileChooserLabel VerdanaBold 10 VerdanaBold 14) (FileChooserContents Verdana 12 Verdana 12) (FileChooserComment Verdana 10 Verdana 12) (FilePickerDirectoryName VerdanaBold 9 VerdanaBold 12) (FilePickerEntry Verdana 11 Verdana 13) (FilePickerEntryHighlighted VerdanaBold 11 VerdanaBold 13) (FrameMorphProjectTitle VerdanaBold 13 ArialBold 16) ). fonts _ Dictionary new. fontsXO _ Dictionary new. fontSpecs do: [:r | fonts at: (r at: 1) put: (StrikeFont fontName: (r at: 2) size: (r at: 3)). fontsXO at: (r at: 1) put: (StrikeFont fontName: (r at: 4) size: (r at: 5))]. Fonts _ fonts. FontsXO _ fontsXO. ! ! !ScratchFrameMorph class methodsFor: 'scratch skin' stamp: 'jm 4/10/2008 12:00'! isXO "Return true if the current skin is XO." ^ IsXO ! ! !ScratchFrameMorph class methodsFor: 'scratch skin' stamp: 'jm 4/10/2008 12:00'! isXO: aBoolean "Set the current skin style to #XO if the argument is true, normal otherwise." "self isXO: true" "self isXO: false" IsXO _ aBoolean. "aBoolean ifTrue: [(Preferences setMenuFontTo: (StrikeFont fontName: #VerdanaBold size: 18))] ifFalse: [(Preferences restoreDefaultFonts)]." "annoying for development" ! ! !ScratchFrameMorph class methodsFor: 'scratch skin' stamp: 'ee 12/5/2008 13:42'! readSkinFrom: aDirectory "Read the Forms for my skin from the given directory and store them in myskin dictionary." "When in XO mode, entries in ScratchSkinXO override the corresponding entries in ScratchSkin." "self readSkinFrom: (FileDirectory default directoryNamed: 'ScratchSkin')" | dict img i xoDict | dict _ Dictionary new. xoDict _ Dictionary new. aDirectory fileNames do: [:fn | Cursor read showWhile: [ img _ [Form fromFileNamed: (aDirectory fullNameFor: fn)] ifError: [nil]]. img ifNotNil: [ i _ fn findLast: [:c | c = $.]. i = 0 ifFalse: [fn _ fn copyFrom: 1 to: i - 1]. (fn asLowercase endsWith: '_xo') ifTrue: [xoDict at: (fn copyFrom: 1 to: fn size - 3) asSymbol put: img] ifFalse: [dict at: fn asSymbol put: img]]]. ScratchSkin _ dict. ScratchSkinXO _ xoDict. img _ ScratchSkin at: #scriptsPaneTexture ifAbsent: [nil]. (img notNil and: [img depth ~= 32]) ifTrue: [ ScratchSkin at: #scriptsPaneTexture put: (img asFormOfDepth: 32)]. ! ! !ScratchFrameMorph class methodsFor: 'scratch skin' stamp: 'jm 4/10/2008 12:11'! skinAt: aSymbolOrString ^ self skinAt: aSymbolOrString ifAbsent: [ScratchSkin errorKeyNotFound] ! ! !ScratchFrameMorph class methodsFor: 'scratch skin' stamp: 'ee 12/4/2008 11:43'! skinAt: aSymbolOrString ifAbsent: aBlock "Answer the skin image with the given name. In XO mode, first check to see if an entry appears in ScratchSkinXO. If so, use it. Otherwise, use the image from the normal skin dictionary." | k | k _ aSymbolOrString asSymbol. self isXO ifTrue: [ k = #scriptsPaneTexture ifTrue: [^ aBlock value]. (ScratchSkinXO includesKey: k) ifTrue: [^ ScratchSkinXO at: k]]. ^ ScratchSkin at: k ifAbsent: aBlock ! ! !ScratchFrameMorph class methodsFor: 'scratch skin' stamp: 'jm 12/8/2008 16:09'! takeOverScreen: aBoolean "self takeOverScreen: true" "self takeOverScreen: false" TakeOverScreen _ aBoolean ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 2/13/2009 15:26'! buttonLabel: aString selector: aSymbolOrNil "Answer a big button with the given label." | button | button _ ResizableToggleButton2 new offForm: (ScratchFrameMorph skinAt: #btn) onForm: (ScratchFrameMorph skinAt: #btnPressed); label: aString font: (ScratchFrameMorph getFont: #Button); actionSelector: aSymbolOrNil. ^ button ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 11/17/2008 17:21'! cameraMode ^ #normal ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 11/17/2008 17:23'! colorToTrack ^ Color red ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 9/23/2009 10:53'! defaultSprite "Return the default sprite if one was set, or the cat otherwise" DefaultSprite ifNotNil: [^ DefaultSprite] ifNil: [^ ScratchSpriteMorph new addMediaItem: (ImageMedia new mediaName: ('costume' localized, '1'); form: (ScratchFrameMorph skinAt: #defaultSpriteCostume)); addMediaItem: (SoundMedia new mediaName: 'meow' localized; sound: ScratchSpriteMorph meowSound); lookLike: ('costume' localized, '1')]. ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 4/14/2008 17:57'! palettePaneColor | c | c _ Color r: 124 g: 128 b: 131 range: 255. self isXO ifTrue: [c _ c mixed: 0.75 with: Color white]. ^ c ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 3/17/2007 11:43'! parseDownloadDatabase: fileName "Parse the HTML file for the Scratch download database." "self parseDownloadDatabase: 'mlist.html'" "To list downloads by date: (db collect: [:r | r first upTo: Character space]) asBag sortedElements" | raw lines inTable s records r | raw _ (FileStream oldFileNamed: fileName) contentsOfEntireFile. lines _ OrderedCollection new. inTable _ false. raw lines do: [:ln | s _ ln withBlanksTrimmed. s = '' ifTrue: [inTable _ false]. inTable ifTrue: [lines addLast: s]. s = '' ifTrue: [inTable _ true]]. records _ OrderedCollection new. lines _ ReadStream on: lines. [lines atEnd] whileFalse: [ ln _ lines next. ln = '' ifTrue: [ r _ self parseRecordFrom: lines. r size > 0 ifTrue: [records addLast: r asArray]]]. ^ records asArray ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 1/21/2007 16:20'! parseRecordFrom: lineStream "An HTML table record from the given stream of lines." | rec ln buf | rec _ OrderedCollection new. [true] whileTrue: [ ln _ lineStream next. ln = '' ifTrue: [^ rec]. (ln beginsWith: '')] whileFalse: [buf _ buf, ' ', lineStream next]. buf _ buf copyFrom: 1 to: buf size - 5. rec addLast: buf]]. ^ rec ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 12/14/2004 23:50'! patchWindowsVM: vmFileName quitMessage: aString "Modify the Squeak application (the .exe file) for Windows to display the given string when you quit. You must start with a copy of the original VM, not one that has already been modified. This file will be modified in place, so make sure you keep a copy of the original!! The string is truncated to 27 characters since it must fit into the space of the original quit message." "self patchWindowsVM: 'Scratch.exe' quitMessage: 'Want to quit Scratch now?'" | orig firstOrigByte replacement f found startPos | orig _ 'Quit Squeak without saving?' asByteArray. firstOrigByte _ orig first. replacement _ aString asByteArray copyFrom: 1 to: (aString size min: orig size). replacement size < orig size ifTrue: [ "pad with zeros to the same size as the original" replacement _ replacement, (ByteArray new: orig size - replacement size withAll: 0)]. f _ (FileStream oldFileNamed: vmFileName) binary. found _ false. [f atEnd | found] whileFalse: [ [f atEnd not and: [f next ~= firstOrigByte]] whileTrue. "scan for first byte" startPos _ f position - 1. f position: startPos. found _ true. 1 to: orig size do: [:i | f next = (orig at: i) ifFalse: [found _ false]]]. found ifTrue: [ "over-write the original message with the replacement" f position: startPos. replacement do: [:byte | f nextPut: byte]]. f position: f size. "position to end of file to avoid possible file truncation" f close. found ifTrue: [self inform: 'New quit message installed'] ifFalse: [self inform: 'Original quit message not found; no change made']. ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'MD 2/12/2004 18:27'! putInClipboard: anObject Clipboard _ anObject.! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 12/31/2006 18:38'! quitFromMenu "The user is using the application menu to quit from Scratch. If there is an open Scratch window, ask the user about saving the project. Otherwise, confirm that the user really wants to quit." | scratchWindow | scratchWindow _ World submorphs detect: [:m | (m isKindOf: self)] ifNone: [nil]. scratchWindow ifNil: [World hands first quitSession] ifNotNil: [scratchWindow quitScratch]. ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 11/8/2006 18:09'! scaledFormForPaintEditor: aForm "Answer either the given form or a copy of it scaled down to fit into the paint editor." "This method supports a quick fix to the following problem: When a big image is loaded onto a sprite and then edited in the image editor, (a) you cannot edit offscreen pixels and (b) if you click 'OK' to accept the edits, the image will be cropped. This fix resizes the loaded image to fit into paint editor canvas to avoid these problems. In the longer term, we should fix the paint editor to allow editing images larger than the canvas size." | maxExtent scale | maxExtent _ WorkpaneExtent. ((aForm width <= maxExtent x) and: [aForm height <= maxExtent y]) ifTrue: [^ aForm]. scale _ (maxExtent x / aForm width) min: (maxExtent y / aForm height). ^ ScratchPlugin scale: aForm by: scale ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 12/31/2006 13:18'! scratchServers ^ ScratchServers ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 4/14/2008 17:39'! scriptsPaneColor | c | c _ Color r: 124 g: 128 b: 131 range: 255. self isXO ifTrue: [c _ c mixed: 0.8 with: Color white]. ^ c ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 3/17/2009 22:02'! setVisibleDrives: driveListString "Set my set of visible drives. If the argument is nil, then all drives are made visible." | in drive | VisibleDrives _ nil. driveListString ifNil: [^ self]. VisibleDrives _ OrderedCollection new. in _ ReadStream on: driveListString. [in atEnd] whileFalse: [ in skipSeparators. drive _ (in upTo: $,) withBlanksTrimmed. drive size > 0 ifTrue: [VisibleDrives addLast: drive asUppercase]]. VisibleDrives size > 0 ifTrue: [VisibleDrives _ VisibleDrives asArray] ifFalse: [VisibleDrives _ nil]. ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 7/19/2004 20:20'! useErrorCatcher ^ UseErrorCatcher ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 8/12/2008 19:32'! version ^ Version ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 8/13/2008 15:13'! version: aString "self version: '1.0 (internal ', (Date today printFormat: #(1 2 3 $- 2 2)), ')'" "self version: '1.0'" Version _ aString. VersionDate _ Date today printFormat: #(3 2 1 $- 1 1). ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 8/13/2008 15:13'! versionDate ^ VersionDate ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 9/27/2007 15:52'! visibleDrives "For Win32. Answer a list of visible drive names or nil. If nil, then all drives are visble." ^ VisibleDrives ! ! !ScratchFrameMorph class methodsFor: 'utilities' stamp: 'jm 4/4/2005 08:09'! workpaneExtent "Answer the extent of the work pane." ^ WorkpaneExtent ! ! I display a graph of numbers, normalized so the full range of values just fits my height. I support a movable cursor that can be dragged with the mouse. Implementation notes: Some operations on me may be done at sound sampling rates (e.g. 11-44 thousand times/second). To allow such high bandwidth application, certain operations that change my appearance do not immediately report a damage rectangle. Instead, a flag is set indicating that my display needs to refreshed and a step method reports the damage rectangle if that flag is set. Also, I cache a bitmap of my graph to allow the cursor to be moved without redrawing the graph. All indices, like startIndex, cursor, etc are in terms of the graph data. IMPORTANT!! The current implementation cannot stand alone, it needs to be a submorph of SoundEditor. ! !ScratchGraphMorph methodsFor: 'initialization' stamp: 'LY 7/31/2003 11:55'! addRuler ruler _ ScratchRulerMorph graphMorph: self. self addMorphBack: ruler. ruler position: self bottomLeft. ! ! !ScratchGraphMorph methodsFor: 'initialization'! initialize super initialize. self color: (Color r: 0.8 g: 0.8 b: 0.6). self extent: 365@80. self borderWidth: 2. dataColor _ Color darkGray. cursorColor _ Color red. playCursorColor _ Color blue. cursorColorAtZeroCrossings _ Color red. startIndex _ 1. viewer _ false. selection _ {nil. nil}. scale _ 1.0. hasChanged _ false. posVals _ negVals _ nil. self data: ((0 to: 360 - 1) collect: [:x | (100.0 * (x degreesToRadians sin)) asInteger]). ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! color: aColor super color: aColor. self flushCachedForm. ! ! !ScratchGraphMorph methodsFor: 'accessing'! cursor ^ editor cursor ! ! !ScratchGraphMorph methodsFor: 'accessing'! cursor: aNumber editor cursor: aNumber. ! ! !ScratchGraphMorph methodsFor: 'accessing'! cursorAtEnd ^editor cursor truncated >= data size ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! cursorColor ^ cursorColor ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! cursorColor: aColor cursorColor _ aColor. self flushCachedForm. ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! cursorColorAtZeroCrossing ^ cursorColorAtZeroCrossings ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! cursorColorAtZeroCrossings: aColor cursorColorAtZeroCrossings _ aColor. self flushCachedForm. ! ! !ScratchGraphMorph methodsFor: 'accessing'! cursorWrapped: aNumber | sz | editor cursor ~= aNumber ifTrue: [ editor cursor: aNumber. sz _ data size. sz = 0 ifTrue: [editor cursor: 1] ifFalse: [ ((editor cursor >= (sz + 1)) or: [editor cursor < 0]) ifTrue: [ editor cursor: editor cursor - ((editor cursor // sz) * sz)]. editor cursor < 1 ifTrue: [editor cursor: sz + editor cursor]]. "assert: 1 <= cursor < data size + 1" hasChanged _ true]. ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 9/10/2004 20:21'! customScale "Called when the user wants to input a scale value." | answer | answer _ FillInTheBlank request: 'Please type desired scale:' initialAnswer: '2x'. answer size = 0 ifTrue: [^ self]. answer _ answer copyWithout: $x. self zoom: answer asNumber. ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! data ^ data ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! data: aCollection data _ aCollection. maxVal _ minVal _ 0. data do: [:x | x < minVal ifTrue: [minVal _ x]. x > maxVal ifTrue: [maxVal _ x]]. self flushCachedForm. ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! dataColor ^ dataColor ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! dataColor: aColor dataColor _ aColor. self flushCachedForm. ! ! !ScratchGraphMorph methodsFor: 'accessing'! editor ^editor! ! !ScratchGraphMorph methodsFor: 'accessing'! editor: aSoundEditor editor _ aSoundEditor.! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/28/2003 10:11'! interpolatedValueAtCursor | sz prev frac next | data isEmpty ifTrue: [^ 0]. sz _ data size. owner cursor < 0 ifTrue: [^ data at: 1]. "just to be safe, though cursor shouldn't be negative" prev _ owner cursor truncated. frac _ owner cursor - prev. prev < 1 ifTrue: [prev _ sz]. prev > sz ifTrue: [prev _ 1]. "assert: 1 <= prev <= sz" frac = 0 ifTrue: [^ data at: prev]. "no interpolation needed" "interpolate" next _ prev = sz ifTrue: [1] ifFalse: [prev + 1]. ^ ((1.0 - frac) * (data at: prev)) + (frac * (data at: next)) ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! lastValue data size = 0 ifTrue: [^ 0]. ^ data last ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! lastValue: aNumber self appendValue: aNumber. ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'LY 7/31/2003 12:09'! ruler ^ruler.! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'LY 7/30/2003 10:54'! scale ^scale.! ! !ScratchGraphMorph methodsFor: 'accessing'! scale: aNumber "setting the absolute scale of how the graph is display. It is relative to MinScale, the minimum scale possible, which is the scale value of the viewer." aNumber < 1 ifTrue: [^self]. scale _ (aNumber*MinScale) asFloat min: 1. self calculateDataArray. self flushCachedForm; changed. editor viewer flushCachedForm; changed. editor updateSlider. ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! selection ^ selection ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'LY 7/26/2003 16:48'! selection: anArrayOrNil "Set the selection to the given (startIndex, stopIndex) pair to to nil." anArrayOrNil ifNil: [ selection at: 1 put: nil. selection at: 2 put: nil] ifNotNil: [ selection _ anArrayOrNil.]. ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! startIndex ^ startIndex ! ! !ScratchGraphMorph methodsFor: 'accessing' stamp: 'jm 7/12/2003 17:23'! startIndex: aNumber startIndex ~= aNumber ifTrue: [ startIndex _ aNumber asInteger. self flushCachedForm]. ! ! !ScratchGraphMorph methodsFor: 'accessing'! valueAtCursor data isEmpty ifTrue: [^ 0]. ^ data at: ((editor cursor truncated max: 1) min: data size). ! ! !ScratchGraphMorph methodsFor: 'accessing'! valueAtCursor: aPointOrNumber data isEmpty ifTrue: [^ 0]. data at: ((editor cursor truncated max: 1) min: data size) put: (self asNumber: aPointOrNumber). self flushCachedForm. ! ! !ScratchGraphMorph methodsFor: 'accessing'! viewer: aBoolean viewer _ aBoolean. "whether or not this graphMorph is the viewer graph Morph" viewer ifFalse: [self color: Color white.].! ! !ScratchGraphMorph methodsFor: 'drawing'! drawOn: aCanvas | c | cachedForm = nil ifTrue: [ c _ FormCanvas extent: bounds extent. c translateBy: bounds origin negated during:[:tempCanvas| self drawDataOn: tempCanvas]. cachedForm _ c form]. aCanvas paintImage: cachedForm at: bounds origin. self drawCursorOn: aCanvas. self drawPlayCursorOn: aCanvas. ! ! !ScratchGraphMorph methodsFor: 'drawing' stamp: 'LY 7/25/2003 17:31'! hasChanged: aBoolean hasChanged _ aBoolean.! ! !ScratchGraphMorph methodsFor: 'change reporting' stamp: 'jm 7/12/2003 17:23'! layoutChanged super layoutChanged. cachedForm _ nil. ! ! !ScratchGraphMorph methodsFor: 'events' stamp: 'jm 7/12/2003 17:23'! handlesMouseDown: evt ^ true ! ! !ScratchGraphMorph methodsFor: 'events'! mouseDown: evt "Handles mouse down and drag events. Updates the cursor's position and sets the selection to an array containing two copies of the current cursor value." | x s | x _ evt cursorPoint x - (bounds left + borderWidth). s _ editor startSelection. editor startSelection: editor cursor. editor cursor: startIndex + (x/scale). evt shiftPressed ifTrue: [ editor selectionNil ifFalse: [ editor startSelection: s. self adjustSelection.].] ifFalse: [ ((editor selectionNil not) and: [(selection at: 2) - (selection at: 1) > 3]) ifTrue: [ editor selection: nil. self flushCachedForm. self changed.]. editor startSelection: editor cursor. editor selection: {editor cursor. editor cursor}.]. ! ! !ScratchGraphMorph methodsFor: 'events'! mouseMove: evt "Updates the cursor position as the mouse moves. Adjusts the selection only if the mouse is currently being pressed" | x w | x _ evt cursorPoint x - (bounds left + borderWidth). w _ self width - (2 * borderWidth). (viewer not and: [x < 0]) ifTrue: [ editor cursor: startIndex + (x /scale). self adjustSelection. editor slider setValue: (startIndex/data size). ^ self startIndex: self editor cursor]. (viewer not and: [x > w]) ifTrue: [ editor cursor: startIndex + (x /scale). self adjustSelection. editor slider setValue: (startIndex/data size). ^ self startIndex: editor cursor - (w/scale) truncated. "^ editor cursor = data size ifTrue: [ self startIndex: editor cursor - (w/(scale*2)) truncated.] ifFalse: [ self startIndex: editor cursor - (w/scale) truncated.]."]. evt anyButtonPressed ifTrue: [editor cursor: (startIndex + (x/scale) truncated). self adjustSelection.] ! ! !ScratchGraphMorph methodsFor: 'events'! mouseUp: evt ((editor selectionNil not) and: [(selection at: 2) - (selection at: 1) <=3]) ifTrue: [editor selection: nil. editor startSelection: nil.]. ! ! !ScratchGraphMorph methodsFor: 'stepping'! step "Make a deferred damage rectangle if I've changed. This allows applications to call methods that invalidate my display at high-bandwidth without paying the cost of doing the damage reporting on ever call; they can merely set hasChanged to true." super step. hasChanged == nil ifTrue: [hasChanged _ false]. hasChanged ifTrue: [ self changed. hasChanged _ false]. ! ! !ScratchGraphMorph methodsFor: 'stepping' stamp: 'LY 7/26/2003 15:51'! stepTime ^150! ! !ScratchGraphMorph methodsFor: 'menu' stamp: 'jm 7/12/2003 17:23'! addCustomMenuItems: aCustomMenu hand: aHandMorph super addCustomMenuItems: aCustomMenu hand: aHandMorph. aCustomMenu add: 'open wave editor' action: #openWaveEditor. aCustomMenu add: 'read file' action: #readDataFromFile. ! ! !ScratchGraphMorph methodsFor: 'menu'! adjustSelection "Adjust the selection, if any, to the current cursor position. Do nothing if there is no selection." editor selectionNil ifTrue: [^ self]. editor selection: editor cursor scd: editor startSelection. ! ! !ScratchGraphMorph methodsFor: 'menu' stamp: 'jm 1/29/2009 14:57'! openWaveEditor | factor scaledData | self data: data. "make sure maxVal and minVal are current" factor _ 32767 // ((minVal abs max: maxVal abs) max: 1). scaledData _ SoundBuffer newMonoSampleCount: data size. 1 to: data size do: [:i | scaledData at: i put: (factor * (data at: i)) truncated]. (SimpleWaveEditor new data: scaledData; samplingRate: 11025) openInWorld. ! ! !ScratchGraphMorph methodsFor: 'menu' stamp: 'jm 12/4/2003 20:21'! readDataFromFile | result fName | result _ StandardFileMenu oldFileExtensions: #(aif aiff au wav). result ifNil: [^ self]. fName _ result directory pathName, FileDirectory slash, result name. self data: (SampledSound fromFileNamed: fName) samples. ! ! !ScratchGraphMorph methodsFor: 'viewing'! computeSlider editor slider sliderThickness: ((bounds width//scale)/data size)*(editor slider extent x). editor slider changed.! ! !ScratchGraphMorph methodsFor: 'viewing'! setScale | menu choice | menu _ CustomMenu new. menu add: '1x' action: '1'; add: '2x' action: '2'; add: '3x' action: '3'; add: '4x' action: '4'; add: '5x' action: '5'; add: 'other' action: #customScale. choice _ menu startUp. choice ifNil: [^self]. choice = #customScale ifFalse: [self scale: choice asNumber.] ifTrue: [ self customScale].! ! !ScratchGraphMorph methodsFor: 'viewing'! viewSelection | diff | selection ifNil: [^self]. scale _ (MinScale*(data size/(selection second - selection first)) asFloat min: 1). self calculateDataArray. diff _ (selection second - selection first) - bounds width. diff < 0 ifTrue: [ self startIndex: selection first asInteger + (diff//2)] ifFalse: [self startIndex: selection first asInteger.]. editor updateSlider. ! ! !ScratchGraphMorph methodsFor: 'viewing'! zoom: aNumber scale _ ((scale*aNumber asFloat) max: MinScale asFloat) min: 1. self calculateDataArray. self flushCachedForm; changed. editor viewer flushCachedForm; changed. editor updateSlider.! ! !ScratchGraphMorph methodsFor: 'viewing' stamp: 'LY 7/30/2003 15:19'! zoomIn self zoom: 2.! ! !ScratchGraphMorph methodsFor: 'viewing' stamp: 'LY 7/30/2003 15:21'! zoomOut self zoom: 0.5.! ! !ScratchGraphMorph methodsFor: 'commands' stamp: 'jm 7/12/2003 17:23'! appendValue: aPointOrNumber | newVal | (data isKindOf: OrderedCollection) ifFalse: [data _ data asOrderedCollection]. newVal _ self asNumber: aPointOrNumber. data addLast: newVal. newVal < minVal ifTrue: [minVal _ newVal]. newVal > maxVal ifTrue: [maxVal _ newVal]. self cursor: data size. self flushCachedForm. ! ! !ScratchGraphMorph methodsFor: 'commands'! centerCursor "Scroll so that the cursor is as close as possible to the center of my window." | w | w _ self width - (2 * borderWidth). self startIndex: ((editor cursor - (w // (scale*2))) max: 1). ! ! !ScratchGraphMorph methodsFor: 'commands' stamp: 'jm 7/12/2003 17:23'! clear self startIndex: 1. self cursor: 1. self data: OrderedCollection new. ! ! !ScratchGraphMorph methodsFor: 'commands' stamp: 'jm 7/12/2003 17:23'! loadSineWave self loadSoundData: FMSound sineTable. ! ! !ScratchGraphMorph methodsFor: 'commands' stamp: 'jm 7/12/2003 17:23'! loadSound: aSound self loadSoundData: aSound samples. ! ! !ScratchGraphMorph methodsFor: 'commands' stamp: 'jm 2/3/2009 14:17'! loadSoundData: aCollection | factor absV newData | factor _ 0. aCollection do: [:v | (absV _ v abs) > factor ifTrue: [scale _ absV]]. scale _ 100.0 / factor. newData _ OrderedCollection new: aCollection size. 1 to: aCollection size do: [:i | newData addLast: (factor * (aCollection at: i))]. self data: newData. self startIndex: 1. self cursor: 1. ! ! !ScratchGraphMorph methodsFor: 'commands' stamp: 'jm 2/3/2009 14:19'! playOnce | factor absV scaledData | data isEmpty ifTrue: [^ self]. "nothing to play" factor _ 1. data do: [:v | (absV _ v abs) > factor ifTrue: [factor _ absV]]. factor _ 32767.0 / factor. scaledData _ SoundBuffer newMonoSampleCount: data size. 1 to: data size do: [:i | scaledData at: i put: (factor * (data at: i)) truncated]. (SampledSound samples: scaledData samplingRate: 11025) play. ! ! !ScratchGraphMorph methodsFor: 'commands' stamp: 'jm 7/12/2003 17:23'! reverse data _ data reversed. self flushCachedForm. ! ! !ScratchGraphMorph methodsFor: 'private'! calculateDataArray | currIndex neg pos | negVals _ OrderedCollection new. posVals _ OrderedCollection new. data isEmpty ifTrue: [^ self]. currIndex_ neg_pos _ 0. (1 to: data size) do: [ :i | ((i * scale) truncated > currIndex) ifTrue: [ currIndex _ (i*scale) truncated. neg _ neg min: (data at: i). pos _ pos max: (data at: i). posVals add: pos. negVals add: neg. pos _ neg _ 0.] ifFalse: [ neg _ neg min: (data at: i). pos _ pos max: (data at: i).].].! ! !ScratchGraphMorph methodsFor: 'private'! drawCursorOn: aCanvas | ptr x r c | ptr _ (editor cursor asInteger max: 1) min: data size. c _ cursorColor. ((ptr > 1) and: [ptr < data size]) ifTrue: [ (data at: ptr) sign ~= (data at: ptr + 1) sign ifTrue: [c _ cursorColorAtZeroCrossings]]. r _ self innerBounds. x _ r left + ((ptr - startIndex)*scale). ((x >= r left) and: [x <= r right]) ifTrue: [ aCanvas fillRectangle: (x@r top corner: x + 1@r bottom) color: c]. ! ! !ScratchGraphMorph methodsFor: 'private'! drawDataOn: aCanvas | x start end left right yScale baseLine top bottom | super drawOn: aCanvas. viewer ifTrue: [self drawViewOn: aCanvas.]. self drawSelectionOn: aCanvas. (posVals isNil) ifTrue: [^ self]. maxVal = minVal ifTrue: [yScale _ 1.] ifFalse: [yScale _ (bounds height - (2 * borderWidth)) asFloat / (maxVal - minVal)]. baseLine _ bounds bottom - borderWidth + (minVal * yScale) truncated. left _ 0. right _ 10. x _ bounds left + borderWidth. start _ (startIndex*scale) truncated min: data size max: 1. end _ start + bounds width min: data size. start to: end do: [:i | i > posVals size ifTrue: [^self]. left _ x truncated. right _ x + 1. right > (bounds right - borderWidth) ifTrue: [^ self]. top _ baseLine min: (baseLine - (yScale*(posVals at: i))) truncated. bottom _ (baseLine max: baseLine - (yScale*(negVals at: i))) truncated. aCanvas fillRectangle: (left@top corner: right@bottom) color: dataColor. x _ x + 1].! ! !ScratchGraphMorph methodsFor: 'private'! drawPlayCursorOn: aCanvas | ptr x r c | editor playCursor ifNil: [^self]. (editor endPlaying ~= data size) & (editor playCursor >= (editor endPlaying)) ifTrue: [^self]. ptr _ (editor playCursor asInteger max: 1) min: data size. c _ cursorColor. r _ self innerBounds. x _ r left + ((ptr - startIndex)*scale). ((x >= r left) and: [x <= r right]) ifTrue: [ aCanvas fillRectangle: (x@r top corner: x + 1@r bottom) color: c]. ! ! !ScratchGraphMorph methodsFor: 'private'! drawSelectionOn: aCanvas | x y lightColor darkColor v1 v2 offsetX s1 s2 bottom | editor selectionNil ifTrue: [^ self]. lightColor _ Color lightBlue. "(Color r: 0.2 g: 1.0 b: 0.907)." darkColor _ lightColor darker darker darker. v1 _ (editor graph startIndex asInteger max: 1) min: data size. v2 _ v1 + (bounds width/(editor graph scale)) min: data size. offsetX _ bounds left + borderWidth. x _ (offsetX + ((selection first - startIndex)*scale)). y _ bounds top + borderWidth. viewer ifFalse: [ selection first > v2 ifTrue: [^ self]. "selection is not visible" selection last < v1 ifTrue: [^ self]. "selection is not visible" aCanvas fillRectangle: (x@y extent: ((selection last - selection first)*scale)@(self height - (2 * borderWidth))) color: lightColor. "lightYellow"] ifTrue: [ s1 _ selection first. s2 _ selection second. bottom _ self height - (2 * borderWidth). ((s1 max: s2) <= v1) | ((s1 min: s2) >= v2) ifTrue: [^aCanvas fillRectangle: ((offsetX + (s1*scale))@y extent: ((s2-s1)*scale)@bottom) color: darkColor.]. (s1 <= v1) & (s2 >= v2) ifTrue: [^aCanvas fillRectangle: ((offsetX + (s1*scale))@y extent: ((v1-s1)*scale)@bottom) color: darkColor; fillRectangle: ((offsetX + (v1*scale))@y extent: ((v2-v1)*scale)@bottom) color: lightColor; fillRectangle: (( offsetX + (v2*scale))@y extent: ((s2-v2)*scale)@bottom) color: darkColor.]. (s1 >= v1) & (s2 <= v2) ifTrue: [^aCanvas fillRectangle: ((offsetX + (s1*scale))@y extent: ((s2-s1)*scale)@bottom) color: lightColor.]. (s1 < v1) & (s2 > v1) & (s2 < v2) ifTrue: [^aCanvas fillRectangle: ((offsetX + (s1*scale))@y extent: ((v1-s1)*scale)@bottom) color: darkColor; fillRectangle: ((offsetX + (v1*scale))@y extent: ((s2- v1)*scale)@bottom) color: lightColor.]. (s1 >= v1) & (s2 >= v2) ifTrue: [^aCanvas fillRectangle: ((offsetX + (s1*scale))@y extent: ((v2-s1)*scale)@bottom) color: lightColor; fillRectangle: ((offsetX + (v2*scale))@y extent: ((s2 - v2)*scale)@bottom) color: darkColor.]. "Transcript show: 'no category :(';cr." ]. ! ! !ScratchGraphMorph methodsFor: 'private'! drawViewOn: aCanvas "highlights the part of the graph morph we're viewing." | y ex start x | viewer ifFalse: [^self]. start _ ((editor graph startIndex*bounds width)/data size) truncated min: data size. ex_ ((editor slider sliderThickness/editor slider extent x)*(bounds width)) min: data size. x _ (bounds left + borderWidth + start). y _ bounds top + borderWidth. aCanvas fillRectangle: (x@y extent: ex@(self height - (2 * borderWidth))) color: Color white. "lightYellow". ! ! !ScratchGraphMorph methodsFor: 'private' stamp: 'LY 7/25/2003 17:31'! flushCachedForm cachedForm _ nil. hasChanged _ true. ! ! !ScratchGraphMorph methodsFor: 'private' stamp: 'LY 8/1/2003 13:18'! keepIndexInView: index | w newStart | w _ bounds width - (2 * borderWidth). index < startIndex ifTrue: [ newStart _ index - w//scale + 1. ^ self startIndex: (newStart max: 1)]. index > (startIndex + w//scale) ifTrue: [ ^ self startIndex: (index min: data size)]. ! ! !ScratchGraphMorph class methodsFor: 'instance creation' stamp: 'LY 7/30/2003 11:52'! MinScale ^MinScale! ! !ScratchGraphMorph class methodsFor: 'instance creation' stamp: 'jm 9/5/2006 18:27'! MinScale: aNumber "aNumber should be equal to: viewer extent x/ graph data size" MinScale _ aNumber asFloat. ! ! !ScratchGraphMorph class methodsFor: 'instance creation' stamp: 'jm 7/12/2003 17:23'! openOn: dataCollection "Open a new GraphMorph on the given sequencable collection of data." ^ (self new data: dataCollection) openInWorld ! ! I am a Scratch library viewer. I display thumbnails of all the Scratch objects that exist in the world including the stage. Clicking on one of these thumbnails views the associated scratch object. ! !ScratchLibraryMorph methodsFor: 'initialization' stamp: 'jm 5/1/2009 10:45'! buildPanes "Build my scroll pane." | bin | stagePane _ Morph new color: Color transparent; position: self position + (7@0). bin _ ScratchSpriteLibraryMorph new color: Color transparent; borderWidth: 0. scrollPane _ ScrollFrameMorph2 new color: Color transparent; contents: bin; showHorizontalScrollbar: false. spritePane _ Morph new color: Color gray; position: self position. spriteLabel _ self buildSpriteLabel. buttonPane _ self makeNewSpriteButtons: (self ownerThatIsA: ScratchFrameMorph). self addMorph: spritePane. self addMorph: spriteLabel. self addMorph: buttonPane. self addMorph: scrollPane. self addMorph: stagePane. ! ! !ScratchLibraryMorph methodsFor: 'initialization' stamp: 'jm 5/19/2009 13:48'! buildSpriteLabel ^ StringMorph new font: (ScratchFrameMorph getFont: #Tab); color: Color white; contents: 'New sprite:' localized ! ! !ScratchLibraryMorph methodsFor: 'initialization' stamp: 'jm 5/1/2009 10:50'! clearLibrary "Remove all library items. My step method will re-add items for existing objects." | sFrame | stagePane removeAllMorphs. scrollPane contents removeAllMorphs. scrollPane vScrollRelative: 0. spriteLabel delete. spriteLabel _ self buildSpriteLabel. sFrame _ self ownerThatIsA: ScratchFrameMorph. (sFrame isNil or: [sFrame viewMode = #normal]) ifTrue: [ self addMorph: spriteLabel]. buttonPane delete. buttonPane _ self makeNewSpriteButtons: sFrame. self addMorph: buttonPane. topSectionHeight _ ((spriteLabel height + 10) max: 40). self fixLayout. ! ! !ScratchLibraryMorph methodsFor: 'initialization' stamp: 'jm 5/1/2009 10:51'! initialize super initialize. self buildPanes. self initFrontFromForm: (ScratchFrameMorph skinAt: 'spriteLibraryPaneFrameTransparent2') topSectionHeight: ((spriteLabel height + 10) max: 40). self middleBarLeftMargin: 0 rightMargin: 6. self extent: 200@100. ! ! !ScratchLibraryMorph methodsFor: 'initialization' stamp: 'jm 3/20/2009 16:59'! makeNewSpriteButtons: aScratchFrameMorph "Return a morph containing a set of new sprite buttons." | panel buttonSpecs buttons button butonExtent x | panel _ Morph new color: Color transparent. buttonSpecs _ #( " icon name selector tooltip" (newSpritePaint paintSpriteMorph 'Paint new sprite') (newSpriteLibrary addSpriteMorph 'Choose new sprite from file') (newSpriteSurprise surpriseSpriteMorph 'Get surprise sprite') ). buttons _ buttonSpecs collect: [:spec | button _ ToggleButton new onForm: (ScratchFrameMorph skinAt: (spec at: 1)) offForm: (ScratchFrameMorph skinAt: (spec at: 1)). button target: aScratchFrameMorph; actionSelector: (spec at: 2); setProperty: #balloonText toValue: (spec at: 3) localized. button]. butonExtent _ ScratchFrameMorph isXO ifTrue: [37@27] ifFalse: [37@27]. x _ 0. buttons do: [:b | b extent: butonExtent. panel addMorph: (b position: x@1). x _ x + 5 + b width]. panel extent: x@(butonExtent y + 1). ^ panel ! ! !ScratchLibraryMorph methodsFor: 'accessing' stamp: 'ee 11/10/2008 14:10'! spriteButtonsTarget: anObject buttonPane submorphs do: [:m | (m isKindOf: ToggleButton) ifTrue: [ m target: anObject]].! ! !ScratchLibraryMorph methodsFor: 'accessing' stamp: 'tis 9/11/2006 10:46'! spriteThumbnails ^ scrollPane contents submorphs ! ! !ScratchLibraryMorph methodsFor: 'geometry' stamp: 'ee 3/20/2009 14:43'! extent: aRectangle | nameHeight | super extent: aRectangle. scrollPane isNil | stagePane isNil ifTrue: [^ self]. thumbWidth _ 50. nameHeight _ (ScratchTranslator stringExtent: ('Sprite1' localized) font: (ScratchFrameMorph getFont: #LibraryItemName)) y. itemExtent _ 54@(54 + nameHeight). stagePane width: itemExtent x + 0. scrollPane left: stagePane right. self fixLayout. ! ! !ScratchLibraryMorph methodsFor: 'geometry' stamp: 'ee 12/30/2008 13:55'! fixLayout "Position and size thumbnails." | libPane x y rMargin sFrame | scrollPane isNil | stagePane isNil | spritePane isNil ifTrue: [^ self]. spritePane width: self width. spritePane height: topSectionHeight. spriteLabel position: (self left + 15)@(self top + (topSectionHeight // 2) - (spriteLabel height // 2) + 3). sFrame _ self ownerThatIsA: ScratchFrameMorph. (sFrame isNil or: [sFrame viewMode = #normal]) ifTrue: [buttonPane position: (spriteLabel right + 10)@(self top + (topSectionHeight // 2) - (buttonPane height // 2) + 3)] ifFalse: [buttonPane position: (self left + 12)@(self top + (topSectionHeight // 2) - (buttonPane height // 2) + 3)]. stagePane height = self height ifFalse: [ stagePane height: self height]. scrollPane height = self height ifFalse: [ scrollPane height: self height - topSectionHeight]. scrollPane width = (self right - stagePane right) ifFalse: [ scrollPane width: self right - stagePane right]. scrollPane position: scrollPane left @ (self top + topSectionHeight + 3). stagePane position: stagePane left @ (self top + topSectionHeight + 3). libPane _ scrollPane contents. ScratchTranslator isRTL ifTrue: [x _ libPane left + 30] ifFalse: [x _ libPane left + 8]. y _ libPane top + 7. rMargin _ x + scrollPane width - 5. libPane submorphs do: [:m | (x + m width) > rMargin ifTrue: [ ScratchTranslator isRTL ifTrue: [x _ libPane left + 30] ifFalse: [x _ libPane left + 8]. y _ y + m height]. m position: x@y. x _ x + m width]. stagePane submorphCount > 0 ifTrue: [ m _ stagePane firstSubmorph. m position: stagePane topLeft + ((stagePane extent - stagePane firstSubmorph extent) // 2) + (0@15) - (0@topSectionHeight)]. ! ! !ScratchLibraryMorph methodsFor: 'drawing' stamp: 'ee 11/10/2008 14:16'! drawBackgroundOn: aCanvas "Draw my background." | r | "color isTransparent ifTrue: [^ self]." color _ Color gray darker. aCanvas fillRectangle: (myBox insetBy: 7) color: color. r _ ((stagePane right + 3) @ (myBox top + topSectionHeight + 12)) extent: (1 @ (myBox height - topSectionHeight - 30)). aCanvas fillRectangle: r color: (Color gray: 0.3). r _ (r origin + (1@0)) extent: (1 @ r height). aCanvas fillRectangle: r color: Color gray. ! ! !ScratchLibraryMorph methodsFor: 'stepping' stamp: 'jm 12/22/2008 13:01'! step "Update my thumbnail list if morphs have been added or deleted." | sFrame workPane morphsWithThumbnails doLayout workPaneObjects | (sFrame _ self ownerThatIsA: ScratchFrameMorph) ifNil: [^ self]. sFrame paintingInProgress ifTrue: [^ self] ifFalse: [scrollPane owner ifNil: [self addMorph: scrollPane]]. workPane _ sFrame workPane. "remove thumbnails for any objects that have been deleted" morphsWithThumbnails _ IdentitySet new. doLayout _ false. scrollPane contents submorphs do: [:m | (m target isNil or: [m target owner isNil]) ifTrue: [m delete. doLayout _ true] ifFalse: [morphsWithThumbnails add: m target]]. stagePane submorphs do: [:m | (m target isNil or: [m target owner isNil]) ifTrue: [m delete. doLayout _ true] ifFalse: [morphsWithThumbnails add: m target]]. "add thumbnail for the background, if necessary" (morphsWithThumbnails includes: workPane) ifFalse: [ self addThumbnailFor: workPane. doLayout _ true]. "add thumbnails for any new objects" workPaneObjects _ workPane sprites select: [:s | (s isKindOf: ScriptableScratchMorph) and: [s isScriptable & s isClone not]]. workPaneObjects do: [:m | (morphsWithThumbnails includes: m) ifFalse: [ self addThumbnailFor: m. doLayout _ true]]. doLayout ifTrue: [ self changed. self fixLayout. self isInWorld ifTrue: [self world startSteppingSubmorphsOf: self]]. self updateHighlight. ! ! !ScratchLibraryMorph methodsFor: 'stepping' stamp: 'jm 6/30/2004 14:17'! stepTime ^ 500 ! ! !ScratchLibraryMorph methodsFor: 'private' stamp: 'nb 3/30/2008 01:49'! addThumbnailFor: aMorph "Add a thumbnail for the given morph." | newThumbnail | newThumbnail _ LibraryItemMorph new. newThumbnail extent: itemExtent thumbWidth: thumbWidth. newThumbnail target: aMorph. (newThumbnail target isKindOf: ScratchStageMorph) ifTrue: [stagePane addMorph: newThumbnail] ifFalse: [scrollPane contents addMorphBack: newThumbnail]. newThumbnail step. ! ! !ScratchLibraryMorph methodsFor: 'private' stamp: 'jm 11/21/2006 16:02'! unhighlightAll scrollPane contents submorphs do: [:m | m highlight: false]. stagePane submorphs do: [:m | m highlight: false]. ! ! !ScratchLibraryMorph methodsFor: 'private' stamp: 'jm 11/21/2006 16:02'! updateHighlight | sFrame viewedObj | (sFrame _ self ownerThatIsA: ScratchFrameMorph) ifNil: [^ self]. viewedObj _ sFrame viewerPane target. scrollPane contents submorphs do: [:m | m highlight: (m target = viewedObj)]. stagePane submorphs do: [:m | m highlight: (m target = viewedObj)]. ! ! A scrollable, ordered list of editable strings. The following are top-level submorphs: titleMorph addItemMorph resizeMorph countMorph scrollPane These morphs appear inside the scroll pane: emptyMorph -- added/removed to indicate when list is empty numberMorphs -- cell numbers (indices) cellMorphs -- list of cells Each cell consists of a frame containing a line-wrapping string morph. ! !ScratchListMorph methodsFor: 'initialization' stamp: 'jm 6/28/2008 12:37'! addScrollPane scrollPane _ ScrollFrameMorph2 new vBarInset: 16; hBarInset: 18; color: Color transparent; growthFraction: 0.0; scrollbarStartInset: 2 endInset: -2; contents: (Morph new color: Color transparent). self addMorph: scrollPane. ! ! !ScratchListMorph methodsFor: 'initialization' stamp: 'jm 7/11/2008 14:58'! addTitleAndControls titleMorph _ StringMorph new contents: 'myList'; font: (ScratchFrameMorph getFont: #Label). addItemMorph _ (ImageMorph new form: (ScratchFrameMorph skinAt: #addItem)). resizeMorph _ ScratchResizeMorph ofType: #corner. countMorph _ StringMorph new contents: '0'; font: (ScratchFrameMorph getFont: #XYReadout); color: (Color gray: 0.15). "emptyMorph is added to contents pane when list is empty" emptyMorph _ StringMorph new contents: '(empty)' localized; font: (ScratchFrameMorph getFont: #CommentBlock). self addMorph: titleMorph. self addMorph: addItemMorph. self addMorph: resizeMorph. self addMorph: countMorph. ! ! !ScratchListMorph methodsFor: 'initialization' stamp: 'jm 5/31/2008 11:56'! initialize super initialize. self color: (Color r: (193/255) g: (196/255) b: (199/255)); borderWidth: 2; borderColor: self normalBorderColor; useRoundedCorners. cellMorphs _ OrderedCollection new: 1000. numberMorphs _ #(). listLayoutNeeded _ true. focusIndex _ 0. lastActivityError _ false. highlightActive _ false. self addTitleAndControls. self addScrollPane. self extent: 65@115. ! ! !ScratchListMorph methodsFor: 'initialization' stamp: 'jm 7/11/2008 15:01'! listName: asString target: aScriptableSpriteMorph listName _ asString asUTF8. target _ aScriptableSpriteMorph. self updateTitle. ! ! !ScratchListMorph methodsFor: 'accessing' stamp: 'jm 5/14/2008 18:13'! focusCell: cellMorph focusIndex _ cellMorphs indexOf: cellMorph ifAbsent: [1]. World activeHand newKeyboardFocus: cellMorph firstSubmorph. ! ! !ScratchListMorph methodsFor: 'accessing' stamp: 'ee 5/6/2008 16:04'! focusIndex ^ focusIndex ! ! !ScratchListMorph methodsFor: 'accessing' stamp: 'jm 5/15/2008 14:21'! focusIndex: anInteger | cell | focusIndex _ anInteger. focusIndex > cellMorphs size ifTrue: [focusIndex _ 1]. focusIndex < 1 ifTrue: [focusIndex _ cellMorphs size]. cellMorphs size > 0 ifTrue: [ cell _ cellMorphs at: focusIndex. World activeHand newKeyboardFocus: cell firstSubmorph. scrollPane scrollSubmorphIntoView: cell]. ! ! !ScratchListMorph methodsFor: 'accessing' stamp: 'jm 5/14/2008 18:22'! indexOfCell: cellMorph ^ cellMorphs indexOf: cellMorph ifAbsent: [-1] ! ! !ScratchListMorph methodsFor: 'accessing' stamp: 'jm 6/4/2008 19:44'! listContents ^ cellMorphs collect: [:m | m firstSubmorph contents]. ! ! !ScratchListMorph methodsFor: 'accessing' stamp: 'jm 6/4/2008 19:07'! listName ^ listName ! ! !ScratchListMorph methodsFor: 'accessing' stamp: 'jens 9/22/2008 11:23'! target ^ target ! ! !ScratchListMorph methodsFor: 'dropping/grabbing' stamp: 'jm 5/15/2008 14:32'! delete super delete. ScratchFrameMorph allInstancesDo: [:frame | frame deletingWatcher]. ! ! !ScratchListMorph methodsFor: 'dropping/grabbing' stamp: 'jm 6/4/2008 18:53'! justDroppedInto: aMorph event: evt (aMorph isKindOf: ScratchStageMorph) ifTrue: [ super justDroppedInto: aMorph event: evt. self world ifNotNil: [self world startSteppingSubmorphsOf: self]. ^ self]. "delete me if dropped anywhere but the stage" self position: evt hand formerPosition. ^ self delete ! ! !ScratchListMorph methodsFor: 'event handling' stamp: 'jm 6/4/2008 19:20'! handlesMouseDown: evt ^ self world notNil! ! !ScratchListMorph methodsFor: 'event handling' stamp: 'jm 8/13/2008 20:01'! mouseDown: evt "Handle a mouse click. Left button either drags or performs click action. Right button brings up a menu." | p | resizeOffset _ nil. evt hand toolType = 'CutTool' ifTrue: [ evt shiftPressed ifFalse: [evt hand toolType: nil]. ^ self delete]. World activeHand showTemporaryCursor: nil. evt hand newKeyboardFocus: nil. p _ evt cursorPoint. ((addItemMorph bounds expandBy: 4) containsPoint: p) ifTrue: [ ^ self insertLine: '' at: (self lineCount + 1)]. evt rightButtonPressed ifTrue: [Sensor waitNoButton. ^ self rightButtonMenu] ifFalse: [ ((resizeMorph bounds expandBy: 4) containsPoint: p) ifFalse: [evt hand grabMorph: self] ifTrue: [resizeOffset _ self bottomRight - p]]. ! ! !ScratchListMorph methodsFor: 'event handling' stamp: 'jm 5/15/2008 14:27'! mouseMove: evt resizeOffset ifNotNil: [ self extent: (evt cursorPoint - self topLeft) + resizeOffset]. ! ! !ScratchListMorph methodsFor: 'event handling' stamp: 'ee 7/31/2008 12:56'! rightButtonMenu | menu | menu _ CustomMenu new. menu add: 'export' action: #exportList. menu add: 'import' action: #importList. menu addLine. menu add: 'hide' action: #delete. menu localize. menu labels at: 2 put: ((menu labels at: 2) copyFrom: 1 to: (menu labels at: 2) size - 1), ScratchTranslator ellipsesSuffix. menu invokeOn: self. ! ! !ScratchListMorph methodsFor: 'geometry' stamp: 'jm 5/16/2008 09:12'! extent: aPoint super extent: (aPoint max: 95@115). self fixLayout. ! ! !ScratchListMorph methodsFor: 'geometry' stamp: 'jm 8/13/2008 18:50'! fixLayout titleMorph fitContents. (titleMorph width > (self width - 12)) ifTrue: [ self width: titleMorph width + 12]. titleMorph position: (self center x - (titleMorph width // 2)) @ (self top + 5). countMorph fitContents. scrollPane position: (self left + 2) @ (titleMorph bottom + 3); extent: (self width - 4) @ (self bottom - titleMorph bottom - countMorph height - 12). addItemMorph position: (self left + 3) @ (self bottom - addItemMorph height - 3). resizeMorph position: (self bottomRight - resizeMorph extent). self updateCountMorph. countMorph bottom: self bottom - 3. self updateContents. ! ! !ScratchListMorph methodsFor: 'geometry' stamp: 'ee 5/14/2009 17:33'! fixLayoutForNewLanguage "This method updates the height and word-wrapping of cells after a language or font change." | cellContents oldH | cellMorphs size = 0 ifTrue: [self fixLayout. ^ self showEmpty]. cellContents _ cellMorphs first firstSubmorph. oldH _ cellContents height. cellContents font: cellContents font. cellContents height = oldH ifTrue: [self fixLayout. ^ self]. "no size change" scrollPane vScrollRelative: 0. cellMorphs do: [:c | c firstSubmorph font: c firstSubmorph font]. self fixLayout. ! ! !ScratchListMorph methodsFor: 'stepping' stamp: 'jm 6/4/2008 19:09'! step self updateTitle. self updateBorder. self updateCountMorph. self updateIndexHighlight. listLayoutNeeded ifTrue: [self updateContents]. ! ! !ScratchListMorph methodsFor: 'stepping' stamp: 'jm 5/31/2008 11:46'! stepTime ^ 200 ! ! !ScratchListMorph methodsFor: 'stepping' stamp: 'jm 5/31/2008 13:56'! updateBorder lastActivityError ifTrue: [self borderColor: Color red] ifFalse: [self borderColor: self normalBorderColor]. lastActivityError _ false. ! ! !ScratchListMorph methodsFor: 'stepping' stamp: 'ee 7/6/2008 20:39'! updateCountMorph countMorph contents: 'length' localized, ScratchTranslator colonSuffix, ' ', self lineCount printString. countMorph left: self left + ((self width - countMorph width) // 2) + 3. ! ! !ScratchListMorph methodsFor: 'stepping' stamp: 'jm 5/31/2008 14:43'! updateIndexHighlight "Update the highlighted index." highlightActive ifTrue: [ numberMorphs do: [:m | m color: self indexColor]. highlightActive _ false]. ! ! !ScratchListMorph methodsFor: 'stepping' stamp: 'jm 6/17/2008 21:35'! updateTitle "Update my title if I am owned by a sprite and the sprite name changes." | s | listName ifNil: [^ self]. (target isKindOf: ScratchSpriteMorph) ifTrue: [s _ target objName, ' ', listName] ifFalse: [s _ listName]. titleMorph contents = s ifFalse: [ titleMorph contents: s. self fixLayout]. ! ! !ScratchListMorph methodsFor: 'list ops' stamp: 'jm 1/28/2009 11:39'! addLettersOf: anObject anObject asString asUTF32 do: [:ch | cellMorphs addLast: (self createCell: (UTF32 with: ch) asUTF8)]. self noteChangeAtIndex: cellMorphs size. ! ! !ScratchListMorph methodsFor: 'list ops' stamp: 'jm 5/14/2008 14:19'! clear cellMorphs _ cellMorphs species new: 1000. self updateContents. ! ! !ScratchListMorph methodsFor: 'list ops' stamp: 'jm 4/1/2009 10:24'! concatenatedLines | isSingleLetters s | isSingleLetters _ true. cellMorphs do: [:m | isSingleLetters ifTrue: [ m firstSubmorph contents asUTF32 size <= 1 ifFalse: [isSingleLetters _ false]]]. s _ WriteStream on: (UTF8 new: 1000). isSingleLetters ifTrue: [ cellMorphs do: [:m | s nextPutAll: m firstSubmorph contents]] ifFalse: [ cellMorphs do: [:m | s nextPutAll: m firstSubmorph contents; space]. s position > 0 ifTrue: [s skip: -1]]. ^ s contents ! ! !ScratchListMorph methodsFor: 'list ops' stamp: 'jm 2/3/2009 14:20'! contains: aStringOrNumber | s | s _ aStringOrNumber asString asUTF8. cellMorphs do: [:m | s = m firstSubmorph contents ifTrue: [^ true]]. ^ false ! ! !ScratchListMorph methodsFor: 'list ops' stamp: 'ee 5/14/2009 13:59'! createCell: aString | readout cell font numberRight | font _ ScratchFrameMorph getFont: #Watcher. readout _ ListMultilineStringMorph new borderWidth: 0; color: Color transparent; textColor: Color white; growWithText: true; contents: aString font: font. readout height: ((ScratchTranslator stringExtent: 'A' font: font) y) + 6. ScratchTranslator isRTL ifTrue: [numberRight _ self right - 5] ifFalse: [numberRight _ self left + self largestIndexWidth + 6]. ScratchTranslator isRTL ifTrue: [readout width: self right - self left - self largestIndexWidth - 33] ifFalse: [readout width: self right - numberRight - 25]. cell _ WatcherReadoutFrameMorph new color: ScriptableScratchMorph listBlockColor; extent: readout extent + 3; addMorphBack: readout. cell position: scrollPane contents bottomLeft. scrollPane contents addMorph: cell. ^ cell ! ! !ScratchListMorph methodsFor: 'list ops' stamp: 'jm 5/31/2008 15:31'! deleteLineAt: aNumber | index | index _ aNumber asInteger. (index > 0 and: [index <= cellMorphs size]) ifFalse: [ lastActivityError _ true. ^ self]. index = numberMorphs size ifTrue: [ numberMorphs last delete. numberMorphs _ numberMorphs copyFrom: 1 to: numberMorphs size - 1]. (cellMorphs removeAt: index) delete. self noteChangeAtIndex: index. ! ! !ScratchListMorph methodsFor: 'list ops' stamp: 'jm 5/31/2008 14:07'! insertLine: aString at: aNumber | index newCell | index _ aNumber asInteger. (index > 0 and: [index <= (cellMorphs size + 1)]) ifFalse: [ lastActivityError _ true. ^ self]. newCell _ self createCell: aString. index = 1 ifTrue: [cellMorphs addFirst: newCell] ifFalse: [ index > cellMorphs size ifTrue: [cellMorphs addLast: newCell] ifFalse: [cellMorphs add: newCell afterIndex: index - 1]]. self noteChangeAtIndex: index. ^ newCell ! ! !ScratchListMorph methodsFor: 'list ops' stamp: 'jm 7/9/2008 17:10'! lineAt: aNumber | index | index _ aNumber asInteger. (index > 0 and: [index <= cellMorphs size]) ifTrue: [ self noteChangeAtIndex: index. ^ (cellMorphs at: index) firstSubmorph contents asUTF8] ifFalse: [ lastActivityError _ true. ^ '']. ! ! !ScratchListMorph methodsFor: 'list ops' stamp: 'jm 5/13/2008 16:48'! lineCount ^ cellMorphs size ! ! !ScratchListMorph methodsFor: 'list ops' stamp: 'jm 5/31/2008 14:11'! setLineAt: aNumber to: newContents | index | index _ aNumber asInteger. (index > 0 and: [index <= cellMorphs size]) ifFalse: [ lastActivityError _ true. ^ self]. (cellMorphs at: index) firstSubmorph contents: newContents asString. self noteChangeAtIndex: index. ! ! !ScratchListMorph methodsFor: 'import/export' stamp: 'jm 5/21/2009 10:25'! exportList | fName f | fName _ titleMorph contents. fName size <= 1 ifTrue: [fName _ 'newList']. fName _ fName, '.txt'. fName _ ScratchFileChooserDialog chooseNewFileDefault: fName title: 'File Name?' type: #list. fName = #cancelled ifTrue: [^ self]. f _ StandardFileStream newScratchFileNamed: fName. f ifNil: [^ self]. cellMorphs do: [:m | f nextPutAll: m firstSubmorph contents; crlf]. f close. ! ! !ScratchListMorph methodsFor: 'import/export' stamp: 'jm 5/21/2009 10:21'! importList | result | result _ ScratchFileChooserDialog chooseExistingFileType: #list extensions: #(txt) title: 'Import List'. #cancelled = result ifTrue: [^ self]. self importListFromFileNamed: result. ! ! !ScratchListMorph methodsFor: 'import/export' stamp: 'jm 6/25/2009 17:13'! importListFromFileNamed: aFilename | f | f _ FileStream readOnlyFileNamedOrNil: (FileDirectory default fullNameFor: aFilename). f ifNil: [ DialogBoxMorph inform: 'File not found' withDetails: aFilename. ^ nil]. "Assume the file was encoded as UTF8" [ self newContents: (f contentsOfEntireFile lines collect: [:s | UTF8 withAll: s]). ] ifError: [self beep]. ! ! !ScratchListMorph methodsFor: 'object i/o' stamp: 'jm 6/17/2008 21:39'! fieldsVersion ^ 2 ! ! !ScratchListMorph methodsFor: 'object i/o' stamp: 'jm 6/17/2008 21:42'! initFieldsFrom: anObjStream version: classVersion "Note: To simplify my representation in the project file, my submorphs are removed before saving and restored when I am read in from an object file." | strings | super initFieldsFrom: anObjStream version: classVersion. listName _ anObjStream nextField. strings _ anObjStream nextField. classVersion > 1 ifTrue: [target _ anObjStream nextField]. self removeAllMorphs. self addTitleAndControls; addScrollPane. titleMorph contents: listName. self newContents: strings. self fixLayout. self updateContents. ! ! !ScratchListMorph methodsFor: 'object i/o' stamp: 'jm 6/17/2008 21:46'! storeFieldsOn: anObjStream "Note: To simplify my representation in the project file, my submorphs are removed before saving and restored when I am read in from an object file." | strings | strings _ cellMorphs asArray collect: [:m | m firstSubmorph contents]. self removeAllMorphs. numberMorphs _ #(). super storeFieldsOn: anObjStream. anObjStream putField: listName. anObjStream putField: strings. anObjStream putField: target. self addTitleAndControls; addScrollPane. titleMorph contents: listName. self newContents: strings. self fixLayout. ! ! !ScratchListMorph methodsFor: 'private' stamp: 'jm 5/31/2008 13:57'! indexColor ^ Color gray: 0.32 ! ! !ScratchListMorph methodsFor: 'private' stamp: 'jm 5/14/2008 11:05'! largestIndexWidth "Estimate the width needed for the largest cell number." "Note: we estimate using the width of the the digit '0' times the number of digits needed." | digitWidth digitCount n | digitWidth _ (ScratchFrameMorph getFont: #Label) widthOf: $0. n _ cellMorphs size + 1 max: 100. digitCount _ n log ceiling. ^ digitCount * digitWidth ! ! !ScratchListMorph methodsFor: 'private' stamp: 'jm 6/4/2008 17:36'! newContents: listOfStrings "Set my contents to the given collection of strings." scrollPane contents removeAllMorphs; top: scrollPane top; extent: scrollPane extent. cellMorphs _ cellMorphs species new: ((2 * listOfStrings size) max: 1000). numberMorphs _ #(). listOfStrings do: [:s | cellMorphs addLast: (self createCell: s)]. self updateContents. ! ! !ScratchListMorph methodsFor: 'private' stamp: 'jm 5/31/2008 11:04'! normalBorderColor ^ Color r: (148/255) g: (145/255) b: (145/255) ! ! !ScratchListMorph methodsFor: 'private' stamp: 'ee 8/7/2008 20:11'! noteChangeAtIndex: index lastActivityIndex _ index. cellMorphs size < 20 ifTrue: [ self updateContents. (index between: 1 and: numberMorphs size) ifTrue: [ (numberMorphs at: index) color: (Color white). highlightActive _ true]. lastActivityIndex _ nil] ifFalse: [ listLayoutNeeded _ true]. ! ! !ScratchListMorph methodsFor: 'private' stamp: 'jm 5/31/2008 15:38'! scrollActiveCellIntoView | cell page newTop | lastActivityIndex _ lastActivityIndex within: 1 and: cellMorphs size. cell _ cellMorphs at: lastActivityIndex. page _ scrollPane contents. (scrollPane bounds containsRect: cell bounds) ifFalse: [ newTop _ (scrollPane bounds center y) - (cell top - page top). newTop _ newTop max: (scrollPane bottom - (cellMorphs last bottom - page top) - 3). newTop _ newTop min: scrollPane top. page top: newTop]. ! ! !ScratchListMorph methodsFor: 'private' stamp: 'jm 8/13/2008 15:33'! showEmpty "Show the empty label." | page | scrollPane hScrollRelative: 0; vScrollRelative: 0. page _ scrollPane contents removeAllMorphs; extent: scrollPane extent. numberMorphs size > 0 ifTrue: [numberMorphs _ #()]. emptyMorph contents: '(empty)' localized; position: (page center - (emptyMorph extent // 2)). page addMorph: emptyMorph. scrollPane updateScrollbars. listLayoutNeeded _ false. ! ! !ScratchListMorph methodsFor: 'private' stamp: 'ee 8/7/2008 20:11'! updateContents "Update and layout my contents." | page numberRight cellWidth inset x y | cellMorphs size = 0 ifTrue: [^ self showEmpty]. emptyMorph delete. page _ scrollPane contents. page changed. ScratchTranslator isRTL ifTrue: [numberRight _ self right - 5] ifFalse: [numberRight _ self left + self largestIndexWidth + 6]. "word-wrap cell contents and fix cell extents, if needed" ScratchTranslator isRTL ifTrue: [cellWidth _ self right - self left - self largestIndexWidth - 33] ifFalse: [cellWidth _ self right - numberRight - 25]. inset _ 3. cellMorphs do: [:cell | ((cell width ~= cellWidth) or: [cell extent ~= (cell firstSubmorph extent + inset)]) ifTrue: [ cell firstSubmorph width: cellWidth - inset. cell extent: cell firstSubmorph extent + inset]]. "position cells" ScratchTranslator isRTL ifTrue: [x _ self left + 26] ifFalse: [x _ numberRight + 4]. y _ page top + 3. cellMorphs do: [:cell | cell position: x@y. y _ y + cell height - 1]. self updateIndices: numberRight. page extent: (self width - (2 * borderWidth)) @ (y + 3). lastActivityIndex ifNotNil: [ (lastActivityIndex between: 1 and: cellMorphs size) ifTrue: [ (numberMorphs at: lastActivityIndex) color: (Color white). highlightActive _ true]. self scrollActiveCellIntoView. lastActivityIndex _ nil]. scrollPane updateScrollbars. scrollPane updateScrollbars. listLayoutNeeded _ false. ! ! !ScratchListMorph methodsFor: 'private' stamp: 'jm 5/31/2008 13:57'! updateIndices: rightX | s newNumbers page num cell | numberMorphs size > cellMorphs size ifTrue: [ cellMorphs size + 1 to: numberMorphs size do: [:i | (numberMorphs at: i) delete]. numberMorphs _ numberMorphs copyFrom: 1 to: cellMorphs size]. newNumbers _ #(). numberMorphs size < cellMorphs size ifTrue: [ page _ scrollPane contents. s _ StringMorph new color: self indexColor; font: (ScratchFrameMorph getFont: #Label). newNumbers _ (numberMorphs size + 1 to: cellMorphs size) collect: [:i | s fullCopy contents: i printString]. newNumbers do: [:m | page addMorph: m]. numberMorphs _ numberMorphs, newNumbers]. 1 to: cellMorphs size do: [:i | num _ numberMorphs at: i. cell _ cellMorphs at: i. num position: (rightX - num width) @ (cell top + ((cell height - num height) // 2))]. ! ! I define the behavior common to all Scratch media objects. There are subclasses of me for still images, movies, and sounds. All media objects have an editable name. ! !ScratchMedia methodsFor: 'initialization' stamp: 'jm 5/15/2004 22:02'! initialize super initialize. mediaName _ 'untitled'. ! ! !ScratchMedia methodsFor: 'accessing' stamp: 'jm 6/3/2004 18:23'! infoString "Answer a string for this media, typically something about its size." self subclassResponsibility ! ! !ScratchMedia methodsFor: 'accessing' stamp: 'jm 6/29/2004 21:53'! isImage "Answer true if I am an image media object." ^ false ! ! !ScratchMedia methodsFor: 'accessing' stamp: 'jm 5/17/2004 11:09'! isMovie "Answer true if I am a movie media object." ^ false ! ! !ScratchMedia methodsFor: 'accessing' stamp: 'jm 5/10/2004 18:47'! isSound "Answer true if I am a sound media object." ^ false ! ! !ScratchMedia methodsFor: 'accessing' stamp: 'jm 5/4/2004 15:41'! mediaName ^ mediaName ! ! !ScratchMedia methodsFor: 'accessing' stamp: 'tis 8/15/2006 00:50'! mediaName: aString mediaName _ aString. ! ! !ScratchMedia methodsFor: 'accessing' stamp: 'jm 10/20/2007 19:43'! mediaSizeInKilobytes "Returns the storage size of this media in kilobytes, rounded to the nearest kilobyte." ^ 0 ! ! !ScratchMedia methodsFor: 'accessing' stamp: 'jm 5/12/2004 16:26'! mediaType "Answer a string describing this type of media." ^ 'generic' ! ! !ScratchMedia methodsFor: 'accessing' stamp: 'jm 10/17/2007 13:04'! revertToUncompressed "Revert to my uncompressed version after temporary compression (e.g. when uploading a project). This default implementation does nothing."! ! !ScratchMedia methodsFor: 'accessing' stamp: 'jm 6/3/2004 18:21'! thumbnailFormExtent: extent "Answer a Form of the given extent containing a thumbnail of me. This default implementation answers a blank thumbnail." ^ (Form extent: extent depth: 2) fillWhite ! ! !ScratchMedia methodsFor: 'playing' stamp: 'jm 5/14/2004 21:16'! mediaStep "Do one animation or movie step and answer true if my appearance has changed. This default implementation does nothing." ^ false ! ! !ScratchMedia methodsFor: 'playing' stamp: 'jm 5/15/2004 20:47'! resumePlaying "This method is called when switching costumes to resume playing a costume that was playing when it was switched out. This default implementaton does nothing."! ! !ScratchMedia methodsFor: 'playing' stamp: 'jm 5/14/2004 21:07'! startPlaying "Start playing if I am a movie or animation." ! ! !ScratchMedia methodsFor: 'playing' stamp: 'jm 5/14/2004 21:07'! stopPlaying "Stop playing if I am a movie or animation." ! ! !ScratchMedia methodsFor: 'playing' stamp: 'jm 5/15/2004 20:48'! suspendPlaying "This method is called when switching costumes to suspect a playing costume. This default implementaton does nothing."! ! !ScratchMedia methodsFor: 'printing' stamp: 'jm 5/12/2004 16:28'! printOn: aStream aStream nextPutAll: mediaName, ' [', self mediaType, ']'. ! ! !ScratchMedia methodsFor: 'object i/o' stamp: 'jm 6/1/2004 16:57'! initFieldsFrom: anObjStream version: classVersion self initFieldsNamed: #( mediaName ) from: anObjStream. ! ! !ScratchMedia methodsFor: 'object i/o' stamp: 'jm 6/1/2004 16:57'! storeFieldsOn: anObjStream self storeFieldsNamed: #( mediaName ) on: anObjStream. ! ! !ScratchMenuTitleMorph methodsFor: 'initialization' stamp: 'jm 2/13/2009 14:59'! initialize super initialize. self font: (ScratchFrameMorph getFont: #MenuTitle); forceUnicodeRendering: true; color: self normalColor. ! ! !ScratchMenuTitleMorph methodsFor: 'accessing' stamp: 'jm 11/10/2008 12:44'! highlightColor ^ Color white ! ! !ScratchMenuTitleMorph methodsFor: 'accessing' stamp: 'ee 5/1/2009 13:03'! normalColor ^ Color gray: 0.3 ! ! !ScratchMenuTitleMorph methodsFor: 'accessing' stamp: 'jm 11/10/2008 12:48'! selector ^ selector ! ! !ScratchMenuTitleMorph methodsFor: 'accessing' stamp: 'jm 10/28/2008 12:50'! target: anObject selector: aSymbol target _ anObject. selector _ aSymbol. ! ! !ScratchMenuTitleMorph methodsFor: 'event handling' stamp: 'jm 10/28/2008 11:34'! handlesMouseDown: evt ^ true ! ! !ScratchMenuTitleMorph methodsFor: 'event handling' stamp: 'jm 10/28/2008 11:39'! handlesMouseOver: evt ^ true ! ! !ScratchMenuTitleMorph methodsFor: 'event handling' stamp: 'jens 3/7/2009 22:24'! mouseDown: evt target isNil | selector isNil ifTrue: [^ self]. Cursor normal show. MenuBarIsActive _ true. target perform: selector with: self. "invoke my menu" ! ! !ScratchMenuTitleMorph methodsFor: 'event handling' stamp: 'jens 3/7/2009 22:44'! mouseEnter: evt self color: self highlightColor. self class menuBarIsActive ifTrue: [ self class closeAllMenus. MenuBarIsActive _ true. target perform: selector with: self ] "invoke my menu" ! ! !ScratchMenuTitleMorph methodsFor: 'event handling' stamp: 'jm 10/28/2008 12:25'! mouseLeave: evt self color: self normalColor. ! ! !ScratchMenuTitleMorph class methodsFor: 'as yet unclassified' stamp: 'jens 3/11/2009 21:25'! closeAllMenus self menuBarIsActive ifFalse: [^self ]. World submorphs do: [:m | (m isKindOf: MenuMorph) ifTrue: [m deleteIfPopUp]]. MenuBarIsActive _ false. ! ! !ScratchMenuTitleMorph class methodsFor: 'as yet unclassified' stamp: 'jens 3/18/2009 17:20'! deactivateMenuBar MenuBarIsActive _ false. ! ! !ScratchMenuTitleMorph class methodsFor: 'as yet unclassified' stamp: 'jens 3/7/2009 22:22'! menuBarIsActive ^ MenuBarIsActive ifNil: [MenuBarIsActive _ false]. ! ! I am a monophonic MIDI note player. I remember the midiPort, channel, volume, and currently playing note, if any. Playing a note will first turn off the note currently playing, if any. ! !ScratchNotePlayer methodsFor: 'initialization' stamp: 'jm 5/6/2007 12:49'! initialize midiPort _ nil. channel _ 1. volume _ 100. midiKey _ nil. ! ! !ScratchNotePlayer methodsFor: 'accessing' stamp: 'jm 8/7/2004 07:29'! channel ^ channel ! ! !ScratchNotePlayer methodsFor: 'accessing' stamp: 'jm 5/6/2007 13:16'! channel: aNumber "Change my MIDI channel. This first turns off any notes in progress on the current channel." channel _ aNumber rounded within: 1 and: 16. ! ! !ScratchNotePlayer methodsFor: 'accessing' stamp: 'jm 6/2/2009 18:21'! instrument: aNumber "Set the MIDI instrument number between 1 and 128. Instrument number 1 is piano. The instrument numbers are defined by the General MIDI specification; you can find the list of instruments on the web." | instr | instr _ aNumber rounded within: 1 and: 128. midiPort ifNotNil: [ midiPort midiCmd: 16rC0 channel: (channel - 1) byte: (instr - 1)]. ! ! !ScratchNotePlayer methodsFor: 'accessing' stamp: 'jm 8/9/2004 09:03'! midiPort: aMIDIPort midiPort _ aMIDIPort. ! ! !ScratchNotePlayer methodsFor: 'accessing' stamp: 'jm 8/7/2004 07:29'! volume ^ volume ! ! !ScratchNotePlayer methodsFor: 'accessing' stamp: 'jm 12/14/2004 19:23'! volume: aNumber "Set my volume in the range 0-100." volume _ aNumber. ! ! !ScratchNotePlayer methodsFor: 'playing' stamp: 'jm 6/2/2009 18:37'! drumOff "Turn of the currently sounding drum, if any." midiKey ifNotNil: [ midiPort ifNotNil: [midiPort midiCmd: 16r90 channel: 9 byte: midiKey byte: 0]. midiKey _ nil]. snd ifNotNil: [ snd stopGracefully. snd _ nil]. ! ! !ScratchNotePlayer methodsFor: 'playing' stamp: 'jm 6/3/2009 16:04'! drumOn: aNumber "Play the given drum number at the current volume level. The drum number is defined by the General MIDI spec, which you can find on the web. The absolute value of the rounded drum number is used." | vol | midiKey _ aNumber rounded abs within: 0 and: 127. midiPort ifNil: [ snd _ (SampledSound samples: SampledSound coffeeCupClink samplingRate: 11025) copy. snd setPitch: (AbstractSound pitchForMIDIKey: midiKey) dur: 1000 loudness: ((volume / 500.0) within: 0.0 and: 1.0). snd play] ifNotNil: [ vol _ (1.27 * volume) rounded within: 0 and: 127. midiPort midiCmd: 16r90 channel: 9 byte: midiKey byte: vol]. ! ! !ScratchNotePlayer methodsFor: 'playing' stamp: 'jm 6/2/2009 18:38'! noteOff "Turn of the currently sounding note, if any." midiKey ifNotNil: [ midiPort ifNotNil: [midiPort midiCmd: 16r90 channel: (channel - 1) byte: midiKey byte: 0]. midiKey _ nil]. snd ifNotNil: [ snd stopGracefully. snd _ nil]. ! ! !ScratchNotePlayer methodsFor: 'playing' stamp: 'jm 6/3/2009 15:54'! noteOn: aNumber "Play the given note at the current volume level. The note number gives the piano key number where 60 is middle-C and there are 12 keys per octave. The absolute value of the note number is used." | vol | midiKey _ aNumber rounded abs within: 0 and: 127. midiPort ifNil: [ snd _ FMSound organ1. snd setPitch: (AbstractSound pitchForMIDIKey: midiKey) dur: 1000 loudness: ((volume / 100.0) within: 0.0 and: 1.0). snd play] ifNotNil: [ vol _ (1.27 * volume) rounded within: 0 and: 127. midiPort midiCmd: 16r90 channel: (channel - 1) byte: midiKey byte: vol]. ! ! !ScratchNoteSelector methodsFor: 'initialization' stamp: 'jm 10/26/2007 08:39'! buildKeyboard: nOctaves baseOctave: baseOctave keyWidth: whiteW "Build keyboard for the given number of octaves starting with the given octave." | whiteH blackW blackH octaveStart octavePt nWhite nBlack noteR key xOffset | self removeAllMorphs. self extent: 5@5. "adjusted later" whiteH _ (3.3 * whiteW) rounded. blackW _ (0.57 * whiteW) rounded. blackW even ifTrue: [blackW _ blackW - 1]. blackH _ (0.6 * whiteH) rounded. 0 to: nOctaves do: [:i | octaveStart _ 12 * (baseOctave + i). octavePt _ self innerBounds topLeft + (((7 * whiteW * i) - 1) @ -1). i < nOctaves ifTrue: [nWhite _ 7. nBlack _ 5] ifFalse: [nWhite _ 1. nBlack _ 0]. "only one 'C' key in top octave" 1 to: nWhite do: [:j | noteR _ (octavePt + (((j - 1) * whiteW) @ 0)) extent: (whiteW + 1) @ whiteH. key _ ScratchNoteSelectorKeyMorph newBounds: noteR. key midiKey: octaveStart + (#(0 2 4 5 7 9 11) at: j). self addMorph: key]. 1 to: nBlack do: [:j | xOffset _ (whiteW * (#(1 2 4 5 6) at: j)) - (blackW // 2). noteR _ (octavePt + (xOffset @ 0)) extent: blackW @ blackH. key _ ScratchNoteSelectorKeyMorph newBounds: noteR. key midiKey: octaveStart + (#(1 3 6 8 10) at: j). self addMorph: key]]. self extent: (self fullBounds extent + borderWidth - 1). ! ! !ScratchNoteSelector methodsFor: 'initialization' stamp: 'jm 10/22/2007 23:55'! initialize super initialize. selectedVal _ nil. "if nothing is ever selected, then don't return anything" notePlayer _ nil. self buildKeyboard: 2 baseOctave: 4 keyWidth: 13. self setUpNoteDisplay. ! ! !ScratchNoteSelector methodsFor: 'initialization' stamp: 'jm 10/22/2007 23:17'! setNotePlayer: thePlayer notePlayer _ thePlayer. ! ! !ScratchNoteSelector methodsFor: 'initialization' stamp: 'jm 10/23/2007 00:12'! setUpNoteDisplay | height rectMorph | height _ 20. "set up the rect morph which holds the note text" rectMorph _ BorderedMorph new color: Color white; position: self bottomLeft - (0@2); extent: (self width @ height). self extent: self fullBounds extent. self addMorph: rectMorph. labelMorph _ StringMorph new. labelMorph position: rectMorph position + (0@4). rectMorph addMorph: labelMorph. self extent: self fullBounds extent. self updateNoteDisplay: 60. ! ! !ScratchNoteSelector methodsFor: 'interaction' stamp: 'jm 10/23/2007 00:19'! startUp "Waits for the user to click a value or to click outside, then returns the selected note or nil." | w result | self openInWorld. w _ self world. Sensor waitNoButton. "start with mouse up" w doOneCycle. w activeHand newMouseFocus: self. selectedVal _ nil. done _ false. [done] whileFalse: [ (w activeHand hasMouseFocus: self) ifFalse: [done _ true]. w doOneCycle]. result _ selectedVal. self shutDown. ^ result ! ! !ScratchNoteSelector methodsFor: 'event handling' stamp: 'jm 10/22/2007 23:31'! handlesMouseDown: evt ^ true ! ! !ScratchNoteSelector methodsFor: 'event handling' stamp: 'jm 10/26/2007 08:40'! mouseMove: evt | keyMorph k | evt anyButtonPressed ifFalse: [^ self]. keyMorph _ submorphs detect: [:m | (m containsPoint: evt cursorPoint) and: [m isKindOf: ScratchNoteSelectorKeyMorph]] ifNone: [nil]. keyMorph ifNil: [self turnOffNote] ifNotNil: [ k _ keyMorph midiKey. k = selectedVal ifTrue: [^ self]. self turnOffNote. keyMorph highlight: true. self turnOnNote: k. selectedVal _ k]. ! ! !ScratchNoteSelector methodsFor: 'stepping' stamp: 'jm 10/26/2007 08:47'! step | keyMorph | Sensor anyButtonPressed ifTrue: [^ self]. keyMorph _ submorphs detect: [:m | (m containsPoint: Sensor cursorPoint) and: [m isKindOf: ScratchNoteSelectorKeyMorph]] ifNone: [^ self]. self updateNoteDisplay: keyMorph midiKey. ! ! !ScratchNoteSelector methodsFor: 'stepping' stamp: 'jm 10/26/2007 08:49'! stepTime ^ 100 ! ! !ScratchNoteSelector methodsFor: 'private' stamp: 'NB 10/22/2007 00:55'! shutDown "closes everything up, and destroys the morph" self turnOffNote. self delete.! ! !ScratchNoteSelector methodsFor: 'private' stamp: 'jm 10/26/2007 08:39'! turnOffNote submorphs do: [:m | (m isKindOf: ScratchNoteSelectorKeyMorph) ifTrue: [m highlight: false]]. notePlayer ifNotNil: [notePlayer noteOff]. selectedVal _ nil. ! ! !ScratchNoteSelector methodsFor: 'private' stamp: 'jm 10/22/2007 23:32'! turnOnNote: midiKey selectedVal _ midiKey. self updateNoteDisplay: midiKey. notePlayer ifNotNil: [notePlayer noteOn: midiKey]. ! ! !ScratchNoteSelector methodsFor: 'private' stamp: 'jm 10/23/2007 00:17'! updateNoteDisplay: noteNum | s | s _ #('C' 'C#' 'D' 'Eb' 'E' 'F' 'F#' 'G' 'G#' 'A' 'Bb' 'B') at: ((noteNum rem: 12) + 1). labelMorph contents: s, ' (' , noteNum asString, ')'; centerInOwner. ! ! !ScratchNoteSelectorKeyMorph methodsFor: 'initialization' stamp: 'jm 10/22/2007 22:14'! initialize super initialize. borderWidth _ 1. midiKey _ 60. self highlight: false. ! ! !ScratchNoteSelectorKeyMorph methodsFor: 'accessing' stamp: 'jm 10/22/2007 22:16'! highlight: aBoolean "If the argumet is true, highlight to show that I'm pressed. Otherwise, set my color based on whether I am a black or white key." | isBlackKey | aBoolean ifTrue: [self color: Color yellow] ifFalse: [ isBlackKey _ #(1 3 6 8 10) includes: (midiKey \\ 12). isBlackKey ifTrue: [self color: Color black] ifFalse: [self color: (Color gray: 0.95)]]. ! ! !ScratchNoteSelectorKeyMorph methodsFor: 'accessing' stamp: 'jm 10/22/2007 17:38'! midiKey ^ midiKey ! ! !ScratchNoteSelectorKeyMorph methodsFor: 'accessing' stamp: 'jm 10/22/2007 22:14'! midiKey: anInteger "Set my midiKey, which determines the pitch of this note. Middle-C is 60." midiKey _ anInteger. self highlight: false. ! ! !ScratchNotesDialog methodsFor: 'initialization' stamp: 'ee 4/30/2009 11:54'! createNotesDialogFor: aScratchFrameMorph "Create a Scratch project notes dialog box." | commentFont | commentFont _ (ScratchFrameMorph getFont: #ProjectNotes). commentMorph _ ScrollingStringMorph new borderWidth: 0; contents: ''; font: commentFont; backForm: (ScratchFrameMorph skinAt: #stringFieldFrame); width: 300. self title: 'Project Notes'. mainColumn addMorphBack: commentMorph. mainColumn addMorphBack: buttonRow. scratchFrame _ aScratchFrameMorph. commentMorph contents: scratchFrame projectCommentOrTemplate. bottomSpacer delete. bottomSpacer _ nil. tabFields add: commentMorph. ! ! !ScratchNotesDialog methodsFor: 'initialization' stamp: 'jm 8/2/2007 13:23'! initialize super initialize. self withButtonsForYes: false no: false okay: true cancel: true. ! ! !ScratchNotesDialog methodsFor: 'other' stamp: 'jm 10/11/2006 14:55'! comment ^ commentMorph contents ! ! !ScratchNotesDialog methodsFor: 'other' stamp: 'jm 2/27/2009 17:42'! getUserResponse "Wait for the user to respond, then answer #ok if the okay button was pressed or #cancelled if the user cancels the operation." "Details: This is invoked synchronously from the caller. In order to keep processing inputs and updating the screen while waiting for the user to respond, this method has its own version of the World's event loop." | w | self openInWorld. self centerOnScreen. w _ self world. w activeHand newKeyboardFocus: commentMorph. done _ false. [done] whileFalse: [w doOneCycle]. response = #cancelled ifTrue: [^ #cancelled]. "save notes in project" scratchFrame projectComment: commentMorph contents. ^ #ok ! ! !ScratchNotesDialog class methodsFor: 'instance creation' stamp: 'jm 2/28/2009 14:31'! editNotesFor: aScratchFrameMorph "Choose a name for sharing the current Scratch project file. Display the project thumbnail and info string and allow the info string to be edited. Answer the new project name." ^ self new createNotesDialogFor: aScratchFrameMorph; extent: 350@450. ! ! This plugin combines a number of primitives needed by Scratch including: a. primitives that manipulate 24-bit color images (i.e. 32-bit deep Forms but alpha is ignored) b. primitive to open browsers, find the user's documents folder, set the window title and other host OS functions This plugin includes new serial port primitives, including support for named serial ports. The underlying plugin code can support up to 32 simultaenously open ports. Port options for Set/GetOption primitives: 1. baud rate 2. data bits 3. stop bits 4. parity type 5. input flow control type 6. output flow control type Handshake lines (options 20-25 for Set/GetOption primitives): 20. DTR (output line) 21. RTS (output line) 22. CTS (input line) 23. DSR (input line) 24. CD (input line) 25. RI (input line) ! !ScratchPlugin methodsFor: 'hsv filters' stamp: 'jm 11/8/2006 05:30'! primitiveBrightnessShift | inOop outOop shift in sz out pix r g b max min hue saturation brightness | self export: true. self var: 'in' declareC: 'unsigned int *in'. self var: 'out' declareC: 'unsigned int *out'. inOop _ interpreterProxy stackValue: 2. outOop _ interpreterProxy stackValue: 1. shift _ interpreterProxy stackIntegerValue: 0. in _ self checkedUnsignedIntPtrOf: inOop. sz _ interpreterProxy stSizeOf: inOop. out _ self checkedUnsignedIntPtrOf: outOop. interpreterProxy success: ((interpreterProxy stSizeOf: outOop) = sz). interpreterProxy failed ifTrue: [^ nil]. 0 to: sz - 1 do: [:i | pix _ (in at: i) bitAnd: 16rFFFFFF. pix = 0 ifFalse: [ "skip pixel values of 0 (transparent)" r _ (pix bitShift: -16) bitAnd: 16rFF. g _ (pix bitShift: -8) bitAnd: 16rFF. b _ pix bitAnd: 16rFF. "find min and max color components" max _ min _ r. g > max ifTrue: [max _ g]. b > max ifTrue: [max _ b]. g < min ifTrue: [min _ g]. b < min ifTrue: [min _ b]. "find current hue with range 0 to 360" hue _ self hueFromR: r G: g B: b min: min max: max. "find current saturation and brightness with range 0 to 1000" max = 0 ifTrue: [saturation _ 0] ifFalse: [saturation _ ((max - min) * 1000) // max]. brightness _ (max * 1000) // 255. "compute new brigthness" brightness _ brightness + (shift * 10). brightness > 1000 ifTrue: [brightness _ 1000]. brightness < 0 ifTrue: [brightness _ 0]. self bitmap: out at: i putH: hue s: saturation v: brightness]]. interpreterProxy pop: 3. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'hsv filters' stamp: 'jm 11/8/2006 05:30'! primitiveHueShift | inOop outOop shift in sz out pix r g b max min brightness saturation hue | self export: true. self var: 'in' declareC: 'unsigned int *in'. self var: 'out' declareC: 'unsigned int *out'. inOop _ interpreterProxy stackValue: 2. outOop _ interpreterProxy stackValue: 1. shift _ interpreterProxy stackIntegerValue: 0. in _ self checkedUnsignedIntPtrOf: inOop. sz _ interpreterProxy stSizeOf: inOop. out _ self checkedUnsignedIntPtrOf: outOop. interpreterProxy success: ((interpreterProxy stSizeOf: outOop) = sz). interpreterProxy failed ifTrue: [^ nil]. 0 to: sz - 1 do: [:i | pix _ (in at: i) bitAnd: 16rFFFFFF. pix = 0 ifFalse: [ "skip pixel values of 0 (transparent)" r _ (pix bitShift: -16) bitAnd: 16rFF. g _ (pix bitShift: -8) bitAnd: 16rFF. b _ pix bitAnd: 16rFF. "find min and max color components" max _ min _ r. g > max ifTrue: [max _ g]. b > max ifTrue: [max _ b]. g < min ifTrue: [min _ g]. b < min ifTrue: [min _ b]. "find current brightness (v) and saturation with range 0 to 1000" brightness _ (max * 1000) // 255. max = 0 ifTrue: [saturation _ 0] ifFalse: [saturation _ ((max - min) * 1000) // max]. brightness < 110 ifTrue: [ "force black to a very dark, saturated gray" brightness _ 110. saturation _ 1000]. saturation < 90 ifTrue: [saturation _ 90]. "force a small color change on grays" ((brightness = 110) | (saturation = 90)) "tint all blacks and grays the same" ifTrue: [hue _ 0] ifFalse: [hue _ self hueFromR: r G: g B: b min: min max: max]. hue _ (hue + shift + 360000000) \\ 360. "compute new hue" self bitmap: out at: i putH: hue s: saturation v: brightness]]. interpreterProxy pop: 3. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'hsv filters' stamp: 'jm 11/8/2006 05:30'! primitiveSaturationShift | inOop outOop shift in sz out pix r g b max min brightness saturation hue | self export: true. self var: 'in' declareC: 'unsigned int *in'. self var: 'out' declareC: 'unsigned int *out'. inOop _ interpreterProxy stackValue: 2. outOop _ interpreterProxy stackValue: 1. shift _ interpreterProxy stackIntegerValue: 0. in _ self checkedUnsignedIntPtrOf: inOop. sz _ interpreterProxy stSizeOf: inOop. out _ self checkedUnsignedIntPtrOf: outOop. interpreterProxy success: ((interpreterProxy stSizeOf: outOop) = sz). interpreterProxy failed ifTrue: [^ nil]. 0 to: sz - 1 do: [:i | pix _ (in at: i) bitAnd: 16rFFFFFF. pix < 2 ifFalse: [ "skip pixel values of 0 (transparent) and 1 (black)" r _ (pix bitShift: -16) bitAnd: 16rFF. g _ (pix bitShift: -8) bitAnd: 16rFF. b _ pix bitAnd: 16rFF. "find min and max color components" max _ min _ r. g > max ifTrue: [max _ g]. b > max ifTrue: [max _ b]. g < min ifTrue: [min _ g]. b < min ifTrue: [min _ b]. "find current brightness (v) and saturation with range 0 to 1000" brightness _ (max * 1000) // 255. max = 0 ifTrue: [saturation _ 0] ifFalse: [saturation _ ((max - min) * 1000) // max]. saturation > 0 ifTrue: [ "do nothing if pixel is unsaturated (gray)" hue _ self hueFromR: r G: g B: b min: min max: max. "compute new saturation" saturation _ saturation + (shift * 10). saturation > 1000 ifTrue: [saturation _ 1000]. saturation < 0 ifTrue: [saturation _ 0]. self bitmap: out at: i putH: hue s: saturation v: brightness]]]. interpreterProxy pop: 3. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'other filters' stamp: 'jm 11/8/2006 05:30'! primitiveBlur | inOop outOop width in out sz height n rTotal gTotal bTotal pix outPix | self export: true. self var: 'in' declareC: 'unsigned int *in'. self var: 'out' declareC: 'unsigned int *out'. inOop _ interpreterProxy stackValue: 2. outOop _ interpreterProxy stackValue: 1. width _ interpreterProxy stackIntegerValue: 0. in _ self checkedUnsignedIntPtrOf: inOop. out _ self checkedUnsignedIntPtrOf: outOop. sz _ interpreterProxy stSizeOf: inOop. interpreterProxy success: ((interpreterProxy stSizeOf: outOop) = sz). interpreterProxy failed ifTrue: [^ nil]. height _ sz // width. 1 to: height - 2 do: [:y | 1 to: width - 2 do: [:x | n _ rTotal _ gTotal _ bTotal _ 0. -1 to: 1 do: [:dY | -1 to: 1 do: [:dX | pix _ (in at: ((y + dY) * width) + (x + dX) "add 1 when testing in Squeak") bitAnd: 16rFFFFFF. pix = 0 ifFalse: [ "skip transparent pixels" rTotal _ rTotal + ((pix bitShift: -16) bitAnd: 16rFF). gTotal _ gTotal + ((pix bitShift: -8) bitAnd: 16rFF). bTotal _ bTotal + (pix bitAnd: 16rFF). n _ n + 1]]]. n = 0 ifTrue: [outPix _ 0] ifFalse: [outPix _ ((rTotal // n) bitShift: 16) + ((gTotal // n) bitShift: 8) + (bTotal // n)]. out at: ((y * width) + x "add 1 when testing in Squeak") put: outPix]]. interpreterProxy pop: 3. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'other filters' stamp: 'jm 11/8/2006 05:30'! primitiveFisheye | inOop outOop width in out sz height centerX centerY dx dy ang pix power r srcX srcY scaledPower | self export: true. self var: 'in' declareC: 'unsigned int *in'. self var: 'out' declareC: 'unsigned int *out'. self var: 'scaleX' declareC: 'double scaleX'. self var: 'scaleY' declareC: 'double scaleY'. self var: 'whirlRadians' declareC: 'double whirlRadians'. self var: 'radiusSquared' declareC: 'double radiusSquared'. self var: 'dx' declareC: 'double dx'. self var: 'dy' declareC: 'double dy'. self var: 'd' declareC: 'double d'. self var: 'factor' declareC: 'double factor'. self var: 'ang' declareC: 'double ang'. self var: 'sina' declareC: 'double sina'. self var: 'cosa' declareC: 'double cosa'. self var: 'r' declareC: 'double r'. self var: 'scaledPower' declareC: 'double scaledPower'. inOop _ interpreterProxy stackValue: 3. outOop _ interpreterProxy stackValue: 2. width _ interpreterProxy stackIntegerValue: 1. power _ interpreterProxy stackIntegerValue: 0. in _ self checkedUnsignedIntPtrOf: inOop. out _ self checkedUnsignedIntPtrOf: outOop. sz _ interpreterProxy stSizeOf: inOop. interpreterProxy success: ((interpreterProxy stSizeOf: outOop) = sz). interpreterProxy failed ifTrue: [^ nil]. "calculate height, center, scales, radius, whirlRadians, and radiusSquared" height _ sz // width. centerX _ width // 2. centerY _ height // 2. height _ sz // width. centerX _ width // 2. centerY _ height // 2. scaledPower _ power / 100.0. 0 to: width - 1 do: [:x | 0 to: height - 1 do: [:y | dx _ (x - centerX) / centerX asFloat. dy _ (y - centerY) / centerY asFloat. r _ ((dx * dx) + (dy * dy)) sqrt raisedTo: scaledPower. r <= 1.0 ifTrue: [ ang _ self cCode: 'atan2(dy,dx)'. srcX _ (1024 * (centerX + ((r * ang cos) * centerX))) asInteger. srcY _ (1024 * (centerY + ((r * ang sin) * centerY))) asInteger] ifFalse: [ srcX _ 1024 * x. srcY _ 1024 * y]. pix _ self interpolatedFrom: in x: srcX y: srcY width: width height: height. out at: ((y * width) + x "+ 1 for Squeak") put: pix]]. interpreterProxy pop: 4. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'other filters' stamp: 'jm 11/8/2006 05:30'! primitiveWaterRipples1 | in out aArray bArray ripply temp pix dx dy dist inOop outOop width allPix aArOop bArOop height t1 blops x y power val val2 dx2 dy2 newLoc | self export: true. self var: 'in' declareC: 'unsigned int *in'. self var: 'out' declareC: 'unsigned int *out'. self var: 'aArray' declareC: 'double *aArray'. self var: 'bArray' declareC: 'double *bArray'. self var: 'ripply' declareC: 'int ripply'. self var: 'temp' declareC: 'double temp'. self var: 'pix' declareC: 'unsigned int pix'. self var: 'dist' declareC: 'double dist'. self var: 'dx2' declareC: 'double dx2'. self var: 'dy2' declareC: 'double dy2'. inOop _ interpreterProxy stackValue: 5. outOop _ interpreterProxy stackValue: 4. width _ interpreterProxy stackIntegerValue: 3. in _ self checkedUnsignedIntPtrOf: inOop. out _ self checkedUnsignedIntPtrOf: outOop. allPix _ interpreterProxy stSizeOf: inOop. ripply _ interpreterProxy stackIntegerValue: 2. aArOop _ interpreterProxy stackValue: 1. bArOop _ interpreterProxy stackValue: 0. aArray _ self checkedFloatPtrOf: aArOop. bArray _ self checkedFloatPtrOf: bArOop. interpreterProxy success: ((interpreterProxy stSizeOf: outOop) = allPix). interpreterProxy failed ifTrue: [^ nil]. height _ allPix // width. t1 _ self cCode: 'rand()'. blops _ t1 \\ ripply -1. 0 to: blops /2-1 do: [:t | t1 _ self cCode: 'rand()'. x _ t1 \\ width. t1 _ self cCode: 'rand()'. y _ t1 \\ height. t1 _ self cCode: 'rand()'. power _ t1 \\ 8. -4 to: 4 do: [:g | -4 to: 4 do: [:h | dist _ ((g*g) + (h*h)) asFloat. ((dist < 25) and: [dist > 0]) ifTrue: [ dx _ (x + g) asInteger. dy _ (y + h) asInteger. ((dx >0) and: [(dy>0) and: [(dy < height) and: [dx < width]]]) ifTrue: [ aArray at: ((dy)*width + dx) put: (power *(1.0 asFloat -(dist/(25.0 asFloat))) asFloat). ]. ]. ]. ]. ]. 1 to: width -2 do: [:f | 1 to: height -2 do: [:d | val _ (d)*width + f. aArray at: val put: ((( (bArray at: (val+1)) + (bArray at: (val-1)) + (bArray at: (val + width)) + (bArray at: (val - width)) + ((bArray at: (val -1 -width))/2) + ((bArray at: (val-1+width))/2) + ((bArray at: (val+1-width))/2) + ((bArray at: (val+1+width))/2)) /4) - (aArray at: (val))). aArray at: (val) put: ((aArray at: (val))*(0.9 asFloat)). ]. ]. "temp _ bArray. bArray _ aArray. aArray _ temp." 0 to: width*height do: [:q | temp _ bArray at: q. bArray at: q put: (aArray at: q). aArray at: q put: temp. ]. 0 to: height-1 do: [:j | 0 to: width-1 do: [:i | ((i > 1) and: [(i<(width-1)) and: [(j>1) and: [(j<(height-1))]]]) ifTrue: [ val2 _ (j)*width + i. dx2 _ ((((aArray at: (val2)) - (aArray at: (val2-1))) + ((aArray at: (val2+1)) - (aArray at: (val2)))) *64) asFloat. dy2 _ ((((aArray at: (val2)) - (aArray at: (val2-width))) + ((aArray at: (val2+width)) - (aArray at: (val2)))) /64) asFloat. (dx2<-2) ifTrue: [dx2 _ -2]. (dx2>2) ifTrue: [dx2 _ 2]. (dy2<-2) ifTrue: [dy2 _ -2]. (dy2>2) ifTrue: [dy2 _ 2]. newLoc _ ((j+dy2)*width + (i+dx2)) asInteger. ((newLoc < (width*height)) and: [newLoc >=0]) ifTrue: [ pix _ in at: newLoc] ifFalse: [ pix _ in at: (i +(j*width)) ]. ] ifFalse: [ pix _ in at: (i +(j*width)) ]. out at: (i + (j*width)) put: pix. ]]. interpreterProxy pop: 6. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'other filters' stamp: 'jm 11/8/2006 05:30'! primitiveWhirl | inOop outOop width degrees in out sz height centerX centerY radius scaleX scaleY whirlRadians radiusSquared dx dy d factor ang sina cosa pix | self export: true. self var: 'in' declareC: 'unsigned int *in'. self var: 'out' declareC: 'unsigned int *out'. self var: 'scaleX' declareC: 'double scaleX'. self var: 'scaleY' declareC: 'double scaleY'. self var: 'whirlRadians' declareC: 'double whirlRadians'. self var: 'radiusSquared' declareC: 'double radiusSquared'. self var: 'dx' declareC: 'double dx'. self var: 'dy' declareC: 'double dy'. self var: 'd' declareC: 'double d'. self var: 'factor' declareC: 'double factor'. self var: 'ang' declareC: 'double ang'. self var: 'sina' declareC: 'double sina'. self var: 'cosa' declareC: 'double cosa'. inOop _ interpreterProxy stackValue: 3. outOop _ interpreterProxy stackValue: 2. width _ interpreterProxy stackIntegerValue: 1. degrees _ interpreterProxy stackIntegerValue: 0. in _ self checkedUnsignedIntPtrOf: inOop. out _ self checkedUnsignedIntPtrOf: outOop. sz _ interpreterProxy stSizeOf: inOop. interpreterProxy success: ((interpreterProxy stSizeOf: outOop) = sz). interpreterProxy failed ifTrue: [^ nil]. "calculate height, center, scales, radius, whirlRadians, and radiusSquared" height _ sz // width. centerX _ width // 2. centerY _ height // 2. centerX < centerY ifTrue: [ radius _ centerX. scaleX _ centerY asFloat / centerX. scaleY _ 1.0] ifFalse: [ radius _ centerY. scaleX _ 1.0. centerY < centerX ifTrue: [scaleY _ centerX asFloat / centerY] ifFalse: [scaleY _ 1.0]]. whirlRadians _ (-3.141592653589793 * degrees) / 180.0. radiusSquared _ (radius * radius) asFloat. 0 to: width - 1 do: [:x | 0 to: height - 1 do: [:y | dx _ scaleX * (x - centerX) asFloat. dy _ scaleY * (y - centerY) asFloat. d _ (dx * dx) + (dy * dy). d < radiusSquared ifTrue: [ "inside the whirl circle" factor _ 1.0 - (d sqrt / radius). ang _ whirlRadians * (factor * factor). sina _ ang sin. cosa _ ang cos. pix _ self interpolatedFrom: in x: (1024.0 * ((((cosa * dx) - (sina * dy)) / scaleX) + centerX)) asInteger y: (1024.0 * ((((sina * dx) + (cosa * dy)) / scaleY) + centerY)) asInteger width: width height: height. out at: ((width * y) + x "for Squeak: + 1") put: pix]]]. interpreterProxy pop: 4. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'scaling' stamp: 'jm 11/8/2006 05:30'! primitiveDoubleSize | in out inOop outOop inW inH outW outH dstX dstY baseIndex pix i | self export: true. self var: 'in' declareC: 'int *in'. self var: 'out' declareC: 'int *out'. inOop _ interpreterProxy stackValue: 7. inW _ interpreterProxy stackIntegerValue: 6. inH _ interpreterProxy stackIntegerValue: 5. outOop _ interpreterProxy stackValue: 4. outW _ interpreterProxy stackIntegerValue: 3. outH _ interpreterProxy stackIntegerValue: 2. dstX _ interpreterProxy stackIntegerValue: 1. dstY _ interpreterProxy stackIntegerValue: 0. in _ self checkedUnsignedIntPtrOf: inOop. out _ self checkedUnsignedIntPtrOf: outOop. interpreterProxy success: (dstX + (2 * inW)) < outW. interpreterProxy success: (dstY + (2 * inH)) < outH. interpreterProxy failed ifTrue: [^ nil]. 0 to: inH - 1 do: [:y | baseIndex _ ((dstY + (2 * y)) * outW) + dstX. 0 to: inW - 1 do: [:x | pix _ in at: x + (y * inW). i _ baseIndex + (2 * x). out at: i put: pix. out at: i + 1 put: pix. out at: i + outW put: pix. out at: i + outW + 1 put: pix]]. interpreterProxy pop: 8. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'scaling' stamp: 'jm 1/1/2009 17:08'! primitiveHalfSizeAverage | in inW inH out outW outH srcX srcY dstX dstY dstW dstH srcIndex dstIndex pixel r g b | self export: true. self var: 'in' declareC: 'int *in'. self var: 'out' declareC: 'int *out'. in _ self checkedUnsignedIntPtrOf: (interpreterProxy stackValue: 11). inW _ interpreterProxy stackIntegerValue: 10. inH _ interpreterProxy stackIntegerValue: 9. out _ self checkedUnsignedIntPtrOf: (interpreterProxy stackValue: 8). outW _ interpreterProxy stackIntegerValue: 7. outH _ interpreterProxy stackIntegerValue: 6. srcX _ interpreterProxy stackIntegerValue: 5. srcY _ interpreterProxy stackIntegerValue: 4. dstX _ interpreterProxy stackIntegerValue: 3. dstY _ interpreterProxy stackIntegerValue: 2. dstW _ interpreterProxy stackIntegerValue: 1. dstH _ interpreterProxy stackIntegerValue: 0. interpreterProxy success: (srcX >= 0) & (srcY >= 0). interpreterProxy success: (srcX + (2 * dstW)) <= inW. interpreterProxy success: (srcY + (2 * dstH)) <= inH. interpreterProxy success: (dstX >= 0) & (dstY >= 0). interpreterProxy success: (dstX + dstW) <= outW. interpreterProxy success: (dstY + dstH) <= outH. interpreterProxy failed ifTrue: [^ nil]. 0 to: dstH - 1 do: [:y | srcIndex _ (inW * (srcY + (2 * y))) + srcX. dstIndex _ (outW * (dstY + y)) + dstX. 0 to: dstW - 1 do: [:x | pixel _ in at: srcIndex. r _ pixel bitAnd: 16rFF0000. g _ pixel bitAnd: 16rFF00. b _ pixel bitAnd: 16rFF. pixel _ in at: srcIndex + 1. r _ r + (pixel bitAnd: 16rFF0000). g _ g + (pixel bitAnd: 16rFF00). b _ b + (pixel bitAnd: 16rFF). pixel _ in at: srcIndex + inW. r _ r + (pixel bitAnd: 16rFF0000). g _ g + (pixel bitAnd: 16rFF00). b _ b + (pixel bitAnd: 16rFF). pixel _ in at: srcIndex + inW + 1. r _ r + (pixel bitAnd: 16rFF0000). g _ g + (pixel bitAnd: 16rFF00). b _ b + (pixel bitAnd: 16rFF). "store combined RGB into target bitmap" out at: dstIndex put: (((r bitShift: -2) bitAnd: 16rFF0000) bitOr: (((g bitShift: -2) bitAnd: 16rFF00) bitOr: (b bitShift: -2))). srcIndex _ srcIndex + 2. dstIndex _ dstIndex + 1]]. interpreterProxy pop: 12. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'scaling' stamp: 'jm 1/1/2009 17:08'! primitiveHalfSizeDiagonal | in inW inH out outW outH srcX srcY dstX dstY dstW dstH srcIndex dstIndex p1 p2 r g b | self export: true. self var: 'in' declareC: 'int *in'. self var: 'out' declareC: 'int *out'. in _ self checkedUnsignedIntPtrOf: (interpreterProxy stackValue: 11). inW _ interpreterProxy stackIntegerValue: 10. inH _ interpreterProxy stackIntegerValue: 9. out _ self checkedUnsignedIntPtrOf: (interpreterProxy stackValue: 8). outW _ interpreterProxy stackIntegerValue: 7. outH _ interpreterProxy stackIntegerValue: 6. srcX _ interpreterProxy stackIntegerValue: 5. srcY _ interpreterProxy stackIntegerValue: 4. dstX _ interpreterProxy stackIntegerValue: 3. dstY _ interpreterProxy stackIntegerValue: 2. dstW _ interpreterProxy stackIntegerValue: 1. dstH _ interpreterProxy stackIntegerValue: 0. interpreterProxy success: (srcX >= 0) & (srcY >= 0). interpreterProxy success: (srcX + (2 * dstW)) <= inW. interpreterProxy success: (srcY + (2 * dstH)) <= inH. interpreterProxy success: (dstX >= 0) & (dstY >= 0). interpreterProxy success: (dstX + dstW) <= outW. interpreterProxy success: (dstY + dstH) <= outH. interpreterProxy failed ifTrue: [^ nil]. 0 to: dstH - 1 do: [:y | srcIndex _ (inW * (srcY + (2 * y))) + srcX. dstIndex _ (outW * (dstY + y)) + dstX. 0 to: dstW - 1 do: [:x | p1 _ in at: srcIndex. p2 _ in at: srcIndex + inW + 1. r _ (((p1 bitAnd: 16rFF0000) + (p2 bitAnd: 16rFF0000)) bitShift: -1) bitAnd: 16rFF0000. g _ (((p1 bitAnd: 16rFF00) + (p2 bitAnd: 16rFF00)) bitShift: -1) bitAnd: 16rFF00. b _ ((p1 bitAnd: 16rFF) + (p2 bitAnd: 16rFF)) bitShift: -1. "store combined RGB into target bitmap" out at: dstIndex put: (r bitOr: (g bitOr: b)). srcIndex _ srcIndex + 2. dstIndex _ dstIndex + 1]]. interpreterProxy pop: 12. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'scaling' stamp: 'jm 11/8/2006 05:30'! primitiveScale "Scale using bilinear interpolation." | inOop inW inH outOop outW outH in out inX inY xIncr yIncr outPix w1 w2 w3 w4 t p1 p2 p3 p4 tWeight | self export: true. self var: 'in' declareC: 'int *in'. self var: 'out' declareC: 'int *out'. inOop _ interpreterProxy stackValue: 5. inW _ interpreterProxy stackIntegerValue: 4. inH _ interpreterProxy stackIntegerValue: 3. outOop _ interpreterProxy stackValue: 2. outW _ interpreterProxy stackIntegerValue: 1. outH _ interpreterProxy stackIntegerValue: 0. interpreterProxy success: (interpreterProxy stSizeOf: inOop) = (inW * inH). interpreterProxy success: (interpreterProxy stSizeOf: outOop) = (outW * outH). in _ self checkedUnsignedIntPtrOf: inOop. out _ self checkedUnsignedIntPtrOf: outOop. interpreterProxy failed ifTrue: [^ nil]. inX _ inY _ 0. "source x and y, scaled by 1024" xIncr _ (inW * 1024) // outW. "source x increment, scaled by 1024" yIncr _ (inH * 1024) // outH. "source y increment, scaled by 1024" 0 to: (outH - 1) do: [:outY | inX _ 0. 0 to: (outW - 1) do: [:outX | "compute weights, scaled by 2^20" w1 _ (1024 - (inX bitAnd: 1023)) * (1024 - (inY bitAnd: 1023)). w2 _ (inX bitAnd: 1023) * (1024 - (inY bitAnd: 1023)). w3 _ (1024 - (inX bitAnd: 1023)) * (inY bitAnd: 1023). w4 _ (inX bitAnd: 1023) * (inY bitAnd: 1023). "get source pixels" t _ ((inY >> 10) * inW) + (inX >> 10). p1 _ in at: t. ((inX >> 10) < (inW - 1)) ifTrue: [p2 _ in at: t + 1] ifFalse: [p2 _ p1]. (inY >> 10) < (inH - 1) ifTrue: [t _ t + inW]. "next row" p3 _ in at: t. ((inX >> 10) < (inW - 1)) ifTrue: [p4 _ in at: t + 1] ifFalse: [p4 _ p3]. "deal with transparent pixels" tWeight _ 0. p1 = 0 ifTrue: [p1 _ p2. tWeight _ tWeight + w1]. p2 = 0 ifTrue: [p2 _ p1. tWeight _ tWeight + w2]. p3 = 0 ifTrue: [p3 _ p4. tWeight _ tWeight + w3]. p4 = 0 ifTrue: [p4 _ p3. tWeight _ tWeight + w4]. p1 = 0 ifTrue: [p1 _ p3. p2 _ p4]. "both top pixels were transparent; use bottom row" p3 = 0 ifTrue: [p3 _ p1. p4 _ p2]. "both bottom pixels were transparent; use top row" outPix _ 0. tWeight < 500000 ifTrue: [ "compute an (opaque) output pixel if less than 50% transparent" t _ (w1 * ((p1 >> 16) bitAnd: 255)) + (w2 * ((p2 >> 16) bitAnd: 255)) + (w3 * ((p3 >> 16) bitAnd: 255)) + (w4 * ((p4 >> 16) bitAnd: 255)). outPix _ ((t >> 20) bitAnd: 255) << 16. t _ (w1 * ((p1 >> 8) bitAnd: 255)) + (w2 * ((p2 >> 8) bitAnd: 255)) + (w3 * ((p3 >> 8) bitAnd: 255)) + (w4 * ((p4 >> 8) bitAnd: 255)). outPix _ outPix bitOr: (((t >> 20) bitAnd: 255) << 8). t _ (w1 * (p1 bitAnd: 255)) + (w2 * (p2 bitAnd: 255)) + (w3 * (p3 bitAnd: 255)) + (w4 * (p4 bitAnd: 255)). outPix _ outPix bitOr: ((t >> 20) bitAnd: 255). outPix = 0 ifTrue: [outPix _ 1]]. out at: (outY * outW) + outX put: outPix. inX _ inX + xIncr]. inY _ inY + yIncr]. interpreterProxy pop: 6. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'bilinear interpolation' stamp: 'jm 11/8/2006 05:30'! primitiveInterpolate | inOop xFixed yFixed width in sz result | self export: true. self var: 'in' declareC: 'unsigned int *in'. inOop _ interpreterProxy stackValue: 3. width _ interpreterProxy stackIntegerValue: 2. xFixed _ interpreterProxy stackIntegerValue: 1. yFixed _ interpreterProxy stackIntegerValue: 0. in _ self checkedUnsignedIntPtrOf: inOop. sz _ interpreterProxy stSizeOf: inOop. interpreterProxy failed ifTrue: [^ nil]. result _ self interpolatedFrom: in x: xFixed y: yFixed width: width height: sz // width. interpreterProxy pop: 5. "pop args and rcvr" interpreterProxy pushInteger: result. ^ 0 ! ! !ScratchPlugin methodsFor: 'os functions' stamp: 'jm 11/8/2006 17:47'! primitiveGetFolderPath "Get the path for the special folder with given ID. Fail if the folder ID is out of range." | nameStr dst folderID count resultOop | self export: true. self var: 'nameStr' declareC: 'char nameStr[2000]'. self var: 'dst' declareC: 'char* dst'. folderID _ interpreterProxy stackIntegerValue: 0. interpreterProxy failed ifTrue: [^ 0]. self cCode: 'GetFolderPathForID(folderID, nameStr, 2000)'. count _ self cCode: 'strlen(nameStr)'. resultOop _ interpreterProxy instantiateClass: interpreterProxy classString indexableSize: count. dst _ self cCoerce: (interpreterProxy firstIndexableField: resultOop) to: 'char *'. 0 to: count - 1 do: [:i | dst at: i put: (nameStr at: i)]. interpreterProxy pop: 2 thenPush: resultOop. "pop arg and rcvr, push result" ^ 0 ! ! !ScratchPlugin methodsFor: 'os functions' stamp: 'jm 6/22/2007 13:47'! primitiveIsHidden "Answer true if the file or folder with the given path should be hidden from the user. On Windows, this is the value of the 'hidden' file property." | pathOop src count fullPath result | self export: true. self var: 'fullPath' declareC: 'char fullPath[1000]'. self var: 'src' declareC: 'char * src'. pathOop _ interpreterProxy stackValue: 0. ((interpreterProxy isIntegerObject: pathOop) or: [(interpreterProxy isBytes: pathOop) not]) ifTrue: [ interpreterProxy success: false]. interpreterProxy failed ifTrue: [^ 0]. src _ self cCoerce: (interpreterProxy firstIndexableField: pathOop) to: 'char *'. count _ interpreterProxy stSizeOf: pathOop. count >= 1000 ifTrue: [interpreterProxy success: false. ^ 0]. 0 to: count - 1 do: [:i | fullPath at: i put: (src at: i)]. fullPath at: count put: 0. result _ self cCode: 'IsFileOrFolderHidden(fullPath)'. interpreterProxy pop: 2. "pop arg and rcvr" interpreterProxy pushBool: result ~= 0. "push result" ^ 0 ! ! !ScratchPlugin methodsFor: 'os functions' stamp: 'jm 11/8/2006 11:07'! primitiveOpenURL "Open a web browser on the given URL." | urlStr src urlOop count | self export: true. self var: 'urlStr' declareC: 'char urlStr[2000]'. self var: 'src' declareC: 'char * src'. urlOop _ interpreterProxy stackValue: 0. ((interpreterProxy isIntegerObject: urlOop) or: [(interpreterProxy isBytes: urlOop) not]) ifTrue: [ interpreterProxy success: false]. interpreterProxy failed ifTrue: [^ 0]. src _ self cCoerce: (interpreterProxy firstIndexableField: urlOop) to: 'char *'. count _ interpreterProxy stSizeOf: urlOop. count >= 2000 ifTrue: [interpreterProxy success: false. ^ 0]. 0 to: count - 1 do: [:i | urlStr at: i put: (src at: i)]. urlStr at: count put: 0. self cCode: 'OpenURL(urlStr)'. interpreterProxy pop: 1. "pop arg, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'os functions' stamp: 'jm 10/4/2007 14:27'! primitiveSetUnicodePasteBuffer "Set the Mac OS X Unicode paste buffer." | utf16 strOop count | self export: true. self var: 'utf16' declareC: 'short int *utf16'. strOop _ interpreterProxy stackValue: 0. ((interpreterProxy isIntegerObject: strOop) or: [(interpreterProxy isBytes: strOop) not]) ifTrue: [ interpreterProxy success: false]. interpreterProxy failed ifTrue: [^ 0]. utf16 _ self cCoerce: (interpreterProxy firstIndexableField: strOop) to: 'short int *'. count _ interpreterProxy stSizeOf: strOop. self cCode: 'SetUnicodePasteBuffer(utf16, count)'. interpreterProxy pop: 1. "pop arg, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'os functions' stamp: 'jm 11/8/2006 11:11'! primitiveSetWindowTitle "Set the title of the Scratch window." | titleStr src titleOop count | self export: true. self var: 'titleStr' declareC: 'char titleStr[1000]'. self var: 'src' declareC: 'char * src'. titleOop _ interpreterProxy stackValue: 0. ((interpreterProxy isIntegerObject: titleOop) or: [(interpreterProxy isBytes: titleOop) not]) ifTrue: [ interpreterProxy success: false]. interpreterProxy failed ifTrue: [^ 0]. src _ self cCoerce: (interpreterProxy firstIndexableField: titleOop) to: 'char *'. count _ interpreterProxy stSizeOf: titleOop. count >= 1000 ifTrue: [interpreterProxy success: false. ^ 0]. 0 to: count - 1 do: [:i | titleStr at: i put: (src at: i)]. titleStr at: count put: 0. self cCode: 'SetScratchWindowTitle(titleStr)'. interpreterProxy pop: 1. "pop arg, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'os functions' stamp: 'jm 4/11/2007 15:45'! primitiveShortToLongPath "On Windows, convert a short file/path name into a long one. Fail on other platforms." | shortPath longPath ptr shortPathOop result count resultOop | self export: true. self var: 'shortPath' declareC: 'char shortPath[1000]'. self var: 'longPath' declareC: 'char longPath[1000]'. self var: 'ptr' declareC: 'char * ptr'. shortPathOop _ interpreterProxy stackValue: 0. ((interpreterProxy isIntegerObject: shortPathOop) or: [(interpreterProxy isBytes: shortPathOop) not]) ifTrue: [ interpreterProxy success: false. ^ 0]. ptr _ self cCoerce: (interpreterProxy firstIndexableField: shortPathOop) to: 'char *'. count _ interpreterProxy stSizeOf: shortPathOop. count >= 1000 ifTrue: [interpreterProxy success: false. ^ 0]. 0 to: count - 1 do: [:i | shortPath at: i put: (ptr at: i)]. shortPath at: count put: 0. result _ self cCode: 'WinShortToLongPath(shortPath, longPath, 1000)'. result = -1 ifTrue: [interpreterProxy success: false. ^ 0]. count _ self cCode: 'strlen(longPath)'. resultOop _ interpreterProxy instantiateClass: interpreterProxy classString indexableSize: count. ptr _ self cCoerce: (interpreterProxy firstIndexableField: resultOop) to: 'char *'. 0 to: count - 1 do: [:i | ptr at: i put: (longPath at: i)]. interpreterProxy pop: 2 thenPush: resultOop. "pop arg and rcvr, push result" ^ 0 ! ! !ScratchPlugin methodsFor: 'serial port' stamp: 'jm 8/28/2005 10:57'! primClose "Close the given serial port." | portNum | self export: true. portNum _ interpreterProxy stackIntegerValue: 0. interpreterProxy failed ifTrue: [^ nil]. self cCode: 'SerialPortClose(portNum)'. interpreterProxy pop: 1. "pop arg, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'serial port' stamp: 'jm 4/8/2007 14:50'! primGetOption "Return the given option value for the given serial port." | portNum attrNum result | self export: true. portNum _ interpreterProxy stackIntegerValue: 1. attrNum _ interpreterProxy stackIntegerValue: 0. interpreterProxy failed ifTrue: [^ nil]. result _ self cCode: 'SerialPortGetOption(portNum, attrNum)'. result = -1 ifTrue: [interpreterProxy success: false. ^ 0]. interpreterProxy pop: 3. "pop args and rcvr, push result" interpreterProxy pushInteger: result. ^ 0 ! ! !ScratchPlugin methodsFor: 'serial port' stamp: 'jm 8/28/2005 10:57'! primIsPortOpen "Answer the true if the given port is open." | portNum result | self export: true. portNum _ interpreterProxy stackIntegerValue: 0. interpreterProxy failed ifTrue: [^ 0]. result _ self cCode: 'SerialPortIsOpen(portNum)'. interpreterProxy pop: 2. "pop arg and rcvr" interpreterProxy pushBool: result ~= 0. "push result" ^ 0 ! ! !ScratchPlugin methodsFor: 'serial port' stamp: 'jm 4/8/2007 14:51'! primOpenPortNamed "Open the port with the given name and baud rate." | nameStr src nameOop baudRate count portNum | self export: true. self var: 'nameStr' declareC: 'char nameStr[1000]'. self var: 'src' declareC: 'char * src'. nameOop _ interpreterProxy stackValue: 1. baudRate _ interpreterProxy stackIntegerValue: 0. ((interpreterProxy isIntegerObject: nameOop) or: [(interpreterProxy isBytes: nameOop) not]) ifTrue: [ interpreterProxy success: false. ^ 0]. interpreterProxy failed ifTrue: [^ 0]. src _ self cCoerce: (interpreterProxy firstIndexableField: nameOop) to: 'char *'. count _ interpreterProxy stSizeOf: nameOop. 0 to: count - 1 do: [:i | nameStr at: i put: (src at: i)]. nameStr at: count put: 0. portNum _ self cCode: 'SerialPortOpenPortNamed(nameStr, baudRate)'. portNum = -1 ifTrue: [interpreterProxy success: false. ^ 0]. interpreterProxy "pop args and rcvr, push result" pop: 3 thenPush: (interpreterProxy integerObjectOf: portNum). ^ 0 ! ! !ScratchPlugin methodsFor: 'serial port' stamp: 'jm 4/8/2007 14:52'! primPortCount "Answer the number of serial ports." | result | self export: true. result _ self cCode: 'SerialPortCount()'. result = -1 ifTrue: [interpreterProxy success: false. ^ 0]. interpreterProxy pop: 1 thenPush: (interpreterProxy integerObjectOf: result). "pop rcvr, push result" ^ 0 ! ! !ScratchPlugin methodsFor: 'serial port' stamp: 'jm 8/28/2005 11:02'! primPortName "Get the name for the port with the given number. Fail if the port number is greater than the number of available ports. Port numbering starts with 1." | portIndex nameStr count resultOop dst | self export: true. self var: 'nameStr' declareC: 'char nameStr[1000]'. self var: 'dst' declareC: 'char* dst'. portIndex _ interpreterProxy stackIntegerValue: 0. interpreterProxy failed ifTrue: [^ 0]. self cCode: 'SerialPortName(portIndex, nameStr, 1000)'. count _ self cCode: 'strlen(nameStr)'. count = 0 ifTrue: [ interpreterProxy success: false. ^ 0]. resultOop _ interpreterProxy instantiateClass: interpreterProxy classString indexableSize: count. dst _ self cCoerce: (interpreterProxy firstIndexableField: resultOop) to: 'char *'. 0 to: count - 1 do: [:i | dst at: i put: (nameStr at: i)]. interpreterProxy pop: 2 thenPush: resultOop. "pop arg and rcvr, push result" ^ 0 ! ! !ScratchPlugin methodsFor: 'serial port' stamp: 'jm 8/16/2005 16:44'! primRead "Read data from the given serial port into the given buffer (a ByteArray or String). Answer the number of bytes read." | portNum bufOop bufPtr bufSize bytesRead | self export: true. self var: 'bufPtr' declareC: 'char *bufPtr'. portNum _ interpreterProxy stackIntegerValue: 1. bufOop _ interpreterProxy stackValue: 0. ((interpreterProxy isIntegerObject: bufOop) or: [(interpreterProxy isBytes: bufOop) not]) ifTrue: [ interpreterProxy success: false. ^ 0]. bufPtr _ self cCoerce: (interpreterProxy firstIndexableField: bufOop) to: 'char *'. bufSize _ interpreterProxy stSizeOf: bufOop. interpreterProxy failed ifTrue: [^ nil]. bytesRead _ self cCode: 'SerialPortRead(portNum, bufPtr, bufSize)'. interpreterProxy pop: 3. "pop args and rcvr" interpreterProxy pushInteger: bytesRead. "push result" ^ 0 ! ! !ScratchPlugin methodsFor: 'serial port' stamp: 'jm 4/8/2007 14:55'! primSetOption "Return the given option value for the given serial port." | portNum attrNum attrValue result | self export: true. portNum _ interpreterProxy stackIntegerValue: 2. attrNum _ interpreterProxy stackIntegerValue: 1. attrValue _ interpreterProxy stackIntegerValue: 0. interpreterProxy failed ifTrue: [^ nil]. result _ self cCode: 'SerialPortSetOption(portNum, attrNum, attrValue)'. result = -1 ifTrue: [interpreterProxy success: false. ^ 0]. interpreterProxy pop: 3. "pop args; leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'serial port' stamp: 'jm 8/16/2005 16:45'! primWrite "Write data to the given serial port from the given buffer (a ByteArray or String). Answer the number of bytes written." | portNum bufOop bufPtr bufSize bytesWritten | self export: true. self var: 'bufPtr' declareC: 'char *bufPtr'. portNum _ interpreterProxy stackIntegerValue: 1. bufOop _ interpreterProxy stackValue: 0. ((interpreterProxy isIntegerObject: bufOop) or: [(interpreterProxy isBytes: bufOop) not]) ifTrue: [ interpreterProxy success: false. ^ 0]. bufPtr _ self cCoerce: (interpreterProxy firstIndexableField: bufOop) to: 'char *'. bufSize _ interpreterProxy stSizeOf: bufOop. interpreterProxy failed ifTrue: [^ nil]. bytesWritten _ self cCode: 'SerialPortWrite(portNum, bufPtr, bufSize)'. interpreterProxy pop: 3. "pop args and rcvr" interpreterProxy pushInteger: bytesWritten. "push result" ^ 0 ! ! !ScratchPlugin methodsFor: 'sound' stamp: 'jm 11/21/2006 21:21'! primitiveCondenseSound | srcOop dstOop factor sz src dst count max v | self export: true. self var: 'src' declareC: 'short *src'. self var: 'dst' declareC: 'short *dst'. srcOop _ interpreterProxy stackValue: 2. dstOop _ interpreterProxy stackValue: 1. factor _ interpreterProxy stackIntegerValue: 0. interpreterProxy success: (interpreterProxy isWords: srcOop). interpreterProxy success: (interpreterProxy isWords: dstOop). count _ (2 * (interpreterProxy stSizeOf: srcOop)) // factor. sz _ 2 * (interpreterProxy stSizeOf: dstOop). interpreterProxy success: (sz >= count). interpreterProxy failed ifTrue: [^ nil]. src _ self cCoerce: (interpreterProxy firstIndexableField: srcOop) to: 'short *'. dst _ self cCoerce: (interpreterProxy firstIndexableField: dstOop) to: 'short *'. 1 to: count do: [:i | max _ 0. 1 to: factor do: [:j | v _ self cCode: '*src++'. v < 0 ifTrue: [v _ 0 - v]. v > max ifTrue: [max _ v]]. self cCode: '*dst++ = max']. interpreterProxy pop: 3. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'sound' stamp: 'jm 11/21/2006 20:53'! primitiveExtractChannel | srcOop dstOop rightFlag sz src dst | self export: true. self var: 'src' declareC: 'short *src'. self var: 'dst' declareC: 'short *dst'. srcOop _ interpreterProxy stackValue: 2. dstOop _ interpreterProxy stackValue: 1. rightFlag _ interpreterProxy booleanValueOf: (interpreterProxy stackValue: 0). interpreterProxy success: (interpreterProxy isWords: srcOop). interpreterProxy success: (interpreterProxy isWords: dstOop). sz _ interpreterProxy stSizeOf: srcOop. interpreterProxy success: ((interpreterProxy stSizeOf: dstOop) >= (sz // 2)). interpreterProxy failed ifTrue: [^ nil]. src _ self cCoerce: (interpreterProxy firstIndexableField: srcOop) to: 'short *'. dst _ self cCoerce: (interpreterProxy firstIndexableField: dstOop) to: 'short *'. rightFlag ifTrue: [self cCode: 'src++']. 1 to: sz do: [:i | self cCode: '*dst++ = *src; src += 2']. interpreterProxy pop: 3. "pop args, leave rcvr on stack" ^ 0 ! ! !ScratchPlugin methodsFor: 'private' stamp: 'jm 11/8/2006 05:30'! bitmap: bitmap at: i putH: hue s: saturation v: brightness | hI hF p q t v outPix | self inline: true. self var: 'bitmap' declareC: 'unsigned int *bitmap'. hI _ hue // 60. "integer part of hue (0..5)" hF _ hue \\ 60. "fractional part ofhue" p _ (1000 - saturation) * brightness. q _ (1000 - ((saturation * hF) // 60)) * brightness. t _ (1000 - ((saturation * (60 - hF)) // 60)) * brightness. v _ (brightness * 1000) // 3922. p _ p // 3922. q _ q // 3922. t _ t // 3922. 0 = hI ifTrue: [outPix _ ((v bitShift: 16) + (t bitShift: 8) + p)]. 1 = hI ifTrue: [outPix _ ((q bitShift: 16) + (v bitShift: 8) + p)]. 2 = hI ifTrue: [outPix _ ((p bitShift: 16) + (v bitShift: 8) + t)]. 3 = hI ifTrue: [outPix _ ((p bitShift: 16) + (q bitShift: 8) + v)]. 4 = hI ifTrue: [outPix _ ((t bitShift: 16) + (p bitShift: 8) + v)]. 5 = hI ifTrue: [outPix _ ((v bitShift: 16) + (p bitShift: 8) + q)]. outPix = 0 ifTrue: [outPix _ 1]. "convert transparent to 1" bitmap at: i put: outPix. ^ 0 ! ! !ScratchPlugin methodsFor: 'private' stamp: 'jm 11/8/2006 05:30'! checkedFloatPtrOf: oop "Return an unsigned int pointer to the first indexable word of oop, which must be a words object." self inline: true. self returnTypeC: 'double *'. interpreterProxy success: (interpreterProxy isWordsOrBytes: oop). interpreterProxy failed ifTrue: [^ 0]. ^ self cCoerce: (interpreterProxy firstIndexableField: oop) to: 'double *' ! ! !ScratchPlugin methodsFor: 'private' stamp: 'jm 11/8/2006 05:30'! checkedUnsignedIntPtrOf: oop "Return an unsigned int pointer to the first indexable word of oop, which must be a words object." self inline: true. self returnTypeC: 'unsigned int *'. interpreterProxy success: (interpreterProxy isWords: oop). interpreterProxy failed ifTrue: [^ 0]. ^ self cCoerce: (interpreterProxy firstIndexableField: oop) to: 'unsigned int *' ! ! !ScratchPlugin methodsFor: 'private' stamp: 'jm 11/8/2006 05:30'! hueFromR: r G: g B: b min: min max: max "Answer the hue, an angle between 0 and 360." | span result | self inline: true. span _ max - min. span = 0 ifTrue: [^ 0]. r = max ifTrue: [result _ ((60 * (g - b)) // span)] ifFalse: [ g = max ifTrue: [result _ 120 + ((60 * (b - r)) // span)] ifFalse: [result _ 240 + ((60 * (r - g)) // span)]]. result < 0 ifTrue: [^ result + 360]. ^ result ! ! !ScratchPlugin methodsFor: 'private' stamp: 'jm 11/8/2006 05:30'! interpolate: pix1 and: pix2 frac: frac2 "Answer the interpolated pixel value between the given two pixel values. If either pixel is zero (transparent) answer the other pixel. If both pixels are transparent, answer transparent. The fraction is between 0 and 1023, out of a total range of 1024." | frac1 r g b result | self inline: true. pix1 = 0 ifTrue: [^ pix2]. "pix1 is transparent" pix2 = 0 ifTrue: [^ pix1]. "pix2 is transparent" frac1 _ 1024 - frac2. r _ ((frac1 * ((pix1 bitShift: -16) bitAnd: 16rFF)) + (frac2 * ((pix2 bitShift: -16) bitAnd: 16rFF))) // 1024. g _ ((frac1 * ((pix1 bitShift: -8) bitAnd: 16rFF)) + (frac2 * ((pix2 bitShift: -8) bitAnd: 16rFF))) // 1024. b _ ((frac1 * (pix1 bitAnd: 16rFF)) + (frac2 * (pix2 bitAnd: 16rFF))) // 1024. result _ (r bitShift: 16) + (g bitShift: 8) + b. result = 0 ifTrue: [result _ 1]. ^ result ! ! !ScratchPlugin methodsFor: 'private' stamp: 'jm 11/8/2006 05:30'! interpolatedFrom: bitmap x: xFixed y: yFixed width: w height: h "Answer the interpolated pixel value from the given bitmap at the given point. The x and y coordinates are fixed-point integers with 10 bits of fraction (i.e. they were multiplied by 1024, then truncated). If the given point is right on an edge, answer the nearest edge pixel value. If it is entirely outside of the image, answer 0 (transparent)." | x y xFrac yFrac index topPix bottomPix | self inline: true. self var: 'bitmap' declareC: 'unsigned int *bitmap'. x _ xFixed bitShift: -10. (x < -1 or: [x >= w]) ifTrue: [^ 0]. y _ yFixed bitShift: -10. (y < -1 or: [y >= h]) ifTrue: [^ 0]. xFrac _ xFixed bitAnd: 1023. x = -1 ifTrue: [x _ 0. xFrac _ 0]. "left edge" x = (w - 1) ifTrue: [xFrac _ 0]. "right edge" yFrac _ yFixed bitAnd: 1023. y = -1 ifTrue: [y _ 0. yFrac _ 0]. "top edge" y = (h - 1) ifTrue: [yFrac _ 0]. "bottom edge" index _ (y * w) + x "for squeak: + 1". topPix _ (bitmap at: index) bitAnd: 16rFFFFFF. xFrac > 0 ifTrue: [ topPix _ self interpolate: topPix and: ((bitmap at: index + 1) bitAnd: 16rFFFFFF) frac: xFrac]. yFrac = 0 ifTrue: [^ topPix]. "no y fraction, so just use value from top row" index _ ((y + 1) * w) + x "for squeak: + 1". bottomPix _ (bitmap at: index) bitAnd: 16rFFFFFF. xFrac > 0 ifTrue: [ bottomPix _ self interpolate: bottomPix and: ((bitmap at: index + 1) bitAnd: 16rFFFFFF) frac: xFrac]. ^ self interpolate: topPix and: bottomPix frac: yFrac ! ! !ScratchPlugin class methodsFor: 'translation' stamp: 'jm 1/29/2009 15:05'! declareCVarsIn: aCCodeGenerator "self translate" super declareCVarsIn: aCCodeGenerator. aCCodeGenerator cExtras: ' #include "scratchOps.h" #include #include #include '. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! blurTest: count "self blurTest: 10" | f outBits | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. f display. count timesRepeat: [ outBits _ f bits copy. self primBlur: f bits into: outBits width: f width. f bits: outBits. f display]. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! brightnessShiftTest "self brightnessShiftTest" | f fOut shift | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ f deepCopy. [Sensor anyButtonPressed] whileFalse: [ shift _ ((Sensor cursorPoint x - Display center x) * 220) // Display width. self primShiftBrightness: f bits into: fOut bits by: shift. fOut display]. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! doubleTest "self doubleTest" | f fOut | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ Form extent: (2 * f extent) + 20 depth: 32. self primDouble: f bits w: f width h: f height into: fOut bits w: fOut width h: fOut height x: 9 y: 10. fOut display. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/17/2008 16:39'! filterBenchmark: count "self filterBenchmark: 100" | f fOut s msecs | s _ (WriteStream on: String new) cr. f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ f deepCopy. msecs _ [count timesRepeat: [self primShiftHue: f bits into: fOut bits byDegrees: 100]] msecs. s nextPutAll: 'hue shift: ', ((msecs asFloat / count) roundTo: 0.01) printString, ' msecs'; cr. fOut display. msecs _ [count timesRepeat: [self primShiftBrightness: f bits into: fOut bits by: 50]] msecs. s nextPutAll: 'brightness shift: ', ((msecs asFloat / count) roundTo: 0.01) printString, ' msecs'; cr. fOut display. msecs _ [count timesRepeat: [self primShiftSaturation: f bits into: fOut bits by: 50]] msecs. s nextPutAll: 'saturation shift: ', ((msecs asFloat / count) roundTo: 0.01) printString, ' msecs'; cr. fOut display. msecs _ [count timesRepeat: [self primFisheye: f bits into: fOut bits width: f width power: 300]] msecs. s nextPutAll: 'fisheye: ', ((msecs asFloat / count) roundTo: 0.01) printString, ' msecs'; cr. fOut display. msecs _ [count timesRepeat: [self primWhirl: f bits into: fOut bits width: f width angle: 1000]] msecs. s nextPutAll: 'whirl: ', ((msecs asFloat / count) roundTo: 0.01) printString, ' msecs'; cr. fOut display. msecs _ [count timesRepeat: [fOut display]] msecs. s nextPutAll: 'display: ', ((msecs asFloat / count) roundTo: 0.01) printString, ' msecs'; cr. ^ s contents ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! fishEye: inForm out: outForm power: power | height sz centerX centerY dx dy ang pix width r srcX srcY | "calculate height, center, scales, radius, whirlRadians, and radiusSquared" sz _ inForm bits size. width _ inForm width. height _ sz // width. centerX _ width // 2. centerY _ height // 2. 0 to: width - 1 do: [:x | 0 to: height - 1 do: [:y | dx _ (x - centerX) / centerX asFloat. dy _ (y - centerY) / centerY asFloat. r _ ((dx * dx) + (dy * dy)) sqrt raisedTo: power. r <= 1.0 ifTrue: [ ang _ dy arcTan: dx. srcX _ centerX + ((r * ang cos) * centerX). srcY _ centerY + ((r * ang sin) * centerY)] ifFalse: [ srcX _ x. srcY _ y]. pix _ self primInterpolate: inForm bits width: inForm width x: (srcX * 1024) truncated y: (srcY * 1024) truncated. outForm bits at: ((y * width) + x + 1) put: pix]]. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! fisheyeTest "self fisheyeTest" | f fOut | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ f deepCopy. f display. 100 to: 300 by: 10 do: [:power | self primFisheye: f bits into: fOut bits width: f width power: power. fOut display]. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! fisheyeTest2: power "self fisheyeTest2: 100" | f fOut | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ f deepCopy. f display. self primFisheye: f bits into: fOut bits width: f width power: power. fOut display. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 1/2/2009 10:05'! halfSizeAverageTest "self halfSizeAverageTest" | f fOut | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ Form extent: f extent // 2 depth: 32. self primHalfSizeAverage: f bits w: f width h: f height into: fOut bits w: fOut width h: fOut height srcX: 0 srcY: 0 dstX: 0 dstY: 0 dstW: fOut width dstH: fOut height. f display. fOut display. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 1/2/2009 10:06'! halfSizeDiagonalTest "self halfSizeDiagonalTest" | f fOut | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ Form extent: f extent // 2 depth: 32. self primHalfSizeDiagonal: f bits w: f width h: f height into: fOut bits w: fOut width h: fOut height srcX: 0 srcY: 0 dstX: 0 dstY: 0 dstW: fOut width dstH: fOut height. f display. fOut display. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! hueShiftTest "self hueShiftTest" | f fOut shift | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ f deepCopy. [Sensor anyButtonPressed] whileFalse: [ shift _ ((Sensor cursorPoint x - Display center x) * 380 * 2) // Display width. self primShiftHue: f bits into: fOut bits byDegrees: shift. fOut display]. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! interpolationTest: scale "Answer a copy of the given form scaled by the given factor using linear interpolation." "(self interpolationTest: 1.5) display" | scaleP srcForm outExtent fOut w h outW pix outH | scaleP _ scale asPoint. (scaleP x <= 0 or: [scaleP y <= 0]) ifTrue: [self error: 'bad scale factor']. srcForm _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. outExtent _ (srcForm extent * scaleP) truncated. (outExtent x > 1000 or: [outExtent y > 1000]) ifTrue: [self halt: 'result width or height will be > 1000 pixels']. fOut _ Form extent: outExtent depth: 32. w _ srcForm width. h _ srcForm height. outW _ fOut width. outH _ fOut height. 0 to: fOut width - 1 do: [:x | 0 to: fOut height - 1 do: [:y | pix _ self primInterpolate: srcForm bits width: srcForm width x: (x * w * 1024) // outW y: (y * h * 1024) // outH. fOut bits at: ((y * outW) + x + 1) put: pix]]. ^ fOut ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! saturationShiftTest "self saturationShiftTest" | f fOut shift | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ f deepCopy. [Sensor anyButtonPressed] whileFalse: [ shift _ ((Sensor cursorPoint x - Display center x) * 220) // Display width. self primShiftSaturation: f bits into: fOut bits by: shift. fOut display]. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! scaleTest: scale "self scaleTest: 1.5" | f fOut | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ Form extent: (f extent * scale) rounded depth: 32. self primScale: f bits w: f width h: f height into: fOut bits w: fOut width h: fOut height. fOut display. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! testFileName: aString "Set the name of an image file for testing." "self testFileName: 'hammy.jpg'" "self testFileName: 'JohnMugShotBW.jpg'" TestFileName _ aString. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! waterRipples1ModuleTest "Smalltalk unloadPlugin: self name" "self waterRipples1ModuleTest" | f fOut ripply aArray bArray | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ f deepCopy. aArray _ ByteArray new: (f width) * (f height) * 8 withAll: 0. bArray _ ByteArray new: (f width) * (f height) * 8 withAll: 0. [Sensor anyButtonPressed] whileFalse: [ ripply _ Sensor cursorPoint x max: 1. ripply _ (((ripply / fOut width) sqrt) * 16.0) asInteger. ripply < 1 ifTrue: [ripply _ 1]. ripply printString display. self primWaterRipples1: f bits into: fOut bits width: f width dropNum: ripply array1: aArray array2: bArray. fOut displayAt: 10@30]. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! waterRipples1ModuleTest: n "Smalltalk unloadPlugin: self name" "self waterRipples1ModuleTest: 100" | f fOut aArray bArray | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ f deepCopy. aArray _ ByteArray new: (f width * f height) * 8 withAll: 0. bArray _ ByteArray new: (f width * f height) * 8 withAll: 0. self primWaterRipples1: f bits into: fOut bits width: f width dropNum: n array1: aArray array2: bArray. [Sensor anyButtonPressed] whileFalse: [ self primWaterRipples1: f bits into: fOut bits width: f width dropNum: 1 array1: aArray array2: bArray. fOut displayAt: 10@30]. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! waterRipplesTime: n "Smalltalk unloadPlugin: self name" "self waterRipplesTime: 100" | f fOut aArray bArray | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ f deepCopy. aArray _ ByteArray new: (f width * f height) * 8 withAll: 0. bArray _ ByteArray new: (f width * f height) * 8 withAll: 0. self primWaterRipples1: f bits into: fOut bits width: f width dropNum: n array1: aArray array2: bArray. ^ [100 timesRepeat: [ self primWaterRipples1: f bits into: fOut bits width: f width dropNum: 100 array1: aArray array2: bArray]] msecs. ! ! !ScratchPlugin class methodsFor: 'image filters-testing' stamp: 'jm 11/8/2006 05:30'! whirlTest "self whirlTest" | f fOut degrees | f _ (Form fromFileNamed: TestFileName) asFormOfDepth: 32. fOut _ f deepCopy. [Sensor anyButtonPressed] whileFalse: [ degrees _ ((Sensor cursorPoint x - Display center x) * 450 * 2) // Display width. self primWhirl: f bits into: fOut bits width: f width angle: degrees. fOut display]. ! ! !ScratchPlugin class methodsFor: 'image filter primitives' stamp: 'jm 11/8/2006 10:58'! primBlur: inBitmap into: outBitmap width: w "Blur all the non-transparent pixels in the given 32-bit image bitmap, storing the result in outBitmap. The two bitmaps must be the same size. Each call to this primitive does one Gausian blur step." self primitiveFailed ! ! !ScratchPlugin class methodsFor: 'image filter primitives' stamp: 'jm 11/8/2006 10:58'! primFisheye: inBitmap into: outBitmap width: w power: anInteger "Do a fisheye lens transform of the given 32-bit image bitmap by the given power, storing the result in outBitmap. The two bitmaps must be the same size. Power is 0 for no change, > 0 for fisheye, < 0 for black hole effect." self primitiveFailed ! ! !ScratchPlugin class methodsFor: 'image filter primitives' stamp: 'jm 2/10/2008 11:58'! primInterpolate: aBitmap width: w x: xFixed y: yFixed "Answer the interpolated pixel value from the given 32-bit bitmap with the given width. The coordinates are given as fixed-point integers with 10-bits of fraction. That is, the float values of x and y are multiplied by 1024, then truncated." ^ 0! ! !ScratchPlugin class methodsFor: 'image filter primitives' stamp: 'jm 11/8/2006 10:58'! primShiftBrightness: inBitmap into: outBitmap by: shift "Shift the brightness of all the non-transparent pixels in the given 32-bit image bitmap, storing the result in outBitmap. The shift should be an integer between -100 and 100. The two bitmaps must be the same size." self primitiveFailed ! ! !ScratchPlugin class methodsFor: 'image filter primitives' stamp: 'jm 1/23/2007 15:58'! primShiftHue: inBitmap into: outBitmap byDegrees: shiftDegrees "Shift the hue of all the non-transparent, non-black pixels in the given 32-bit image bitmap, storing the result in outBitmap. The shift should be an integer between -360 and 360. The two bitmaps must be the same size." self primitiveFailed. ^ nil ! ! !ScratchPlugin class methodsFor: 'image filter primitives' stamp: 'jm 11/8/2006 10:58'! primShiftSaturation: inBitmap into: outBitmap by: shift "Shift the saturation of all the non-transparent, non-black pixels in the given 32-bit image bitmap, storing the result in outBitmap. The shift should be an integer between -100 and 100. The two bitmaps must be the same size." self primitiveFailed ! ! !ScratchPlugin class methodsFor: 'image filter primitives' stamp: 'jm 11/8/2006 10:58'! primWaterRipples1: inBitmap into: outBitmap width: w dropNum: aNum array1: aArray array2: bArray "Apply the water ripple effect to inBitmap putting the result into outBitmap. The two bitmaps must have the same length and are for 32-bit deep Forms of the given width. The dropNum determines how many new water drops are started. The two arrays hold the state of the water-surface model." self primitiveFailed ! ! !ScratchPlugin class methodsFor: 'image filter primitives' stamp: 'jm 11/8/2006 10:58'! primWhirl: inBitmap into: outBitmap width: w angle: anAngle "Whirl all the non-transparent pixels in the given 32-bit image bitmap by the given angle, storing the result in outBitmap. The two bitmaps must be the same size." self primitiveFailed ! ! !ScratchPlugin class methodsFor: 'image scale primitives' stamp: 'jm 1/23/2009 11:13'! halfSize: srcForm into: dstForm srcPoint: srcPoint dstRect: dstRect "Display the source form at half-size onto the destination form at the given location. Use WarpBlt if the fast primitive fails." | result srcR | srcForm unhibernate. dstForm unhibernate. result _ self primHalfSizeAverage: srcForm bits w: srcForm width h: srcForm height into: dstForm bits w: dstForm width h: dstForm height srcX: srcPoint x srcY: srcPoint y dstX: dstRect left dstY: dstRect top dstW: dstRect width dstH: dstRect height. result ifNotNil: [^ self]. srcR _ srcPoint extent: 2 * dstRect extent. (WarpBlt toForm: dstForm) sourceForm: srcForm; combinationRule: Form over; clipRect: dstRect; cellSize: 2; copyQuad: srcR corners toRect: dstRect. ! ! !ScratchPlugin class methodsFor: 'image scale primitives' stamp: 'jm 11/8/2006 10:58'! primDouble: srcBitmap w: srcWidth h: srcHeight into: dstBitmap w: dstWidth h: dstHeight x: dstX y: dstY "Display the source form at double-size onto the destination form at the given location. Fails if the target rectangle does not fit entirely within the destination form." self primitiveFailed ! ! !ScratchPlugin class methodsFor: 'image scale primitives' stamp: 'jm 1/2/2009 10:18'! primHalfSizeAverage: srcBits w: srcW h: srcH into: dstBits w: dstW h: dstH srcX: x1 srcY: y1 dstX: x2 dstY: y2 dstW: w dstH: h "Display the source form at half-size onto the destination form at the given location by averaging the colors a 2x2 cell. Does not handle alpha or transparency. Return nil if the target rectangle does not fit entirely within the destination form." ^ nil ! ! !ScratchPlugin class methodsFor: 'image scale primitives' stamp: 'jm 1/2/2009 10:18'! primHalfSizeDiagonal: srcBits w: srcW h: srcH into: dstBits w: dstW h: dstH srcX: x1 srcY: y1 dstX: x2 dstY: y2 dstW: w dstH: h "Display the source form at half-size onto the destination form at the given location by averaging the colors of two pixels along the diagonal of a 2x2 cell, which is faster than averaging all four pixels but does not do as good a job on text. Does not handle alpha or transparency. Return nil if the target rectangle does not fit entirely within the destination form." ^ nil ! ! !ScratchPlugin class methodsFor: 'image scale primitives' stamp: 'jm 11/8/2006 10:58'! primScale: srcBitmap w: srcWidth h: srcHeight into: dstBitmap w: dstWidth h: dstHeight "Scale the source form to exactly fit the destination form using bilinear interpolation." self primitiveFailed ! ! !ScratchPlugin class methodsFor: 'image scale primitives' stamp: 'jm 11/8/2006 10:58'! primScaleNoFail: srcBitmap w: srcWidth h: srcHeight into: dstBitmap w: dstWidth h: dstHeight "Scale the source form to exactly fit the destination form using bilinear interpolation. Answer nil if I fail." ^ nil ! ! !ScratchPlugin class methodsFor: 'image scale primitives' stamp: 'jm 11/8/2006 05:30'! scale: aForm by: scale "Answer a 32-bit deep Form that's aForm scaled by the given factor. Scales using linear interpolation." | srcF scaledF r | srcF _ aForm asFormOfDepth: 32. srcF unhibernate. scaledF _ Form extent: (srcF extent * scale) rounded depth: 32. r _ self primScaleNoFail: srcF bits w: srcF width h: srcF height into: scaledF bits w: scaledF width h: scaledF height. r ifNil: [^ srcF magnify: srcF boundingBox by: scale asFloat smoothing: 1]. ^ scaledF ! ! !ScratchPlugin class methodsFor: 'OS utilities' stamp: 'jm 10/29/2007 10:30'! isHidden: fullPath "Return true if file or folder with the given path should be hidden from the user. Return false if the primitive fails." "self isHidden: 'testfile.txt'" Smalltalk isMacOSX ifTrue: [^ false]. ((fullPath endsWith: ':\') and: [fullPath size = 3]) ifTrue: [^ false]. ^ self primIsHidden: fullPath ! ! !ScratchPlugin class methodsFor: 'OS utilities' stamp: 'jm 1/23/2007 15:59'! pluginAvailable "self pluginAvailable" | f r | f _ Form extent: 1@1 depth: 32. [r _ self primShiftHue: f bits into: f bits byDegrees: 180] ifError: [^ false]. ^ r notNil ! ! !ScratchPlugin class methodsFor: 'OS utilities' stamp: 'jm 1/23/2007 15:46'! primGetFolderPath: anInteger "Return the folder path for the given ID. Folder ID's are: 1 home 2 desktop 3 documents 4 my pictures 5 my music. Return the path for the Scratch folder if the primitive fails." "self primGetFolderPath: 1" ^ FileDirectory default pathName ! ! !ScratchPlugin class methodsFor: 'OS utilities' stamp: 'jm 2/22/2009 21:58'! primGetFolderPathOrNil: anInteger "Return the folder path for the given ID. Folder ID's are: 1 home 2 desktop 3 documents 4 my pictures 5 my music. Return nil if the primitive fails." "self primGetFolderPathOrNil: 1" ^ nil ! ! !ScratchPlugin class methodsFor: 'OS utilities' stamp: 'jm 6/22/2007 13:28'! primIsHidden: fullPath "Return true if file or folder with the given path should be hidden from the user. Return false if the primitive fails." "self primIsHidden: 'testfile.txt'" ^ false ! ! !ScratchPlugin class methodsFor: 'OS utilities' stamp: 'jm 1/23/2007 15:45'! primOpenURL: aString "Open a browser window on the given URL. Do nothing if the primitive fails." "self primOpenURL: 'http://www.google.com'" ! ! !ScratchPlugin class methodsFor: 'OS utilities' stamp: 'jm 10/5/2007 09:12'! primSetUnicodePasteBuffer: aByteArray "Set the Mac OS X Unicode paste buffer. The argument is a big-endian UTF-16 Unicode string packed into a ByteArray. Needed to paste strings from Squeak into Second Life's code editor under Mac OS X. Do nothing if the primitive fails." "self primSetUnicodePasteBuffer: ByteArray new" ! ! !ScratchPlugin class methodsFor: 'OS utilities' stamp: 'jm 1/23/2007 15:45'! primSetWindowTitle: aString "Set the title of the Scratch window to the given string. Do nothing if the primitive fails." "self primSetWindowTitle: 'hello!!'" ! ! !ScratchPlugin class methodsFor: 'OS utilities' stamp: 'jm 4/11/2007 09:02'! primShortToLongPath: aString "Convert the given Windows short-filename path into a long-filename path. On other platforms this primitive will just return the input string." "self primShortToLongPath: 'hello!!'" ^ aString ! ! !ScratchPlugin class methodsFor: 'OS utilities' stamp: 'jm 9/2/2008 16:45'! setUnicodePasteBuffer: aString "Set the Mac OS X Unicode paste buffer to the given Squeak string. Since the source is a Squeak string, there is no need to handle UTF-16 extended (4-byte) characters. However, we must take byte order into account to accomodate both Intel and PowerPC." "self setUnicodePasteBuffer: 'Hello, Unicode!!'" | utf32 s | utf32 _ aString asUTF32. s _ WriteStream on: (ByteArray new: 2 * aString size). Smalltalk isBigEndian ifTrue: [utf32 do: [:u | s nextPut: (u >> 8). s nextPut: (u bitAnd: 255)]] ifFalse: [utf32 do: [:u | s nextPut: (u bitAnd: 255). s nextPut: (u >> 8)]]. self primSetUnicodePasteBuffer: s contents. ! ! !ScratchPlugin class methodsFor: 'sound buffer utilities' stamp: 'jm 11/22/2006 19:40'! compactSound: aSoundBuffer by: log2 "Collapse the given by cutting in half log2 times." "self compactSound: (SoundBuffer fromArray: (1 to: 100) asArray) by: 3" | buf | buf _ aSoundBuffer. log2 timesRepeat: [buf _ self extractChannelFrom: buf rightFlag: false]. ^ buf ! ! !ScratchPlugin class methodsFor: 'sound buffer utilities' stamp: 'jm 11/21/2006 21:22'! condenseSoundBuffer: aSoundBuffer by: factor "Condense the given SoundBuffer by the given factor. The result is a SoundBuffer 1/factor of the original size in which each sample represents the peak signal value over factor samples of the source." "self condenseSoundBuffer: (SoundBuffer fromArray: (1 to: 100) asArray) by: 10" | result | result _ SoundBuffer newMonoSampleCount: (aSoundBuffer size + factor - 1) // factor. self primCondense: aSoundBuffer into: result by: factor. ^ result ! ! !ScratchPlugin class methodsFor: 'sound buffer utilities' stamp: 'jm 11/21/2006 21:22'! extractChannelFrom: aSoundBuffer rightFlag: rightFlag "Extract one channel from the given stereo sound buffer. If rightFlag is true, extract the right channel; otherwise, extract the left one." "self extractChannelFrom: (SoundBuffer fromArray: #(1 2 3 4)) rightFlag: true" | result | result _ SoundBuffer newMonoSampleCount: aSoundBuffer size // 2. self primExtractChannelFrom: aSoundBuffer into: result rightFlag: rightFlag. ^ result ! ! !ScratchPlugin class methodsFor: 'sound buffer utilities' stamp: 'jm 11/21/2006 21:12'! primCondense: srcSoundBuffer into: dstSoundBuffer by: anInteger "Condense the given SoundBuffer by the given factor storing the rsult into the destination SoundBuffer." self primitiveFailed ! ! !ScratchPlugin class methodsFor: 'sound buffer utilities' stamp: 'jm 11/21/2006 20:33'! primExtractChannelFrom: srcSoundBuffer into: dstSoundBuffer rightFlag: rightFlag "Extract the given channel of the source SoundBuffer in the destination SoundBuffer." self primitiveFailed ! ! !ScratchPlugin class methodsFor: 'serial port primitives' stamp: 'jm 8/31/2005 10:01'! closePort: portNum "Close the given port." "self closePort: 1" ! ! !ScratchPlugin class methodsFor: 'serial port primitives' stamp: 'jm 8/28/2005 11:11'! isPortOpen: portNum "Answer true if the given serial port is open." "self isPortOpen: 1" ^ false ! ! !ScratchPlugin class methodsFor: 'serial port primitives' stamp: 'jm 8/31/2005 10:03'! openPortNamed: portName baud: baudRate "Open the port with the given name at the given baud rate. Answer the port number to use for further operations on the given port or -1 if the port could not be opened." "self openPortNamed: '/dev/cu.USA19QW3b1P1.1' baud: 9600" ^ -1 ! ! !ScratchPlugin class methodsFor: 'serial port primitives' stamp: 'jm 9/12/2005 16:56'! port: portNum getOption: optionNum "Answer the value of the given serial port option, or nil if the port is not open or the option is not defined. See the class comment for the list of options." ^ nil ! ! !ScratchPlugin class methodsFor: 'serial port primitives' stamp: 'jm 9/12/2005 16:57'! port: portNum setOption: optionNum to: anInteger "Set the given serial port option to the given value. Do nothing if the option is not defined. See the class comment for the list of options." ! ! !ScratchPlugin class methodsFor: 'serial port primitives' stamp: 'jm 8/4/2005 17:24'! portCount "Answer the number of serial ports. Answer 0 if there are no ports or if this primitive fails." "self portCount" ^ 0 ! ! !ScratchPlugin class methodsFor: 'serial port primitives' stamp: 'jm 8/26/2005 15:25'! portCountOrNil "Answer the number of serial ports. Answer nil if this primitive fails." "self portCountOrNil" ^ nil ! ! !ScratchPlugin class methodsFor: 'serial port primitives' stamp: 'jm 8/26/2005 15:47'! portName: portIndex "Answer the name of the serial port with the given index. Answer nil if there is no port with the given index." "self portName: 1" ^ nil ! ! !ScratchPlugin class methodsFor: 'serial port primitives' stamp: 'jm 8/16/2005 15:57'! readPort: portNum into: buffer "Read from the given port into the given ByteArray or String and answer the number of bytes read." "self readPort: 1 into: (ByteArray new: 10)" ^ 0 ! ! !ScratchPlugin class methodsFor: 'serial port primitives' stamp: 'jm 11/8/2006 19:45'! serialPortOpsAvailable "Answer true if this plugin is available." "self serialPortOpsAvailable" "Smalltalk unloadPlugin: self name" ^ self portCountOrNil notNil ! ! !ScratchPlugin class methodsFor: 'serial port primitives' stamp: 'jm 8/16/2005 16:46'! writePort: portNum data: buffer "Write data from the given ByteArray or String to the given port and answer the number of bytes written." "self writePort: 1 into: (ByteArray new: 10)" ^ 0 ! ! !ScratchPlugin class methodsFor: 'primitive failure' stamp: 'jm 1/23/2007 15:53'! primitiveFailed "Just beep rather than bringing up an error notifier." self beep. ! ! I present the Scratch stage all by itself on the screen in presentation mode. ! !ScratchPresenterMorph methodsFor: 'initialization' stamp: 'jm 10/21/2005 11:45'! beDoubleSize doubleSize _ true. ScriptableScratchMorph doubleSize: true. ! ! !ScratchPresenterMorph methodsFor: 'initialization' stamp: 'jm 12/2/2005 18:51'! buttons ^ buttons ! ! !ScratchPresenterMorph methodsFor: 'initialization' stamp: 'jm 8/3/2008 14:21'! frame: aScratchFrameMorph frame _ aScratchFrameMorph. stage _ aScratchFrameMorph workPane. "insert stage in an OffscreenWorldMorph to detect damage" offscreenWorld _ OffscreenWorldMorph new on: stage. offscreenWorld frame: aScratchFrameMorph. ! ! !ScratchPresenterMorph methodsFor: 'initialization' stamp: 'jm 3/24/2009 17:23'! initialize | button | super initialize. self color: Color black. doubleSize _ false. lastStepTime _ IdentityDictionary new. buttons _ #( (presentationExit exitPresentation 'Exit presentation') (go shoutGo 'Start green flag scripts') (stop stopAll 'Stop everything') ) collect: [:spec | button _ ToggleButton onForm: (ScratchFrameMorph skinAt: (spec first, 'ButtonBlackPressed')) offForm: (ScratchFrameMorph skinAt: (spec first, 'ButtonBlack')) overForm: (ScratchFrameMorph skinAt: (spec first, 'ButtonBlackPressed')). button target: self; actionSelector: spec second; isMomentary: true; actWhen: #buttonUp; setProperty: #balloonText toValue: spec third localized; drawToolTipAbove: true; off. #shoutGo = spec second ifTrue: [flagButton _ button]. button]. buttons do: [:b | self addMorph: b]. ! ! !ScratchPresenterMorph methodsFor: 'button actions' stamp: 'ee 12/30/2008 13:11'! exitPresentation self delete. frame exitPresentationMode. ! ! !ScratchPresenterMorph methodsFor: 'button actions' stamp: 'jm 8/3/2008 14:11'! shoutGo frame ifNotNil: [frame shoutGo]. flagButton on. World displayWorldSafely. "force button flash" Delay waitMSecs: 20. ! ! !ScratchPresenterMorph methodsFor: 'button actions' stamp: 'jm 8/3/2008 14:15'! stopAll frame ifNotNil: [frame stopAll]. ! ! !ScratchPresenterMorph methodsFor: 'geometry' stamp: 'jm 8/3/2005 11:45'! extent: aPoint super extent: aPoint. self fixLayout. ! ! !ScratchPresenterMorph methodsFor: 'geometry' stamp: 'ee 1/28/2009 14:15'! fixLayout | stageExtent stageBox y totalW x | stageExtent _ doubleSize ifTrue: [2 * stage extent] ifFalse: [stage extent]. stageBox _ Rectangle center: self center extent: stageExtent. stage owner center: self center. y _ stageBox top - 24. buttons first position: (stageBox left + 5) @ y. "exit presentation button" "other buttons" totalW _ (buttons collect: [:b | b width] from: 2 to: buttons size) sum. x _ stageBox right - totalW - (2 * (buttons size - 2)) - 5. buttons allButFirst do: [:b | b position: x@y. x _ x + b width + 2]. offscreenWorld redrawAll. ! ! !ScratchPresenterMorph methodsFor: 'drawing' stamp: 'jm 10/22/2005 18:23'! drawOn: aCanvas | stageRect | stageRect _ Rectangle center: stage center extent: (doubleSize ifTrue: [2 * stage extent] ifFalse: [stage extent]). (self bounds areasOutside: stageRect) do: [:r | aCanvas fillRectangle: r color: color]. (aCanvas clipRect intersects: stageRect) ifTrue: [ offscreenWorld invalidRect: aCanvas clipRect. offscreenWorld incrRedrawDouble: doubleSize]. ! ! !ScratchPresenterMorph methodsFor: 'event handling' stamp: 'jm 10/22/2005 14:00'! handlesMouseDown: evt ^ true ! ! !ScratchPresenterMorph methodsFor: 'event handling' stamp: 'jm 6/2/2009 11:43'! mouseDown: evt "There are three possible cases: drag a sprite, and adjust a slider, and do nothing. To drag a sprite, set mouseFocus to the sprite and dragOffset to the cursor offset the sprite's position. To adjust a slider, set the mouseFocus to the slider and dragOffset to nil. To do nothing, set both to nil." | p adjustedEvt rootM | mouseFocus _ nil. dragOffset _ nil. clickWasHandled _ false. mouseMoved _ false. evt hand toolType: nil. p _ mouseDownPoint _ stage adjustedCursorPoint. adjustedEvt _ evt copy setCursorPoint: p. rootM _ stage submorphs detect: [:m | (m containsPoint: p) & m isVisible] ifNone: [ (stage containsPoint: p) ifTrue: [stage click: adjustedEvt]. clickWasHandled _ true. ^ self]. (rootM isKindOf: ScratchSpriteMorph) ifTrue: [ rootM draggable ifTrue: [ rootM comeToFront. mouseFocus _ rootM. dragOffset _ rootM position - p] ifFalse: [ rootM click: adjustedEvt. clickWasHandled _ true]. ^ self]. (rootM unlockedMorphsAt: p) do: [:m | (m handlesMouseDown: adjustedEvt) ifTrue: [ mouseFocus _ m. dragOffset _ nil. mouseFocus mouseDown: adjustedEvt. ^ self]]. ! ! !ScratchPresenterMorph methodsFor: 'event handling' stamp: 'jm 11/28/2007 15:32'! mouseMove: evt | p | p _ stage adjustedCursorPoint. p = mouseDownPoint ifFalse: [mouseMoved _ true]. mouseFocus ifNotNil: [ dragOffset ifNil: [mouseMoved ifTrue: [mouseFocus mouseMove: (evt copy setCursorPoint: p)]] ifNotNil: [mouseFocus position: p + dragOffset]]. ! ! !ScratchPresenterMorph methodsFor: 'event handling' stamp: 'jm 11/28/2007 15:37'! mouseUp: evt | p | self mouseMove: evt. clickWasHandled ifTrue: [^ self]. p _ stage adjustedCursorPoint. mouseFocus ifNotNil: [ mouseMoved ifFalse: [mouseFocus click: (MorphicEvent new setCursorPoint: p)]]. mouseFocus _ nil. dragOffset _ nil. ! ! !ScratchPresenterMorph methodsFor: 'event handling' stamp: 'jm 3/2/2009 15:39'! processKeyboardEvents | prompter evt ch | prompter _ stage submorphs detect: [:m | m isKindOf: ScratchPrompterMorph] ifNone: [nil]. World hands do: [:h | [(evt _ h nextUnclaimedKeystrokeOrNil) notNil] whileTrue: [ ch _ evt keyValue. ch = 27 ifTrue: [^ self exitPresentation]. "escape" prompter ifNotNil: [ prompter dispatchKeyStroke: evt] ifNil: [ (ch = 3) | (ch = 13) ifTrue: [^ self shoutGo]. stage broadcastEventNamed: 'Scratch-KeyPressedEvent' with: evt]]]. ! ! !ScratchPresenterMorph methodsFor: 'event handling' stamp: 'jm 6/4/2009 12:48'! processWhenConditions "Trigger any 'when ' hats." | objList | true ifTrue: [^ self]. "disabled" objList _ stage submorphs select: [:m | m isKindOf: ScriptableScratchMorph]. objList _ objList copyWith: stage. objList do: [:obj | obj scripts do: [:hat | (hat isMemberOf: WhenHatBlockMorph) ifTrue: [ (hat hasRunningProcess not and: [hat evaluateCondition]) ifTrue: [ hat start; layoutChanged]]]]. ! ! !ScratchPresenterMorph methodsFor: 'stepping' stamp: 'jm 3/24/2009 17:26'! step "Run each process until it gives up control, then filter out any processes that have terminated." | screenExtent | screenExtent _ DisplayScreen actualScreenSize. ((self position = (0@0)) and: [self extent = screenExtent]) ifFalse: [ self position: 0@0. ^ self extent: screenExtent]. ScriptableScratchMorph scratchOrigin: stage center. stage scratchServer ifNotNil: [stage scratchServer stepServer]. self processWhenConditions. self processKeyboardEvents. stage stepProcesses. stage step. self stepSubmorphs. offscreenWorld incrRedrawDouble: doubleSize. stage processesToRun size > 0 ifTrue: [flagButton on] ifFalse: [flagButton off]. ! ! !ScratchPresenterMorph methodsFor: 'stepping' stamp: 'jm 3/24/2009 17:30'! stepSubmorphs "Run my submorph 'step' methods if their time has come." | now lastTime | now _ Time millisecondClockValue. stage submorphsDo: [:topM | topM allMorphsDo: [:m | lastTime _ lastStepTime at: m ifAbsent: [-1000000]. ((now - lastTime) > m stepTime) ifTrue: [ m step. lastStepTime at: m put: now]]]. ! ! !ScratchPresenterMorph methodsFor: 'stepping' stamp: 'jm 8/3/2005 11:26'! stepTime "Every screen update cycle." ^ 0 ! ! A ScratchProcess is what brings a stack of blocks to life. The process keeps track of which block to run next, evaluates block arguments, handles control structures, and so forth. The ScratchFrameMorph is the scheduler, telling each process when to run by calling its runStep method. The runStep method will execute some number of blocks, then voluntarily yield control so that the ScratchFrameMorph can run another process. The etiquette is that a process should yield control at the end of every loop iteration, and while it is running a timed command (e.g. "wait 5 secs") or a synchronous command (e.g. "broadcast xxx and wait"). Structure: stackFrame the ScratchStackFrame describing the current state of this process readyToYield boolean indicating whether to yield control to another process errorFlag boolean indicating whether an error was encountered readyToTerminate boolean indicating whether the stop method has been called ! !ScratchProcess methodsFor: 'initialization' stamp: 'jm 3/22/2005 10:59'! initialize stackFrame _ nil. readyToYield _ false. errorFlag _ false. readyToTerminate _ false. ! ! !ScratchProcess methodsFor: 'accessing' stamp: 'jm 11/20/2003 11:12'! errorFlag "Answer true if this process has stopped due to an error." ^ errorFlag ! ! !ScratchProcess methodsFor: 'accessing' stamp: 'jm 6/4/2009 14:43'! errorFlag: aBoolean "Set the error flag for this process." errorFlag _ aBoolean. stackFrame expression showError. stackFrame expression topBlock showErrorFeedback. ! ! !ScratchProcess methodsFor: 'accessing' stamp: 'jm 11/20/2003 11:12'! expression: expression "Sets the expression to evaluate. This can be a single block or a collection of blocks. It should only be called before running the process." stackFrame ifNotNil: [self error: 'Cannot modify expression']. stackFrame _ ScratchStackFrame new expression: expression. ! ! !ScratchProcess methodsFor: 'accessing' stamp: 'jm 12/9/2008 17:09'! includesDeletedReceiver "Answer true if I contain a block whose receiver has been deleted." self stackAsArray do: [:b | ((b isKindOf: CommandBlockMorph) and: [b receiver owner isNil]) ifTrue: [^ true]]. ^ false ! ! !ScratchProcess methodsFor: 'accessing' stamp: 'jm 5/28/2004 20:38'! includesReceiver: anObject "Answer true if I have the given object as a block receiver." self stackAsArray do: [:frm | ((frm respondsTo: #receiver) and: [frm receiver == anObject]) ifTrue: [^ true]]. ^ false ! ! !ScratchProcess methodsFor: 'accessing' stamp: 'jm 6/4/2009 13:00'! isRunning "Answer true if I am still running, have not encountered an error, and don't refer to any deleted objects." ^ (stackFrame notNil and: [errorFlag not and: [self includesDeletedReceiver not]]) ! ! !ScratchProcess methodsFor: 'accessing' stamp: 'jm 12/30/2008 18:33'! topBlock: aBlock topBlock _ aBlock. ! ! !ScratchProcess methodsFor: 'entry points' stamp: 'jm 10/24/2005 18:45'! runStepFor: aStageMorph "Evaluates the current expression, then the next, etc., until the next yield." | expr | "evaluate blocks until we're ready to yield" readyToYield _ false. [readyToYield or: [stackFrame isNil]] whileFalse: [ self evaluateFor: aStageMorph]. readyToTerminate ifTrue: [ "pop all stack frames, unlighting enclosing blocks:" [stackFrame isNil] whileFalse: [ expr _ stackFrame expression. (expr isKindOf: BlockMorph) ifTrue: [expr litUp: false]. self popStackFrame]]. ! ! !ScratchProcess methodsFor: 'entry points' stamp: 'jm 2/3/2008 19:14'! stepUntilDoneFor: aStageMorph [self isRunning] whileTrue: [ Sensor keyboardPressed. self runStepFor: aStageMorph]. ! ! !ScratchProcess methodsFor: 'entry points' stamp: 'jm 6/4/2009 13:12'! stop "Permanently terminates this process." stackFrame ifNotNil: [stackFrame stopMIDI; stopMotors; stopTalkThinkAsk]. readyToYield _ true. readyToTerminate _ true. topBlock ifNotNil: [topBlock scratchProc: nil]. ! ! !ScratchProcess methodsFor: 'private-evaluation' stamp: 'jm 2/23/2004 11:25'! applyPrimitive "Apply the current expression (which must be a CommandBlock) to the current arguments (which must all have been evaluated)." | value | value _ stackFrame expression evaluateWithArgs: stackFrame arguments. "save the return value in the parent frame before popStackFrame because popFrame adds a frame while single-stepping" self returnValueToParentFrame: value. self popStackFrame. ! ! !ScratchProcess methodsFor: 'private-evaluation' stamp: 'jm 6/3/2008 17:30'! applyTimedCommand "Applies the current command to the already evaluated list of arguments over a particular time interval." | block arguments currentTime startTime args totalMSecs elapsedMSecs | block _ stackFrame expression. arguments _ stackFrame arguments. "Do we still need to evaluate more arguments?" arguments size < block argumentCount ifTrue: [^ self evaluateNextArgument]. arguments _ block coerceArgs: arguments. "Record or get the time when command was first invoked." currentTime _ Time millisecondClockValue. startTime _ stackFrame startTime. startTime ifNil: [ "first call; just set starting time and value" args _ arguments asArray, (Array with: 0 with: nil). stackFrame startValue: (block receiver perform: block selector withArguments: args). stackFrame startTime: currentTime. readyToYield _ true. ^ self]. "Call primitive time command with its arguments and the elapsed time in seconds" totalMSecs _ arguments last * 1000. block selector = #glideSecs:toX:y:elapsed:from: ifTrue: [totalMSecs _ arguments first * 1000]. block selector = #mwait:elapsed:from: ifTrue: [totalMSecs _ arguments last]. ((block selector = #drum:duration:elapsed:from:) or: [block selector = #noteOn:duration:elapsed:from:]) ifTrue: [totalMSecs _ (60000 * arguments second) / block receiver tempo]. block selector = #rest:elapsed:from: ifTrue: [totalMSecs _ (60000 * arguments first) / block receiver tempo]. elapsedMSecs _ currentTime - startTime. currentTime < startTime ifTrue: [elapsedMSecs _ totalMSecs]. "clock wrap" args _ arguments asArray, (Array with: elapsedMSecs with: stackFrame startValue). block receiver perform: block selector withArguments: args. "If not done, then we leave stack as is and yield." elapsedMSecs < totalMSecs ifTrue: [ readyToYield _ true. ^ self]. "Pop this command off the stack and return." self popStackFrame. ! ! !ScratchProcess methodsFor: 'private-evaluation' stamp: 'jm 1/3/2006 22:21'! evaluateCommandFor: aStageMorph "Evaluates the current block. If the argument is non-nil, redraw the stage." | expression | expression _ stackFrame expression. BlockHighlightMSecs > 1 ifTrue: [expression litUp: true]. expression isSpecialForm ifTrue: [^ self evaluateSpecialForm]. "evaluate arguments, if necessary" stackFrame arguments size < expression argumentCount ifTrue: [^ self evaluateNextArgument]. expression isTimed ifTrue: [^ self applyTimedCommand]. self applyPrimitive. aStageMorph ifNotNil: [ aStageMorph updateTrailsForm. BlockHighlightMSecs = 1 ifTrue: [ "normal (non-turbo) mode; redraw after each cmd" World displayWorldSafely]]. ! ! !ScratchProcess methodsFor: 'private-evaluation' stamp: 'jm 1/1/2009 17:00'! evaluateFor: aStageMorph "Evaluates the current expression in the current environment for the given StageMorph." | expression | stackFrame shouldYield ifTrue: [^ self yield]. expression _ stackFrame expression. stackFrame shouldUnlight ifTrue: [ expression litUp: false. ^ self popStackFrame]. expression isCollection ifTrue: [^ self evaluateSequence]. expression isArgMorph ifTrue: [^ self evaluateSelfEvaluating]. expression isBlockMorph ifTrue: [^ self evaluateCommandFor: aStageMorph]. self error: 'Unknown expression type: ', expression printString. ! ! !ScratchProcess methodsFor: 'private-evaluation' stamp: 'DaveF 7/9/2003 18:58'! evaluateNextArgument "Evaluates the next argument of the command in expression. Requires that expression be a block that takes more args than are already evaluated in the current stack frame." | argumentExpression | argumentExpression _ stackFrame expression argumentAt: stackFrame arguments size + 1. self pushStackFrame: (ScratchStackFrame new expression: argumentExpression).! ! !ScratchProcess methodsFor: 'private-evaluation' stamp: 'jm 3/23/2005 09:53'! evaluateSelfEvaluating "The easiest case. Evaluate a number, color, or any self-evaluating expression." | value | value _ stackFrame expression evaluate. self returnValueToParentFrame: value. self popStackFrame. ! ! !ScratchProcess methodsFor: 'private-evaluation' stamp: 'DaveF 7/9/2003 10:45'! evaluateSequence "Requires that the current expression be a collection of blocks. Evaluates the block in the current expression (which is a list of blocks) at the given pc." | blocks pc | blocks _ stackFrame expression. pc _ stackFrame pc. pc > blocks size ifTrue: [self popStackFrame] ifFalse: [stackFrame pc: pc + 1. self pushStackFrame: (ScratchStackFrame new expression: (blocks at: pc))]. ! ! !ScratchProcess methodsFor: 'private-evaluation' stamp: 'DaveF 7/9/2003 11:20'! evaluateSpecialForm "Evaluates the current special form expression. Requires that no arguments have been evaluated, and that the current expression be a special form." self perform: stackFrame expression selector.! ! !ScratchProcess methodsFor: 'private-evaluation' stamp: 'DaveF 7/9/2003 18:37'! yield "Sets a flag indicating that we're ready to yield to another process." readyToYield _ true. self popStackFrame.! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'jm 3/24/2009 17:00'! doAsk "Ask the user a question and wait until the user types some text and hits the accept button." | block args question prompter | block _ stackFrame expression. args _ stackFrame arguments. "first time (args size = 0): open prompter" args size = 0 ifTrue: [ block receiver promptInProgress ifTrue: [ self pushStackFrame: (ScratchStackFrame new shouldYield: true). ^ self]. question _ block args first asString. prompter _ block receiver promptForInput: question] ifFalse: [ prompter _ args first]. "if all processes have stopped, pop this frame and we're done." prompter isDone ifTrue: [^ self popStackFrame]. "Set up the wait loop. We need to: 1. yield to another process 2. evaluate the termination expression again Push these onto stack in reverse order." "2. evaluate this block again" "(Remove the current frame and replace it with one that will force the termination expression to be re-evaluated.)" self popStackFrameNoUnhightlight. self pushStackFrame: (ScratchStackFrame new expression: block; addArgument: prompter). "1. yield to another process" self pushStackFrame: (ScratchStackFrame new shouldYield: true). ! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'jm 6/4/2008 16:53'! doBroadcastAndWait "Broadcast an event and wait until all processes started by that broadcast have terminated." | block args eventName stage procs | block _ stackFrame expression. args _ stackFrame arguments. "first time: send the event and collect a list of processes" args size = 0 ifTrue: [ eventName _ block args first asString. stage _ block receiver ownerThatIsA: ScratchStageMorph. procs _ stage broadcastEventNamed: eventName with: 0] ifFalse: [ procs _ args first]. "if all processes have stopped, pop this frame and we're done." (procs select: [:p | p isRunning]) size = 0 ifTrue: [^ self popStackFrame]. "Set up the wait loop. We need to: 1. yield to another process 2. evaluate the termination expression again Push these onto stack in reverse order." "2. evaluate this block again" "(Remove the current frame and replace it with one that will force the termination expression to be re-evaluated.)" self popStackFrameNoUnhightlight. self pushStackFrame: (ScratchStackFrame new expression: block; addArgument: procs). "1. yield to another process" self pushStackFrame: (ScratchStackFrame new shouldYield: true). ! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'jm 8/24/2003 12:12'! doForever "Executes one iteration of a forever loop." "Right now, the forever special form is at the top of the stack. We're going to add two more things on top of this: (1) a frame for evaluating the blocks in the forever block, and (2) a frame that just results in yielding control. That way we'll evaluate the contents of the block, give up control, and do it all again." | blocks | "Remember blocks to evaluate before we clobber the stack." blocks _ stackFrame expression firstBlockList. "When we're all done with these blocks, we want to yield." self pushStackFrame: (ScratchStackFrame new shouldYield: true). "First, we want to evaluate the blocks inside the loop." self pushStackFrame: (ScratchStackFrame new expression: blocks). ! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'jm 1/22/2007 13:14'! doForeverIf "Executes one iteration of a forever loop." | block args | block _ stackFrame expression. args _ stackFrame arguments. "evaluate the condition if we haven't yet" args size < 1 ifTrue: [^ self evaluateNextArgument]. "remove the current frame and replace it with one that will force the test expression to be re-evaluated." self popStackFrameNoUnhightlight. self pushStackFrame: (ScratchStackFrame new expression: block). self pushStackFrame: (ScratchStackFrame new shouldYield: true). "yield after possibly running body" "if the condition was true, run the body" args first ifTrue: [ self pushStackFrame: (ScratchStackFrame new expression: block firstBlockList)]. ! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'jm 8/24/2003 12:12'! doIf "Evaluate the current expression (which must be an if)." | block arguments argExp | block _ stackFrame expression. arguments _ stackFrame arguments. "Evaluate the arg if we haven't already." arguments size = 0 ifTrue: [argExp _ block argumentAt: 1. ^self pushStackFrame: (ScratchStackFrame new expression: argExp)]. "We can pop this expression off the stack either way." self popStackFrame. "If the predicate was false, just return." arguments first ifFalse: [^self]. "Since the predicate was true, evaluate the body of the if." self pushStackFrame: (ScratchStackFrame new expression: block firstBlockList).! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'jm 12/13/2005 20:37'! doIfElse "Evaluate the current expression (which must be an if-then-else)." | block arguments argExp | block _ stackFrame expression. arguments _ stackFrame arguments. "evaluate the arg if we haven't already." arguments size = 0 ifTrue: [ argExp _ block argumentAt: 1. ^ self pushStackFrame: (ScratchStackFrame new expression: argExp)]. "we can pop this expression off the stack either way" self popStackFrame. arguments first ifTrue: [self pushStackFrame: (ScratchStackFrame new expression: block trueBlockList)] ifFalse: [self pushStackFrame: (ScratchStackFrame new expression: block falseBlockList)]. ! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'jm 5/5/2007 17:04'! doPlaySoundAndWait "Play a sound and block until it has finished playing." | block args soundName snd | block _ stackFrame expression. args _ stackFrame arguments. "first time: send the event and collect a list of processes" args size = 0 ifTrue: [ soundName _ block args first. snd _ block receiver soundNamed: soundName ifAbsent: [^ self popStackFrame]. snd playFromStart] ifFalse: [ snd _ args first]. "if sound has stopped, pop this frame and we're done" snd isPlaying ifFalse: [^ self popStackFrame]. "Set up the wait loop. We need to: 1. yield to another process 2. evaluate our termination condition again Push these onto stack in reverse order." "2. evaluate this block again" "(Remove the current frame and replace it with one that will force the termination condition to be re-evaluated.)" self popStackFrameNoUnhightlight. self pushStackFrame: (ScratchStackFrame new expression: block; addArgument: snd). "1. yield to another process" self pushStackFrame: (ScratchStackFrame new shouldYield: true). ! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'jm 6/4/2008 16:46'! doRepeat "Handles one iteration of a repeat block." | arguments argExp block counter frame | block _ stackFrame expression. arguments _ stackFrame arguments. "If we haven't done so yet, evaluate the argument to repeat." arguments size < 1 ifTrue: [argExp _ block argumentAt: 1. ^self pushStackFrame: (ScratchStackFrame new expression: argExp)]. "If the number of times to repeat is 0, then we're done." counter _ arguments first asNumberNoError. counter <= 0 ifTrue: [^ self popStackFrame]. "Pop this instruction from the stack." self popStackFrameNoUnhightlight. "At top of stack should now be: 1. evaluate body of repeat block. 2. yield. 3. evaluate repeat block with decremented counter value. Need to add these to the stack in reverse order." "3. evaluate repeat block with decremented counter value." frame _ ScratchStackFrame new expression: block; addArgument: counter - 1. self pushStackFrame: frame. "2. yield." self pushStackFrame: (ScratchStackFrame new shouldYield: true). "1. evaluate body of repeat block." self pushStackFrame: (ScratchStackFrame new expression: block firstBlockList). ! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'jm 3/22/2005 11:17'! doReturn "Evaluates its argument, and returns the value to the frame from which the current method was called." | value args | args _ stackFrame arguments. "Evaluate the argument, if necessary." args size < stackFrame expression argumentCount ifTrue: [^self evaluateNextArgument]. "Remember the return value." args size > 0 ifTrue: [value _ args first] ifFalse: [value _ nil]. "Pop until we're out of frames to pop, or we hit a return marker." [stackFrame isNil] whileFalse: [self popStackFrame]. stackFrame ifNotNil: [ self returnValueToParentFrame: value. self popStackFrame]. ! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'DaveF 7/25/2003 11:15'! doUntil "Evaluates the current until-loop expression." ^self doUntil: true.! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'jm 1/22/2007 13:24'! doUntil: terminatingBoolean "Evaluates the current while or until loop expression until the loop condition evaluates to the boolean terminating value." | args block | block _ stackFrame expression. args _ stackFrame arguments. "Evaluate the condition if we haven't yet." args size < 1 ifTrue: [^self evaluateNextArgument]. "If the condition matches the terminatingBoolean, pop this frame and we're done." args first = terminatingBoolean ifTrue: [^self popStackFrame]. "Set up for an iteration of the while loop. We need to: 1. evaluate the body 2. yield to another process 3. evaluate the while/until block again Push these onto stack in reverse order." "3. evaluate the while/until block again" "(We remove the current frame and replace it with one that will force the argument to be re-evaluated.)" self popStackFrameNoUnhightlight. self pushStackFrame: (ScratchStackFrame new expression: block). "2. yield to another process" self pushStackFrame: (ScratchStackFrame new shouldYield: true). "1. evaluate the body" self pushStackFrame: (ScratchStackFrame new expression: block firstBlockList). ! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'jm 1/22/2007 13:09'! doWaitUntil "Evaluates the the wait condition until it evaluates to true." | block args | block _ stackFrame expression. args _ stackFrame arguments. "evaluate the termination expression if we haven't yet." args size < 1 ifTrue: [^ self evaluateNextArgument]. "if the termination condition is met, pop this frame and we're done." args first ifTrue: [^ self popStackFrame]. "Set up for waitUntil loop. We need to: 1. yield to another process 2. evaluate the termination expression again Push these onto stack in reverse order." "2. evaluate this block again" "(Remove the current frame and replace it with one that will force the termination expression to be re-evaluated.)" self popStackFrameNoUnhightlight. self pushStackFrame: (ScratchStackFrame new expression: block). "1. yield to another process" self pushStackFrame: (ScratchStackFrame new shouldYield: true). ! ! !ScratchProcess methodsFor: 'private-special forms' stamp: 'DaveF 7/25/2003 11:14'! doWhile "Evaluates the current while-loop expression." ^self doUntil: false.! ! !ScratchProcess methodsFor: 'private-stack' stamp: 'jm 8/7/2003 11:36'! popStackFrame "Pops the current stack frame off the top of the stack, so that the next one becomes the current frame." | command frame unlightFrame | frame _ stackFrame. stackFrame _ stackFrame parentFrame. command _ frame expression. BlockHighlightMSecs > 0 ifTrue: [ (frame shouldUnlight not and: [command isKindOf: BlockMorph]) ifTrue: [ "We push a new frame for unlighting this command." unlightFrame _ ScratchStackFrame new expression: command; shouldUnlight: true. self pushStackFrame: unlightFrame]]. ! ! !ScratchProcess methodsFor: 'private-stack' stamp: 'jm 1/22/2007 13:12'! popStackFrameNoUnhightlight "Pops the current stack frame off the top of the stack, so that the next one becomes the current frame. Do not add a stack frame to unhighlight this block. Use this method when a block will re-evaluated, such 'wait until'." stackFrame _ stackFrame parentFrame. ! ! !ScratchProcess methodsFor: 'private-stack' stamp: 'jm 3/23/2005 09:06'! pushStackFrame: aScratchStackFrame "Pushes the given new stack frame onto the top of the stack, so that it becomes the current stack frame." aScratchStackFrame parentFrame: stackFrame. stackFrame _ aScratchStackFrame. ! ! !ScratchProcess methodsFor: 'private-stack' stamp: 'jm 8/13/2003 17:40'! returnValueToParentFrame: anObject "Append the given object to the argument list of my parent frame." | f | stackFrame ifNil: [^ self]. f _ stackFrame parentFrame. [f notNil and: [f shouldUnlight]] whileTrue: [ f _ f parentFrame]. f ifNotNil: [f addArgument: anObject]. ! ! !ScratchProcess methodsFor: 'private-stack' stamp: 'DaveF 7/9/2003 18:26'! stackAsArray "Returns an array describing the contents of the stack. Useful for debugging." | frame collection | frame _ stackFrame. collection _ OrderedCollection new. [frame isNil] whileFalse: [collection add: frame expression. frame _ frame parentFrame]. ^collection asArray! ! !ScratchProcess class methodsFor: 'class initialization' stamp: 'jm 3/22/2005 11:00'! initialize BlockHighlightMSecs _ 1. ! ! !ScratchProcess class methodsFor: 'class variables' stamp: 'jm 8/10/2004 12:45'! blockHighlightMSecs ^ BlockHighlightMSecs ! ! !ScratchProcess class methodsFor: 'class variables' stamp: 'jm 8/8/2003 14:02'! blockHighlightMSecs: aNumber BlockHighlightMSecs _ aNumber. ! ! Supports Scratch text input from the user. ! !ScratchPrompterMorph methodsFor: 'initialize' stamp: 'ee 4/28/2009 20:29'! initialize "Set the forms for all my UI elements, create a row to hold my buttons, and a column to hold my shortcut buttons." super initialize. self initFromForm: (ScratchFrameMorph skinAt: #promptBubbleFrame). done _ false. typeinMorph _ StringFieldMorph new client: self; borderWidth: 2; color: (Color gray: 55); font: (ScratchFrameMorph getFont: #StringDialogTypeIn). okButton _ ToggleButton onForm: (ScratchFrameMorph skinAt: #promptCheckButtonPressed) offForm: (ScratchFrameMorph skinAt: #promptCheckButton). okButton target: self; actionSelector: #accept; actWhen: #buttonDown; toggleMode: false; setBalloonText: 'Close and continue' localized. ScratchTranslator isRTL ifTrue: [self addMorph: okButton. self addMorph: typeinMorph] ifFalse: [self addMorph: typeinMorph. self addMorph: okButton]. self extent: 450@37. ! ! !ScratchPrompterMorph methodsFor: 'accessing' stamp: 'ee 3/1/2009 12:34'! question: aString questionMorph _ StringMorph new contents: aString; font: (ScratchFrameMorph getFont: #DialogBoxButton). self addMorph: questionMorph. self height: questionMorph height + typeinMorph height + 16. ! ! !ScratchPrompterMorph methodsFor: 'accessing' stamp: 'jm 2/25/2009 09:47'! sprite: aSprite sprite _ aSprite. ! ! !ScratchPrompterMorph methodsFor: 'geometry' stamp: 'jm 2/10/2009 20:04'! extent: aPoint super extent: aPoint. self fixLayout. ! ! !ScratchPrompterMorph methodsFor: 'geometry' stamp: 'ee 4/28/2009 20:40'! fixLayout typeinMorph ifNil: [^ self]. typeinMorph width: self width - 36; bottom: self bottom - 6. ScratchTranslator isRTL ifTrue:[typeinMorph right: self right - 8] ifFalse:[typeinMorph left: self left + 8]. questionMorph ifNotNil: [ ScratchTranslator isRTL ifTrue: [questionMorph position: self topRight - ((questionMorph width + 8)@0) + (0@5)] ifFalse: [questionMorph position: self position + (8@5)]]. ScratchTranslator isRTL ifTrue:[okButton position: typeinMorph topLeft - ((okButton width + 2)@0)] ifFalse:[okButton position: typeinMorph topRight + (2@0)]. ! ! !ScratchPrompterMorph methodsFor: 'other' stamp: 'jm 2/25/2009 09:50'! accept "Accept button was pressed." LastAnswer _ typeinMorph contents. done _ true. self delete. sprite ifNotNil: [sprite sayNothing]. World doOneCycle. "erase myself from the screen" ! ! !ScratchPrompterMorph methodsFor: 'other' stamp: 'jm 3/2/2009 15:38'! dispatchKeyStroke: evt typeinMorph keyStroke: evt. ! ! !ScratchPrompterMorph methodsFor: 'other' stamp: 'jm 2/10/2009 19:53'! enterKeyPressed "Respond to the enter key being pressed in one of my input fields." self accept. ! ! !ScratchPrompterMorph methodsFor: 'other' stamp: 'jm 2/10/2009 20:51'! grabKeyboardFocus World activeHand newKeyboardFocus: typeinMorph. ! ! !ScratchPrompterMorph methodsFor: 'other' stamp: 'jm 2/10/2009 20:39'! isDone ^ done ! ! !ScratchPrompterMorph methodsFor: 'other' stamp: 'jm 2/26/2009 21:34'! stopAsk "Stop button was pressed." done _ true. self delete. sprite ifNotNil: [sprite sayNothing]. World doOneCycle. "erase myself from the screen" ! ! !ScratchPrompterMorph class methodsFor: 'initialization' stamp: 'jm 2/11/2009 09:45'! initialize LastAnswer _ ''. ! ! !ScratchPrompterMorph class methodsFor: 'accessing' stamp: 'jm 2/27/2009 18:13'! clearLastAnswer LastAnswer _ ''. ! ! !ScratchPrompterMorph class methodsFor: 'accessing' stamp: 'jm 2/11/2009 09:46'! lastAnswer ^ LastAnswer ! ! !ScratchRecordMeter methodsFor: 'initialization' stamp: 'ee 9/17/2005 22:47'! initialize super initialize. self extent: 283@22; color: Color transparent. lineForm _ Form extent: 1@20 depth: 32. lineForm fillColor: Color blue. level _ 0.! ! !ScratchRecordMeter methodsFor: 'accessing' stamp: 'ee 6/20/2005 14:42'! setLevel: aNumber level _ aNumber. self changed.! ! !ScratchRecordMeter methodsFor: 'drawing' stamp: 'ee 9/17/2005 22:48'! drawOn: aCanvas | clipC p width | super drawOn: aCanvas. p _ self topLeft + (1@1). clipC _ aCanvas copyClipRect: self bounds. (level = 0) ifTrue: [ lineForm fillColor: Color green. clipC paintImage: lineForm at: p. ^ self]. [(p x - self topLeft x) <= level] whileTrue: [ width _ p x - self topLeft x. (width > 0) ifTrue: [lineForm fillColor: Color green]. (width > 150) ifTrue: [lineForm fillColor: Color yellow]. (width > 230) ifTrue: [lineForm fillColor: Color red]. clipC paintImage: lineForm at: p. p _ (p x + (lineForm width * 3)) @ p y].! ! !ScratchReporterToolTipMorph methodsFor: 'accessing' stamp: 'ee 3/24/2009 18:30'! message: aString | verts | super message: aString. ScratchTranslator isRTL ifFalse: [verts _ (Array with: self bottomLeft + (2@6) with: self bottomLeft + (5@0) with: self bottomLeft + (10@0))] ifTrue: [verts _ (Array with: self bottomRight - (2@0) + (0@6) with: self bottomRight - (5@0) with: self bottomRight - (10@0))]. self addMorph: (PolygonMorph vertices: verts color: (Color r: 0.9 g: 0.9 b: 0.9) borderWidth: 0 borderColor: Color yellow). ! ! !ScratchReporterToolTipMorph methodsFor: 'accessing' stamp: 'jm 5/7/2009 11:40'! messageFont ^ ScratchFrameMorph getFont: #ReporterToolTip ! ! !ScratchReporterToolTipMorph class methodsFor: 'instance creation' stamp: 'ee 3/24/2009 18:21'! string: str for: morph ^ self new message: str; target: morph; color: (Color r: 0.9 g: 0.9 b: 0.9); useRoundedCorners; borderWidth: 0. ! ! !ScratchResizeMorph methodsFor: 'initialization' stamp: 'ee 5/14/2008 17:49'! initialize super initialize. self color: Color transparent; orientation: #vertical; centering: #center; hResizing: #rigid; vResizing: #rigid; inset: 0; borderWidth: 0. self addMorph: ImageMorph new. ! ! !ScratchResizeMorph methodsFor: 'event handling' stamp: 'ee 5/14/2008 17:04'! handlesMouseOver: evt ^ true ! ! !ScratchResizeMorph methodsFor: 'event handling' stamp: 'ee 5/14/2008 17:31'! mouseEnter: evt (type = #edge) ifTrue: [World activeHand showTemporaryCursor: Cursor resizeHorizontally] ifFalse: [World activeHand showTemporaryCursor: Cursor resizeCorner]. ! ! !ScratchResizeMorph methodsFor: 'event handling' stamp: 'ee 5/14/2008 17:04'! mouseLeave: evt World activeHand showTemporaryCursor: nil. ! ! !ScratchResizeMorph methodsFor: 'event handling' stamp: 'ee 5/14/2008 17:42'! type: aSymbol type _ aSymbol. (type = #edge) ifTrue: [ self addMorphBack: (AlignmentMorph newSpacer: Color transparent). self addMorphFront: (AlignmentMorph newSpacer: Color transparent)]. ! ! !ScratchResizeMorph methodsFor: 'accessing' stamp: 'ee 5/14/2008 17:35'! form: aForm (self submorphs at: 1) form: aForm. self extent: (self submorphs at: 1) extent. ! ! !ScratchResizeMorph class methodsFor: 'instance creation' stamp: 'ee 5/14/2008 17:48'! ofType: aSymbol "#edge #corner" (aSymbol = #edge) ifTrue: [^ self new form: (ScratchFrameMorph skinAt: #resizeIconEdge); type: aSymbol] ifFalse: [^ self new form: (ScratchFrameMorph skinAt: #resizeIconCorner); type: aSymbol]. ! ! !ScratchRulerMorph methodsFor: 'initialize' stamp: 'LY 7/23/2003 10:30'! initialize super initialize. min _ 0.0. max _ 100. marks _ 20.0. sigDigs _ 1. graph _ nil. "used specifically to coord with ScratchGraphMorph" self borderWidth: 1. self extent: 200@20. self color: Color white.! ! !ScratchRulerMorph methodsFor: 'accessing' stamp: 'LY 7/23/2003 10:31'! graph: aScratchGraphMorph graph _ aScratchGraphMorph.! ! !ScratchRulerMorph methodsFor: 'accessing' stamp: 'LY 7/23/2003 10:21'! marks: aNumber marks _ aNumber. self changed.! ! !ScratchRulerMorph methodsFor: 'accessing' stamp: 'LY 7/23/2003 10:19'! min: aMin max: aMax min _ aMin. max _ aMax. self changed.! ! !ScratchRulerMorph methodsFor: 'accessing' stamp: 'LY 7/23/2003 10:21'! sigDigs: aNumber sigDigs _ aNumber truncated. self changed.! ! !ScratchRulerMorph methodsFor: 'drawing'! drawOn: aCanvas | space bigSpace currVal s ss | "the amt of space btw each bar. big Space = the amt of space btw each big bar" space _ (self extent x/marks) truncated max: 1. bigSpace _ 5* space asFloat. graph ifNotNil: [ss _ graph editor origSamplingRate] ifNil: [ss _ 1]. currVal _ (min/ss) asFloat roundTo: 0.01. "the value of where we are in teh rule" aCanvas fillRectangle: (Rectangle origin: (self left)@(self top) corner: (self right)@(self bottom)) color: Color lightGray. self removeAllMorphs. (self left) to: (self right) by: space do: [:pos | (pos - (self left)) \\ bigSpace = 0 ifTrue: [aCanvas line: (pos truncated)@((self top) truncated) to: (pos truncated)@((self top + 5) truncated) color: Color black. s _ StringMorph contents: (currVal asString). s center: (pos truncated)@(self top + 12). self addMorph: s.] ifFalse: [aCanvas line: (pos truncated)@(self top truncated) to: (pos truncated)@((self top + 1) truncated) color: Color black.]. currVal _ currVal + ((max-min)/(marks*ss)) roundTo: 0.01. ]. ! ! !ScratchRulerMorph methodsFor: 'stepping' stamp: 'LY 7/31/2003 11:48'! step | graphStart graphEnd | graph ifNil: [^self]. self extent x = graph extent x ifFalse: [self extent: (graph extent x)@(self extent y).]. graphStart _ graph startIndex min: graph data size. graphEnd _ graphStart + (graph bounds width/graph scale) min: graph data size. (min = graphStart and:[ max = graphEnd]) ifFalse: [ min _ graphStart. max _ graphEnd truncated. self changed.].! ! !ScratchRulerMorph class methodsFor: 'instance creation' stamp: 'LY 7/23/2003 10:46'! graphMorph: aScratchGraphMorph ^ self new graph: aScratchGraphMorph; extent: (aScratchGraphMorph extent x)@20. ! ! I am a viewer for the scripts and media closet of a Scratch object. I have a heading row containing the object name (editable) and a drop-down menu icon. Below that is a scrollable content area. ! !ScratchScriptEditorMorph methodsFor: 'initialization' stamp: 'jm 7/4/2008 12:31'! addNameBox nameMorph _ UpdatingStringFieldMorph new font: (ScratchFrameMorph getFont: #UpdatingStringField); rightJustify: ScratchTranslator isRTL; acceptWhenFocusLost: true; position: thumbnailMorph topRight + (17@(thumbnailMorph height * 0.12)). self addMorphBack: nameMorph. ! ! !ScratchScriptEditorMorph methodsFor: 'initialization' stamp: 'ee 12/5/2008 22:59'! createTabPane | tabOnForm tabOffForm tabID tabLabel | "create tab pane" tabPaneMorph _ ScratchTabPaneMorph new. tabPaneMorph borderWidth: 0; color: Color transparent; targetPane: self. tabOnForm _ (ScratchFrameMorph skinAt: #tabOn). tabOffForm _ (ScratchFrameMorph skinAt: #tabOff). "add the tabs" #(Scripts Costumes Sounds) do: [:spec | tabID _ spec asString. tabLabel _ tabID localized. tabPaneMorph createTab: tabID withLabel: tabLabel onForm: tabOnForm offForm: tabOffForm]. "set current tab and add to frame" tabPaneMorph currentTab: 'Scripts'. self addMorph: tabPaneMorph. ! ! !ScratchScriptEditorMorph methodsFor: 'initialization' stamp: 'ee 11/4/2008 12:24'! initialize super initialize. self initFrontFromForm: (ScratchFrameMorph skinAt: #scriptPaneFrameTransparent2) topSectionHeight: 90. self color: (Color r: (149/255) g: (154/255) b: (159/255)). thumbnailMorph _ ScratchThumbnailMorph new. self addMorph: (thumbnailMorph position: self position + (37@16)). self addNameBox. pageViewerMorph _ ScrollFrameMorph2 new growthFraction: 0.1; color: ScratchFrameMorph scriptsPaneColor. self addMorph: (pageViewerMorph position: (self left @ (self top + topSectionHeight))). rotationButtons _ #(). readoutMorphs _ #(). self target: nil. thumbnailMorph extent: 50@50. self extent: 300@400. self createTabPane. ! ! !ScratchScriptEditorMorph methodsFor: 'accessing' stamp: 'jm 4/9/2009 10:09'! bareMinimumWidth "Answer the bare minimum width for this pane to be useable." lockButton ifNil: [^ 100]. ^ lockButton right - self left! ! !ScratchScriptEditorMorph methodsFor: 'accessing' stamp: 'jm 2/28/2005 17:07'! categoryChanged: aString "If the given category is my current category, update my contents. Otherwise, do nothing." self target ifNil: [^ self]. currentCategory = aString ifTrue: [self currentCategory: aString]. ! ! !ScratchScriptEditorMorph methodsFor: 'accessing' stamp: 'jm 2/28/2005 14:29'! currentCategory ^ currentCategory ! ! !ScratchScriptEditorMorph methodsFor: 'accessing' stamp: 'jm 4/23/2008 12:43'! currentCategory: aString | xOffset | currentCategory _ aString. self target ifNil: [^ self]. xOffset _ 0. World activeHand newKeyboardFocus: nil. currentCategory = 'Scripts' ifTrue: [ pageViewerMorph contents: self target blocksBin]. currentCategory = 'Costumes' ifTrue: [ pageViewerMorph contents: (self target costumesPage: xOffset)]. currentCategory = 'Sounds' ifTrue: [ pageViewerMorph contents: (self target soundsPage: xOffset)]. pageViewerMorph contents color: ScratchFrameMorph scriptsPaneColor. self world ifNotNil: [self world startSteppingSubmorphsOf: pageViewerMorph contents]. ! ! !ScratchScriptEditorMorph methodsFor: 'accessing' stamp: 'ee 8/11/2008 13:48'! getTabTop "Used by ScratchFrameMorph when creating the tab pane. This returns the y position of where the top of the tabs should align. This situation is unfortunate (tabs should probaby be part of ScratchScriptEditorMorph), but we'll fix it later." ScratchFrameMorph isXO ifTrue: [^ thumbnailMorph bottom + 20]. (readoutMorphs size > 0) ifTrue: [^ (readoutMorphs at: 1) bottom + 6] ifFalse: [^ nameMorph bottom + 5 + (ScratchTranslator stringExtent: '0' font: (ScratchFrameMorph getFont: #XYReadoutBold)) y + 6]. ! ! !ScratchScriptEditorMorph methodsFor: 'accessing' stamp: 'ee 11/4/2008 11:44'! tabPane ^ tabPaneMorph! ! !ScratchScriptEditorMorph methodsFor: 'accessing' stamp: 'ee 7/1/2003 12:58'! target ^ nameMorph target ! ! !ScratchScriptEditorMorph methodsFor: 'accessing' stamp: 'jm 7/2/2008 14:55'! target: aScratchObjectOrNil "Start viewing the given object or no object." | sFrame nameSel | World activeHand newKeyboardFocus: nil. (aScratchObjectOrNil isNil or: [aScratchObjectOrNil isScriptable not]) ifTrue: [ thumbnailMorph target: nil. nameMorph target: nil; contents: 'no object '. pageViewerMorph contents: (Morph new color: Color red). (sFrame _ self ownerThatIsA: ScratchFrameMorph) ifNotNil: [ sFrame viewerPane target: nil]. self showOrHideReadouts. ^ self]. thumbnailMorph target: aScratchObjectOrNil. nameSel _ (aScratchObjectOrNil isKindOf: ScratchStageMorph) ifTrue: [nil] ifFalse: [#objName:]. nameMorph target: aScratchObjectOrNil; getSelector: #objName; putSelector: nameSel. self showOrHideReadouts. self fixLayout. ! ! !ScratchScriptEditorMorph methodsFor: 'drawing' stamp: 'ee 1/29/2009 10:12'! drawBackgroundOn: aCanvas "Draw my background." color isTransparent ifTrue: [^ self]. aCanvas fillRectangle: (self topLeft corner: pageViewerMorph topRight) color: color. ! ! !ScratchScriptEditorMorph methodsFor: 'drawing' stamp: 'ee 1/29/2009 10:35'! drawSubmorphsOn: aCanvas "Display submorphs back to front." submorphs reverseDo: [:m | (m = tabPaneMorph) ifFalse: [aCanvas fullDrawMorph: m]]. ! ! !ScratchScriptEditorMorph methodsFor: 'drawing' stamp: 'ee 1/29/2009 10:35'! fullDrawOn: aCanvas "Draw my frame in front of my submorphs." | clipC | self isHidden ifTrue: [^ self]. (self hasProperty: #errorOnDraw) ifTrue:[^ self drawErrorOn: aCanvas]. (aCanvas isVisible: self fullBounds) ifFalse: [^ self]. "myBox has integer position and extent and has a potentially inset bottom" myBox _ bounds truncated. clipC _ aCanvas copyClipRect: myBox. frameInFront ifTrue: [ self drawOn: clipC. self drawSubmorphsOn: clipC. self drawFrameOn: clipC. aCanvas fullDrawMorph: tabPaneMorph] ifFalse: [ self drawOn: clipC. self drawSubmorphsOn: clipC]. ! ! !ScratchScriptEditorMorph methodsFor: 'event handling' stamp: 'ee 2/3/2009 13:28'! handlesMouseOverDragging: evt | m | evt hand submorphs size = 1 ifFalse: [^ false]. m _ evt hand firstSubmorph. ^ m isKindOf: BlockMorph. ! ! !ScratchScriptEditorMorph methodsFor: 'event handling' stamp: 'ee 2/8/2009 17:02'! mouseEnterDragging: evt "Switch the tabs to script if a block is current being dragged" (currentCategory = 'Scripts') ifFalse:[ self currentCategory: 'Scripts'. tabPaneMorph currentTab: 'Scripts']. ! ! !ScratchScriptEditorMorph methodsFor: 'geometry' stamp: 'jm 10/28/2008 13:13'! extent: aPoint super extent: aPoint. pageViewerMorph ifNotNil: [ pageViewerMorph extent: self extent - (pageViewerMorph position - self position)]. ! ! !ScratchScriptEditorMorph methodsFor: 'geometry' stamp: 'ee 2/5/2009 16:25'! fixLayout | y x | "layout readout morphs vertically" y _ nameMorph bottom + 5. readoutMorphs do: [:m | m position: m left@y]. "layout readout and name morphs horizontally" nameMorph position: (thumbnailMorph topRight + (17@(thumbnailMorph height * 0.12))). x _ nameMorph left. readoutMorphs do: [:m | m position: x@m top. x _ m right + 5]. "layout lock and pen morphs" lockButton ifNotNil: [ lockButton position: (nameMorph right + 4)@(nameMorph top + ((nameMorph height - lockButton height) / 2)). penReadout position: (lockButton right + 4)@(nameMorph top + ((nameMorph height - penReadout height) / 2))]. "place tab morph" (readoutMorphs size > 1) ifTrue: [ topSectionHeight _ (readoutMorphs at: 1) bottom - self top + tabPaneMorph height + 5]. tabPaneMorph width: self width; position: (self left + 15) @ (self top + topSectionHeight - tabPaneMorph height + 1). "place scripts scroll pane" pageViewerMorph position: (self left @ (self top + topSectionHeight)). self extent: self extent. "force resize of page viewer morph" ! ! !ScratchScriptEditorMorph methodsFor: 'stepping' stamp: 'ee 4/14/2008 15:01'! step currentCategory = 'Costumes' ifTrue: [self updateCostumeSelection]. (penReadout isNil or: [penReadout owner ~= self]) ifTrue: [^ self]. self target penDown ifTrue: [penReadout color: self target penColor] ifFalse: [penReadout color: Color transparent]. ! ! !ScratchScriptEditorMorph methodsFor: 'stepping' stamp: 'jm 1/5/2006 11:11'! stepTime ^ 50 ! ! !ScratchScriptEditorMorph methodsFor: 'stepping' stamp: 'ee 4/14/2008 15:01'! updateCostumeSelection "Update the currently selected costume if the costumes tab is selected." | currentCostume | currentCategory = 'Costumes' ifFalse: [^ self]. currentCostume _ self target costume. pageViewerMorph contents submorphsDo: [:m | ((m isKindOf: MediaItemMorph) and: [m media isImage]) ifTrue: [ m highlight: (m media = currentCostume)]]. ! ! !ScratchScriptEditorMorph methodsFor: 'menu/button ops' stamp: 'ee 5/14/2008 16:40'! addComment: aPosition | c scriptsMorph | scriptsMorph _ (pageViewerMorph allMorphs select: [: m | m isKindOf: ScratchScriptsMorph]) first. scriptsMorph addMorph: (c _ ScratchCommentMorph new position: aPosition). World activeHand newKeyboardFocus: c commentMorph. ! ! !ScratchScriptEditorMorph methodsFor: 'menu/button ops' stamp: 'jm 4/27/2007 16:12'! animateRotationStyle | style thumbForm wasFlipped currentRotation pen center rotatedForm doFlip | style _ self target rotationStyle. thumbnailMorph updateThumbnail. thumbForm _ thumbnailMorph form deepCopy. currentRotation _ self target rotationDegrees rounded. wasFlipped _ ((currentRotation \\ 360) >= 90) & ((currentRotation \\ 360) <= 270). thumbnailMorph showDirection: false. pen _ (Pen newOnForm: thumbnailMorph form) color: Color white. center _ thumbnailMorph form center. currentRotation to: currentRotation + 360 by: 12 do: [:i | rotatedForm _ thumbForm. "no rotation by default" style = #normal ifTrue: [rotatedForm _ thumbForm rotateBy: i]. style = #leftRight ifTrue: [ doFlip _ ((i \\ 360) >= 90) & ((i \\ 360) <= 270). wasFlipped ifTrue: [doFlip _ doFlip not]. doFlip ifTrue: [rotatedForm _ thumbForm flipBy: #horizontal centerAt: 0@0]]. thumbnailMorph form fill: thumbnailMorph form boundingBox fillColor: Color transparent. rotatedForm displayOn: thumbnailMorph form at: (thumbnailMorph extent - rotatedForm extent) // 2 rule: Form paint. pen place: center. pen goto: center + (Point r: 22 degrees: i). thumbnailMorph changed. World displayWorldSafely. Delay waitMSecs: 20]. thumbnailMorph showDirection: true. thumbnailMorph updateThumbnail. ! ! !ScratchScriptEditorMorph methodsFor: 'menu/button ops' stamp: 'ee 11/5/2007 11:50'! cleanUp "Align all scripts vertically in alphabetical order" | scriptsMorph | scriptsMorph _ (pageViewerMorph allMorphs select: [:c | c isKindOf: ScratchScriptsMorph]) first. scriptsMorph cleanUp. pageViewerMorph updateContentsExtent; updateScrollbars. ! ! !ScratchScriptEditorMorph methodsFor: 'menu/button ops' stamp: 'ee 2/12/2009 16:23'! deleteSprite "Ask the user if they want to delete the currently selected sprite" | response | response _ DialogBoxMorph askWithCancel: 'Delete this sprite?' localized. response = #cancelled ifTrue: [^ self]. response ifTrue: [thumbnailMorph target undoableDeleteSprite]. ! ! !ScratchScriptEditorMorph methodsFor: 'menu/button ops' stamp: 'jens 3/9/2009 13:19'! saveScriptsToImage "Take a snapshot of all scripts for a sprite and save as a GIF file" | fName saveForm | saveForm _ pageViewerMorph contents screenshot. fName _ ScratchFileChooserDialog chooseNewFileDefault: '' title: 'Save Scripts Snapshot' type: #scriptsSnapshot. fName = #cancelled ifTrue: [^ self]. fName size = 0 ifTrue: [^ self]. (fName asLowercase endsWith: '.gif') ifFalse: [fName _ fName, '.gif']. saveForm writeGIFFileNamed: fName. ! ! !ScratchScriptEditorMorph methodsFor: 'menu/button ops' stamp: 'jm 3/2/2009 13:00'! scriptsMenu: aPosition "Present a menu of Scratch script operations." | menu choice | self target ifNil: [^ self]. menu _ CustomMenu new. menu add: 'clean up' action: #cleanUp. menu add: 'save picture of scripts' action: #saveScriptsToImage. menu add: 'add comment' action: #addComment:. choice _ menu localize startUp. choice ifNil: [^ self]. choice = #addComment: ifTrue: [self perform: choice with: aPosition] ifFalse: [self perform: choice]. ! ! !ScratchScriptEditorMorph methodsFor: 'menu/button ops' stamp: 'jm 4/27/2007 15:52'! setRotationStyle: aSymbol aSymbol == #Smooth ifTrue: [self target rotationStyle: #normal]. aSymbol == #Flip ifTrue: [self target rotationStyle: #leftRight]. aSymbol == #None ifTrue: [self target rotationStyle: #none]. self updateRotationButtonHighlight. (self target respondsTo: #rotationDegrees:) ifFalse: [^ self]. self animateRotationStyle. ! ! !ScratchScriptEditorMorph methodsFor: 'menu/button ops' stamp: 'jm 10/19/2007 11:35'! toggleSpriteDraggable "Add buttons to set the rotation style." self target draggable: self target draggable not. self updateLockButton. ! ! !ScratchScriptEditorMorph methodsFor: 'private' stamp: 'jm 2/22/2009 21:37'! addDeleteButton "Add button to delete sprite." self deleteDeleteButton. deleteButton _ ToggleButton onForm: (ScratchFrameMorph skinAt: #deleteSprite) offForm: (ScratchFrameMorph skinAt: #deleteSprite) overForm: (ScratchFrameMorph skinAt: #deleteSprite). deleteButton target: self; actionSelector: #deleteSprite; setBalloonText: 'Delete this sprite' localized; actWhen: #buttonUp; isMomentary: true; position: (lockButton right + 27)@(nameMorph top + ((nameMorph height - deleteButton height) / 2)). self addMorph: deleteButton. ! ! !ScratchScriptEditorMorph methodsFor: 'private' stamp: 'ee 2/11/2009 14:13'! addLockButton "Add button to set sprite locked status." self deleteLockButton. lockButton _ ToggleButton onForm: (ScratchFrameMorph skinAt: #locked) offForm: (ScratchFrameMorph skinAt: #unlocked). lockButton target: self; actionSelector: #toggleSpriteDraggable; setBalloonText: 'draggable on website?' localized; actWhen: #buttonUp; isMomentary: true; position: (nameMorph right + 4)@(nameMorph top + ((nameMorph height - lockButton height) / 2)). self addMorph: lockButton. self updateLockButton. ! ! !ScratchScriptEditorMorph methodsFor: 'private' stamp: 'ee 2/20/2009 13:14'! addReadouts "Add readouts for my target's position and direction." | x y label readout s | self deleteReadouts. readoutMorphs _ OrderedCollection new. x _ nameMorph left. y _ nameMorph bottom + 5. #(('x' xpos) ('y' ypos)('direction' heading)) do: [:spec | (ScratchTranslator isRTL and: [(spec at: 1) = 'x' or: [(spec at: 1) = 'y']]) ifTrue: [s _ (':', spec first) asUTF8] ifFalse: [s _ (spec first localized, ScratchTranslator colonSuffix)]. label _ StringMorph new contents: s; font: (ScratchFrameMorph getFont: #XYReadout); position: x@y. readout _ (UpdatingStringMorph on: self target selector: spec second) font: (ScratchFrameMorph getFont: #XYReadoutBold); forceUnicodeRendering: true; color: (Color gray: 0.2); contents: '-000'; "this sets the readout size" growable: false; stepTime: 100; position: (label right + 4)@y. ScratchTranslator isRTL ifTrue:[ readout rightJustify: true]. self addMorph: label; addMorph: readout. readoutMorphs add: label; add: readout. readout startStepping. x _ readout right + 2]. ScratchTranslator isRTL ifTrue: [ readoutMorphs reversed do: [: m | readoutMorphs remove: m. readoutMorphs add: m]]. penReadout _ Morph new extent: 15@5. penReadout position: (lockButton right + 4)@(nameMorph top + ((nameMorph height - penReadout height) / 2)); color: Color transparent. self addMorph: penReadout. readoutMorphs add: penReadout. penReadout startStepping. readoutMorphs _ readoutMorphs asArray. ! ! !ScratchScriptEditorMorph methodsFor: 'private' stamp: 'jm 3/21/2008 14:02'! addRotationButtons "Add buttons to set the rotation style." | specs x y style button | self deleteRotationButtons. (self target respondsTo: #rotationStyle:) ifFalse: [^ self]. specs _ #( (Smooth 'can rotate') (Flip 'only face left-right') (None 'don''t rotate')). x _ self left + 13. y _ self top + 18. specs do: [:pair | style _ pair first. button _ ToggleButton onForm: (ScratchFrameMorph skinAt: ('rotStyle', style, 'On')) offForm: (ScratchFrameMorph skinAt: ('rotStyle', style)) overForm: (ScratchFrameMorph skinAt: ('rotStyle', style, 'Over')). button target: self; arguments: (Array with: style); actionSelector: #setRotationStyle:; setBalloonText: pair second localized; actWhen: #buttonDown; position: x@y. self addMorph: button. rotationButtons _ rotationButtons copyWith: button. y _ y + button height + 2]. self updateRotationButtonHighlight. ! ! !ScratchScriptEditorMorph methodsFor: 'private' stamp: 'ee 2/12/2009 10:49'! deleteDeleteButton "Delete my delete button." deleteButton ifNotNil: [ deleteButton delete. deleteButton _ nil]. ! ! !ScratchScriptEditorMorph methodsFor: 'private' stamp: 'jm 10/3/2007 16:42'! deleteLockButton "Delete my lock button." lockButton ifNotNil: [ lockButton delete. lockButton _ nil]. ! ! !ScratchScriptEditorMorph methodsFor: 'private' stamp: 'jm 2/28/2005 17:46'! deleteReadouts "Delete the position/rotation readouts." readoutMorphs do: [:m | m delete]. readoutMorphs _ #(). ! ! !ScratchScriptEditorMorph methodsFor: 'private' stamp: 'jm 2/28/2005 17:40'! deleteRotationButtons "Delete the rotation style buttons." rotationButtons do: [:m | m delete]. rotationButtons _ #(). ! ! !ScratchScriptEditorMorph methodsFor: 'private' stamp: 'jm 7/3/2008 18:22'! showOrHideReadouts "If this is a sprite, show the position and direction readouts and the rotation style buttons. Otherwise, hide them." self deleteRotationButtons; deleteLockButton; deleteReadouts. nameMorph font: nameMorph font; width: nameMorph height * 4; rightJustify: ScratchTranslator isRTL. (self target isKindOf: ScratchSpriteMorph) ifTrue: [ self addRotationButtons; addLockButton; addReadouts. World ifNotNil: [World startSteppingSubmorphsOf: self]]. ! ! !ScratchScriptEditorMorph methodsFor: 'private' stamp: 'jm 10/23/2007 15:24'! updateLockButton lockButton ifNil: [^ self]. self target draggable ifTrue: [lockButton off] ifFalse: [lockButton on]. ! ! !ScratchScriptEditorMorph methodsFor: 'private' stamp: 'jm 2/28/2005 17:33'! updateRotationButtonHighlight "Highlight the appropriate rotation style button. Do nothing if my target is not a sprite." | style sym | (self target isKindOf: ScratchSpriteMorph) ifFalse: [^ self]. style _ self target rotationStyle. style = #normal ifTrue: [sym _ #Smooth]. style = #leftRight ifTrue: [sym _ #Flip]. style = #none ifTrue: [sym _ #None]. rotationButtons do: [:m | sym = m arguments first ifTrue: [m on] ifFalse: [m off]]. ! ! I hold the scripts and partially assembled scripts for a ScriptableScratchMorph. ! !ScratchScriptsMorph methodsFor: 'initialization' stamp: 'ee 11/4/2008 11:49'! initialize super initialize. color _ Color white. borderWidth _ 0. self enableDragNDrop: true. ! ! !ScratchScriptsMorph methodsFor: 'stepping' stamp: 'jens 3/3/2009 21:46'! showCommentDropFeedback | hand comment target r | hand _ World activeHand. comment _ hand firstSubmorph. target _ comment attachTargetIn: self. target ifNil: [^ self]. r _ target bounds. ((target isKindOf: CBlockMorph) and: [target firstBlockList size > 0]) ifTrue: [r _ r merge: target firstBlockList first fullBounds]. (target isKindOf: IfElseBlockMorph) ifTrue: [ (target trueBlockList size > 0) ifTrue: [r _ r merge: target trueBlockList first fullBounds]. (target falseBlockList size > 0) ifTrue: [r _ r merge: target falseBlockList first fullBounds]]. feedbackMorph bounds: (r expandBy: 3); color: Color transparent; borderColor: (comment topBarColor). self addMorphFront: feedbackMorph. ! ! !ScratchScriptsMorph methodsFor: 'stepping' stamp: 'jens 3/3/2009 21:42'! step "Give feedback about possible drop targets." | feedbackColor h b targetArg targetAssoc targetP targetBlock | feedbackMorph ifNil: [feedbackMorph _ BorderedMorph new borderWidth: 3] "create feedback morph if necessary" ifNotNil: [feedbackMorph delete]. "remove old feedback" feedbackColor _ Color white. feedbackMorph useSquareCorners. h _ World activeHand. h toolType = 'CutTool' ifTrue: [^ self showDeleteFeedback]. (self bounds containsPoint: h position) ifFalse: [^ self]. h submorphCount = 1 ifFalse: [^ self]. b _ h firstSubmorph. (b isKindOf: ScratchCommentMorph) ifTrue: [^ self showCommentDropFeedback]. (b isKindOf: BlockMorph) ifFalse: [^ self]. "attempt at auto-scrolling (has some issues, commented out for now): ((self owner bounds containsPoint: h position) and: [(h position x - self owner left) < 50 or: [ (self owner right - h position x) < 50 or: [ (self owner bottom - h position y) < 50 or: [ (h position y - self owner top) < 50]]]]) ifTrue:[self owner scrollMorphIntoView: h firstSubmorph]. xxxxxxxx" b isReporter ifTrue: [ "reporter block" (targetArg _ self topArgMorphAt: b bounds exclude: nil) ifNil: [^ self]. (targetArg acceptsTypeOf: b) ifFalse: [^ self]. feedbackMorph bounds: (targetArg bounds expandBy: 5); color: (feedbackColor alpha: 0.4); borderColor: feedbackColor; useRoundedCorners. ^ self addMorphFront: feedbackMorph]. "non-reporter (i.e. command block or hat block)" targetAssoc _ b closestAttachTargetIn: self. targetAssoc ifNil: [ (b bottomBlock isKindOf: CBlockMorph) ifFalse: [ targetAssoc _ b bottomBlock closestAttachTargetIn: self. targetAssoc ifNotNil:[ (targetAssoc value owner isKindOf: BlockMorph) ifTrue:[ targetAssoc _ nil]]]]. targetAssoc ifNil: [^ self]. targetP _ targetAssoc key. targetBlock _ targetAssoc value. feedbackMorph borderColor: feedbackColor; color: feedbackColor. "subtract the attachment point x from the width so that the feedback in CBlock won't stick out" ScratchTranslator isRTL ifTrue: [feedbackMorph extent: (targetP x - targetBlock left)@5. self addMorphFront: (feedbackMorph position: targetP - (feedbackMorph width@0))] ifFalse: [feedbackMorph extent: (targetBlock right - targetP x)@5. self addMorphFront: (feedbackMorph position: targetP)]. ! ! !ScratchScriptsMorph methodsFor: 'stepping' stamp: 'md 2/20/2004 17:31'! stepTime ^ 50 ! ! !ScratchScriptsMorph methodsFor: 'stepping' stamp: 'ee 10/12/2007 12:44'! topArgMorphAt: aRectangle exclude: aMorph "Give feedback about possible drop targets." | stack argM | "find the top block or stack under the given point, excluding the given morph" stack _ submorphs detect: [:m | (m ~~ aMorph) and: [(m isKindOf: BlockMorph) and: [m fullBounds intersects: aRectangle]]] ifNone: [^ nil]. stack allMorphsDo: [:b | ((b isKindOf: CommandBlockMorph) and: [(b isKindOf: HatBlockMorph) not]) ifTrue: [ 1 to: b argumentCount do: [:i | argM _ b argumentAt: i. ((argM bounds intersects: aRectangle) and: [argM acceptsDroppedReporters]) ifTrue: [^ argM]]]. (b isKindOf: WhenHatBlockMorph) ifTrue: [ argM _ b argMorph. ((argM bounds intersects: aRectangle) and: [argM acceptsDroppedReporters]) ifTrue: [^ argM]]]. ^ nil ! ! !ScratchScriptsMorph methodsFor: 'stepping' stamp: 'md 3/1/2004 16:45'! topBlockMorphAt: aPoint exclude: aMorph "Give feedback about possible drop targets." | stack answer blocksUnderHand | stack _ submorphs detect: [:m | (m isKindOf: BlockMorph) and: [m fullBounds containsPoint: aPoint]] ifNone: [^ nil]. blocksUnderHand _ OrderedCollection new. stack allMorphsDo: [:s | ((s isKindOf: BlockMorph) and: [s bounds containsPoint: aPoint]) ifTrue: [blocksUnderHand addLast: s] ]. (blocksUnderHand isEmpty) ifTrue: [^ nil] ifFalse: [answer _ blocksUnderHand first. ^ answer] ! ! !ScratchScriptsMorph methodsFor: 'event handling' stamp: 'ee 7/1/2008 20:40'! cleanUp "Align all scripts vertically in alphabetical order" | sortedBlocks y collectedBlocks | collectedBlocks _ OrderedCollection new. submorphs do: [:m | (m isKindOf: BlockMorph) ifTrue:[collectedBlocks add: m]]. sortedBlocks _ collectedBlocks copy sortBy: [:s1 :s2 | s1 top < s2 top]. y _ 20. (owner isKindOf: ScrollFrameMorph2) ifTrue: [ owner hScrollPixels: 0. owner vScrollPixels: 0]. sortedBlocks do: [:s | ScratchTranslator isRTL ifTrue: [(owner isKindOf: ScrollFrameMorph2) ifTrue: [s position: (owner left + owner visibleExtent x - s width - 29)@(self top + y)] ifFalse: [s position: (self right - s width - 20)@(self top + y)]] ifFalse: [s position: self position + (20@y)]. y _ y + s fullBounds height + 15]. self layoutChanged. ! ! !ScratchScriptsMorph methodsFor: 'event handling' stamp: 'md 2/18/2004 14:58'! handlesMouseDown: evt ^ true! ! !ScratchScriptsMorph methodsFor: 'event handling' stamp: 'jm 12/19/2008 17:33'! mouseDown: evt | m | evt hand newKeyboardFocus: nil. evt hand toolType ifNotNil: [ "revert to normal cursor" evt hand toolType: nil. ^ self]. evt rightButtonPressed ifTrue: [ Sensor waitNoButton. (m _ self ownerThatIsA: ScratchScriptEditorMorph) ifNil: [^ self]. (m target notNil and: [m currentCategory = 'Scripts']) ifTrue: [m scriptsMenu: evt hand position]. ^ self]. evt hand waitForClicksOrDrag: self event: evt. ! ! !ScratchScriptsMorph methodsFor: 'event handling' stamp: 'ee 2/3/2009 13:29'! mouseEnterDragging: evt "Switch the tabs to script if a block is current being dragged" | scriptEditorMorph | scriptEditorMorph _ self ownerThatIsA: ScratchScriptEditorMorph. (scriptEditorMorph currentCategory = 'Scripts') ifFalse:[ scriptEditorMorph currentCategory: 'Scripts']. ! ! !ScratchScriptsMorph methodsFor: 'event handling' stamp: 'jm 12/30/2008 18:34'! mouseHold: evt | m | (m _ self ownerThatIsA: ScratchScriptEditorMorph) ifNil: [^ self]. (m target notNil and: [m currentCategory = 'Scripts']) ifTrue: [m scriptsMenu: evt hand position]. ! ! !ScratchScriptsMorph methodsFor: 'drawing' stamp: 'jens 3/9/2009 13:25'! drawOn: aCanvas "Fill with my texture patch. Fill with a solid color if there is no texture in the current skin dictionary." | patchF clipC y x | self isScreenshooting ifTrue: [ aCanvas fillRectangle: self bounds color: Color transparent. ^ self]. patchF _ ScratchFrameMorph skinAt: #scriptsPaneTexture ifAbsent: [ aCanvas fillRectangle: self bounds color: color. ^ self]. clipC _ aCanvas copyClipRect: self bounds. y _ self top. [y <= self bottom] whileTrue: [ x _ self left. [x <= self right] whileTrue: [ clipC paintImage: patchF at: x@y. x _ x + patchF width]. y _ y + patchF height]. ! ! !ScratchScriptsMorph methodsFor: 'drawing' stamp: 'ee 1/1/2006 15:42'! showDeleteFeedback "Display feedback for the scissors tool." | h cutBlock r | h _ World activeHand. (cutBlock _ self topBlockMorphAt: h cursorPoint exclude: nil) ifNil: [^ self]. r _ cutBlock bounds. ((cutBlock isKindOf: CBlockMorph) and: [cutBlock firstBlockList size > 0]) ifTrue: [r _ r merge: cutBlock firstBlockList first fullBounds]. (cutBlock isKindOf: HatBlockMorph) ifTrue: [r _ cutBlock fullBounds]. feedbackMorph bounds: (r expandBy: 3); color: Color transparent; borderColor: (Color r: 212/255 g: 40/255 b: 40/255). self addMorphFront: feedbackMorph. ! ! !ScratchScriptsMorph methodsFor: 'object i/o' stamp: 'jm 9/30/2003 21:29'! fieldsVersion "I depend on my superclass for object i/o. Currently, thos methods are inherited from Morph." ^ 1 ! ! !ScratchScriptsMorph methodsFor: 'screenshot' stamp: 'jens 3/9/2009 13:02'! isScreenshooting ^ screenshooting ifNil: [screenshooting _ false] ! ! !ScratchScriptsMorph methodsFor: 'screenshot' stamp: 'jens 3/9/2009 13:44'! screenshot "answer a Form of myself apt for exporting" | saveForm leftX topY rightX bottomY | screenshooting _ true. self changed. saveForm _ self imageForm. "clip" leftX _ submorphs anyOne left. topY _ submorphs anyOne top. rightX _ submorphs anyOne right. bottomY _ submorphs anyOne bottom. (self allMorphs select: [:m | m ~= self]) do: [:m | leftX _ leftX min: m left. topY _ topY min: m top. rightX _ rightX max: m right. bottomY _ bottomY max: m bottom]. saveForm _ saveForm copy: (((leftX @ topY) - self position) rect: ((rightX @ bottomY) - self position)). screenshooting _ false. self changed. ^ saveForm ! ! !ScratchScrollBar methodsFor: 'initialization' stamp: 'ee 2/11/2009 13:40'! hFrameForm: frameForm sliderForm: sliderForm "Initialize myself as a horizontal slider, setting my forms by cutting up the given forms." | midX h | "set my forms" midX _ frameForm height // 2. h _ frameForm height. frameStartForm _ frameForm copy: (0@0 extent: midX@h). frameMiddleForm _ frameForm copy: (midX@0 extent: 1@h). frameEndForm _ frameForm copy: ((midX + 1)@0 corner: frameForm extent). h _ sliderForm height. sliderStartForm _ sliderForm copy: (0@0 extent: 7@h). sliderMiddleForm _ sliderForm copy: (7@0 extent: 1@h). sliderEndForm _ sliderForm copy: (8@0 corner: sliderForm extent). sliderInsets _ 3@3 corner: 3@5. self extent: 200 @ frameStartForm height. ! ! !ScratchScrollBar methodsFor: 'initialization' stamp: 'ee 2/11/2009 13:39'! initVertical: aBoolean aBoolean ifTrue: [ self vFrameForm: (ScratchFrameMorph skinAt: #vScrollFrame) sliderForm: (ScratchFrameMorph skinAt: #vScrollSlider)] ifFalse: [ self hFrameForm: (ScratchFrameMorph skinAt: #hScrollFrame) sliderForm: (ScratchFrameMorph skinAt: #hScrollSlider)]. ! ! !ScratchScrollBar methodsFor: 'initialization' stamp: 'jm 5/26/2005 18:32'! initialize super initialize. target _ nil. selector _ nil. sliderStart _ 0. sliderLength _ 50. sliderInsets _ 3@3 corner: 0@5. dragOffset _ 0. ! ! !ScratchScrollBar methodsFor: 'initialization' stamp: 'ee 2/11/2009 13:38'! vFrameForm: frameForm sliderForm: sliderForm "Initialize myself as a vertical slider, setting my forms by cutting up the given forms." | midY w | "set my forms" midY _ frameForm height // 2. w _ frameForm width. frameStartForm _ frameForm copy: (0@0 extent: w@midY). frameMiddleForm _ frameForm copy: (0@midY extent: w@1). frameEndForm _ frameForm copy: (0@(midY + 1) corner: frameForm extent). w _ sliderForm width. sliderStartForm _ sliderForm copy: (0@0 extent: w@7). sliderMiddleForm _ sliderForm copy: (0@(7) extent: w@1). sliderEndForm _ sliderForm copy: (0@(11) corner: sliderForm extent). sliderInsets _ 3@3 corner: 0@5. self extent: frameStartForm width @ 200. ! ! !ScratchScrollBar methodsFor: 'accessing' stamp: 'jm 3/14/2005 14:58'! scrollFraction "Answer the scroll fraction, a number between 0.0 and 1.0." | maxStart | (maxStart _ self maxSliderStart) = 0 ifTrue: [^ 0.0]. ^ sliderStart asFloat / maxStart ! ! !ScratchScrollBar methodsFor: 'accessing' stamp: 'jm 2/23/2005 15:02'! scrollFraction: aNumber "Set my scroll fraction, a number between 0.0 and 1.0." sliderStart _ (((aNumber asFloat min: 1.0) max: 0.0) * self maxSliderStart) rounded. self changed. ! ! !ScratchScrollBar methodsFor: 'accessing' stamp: 'jm 2/1/2005 14:41'! selector: aSymbol selector _ aSymbol. ! ! !ScratchScrollBar methodsFor: 'accessing' stamp: 'jm 2/1/2005 14:41'! target: anObject target _ anObject. ! ! !ScratchScrollBar methodsFor: 'scroll bar protocol' stamp: 'jm 11/12/2005 13:54'! percentVisible: percentVisible "Supply an optional floating fraction so slider can expand to indicate the percent of the content that is visible." sliderLength _ ((percentVisible min: 1.0) * self maxSliderLength) rounded. sliderLength _ sliderLength max: self minSliderLength. sliderLength = self maxSliderLength ifTrue: [self scrollFraction: 0]. self changed. ! ! !ScratchScrollBar methodsFor: 'scroll bar protocol' stamp: 'jm 2/1/2005 14:43'! scrollDelta: ignore1 pageDelta: ignore2 "For compatability with old scrollbars. I don't support paging or scrolling arrows." ! ! !ScratchScrollBar methodsFor: 'scroll bar protocol' stamp: 'jm 2/23/2005 14:49'! updateTarget "Update my target object with my current value, a number between 0.0 and 1.0. Do nothing if either my target or my selector is nil." target notNil & selector notNil ifTrue: [target perform: selector with: self scrollFraction]. ! ! !ScratchScrollBar methodsFor: 'scroll bar protocol' stamp: 'jm 2/23/2005 15:01'! value: scrollFraction "Set my scroll fraction (range is 0.0 to 1.0). This method is for compatability with ScrollBar." self scrollFraction: scrollFraction. ! ! !ScratchScrollBar methodsFor: 'drawing' stamp: 'jm 4/5/2005 21:27'! drawHEdge: aForm from: startX to: endX yOffset: yOffset on: aCanvas "Use the given form starting at the given point to draw a horizontal edge between the given starting and ending x positions. Do nothing if the given form is nil." | x y | aForm ifNil: [^ self]. x _ startX. y _ bounds top + yOffset. [x <= endX] whileTrue: [ aCanvas translucentImage: aForm at: x@y. x _ x + aForm width]. ! ! !ScratchScrollBar methodsFor: 'drawing' stamp: 'ee 2/12/2009 16:26'! drawHSliderOn: aCanvas "Draw a horizontal slider on the given canvas." | y leftX rightX | sliderLength = self maxSliderLength ifTrue: [^ self]. y _ self top + sliderInsets top. leftX _ self left + sliderInsets left + sliderStart. rightX _ leftX + sliderLength. self drawHEdge: sliderMiddleForm from: leftX + sliderStartForm width to: (rightX - sliderEndForm width) yOffset: sliderInsets top on: aCanvas. aCanvas translucentImage: sliderStartForm at: (leftX @ y). aCanvas translucentImage: sliderEndForm at: ((rightX - sliderEndForm width + 1) @ y). ! ! !ScratchScrollBar methodsFor: 'drawing' stamp: 'jm 5/26/2005 18:48'! drawOn: aCanvas "Draw myself, clipping to my bounds." | clipC | clipC _ aCanvas copyClipRect: self bounds. self bounds isWide ifTrue: [ "draw horizontal frame" self drawHEdge: frameMiddleForm from: (self left + frameStartForm width) to: (self right - frameEndForm width) yOffset: 0 on: clipC. clipC translucentImage: frameStartForm at: self topLeft. clipC translucentImage: frameEndForm at: ((self right + 1 - frameEndForm width) @ self top). self drawHSliderOn: clipC] ifFalse: [ "draw vertical frame" self drawVEdge: frameMiddleForm from: (self top + frameStartForm height) to: (self bottom - frameEndForm height) xOffset: 0 on: clipC. clipC translucentImage: frameStartForm at: self topLeft. clipC translucentImage: frameEndForm at: (self left @ (self bottom + 1 - frameEndForm height)). self drawVSliderOn: clipC]. ! ! !ScratchScrollBar methodsFor: 'drawing' stamp: 'jm 4/5/2005 21:27'! drawVEdge: aForm from: startY to: endY xOffset: xOffset on: aCanvas "Use the given form starting at the given point to draw a vertical edge between the given starting and ending y positions. Do nothing if the given form is nil." | x y | aForm ifNil: [^ self]. x _ bounds left + xOffset. y _ startY. [y <= endY] whileTrue: [ aCanvas translucentImage: aForm at: x@y. y _ y + aForm height]. ! ! !ScratchScrollBar methodsFor: 'drawing' stamp: 'ee 2/11/2009 13:38'! drawVSliderOn: aCanvas "Draw a vertical slider on the given canvas." | x topY bottomY | sliderLength = self maxSliderLength ifTrue: [^ self]. x _ self left + sliderInsets left. topY _ self top + sliderInsets top + sliderStart. bottomY _ topY + sliderLength. self drawVEdge: sliderMiddleForm from: topY + sliderStartForm height to: (bottomY - sliderEndForm height) xOffset: sliderInsets left on: aCanvas. aCanvas translucentImage: sliderStartForm at: (x @ topY). aCanvas translucentImage: sliderEndForm at: (x @ (bottomY - sliderEndForm height + 1)). ! ! !ScratchScrollBar methodsFor: 'event handling' stamp: 'jm 3/27/2005 10:53'! handlesMouseDown: evt ^ true ! ! !ScratchScrollBar methodsFor: 'event handling' stamp: 'jm 2/1/2005 11:20'! linearOffset: evt "Answer the linear position of the given event's cursor point along my slider's range. For example, this will be zero at the leftmost/topmost edge of the slider's range." bounds isWide ifTrue: [^ evt cursorPoint x - (self left + sliderInsets left)] "horizontal" ifFalse: [^ evt cursorPoint y - (self top + sliderInsets top)]. "vertical" ! ! !ScratchScrollBar methodsFor: 'event handling' stamp: 'jm 3/14/2005 14:54'! maxSliderLength "Answer the maximum slider length." bounds isWide ifTrue: [^ bounds width - (sliderInsets left + sliderInsets right)] ifFalse: [^ bounds height - (sliderInsets top + sliderInsets bottom)]. ! ! !ScratchScrollBar methodsFor: 'event handling' stamp: 'jm 3/14/2005 14:56'! maxSliderStart "Answer the maximum sliderStart value given the slider's length." bounds isWide ifTrue: [^ bounds width - (sliderLength + sliderInsets left + sliderInsets right)] ifFalse: [^ bounds height - (sliderLength + sliderInsets top + sliderInsets bottom)]. ! ! !ScratchScrollBar methodsFor: 'event handling' stamp: 'ee 2/12/2009 16:26'! minSliderLength "Answer the minimum slider length." bounds isWide ifTrue: [^ sliderEndForm width + sliderEndForm width] ifFalse: [^ sliderEndForm height + sliderEndForm height]. ! ! !ScratchScrollBar methodsFor: 'event handling' stamp: 'jm 3/27/2005 11:05'! mouseDown: evt "Record the offset between the slider's start offset and the relative event's location." | offset pageDelta | dragOffset _ nil. self maxSliderLength = sliderLength ifTrue: [^ self]. "do nothing" offset _ self linearOffset: evt. ((offset >= sliderStart) & (offset <= (sliderStart + sliderLength))) ifTrue: [ "mouse went down on the slider: start dragging it" dragOffset _ sliderStart - offset]. "handle page up or down" pageDelta _ sliderLength / self maxSliderLength. offset < sliderStart ifTrue: [self scrollFraction: self scrollFraction - pageDelta; updateTarget] ifFalse: [self scrollFraction: self scrollFraction + pageDelta; updateTarget] ! ! !ScratchScrollBar methodsFor: 'event handling' stamp: 'jm 3/27/2005 10:32'! mouseMove: evt "Update the slider position if in drag mode." dragOffset ifNil: [^ self]. "not in slider drag mode" sliderStart _ (self linearOffset: evt) + dragOffset. sliderStart _ (sliderStart max: 0) min: (self maxSliderStart). self updateTarget. self changed. ! ! !ScratchScrollBar class methodsFor: 'instance creation' stamp: 'jm 5/26/2005 18:17'! newHorizontal "Answer a new horizontal scrollbar." ^ self new initVertical: false ! ! !ScratchScrollBar class methodsFor: 'instance creation' stamp: 'jm 5/26/2005 18:17'! newVertical "Answer a new vertical scrollbar." ^ self new initVertical: true ! ! This class supports peer-to-peer collaboration between Scratch instances or between Scratch and other applications that want to interact with Scratch. Scratch 1.3 includes an experimental extension feature that supports interaction between Scratch and other programs. Three kinds of interaction are supported: a. sharing broadcasts (in both directions) b. virtual sensors c. Scratch's global variables are made visible Note that peers can read, but not change, each other's global variables. Non-Scratch applications can use these mechanisms to control Scratch projects by sending broadcasts and virtual sensors available to Scratch. For example, a music sequencer might send Scratch broadcasts to synchronize animations to music. Or, Scratch can be used to control another application or (through some intermediary program) external hardware devices. Topology The network topology is a star, with one Scratch acting as the session host. Clients connect to this host. In addition to processing incoming messages from clients, the host forwards a message from one client to all other clients. The advantage of the star topology is that only the host must have a public IP address and be able to accept incoming TCP/IP connections; the clients may be bethind firewalls that do Network Address Translation (NAT) or disallow incoming connections. The disadvantages of the star toplogy are (a) messages must be forwarded by the host, increasing latency and (b) the host cannot leave the session. However, the star topology is eaiser for distributed users to set up and understand. A common use of this mechanism is to provide additional sensors to Scratch or allow Scratch to control an external program. In that case, latency is typically very low. Protocol The experimental extension feature is enabled using the right-button menu on one of the two sensor blocks. When remote sensors are enabled, Scratch listens for connections on port 42001. Once a connection is established, messages are sent in both directions over the socket connection. Each message consists of a four-byte size, most-significant byte first, followed by the message itself: The four-byte size field is not counted as part of the message size. Thus, the empty message is four zero bytes. The message up to the first whitespace character (any byte <= 32 is considered whitespace) is interpreted to as a case-insensitive command that is used to decide what to do with the rest of the message. Messages may eventually be used to transmit sprites or media, so large amounts of binary binary data in arbitrary formats may follow the initial command string. The most common messages are human-readable strings made up of the following elements: - unquoted words with no embedded whitespace (cat, dog, mouse) - quoted strings ("three word string", "embedded ""quotation marks"" are doubled") - signed decimal numbers (1, -1, 3.14, -1.2, .1, -.2) - booleans (true or false) Common Commands The command set will get extended over time, so clients should just skip any commands that they do not understand. Here are the two most useful ones: broadcast sensor-update [ ] A sensor update command is followed by zero or more (variable name, value) pairs. Variable names are strings. Values are either numbers or strings. Scratch outputs these commands when broadcasts or global variable changes occur. Scratch also responds to these commands. Broadcast commands sent to Scratch cause a broadcast to occur. Sensor-update commands update the values of virtual sensors available in the sensor block drop-down menu. ! !ScratchServer methodsFor: 'initialization' stamp: 'jm 5/14/2009 12:03'! initialize userName _ 'anonymous'. peerSockets _ OrderedCollection new. peerNames _ IdentityDictionary new. sensors _ Dictionary new. lastSentValues _ Dictionary new. incomingBroadcasts _ OrderedCollection new. outgoingBroadcasts _ OrderedCollection new. broadcastCache _ Set new. ! ! !ScratchServer methodsFor: 'accessing' stamp: 'jm 5/14/2009 12:06'! broadcastsSeen ^ broadcastCache asArray sort ! ! !ScratchServer methodsFor: 'accessing' stamp: 'jm 2/14/2008 20:43'! sensorNames ^ sensors keys asArray sort ! ! !ScratchServer methodsFor: 'accessing' stamp: 'jm 2/14/2008 20:47'! sensorValueFor: sensorName "Answer the value for the given virtual sensor or nil if the given sensor name is not found." ^ sensors at: sensorName ifAbsent: [nil] ! ! !ScratchServer methodsFor: 'accessing' stamp: 'jm 2/14/2008 18:41'! stage: aScratchStageMorph stage _ aScratchStageMorph. ! ! !ScratchServer methodsFor: 'accessing' stamp: 'jm 2/14/2008 16:08'! userName ^ userName ! ! !ScratchServer methodsFor: 'accessing' stamp: 'jm 2/14/2008 16:08'! userName: aString userName _ aString. ! ! !ScratchServer methodsFor: 'server control' stamp: 'jm 5/14/2009 12:15'! clearCaches broadcastCache _ broadcastCache species new. sensors _ sensors species new. lastSentValues _ lastSentValues species new. self resend: 'send-vars' toPeersExcept: nil. ! ! !ScratchServer methodsFor: 'server control' stamp: 'jm 6/22/2008 21:23'! endScratchSession "Close all sockets. If I am hosting, this will end the session for everyone. If I am only a client, then I will leave the session but the session itself can continue to exist." self shutdownServer. ! ! !ScratchServer methodsFor: 'server control' stamp: 'jm 6/22/2008 21:15'! isHosting "Answer true if this is the host of a Scratch session." ^ serverSocket notNil ! ! !ScratchServer methodsFor: 'server control' stamp: 'jm 6/23/2008 08:12'! joinSessionAt: ipAddressString "Add an outgoing connection to the given address. Fail if a connection cannot be made in bounded amount of time. Answer true if the connection was added successfully." | addr sock ok | addr _ NetNameResolver addressForName: ipAddressString timeout: 5. sock _ MessageSocket new. ok _ sock connectTo: addr port: ScratchServer portNumber waitSecs: 5. ok ifFalse: [sock destroy. ^ false]. sock sendMessage: 'peer-name ', userName. peerSockets add: sock. ^ true ! ! !ScratchServer methodsFor: 'server control' stamp: 'jm 6/23/2008 09:23'! sessionInProgress "Answer true if this Scratch server has any external connections." ^ (peerSockets size > 0) | (serverSocket notNil) ! ! !ScratchServer methodsFor: 'server control' stamp: 'jm 6/22/2008 16:11'! shutdownServer "Destroy all open sockets." serverSocket ifNotNil: [ serverSocket destroy. serverSocket _ nil]. incomingUDPSocket ifNotNil: [ incomingUDPSocket destroy. incomingUDPSocket _ nil]. peerSockets do: [:sock | sock destroy]. peerSockets _ peerSockets species new. peerNames _ peerNames species new. sensors _ sensors species new. lastSentValues _ lastSentValues species new. outgoingBroadcasts _ outgoingBroadcasts species new. ! ! !ScratchServer methodsFor: 'server control' stamp: 'jm 6/22/2008 21:13'! startHosting "Open a socket on my port and start accepting connections." Socket initializeNetwork. self shutdownServer. serverSocket _ Socket new. serverSocket listenOn: self class portNumber backlogSize: 20. incomingUDPSocket _ Socket newUDP setPort: self class portNumber. ! ! !ScratchServer methodsFor: 'server control' stamp: 'jm 6/23/2008 09:26'! stepServer "Do one server step." "Note: processIncomingCommands clears outgoingBroadcasts, so do sendOutgoingCommands first." self sessionInProgress ifFalse: [^ self]. self acceptNewConnections. self sendOutgoingCommands. self processIncomingCommands. self processIncomingBroadcasts. ! ! !ScratchServer methodsFor: 'private-server' stamp: 'jm 3/26/2009 09:38'! acceptNewConnections "Accept new connections, if any. Also process any newly connected outgoing sockets." | newSock | serverSocket ifNil: [^ self]. [serverSocket isConnected] whileTrue: [ newSock _ serverSocket accept. peerSockets addLast: (MessageSocket new on: newSock). self resendAllVars]. ! ! !ScratchServer methodsFor: 'private-server' stamp: 'jm 6/22/2008 21:28'! processIncomingBroadcasts "NOTE: Broadcast events are added to the outgoingBroadcasts queue as a side-effect of re-broadcasting them locally. To avoid having broadcasts bounce from peer to peer forever, the outgoingBroadcasts queue is cleard after incoming commands have been processed." stage ifNotNil: [ incomingBroadcasts do: [:evtName | stage broadcastEventNamed: evtName with: 0]]. incomingBroadcasts _ incomingBroadcasts species new: 100. outgoingBroadcasts _ outgoingBroadcasts species new: 100. ! ! !ScratchServer methodsFor: 'private-server' stamp: 'jm 6/22/2008 21:28'! processIncomingCommands "Process incoming commands from my peers." peerSockets copy do: [:sock | sock isConnected ifTrue: [ self processCommandFrom: sock. sock sendData] ifFalse: [ sock destroy. peerSockets remove: sock ifAbsent: []]]. self processUDPCommands. ! ! !ScratchServer methodsFor: 'private-server' stamp: 'jm 6/22/2008 16:54'! processUDPCommands "Process incoming UDP commands." | buf n msg | incomingUDPSocket ifNil: [^ self]. buf _ String new: 3000. [true] whileTrue: [ n _ [incomingUDPSocket receiveDataInto: buf] ifError: [0]. n = 0 ifTrue: [^ self]. msg _ buf copyFrom: 1 to: n. [self dispatch: msg from: nil] ifError: []]. ! ! !ScratchServer methodsFor: 'private-server' stamp: 'jm 5/14/2009 20:28'! queueBroadcast: aString "Queue the given broadcast event to be sent to my peers." self sessionInProgress ifFalse: [^ self]. (#( 'Scratch-KeyPressedEvent' 'Scratch-MouseClickEvent' 'Scratch-StartClicked' ) includes: aString) ifTrue: [^ self]. outgoingBroadcasts add: aString. ! ! !ScratchServer methodsFor: 'private-server' stamp: 'jm 5/19/2009 10:48'! sendOutgoingCommands "Send broadcasts and variable updates to my peers." | varUpdateMsg | self someVariableHasChanged ifTrue: [varUpdateMsg _ self variableUpdateMessage] ifFalse: [varUpdateMsg _ nil]. peerSockets copy do: [:sock | sock isConnected ifTrue: [ varUpdateMsg ifNotNil: [sock sendMessage: varUpdateMsg]. outgoingBroadcasts do: [:evt | sock sendMessage: (self broadcastMessageFor: evt)]. sock sendData] ifFalse: [ sock destroy. peerSockets remove: sock ifAbsent: []]]. self recordVariableValues. ! ! !ScratchServer methodsFor: 'private-incoming commands' stamp: 'jm 8/9/2008 17:48'! dispatch: aString from: requestSock "Dispatch an incoming command from a remote Scratch." | cmd op | aString size = 0 ifTrue: [ requestSock sendMessage: 'version "ScratchServer 2.0 alpha"'. ^ self]. op _ self opcodeFrom: aString. op size = 0 ifTrue: [^ self]. (#('broadcast' 'sensor-update' 'peer-name' 'send-vars') includes: op) ifFalse: [^ self]. cmd _ self parse: aString. 'broadcast' = op ifTrue: [ self doBroadcast: cmd from: requestSock. ^ self resend: aString toPeersExcept: requestSock]. 'sensor-update' = op ifTrue: [ self doSensorUpdate: cmd from: requestSock. ^ self resend: aString toPeersExcept: requestSock]. 'peer-name' = op ifTrue: [^ self doPeerName: cmd from: requestSock]. 'send-vars' = op ifTrue: [ self doSendVars: cmd from: requestSock. ^ self resend: aString toPeersExcept: requestSock]. ! ! !ScratchServer methodsFor: 'private-incoming commands' stamp: 'jm 5/14/2009 12:25'! doBroadcast: cmd from: requestSocket "Handle a broadcast command: broadcast " | evtName | cmd size = 2 ifFalse: [^ self]. ((evtName _ cmd at: 2) isKindOf: String) ifFalse: [^ self]. incomingBroadcasts add: evtName. broadcastCache add: evtName. ! ! !ScratchServer methodsFor: 'private-incoming commands' stamp: 'jm 6/23/2008 09:20'! doPeerName: cmd from: requestSock "Handle a peer-name command: peer-name " | peerName | cmd size = 2 ifFalse: [^ self]. ((peerName _ cmd at: 2) isKindOf: String) ifFalse: [^ self]. peerNames at: requestSock put: peerName. self dispatch: 'send-vars' from: nil. ! ! !ScratchServer methodsFor: 'private-incoming commands' stamp: 'jm 6/23/2008 09:19'! doSendVars: cmd from: requestSock "Handle a variable send request: send-vars" "force all my global variables to be re-transmitted" lastSentValues _ lastSentValues species new. ! ! !ScratchServer methodsFor: 'private-incoming commands' stamp: 'jm 9/2/2008 13:18'! doSensorUpdate: cmd from: requestSocket "Handle a sensor update command: sensor-update [ ]" | i sName sValue | i _ 2. [i < cmd size] whileTrue: [ sName _ cmd at: i. sValue _ cmd at: i + 1. (sName isKindOf: String) ifTrue: [sensors at: sName put: sValue]. i _ i + 2]. ! ! !ScratchServer methodsFor: 'private-incoming commands' stamp: 'jm 8/4/2008 08:06'! opcodeFrom: aString "Answer the command opcode for the given message. The opcode is the unquoted string at the beginning of the message up to the first whitespace character." | i | i _ 1. [i <= aString size and: [(aString at: i) asciiValue > 32]] whileTrue: [i _ i + 1]. ^ (aString copyFrom: 1 to: i - 1) asLowercase ! ! !ScratchServer methodsFor: 'private-incoming commands' stamp: 'jm 6/22/2008 16:54'! processCommandFrom: requestSock "Handle a command from the given socket. Do nothing if there are no messages." | msg | (msg _ requestSock nextMessage) ifNil: [^ self]. [self dispatch: msg asString from: requestSock] ifError: [:err :rcvr | err]. ! ! !ScratchServer methodsFor: 'private-incoming commands' stamp: 'jm 6/22/2008 17:05'! resend: msgString toPeersExcept: requestSocket "Handle a broadcast command: broadcast " peerSockets do: [:sock | sock ~= requestSocket ifTrue: [sock sendMessage: msgString]]. ! ! !ScratchServer methodsFor: 'private-command parsing' stamp: 'jm 2/14/2008 13:42'! parse: aString "ScratchServer new parse: 'test abc 1 -1 .01 -.01 true false'" | out token | in _ ReadStream on: aString asString. out _ OrderedCollection new. [in atEnd] whileFalse: [ token _ self readNext. token ifNotNil: [out addLast: token]]. ^ out asArray ! ! !ScratchServer methodsFor: 'private-command parsing' stamp: 'jm 2/14/2008 12:41'! readNext "Read the next command line token from 'in' or nil if there are not more tokens. A token is a quoted string, a number, a boolean, or an unquoted string with no embedded whitespace. Strings that start with a digit or minus sign must be quoted to avoid being treated as an integer." | ch s | "skip white space" [in atEnd not and: [in peek asciiValue <= 32]] whileTrue: [in next]. in atEnd ifTrue: [^ nil]. ch _ in peek. ch = $" ifTrue: [^ self readQuotedString]. (ch = $-) | (ch = $.) | ch isDigit ifTrue: [^ self readNumber]. "unquoted string or boolean" s _ self readString. 'true' = s ifTrue: [^ true]. 'false' = s ifTrue: [^ false]. ^ s ! ! !ScratchServer methodsFor: 'private-command parsing' stamp: 'jm 2/14/2008 12:20'! readNumber | s sign | s _ self readString. sign _ 1. s first = $- ifTrue: [s _ s copyFrom: 2 to: s size. sign _ -1]. s first = $. ifTrue: [s _ '0', s]. "add leading zero" ^ s asNumber * sign ! ! !ScratchServer methodsFor: 'private-command parsing' stamp: 'jm 7/31/2008 07:45'! readQuotedString | result ch | result _ WriteStream on: String new. in next. "skip opening quote" [in atEnd] whileFalse: [ ch _ in next. ch = $" ifTrue: [ in peek = $" ifTrue: [in skip: 1] ifFalse: [^ result contents]]. result nextPut: ch]. ^ result contents ! ! !ScratchServer methodsFor: 'private-command parsing' stamp: 'jm 2/14/2008 12:26'! readString | result ch | result _ WriteStream on: String new. [in atEnd] whileFalse: [ ch _ in peek. ch asciiValue <= 32 ifTrue: [^ result contents]. result nextPut: in next]. ^ result contents ! ! !ScratchServer methodsFor: 'private-variables' stamp: 'jm 5/19/2009 10:46'! broadcastMessageFor: aString "Answer a message to broadcast the given string." | msg | msg _ WriteStream on: String new. msg nextPutAll: 'broadcast '. self putString: aString on: msg. ^ msg contents ! ! !ScratchServer methodsFor: 'private-variables' stamp: 'jm 7/31/2008 14:47'! putString: aString on: aStream "Append the given string to the given stream. Double any embedded double-quote characters." aStream nextPut: $". aString do: [:ch | aStream nextPut: ch. ch = $" ifTrue: [aStream nextPut: ch]]. aStream nextPut: $". ! ! !ScratchServer methodsFor: 'private-variables' stamp: 'jm 2/15/2008 08:01'! recordVariableValues "Record the current values of my global variables." lastSentValues _ Dictionary new. stage ifNotNil: [ stage varNames do: [:v | lastSentValues at: v put: (stage getVar: v)]]. ! ! !ScratchServer methodsFor: 'private-variables' stamp: 'jm 3/26/2009 09:36'! resendAllVars "Force all my global variables to be re-transmitted." lastSentValues _ lastSentValues species new. ! ! !ScratchServer methodsFor: 'private-variables' stamp: 'jm 2/15/2008 07:58'! someVariableHasChanged "Answer true if a global variable has changed since I last sent variable updates to my peers." | lastVal | stage ifNil: [^ false]. stage varNames do: [:v | lastVal _ lastSentValues at: v ifAbsent: [nil]. lastVal ifNil: [^ true]. (lastVal = (stage getVar: v)) ifFalse: [^ true]]. ^ false ! ! !ScratchServer methodsFor: 'private-variables' stamp: 'jm 5/14/2009 08:16'! variableUpdateMessage "Answer a variable update message for all variables whose values have not already been sent." | msg currentValue lastValue v | msg _ WriteStream on: String new. msg nextPutAll: 'sensor-update '. stage varNames do: [:varName | currentValue _ stage getVar: varName. lastValue _ lastSentValues at: varName ifAbsent: [nil]. lastValue = currentValue ifFalse: [ self putString: varName on: msg. msg space. v _ stage interpretStringAsNumberIfPossible: currentValue. v isNumber ifTrue: [msg nextPutAll: v printString] ifFalse: [self putString: v asString on: msg]. msg space]]. ^ msg contents ! ! !ScratchServer class methodsFor: 'utilities' stamp: 'jm 2/16/2008 11:28'! getIPAddressFromServer "Attempt to get my IP address from the AddressServer in bounded time. Returns nil if unsuccessful." "[self getIPAddressFromServer] msecs" | serverAddr sock msg | serverAddr _ #(18 85 18 78) asByteArray. sock _ MessageSocket new. sock connectTo: serverAddr port: 54330 waitSecs: 4. sock isConnected ifFalse: [^ nil]. sock sendMessage: 'lookup'. 20 timesRepeat: [ sock sendData. (msg _ sock nextMessage) ifNotNil: [sock destroy. ^ msg]. Delay waitMSecs: 100]. "timed out" sock destroy. ^ nil ! ! !ScratchServer class methodsFor: 'utilities' stamp: 'jm 6/22/2008 16:26'! portNumber "Answer the port number for the Scratch server." ^ 42001! ! I represent a virtual Scratch 'directory' that is actually stored on an HTTP server. A directory is actually a web page, where links from that page appear as subdirectories. This is not meant to be used with on arbitrary web pages. Instead, it's meant to view a repository of Scratch projects structured like a folder hierarchy. ! !ScratchServerDirectory methodsFor: 'accessing' stamp: 'jm 11/9/2006 11:08'! allNames "If the last fetch completed successfully, answer an array of all my entry names. Otherwise, answer an empty array." | allNames | self fetchSucceeded ifFalse: [^ #()]. allNames _ (HtmlChunker linksFrom: fetcher bodyData asString) asArray collect: [:assoc | assoc value replaceHtmlHexChars]. allNames _ allNames select: [:s | s size > 0 and: [s first ~= $/]]. "eliminate parent folder links" allNames _ allNames select: [:s | ((s includes: $=) | (s includes: $;)) not]. "eliminate Apache sorting links" allNames _ allNames collect: [:s | ((s size > 1) and: [s last = $/]) ifTrue: [s copyFrom: 1 to: s size - 1] ifFalse: [s]]. "remove trailing '/' characters" allNames sort: [:s1 :s2 | s1 caseInsensitiveLessOrEqual: s2]. ^ allNames ! ! !ScratchServerDirectory methodsFor: 'accessing' stamp: 'jm 5/21/2005 16:54'! directoryNamed: aString "Answer a new ServerDirectory on the given subdirectory of my current path. In HTML terms, a subdirectory is a link from the current page." | newPath | fetcher ifNotNil: [fetcher stopDownload. fetcher _ nil]. newPath _ aString. (newPath endsWith: '/') ifFalse: [newPath _ newPath, '/']. newPath first = $/ ifTrue: [^ self copy path: newPath]. "absolute path" "relative path" newPath _ path last = $/ ifTrue: [path, aString] ifFalse: [path, '/', aString]. (newPath endsWith: '/') ifFalse: [newPath _ newPath, '/']. ^ self copy path: newPath ! ! !ScratchServerDirectory methodsFor: 'accessing' stamp: 'jm 5/17/2005 19:43'! parentDirectory "Answer a new ServerDirectory on the parent of my current path." | newPath parts | fetcher ifNotNil: [fetcher stopDownload. fetcher _ nil]. newPath _ ''. parts _ path findTokens: '/'. 1 to: parts size - 1 do: [:i | newPath _ newPath, '/', (parts at: i)]. ((newPath size = 0) or: [newPath last ~= $/]) ifTrue: [newPath _ newPath, '/']. ^ self copy path: newPath ! ! !ScratchServerDirectory methodsFor: 'accessing' stamp: 'jm 5/20/2005 20:12'! path ^ path ! ! !ScratchServerDirectory methodsFor: 'accessing' stamp: 'jm 5/22/2005 21:28'! path: aString "Set my path to the given string." fetcher ifNotNil: [fetcher stopDownload. fetcher _ nil]. path _ aString. ! ! !ScratchServerDirectory methodsFor: 'accessing' stamp: 'jm 5/18/2005 18:16'! pathNameDelimiter ^ $/ ! ! !ScratchServerDirectory methodsFor: 'accessing' stamp: 'jm 5/18/2005 12:12'! pathParts "Answer my path as an array of directory names." ^ (path findTokens: '/') asArray ! ! !ScratchServerDirectory methodsFor: 'accessing' stamp: 'jm 5/18/2005 15:11'! serverName ^ serverName ! ! !ScratchServerDirectory methodsFor: 'accessing' stamp: 'jm 5/17/2005 20:04'! serverName: aString "Set my server name to the given string (e.g. 'web.media.mit.edu')." serverName _ aString. ! ! !ScratchServerDirectory methodsFor: 'accessing' stamp: 'jm 5/18/2005 15:19'! stopFetching "Stop the fetch that is in progress. Do nothing if a fetch is not in progress." fetcher ifNotNil: [ fetcher stopDownload. fetcher _ nil]. ! ! !ScratchServerDirectory methodsFor: 'fetching' stamp: 'jm 5/17/2005 20:00'! fetchFailureReason "Answer the reason for the fetch failure. Answer the empty string if there has not been a failure." fetcher ifNil: [^ '']. ^ fetcher failureReason ! ! !ScratchServerDirectory methodsFor: 'fetching' stamp: 'jm 5/17/2005 20:15'! fetchInProgress "Answer true if my contents is still being fetched. If a fetch has not yet been started, start one." "Note: When this method answer's true, the client should first make sure that the fetch succeeded using fetchSucceeded. If it did, then directoryNames and projectNames can be called to access the directory contents." fetcher ifNil: [ fetcher _ HTTPFetcher new. fetcher startDownload: path fromServer: serverName]. ^ fetcher inProgress ! ! !ScratchServerDirectory methodsFor: 'fetching' stamp: 'jm 5/17/2005 20:01'! fetchSucceeded "Answer true if fetching my contents was successful." fetcher ifNil: [^ false]. ^ fetcher succeeded ! ! I am a slider with built-in fields for my miniumum and maximum values. ! !ScratchSliderMorph methodsFor: 'initialization' stamp: 'jm 6/14/2004 20:29'! buildNumberBox | box m | box _ BorderedMorph new borderWidth: 1; borderInset; color: Color paleBlue; extent: 20@17. m _ UpdatingStringMorph new target: slider; floatPrecision: 0.01; useDefaultFormat. box addMorph: m. ^ box ! ! !ScratchSliderMorph methodsFor: 'initialization' stamp: 'jm 6/14/2004 20:28'! initialize super initialize. slider _ SimpleSliderMorph new minVal: 0; maxVal: 100; setValue: 50; extent: 128@10. self orientation: #horizontal; centering: #center; hResizing: #shrinkWrap; borderWidth: 1; borderColor: (Color r: 0.0 g: 0.65 b: 0.0); addMorphBack: (sliderMin _ self buildNumberBox); addMorphBack: (AlignmentMorph new width: 5; color: Color transparent; hResizing: #rigid); addMorphBack: slider; addMorphBack: (AlignmentMorph new width: 5; color: Color transparent; hResizing: #rigid); addMorphBack: (sliderMax _ self buildNumberBox). sliderMin firstSubmorph putSelector: #minVal:; getSelector: #minVal. sliderMax firstSubmorph putSelector: #maxVal:; getSelector: #maxVal. ! ! !ScratchSliderMorph methodsFor: 'accessing' stamp: 'TIS 7/10/2003 14:08'! slider ^ slider! ! !ScratchSliderMorph methodsFor: 'accessing' stamp: 'jm 9/25/2003 11:20'! target ^ slider target ! ! !ScratchSliderMorph methodsFor: 'accessing' stamp: 'jm 6/14/2004 20:25'! target: anObject slider target: anObject. ! ! !ScratchSliderMorph methodsFor: 'accessing' stamp: 'jm 9/25/2003 11:23'! variable ^ variable ! ! !ScratchSliderMorph methodsFor: 'accessing' stamp: 'jm 9/25/2003 11:23'! variable: aVariable variable _ aVariable. ! ! !ScratchSliderMorph methodsFor: 'stepping' stamp: 'jm 6/14/2004 20:33'! step self updateSliderPosition. self fixLayoutOf: sliderMin. self fixLayoutOf: sliderMax. ! ! !ScratchSliderMorph methodsFor: 'stepping' stamp: 'tis 7/7/2003 12:57'! stepTime ^ 100! ! !ScratchSliderMorph methodsFor: 'private' stamp: 'jm 6/14/2004 20:32'! fixLayoutOf: numberBox | stringMorph | stringMorph _ numberBox firstSubmorph. numberBox width: stringMorph width + 6. stringMorph position: numberBox position + (3@2). ! ! !ScratchSliderMorph methodsFor: 'private' stamp: 'jm 11/15/2006 11:27'! updateActionSelector slider actionSelector: #setVar:to:; arguments: (Array with: variable). ! ! !ScratchSliderMorph methodsFor: 'private' stamp: 'jm 11/15/2006 11:37'! updateSliderPosition "Set my slider position to the variable's current value" | value | self target ifNil: [^ self]. value _ self target perform: #setVar:to: with: variable. value ifNil: [^ self]. "can happen when variable has been deleted" slider adjustToValue: value. ! ! !ScratchSliderMorph methodsFor: 'object i/o' stamp: 'jm 9/25/2003 11:16'! fieldsVersion ^ 1 ! ! !ScratchSliderMorph methodsFor: 'object i/o' stamp: 'jm 9/25/2003 11:27'! initFieldsFrom: anObjStream version: classVersion super initFieldsFrom: anObjStream version: classVersion. self initFieldsNamed: #( slider sliderMin sliderMax variable ) from: anObjStream. ! ! !ScratchSliderMorph methodsFor: 'object i/o' stamp: 'jm 9/25/2003 11:28'! storeFieldsOn: anObjStream super storeFieldsOn: anObjStream. self storeFieldsNamed: #( slider sliderMin sliderMax variable ) on: anObjStream. ! ! A sound editor, based on the wave editor. It can be brought up by the SoundMorph. When the soundEditor is brought up, it is created with a copy of the sound in the sound morph. It contains the sound of the soundmorph and is capable of editing that sound. That sound is set in the method "sound:" "viewing" graph GraphMorph viewer GraphMorph selection Array an array of 2 #s that are the start and end of the selection. This and the next 2 variables are in terms of indices of the graph data startSelection Number a number indicating the constant pt in a selection (mouse down loc) cursor Number a number indicating the location of the cursor "Scrolling in view" scrollDelta Number a number telling us how much to scroll by, used in stepping to allow scrolling by buttons slider slider morph a slider for the graph time a number where we are in the graph in terms os msecs deltaVal number how much the cursor scrolls by, scrolldelta is set to +- this value fwdButton button scolling backButton button scrolling "Playing variables" origSamplingRate big number indicates the sampling rate of the song @ loading so u can reset to that sampling rate. samplingRate another big number indicates current sampling rate. (2 above aren't currently used) volume slider morph indicates volume, (0 to 1.0) preview boolean whether we're in play all mode or play from cursor mode, where the cursor moves along with the music "Sound manipulation" snd SampledSound Current sound used for playing from cursor + selection completeSnd SampledSound The entire song after editing (doesn't change with playing) soundMorphSound SampledSound a ref to the sound of the SoundMorph that created this, so that u can change the sound within that sound editor "Editing tools" copy SoundBuffer the portion of the graph data that has been copied undoSound SampledSound the sound before any cuts/pastes crops undoSel Array a copy of the selection bf any cut/paste/crop rateSlider currently not implemented keyboard! !ScratchSoundEditor methodsFor: 'initialization' stamp: 'jm 9/30/2003 09:14'! addControls | b r spacer | b _ SimpleButtonMorph new target: self; borderColor: Color black; useSquareCorners. b borderColor: #raised; borderWidth: 3. r _ AlignmentMorph newRow. r color: Color transparent; borderWidth: 0; inset: 0. r hResizing: #spaceFill; vResizing: #shrinkWrap; extent: 5@5. r centering: #center. spacer _ Morph new color: r color; extent: 40@5. "spacer" r addMorphBack: (spacer fullCopy width: 5). r addMorphBack: (b fullCopy label: 'undo'; actionSelector: #undo). r addMorphBack: (spacer fullCopy width: 5). r addMorphBack: (b fullCopy label: 'copy'; actionSelector: #copy). r addMorphBack: (spacer fullCopy width: 5). r addMorphBack: (b fullCopy label: 'paste'; actionSelector: #paste). r addMorphBack: (spacer fullCopy width: 5). r addMorphBack: (b fullCopy label: 'cut'; actionSelector: #cut). r addMorphBack: (spacer fullCopy width: 5). r addMorphBack: (b fullCopy label: 'crop'; actionSelector: #crop). r addMorphBack: (spacer fullCopy width: 25). r addMorphBack: (b copy target: graph; label: 'zoom selection'; actionSelector: #viewSelection). r addMorphBack: (spacer fullCopy width: 25). r addMorphBack: (b fullCopy label: 'okay'; actionSelector: #save). r addMorphBack: (spacer fullCopy width: 5). r addMorphBack: (b fullCopy label: 'cancel'; actionSelector: #cancel). self addMorphBack: r. ! ! !ScratchSoundEditor methodsFor: 'initialization' stamp: 'jm 9/29/2003 20:44'! addGraph | r | r _ AlignmentMorph newRow. r color: Color transparent; borderWidth: 0; inset: 0. r hResizing: #shrinkWrap; vResizing: #shrinkWrap; extent: 5@5. r centering: #center. r addMorphBack: (AlignmentMorph newSpacer: r color). r addMorphBack: graph. r addMorphBack: (AlignmentMorph newSpacer: r color). self addMorphBack: r. ! ! !ScratchSoundEditor methodsFor: 'initialization' stamp: 'jm 5/6/2004 19:40'! addPlayButtons | b r m space n scaleSelector | b _ SimpleButtonMorph new target: self; borderColor: Color black; useSquareCorners. b borderColor: #raised; borderWidth: 3. r _ AlignmentMorph newColumn. r color: Color transparent; borderWidth: 0; inset: 0. r hResizing: #spaceFill; vResizing: #shrinkWrap; extent: 5@5. r centering: #center. m _ AlignmentMorph newRow. m color: Color transparent; borderWidth: 0; inset: 0. m hResizing: #shrinkWrap; vResizing: #shrinkWrap; extent: 5@5. m centering: #center. space _ Morph new color: r color; extent: 3@5. n _ StringMorph new contents: 'Zoom to: '. m addMorphBack: n. scaleSelector _ (IconicButton new labelGraphic: self class downArrow; extent: 15@15; color: Color transparent; borderWidth: 0; target: graph; actWhen: #buttonDown; actionSelector: #setScale). m addMorphBack: scaleSelector. m addMorphBack: (Morph new color: r color; extent: 5@5). m addMorphBack: (b copy target: graph; label: '+'; actionSelector: #zoomIn). m addMorphBack: (Morph new color: r color; extent: 5@5). m addMorphBack: (b copy target: graph; label: '-'; actionSelector: #zoomOut). "n _ UpdatingStringMorph new target: graph; getSelector: #scale; growable: false; width: 25; step. m addMorphBack: n." m addMorphBack: (Morph new color: r color; extent: 50@5). m addMorphBack: (b fullCopy label: 'Play All'; actionSelector: #playAll). m addMorphBack: space copy. m addMorphBack: (b fullCopy label: 'Play'; actionSelector: #playAfterCursor). m addMorphBack: space copy. m addMorphBack: (b fullCopy label: 'Stop'; actionSelector: #stop). m addMorphBack: (Morph new color: r color; extent: 50@5). m addMorphBack: (StringMorph new contents: 'Volume'). m addMorphBack: space copy. volume _ SimpleSliderMorph new color: Color veryVeryLightGray; extent: 60@2; target: self; actionSelector: #setVolume:. m addMorphBack: volume. m addMorphBack: (Morph new color: r color; extent: 50@5). r addMorphBack: (Morph new color: r color; extent: 5@5). r addMorphBack: m. self addMorphBack: r. ! ! !ScratchSoundEditor methodsFor: 'initialization' stamp: 'jm 5/6/2004 19:41'! addSlider | r m spacer | r _ AlignmentMorph newRow. r color: Color transparent; borderWidth: 0; inset: 0. r hResizing: #spaceFill; vResizing: #shrinkWrap; extent: 5@5. r centering: #center. spacer _ Morph new color: r color; extent: (graph left -10)@5. backButton _ self makeSliderButton: 10@13. backButton addMorphCentered: (ImageMorph new form: (self class upArrow rotateBy: #left centerAt: 0@0)). fwdButton _ self makeSliderButton: 10@13. fwdButton addMorphCentered: (ImageMorph new form: (self class upArrow rotateBy: #right centerAt: 0@0)). slider _ SimpleSliderMorph new color: Color veryVeryLightGray; extent: (graph width)@2; target: self; setMaxVal: (graph bounds width/(graph scale*graph data size)); actionSelector: #scrollTime:. r addMorph: spacer. r addMorph: fwdButton. r addMorph: slider. r addMorph: backButton. m _ Morph new color: r color; extent: 15@5. "spacer" r addMorphBack: m. time _ UpdatingStringMorph new target: self; getSelector: #startTime; width: 40; step. time useStringFormat. r addMorphBack: time. self addMorphBack: r. ! ! !ScratchSoundEditor methodsFor: 'initialization' stamp: 'LY 7/11/2003 14:05'! addValueSelectors | r m b | b _ SimpleButtonMorph new target: self; borderColor: Color black; useSquareCorners. b borderColor: #raised; borderWidth: 3. r _ AlignmentMorph newRow. r color: Color transparent; borderWidth: 0; inset: 0. r hResizing: #spaceFill; vResizing: #shrinkWrap; extent: 5@5. r centering: #center. r addMorphBack: (Morph new color: r color; extent: 5@5). "r addMorphBack: (b fullCopy label: '<<'; actionSelector: #slower)." r addMorphBack: (b fullCopy label: 'Reset Play Rate'; actionSelector: #resetSamplingRate). "r addMorphBack: (b fullCopy label: '>>'; actionSelector: #faster)." r addMorphBack: (Morph new color: r color; extent: 25@5). m _ StringMorph new contents: 'Sampling Rate'. r addMorphBack: m. m_ Morph new color: r color; extent: 5@5. "spacer" r addMorphBack: m. rateSlider _ SimpleSliderMorph new color: Color veryVeryLightGray; extent: 60@2; target: self; minVal: 0.2; maxVal: 2.5; actionSelector: #samplingRate:. r addMorphBack: rateSlider. r addMorphBack: (Morph new color: r color; extent: 30@5). m _ StringMorph new contents: 'Index: '. r addMorphBack: m. m _ UpdatingStringMorph new target: graph; getSelector: #cursor; putSelector: #cursor:; growable: false; width: 30; step. r addMorphBack: m. m_ Morph new color: r color; extent: 20@5. "spacer" r addMorphBack: m. m _ StringMorph new contents: 'Value: '. r addMorphBack: m. m _ UpdatingStringMorph new target: graph; getSelector: #valueAtCursor; putSelector: #valueAtCursor:; growable: false; width: 40; step. r addMorphBack: m. m_ Morph new color: r color; extent: 20@5. "spacer" r addMorphBack: m. "r addMorphBack: ( b fullCopy label: 'zoom to selection'; actionSelector: #zoomToSelection). m _ Morph new color: r color; extent: 5@5. r addMorphBack: m." self addMorphBack: r. ! ! !ScratchSoundEditor methodsFor: 'initialization'! addViewer | r | r _ AlignmentMorph newRow. r color: Color transparent; borderWidth: 0; inset: 0. r hResizing: #shrinkWrap; vResizing: #shrinkWrap; extent: 5@5. r centering: #center. r addMorphBack: (AlignmentMorph newSpacer: r color). r addMorphBack: viewer. r addMorphBack: (AlignmentMorph newSpacer: r color). self addMorphBack: r. ! ! !ScratchSoundEditor methodsFor: 'initialization' stamp: 'jm 6/4/2004 10:11'! initialize super initialize. copy _ nil. selection _ {nil. nil}. scrollDelta _ 0. deltaVal _ 10. cursor _ 200. playCursor _ nil. samplingRate _ SoundPlayer samplingRate. soundName _ 'sound'. self extent: 5@5; orientation: #vertical; centering: #centered; hResizing: #spaceFill; vResizing: #shrinkWrap; inset: 3; color: Color lightGray; borderWidth: 2. graph _ ScratchGraphMorph new extent: 380@150. graph editor: self. graph selection: selection. graph addRuler. graph viewer: false. viewer _ ScratchGraphMorph new extent: 380@30. viewer editor: self. viewer cursorColorAtZeroCrossings: Color red. viewer viewer: true. viewer selection: selection. self addControls. self addPlayButtons. self addMorphBack: (Morph new color: self color; extent: 10@5). self addGraph. self addMorphBack: (Morph newBounds: (0@0 extent: 0@3) color: Color transparent). self addSlider. self addViewer. viewer left: backButton right. ! ! !ScratchSoundEditor methodsFor: 'initialization' stamp: 'jm 2/3/2009 14:27'! makeSliderButton: buttonExtent | button | button _ BorderedMorph newBounds: (self innerBounds bottomRight - buttonExtent extent: buttonExtent) color: Color lightGray. button setBorderWidth: 1 borderColor: #raised. ^ button ! ! !ScratchSoundEditor methodsFor: 'initialization'! playAfterCursor | sel currSel | graph data size < 2 ifTrue: [^ self]. sel _ ((selection at: 1) notNil and: [(selection at: 2) - (selection at: 1) > 3]). sel ifTrue: [currSel _ graph data copyFrom: (selection at: 1) asInteger to: (selection at: 2) asInteger. playCursor _ selection at: 1. endPlaying _ selection at: 2.] ifFalse: [currSel _ graph data copyFrom: (cursor max: 1) asInteger to: graph data size. playCursor _ cursor. endPlaying _ graph data size]. self changed. snd isNil ifTrue: [ snd _ (SampledSound samples: currSel samplingRate: origSamplingRate). snd play.] ifFalse: [ currSel = snd samples ifTrue: [snd samplesRemaining = 0 ifTrue: [snd reset]. snd resumePlaying.] ifFalse: [ snd setSamples: currSel samplingRate: origSamplingRate. snd play]]. ! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'LY 7/10/2003 16:34'! faster | rate | rate _ rateSlider getScaledValue*1.1. rateSlider setScaledValue: rate. self samplingRate: rate. ! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'jm 7/12/2003 17:39'! invokeMenu "Invoke a menu of additonal functions." | aMenu | aMenu _ CustomMenu new. aMenu addList: #( ('save to file' saveToFile) ('read from file' readFromFile)). aMenu invokeOn: self defaultSelection: nil. ! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'LY 7/10/2003 14:02'! origSamplingRate ^origSamplingRate. ! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'LY 7/21/2003 13:45'! pause (snd notNil and: [snd isPlaying]) ifTrue: [snd pause.]. ! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'ee 8/13/2003 15:25'! playAll "Resumes playing the selection if there is one, otherwise resumes playing the entire soundtrack." graph data size < 2 ifTrue: [^ self]. playCursor _ 1. endPlaying _ graph data size. snd isNil ifTrue: [ snd _ (SampledSound samples: graph data samplingRate: origSamplingRate). snd play.] ifFalse: [ snd reset. (graph data = snd samples) ifTrue: [snd samplesRemaining = 0 ifTrue: [snd reset]. snd resumePlaying.] ifFalse: [snd setSamples: graph data samplingRate: origSamplingRate. snd play]] ! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'LY 7/18/2003 14:00'! playFrom: start to: end | sz i1 i2 snd2 | sz _ graph data size. i1 _ ((start + 1) min: sz) max: 1. i2 _ ((end + 1) min: sz) max: i1. (i1 + 2) >= i2 ifTrue: [^ self]. snd2 _ SampledSound samples: (graph data copyFrom: i1 to: i2) samplingRate: samplingRate. snd2 play. ! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'jm 12/4/2003 20:19'! readFromFile "Read my samples from a file selected by the user." | result | result _ StandardFileMenu oldFileExtensions: #(aif aiff au wav). result ifNil: [^ self]. self readFromFile: result directory pathName, FileDirectory slash, result name. ! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'ee 11/15/2007 17:23'! readFromFile: fName "Read my samples from the file with the given name." (FileDirectory default fileExists: fName) ifFalse: [^ self inform: 'File not found' withDetails: fName]. snd _ SampledSound fromFileNamed: fName. completeSnd _ snd copy. graph cursor: 200. samplingRate _ snd originalSamplingRate. origSamplingRate _ snd originalSamplingRate. graph data: snd samples. graph scale: 2.0. self setVolume: 0.5. volume setScaledValue: 0.5. graph data size > 5000 ifTrue: [deltaVal _ 50] ifFalse: [deltaVal _ graph data size // 10]. ! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'LY 7/18/2003 14:20'! resetSamplingRate snd ifNil: [^self]. snd setSamples: snd samples samplingRate: origSamplingRate. rateSlider setScaledValue: 1.0.! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'LY 7/18/2003 14:17'! samplingRate: sampleRate "Sets the samplingRate to somewhere btw 0.1 and 2.5 the original samplingRate, given a number btw 0.2 and 2.5 (sampleRate)" snd ifNil: [^self.]. origSamplingRate = 0 ifFalse: [snd setSamples: snd samples samplingRate: (sampleRate*origSamplingRate)] ifTrue: [ snd setSamples: snd samples samplingRate: (sampleRate*10000)]. ! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'ee 6/27/2008 17:45'! saveToFile "Export my samples to a WAV file." | fileName samples f | fileName _ StringDialog ask: 'File name?'. fileName size = 0 ifTrue: [^ self]. (fileName asLowercase endsWith: '.wav') ifFalse: [fileName _ fileName, '.wav']. (graph data isKindOf: SoundBuffer) ifTrue: [samples _ graph data] ifFalse: [samples _ SoundBuffer fromArray: graph data]. f _ (FileStream newFileNamed: fileName) binary. (SampledSound samples: samples samplingRate: samplingRate) storeWAVSamplesOn: f. f close. ! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'LY 7/10/2003 17:16'! setVolume: aFloat "aFloat is a value btw 0 and 1.0" snd ifNil: [^self]. snd adjustVolumeTo: aFloat overMSecs: 50.! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'LY 7/10/2003 16:34'! slower | rate | rate _ rateSlider getScaledValue/1.1. rateSlider setScaledValue: rate. self samplingRate: rate. ! ! !ScratchSoundEditor methodsFor: 'menu' stamp: 'ee 8/13/2003 15:24'! stop snd pause. playCursor _ nil. ! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'jm 5/15/2004 21:54'! client: anObject client _ anObject. ! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'LY 7/25/2003 10:41'! cursor ^cursor.! ! !ScratchSoundEditor methodsFor: 'accessing'! cursor: aNumber graph data ifNil: [^self]. cursor ~= aNumber ifTrue: [ cursor _ ((aNumber truncated max: 1) min: graph data size) truncated. "graph keepIndexInView: cursor." ].! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'LY 7/1/2003 14:14'! data: newData graph data: newData. ! ! !ScratchSoundEditor methodsFor: 'accessing'! endPlaying ^endPlaying.! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'LY 7/1/2003 14:14'! graph ^ graph ! ! !ScratchSoundEditor methodsFor: 'accessing'! playCursor ^playCursor.! ! !ScratchSoundEditor methodsFor: 'accessing'! playCursor: aNumber graph data ifNil: [^self]. "allows the graph to set where the playing cursor is" cursor ~= aNumber ifTrue: [ cursor _ ((aNumber truncated max: 1) min: graph data size) truncated. ].! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'LY 7/1/2003 14:14'! samplingRate ^ samplingRate ! ! !ScratchSoundEditor methodsFor: 'accessing'! scrollTime: relativeValue graph startIndex: relativeValue*(graph data size). viewer flushCachedForm; changed. ! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'LY 7/25/2003 17:54'! selection ^selection.! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'LY 7/25/2003 21:25'! selection: anArrayorNil anArrayorNil ifNil: [selection _ {nil. nil}.] ifNotNil: [selection _ anArrayorNil.]. graph selection: selection. viewer selection: selection! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'LY 7/25/2003 23:19'! selection: aNumber scd: anotherNumber selection _ { aNumber min: anotherNumber. aNumber max: anotherNumber}. graph selection: selection. viewer selection: selection. graph flushCachedForm; changed. viewer flushCachedForm; changed.! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'LY 7/25/2003 17:53'! selectionNil (selection at: 1) ifNil: [^ true]. ^false.! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'LY 7/30/2003 17:03'! slider ^slider.! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'LY 7/18/2003 12:42'! sound ^snd.! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'jm 6/4/2004 10:12'! soundName: aString "Note the sound name to use when saving." soundName _ aString. ! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'LY 7/25/2003 10:16'! startSelection ^ startSelection.! ! !ScratchSoundEditor methodsFor: 'accessing' stamp: 'LY 7/25/2003 10:16'! startSelection: aNumber startSelection _ aNumber.! ! !ScratchSoundEditor methodsFor: 'accessing'! updateSliderValue slider setScaledValue: ((graph startIndex/graph data size min: slider maxVal) max: 0). ! ! !ScratchSoundEditor methodsFor: 'accessing'! viewer ^viewer.! ! !ScratchSoundEditor methodsFor: 'event handling' stamp: 'LY 7/7/2003 21:39'! handlesMouseDown: evt | p | p _ evt cursorPoint. ^ (Rectangle origin: backButton topLeft corner: fwdButton bottomRight) containsPoint: p.! ! !ScratchSoundEditor methodsFor: 'event handling' stamp: 'LY 7/9/2003 12:38'! handlesMouseOver: evt ^true.! ! !ScratchSoundEditor methodsFor: 'event handling' stamp: 'LY 7/25/2003 11:27'! keyStroke: evt | keyVal | keyVal _ evt keyCharacter asciiValue. keyVal = 28 ifTrue: [ cursor _ cursor + (( -10) / graph scale)]. keyVal = 29 ifTrue: [ cursor _ cursor + (10/graph scale)].! ! !ScratchSoundEditor methodsFor: 'event handling' stamp: 'LY 7/8/2003 11:55'! mouseDown: evt | p | "do stuff" p _ evt cursorPoint. (slider containsPoint: p) ifTrue: [ slider descending ifTrue: [scrollDelta _ deltaVal negated.] ifFalse: [scrollDelta _ deltaVal.].]. (backButton containsPoint: p) ifTrue: [ backButton borderInset. scrollDelta _ deltaVal negated ]. (fwdButton containsPoint: p) ifTrue: [ fwdButton borderInset. scrollDelta _ deltaVal].! ! !ScratchSoundEditor methodsFor: 'event handling' stamp: 'LY 7/9/2003 12:40'! mouseEnter: evt evt hand newKeyboardFocus: self.! ! !ScratchSoundEditor methodsFor: 'event handling' stamp: 'LY 7/8/2003 13:16'! mouseUp: evt "do stuff" scrollDelta _ 0. fwdButton borderRaised. backButton borderRaised. ! ! !ScratchSoundEditor methodsFor: 'graph ops' stamp: 'LY 8/2/2003 17:13'! back "Moves the cursor back in the music score." scrollDelta _ -5. self cursor: cursor + scrollDelta.! ! !ScratchSoundEditor methodsFor: 'graph ops' stamp: 'jm 8/23/2003 14:40'! cancel self delete. ! ! !ScratchSoundEditor methodsFor: 'graph ops' stamp: 'LY 7/24/2003 15:34'! copy "copies the current selection if there is one." (selection at: 1) ifNotNil: [copy _ graph data copyFrom: (graph selection at: 1) to: (graph selection at: 2).]! ! !ScratchSoundEditor methodsFor: 'graph ops'! crop | sel | "Crops the sound in the sound editor to be only the selected region" sel _ ((selection at: 1) notNil and: [(selection at: 2) - (selection at: 1) > 3]). sel ifFalse: [^self]. undoSel _ graph selection copy. undoSound _ SampledSound samples: graph data samplingRate: samplingRate. undoCursor _ cursor. undoScale _ graph scale/ScratchGraphMorph MinScale. sel _ graph data copyFrom: (selection at: 1) to: (selection at: 2). graph data: sel. viewer data: graph data. cursor _ ((cursor - (selection at: 1)) truncated max: 1) min: graph data size truncated. "scales the botton viewer so that the data fits on the entire screen." (selection second - selection first)/graph data size >= 0.05 ifTrue: [ScratchGraphMorph MinScale: (viewer extent x/ graph data size min: 1). "the mult factor in all scales, the minimum scale allowed...to get this just do self scale: 1, since this multiplied by input" viewer scale: 1. graph scale: 1. viewer startIndex: 1. graph computeSlider. self fixSliderRange.]. graph calculateDataArray. viewer calculateDataArray. snd setSamples: sel samplingRate: samplingRate. completeSnd setSamples: sel samplingRate: samplingRate. selection at: 1 put: 1. selection at: 2 put: graph data size. ! ! !ScratchSoundEditor methodsFor: 'graph ops'! cut | data1 data2 | (selection at: 1) ifNil: [^ self.]. undoSound _ SampledSound samples: graph data samplingRate: samplingRate. undoSel _ graph selection copy. undoCursor _ cursor. undoScale _ graph scale/ScratchGraphMorph MinScale. self copy. data1 _ graph data copyFrom: 1 to: (selection at: 1). data2 _ graph data copyFrom: (selection at: 2) to: graph data size. graph data: (data1, data2). viewer data: graph data. "scales the botton viewer so that the data fits on the entire screen." ((selection second - selection first)/graph data size asFloat) >= 0.05 ifTrue: [ScratchGraphMorph MinScale: (viewer extent x/ graph data size min: 1). viewer scale: 1. graph scale: undoScale. viewer startIndex: 1. graph computeSlider. self fixSliderRange.]. graph calculateDataArray. viewer calculateDataArray. snd setSamples: graph data samplingRate: samplingRate. completeSnd setSamples: graph data samplingRate: samplingRate. cursor _ (selection at: 1). selection at: 1 put: nil. selection at: 2 put: nil.! ! !ScratchSoundEditor methodsFor: 'graph ops'! fixSliderRange slider maxVal: (1 -(slider sliderThickness/slider extent x)). ^slider! ! !ScratchSoundEditor methodsFor: 'graph ops' stamp: 'LY 7/26/2003 15:32'! fwd "moves the cursor forward in the music." scrollDelta _ 5. self cursor: cursor + 5.! ! !ScratchSoundEditor methodsFor: 'graph ops'! paste | data1 data2 | "inserts the copied data into the dataset where the cursor is." copy ifNil: [^self ]. "self selectionNil ifTrue: [undoSel _ nil.] ifFalse: [undoSel _ selection copy.]." undoSel _ selection copy. undoSound _ SampledSound samples: graph data samplingRate: samplingRate. undoCursor _ cursor. undoScale _ graph scale/ScratchGraphMorph MinScale. ((self selectionNil not) and: [(selection at: 2) - (selection at: 1) > 3]) ifTrue: [data1 _ graph data copyFrom: 1 to: (graph selection at: 1). data2 _ graph data copyFrom: (graph selection at: 2) to: graph data size.] ifFalse: [data1 _ graph data copyFrom: 1 to: graph cursor truncated. data2 _ graph data copyFrom: graph cursor truncated + 1 to: graph data size.]. graph data: (data1, copy, data2). viewer data: graph data. "scales the botton viewer so that the data fits on the entire screen." (copy size)/graph data size >= 0.05 ifTrue: [ScratchGraphMorph MinScale: (viewer extent x/ graph data size min: 1). viewer scale: 1. graph scale: undoScale. self fixSliderRange. viewer startIndex: 1. graph computeSlider.]. graph calculateDataArray. viewer calculateDataArray. snd setSamples: graph data samplingRate: samplingRate. completeSnd setSamples: graph data samplingRate: samplingRate. self selection: {data1 size. (data1 size) + (copy size).}. cursor _ selection at: 2. graph startIndex: (slider getScaledValue)*(graph data size). viewer flushCachedForm. ! ! !ScratchSoundEditor methodsFor: 'graph ops' stamp: 'jm 6/4/2004 10:15'! save snd _ SampledSound samples: completeSnd samples samplingRate: samplingRate. client ifNotNil: [client saveSound: snd name: soundName]. self delete. ! ! !ScratchSoundEditor methodsFor: 'graph ops'! sound: aSampledSound "completeSnd is a version of the sound that is unaffected by the play mechanism. This method is called when a SoundMorph brings up a SoundEditor. soundMorphSound is a copy of the initial sampledSound, used for saving and cancelling edits purposes. It serves as a reference to the original sound so that we can actually change the sound in the soundMorph" soundMorphSound _ aSampledSound. snd _ aSampledSound copy. completeSnd _ snd copy. graph cursor: 200. samplingRate _ snd originalSamplingRate. origSamplingRate _ snd originalSamplingRate. ScratchGraphMorph MinScale: (graph extent x/completeSnd samples size). graph data: completeSnd samples. "SHOULD IT BE COPY?/" graph scale: 2. graph calculateDataArray. "self fixSliderRange. graph computeSlider." viewer data: completeSnd samples. viewer scale: 1. viewer calculateDataArray. self setVolume: snd loudness. volume setScaledValue: snd loudness. (graph data size > 5000) ifTrue: [deltaVal _ 200] ifFalse: [deltaVal _ (graph data size) // 10]. self updateSlider! ! !ScratchSoundEditor methodsFor: 'graph ops' stamp: 'LY 8/2/2003 17:25'! startTime | ss | "secs _ cursor / samplingRate. hrs _ secs // 3600. mins _ (secs \\ 3600) // 60. secs _ secs \\ 60 asFloat. ms _ ((secs \\ 1) * 100) asFloat truncated. secs _ secs // 1." " hrs asString,':',mins asString, ':', secs asString, '.', ms asString." origSamplingRate isNil ifTrue: [ss _ 20000] ifFalse: [ss _ origSamplingRate]. ^ ((cursor asFloat/ss) roundTo: 0.01) asString.! ! !ScratchSoundEditor methodsFor: 'graph ops' stamp: 'jm 2/3/2009 14:27'! startTime: hrMinSecs | secs hrs mins hms | hms _ hrMinSecs findTokens: ' :.'. hrs _ hms at: 1. mins _ hms at: 2. secs _ hms at: 3. self startIndex: ((hrs asNumber * 3600) + (mins asNumber * 60) + secs asNumber) * samplingRate. ! ! !ScratchSoundEditor methodsFor: 'graph ops'! undo | tmpSound tmpSel tmpCursor tmpScale | undoSound ifNil: [^self]. tmpSound _ SampledSound samples: graph data samplingRate: samplingRate. tmpSel _ graph selection copy. tmpCursor _ cursor. tmpScale _ graph scale/ScratchGraphMorph MinScale. graph data: undoSound samples copy. viewer data: graph data. "scales the botton viewer so that the data fits on the entire screen." (tmpSound samples size - graph data size) abs > 3 ifTrue: [ScratchGraphMorph MinScale: ((viewer extent x/ graph data size) min: 1). viewer scale: 1. graph scale: undoScale. viewer startIndex: 1. graph computeSlider. self fixSliderRange.]. graph calculateDataArray. viewer calculateDataArray. snd setSamples: graph data samplingRate: samplingRate. completeSnd setSamples: graph data samplingRate: samplingRate. self selection: undoSel copy. cursor _ undoCursor. undoSound _ tmpSound. undoSel _ tmpSel. undoCursor _ tmpCursor. undoScale _ tmpScale. ! ! !ScratchSoundEditor methodsFor: 'stepping' stamp: 'jm 9/29/2003 20:28'! step | played prev | (SoundPlayer isPlaying: snd) ifTrue: [ played _ ((snd millisecondsSinceStart) * snd samples size) / (1000.0 * snd duration). prev _ (completeSnd samples size - snd samples size). playCursor _ (played + prev truncated min: graph data size). self changed]. time contents: self startTime. scrollDelta = 0 ifFalse: [ graph startIndex: graph startIndex + (scrollDelta/graph scale). self updateSliderValue.]. graph computeSlider. self fixSliderRange. ! ! !ScratchSoundEditor methodsFor: 'stepping' stamp: 'jm 9/29/2003 20:27'! stepTime ^ 150 ! ! !ScratchSoundEditor methodsFor: 'other' stamp: 'LY 7/1/2003 14:14'! normalize: sampleArray "Return a copy of the given sample array scaled to use the maximum 16-bit sample range. Remove any D.C. offset." | max abs scale out | max _ 0. sampleArray do: [:s | s > 0 ifTrue: [abs _ s] ifFalse: [abs _ 0 - s]. abs > max ifTrue: [max _ abs]]. scale _ ((1 << 15) - 1) asFloat / max. out _ sampleArray species new: sampleArray size. 1 to: sampleArray size do: [:i | out at: i put: (scale * (sampleArray at: i)) truncated]. ^ out ! ! !ScratchSoundEditor methodsFor: 'other' stamp: 'LY 7/1/2003 14:14'! stretch: sampleArray by: stretchFactor "Return an array consisting of the given samples \stretched in time by the given factor." | out end incr i frac index | out _ OrderedCollection new: (stretchFactor * sampleArray size) asInteger + 1. end _ (sampleArray size - 1) asFloat. incr _ 1.0 / stretchFactor. i _ 1.0. [i < end] whileTrue: [ frac _ i fractionPart. index _ i truncated. i _ i + incr. out addLast: (((1.0 - frac) * (sampleArray at: index)) + (frac * (sampleArray at: index + 1))) rounded]. ^ out asArray ! ! !ScratchSoundEditor methodsFor: 'other'! updateSlider "this is the order that these methods have to be called in order to update slider!!!!!!" graph computeSlider. self fixSliderRange. self updateSliderValue.! ! !ScratchSoundEditor class methodsFor: 'instance creation' stamp: 'LY 7/1/2003 14:14'! openOn: dataCollection "Open a new WaveEditor on the given sequencable collection of data." ^ (self new data: dataCollection) openInWorld ! ! !ScratchSoundEditor class methodsFor: 'icons' stamp: 'jm 5/6/2004 19:39'! downArrow ^ Form extent: 10@12 depth: 4 fromArray: #(3722304989 3707764736 3722304989 3707764736 3704479196 3170893824 3692739489 3170893824 3550548241 1023410176 3720417563 3707764736 3711570339 3707764736 3722121645 3707764736 3722252605 3707764736 3722296285 3707764736 3722261469 3707764736 3722304989 3707764736) offset: 0@0 ! ! !ScratchSoundEditor class methodsFor: 'icons' stamp: 'jm 5/6/2004 19:43'! upArrow "Uparrow form used in slider." ^ Form extent: 6@3 fromArray: #(2r11e28 2r1111e27 2r111111e26) offset: 0@0 ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'initialization' stamp: 'jm 3/21/2008 14:10'! buttonType: type action: actionSelector "Answer a new button with the given type and selector. The button target will be me and it will use custom forms." | onForm offForm overForm disabledForm | onForm _ ScratchFrameMorph skinAt: ('record', type, 'ButtonPressed'). offForm _ ScratchFrameMorph skinAt: ('record', type, 'Button'). overForm _ ScratchFrameMorph skinAt: ('record', type, 'ButtonOver'). disabledForm _ ScratchFrameMorph skinAt: ('record', type, 'ButtonDisabled'). ^ ToggleButton new isMomentary: false; onForm: onForm offForm: offForm overForm: overForm disabledForm: disabledForm; label: '' font: (ScratchFrameMorph getFont: #SoundRecorderButton); target: self; actionSelector: actionSelector. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'initialization' stamp: 'ee 9/16/2005 10:13'! convertToMmss: aNumber "Answer a string of the form MM:SS. For displaying a time in seconds as hours, minutes, and seconds." | mm ss | mm _ ((aNumber // 60) \\ 60) printString. "mm size < 2 ifTrue: [mm _ '0', mm]." ss _ (aNumber \\ 60) printString. ss size < 2 ifTrue: [ss _ '0', ss]. ^ mm, ':', ss ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'initialization' stamp: 'ee 6/28/2008 14:37'! initialize super initialize. self makeModeButtonsPane. self makeRecordMeter. self makeRecordTimer. recorder _ ScriptableScratchMorph soundRecorder stopRecording. recorder recordLevel: 0.5. savedMeterLevels _ OrderedCollection new. curStep _ 1. duration _ 0. self setMode: #stop. self setMode: #play isDisabled: true. "fixes layout" mainColumn centering: #topLeft. self extent: 380@145. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'initialization' stamp: 'ee 6/26/2008 17:52'! makeModeButtonsPane | button tempBin | modeButtonsPane _ AlignmentMorph newRow vResizing: #shrinkWrap. modeButtonsPane color: Color transparent. #(play stop record) do: [:n | button _ (self buttonType: n asString capitalized action: n). button setProperty: #balloonText toValue: n asString localized. modeButtonsPane addMorphBack: button]. tempBin _ AlignmentMorph newRow color: Color transparent; centering: #center. tempBin addMorphBack: modeButtonsPane; addMorphBack: buttonRow. mainColumn addMorphBack: tempBin. bottomSpacer delete. bottomSpacer _ nil. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'initialization' stamp: 'ee 6/26/2008 17:55'! makeRecordMeter recorderBackgroundLeft _ (ScratchFrameMorph skinAt: #recorderBackgroundLeft). recorderBackgroundCenter _ (ScratchFrameMorph skinAt: #recorderBackgroundCenter). recorderBackgroundRight _ (ScratchFrameMorph skinAt: #recorderBackgroundRight). recordMeter _ ScratchRecordMeter new. mainColumn addMorphFront: (Morph new extent: 5@5; color: Color transparent); addMorphFront: recordMeter. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'initialization' stamp: 'ee 6/26/2008 17:55'! makeRecordTimer "timer" timerMorph _ StringMorph "morph to display the duration" contents: (self convertToMmss: 0) font: (ScratchFrameMorph getFont: #SoundRecorderTimer). timerMorph color: Color darkGray. fileInfoColumn addMorphBack: (Morph new extent: (5@5); color: Color transparent); addMorphBack: timerMorph. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'accessing' stamp: 'ee 6/16/2005 12:08'! client: anObject client _ anObject. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'drawing' stamp: 'ee 6/28/2008 14:35'! drawOn: aCanvas | p clipC lastX | super drawOn: aCanvas. "draw recorder meter background, scalable" recorderBackgroundCenter ifNil: [^ self]. clipC _ aCanvas copyClipRect: self fullBounds. p _ (self bounds topLeft + (20@(recordMeter top - self top - 3))) + (recorderBackgroundLeft width@0). clipC paintImage: recorderBackgroundLeft at: (self bounds topLeft + (20@(recordMeter top - self top - 3))). lastX _ (self bounds right - 66) - (recorderBackgroundRight width). [p x <= lastX] whileTrue: [ clipC paintImage: recorderBackgroundCenter at: p. p _ (p x + recorderBackgroundCenter width) @ p y]. clipC paintImage: recorderBackgroundRight at: ((self bounds right - 66) - recorderBackgroundRight width)@(self bounds top + (recordMeter top - self top - 3)). ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'button commands' stamp: 'jm 5/10/2006 11:37'! cancelled "Cancel button was pressed." recorder pause. recorder clearRecordedSound. super cancelled. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'button commands' stamp: 'ee 6/23/2005 14:33'! play recorder pause. recorder playback. curStep _ 1. self setMode: #play; setMode: #record isDisabled: true. self changed.! ! !ScratchSoundRecorderDialogMorph methodsFor: 'button commands' stamp: 'ee 6/26/2005 01:11'! record recorder isActive ifFalse:[ recorder clearRecordedSound. recorder resumeRecording. savedMeterLevels _ OrderedCollection new. duration _ 0. self setMode: #record; setMode: #play isDisabled: false. self changed]. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'button commands' stamp: 'jm 8/22/2006 07:49'! setMode: aSymbol "Update the highlighting of my tool buttons." | modeButtons | currentMode _ aSymbol. modeButtons _ modeButtonsPane submorphs select: [:m | m isKindOf: ToggleButton]. modeButtons do: [:b | b actionSelector = currentMode ifTrue: [b isDisabled: false. b on] ifFalse: [b off]]. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'button commands' stamp: 'jm 8/22/2006 07:49'! setMode: aSymbol isDisabled: aBoolean | modeButtons | modeButtons _ modeButtonsPane submorphs select: [:m | m isKindOf: ToggleButton]. modeButtons do: [:b | b actionSelector = aSymbol ifTrue: [b isDisabled: aBoolean]]. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'button commands' stamp: 'jm 9/25/2006 11:59'! stop recorder isActive ifTrue: [ recorder pause. recorder trim: 1300 normalizedVolume: 80.0]. self setMode: #stop; setMode: #play isDisabled: false; setMode: #record isDisabled: false. recorder recordedSound ifNil: [duration _ 0.0] ifNotNil: [duration _ recorder recordedSound duration]. self updateTime. self changed. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'button commands' stamp: 'ee 11/12/2007 15:20'! yes | samples samplingRate snd | self stop. samples _ recorder condensedSamples. samplingRate _ recorder samplingRate. samplingRate = 44100 ifTrue: [ samples _ samples downSampledLowPassFiltering: false. samplingRate _ 22050]. snd _ SampledSound samples: samples samplingRate: samplingRate. (client notNil and: [samples size > 0]) ifTrue: [ client saveSound: snd name: 'recording' localized,'1']. recorder clearRecordedSound. super yes. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'stepping' stamp: 'ee 6/16/2005 12:24'! startStepping "Make the level meter active when dropped into the world. Do nothing if already recording. Note that this will cause other recorders to stop recording..." super startStepping. recorder isPaused ifTrue: [ SoundRecorder allSubInstancesDo: [:r | r stopRecording]. "stop all other sound recorders" recorder pause]. "meter is updated while paused" ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'stepping' stamp: 'jm 9/20/2005 16:08'! step | m | recorder isPaused ifTrue: [(currentMode = #play) ifTrue: [ (recorder recordedSound notNil and: [recorder recordedSound isPlaying]) ifTrue: [ duration _ recorder recordedSound millisecondsSinceStart // 1000. (curStep < savedMeterLevels size) ifTrue: [recordMeter setLevel: (savedMeterLevels at: curStep)]. curStep _ curStep + 1. self updateTime] ifFalse: [self stop]] ifFalse: [recordMeter setLevel: 0.0]] ifFalse: [ duration _ duration + 0.2. self updateTime. m _ recorder meterLevel*3. recordMeter setLevel: m. savedMeterLevels add: m.]. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'stepping' stamp: 'ee 6/20/2005 15:58'! stepTime "^ 50" ^ 200. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'stepping' stamp: 'ee 6/20/2005 13:10'! stopStepping "Turn off recording." super stopStepping. recorder stopRecording. ! ! !ScratchSoundRecorderDialogMorph methodsFor: 'stepping' stamp: 'ee 9/16/2005 10:12'! updateTime timerMorph contents: (self convertToMmss: duration rounded).! ! !ScratchSoundRecorderDialogMorph class methodsFor: 'instance creation' stamp: 'ee 6/20/2005 12:47'! forClient: aScriptableScratchMorph "SoundRecorderDialogMorph forClient: " | dialog | dialog _ super new withButtonsForYes: false no: false okay: true cancel: true; client: aScriptableScratchMorph; title: 'Sound Recorder'. ^ dialog getUserResponse! ! !ScratchSpriteLibraryMorph methodsFor: 'dropping/grabbing' stamp: 'jens 6/14/2009 01:54'! acceptDroppingMorph: aMorph event: evt "This method allows re-ordering of sprites in the sprite library area." | insertAfter libraryItems spriteLibrary movedThumbnail mostOverlap mostOverlapIndex sf | ((aMorph isKindOf: LibraryItemMorph) and: [(spriteLibrary _ self ownerThatIsA: ScratchSpriteLibraryMorph) notNil]) ifFalse: [^ super acceptDroppingMorph: aMorph event: evt]. "Set the new position of the morph that just moved" movedThumbnail _ self submorphs detect: [:m | (m target == aMorph target)] ifNone: [^ self]. libraryItems _ self submorphs select: [:m | (m bounds intersects: aMorph bounds) and: [(m ~~ aMorph)]]. "Select the thumbnail with the most overlap with movedThumbnail" libraryItems _ libraryItems sort: [:m1 :m2 | ((m1 bounds intersect: aMorph bounds) area < (m2 bounds intersect: aMorph bounds) area)]. movedThumbnail position: aMorph position. aMorph delete. libraryItems isEmpty ifTrue: [insertAfter _ nil] "end of list" ifFalse: [ mostOverlap _ libraryItems last. (movedThumbnail center x > mostOverlap center x) ifTrue: [ mostOverlapIndex _ self submorphs indexOf: mostOverlap. self submorphCount == mostOverlapIndex ifTrue: [insertAfter _ nil] ifFalse: [insertAfter _ self submorphs at: (mostOverlapIndex + 1)]] ifFalse: [insertAfter _ mostOverlap]]. spriteLibrary insertItem: movedThumbnail before: insertAfter. sf _ self ownerThatIsA: ScratchFrameMorph. sf ifNotNil: [ sf workPane updateSpritesList ]. ! ! !ScratchSpriteLibraryMorph methodsFor: 'dropping/grabbing' stamp: 'tis 11/10/2006 18:07'! insertItem: item1 before: item2orNil "Shuffle my sprite thumbnails. I rearrange my submorphs so that the library is shuffled and layed out properly. I don't bother updating the sprites list which is stored in the stage; I let it update on its own when updateSpritesList is called" | nextItem | item1 == item2orNil ifFalse: [ item2orNil ifNil: [ self addMorphBack: item1] ifNotNil: [ self removeAllMorphsIn: (Array with: item1). self replaceSubmorph: item2orNil by: item1. nextItem _ item2orNil. self submorphsBehind: item1 do: [:s | self replaceSubmorph: s by: nextItem. nextItem _ s]. self addMorphBack: nextItem]]. (self ownerThatIsA: ScratchLibraryMorph) fixLayout. ! ! !ScratchSpriteLibraryMorph methodsFor: 'dropping/grabbing' stamp: 'tis 10/1/2006 15:08'! isPartsBin ^ true ! ! !ScratchSpriteLibraryMorph methodsFor: 'dropping/grabbing' stamp: 'tis 10/1/2006 15:43'! rootForGrabOf: aMorph "I act like a parts bin; answer a new copy of the morph being extracted." | root | root _ aMorph. (root isKindOf: BlockMorph) ifFalse: [^ nil]. [root = self] whileFalse: [ root owner == self ifTrue: [^ root fullCopy]. root _ root owner]. ^ super rootForGrabOf: aMorph ! ! !ScratchSpriteLibraryMorph methodsFor: 'dropping/grabbing' stamp: 'tis 10/1/2006 15:10'! wantsDroppedMorph: aMorph event: evt ^ (aMorph isKindOf: LibraryItemMorph) ! ! I represent a programmable Scratch object. I have a costume, a bitmapped image that can be rotated, scaled, and image-filtered. The raw Form before any rotation or scaling is stored in originalForm. rotatedForm is a cache of the rotated and scaled version of originalForm. rotationStyle has three possible values: normal continuous rotation with direction leftRight for directions with x component < 0, flip the bitmap around the y-axis, otherwise no rotation none don't rotate with direction The leftRight style is useful for side-views of things such as cars, horses, etc. ! !ScratchSpriteMorph methodsFor: 'initialization' stamp: 'ee 8/5/2008 10:17'! initialize super initialize. scalePoint _ 1.0@1.0. rotationDegrees _ 0.0. "clockwise angle of rotation" rotationStyle _ #normal. "#normal, #leftRight, or #none" rotatedForm _ self costumeForm. "cached rotated/scaled copy of costume form" offsetWhenRotated _ 0@0. "offset for rotated form needed to keep rotation center invariant" draggable _ false. penDown _ false. penSize _ 1. penHue _ 133.3. penShade _ 50. self penColor: Color blue. self extent: rotatedForm extent. ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 8/7/2008 13:16'! defaultImageMedia ^ ImageMedia new form: (Form extent: 8@8 depth: 8) fillWhite; mediaName: ('costume' localized, '1') ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 10/19/2007 11:35'! draggable draggable ifNil: [draggable _ true]. ^ draggable ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 10/19/2007 11:35'! draggable: aBoolean draggable _ aBoolean. ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 12/5/2004 10:40'! heading "Answer my heading in degrees, a number between -180 and 180." | result | result _ rotationDegrees + 90. result > 180 ifTrue: [result _ result - 360]. ^ result ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 12/5/2004 10:40'! heading: headingDegrees "Set my heading in degrees, where 0 degrees is facing up and the heading increases clockwise." "Note: Our convention is that artwork is drawing facing to the right. Thus, a heading of 90 corresponds to a rotationDegrees of zero." self rotationDegrees: headingDegrees - 90 ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 5/10/2004 19:37'! isClone: aBoolean isClone _ aBoolean. ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 4/30/2004 19:05'! isPaintable "Answer true if my image can be repainted." ^ true ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 5/6/2004 19:49'! isRotatable "Answer true if my image can be rotated." ^ true ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 7/3/2008 15:14'! isSprite ^ true ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 11/18/2008 17:35'! position: aPoint " self halt." super position: aPoint.! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 5/8/2009 12:11'! referencePosition | p s | p _ (bounds origin + offsetWhenRotated) - ScratchOrigin. "adjust when in Hand in quartersize mode:" ((owner isKindOf: HandMorph) and: [((s _ owner formerOwner) isKindOf: ScratchStageMorph) and: [s isQuarterSize]]) ifTrue: [ "Note: this is not quite right when rotation center is offset" p _ (p * 2) + (240@180)]. ^ p x @ p y negated ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 9/25/2007 13:12'! referencePosition: aPoint "Set my reference position. Avoid infinite or NaN coordinates. Keep on screen." | newX newY p | newX _ aPoint x. newX isNaN ifTrue: [newX _ 0]. newX isInf ifTrue: [newX _ newX sign * 10000]. newY _ aPoint y. newY isNaN ifTrue: [newY _ 0]. newY isInf ifTrue: [newY _ newY sign * 10000]. p _ newX @ newY negated. self position: ScratchOrigin + (p - offsetWhenRotated). self keepOnScreen. ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 6/2/2004 19:17'! rotatedForm "Answer my rotated and scaled form." rotatedForm ifNil: [self costumeChanged]. ^ rotatedForm ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 5/3/2004 20:21'! rotationCenter ^ costume rotationCenter ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 4/30/2004 19:05'! rotationDegrees ^ rotationDegrees ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 12/1/2006 11:27'! rotationDegrees: newRotationDegrees rotationDegrees ~= newRotationDegrees ifTrue: [ rotationDegrees _ newRotationDegrees asFloat \\ 360.0. (rotationStyle = #none) ifFalse: [ self positionTalkBubble. self costumeChanged]]. ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 4/30/2004 19:05'! rotationStyle ^ rotationStyle ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 12/1/2006 11:30'! rotationStyle: aSymbol "Set my rotation style to #normal, #leftRight, or #none. Styles mean: #normal -- smooth 360 degree rotation #leftRight -- flip about the vertical axis #none -- do not rotate" rotationStyle _ aSymbol. self costumeChanged. self positionTalkBubble. ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 7/2/2004 00:45'! scale ^ (100 * scalePoint x) rounded ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 1/4/2006 14:36'! scalePoint ^ scalePoint ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 12/1/2006 11:28'! scalePoint: aPoint scalePoint _ aPoint. self costumeChanged. self positionTalkBubble. ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 5/15/2004 20:20'! xpos ^ self referencePosition x ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 6/19/2004 07:49'! xpos: aNumber | newX | newX _ aNumber isInteger ifTrue: [aNumber] ifFalse: [aNumber asFloat]. self referencePosition: (newX @ self ypos). ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 5/15/2004 20:20'! ypos ^ self referencePosition y ! ! !ScratchSpriteMorph methodsFor: 'accessing' stamp: 'jm 6/19/2004 07:51'! ypos: aNumber | newY | newY _ aNumber isInteger ifTrue: [aNumber] ifFalse: [aNumber asFloat]. self referencePosition: (self xpos @ newY). ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 4/1/2005 13:31'! changePenHueBy: aNumber "Change the pen hue by given number." self setPenHueTo: penHue + aNumber. ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 4/1/2005 13:33'! changePenShadeBy: aNumber "Change the pen shade (lightness) by given number" self setPenShadeTo: penShade + aNumber. ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 3/16/2005 11:31'! changePenSizeBy: aNumber "Change my pen width." self penSize: penSize + aNumber. ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 8/2/2005 18:52'! clearPenTrails "Clear the pen trails layer." | m | (m _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [m clearPenTrails]. ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 2/5/2005 10:52'! penColor ^ penColor ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 4/1/2005 13:29'! penColor: aColor "Set my pen color." | b | penColor _ aColor. penHue _ (penColor hue * 200.0) / 360.0. b _ penColor brightness. b = 1.0 ifTrue: [penShade _ 50.0 + (50.0 * (1.0 - penColor saturation))] ifFalse: [penShade _ 50.0 * b]. ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 2/5/2005 10:52'! penDown ^ penDown ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 8/2/2005 18:52'! penDown: aBoolean | m | penDown _ aBoolean. penColor ifNil: [penColor _ Color black]. "initialize if necessary" penSize ifNil: [penSize _ 1]. "initialize if necessary" (m _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [m penUpOrDownChangeFor: self]. ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 2/5/2005 10:39'! penPosition ^ self referencePosition ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 2/5/2005 10:52'! penSize ^ penSize ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jens 11/18/2008 09:47'! penSize: aNumber "Set my pen width." penSize _ aNumber rounded min: (ScratchFrameMorph workpaneExtent x * 2) max: 1. ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 2/5/2005 11:41'! putPenDown "Put down my drawing pen (i.e. start drawing a pen trail)." self penDown: true. ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 2/5/2005 11:40'! putPenUp "Put up my drawing pen (i.e. stop drawing a pen trail)." self penDown: false. ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 2/10/2008 22:20'! setPenColorFromCostumedNamed: costumeNameOrIndex x: x y: y "Set my pen color from my costume of the given name at the given point. If the point is off the edge, set my color to black." | cName m f pixel | cName _ costumeNameOrIndex. cName isNumber ifTrue: [cName _ self costumeNameFromNumber: costumeNameOrIndex]. m _ media detect: [:el | el isSound not and: [el mediaName caseInsensitiveEqual: cName]] ifNone: [^ self]. f _ m form. f unhibernate. pixel _ ScratchPlugin primInterpolate: f bits width: f width x: (x * 1024) rounded y: ((f height - y) * 1024) rounded. pixel = 0 ifTrue: [self penColor: Color black] ifFalse: [self penColor: (Color colorFromPixelValue: pixel depth: 24)]. ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 4/1/2005 13:30'! setPenHueTo: aNumber "Set the pen hue to given number between 0 and 200. (That makes 100 the 'maximum' distance away from the original color on the color wheel.)" penHue _ aNumber \\ 200. self setPenShadeTo: penShade. "compute and set penColor" ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 4/1/2005 14:22'! setPenShadeTo: aNumber "Set the pen shade (lightness) to given number between 0 and 100. A shade of 0 is black, 100 is white, and 50 is fully saturated color." "Details: We don't want to get all the way to pure black or white, so we divide the range 0..50 by a number > 50 when computing the fraction of the original color to mix with black or white." | normalizeShade scale k | penShade _ aNumber asFloat \\ 200.0. penColor _ Color h: (360.0 * penHue) / 200.0 s: 1.0 v: 1.0. normalizeShade _ penShade > 100.0 ifTrue: [200.0 - penShade] ifFalse: [penShade]. normalizeShade = 50.0 ifTrue: [^ self]. "pure color" scale _ 1.0 / 60.0. k _ 1.0 - (50.0 * scale). normalizeShade < 50.0 ifTrue: [ penColor _ penColor mixed: (scale * normalizeShade) + k with: Color black]. normalizeShade > 50.0 ifTrue: [ penColor _ penColor mixed: 1.0 - (scale * (normalizeShade - 50.0)) with: Color white]. ! ! !ScratchSpriteMorph methodsFor: 'pen ops' stamp: 'jm 8/2/2005 18:52'! stampCostume "Stamp a copy of my current costume on the pen trails layer." | m | self step. "update costume if necessary" (m _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [m stampCostume: self]. ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 12/4/2006 11:54'! bounceOffEdge "Set my direction to bounce off the edge." | myBox edgeBox dirX dirY | owner ifNil: [^ self]. myBox _ self fullBounds. edgeBox _ owner bounds. (edgeBox containsRect: myBox) ifTrue: [^ self]. dirX _ self rotationDegrees degreesToRadians cos. dirY _ self rotationDegrees degreesToRadians sin negated. myBox left < edgeBox left ifTrue: [dirX _ dirX abs]. myBox right > edgeBox right ifTrue: [dirX _ dirX abs negated]. myBox top < edgeBox top ifTrue: [dirY _ dirY abs negated]. myBox bottom > edgeBox bottom ifTrue: [dirY _ dirY abs]. self rotationDegrees: (dirY negated asFloat arcTan: dirX) radiansToDegrees. self position: self position + (myBox amountToTranslateWithin: edgeBox). ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 7/2/2004 00:47'! changeXposBy: aNumber "Move right by the given amount." self referencePosition: self referencePosition + (aNumber@0). ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 7/2/2004 00:47'! changeYposBy: aNumber "Move up by the given amount." self referencePosition: self referencePosition + (0@aNumber). ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 11/30/2006 21:08'! color: sensitiveColor sees: soughtColor "Return true if any of my pixels of sensitiveColor intersect with pixels of soughtColor in the world." | r myImage sensitivePixelsMask map index imageBelowMe | r _ self bounds intersect: owner bounds. r area = 0 ifTrue: [^ false]. "make a mask with 1 where pixel = sensitiveColor, 0 elsewhere" myImage _ self imageForm asFormOfDepth: 16. sensitivePixelsMask _ Form extent: myImage extent depth: 1. map _ Bitmap new: (1 bitShift: (myImage depth min: 15)). map at: (index _ sensitiveColor indexInMap: map) put: 1. sensitivePixelsMask copyBits: ((r origin - self position) extent: r extent) from: myImage form at: 0@0 colorMap: map. "grab an image of the world below me" imageBelowMe _ owner patchAt: r without: self andNothingAbove: false. "intersect world pixels of the color we're looking for with sensitive pixels mask" map at: index put: 0. "clear map and reuse it" map at: (soughtColor indexInMap: map) put: 1. sensitivePixelsMask copyBits: imageBelowMe boundingBox from: imageBelowMe at: 0@0 clippingBox: imageBelowMe boundingBox rule: Form and fillColor: nil map: map. ^ (sensitivePixelsMask tallyPixelValues at: 2) > 0 ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 6/8/2009 10:09'! directionMenu "Provides a drop-down menu for setting the sprite direction." | menu | menu _ CustomMenu new. #( ('right' 90) ('left' -90) ('up' 0) ('down' 180) ) do: [:pair | menu add: '(' asUTF8, pair second printString, ') ', pair first localized action: pair second]. ^ menu ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 3/28/2009 20:15'! distanceTo: anObject "Answer the distance to the given sprite." | aSpriteOrSymbol | aSpriteOrSymbol _ self coerceSpriteArg: anObject. aSpriteOrSymbol = #mouse ifTrue: [ ^ ((self mouseX @ self mouseY) - self referencePosition) r]. (aSpriteOrSymbol isKindOf: self class) ifFalse: [^ 10000]. ^ (aSpriteOrSymbol referencePosition - self referencePosition) r ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jens 11/21/2008 09:49'! forward: distance "Move the object forward (i.e., the direction of its heading) by the given distance. Avoid infinite or NaN coordinates" | radians deltaP newPos newX newY | radians _ rotationDegrees degreesToRadians. deltaP _ ((radians cos)@(radians sin)) * distance. newPos _ self position + deltaP. newX _ newPos x. newY _ newPos y. newX isNaN ifTrue: [newX _ 0]. newX isInf ifTrue: [newX _ newX sign * 10000]. newY isNaN ifTrue: [newY _ 0]. newY isInf ifTrue: [newY _ newY sign * 10000]. self position: newX @ newY. self keepOnScreen. ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 12/14/2005 19:46'! glideSecs: duration toX: endX y: endY elapsed: elapsed from: startPoint "Interpolate my position from my current postion to the given x and y over the given duration. If startPoint is nil, just answer my current position; this sets the starting point for the interpolation." | fraction endPoint | startPoint ifNil: [^ self referencePosition]. endPoint _ endX@endY. duration < 0.001 ifTrue: [fraction _ 1] ifFalse: [fraction _ elapsed asFloat / (1000.0 * duration)]. fraction > 1.0 ifTrue: [fraction _ 1]. fraction < 0.0 ifTrue: [fraction _ 0]. self referencePosition: startPoint + (fraction * (endPoint - startPoint)) truncated. ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 3/28/2009 20:15'! gotoSpriteOrMouse: anObject "Go to the given sprite or mouse position." | aSpriteOrSymbol p | aSpriteOrSymbol _ self coerceSpriteArg: anObject. aSpriteOrSymbol = #mouse ifTrue: [^ self gotoX: self mouseX y: self mouseY]. (aSpriteOrSymbol isKindOf: self class) ifFalse: [^ self]. p _ aSpriteOrSymbol referencePosition. self gotoX: p x y: p y. ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 5/10/2004 16:31'! gotoX: x y: y self referencePosition: x@y. ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 10/12/2005 22:08'! gotoX: endX y: endY duration: duration elapsed: elapsed from: startPoint "Interpolate my position from startPoint to endPoint over the given duration. If startPoint is nil, just answer my current position; this sets the starting point for the interpolation." | fraction endPoint | startPoint ifNil: [^ self referencePosition]. endPoint _ endX@endY. duration < 0.001 ifTrue: [fraction _ 1] ifFalse: [fraction _ elapsed asFloat / (1000.0 * duration)]. fraction > 1.0 ifTrue: [fraction _ 1]. fraction < 0.0 ifTrue: [fraction _ 0]. self referencePosition: startPoint + (fraction * (endPoint - startPoint)) truncated. ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 5/10/2004 19:40'! isClone ^ isClone ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 5/26/2005 16:26'! isOnEdge "Answer true if I'm touching the edge of my owner." owner ifNil: [^ false]. ^ (owner bounds containsRect: self bounds) not ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 6/9/2004 09:23'! pointToX: x y: y "Set my heading to point at the given point." | delta angle | delta _ (x@y) - self referencePosition. angle _ (delta x abs < 0.001) ifTrue: [ delta y < 0 ifTrue: [90] ifFalse: [270]] ifFalse: [ ((delta x >= 0 ifTrue: [0] ifFalse: [180]) - ((delta y / delta x) arcTan * 57.2957795131)) rounded]. self rotationDegrees: angle. ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 3/28/2009 20:16'! pointTowards: anObject "Point toward the given sprite." | aSpriteOrSymbol p | aSpriteOrSymbol _ self coerceSpriteArg: anObject. aSpriteOrSymbol = #mouse ifTrue: [^ self pointToX: self mouseX y: self mouseY]. (aSpriteOrSymbol isKindOf: self class) ifFalse: [^ self]. p _ aSpriteOrSymbol referencePosition. self pointToX: p x y: p y. ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 3/28/2009 20:17'! touching: anObject "Answer true if any visible part of me touches a visible part of the given sprite." "Details: Currently uses the bounding box; should follow this up with comparison of visible pixels." | aSpriteOrSymbol stage intersection f1 f2 map oldVis | aSpriteOrSymbol _ self coerceSpriteArg: anObject. aSpriteOrSymbol = #mouse ifTrue: [ (stage _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ false]. ^ self containsPoint: stage adjustedCursorPoint]. aSpriteOrSymbol = #edge ifTrue: [^ self isOnEdge]. (aSpriteOrSymbol isKindOf: self class) ifFalse: [^ false]. (self isHidden not and: [aSpriteOrSymbol isHidden not]) ifFalse: [^ false]. intersection _ self bounds intersect: aSpriteOrSymbol bounds. (intersection width > 0 and: [intersection height > 0]) ifFalse: [^ false]. f1 _ Form extent: intersection extent depth: 2. f2 _ f1 deepCopy. oldVis _ self visibility. self visibility: 100. self drawOn: ((FormCanvas on: f1) copyOffset: intersection topLeft negated). self visibility: oldVis. oldVis _ aSpriteOrSymbol visibility. aSpriteOrSymbol visibility: 100. aSpriteOrSymbol drawOn: ((FormCanvas on: f2) copyOffset: intersection topLeft negated). aSpriteOrSymbol visibility: oldVis. map _ Bitmap new: 4 withAll: 1. map at: 1 put: 0. "transparent" f1 copyBits: f1 boundingBox from: f1 at: 0@0 colorMap: map. "make mask with 0 where transparent, 1 elsewhere" f2 copyBits: f2 boundingBox from: f2 at: 0@0 colorMap: map. "ditto for other sprite image" f2 displayOn: f1 at: 0@0 rule: Form and. "and the masks together" ^ (f1 tallyPixelValues at: 1) < (f1 width * f1 height) "are any pixels of the result non-zero?" ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'ee 7/2/2008 14:10'! touchingColor: soughtColor "Answer true if any of my non-transparent pixels touch pixels of the given color in the world." | r myImage sensitivePixelsMask map imageBelowMe | r _ self bounds intersect: owner bounds. r area = 0 ifTrue: [^ false]. "make a mask with 0 where transparent, 1 elsewhere" myImage _ self imageForm asFormOfDepth: 16. sensitivePixelsMask _ Form extent: myImage extent depth: 1. map _ Bitmap new: (1 bitShift: (myImage depth min: 15)). map atAllPut: 1. map at: (Color transparent indexInMap: map) put: 0. sensitivePixelsMask copyBits: ((r origin - self position) extent: r extent) from: myImage form at: 0@0 colorMap: map. "grab an image of the world below me" imageBelowMe _ owner patchAt: r withoutWatchersAnd: self andNothingAbove: false. "intersect world pixels of the color we're looking for with sensitive pixels mask" map atAllPut: 0. "clear map and reuse it" map at: (soughtColor indexInMap: map) put: 1. sensitivePixelsMask copyBits: imageBelowMe boundingBox from: imageBelowMe at: 0@0 clippingBox: imageBelowMe boundingBox rule: Form and fillColor: nil map: map. ^ (sensitivePixelsMask tallyPixelValues at: 2) > 0 "true if any pixels are 1" ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 4/30/2004 19:05'! turn: degrees "Turn clockwise the given number of degrees." self rotationDegrees: rotationDegrees + degrees. ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 12/2/2005 14:03'! turnAwayFromEdge "Turn away from the nearest edge." | dirX dirY refP delta | dirX _ self rotationDegrees degreesToRadians cos. dirY _ self rotationDegrees degreesToRadians sin negated. refP _ self referencePosition. delta _ (ScratchFrameMorph workpaneExtent // 2) - refP abs. (delta x < delta y) | (delta x < 0) ifTrue: [ "point dirX towards center" dirX = 0.0 ifTrue: [dirX _ 0.1]. refP x > 0 ifTrue: [dirX _ dirX abs negated] ifFalse: [dirX _ dirX abs]]. (delta y < delta x) | (delta y < 0) ifTrue: [ "point dirY towards center" dirY = 0.0 ifTrue: [dirY _ 0.1]. refP y > 0 ifTrue: [dirY _ dirY abs negated] ifFalse: [dirY _ dirY abs]]. self rotationDegrees: (dirY negated asFloat arcTan: dirX) radiansToDegrees. ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 6/9/2004 09:09'! turnLeft: degrees "Turn counter-clockwise the given number of degrees." self rotationDegrees: rotationDegrees - degrees. ! ! !ScratchSpriteMorph methodsFor: 'motion ops' stamp: 'jm 6/9/2004 09:11'! turnRight: degrees "Turn clockwise the given number of degrees." self rotationDegrees: rotationDegrees + degrees. ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 6/22/2009 20:16'! bubble: obj thinkFlag: thinkFlag promptFlag: promptFlag "Make a talk bubble with the given string." | s talkBubble | self sayNothing. obj isNumber ifTrue: [ obj isInteger ifTrue: [s _ obj printString] ifFalse: [s _ (obj asFloat roundTo: 0.01) printString]] ifFalse: [(obj isKindOf: Boolean) ifTrue: [s _ obj asString localized] ifFalse: [s _ obj asString]]. (s skipDelimiters: ' ' startingAt: 1) > s size ifTrue: [^ self]. s size < 5 ifTrue: [s _ s, ' ']. talkBubble _ ScratchTalkBubbleMorph new message: s. thinkFlag ifTrue: [talkBubble beThoughtBubble: true]. promptFlag ifTrue: [talkBubble bePrompt: true]. talkBubble lock; position: self position. self setProperty: #talkBubble toValue: talkBubble. self addMorphFront: talkBubble. self positionTalkBubble. World displayWorldSafely. ^ talkBubble ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 12/1/2006 11:23'! changeSizeBy: delta "Change my size by the given delta." | currentScale | currentScale _ 100.0 * (scalePoint x max: scalePoint y). self setSizeTo: (currentScale + delta). ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 12/1/2006 11:23'! changeStretchBy: delta "Change my aspect ratio by the given amount." | currentStretch | currentStretch _ (100.0 * scalePoint x) / scalePoint y. self setStretchTo: (currentStretch + delta). ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 5/10/2004 16:51'! goBackByLayers: aNumber "Decrease my layer by (i.e., move me towards the back by) the given number of layers." owner ifNil: [^ self]. self layer: (owner submorphs indexOf: self) + aNumber truncated. ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 5/26/2005 16:00'! hide "Make myself invisible." self isHidden: true. ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 5/10/2004 16:54'! layer: aNumber | n submorphsMinusMe newSubmorphs | owner ifNil: [^ 1]. n _ (aNumber rounded max: 1) min: owner submorphCount. submorphsMinusMe _ owner submorphs copyWithout: self. newSubmorphs _ (submorphsMinusMe copyFrom: 1 to: (n - 1 min: submorphsMinusMe size)), (Array with: self), (submorphsMinusMe copyFrom: n to: submorphsMinusMe size). owner privateSubmorphs: newSubmorphs. self changed. ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 12/1/2006 11:22'! lookLike: costumeName super lookLike: costumeName. self positionTalkBubble. ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 12/1/2006 11:22'! multiplySizeBy: factor "Used by the magnifying glass tool. Multiply my scale by the given factor." self setSizeTo: 100.0 * (self scalePoint x max: self scalePoint y) * factor. ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'nb 1/7/2008 13:00'! recordScene: sceneName | state | self isHidden ifTrue: [ sceneStates removeKey: sceneName ifAbsent: []. ^ self ]. state _ Dictionary new. state at: #referencePosition put: self referencePosition. state at: #size put: self scale. state at: #heading put: self heading. sceneStates at: sceneName put: state. ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'ee 3/1/2009 12:30'! say: aValue "Present a talk bubble with the given string." self bubble: aValue thinkFlag: false promptFlag: false. ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 12/1/2006 19:21'! say: stringOrNum duration: durationSecs elapsed: elapsedMSecs from: startPoint startPoint ifNil: [^ self say: stringOrNum]. "first call, show talk bubble" elapsedMSecs >= (1000 * durationSecs) ifTrue: [self sayNothing]. "clear bubble" ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 4/30/2004 19:05'! sayNothing "Delete my talk bubble if I have one." | talkBubble | (talkBubble _ self valueOfProperty: #talkBubble) ifNil: [^ self]. talkBubble delete. self setProperty: #talkBubble toValue: nil. ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'nb 1/7/2008 12:20'! scenes ^ sceneStates keys ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'nb 1/7/2008 14:07'! setScene: sceneName | sceneState | sceneState _ sceneStates at: sceneName ifAbsent: [^ self hide]. self show. self referencePosition: (sceneState at: #referencePosition). self setSizeTo: (sceneState at: #size). self heading: (sceneState at: #heading). ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 10/28/2007 17:52'! setSizeTo: percent "Set my size to the given percent of the original size. Limit the range to avoid accidentally making me invisibly tiny or really huge. Retain my aspect ratio (i.e., my stretch)." | origExtent minExtent maxExtent minScale maxScale | origExtent _ self costumeForm extent asFloatPoint. minExtent _ (origExtent min: 5@5) asFloatPoint. maxExtent _ ScratchFrameMorph workpaneExtent asFloatPoint * 1.5. minScale _ ((minExtent x / origExtent x) max: (minExtent y / origExtent y)) min: 1.0. maxScale _ (maxExtent x / origExtent x) min: (maxExtent y / origExtent y). self scalePoint: ((percent / 100.0) within: minScale and: maxScale) asPoint. "old code that maintained the aspect ratio (stretch): stretchFactor _ scalePoint x / scalePoint y. origExtent _ self costumeForm extent. minScale _ (8.0 / (stretchFactor * origExtent x)) max: (8.0 / origExtent y). .min size 8 pixels. maxScale _ (500.0 / (stretchFactor * origExtent x)) min: (500.0 / origExtent y). .max size 500 pixels. newScale _ (percent / (100.0 * stretchFactor)) within: minScale and: maxScale. self scalePoint: newScale * (stretchFactor @ 1.0). xxx" ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 12/1/2006 11:29'! setStretchTo: percent "Set x stretch percent relative to y. For example, a stretch of 200 stretches x by a factor of two relative to y while a stretch of 50 compresses x by a factor of two. As my stretch is changed, my x scale changes but my y scale remains the same." | baseScale origExtent minStretch maxStretch newStretchFactor | baseScale _ scalePoint y. origExtent _ self costumeForm extent. minStretch _ 8.0 / (baseScale * origExtent x). "min width 8 pixels" maxStretch _ 500.0 / (baseScale * origExtent x). "max width 500 pixels" newStretchFactor _ ((percent max: 0) / 100.0) within: minStretch and: maxStretch. self scalePoint: baseScale * (newStretchFactor @ 1.0). ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 5/26/2005 16:00'! show "Make myself visible." self isHidden: false. ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'ee 3/1/2009 12:30'! think: aValue "Present a thought bubble with the given string." self bubble: aValue thinkFlag: true promptFlag: false. ! ! !ScratchSpriteMorph methodsFor: 'looks ops' stamp: 'jm 12/1/2006 19:21'! think: stringOrNum duration: durationSecs elapsed: elapsedMSecs from: startPoint startPoint ifNil: [^ self think: stringOrNum]. "first call, show think bubble" elapsedMSecs >= (1000 * durationSecs) ifTrue: [self sayNothing]. "clear bubble" ! ! !ScratchSpriteMorph methodsFor: 'movie ops' stamp: 'tis 3/2/2007 23:23'! stopPlaying "Reset my transient state, such as pen down." super stopPlaying. self sayNothing. ! ! !ScratchSpriteMorph methodsFor: 'sensing ops' stamp: 'ee 11/9/2007 14:48'! attributeNames ^ #('x position' 'y position' 'direction' 'costume #' 'size' 'volume') collect: [:s | s] ! ! !ScratchSpriteMorph methodsFor: 'sensing ops' stamp: 'jm 11/6/2007 13:05'! getAttribute: attr "Answer the value of my variable or built-in attribute with the given name. Answer zero if I have no attribute or variable with the given name." | a | (vars includesKey: attr) ifTrue: [^ vars at: attr]. a _ attr localized. 'x position' localized = a ifTrue: [^ self xpos]. 'y position' localized = a ifTrue: [^ self ypos]. 'direction' localized = a ifTrue: [^ self heading]. 'costume #' localized = a ifTrue: [^ self costumeIndex]. 'size' localized = a ifTrue: [^ self scale]. 'volume' localized = a ifTrue: [^ self volume]. ^ 0 ! ! !ScratchSpriteMorph methodsFor: 'sensing ops' stamp: 'jm 2/11/2009 09:57'! hideQuestion "Hide my question prompt." ! ! !ScratchSpriteMorph methodsFor: 'sensing ops' stamp: 'ee 3/1/2009 12:29'! showQuestion: aString "Show the given question prompt." self bubble: aString thinkFlag: false promptFlag: true. ! ! !ScratchSpriteMorph methodsFor: 'clone ops' stamp: 'jm 6/6/2008 14:29'! cloneAndSend: msgName "Clone this sprite and send the clone (and only the clone) the given message." | frame clone | (frame _ self ownerThatIsA: ScratchFrameMorph) ifNil: [^ self]. clone _ self fullCopy. clone objName: objName, ' clone'. clone isClone: true. clone blocksBin allMorphsDo: [:m | (m isKindOf: BlockMorph) ifTrue: [m stop]]. frame workPane addMorph: clone. clone eventReceived: (ScratchEvent new name: msgName asString argument: 0). ! ! !ScratchSpriteMorph methodsFor: 'drawing' stamp: 'jm 1/8/2006 18:42'! drawOn: aCanvas "Draw myself if my visibility is > 0. If my visibility is 1, draw using the normal 'paint' mode. Otherwise, draw using 'alpha' resulting in a partially transparent rendering." | f alpha | f _ self filteredForm. visibility < 100 ifTrue: [ visibility > 0 ifTrue: [ alpha _ ((255.0 * visibility) / 100.0) truncated. aCanvas paintImage: f at: bounds origin sourceRect: f boundingBox alpha: alpha]. ^ self]. aCanvas paintImage: f at: bounds origin. ! ! !ScratchSpriteMorph methodsFor: 'drawing' stamp: 'jm 7/9/2008 10:12'! drawSubmorphsOn: aCanvas "Display submorphs back to front, but do not draw and talk/think bubble submorphs." submorphs reverseDo: [:m | (m isKindOf: ScratchTalkBubbleMorph) ifFalse: [ aCanvas fullDrawMorph: m]]. ! ! !ScratchSpriteMorph methodsFor: 'drawing' stamp: 'jm 7/9/2008 10:12'! drawTalkBubbleOn: aCanvas "Draw and talk/think bubble submorphs." submorphs size = 0 ifTrue: [^ self]. submorphs reverseDo: [:m | (m isKindOf: ScratchTalkBubbleMorph) ifTrue: [ aCanvas fullDrawMorph: m]]. ! ! !ScratchSpriteMorph methodsFor: 'geometry' stamp: 'jm 4/30/2004 19:05'! containsPoint: aPoint ^ (self bounds containsPoint: aPoint) and: [(self rotatedForm isTransparentAt: aPoint - bounds origin) not] ! ! !ScratchSpriteMorph methodsFor: 'event handling' stamp: 'jm 1/2/2006 12:31'! justDroppedInto: newOwner event: evt super justDroppedInto: newOwner event: evt. (newOwner isKindOf: ScratchStageMorph) ifTrue: [self positionTalkBubble]. ! ! !ScratchSpriteMorph methodsFor: 'right button menu' stamp: 'jm 8/3/2008 13:14'! grabFromScreen "Set my form to be a rectangular portion of the screen." | f el | (f _ self grabFormFromScreen) ifNil: [^ self]. el _ ImageMedia new form: f. el mediaName: (self unusedMediaNameFromBaseName: 'costume1'). media addLast: el. self lookLike: el mediaName. self updateMediaCategory. ! ! !ScratchSpriteMorph methodsFor: 'right button menu' stamp: 'jens 11/28/2008 15:25'! rightButtonMenu "Present the right button menu." | menu | menu _ CustomMenu new. menu add: 'grab screen region for new costume' action: #grabFromScreen. menu add: 'export this sprite' action: #exportObject. menu addLine. menu add: 'duplicate' action: #duplicate. menu add: 'delete' action: #undoableDeleteSprite. menu addLine. menu add: 'resize this sprite' action: #resizeHandle. menu add: 'rotate this sprite' action: #rotateHandle. menu localize; invokeOn: self. ! ! !ScratchSpriteMorph methodsFor: 'object i/o' stamp: 'nb 1/7/2008 14:00'! fieldsVersion ^ 3 ! ! !ScratchSpriteMorph methodsFor: 'object i/o' stamp: 'jm 7/8/2008 06:12'! initFieldsFrom: anObjStream version: classVersion super initFieldsFrom: anObjStream version: classVersion. self initFieldsNamed: #( visibility scalePoint rotationDegrees rotationStyle ) from: anObjStream. scalePoint ifNil: [scalePoint _ 1.0@1.0]. offsetWhenRotated _ 0@0. draggable _ false. "default for old sprites" self layoutChanged. classVersion = 1 ifTrue: [^ self]. "fields added in version 2" self initFieldsNamed: #( volume tempoBPM draggable ) from: anObjStream. classVersion = 2 ifTrue: [^ self]. "fields added in version 3" self initFieldsNamed: #( sceneStates lists ) from: anObjStream. lists ifNil: [lists _ Dictionary new]. "work around" ! ! !ScratchSpriteMorph methodsFor: 'object i/o' stamp: 'jm 5/12/2008 12:00'! storeFieldsOn: anObjStream | oldP | self sayNothing. "delete talk bubble before saving" "for backward compatability, remove offsetWhenRotated and subtract costume rotationCenter when saving" oldP _ self position. self position: self position + offsetWhenRotated - costume rotationCenter. super storeFieldsOn: anObjStream. self storeFieldsNamed: #( visibility scalePoint rotationDegrees rotationStyle volume tempoBPM draggable sceneStates lists ) on: anObjStream. self position: oldP. "restore position" ! ! !ScratchSpriteMorph methodsFor: 'private' stamp: 'jm 12/15/2006 11:48'! copyForExport "Answer a copy of me for exporting." "Note: Sprites are always exported in the context of an empty background." | objToExport | objToExport _ ScratchStageMorph new clearMediaAndCostume. objToExport position: owner position. objToExport addMorph: super copyForExport. ^ objToExport ! ! !ScratchSpriteMorph methodsFor: 'private' stamp: 'jm 10/16/2007 18:21'! generateRotatedForm "Compute my rotatedForm and offsetWhenRotated." | adjustedAngle srcForm smoothPix pair | rotationStyle = #normal ifTrue: [adjustedAngle _ rotationDegrees] "smooth rotation" ifFalse: [adjustedAngle _ 0.0]. "leftRight or none" srcForm _ self costumeForm. ((srcForm width = 1) & (srcForm height = 1)) ifTrue: [adjustedAngle _ 0.0]. "don't rotate a 1x1 costume" ((adjustedAngle = 0.0) and: [1.0@1.0 = scalePoint]) ifTrue: [ "no rotation or scaling; use original" rotatedForm _ srcForm. offsetWhenRotated _ costume rotationCenter] ifFalse: [ "generated rotated and/or scaled form" (((adjustedAngle rounded \\ 90) = 0) and: [1.0@1.0 = scalePoint]) ifTrue: [ ^ self rotateByFlipping]. ((scalePoint x < 1.0) or: [scalePoint y < 1.0]) ifTrue: [smoothPix _ 2] ifFalse: [smoothPix _ 1]. pair _ WarpBlt rotate: srcForm degrees: adjustedAngle negated center: costume rotationCenter scaleBy: scalePoint smoothing: smoothPix. rotatedForm _ pair first. offsetWhenRotated _ (costume rotationCenter + pair last) rounded]. ((rotationStyle = #leftRight) and: [(rotationDegrees > 90.0) and: [rotationDegrees < 270.0]]) ifTrue: [ "headed left; use flipped" rotatedForm _ rotatedForm flipBy: #horizontal centerAt: 0@0. offsetWhenRotated _ (rotatedForm width - offsetWhenRotated x - 1) @ offsetWhenRotated y]. ! ! !ScratchSpriteMorph methodsFor: 'private' stamp: 'jm 8/3/2008 13:13'! grabFormFromScreen "Grab a rectangular portion of the screen selected by the user and trim white pixels from around edges (this assumes the stage is white). If the resulting form is empty, return nil." | f f2 | f _ Form fromUser. f2 _ (Form extent: (f extent + 2) depth: f depth) fillWhite. f displayOn: f2 at: 1@1 rule: Form over. f2 shapeFill: Color transparent interiorPoint: 0@0. f _ f2 trimBordersOfColor: Color transparent. (f width = 0) | (f height = 0) ifTrue: [^ nil]. ^ f ! ! !ScratchSpriteMorph methodsFor: 'private' stamp: 'jm 5/27/2005 22:48'! keepOnScreen "Make me stick to edge of my owner." | edgeBox | owner ifNil: [^ self]. (owner bounds containsRect: bounds) ifTrue: [^ self]. "entirely on screen" edgeBox _ owner bounds insetBy: (18 min: (bounds width min: bounds height) // 2). (bounds intersects: edgeBox) ifTrue: [^ self]. self right < edgeBox left ifTrue: [self right: edgeBox left]. self left > edgeBox right ifTrue: [self left: edgeBox right]. self bottom < edgeBox top ifTrue: [self bottom: edgeBox top]. self top > edgeBox bottom ifTrue: [self top: edgeBox bottom]. ! ! !ScratchSpriteMorph methodsFor: 'private' stamp: 'jm 1/8/2006 18:33'! layoutChanged "Update rotatedForm and offsetWhenRotated and compute new bounds." | refPoint | self changed. refPoint _ bounds origin + offsetWhenRotated. (rotationStyle == #none and: [scalePoint = (1.0@1.0)]) ifTrue: [ "zero rotation and scale; use original Form" rotatedForm _ self costumeForm. offsetWhenRotated _ costume rotationCenter] ifFalse: [self generateRotatedForm]. "compute the new rotatedForm and offsetWhenRotated" bounds _ (refPoint - offsetWhenRotated) extent: rotatedForm extent. super layoutChanged. self changed. ! ! !ScratchSpriteMorph methodsFor: 'private' stamp: 'jm 11/29/2007 17:17'! positionTalkBubble | bubble stage stageBounds y f r yInset strip x | (bubble _ self valueOfProperty: #talkBubble) ifNil: [^ self]. (stage _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ self]. stageBounds _ stage bounds. f _ self imageForm. r _ f rectangleEnclosingPixelsNotOfColor: Color transparent. yInset _ 0. ((r area = 0) or: [r height < 10]) ifTrue: [r _ f boundingBox] ifFalse: [ yInset _ (r top max: 0) min: (r height - 10). strip _ f copy: ((0@r top) extent: (f width@(10 min: f height))). r _ strip rectangleEnclosingPixelsNotOfColor: Color transparent]. x _ self right - ((f width - r right) min: f width // 2). bubble pointLeft: true. ((x + bubble width) <= stageBounds right) ifFalse: [ x _ (self left - bubble width) + (r left min: (f width // 2)). bubble pointLeft: false]. y _ (self top + yInset + 10 - bubble height) within: stageBounds top and: (stageBounds bottom - bubble height). bubble position: x@y. self layoutChanged. ! ! !ScratchSpriteMorph methodsFor: 'private' stamp: 'jm 10/16/2007 19:06'! rotateByFlipping "Compute my rotatedForm and offsetWhenRotated for unscaled rotation by a multiple of 90 degrees." | a center srcForm | a _ rotationDegrees rounded \\ 360. a < 0 ifTrue: [a _ a + 360]. srcForm _ self costumeForm. center _ costume rotationCenter. 0 = a ifTrue: [ rotatedForm _ srcForm. offsetWhenRotated _ center]. 90 = a ifTrue: [ rotatedForm _ srcForm rotateBy: #right centerAt: 0@0. offsetWhenRotated _ (srcForm height - 1 - center y) @ center x]. 180 = a ifTrue: [ rotatedForm _ srcForm rotateBy: #pi centerAt: 0@0. offsetWhenRotated _ srcForm extent - 1 - center]. 270 = a ifTrue: [ rotatedForm _ srcForm rotateBy: #left centerAt: 0@0. offsetWhenRotated _ center y @ (srcForm width - 1 - center x)]. ! ! !ScratchSpriteMorph methodsFor: 'private' stamp: 'jm 6/2/2004 19:17'! rotationDegrees: newRotationDegrees scalePoint: newScalePoint ((newRotationDegrees ~= rotationDegrees) or: [scalePoint ~= newScalePoint]) ifTrue: [ rotationDegrees _ newRotationDegrees asFloat \\ 360.0. scalePoint _ newScalePoint. self costumeChanged]. ! ! !ScratchSpriteMorph methodsFor: 'handle ops' stamp: 'jens 10/27/2008 10:58'! resizeHandle SpriteHandleMorph resize: self! ! !ScratchSpriteMorph methodsFor: 'handle ops' stamp: 'jens 10/27/2008 12:53'! rotateHandle SpriteHandleMorph rotate: self! ! !ScratchSpriteMorph class methodsFor: 'block specs' stamp: 'ee 4/16/2009 17:31'! blockSpecs | blocks | blocks _ #( 'motion' ('move %n steps' - forward:) ('turn %n degrees' - turnRight: 15) "icon shows turn direction" ('turn %n degrees' - turnLeft: 15) "icon shows turn direction" - ('point in direction %d' - heading: 90) ('point towards %m' - pointTowards:) - ('go to x:%n y:%n' - gotoX:y: 0 0) ('go to %m' - gotoSpriteOrMouse:) ('glide %n secs to x:%n y:%n' t glideSecs:toX:y:elapsed:from: 1 50 50) - ('change x by %n' - changeXposBy: 10) ('set x to %n' - xpos: 0) ('change y by %n' - changeYposBy: 10) ('set y to %n' - ypos: 0) - ('if on edge, bounce' - bounceOffEdge) - ('x position' r xpos) ('y position' r ypos) ('direction' r heading) 'pen' ('clear' - clearPenTrails) - ('pen down' - putPenDown) ('pen up' - putPenUp) - ('set pen color to %c' - penColor:) ('change pen color by %n' - changePenHueBy:) ('set pen color to %n' - setPenHueTo: 0) - ('change pen shade by %n' - changePenShadeBy:) ('set pen shade to %n' - setPenShadeTo: 50) - ('change pen size by %n' - changePenSizeBy: 1) ('set pen size to %n' - penSize: 1) - ('stamp' - stampCostume) ). blocks _ blocks, #( 'looks' ('switch to costume %l' - lookLike:) ('next costume' - nextCostume) ('costume #' r costumeIndex) - ('say %s for %n secs' t say:duration:elapsed:from: 'Hello!!' 2) ('say %s' - say: 'Hello!!') ('think %s for %n secs' t think:duration:elapsed:from: 'Hmm...' 2) ('think %s' - think: 'Hmm...') - ('change %g effect by %n' - changeGraphicEffect:by: 'color' 25) ('set %g effect to %n' - setGraphicEffect:to: 'color' 0) ('clear graphic effects' - filterReset) - ('change size by %n' - changeSizeBy:) ('set size to %n%' - setSizeTo: 100) ('size' r scale) - ('show' - show) ('hide' - hide) - ('go to front' - comeToFront) ('go back %n layers' - goBackByLayers: 1) 'sensing' ('touching %m?' b touching:) ('touching color %C?' b touchingColor:) ('color %C is touching %C?' b color:sees:) - ('ask %s and wait' s doAsk 'What''s your name?') ('answer' r answer) - ('mouse x' r mouseX) ('mouse y' r mouseY) ('mouse down?' b mousePressed) - ('key %k pressed?' b keyPressed: 'space') - ('distance to %m' r distanceTo:) - ('reset timer' - timerReset) ('timer' r timer) - ('%a of %m' r getAttribute:of:) - ('loudness' r soundLevel) ('loud?' b isLoud) ~ ('%H sensor value' r sensor: 'slider') ('sensor %h?' b sensorPressed: 'button pressed') ). ^ blocks, super blockSpecs ! ! A ScratchStackFrame describes the state of a ScratchProcess. Each ScratchProcess has a pointer to a ScratchStackFrame containing its state. Whenever the ScratchProcess yields control, its ScratchStackFrame tells it exactly where it left off. Structure: parentFrame the ScratchStackFrame to return to when this one has been evaluated. expression ArgMorph, BlockMorph, a collection of blocks to evaluate, #shouldYield or #returnMarker arguments the OrderedCollection of arg values computed so far (if expression is a BlockMorph) pc the index of the next block to evaluate (if expression is a Collection) startTime the time at which evaluation began (if expression is a timed CommandBlockMorph) startValue the starting value for interpolation (if expression is a timed CommandBlockMorph) shouldUnlight a Boolean that indicates if we should unlight the current expression (a block) ! !ScratchStackFrame methodsFor: 'initialization' stamp: 'jm 3/23/2005 09:09'! initialize parentFrame _ nil. expression _ nil. arguments _ OrderedCollection new. pc _ 1. shouldUnlight _ false. ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'DaveF 7/9/2003 19:46'! addArgument: aValue "Adds this value to the list of evaluated arguments available in this stack frame." arguments add: aValue. ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'ee 2/27/2009 13:09'! arguments "Answers the values of the arguments to be sent to the current command." ^ arguments ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'DaveF 7/4/2003 11:50'! expression ^ expression ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'DaveF 7/4/2003 11:33'! expression: anExpression expression _ anExpression. ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'DaveF 7/8/2003 00:11'! parentFrame ^ parentFrame ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'DaveF 7/8/2003 00:10'! parentFrame: aScratchStackFrame "Sets this frame's parent frame." parentFrame _ aScratchStackFrame. ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'DaveF 7/8/2003 00:06'! pc "Answers the current program counter." ^ pc ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'jm 3/23/2005 09:04'! pc: anInteger "Set the program counter to the current index, indicating the next block to evaluate." pc _ anInteger. ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'jm 1/22/2007 13:01'! printOn: aStream aStream nextPutAll: '[', expression printString, ' ', shouldUnlight printString, ']'.! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'DaveF 8/4/2003 17:23'! shouldUnlight ^ shouldUnlight ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'DaveF 8/4/2003 17:23'! shouldUnlight: aBoolean shouldUnlight _ aBoolean. ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'DaveF 7/9/2003 18:35'! shouldYield "Answers true iff this is a dummy frame simply indicating that the process should yield control." ^ expression = #shouldYield ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'DaveF 7/9/2003 18:05'! shouldYield: aBoolean "Makes this into a special stack frame that tells the evaluator to yield control to another process. When this flag is set, the rest of the stack frame is ignored." aBoolean ifTrue: [expression _ #shouldYield] ifFalse: [self error: 'Should not call shouldYield: with false.']. ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'jm 8/22/2003 18:56'! startTime ^ startTime ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'jm 8/22/2003 18:56'! startTime: mSecs "Record the starting time for an interpolating command." startTime _ mSecs. ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'jm 8/22/2003 18:55'! startValue ^ startValue ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'jm 8/22/2003 18:55'! startValue: aTime "Record the starting value for an interpolating command." startValue _ aTime. ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'jm 5/6/2007 13:47'! stopMIDI (self startValue isKindOf: ScratchNotePlayer) ifTrue: [startValue noteOff]. ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'jm 5/19/2009 13:10'! stopMotors | motorName | (expression isKindOf: CommandBlockMorph) ifTrue: [ #motorOnFor:elapsed:from: = expression selector ifTrue: [ motorName _ startValue. expression receiver motorOff: motorName]]. ! ! !ScratchStackFrame methodsFor: 'accessing' stamp: 'jm 6/2/2009 11:31'! stopTalkThinkAsk | s | (expression isKindOf: CommandBlockMorph) ifTrue: [ ((#say:duration:elapsed:from: = expression selector) | (#think:duration:elapsed:from: = expression selector)) ifTrue: [ expression receiver sayNothing]. #doAsk = expression selector ifTrue: [ (s _ expression receiver ownerThatIsA: ScratchStageMorph) ifNotNil: [s stopAsks]]]. ! ! I represent the background of the Scratch work pane. Like a sprite, I have a set of media and can change my costume. I also have my own scripts and variables. The main difference between me and a sprite is that I am fixed in position: I cannot move or rotate. ! !ScratchStageMorph methodsFor: 'initialization' stamp: 'jm 6/4/2009 12:03'! initialize super initialize. color _ Color white. self enableDragNDrop: true. objName _ 'Stage' localized. costume _ self defaultImageMedia. media _ OrderedCollection with: costume with: SoundMedia new. zoom _ 1.0. hPan _ 0. vPan _ 0. runningBlocks _ OrderedCollection new. inProcessStep _ false. sensorBoard _ SensorBoardMorph new. midiPortNum _ -1. notePlayerDict _ Dictionary new. obsoleteSavedState _ nil. sprites _ OrderedCollection new. showMotorBlocks _ false. ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 10/22/2005 18:15'! adjustedCursorPoint "Answer the current mouse cursorPoint, possibly adjusted for the scaling in 2x presentation mode." ^ DoubleSize ifTrue: [((Sensor cursorPoint - self center) // 2) + self center] ifFalse: [Sensor cursorPoint] ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 8/2/2004 20:14'! copyForExport "Answer a copy of me with no sprites for use in exporting the background by itself." ^ super copyForExport removeAllMorphs ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 11/22/2004 13:43'! costumeChanged costumeChangeMSecs _ Time millisecondClockValue. filterPack ifNotNil: [filterPack clearFilterCaches]. self changed. ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'ee 11/12/2007 14:12'! defaultImageMedia ^ ImageMedia new form: DefaultBackgroundForm; mediaName: ('background' localized, '1') ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 11/18/2008 15:37'! isQuarterSize isQuarterSize ifNil: [isQuarterSize _ false]. "lazy initialization" ^ isQuarterSize ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 11/18/2008 13:09'! isQuarterSize: aBoolean isQuarterSize _ aBoolean. ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 10/25/2007 19:21'! objName ^ 'Stage' localized ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 3/18/2005 23:41'! rotationCenter ^ costume rotationCenter ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 2/14/2008 18:21'! scratchServer ^ scratchServer ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 2/14/2008 18:22'! scratchServer: anObject scratchServer _ anObject. ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 8/2/2005 19:02'! sensorBoard ^ sensorBoard ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 11/28/2005 14:52'! sensorBoard: aSensorBoardMorph sensorBoard _ aSensorBoardMorph. ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 2/13/2009 14:49'! showMotorBlocks ^ showMotorBlocks ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'jm 2/13/2009 14:49'! showMotorBlocks: aBoolean showMotorBlocks _ aBoolean. ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'tis 11/2/2006 18:44'! sprites ^ sprites ! ! !ScratchStageMorph methodsFor: 'accessing' stamp: 'tis 12/11/2006 10:27'! updateSpritesList "Populate the sprites list, which keeps track of the ordering of the sprite thumbnails" | frame | frame _ self ownerThatIsA: ScratchFrameMorph. frame ifNil: [^ self]. sprites _ OrderedCollection new. frame libraryPane spriteThumbnails do: [:m | m target ifNotNil: [sprites addLast: m target]]. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 10/3/2007 09:13'! backgroundIndex "Answer the index of my current costume." ^ self costumeIndex ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 10/6/2006 22:22'! changeBackgroundIndexBy: aNumber "Change my background index by the given amount." self changeCostumeIndexBy: aNumber. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 6/2/2004 18:58'! changeHPanBy: amount "Change my horizontal pan by the given amount." hPan _ hPan + amount truncated. self changed. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 6/2/2004 18:59'! changeVPanBy: amount "Change my vertical pan by the given amount." vPan _ vPan + amount truncated. self changed. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 6/2/2004 19:00'! changeZoomBy: percent "Change my zoom by the given percent." zoom _ zoom + percent. self changed. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 6/2/2004 19:31'! lookLike: costumeName "Change to the costume with the given name. Noop if there is no costume of the given name in my library." zoom _ 100. hPan _ 0. vPan _ 0. super lookLike: costumeName. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 6/18/2004 18:16'! makeVisible "Do nothing. I'm always visible." ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'ee 6/27/2008 17:46'! newScene | sceneName | sceneName _ StringDialog ask: 'Enter Scene Name:'. sceneName size = 0 ifTrue: [ ^ self ]. sceneStates at: sceneName put: self backgroundIndex. submorphs do: [:m | (m isKindOf: ScratchSpriteMorph) ifTrue: [ m recordScene: sceneName]]. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 11/28/2006 13:39'! nextBackground "Show the next background in my backgrounds list." self nextCostume. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'nb 1/7/2008 14:19'! rerecordScene | sceneName sceneList menu | sceneList _ self sceneNames . sceneList _ sceneList copyFrom: 1 to: sceneList size - 3. menu _ CustomMenu new. sceneList do: [:n | menu add: n action: n]. (sceneName _ menu startUp) ifNil: [^ self]. sceneStates at: sceneName put: self backgroundIndex. submorphs do: [:m | (m isKindOf: ScratchSpriteMorph) ifTrue: [ m recordScene: sceneName]]. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 4/25/2008 16:01'! sceneNames | setOfNames | setOfNames _ Set new. submorphs do: [:m | (m isKindOf: ScratchSpriteMorph) ifTrue: [ setOfNames addAll: m scenes]]. ^ setOfNames asArray sort, (Array with: '-' with: 'record' localized, ScratchTranslator ellipsesSuffix with: 're-record' localized, ScratchTranslator ellipsesSuffix) ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 6/2/2004 19:00'! setHPanTo: aNumber "Set my horizontal pan to the given offset." hPan _ aNumber truncated. self changed. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 6/2/2004 19:00'! setVPanTo: aNumber "Set my vertical pan to the given offset." vPan _ aNumber truncated. self changed. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 6/2/2004 19:01'! setZoomTo: percent "Set my zoom to the given percent." zoom _ percent truncated. self changed. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'jm 12/11/2006 11:42'! showBackground: costumeNameOrIndex "This is lookLike: for the stage..." self lookLike: costumeNameOrIndex. ! ! !ScratchStageMorph methodsFor: 'looks ops' stamp: 'nb 1/7/2008 12:52'! showScene: sceneName (sceneStates includesKey: sceneName) ifTrue: [ self showBackground: (sceneStates at: sceneName)]. submorphs do: [:m | (m isKindOf: ScratchSpriteMorph) ifTrue: [ m setScene: sceneName]]. ! ! !ScratchStageMorph methodsFor: 'sensing ops' stamp: 'jm 4/18/2008 14:07'! attributeNames ^ #('background #' 'volume') collect: [:s | s] ! ! !ScratchStageMorph methodsFor: 'sensing ops' stamp: 'jm 10/19/2007 13:59'! getAttribute: attr "Answer the value of my variable or built-in attribute with the given name. Answer zero if I have no attribute or variable with the given name." | a | (vars includesKey: attr) ifTrue: [^ vars at: attr]. a _ attr localized. 'background #' localized = a ifTrue: [^ self backgroundIndex]. 'volume' localized = a ifTrue: [^ self volume]. ^ 0 ! ! !ScratchStageMorph methodsFor: 'sound ops' stamp: 'jm 11/28/2007 12:55'! setTempoTo: aNumber tempoBPM _ (aNumber within: 20 and: 500). ! ! !ScratchStageMorph methodsFor: 'sound ops' stamp: 'jm 11/27/2007 11:26'! tempo ^ tempoBPM ! ! !ScratchStageMorph methodsFor: 'event handling' stamp: 'jm 11/21/2008 13:57'! containsPoint: aPoint self isQuarterSize ifTrue: [^ (self position extent: self extent // 2) containsPoint: aPoint]. ^ self bounds containsPoint: aPoint ! ! !ScratchStageMorph methodsFor: 'event handling' stamp: 'jm 12/9/2008 10:53'! fullContainsPoint: aPoint "Answer true if the given point is in my visible bounds. In quarterSize mode, my visible bounds is only half of my extent." | r | r _ self isQuarterSize ifTrue: [self position extent: bounds extent // 2] ifFalse: [bounds]. ^ r containsPoint: aPoint ! ! !ScratchStageMorph methodsFor: 'event handling' stamp: 'jm 8/3/2008 13:16'! grabSpriteFromScreen "Create a new sprite, grabbing it's costume from an area of the screen." | frame m f | (frame _ self ownerThatIsA: ScratchFrameMorph) ifNil: [^ self]. m _ ScratchSpriteMorph new. (f _ m grabFormFromScreen) ifNil: [^ self]. m onlyCostume: f. frame addAndView: m. ! ! !ScratchStageMorph methodsFor: 'event handling' stamp: 'jm 12/19/2008 17:22'! mouseDown: evt "Handle a mouse down event." evt hand newKeyboardFocus: nil. evt hand toolType ifNotNil: [evt hand toolType: nil]. evt rightButtonPressed ifTrue: [Sensor waitNoButton. ^ self rightButtonMenu] ifFalse: [evt hand waitForClicksOrDrag: self event: evt]. ! ! !ScratchStageMorph methodsFor: 'event handling' stamp: 'tis 12/7/2006 16:04'! stageShot | result | result _ ScratchFileChooserDialog chooseNewFileDefault: '' title: 'Save Stage Shot' type: #stageShot. result = #cancelled ifTrue: [^ self]. result size > 0 ifTrue: [self exportFileName: result]. ! ! !ScratchStageMorph methodsFor: 'event handling' stamp: 'jm 3/2/2009 15:22'! startDrag: evt "Ignore drag events." ! ! !ScratchStageMorph methodsFor: 'event handling' stamp: 'jm 11/18/2008 19:34'! transformFrom: uberMorph "Return a transform to map coorinates of uberMorph, a morph above me in my owner chain, into the coordinates of my submorphs." | transform | self isQuarterSize ifFalse: [^ super transformFrom: uberMorph]. transform _ MorphicTransform offset: (self position // -2) angle: 0.0 scale: 0.5. owner == uberMorph ifTrue: [^ transform]. owner ifNil: [^ transform]. ^ (owner transformFrom: uberMorph) composedWith: transform ! ! !ScratchStageMorph methodsFor: 'event handling' stamp: 'jm 12/9/2008 10:53'! unlockedMorphsAt: aPoint addTo: mList "Adjust aPoint to handle quarter-size case if necessary." | p | self isQuarterSize ifFalse: [ super unlockedMorphsAt: aPoint addTo: mList. ^ mList]. (self containsPoint: aPoint) ifFalse: [^ mList]. "quick elimination" p _ self position + (2 * (aPoint - self position)). submorphs size > 0 ifTrue: [ submorphs do: [:m | m unlockedMorphsAt: p addTo: mList]]. mList addLast: self. ^ mList ! ! !ScratchStageMorph methodsFor: 'dropping/grabbing' stamp: 'jm 11/18/2008 17:43'! acceptDroppingMorph: aMorph event: evt self isQuarterSize ifTrue: [ aMorph center: (2 * aMorph center) - self position. self changed]. self addMorph: aMorph. self isInWorld ifTrue: [self world startSteppingSubmorphsOf: aMorph]. ((aMorph respondsTo: #penDown) and: [aMorph penDown]) ifTrue: [self penUpOrDownChangeFor: aMorph]. self changed. ! ! !ScratchStageMorph methodsFor: 'dropping/grabbing' stamp: 'jm 5/25/2004 14:56'! rootForGrabOf: aMorph "Allow the given submorph to be extracted." | root | root _ aMorph. [root = self] whileFalse: [ root owner == self ifTrue: [^ root]. root _ root owner]. ^ super rootForGrabOf: aMorph ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'jm 8/2/2006 18:24'! drawOn: aCanvas "Draw myself if my visibility is > 0. If my visibility is 1, draw using the normal 'paint' mode. Otherwise, draw using 'alpha' resulting in a partially transparent rendering." | clipC f p alpha | clipC _ aCanvas copyClipRect: bounds. clipC fillRectangle: bounds color: Color white. f _ self filteredForm. f ifNotNil: [ p _ bounds center - (f extent // 2) + (hPan@vPan). visibility < 100 ifTrue: [ visibility > 0 ifTrue: [ alpha _ ((255.0 * visibility) / 100.0) truncated. clipC paintImage: f at: p sourceRect: f boundingBox alpha: alpha]] ifFalse: [clipC paintImage: f at: p]]. self updateTrailsForm. penTrailsForm ifNotNil: [clipC paintImage: penTrailsForm at: self position]. ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'jm 1/23/2009 11:14'! drawQuarterSizeOn: aCanvas "Draw myself and my submorphs to an offscreen canvas, then scale down to quarter size and draw that on the given canvas." | r srcR c | cachedForm ifNil: [cachedForm _ Form extent: self extent depth: 32]. r _ aCanvas clipRect intersect: (bounds origin extent: bounds extent // 2). srcR _ ((r origin - bounds origin) * 2.0) truncated extent: (r extent * 2.0) rounded. c _ (FormCanvas on: cachedForm) copyOrigin: self position negated clipRect: srcR. super fullDrawOn: c. ScratchPlugin halfSize: cachedForm into: Display srcPoint: srcR origin dstRect: r. "xxx cachedForm unhibernate. LowResPlugin primHalf2Average: cachedForm bits w: cachedForm width h: cachedForm height into: Display bits w: Display width h: Display height srcX: srcR left srcY: srcR top dstX: r left dstY: r top dstW: r width dstH: r height. (WarpBlt toForm: Display) sourceForm: cachedForm; combinationRule: Form over; clipRect: aCanvas clipRect; cellSize: 2; copyQuad: srcR corners toRect: r. xxx" "the following scales down entire stage:" " LowResPlugin scale: cachedForm into: aCanvas form at: aCanvas origin + self position." ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'jm 11/30/2007 23:41'! drawSubmorphsOn: aCanvas "Clip submorph drawing to my bounds." | clipCanvas | clipCanvas _ aCanvas copyClipRect: bounds. submorphs reverseDo:[:m | (clipCanvas isVisible: m fullBounds) ifTrue: [ m fullDrawOn: clipCanvas]]. "draw sprite talk bubbles in front of all morphs:" submorphs reverseDo:[:m | ((m isKindOf: ScratchSpriteMorph) and: [m isHidden not and: [clipCanvas isVisible: m fullBounds]]) ifTrue: [ m drawTalkBubbleOn: clipCanvas]]. ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'jm 12/16/2006 12:21'! exportFileName: fileName | form fName | form _ self stageShotForm. form depth <= 8 ifTrue: [ (fileName asLowercase endsWith: '.gif') ifTrue: [fName _ fileName] ifFalse: [fName _ fileName, '.gif']. GIFReadWriter putForm: form colorReduced8Bit onFileNamed: fName. ^ self]. (fileName asLowercase endsWith: '.bmp') ifTrue: [fName _ fileName] ifFalse: [fName _ fileName, '.bmp']. (form asFormOfDepth: 32) writeBMPFileNamed: fName. ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'jm 6/3/2004 20:17'! fullBounds "Overridden to clip submorph hit detection to my bounds." ^ bounds ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'jm 12/9/2008 10:51'! fullDrawOn: aCanvas "Calls super fullDrawOn and then draws the frame shadow" | shadowOrigin topShadowExtent leftShadowExtent alphas | (self isQuarterSize and: [self isInWorld]) ifTrue: [ ^ self drawQuarterSizeOn: aCanvas]. super fullDrawOn: aCanvas. "don't draw shadows if owner is not a ScratchFrameMorph" (owner isKindOf: ScratchFrameMorph) ifFalse: [^ self]. "shadow constants" shadowOrigin _ self topLeft + aCanvas origin. topShadowExtent _ self width@1. leftShadowExtent _ 1@self height. "shadow alpha values" alphas _ FloatArray with: 0.51 with: 0.26 with: 0.07. "top/horizontal shadow" 1 to: 3 do: [:i | aCanvas grafPort fill: (shadowOrigin + (0@(i-1)) extent: topShadowExtent) fillColor: (Color black alpha: (alphas at: i)) rule: Form blend]. "left/vertical shadow" 1 to: 3 do: [:i | aCanvas grafPort fill: (shadowOrigin + ((i-1)@0) extent: leftShadowExtent) fillColor: (Color black alpha: (alphas at: i)) rule: Form blend]. "corner shadow fix" aCanvas grafPort fill: (shadowOrigin extent: (1@1)) fillColor: (Color white alpha: 0.32) rule: Form blend. aCanvas grafPort fill: (shadowOrigin +(0@1) extent: (1@1)) fillColor: (Color white alpha: 0.19) rule: Form blend. aCanvas grafPort fill: (shadowOrigin + (1@0) extent: (1@1)) fillColor: (Color white alpha: 0.19) rule: Form blend. aCanvas grafPort fill: (shadowOrigin +(0@2) extent: (1@1)) fillColor: (Color white alpha: 0.05) rule: Form blend. aCanvas grafPort fill: (shadowOrigin + (2@0) extent: (1@1)) fillColor: (Color white alpha: 0.05) rule: Form blend. ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'jm 11/8/2006 18:09'! incrRedraw: damageList "Redraw the damaged areas of this stage directly onto the display. Assumes that no other morph is in front of me." | f c p screenR | damageList do: [:r | f _ Form extent: r extent depth: 32. c _ (FormCanvas on: f) copyOffset: r origin negated. self fullDrawOn: c. DoubleSize ifTrue: [ p _ (self center - self extent) + (2 * (r origin - self topLeft)). screenR _ p extent: 2 * f extent. (Display boundingBox containsRect: screenR) ifTrue: [ [ ScratchPlugin primDouble: f bits w: f width h: f height into: Display bits w: Display width h: Display height x: screenR left y: screenR top. ] ifError: []. Display forceToScreen: screenR]] ifFalse: [ f displayOn: Display at: r topLeft rule: Form over]]. ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'jm 12/10/2008 13:57'! invalidRect: damageRect "Clip damage reports to my bounds, since drawing is clipped to my bounds." | r | (owner isKindOf: ScratchFrameMorph) ifTrue: [owner projectModified]. (self isQuarterSize and: [owner isKindOf: ScratchFrameMorph]) ifTrue: [ r _ (bounds origin + ((damageRect origin - bounds origin) / 2.0)) extent: (damageRect extent / 2.0). r _ r intersect: (bounds origin extent: bounds extent // 2)] ifFalse: [ r _ (damageRect topLeft truncated) corner: (damageRect right ceiling@damageRect bottom ceiling). r _ r intersect: self bounds]. (r width > 0 and: [r height > 0]) ifTrue: [super invalidRect: r]. ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'ee 8/1/2008 17:17'! patchAt: patchRect withoutWatchersAnd: stopMorph andNothingAbove: stopThere "Return a complete rendering of this patch of the display screen without drawing stopMorph and, if stopThere is true, without drawing any morph above it." | c morphsToDraw i | c _ FormCanvas extent: patchRect extent depth: Display depth. c _ c copyOrigin: patchRect topLeft negated clipRect: (0@0 extent: patchRect extent). (self bounds containsRect: patchRect) ifFalse: [ "fill areas of patchRect outside my bounds with black" c form fillColor: Color black]. (self bounds intersects: patchRect) ifFalse: [^ c form]. "entirely out of bounds" "draw all morphs intersecting the given patch, stopping at the given morph" c fillRectangle: self bounds color: color. "draw world color" self drawOn: c. morphsToDraw _ submorphs reversed asOrderedCollection. (i _ morphsToDraw indexOf: stopMorph) > 0 ifTrue: [ stopThere ifTrue: [morphsToDraw _ morphsToDraw copyFrom: 1 to: i - 1] "stop at stopMorph" ifFalse: [morphsToDraw removeIndex: i]]. "skip stopMorph" morphsToDraw do: [:m | ((m isKindOf: WatcherMorph) or: [(m isKindOf: SensorBoardMorph)]) ifFalse: [m fullDrawOn: c]]. ^ c form ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'jm 8/2/2006 14:43'! previewForm "Answer a full-size preview of me and my submorphs. Use super fullDrawOn: to avoid drawing the shadows along the top and left edges of the workspace." | canvas | canvas _ FormCanvas extent: bounds extent depth: 32. canvas translateBy: bounds topLeft negated during: [:c | super fullDrawOn: c]. ^ canvas form ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'tis 8/3/2006 16:06'! stageShotForm "Answer a stage shot of me and my submorphs." ^ self stageShotSized: self width @ self height ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'tis 8/3/2006 15:55'! stageShotSized: size "Answer a thumbnail of me and my submorphs. Use super fullDrawOn: to avoid drawing the shadows along the top and left edges of the workspace." | canvas thumbForm | canvas _ FormCanvas extent: bounds extent depth: 32. canvas translateBy: bounds topLeft negated during: [:c | super fullDrawOn: c]. thumbForm _ Form extent: size depth: 32. (WarpBlt toForm: thumbForm) sourceForm: canvas form; cellSize: 2; combinationRule: Form over; copyQuad: (0@0 extent: canvas extent) innerCorners toRect: thumbForm boundingBox. thumbForm _ thumbForm colorReduced. "first try to make a ColorForm with exact colors" thumbForm depth > 8 ifTrue: [ thumbForm _ thumbForm asFormOfDepth: 8]. "if that fails, use the closest 8-bit colors" ^ thumbForm ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'tis 8/3/2006 16:01'! thumbnailForm "Answer a thumbnail of me and my submorphs." ^ self stageShotSized: (160@120) ! ! !ScratchStageMorph methodsFor: 'drawing' stamp: 'jm 1/3/2006 22:23'! updateStageDisplay "Redraw the damaged areas of this stage directly onto the display. Assumes that no other morph is in front of me." | root damageList | root _ owner. [root owner notNil] whileTrue: [root _ root owner]. (root respondsTo: #damageRecorder) ifFalse: [^ self]. damageList _ root damageRecorder filteredDamageWithin: self bounds. damageList size > 0 ifTrue: [self incrRedraw: damageList]. ! ! !ScratchStageMorph methodsFor: 'scratch processes/events' stamp: 'jm 5/14/2009 14:28'! allEventNames "Answer a list of all events that have been defined in this project." | result | result _ Set new: 100. self submorphsDo: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [ m addEventNamesTo: result]]. self addEventNamesTo: result. scratchServer ifNotNil: [ result addAll: scratchServer broadcastsSeen]. "remove empty string" result remove: '' ifAbsent: []. ^ result asArray sort ! ! !ScratchStageMorph methodsFor: 'scratch processes/events' stamp: 'jm 2/14/2008 18:23'! broadcastEventNamed: name with: value "Broadcast a ScratchEvent with given name and argument value to all Scratch objects and answer a collection of the newly created processes. This is done by finding all public scripts that respond to this event, and starting new processes for any not already running." | event objList newProcs | scratchServer ifNotNil: [scratchServer queueBroadcast: name]. event _ ScratchEvent new name: name argument: value. newProcs _ OrderedCollection new. "start scripts" objList _ submorphs select: [:m | m isKindOf: ScriptableScratchMorph]. objList do: [:obj | newProcs addAll: (obj eventReceived: event)]. newProcs addAll: (self eventReceived: event). ^ newProcs asArray ! ! !ScratchStageMorph methodsFor: 'scratch processes/events' stamp: 'jm 9/27/2007 15:10'! defaultEventName "Answer a default event name for message send and receive blocks." | evtNames | evtNames _ self allEventNames. ^ evtNames size = 0 ifTrue: [''] ifFalse: [evtNames first] ! ! !ScratchStageMorph methodsFor: 'scratch processes/events' stamp: 'jm 6/4/2009 12:10'! processesToRun "Answer a collection of processes to run. Filter out any processes for objects have been picked up. Always return a copy of the processes list." | result m | result _ runningBlocks collect: [:b | b scratchProc]. result _ result select: [:proc | proc notNil]. World activeHand submorphs size > 0 ifTrue: [ m _ World activeHand submorphs first. result _ result select: [:proc | (proc includesReceiver: m) not]]. ^ result ! ! !ScratchStageMorph methodsFor: 'scratch processes/events' stamp: 'jm 6/4/2009 13:06'! removeTerminatedProcesses "Remove terminated processes from the process list." | newRunning proc | newRunning _ runningBlocks species new: 100. runningBlocks do: [:b | (proc _ b scratchProc) ifNotNil: [ proc isRunning ifTrue: [newRunning addLast: b] ifFalse: [proc errorFlag ifFalse: [b stop]]]]. runningBlocks _ newRunning. ! ! !ScratchStageMorph methodsFor: 'scratch processes/events' stamp: 'jm 6/4/2009 12:27'! startProcessFor: topBlock "Start a process to run the given block or stack of blocks. Return the new process." | sequence proc | sequence _ topBlock blockSequence. sequence first isHatBlock ifTrue: [ sequence _ sequence allButFirst]. "skip hat block" topBlock scratchProc ifNotNil: [topBlock stop]. proc _ ScratchProcess new topBlock: topBlock; expression: sequence. topBlock scratchProc: proc. (runningBlocks includes: topBlock) ifFalse: [runningBlocks addLast: topBlock]. ^ proc ! ! !ScratchStageMorph methodsFor: 'scratch processes/events' stamp: 'jm 12/12/2008 14:08'! stepProcesses "Run each Scratch process until it gives up control, then filter out any processes that have terminated." "Details: Iterate over a copy of processes to allow processes to stop themselves. During development, the error catcher makes it difficult to track down errors, so it can be disabled." | proc | sensorBoard processIncomingData. ScratchProcess blockHighlightMSecs = 0 ifTrue: [^ self stepProcessesTurbo]. inProcessStep ifTrue: [^ self]. inProcessStep _ true. ScratchFrameMorph useErrorCatcher ifTrue: [ [self processesToRun do: [:p | (proc _ p) runStepFor: self]] ifError: [proc errorFlag: true]] ifFalse: [ self processesToRun do: [:p | p runStepFor: self]]. self removeTerminatedProcesses. " self deleteTerminatedClones." inProcessStep _ false. ! ! !ScratchStageMorph methodsFor: 'scratch processes/events' stamp: 'jm 6/4/2009 12:03'! stepProcessesTurbo "Run each Scratch process until it gives up control, then filter out any processes that have terminated. Do this repeatedly until time is up." | sliceMSecs startMSecs now proc | sliceMSecs _ 100. inProcessStep ifTrue: [^ self]. inProcessStep _ true. startMSecs _ Time millisecondClockValue. [ [now _ Time millisecondClockValue. runningBlocks size > 0 and: [(now >= startMSecs) and: [(now - startMSecs) < sliceMSecs]]] whileTrue: [ self processesToRun do: [:p | (proc _ p) runStepFor: self]. self removeTerminatedProcesses]. ] ifError: [proc errorFlag: true]. inProcessStep _ false. ! ! !ScratchStageMorph methodsFor: 'scratch processes/events' stamp: 'jm 5/12/2009 15:08'! stopAll "Stop all processes and make sure I am stepping." | sFrame | World hands do: [:h | h newKeyboardFocus: nil; clearUnclaimedKeystrokes]. Sensor clearKeystate. SoundPlayer stopPlayingAll. self class stopSoundRecorder. self stopAllProcesses. self stopAsks. self deleteAllClones. self midiAllNotesOff. WeDoPlugin resetWeDo. self stopPlaying. self allMorphsDo: [:m | (m isKindOf: ScriptableScratchMorph) ifTrue: [m stopPlaying]]. (sFrame _ self ownerThatIsA: ScratchFrameMorph) ifNotNil: [ sFrame scriptsPane allMorphsDo: [:m | (m respondsTo: #stop) ifTrue: [m stop]. (m respondsTo: #litUp:) ifTrue: [m litUp: false]]. World startSteppingSubmorphsOf: sFrame]. World startSteppingSubmorphsOf: self. ! ! !ScratchStageMorph methodsFor: 'scratch processes/events' stamp: 'jm 6/4/2009 13:13'! stopAllProcesses "Stop all running Scratch processes." | allObjs | "clear all processes, including those with error feedback" allObjs _ submorphs select: [:m | m isKindOf: ScriptableScratchMorph]. allObjs _ allObjs copyWith: self. allObjs do: [:obj | (obj blocksBin isKindOf: Morph) ifTrue: [ obj blocksBin submorphs do: [:b | (b isKindOf: BlockMorph) ifTrue: [b clearProcess]]]]. runningBlocks _ OrderedCollection new. inProcessStep _ false. ! ! !ScratchStageMorph methodsFor: 'scratch processes/events' stamp: 'jm 12/1/2006 19:02'! stopAllSounds "Stop all sounds and MIDI notes/drums." SoundPlayer shutDown. self midiAllNotesOff. ! ! !ScratchStageMorph methodsFor: 'scratch processes/events' stamp: 'jm 3/24/2009 17:09'! stopAsks "Stop/close any ask that is currently on the screen." ScratchPrompterMorph allInstancesDo: [:m | m stopAsk]. ScratchPrompterMorph clearLastAnswer. ! ! !ScratchStageMorph methodsFor: 'clones' stamp: 'jm 6/6/2008 14:41'! allClones "Answer a collection of all sprite clones." ^ self submorphs select: [:m | (m isKindOf: ScriptableScratchMorph) and: [m isClone]]. ! ! !ScratchStageMorph methodsFor: 'clones' stamp: 'jm 6/6/2008 14:41'! deleteAllClones "Delete all clones." self allClones do: [:clone | clone delete]. ! ! !ScratchStageMorph methodsFor: 'clones' stamp: 'jm 6/4/2009 12:34'! deleteTerminatedClones "Delete all clones that have no running scripts." | isRunning | self allClones do: [:clone | isRunning _ false. clone blocksBin allMorphsDo: [:b | ((b isKindOf: BlockMorph) and: [b hasRunningProcess]) ifTrue: [isRunning _ true]]. isRunning ifFalse: [clone delete]]. ! ! !ScratchStageMorph methodsFor: 'midi' stamp: 'jm 8/2/2005 19:08'! closeMIDI "Close the MIDI port and clear the note player dictionary." midiPort ifNotNil: [ midiPort close. midiPort _ nil]. notePlayerDict _ Dictionary new. ! ! !ScratchStageMorph methodsFor: 'midi' stamp: 'jm 8/2/2005 19:09'! midiAllNotesOff "If the MIDI port is open, send an 'all notes off' command on every channel." midiPort ifNil: [^ self]. midiPort ensureOpenIfFail: [self closeMIDI]. notePlayerDict do: [:player | player noteOff]. ! ! !ScratchStageMorph methodsFor: 'midi' stamp: 'jm 8/2/2005 19:23'! midiPortNum ^ midiPortNum ! ! !ScratchStageMorph methodsFor: 'midi' stamp: 'jm 8/2/2005 19:23'! midiPortNum: anInteger midiPortNum _ anInteger. ! ! !ScratchStageMorph methodsFor: 'midi' stamp: 'jm 6/2/2009 18:46'! notePlayerFor: aScratchObject "Answer a note player for the given object, creating one if necessary. Open the MIDI port if necessary." | deletedMorphs channelUsage ch newCh newPlayer | midiPort ifNil: [self tryToOpenMidiPort]. (notePlayerDict includesKey: aScratchObject) ifTrue: [ ^ notePlayerDict at: aScratchObject]. "remove deleted morphs from the note player dictionary" deletedMorphs _ notePlayerDict keys select: [:m | m owner isNil]. deletedMorphs do: [:m | notePlayerDict removeKey: m]. "find the channel used by the fewest objects" channelUsage _ Array new: 16 withAll: 0. channelUsage at: 10 put: 1000000. "make sure channel 10 (drums) is not chosen" notePlayerDict do: [:player | ch _ player channel. channelUsage at: ch put: (channelUsage at: ch) + 1]. newCh _ channelUsage indexOf: channelUsage min. newPlayer _ ScratchNotePlayer new channel: newCh; midiPort: midiPort; instrument: 1. notePlayerDict at: aScratchObject put: newPlayer. ^ newPlayer ! ! !ScratchStageMorph methodsFor: 'midi' stamp: 'jm 8/2/2005 19:09'! openMIDI "Prompt the user to select a MIDI port number, then open it." | possiblePorts dir menu choice | self closeMIDI. possiblePorts _ (0 to: SimpleMIDIPort primPortCount - 1) select: [:i | dir _ SimpleMIDIPort primPortDirectionalityOf: i. (dir = 2) | (dir = 3)]. "out or in/out port" possiblePorts size = 0 ifTrue: [^ self inform: 'No MIDI ports currently available.']. menu _ CustomMenu new title: 'MIDI port:'. possiblePorts do: [:i | menu add: (SimpleMIDIPort portDescription: i) action: i]. choice _ menu startUp. choice ifNil: [^ self]. midiPortNum _ choice. self tryToOpenMidiPort. ! ! !ScratchStageMorph methodsFor: 'midi' stamp: 'jm 6/3/2009 15:37'! tryToOpenMidiPort "Attempt to open the MIDI port. First try the port selected by the user, if any. If that port number is not a MIDI output port, try to find another port number. If all measures fail, leave midiPort set to nil." | possiblePorts dir portNum | Smalltalk isUnix ifTrue: [midiPort _ nil. ^ self]. possiblePorts _ (0 to: SimpleMIDIPort primPortCount - 1) select: [:i | dir _ SimpleMIDIPort primPortDirectionalityOf: i. (dir = 2) | (dir = 3)]. "out or in/out port" possiblePorts size = 0 ifTrue: [midiPort _ nil. ^ self]. "no midi ports" (possiblePorts includes: midiPortNum) ifTrue: [portNum _ midiPortNum] "use the port requested by the user" ifFalse: [portNum _ possiblePorts first]. "use the first available port" [midiPort _ SimpleMIDIPort openOnPortNumber: portNum] ifError: [midiPort _ nil]. ! ! !ScratchStageMorph methodsFor: 'pen support' stamp: 'jm 2/5/2005 11:31'! clearPenTrails "Remove my pen trails Form. It will be recreated later if it is needed." penTrailsForm _ nil. self changed. ! ! !ScratchStageMorph methodsFor: 'pen support' stamp: 'jm 2/11/2005 21:56'! createOrResizeTrailsForm "If necessary, create a new penTrailsForm or resize the existing one to fill my bounds. On return, penTrailsForm will be a Form of the correct size." | newForm | penTrailsForm ifNil: [ penTrailsForm _ Form extent: self extent depth: 32. ^ self]. penTrailsForm extent = self extent ifFalse: [ "resize trails Form to my current exent" newForm _ Form extent: self extent depth: 32. newForm copy: penTrailsForm boundingBox from: penTrailsForm to: 0@0 rule: Form paint. penTrailsForm _ newForm]. ! ! !ScratchStageMorph methodsFor: 'pen support' stamp: 'jm 10/6/2007 15:03'! drawPenTrailFor: aMorph from: oldPoint to: newPoint "Draw a pen trail between the given points for the given morph using its pen size and color. The points are in Scratch coordinates (that is, 0@0 is the center of the work pane and y increases toward the top of the screen." "The penTrailsForm is created on demand when the first pen is put down and removed (to save space) when pen trails are cleared." | pen penSize offset p1 p2 r | self createOrResizeTrailsForm. pen _ Pen newOnForm: penTrailsForm. penSize _ aMorph penSize. aMorph penSize ~= 1 ifTrue: [ pen roundNib: penSize. pen sourceForm offset: nil]. "clear form offset" pen color: aMorph penColor. offset _ (penTrailsForm extent - penSize) / 2.0. p1 _ ((oldPoint * (1 @ -1)) + offset) rounded. p2 _ ((newPoint * (1 @ -1)) + offset) rounded. pen drawFrom: p1 to: p2. r _ ((p1 rect: p2) expandBy: penSize + 1) translateBy: self topLeft. self invalidRect: r. ! ! !ScratchStageMorph methodsFor: 'pen support' stamp: 'jm 6/2/2009 18:50'! penTrailsForm ^ penTrailsForm ! ! !ScratchStageMorph methodsFor: 'pen support' stamp: 'jm 6/2/2009 18:51'! penTrailsForm: aForm penTrailsForm _ aForm. penTrailsForm ifNotNil: [self createOrResizeTrailsForm]. ! ! !ScratchStageMorph methodsFor: 'pen support' stamp: 'jm 10/6/2007 14:53'! penUpOrDownChangeFor: aSprite "The pen up/down state for the given sprite may have changed; update lastPenPositions accordingly." | p | aSprite penDown ifTrue: [ "pen down transition" lastPenPositions ifNil: [lastPenPositions _ IdentityDictionary new]. p _ aSprite penPosition. lastPenPositions at: aSprite put: p. self drawPenTrailFor: aSprite from: p to: p] ifFalse: [ lastPenPositions ifNil: [^ self]. lastPenPositions removeKey: aSprite ifAbsent: []. lastPenPositions size = 0 ifTrue: [lastPenPositions _ nil]]. ! ! !ScratchStageMorph methodsFor: 'pen support' stamp: 'jm 1/13/2006 09:36'! stampCostume: aSprite "Stamp a copy of the given sprite on my pen trails form." self createOrResizeTrailsForm. aSprite filteredForm displayOn: penTrailsForm at: (aSprite position - self topLeft) rule: Form paint. ! ! !ScratchStageMorph methodsFor: 'pen support' stamp: 'jm 11/15/2006 16:57'! updatePenPositionFor: aSprite "Update the given sprites pen position if necessary. Used to avoid drawing glitches when going between normal and presentation mode." lastPenPositions ifNil: [^ self]. "no pens are down" (lastPenPositions includes: aSprite) ifTrue: [ lastPenPositions at: aSprite put: aSprite penPosition]. ! ! !ScratchStageMorph methodsFor: 'pen support' stamp: 'jm 2/5/2005 11:33'! updateTrailsForm "Update the pen trails form using the current positions of all sprites with their pens down." "Details: The positions of all sprites with their pens down are recorded by my draw method. If the list from the last display update isn't empty, then trails are drawn from the old to the current positions of all such morphs on the pen trails form. The pen trails form is created on demand when the first pen is put down and removed (to save space) when the pen trails are cleared." | spritesToRemove m oldPoint newPoint | (lastPenPositions isNil or: [lastPenPositions size = 0]) ifTrue: [^ self]. spritesToRemove _ OrderedCollection new. lastPenPositions associationsDo: [:assoc | m _ assoc key. (m penDown and: [m owner == self]) ifTrue: [ oldPoint _ assoc value. newPoint _ m penPosition. newPoint = oldPoint ifFalse: [ self drawPenTrailFor: m from: oldPoint to: newPoint. assoc value: newPoint]] ifFalse: [spritesToRemove add: m]]. "remove sprites that are not longer owned by me or whose pens are up" spritesToRemove do: [:key | lastPenPositions removeKey: key ifAbsent: []]. ! ! !ScratchStageMorph methodsFor: 'object i/o' stamp: 'nb 1/7/2008 14:01'! fieldsVersion ^ 5 ! ! !ScratchStageMorph methodsFor: 'object i/o' stamp: 'jm 7/8/2008 06:13'! initFieldsFrom: anObjStream version: classVersion super initFieldsFrom: anObjStream version: classVersion. self initFieldsNamed: #( zoom hPan vPan ) from: anObjStream. classVersion = 1 ifTrue: [^ self]. "fields added in version 2" self initFieldsNamed: #( obsoleteSavedState ) from: anObjStream. classVersion = 2 ifTrue: [^ self]. "fields added in version 3" self initFieldsNamed: #( sprites ) from: anObjStream. classVersion = 3 ifTrue: [^ self]. "fields added in version 4" self initFieldsNamed: #( volume tempoBPM ) from: anObjStream. classVersion = 4 ifTrue: [^ self]. "fields added in version 5" self initFieldsNamed: #( sceneStates lists ) from: anObjStream. lists ifNil: [lists _ Dictionary new]. "work around" ! ! !ScratchStageMorph methodsFor: 'object i/o' stamp: 'jm 5/12/2008 11:58'! storeFieldsOn: anObjStream super storeFieldsOn: anObjStream. self storeFieldsNamed: #( zoom hPan vPan obsoleteSavedState sprites volume tempoBPM sceneStates lists ) on: anObjStream. ! ! !ScratchStageMorph methodsFor: 'menus' stamp: 'jm 10/25/2007 19:25'! rightButtonMenu "Present the right button menu." | menu | menu _ CustomMenu new. menu add: 'grab screen region for new sprite' action: #grabSpriteFromScreen. menu addLine. menu add: 'save picture of stage...' action: #stageShot. menu localize; invokeOn: self. ! ! !ScratchStageMorph methodsFor: 'media' stamp: 'jm 6/22/2009 14:15'! savePhoto: aForm | n f | n _ self unusedMediaNameFromBaseName: 'background' localized, '1'. f _ Form extent: self extent depth: 32. (WarpBlt toForm: f) sourceForm: aForm; cellSize: 1; "installs a new colormap if cellSize > 1" combinationRule: Form over; copyQuad: aForm boundingBox innerCorners toRect: f boundingBox. self addMediaItem: (ImageMedia new mediaName: n; form: f). ! ! !ScratchStageMorph class methodsFor: 'block specs' stamp: 'ee 4/16/2009 17:31'! blockSpecs | blocks | blocks _ #( 'sensing' ('ask %s and wait' s doAsk 'What''s your name?') ('answer' r answer) - ('mouse x' r mouseX) ('mouse y' r mouseY) ('mouse down?' b mousePressed) - ('key %k pressed?' b keyPressed: 'space') - ('reset timer' - timerReset) ('timer' r timer) - ('%a of %m' r getAttribute:of:) - ('loudness' r soundLevel) ('loud?' b isLoud) ~ ('%H sensor value' r sensor: 'slider') ('sensor %h?' b sensorPressed: 'button pressed') 'looks' ('switch to background %l' - showBackground: 'background1') ('next background' - nextBackground) ('background #' r backgroundIndex) - ('change %g effect by %n' - changeGraphicEffect:by: 'color' 25) ('set %g effect to %n' - setGraphicEffect:to: 'color' 0) ('clear graphic effects' - filterReset) - "xxx ('place sprites for scene %x' - showScene:) " 'pen' ('clear' - clearPenTrails) ). ^ blocks, super blockSpecs ! ! !ScratchTabPaneMorph methodsFor: 'initialization' stamp: 'ee 2/3/2009 13:22'! initialize | barFormSource | super initialize. self color: Color red. self extent: 100@1. barFormSource _ ScratchFrameMorph skinAt: #dividedImageFrameBar. barForm _ barFormSource copy: (((barFormSource width // 2) @ 0) extent: (1 @ barFormSource height)) ! ! !ScratchTabPaneMorph methodsFor: 'accessing' stamp: 'ee 1/29/2009 11:33'! createTab: tabID withLabel: aString onForm: aForm1 offForm: aForm2 | button | button _ ResizableToggleButton2 new target: self; actionSelector: #currentTab:; toggleMode: false; toggleButtonMode: true; arguments: (Array with: tabID). button offForm: aForm2 onForm: aForm1. self tab: button label: aString. button position: (lastTabAdded ifNil: [-0@0] ifNotNil: [(lastTabAdded right - 16)@(self bottom - button height)]). lastTabAdded _ button. self addMorph: button. (button height > self height) ifTrue: [ self height: button height. "if we just changed the height, then we must make sure all the previous tabs are aligned to the bottom" self submorphs do: [:m | (m isKindOf: ResizableToggleButton2) ifTrue: [ m height: self height; bottom: self bottom. m label top: m label top]]]. ! ! !ScratchTabPaneMorph methodsFor: 'accessing' stamp: 'ee 2/21/2005 23:30'! currentTab ^ currentTab.! ! !ScratchTabPaneMorph methodsFor: 'accessing' stamp: 'ee 4/14/2008 15:01'! currentTab: aString currentTab _ aString. self targetPane ifNil: [^ self]. (targetPane target isKindOf: ScratchStageMorph) ifTrue: [self setLabelForTab: 'Costumes' to: 'Backgrounds' localized] ifFalse: [self setLabelForTab: 'Costumes' to: 'Costumes' localized]. self lightUpCurrentTab. targetPane currentCategory: aString. ! ! !ScratchTabPaneMorph methodsFor: 'accessing' stamp: 'ee 2/20/2005 23:51'! targetPane ^ targetPane.! ! !ScratchTabPaneMorph methodsFor: 'accessing' stamp: 'ee 2/21/2005 23:13'! targetPane: aMorph targetPane _ aMorph.! ! !ScratchTabPaneMorph methodsFor: 'drawing' stamp: 'ee 2/5/2009 17:01'! drawSubmorphsOn: aCanvas "Display submorphs back to front, draw a bar where appropriate in between tabs" | f barWidth myBox clipC | myBox _ Rectangle origin: (self bounds origin) extent: (self bounds extent x - 20)@(self bounds extent y). clipC _ aCanvas copyClipRect: myBox. barWidth _ self width - 7. submorphs reverseDo:[:m | ((m isKindOf: ResizableToggleButton2) and: [m isOn]) ifTrue: [f _ Form extent: (barWidth @ barForm height) depth: 32. 10 to: barWidth by: barForm width do: [:x | barForm displayOn: f at: x@0 rule: Form blend]]. f ifNotNil: [clipC translucentImage: f at: ((self left - 13) @ (m bottom - 5))]. clipC fullDrawMorph: m]. ! ! !ScratchTabPaneMorph methodsFor: 'private' stamp: 'ee 1/29/2009 12:05'! lightUpCurrentTab (self submorphs) do: [:m | (m isKindOf: ResizableToggleButton2) ifTrue: [ m arguments first = currentTab ifTrue: [m setLabelColor: (Color r:(33/255) g:(33/255) b:(31/255)); on; comeToFront] ifFalse: [m setLabelColor: (Color r:(88/255) g:(89/255) b:(93/255)); off]]]. ! ! !ScratchTabPaneMorph methodsFor: 'private' stamp: 'nb 3/30/2008 17:15'! setLabelForTab: tabID to: aString | theTab wInitial wDelta | self submorphs do: [:m | ((m isKindOf: ResizableToggleButton2) and: [m arguments first = tabID]) ifTrue: [ theTab _ m. wInitial _ m width. self tab: m label: aString. wDelta _ m width - wInitial]]. theTab ifNotNil: [ self submorphs do: [:m | ((m isKindOf: ResizableToggleButton2) and: [m left > (theTab left)]) ifTrue: [ "scoot the others down if the tab's width changed" m position: (m position) + (wDelta@0)]]]. ! ! !ScratchTabPaneMorph methodsFor: 'private' stamp: 'ee 1/29/2009 11:02'! tab: aResizableToggleButton2 label: aString | label | aResizableToggleButton2 label: aString font: (ScratchFrameMorph getFont: #Tab). ScratchTranslator renderWithSqueak ifFalse: [ aResizableToggleButton2 forceUnicodeRendering: true]. "account for the size of the tab skin." aResizableToggleButton2 extent: (aResizableToggleButton2 extent - (0@10)). (label _ aResizableToggleButton2 findA: StringMorph) ifNotNil: [ "slide label up a little" label top: label top]. ! ! !ScratchTalkBubbleMorph methodsFor: 'intialization' stamp: 'ee 3/2/2009 11:53'! bePrompt: aBoolean self initFromForm: (ScratchFrameMorph skinAt: #promptBubbleFrame). aBoolean ifTrue: [leftPointerForm _ (ScratchFrameMorph skinAt: #promptBubblePointer)] ifFalse: [leftPointerForm _ (ScratchFrameMorph skinAt: #talkBubbleTalkPointer)]. rightPointerForm _ leftPointerForm flipBy: #horizontal centerAt: leftPointerForm center. self changed. ! ! !ScratchTalkBubbleMorph methodsFor: 'intialization' stamp: 'jm 3/21/2008 14:03'! beThoughtBubble: aBoolean "If true, then be a thought bubble. Otherwise, be a talk bubble." aBoolean ifTrue: [leftPointerForm _ (ScratchFrameMorph skinAt: #talkBubbleThinkPointer)] ifFalse: [leftPointerForm _ (ScratchFrameMorph skinAt: #talkBubbleTalkPointer)]. rightPointerForm _ leftPointerForm flipBy: #horizontal centerAt: leftPointerForm center. ! ! !ScratchTalkBubbleMorph methodsFor: 'intialization' stamp: 'jm 8/11/2008 17:56'! initialize "Initialize the forms for all my elements." super initialize. self initFromForm: (ScratchFrameMorph skinAt: #talkBubbleFrame). self beThoughtBubble: false. pointLeft _ false. contentsMorph _ MultilineStringMorph new centerText: true; font: (ScratchFrameMorph getFont: #TalkBubble). contentsMorph borderWidth: 0; lock. self addMorph: contentsMorph. ! ! !ScratchTalkBubbleMorph methodsFor: 'accessing' stamp: 'jm 7/9/2008 07:59'! message contentsMorph ifNil: [^ '']. ^ contentsMorph contents ! ! !ScratchTalkBubbleMorph methodsFor: 'accessing' stamp: 'jm 8/11/2008 18:08'! message: aString "(ScratchTalkBubbleMorph new message: 'Hello!!') openInWorld" | maxWidth xOffset | maxWidth _ 145. contentsMorph width: maxWidth; contents: aString asUTF8 withBlanksTrimmed; fitContents. self extent: contentsMorph extent + (0@28). xOffset _ contentsMorph width >= maxWidth ifTrue: [3] ifFalse: [((self width - contentsMorph width) // 2) + 3]. contentsMorph position: self position + (xOffset @ 5). ! ! !ScratchTalkBubbleMorph methodsFor: 'accessing' stamp: 'jm 2/11/2009 10:29'! noPointer "Use no pointer at all." leftPointerForm _ rightPointerForm _ Form extent: 0@leftPointerForm height depth: 1. ! ! !ScratchTalkBubbleMorph methodsFor: 'accessing' stamp: 'jm 12/1/2006 10:47'! pointLeft: aBoolean pointLeft _ aBoolean. self changed. ! ! !ScratchTalkBubbleMorph methodsFor: 'drawing' stamp: 'jm 12/19/2006 23:45'! drawOn: aCanvas | f x y | super drawOn: aCanvas. y _ self bottom - leftPointerForm height. pointLeft ifTrue: [f _ leftPointerForm. x _ self left + 7] ifFalse: [f _ rightPointerForm. x _ self right - 53]. aCanvas paintImage: f at: (x@y) truncated. ! ! !ScratchTalkBubbleMorph methodsFor: 'drawing' stamp: 'jm 12/1/2006 12:14'! hasTranslucentColor ^ false ! ! !ScratchTalkBubbleMorph methodsFor: 'geometry' stamp: 'jm 11/29/2006 16:40'! extent: aPoint super extent: (aPoint max: 65@52). ! ! !ScratchTalkBubbleMorph methodsFor: 'geometry' stamp: 'jm 7/9/2008 07:53'! insetBottom ^ self bottom - leftPointerForm height + 3 ! ! This class may eventually replace ScratchProcess and ScratchStackFrame. The current version operates on tuples rather than on command blocks. It was written as a test before implementing the same code in Java for the Java Scratch player. ! !ScratchThread methodsFor: 'entry points' stamp: 'jm 6/29/2005 17:33'! isRunning ^ done not ! ! !ScratchThread methodsFor: 'entry points' stamp: 'jm 7/3/2005 11:45'! runUntilYield "Execute commands until it is time to yield control, a 'done' command is executed, or it is time to yield control." ip > cmds size ifTrue: [done _ true]. done ifTrue: [^ self]. yield _ false. [yield] whileFalse: [ self evalCommand: (cmds at: ip). yield ifFalse: [ip _ ip + 1]. ip > cmds size ifTrue: [ self popState. yield _ true]]. ! ! !ScratchThread methodsFor: 'entry points' stamp: 'jm 7/2/2005 19:02'! startOn: anArrayOfCommands sprite: aSprite sprite _ aSprite. cmds _ anArrayOfCommands. ip _ 1. stack _ OrderedCollection new. yield _ false. done _ false. startTime _ nil. "used by timed commands like glide" tmp _ nil. "used by timed commands and repeat" ! ! !ScratchThread methodsFor: 'entry points' stamp: 'jm 11/15/2006 12:41'! stop done _ true. ! ! !ScratchThread methodsFor: 'var cmds' stamp: 'jm 11/15/2006 12:16'! changeVar: anArray "Read a variable and return its value." "Format of anArray is: (#changeVariable )" | arg | arg _ self evalArg: (anArray at: 4). #setVar:to: = (anArray at: 3) ifTrue: [sprite setVar: (anArray at: 2) to: arg] ifFalse: [sprite changeVar: (anArray at: 2) by: arg]. ! ! !ScratchThread methodsFor: 'var cmds' stamp: 'jm 6/28/2005 16:39'! readVar: anArray "Read a variable and return its value." ! ! !ScratchThread methodsFor: 'system cmds' stamp: 'jm 6/28/2005 17:22'! doBroadcastAndWait "Broadcast a message and wait until all threads started by that message have stopped." self halt. "not yet implemented" ! ! !ScratchThread methodsFor: 'system cmds' stamp: 'jm 6/28/2005 17:27'! doForever "Execute my enclosed blocks forever." | cmd | cmd _ cmds at: ip. self evalCmdList: (cmd at: 2). ! ! !ScratchThread methodsFor: 'system cmds' stamp: 'jm 6/28/2005 17:27'! doIf "Execute my enclosed blocks if my condition expression evaluates to true." | cmd | cmd _ cmds at: ip. (self evalArg: (cmd at: 2)) ifTrue: [ self evalCmdList: (cmd at: 3)]. ! ! !ScratchThread methodsFor: 'system cmds' stamp: 'jm 7/2/2005 19:04'! doRepeat "Execute my enclosed blocks the number of times given by my argument. tmp is used to count down the iterations." | cmd | cmd _ cmds at: ip. tmp ifNil: [ "first time" tmp _ self evalArg: (cmd at: 2)]. tmp <= 0 ifTrue: [^ self]. "repeat is finished" tmp _ tmp - 1. self evalCmdList: (cmd at: 3). ! ! !ScratchThread methodsFor: 'system cmds' stamp: 'jm 6/29/2005 17:33'! doReturn "Stop this thread and yield." done _ yield _ true. ! ! !ScratchThread methodsFor: 'system cmds' stamp: 'jm 6/28/2005 17:25'! doWaitUntil "Test an expression and proceed only when it becomes true (i.e. yield if it is false)." | cmd | cmd _ cmds at: ip. (self evalArg: (cmd at: 2)) ifFalse: [yield _ true]. ! ! !ScratchThread methodsFor: 'private' stamp: 'jm 6/29/2005 17:36'! evalArg: anObject "Evaluate the given argument. If it is a list (array), then it is an expression that must be evaluated. Otherwise it is a constant value that can be returned." (anObject isKindOf: Array) ifTrue: [^ self evalCommand: anObject]. ^ anObject ! ! !ScratchThread methodsFor: 'private' stamp: 'jm 6/28/2005 17:18'! evalCmdList: anArrayOrNil "Evaluate the given sequence of commands. A nil argument means to execute the empty list of commands." self pushState. anArrayOrNil ifNil: [cmds _ #()] ifNotNil: [cmds _ anArrayOrNil]. ip _ 0. ! ! !ScratchThread methodsFor: 'private' stamp: 'jm 6/29/2005 17:47'! evalCommand: anArray "Evaluate the given command and return it's value." | selector args | selector _ anArray first asSymbol. selector = #readVariable ifTrue: [^ self readVar: anArray]. selector = #changeVariable ifTrue: [^ self changeVar: anArray]. (SystemCommands includes: selector) ifTrue: [ ^ self perform: selector]. args _ (2 to: anArray size) collect: [:i | self evalArg: (anArray at: i)]. (args size = 1 and: [#(abs not sqrt) includes: selector]) ifTrue: [ ^ (args at: 1) perform: selector]. (args size = 2 and: [selector isInfix]) ifTrue: [ "binary math/logic ops" ^ (args at: 1) perform: selector with: (args at: 2)]. sprite perform: selector withArguments: args. ! ! !ScratchThread methodsFor: 'private' stamp: 'jm 7/2/2005 19:03'! popState "Restore the last state pushed onto the stack." | oldState | (stack isNil or: [stack size = 0]) ifTrue: [ cmds _ #(). ip _ 1. done _ yield _ true. ^ self]. oldState _ stack removeLast. cmds _ oldState at: 1. ip _ oldState at: 2. startTime _ oldState at: 3. tmp _ oldState at: 4. ! ! !ScratchThread methodsFor: 'private' stamp: 'jm 7/2/2005 19:02'! pushState "Save my current state on the stack." stack addLast: (Array with: cmds with: ip with: startTime with: tmp). ! ! !ScratchThread class methodsFor: 'class initialization' stamp: 'jm 12/19/2006 18:02'! initialize "self initialize" SystemCommands _ IdentitySet new. SystemCommands addAll: #( doForever doForeverIf doIf doRepeat doReturn doBroadcastAndWait doPlaySoundAndWait doWaitUntil). ! ! I show an updating thumbnail picture of my target morph. I allow the direction of my target to be set by dragging. ! !ScratchThumbnailMorph methodsFor: 'initialization' stamp: 'jm 4/27/2007 15:47'! initialize super initialize. self form: (Form extent: 46@46 depth: 16). form fillColor: Color transparent. form border: form boundingBox width: 1. showDirection _ true. lastUpdateMSecs _ -1. ! ! !ScratchThumbnailMorph methodsFor: 'accessing' stamp: 'nb 3/8/2008 13:40'! extent: aPoint "Only use squares" self form: (Form extent: aPoint depth: 16). form fillColor: Color transparent. form border: form boundingBox width: 1. showDirection _ true. lastUpdateMSecs _ -1.! ! !ScratchThumbnailMorph methodsFor: 'accessing' stamp: 'jm 4/27/2007 15:47'! showDirection: aBoolean showDirection _ aBoolean. ! ! !ScratchThumbnailMorph methodsFor: 'accessing' stamp: 'TIS 7/14/2003 14:08'! target ^ target ! ! !ScratchThumbnailMorph methodsFor: 'accessing' stamp: 'jm 6/22/2003 22:57'! target: aMorph target _ aMorph. self updateThumbnail. ! ! !ScratchThumbnailMorph methodsFor: 'drawing' stamp: 'jm 4/27/2007 15:48'! drawOn: aCanvas | offset | super drawOn: aCanvas. (target respondsTo: #rotationDegrees:) ifFalse: [^ self]. "nil or background" showDirection ifTrue: [ offset _ Point r: (self width / 2) - 1 degrees: target rotationDegrees. aCanvas line: self center to: self center + offset width: 1 color: Color blue]. ! ! !ScratchThumbnailMorph methodsFor: 'event handling' stamp: 'jm 11/1/2005 13:58'! doubleClick: evt "Set my target to it's normal orientation." (target respondsTo: #rotationDegrees:) ifFalse: [^ self]. target rotationDegrees: 0. self updateThumbnail. ! ! !ScratchThumbnailMorph methodsFor: 'event handling' stamp: 'jm 11/1/2005 13:55'! handlesMouseDown: evt ^ true ! ! !ScratchThumbnailMorph methodsFor: 'event handling' stamp: 'jm 11/1/2005 14:05'! mouseDown: evt draggingDirection _ false. evt shiftPressed ifTrue: [ target ifNotNil: [target makeVisible]. ^ self]. evt hand waitForClicksOrDrag: self event: evt. ! ! !ScratchThumbnailMorph methodsFor: 'event handling' stamp: 'jm 11/6/2007 09:18'! mouseMove: evt | p | draggingDirection ifFalse: [^ self]. (target respondsTo: #rotationDegrees:) ifFalse: [^ self]. p _ evt cursorPoint - self center. p r > 0 ifTrue: [target rotationDegrees: p theta radiansToDegrees rounded]. self updateThumbnail. ! ! !ScratchThumbnailMorph methodsFor: 'event handling' stamp: 'jm 11/1/2005 13:53'! startDrag: evt draggingDirection _ true. ! ! !ScratchThumbnailMorph methodsFor: 'stepping' stamp: 'jm 12/7/2004 19:07'! step "Optimization: Don't update unless the costume has changed." | delta | ((target respondsTo: #rotationDegrees) and: [lastRotationDegrees ~= target rotationDegrees]) ifTrue: [ self changed. "update the direction marker" lastRotationDegrees _ target rotationDegrees]. (target notNil and: [lastUpdateMSecs = target costumeChangeMSecs]) ifTrue: [^ self]. delta _ Time millisecondClockValue - lastUpdateMSecs. (delta < 0) | (delta > 100) ifTrue: [self updateThumbnail]. ! ! !ScratchThumbnailMorph methodsFor: 'stepping' stamp: 'jm 12/9/2008 16:49'! stepTime ^ 100 ! ! !ScratchThumbnailMorph methodsFor: 'stepping' stamp: 'jm 3/27/2005 18:12'! updateThumbnail | f e r | form fillColor: Color transparent. target ifNil: [^ self]. f _ target imageForm. ((f width <= form width) & (f height <= form height)) ifTrue: [ "target's image fits without shrinking" f displayOn: form at: ((form extent - f extent) // 2). ^ self changed]. f width > f height ifTrue: [e _ form width @ ((f height * form width) // f width)] ifFalse: [e _ ((f width * form height) // f height) @ form height]. e _ e max: (8@8). r _ Rectangle center: (form extent // 2) extent: e. (WarpBlt toForm: form) sourceForm: f; cellSize: 2; "do smoothing; this also installs a colormap" combinationRule: Form paint; copyQuad: f boundingBox innerCorners toRect: (r insetBy: 2). lastUpdateMSecs _ target costumeChangeMSecs. self changed. ! ! !ScratchToolTipMorph methodsFor: 'accessing' stamp: 'jm 3/14/2009 10:11'! initialCursorPoint: aPoint initialCursorPoint _ aPoint. ! ! !ScratchToolTipMorph methodsFor: 'accessing' stamp: 'jm 5/7/2009 11:40'! message: aString message _ StringMorph contents: aString asUTF8 font: self messageFont. self addMorph: message. self extent: message extent + (12@3). message position: 6@1. ! ! !ScratchToolTipMorph methodsFor: 'accessing' stamp: 'jm 3/15/2009 14:14'! messageColor: aColor message color: aColor. ! ! !ScratchToolTipMorph methodsFor: 'accessing' stamp: 'jm 5/7/2009 11:39'! messageFont ^ ScratchFrameMorph getFont: #ToolTip ! ! !ScratchToolTipMorph methodsFor: 'accessing' stamp: 'jm 12/15/2005 14:31'! target: aMorph target _ aMorph. ! ! !ScratchToolTipMorph methodsFor: 'stepping' stamp: 'jm 5/5/2009 17:15'! step initialCursorPoint ifNotNil: [ (Sensor cursorPoint - initialCursorPoint) r > 80 ifTrue: [ target ifNotNil: [target setProperty: #toolTip toValue: nil]. self delete]]. ! ! !ScratchToolTipMorph methodsFor: 'stepping' stamp: 'jm 3/14/2009 10:16'! stepTime ^ 200 ! ! !ScratchToolTipMorph class methodsFor: 'instance creation' stamp: 'jm 12/15/2005 14:31'! string: str for: morph ^ self new message: str; target: morph; color: Color paleYellow; borderWidth: 1; borderColor: Color black ! ! !ScratchToolTipMorph class methodsFor: 'utilities' stamp: 'jm 8/22/2006 08:04'! clearToolTips "Delete all tooltip morphs from the world." World ifNil: [^ self]. World submorphsDo: [:m | (m isKindOf: ScratchToolTipMorph) ifTrue: [m delete]]. ! ! I manage language translations for Scratch. All of my code in in class methods. ! !ScratchTranslator class methodsFor: 'class initialization' stamp: 'jm 6/9/2008 14:50'! initialize "ScratchTranslator initialize" TranslationDict _ Dictionary new. ISODict _ Dictionary new. MIDITranslationSet _ Set new. UITranslationSet _ Set new. RenderAntiAliasing _ false. HeaderString _ ''. RenderPlugin _ nil. self setRenderingHints. ColonSuffix _ ':' asUTF8, (UTF32 with: 16r200F) asUTF8. EllipsesSuffix _ '...' asUTF8, (UTF32 with: 16r200F) asUTF8. ! ! !ScratchTranslator class methodsFor: 'startup' stamp: 'jm 10/29/2008 13:22'! shutDown "Clear the rendering plugin." RenderPlugin _ nil. ! ! !ScratchTranslator class methodsFor: 'startup' stamp: 'jm 6/27/2008 21:23'! startUp "self startup" self detectRenderPlugin. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 10/14/2007 17:22'! addMIDITranslation: aString MIDITranslationSet add: aString. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'jm 6/9/2008 09:36'! addSensorTranslations "Add translations for the sensor names such as 'resistance-A' and 'A connected' from the translations of the root words if they exist." | root prefix postfix | root _ TranslationDict at: 'connected' ifAbsent: [nil]. root ifNotNil: [ #(A B C D) do: [:ch | prefix _ ch asString, ' '. root isUnicode ifTrue: [prefix _ UTF8 withAll: prefix]. TranslationDict at: (prefix, 'connected') put: (prefix, root)]]. root _ TranslationDict at: 'resistance' ifAbsent: [nil]. root ifNotNil: [ #(A B C D) do: [:ch | postfix _ '-', ch asString. TranslationDict at: ('resistance', postfix) put: (root, postfix)]]. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 9/21/2007 16:00'! addUITranslation: aString UITranslationSet add: aString. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 9/20/2007 18:36'! checkAllTranslations "Check all the translation dictionaries." "self checkAllTranslations" | untranslated | self languageNames do: [:lang | self setLanguage: lang. untranslated _ self checkTranslationDict. untranslated size > 0 ifTrue: [self halt: lang, ' missing ', untranslated size printString]]. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 9/20/2007 18:37'! checkTranslationDict "Do some sanity check on the current translation dictionary." | categories allSpecs mathOps | TranslationDict size = 0 ifTrue: [^ #()]. "English" categories _ #(motion control looks pen numbers sound sensing variables). categories do: [:cat | self assert: [TranslationDict includesKey: cat asString]]. "make sure all translations have the same number arguments" TranslationDict associationsDo: [:assoc | self assert: [(self parameterSpecs: assoc key) = (self parameterSpecs: assoc value)]]. "makes sure there is a translation for every block in every category (except variables)" allSpecs _ Set new: 100. (ScratchSpriteMorph blockSpecs, ScratchStageMorph blockSpecs) do: [:spec | (spec isKindOf: Array) ifTrue: [allSpecs add: spec]]. allSpecs add: #('else'). "remove obsolete specs and math operators" ScratchSpriteMorph obsoleteBlockSpecs do: [:spec | "remove obsolete block specs" allSpecs remove: spec ifAbsent: []]. mathOps _ ('+-*/<=>' asArray collect: [:ch | '%n ', ch asString, ' %n']) asSet. allSpecs _ allSpecs select: [:spec | (mathOps includes: spec first) not]. allSpecs _ allSpecs asArray sort: [:s1 :s2 | s1 first < s2 first]. ^ allSpecs select: [:spec | (TranslationDict includesKey: spec first) not] ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'jm 4/25/2008 15:57'! colonSuffix "Answer a colon suffix. If the current language is RTL, then include the Unicode RTL mark after the colon." ^ self isRTL ifTrue: [ColonSuffix] ifFalse: [':'] ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'jm 10/27/2007 09:49'! currentLanguage Language ifNil: [Language _ 'en']. ^ Language ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 5/15/2009 14:17'! doNotTranslate ^ #( '!!' '0' '1' '2' '3' '4' '5' '6' '7' '8' '9' '10' 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 'u' 'v' 'w' 'x' 'y' 'z' 'A' 'B' '%n * %n' '%n + %n' '%n - %n' '%n / %n' '%n < %n' '%n = %n' '%n > %n' '%s > %s' '%s = %s' '%s < %s' '*' '+' '-' '/' '<' '=' '>' 'sin' 'cos' 'tan' 'asin' 'acos' 'atan' 'ln' 'log' 'e ^' '10 ^' 'enter' 'motor' 'Scratch' 'Sprite1' '[' ']' 'set translation formatting...' 'enable remote access...' 'Host Mesh' 'Join Mesh' 're-record') ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'jm 4/25/2008 15:55'! ellipsesSuffix "Answer an ellipses suffix (three periods). If the current language is RTL, then include the Unicode RTL mark after the colon." ^ self isRTL ifTrue: [EllipsesSuffix] ifFalse: ['...'] ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 10/4/2007 14:20'! extractQuotedStringFrom: aString "Extract the contents of a quoted string from a translation file line. If the line contains no double-quote characters, the string after the first space character is returned or, if there is no space character, the entire line. Only two escape sequences are currently recognized: newline and double-quote." | i s result ch | i _ aString indexOf: $". i = 0 ifTrue: [aString indexOf: String space]. s _ ReadStream on: (aString copyFrom: i + 1 to: aString size). result _ WriteStream on: String new. [s atEnd] whileFalse: [ ch _ s next. ch = $" ifTrue: [^ result contents]. ch = $\ ifTrue: [ ch _ s next. ch = $n ifTrue: [result cr]. ch = $" ifTrue: [result nextPut: $"]] ifFalse: [result nextPut: ch]]. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 7/9/2008 16:12'! formattingHeaderFields ^ #( 'Language-Name' 'Language-Direction' 'Font-Scale' 'Suppress-Bold' 'Win-Font' 'Mac-Font' 'Linux-Font') ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 8/8/2008 20:56'! formattingSectionForPOT | s | s _ '# Language name as you''d like it to appear in the Languages menu # (Required) msgid "Language-Name" msgstr "" # Directionality of language # LTR = Left to Right # RTL = Right to Left msgid "Language-Direction" msgstr "" # Scale to apply to font size (2 for twice as large) # Use this if the font is too small for legibility on the Scratch interface msgid "Font-Scale" msgstr "" # Set to ''true'' or ''false'' # Use this if you do not want any of the text to be bolded, for legibility msgid "Suppress-Bold" msgstr "" # Font to use on a Windows system msgid "Win-Font" msgstr "" # Font to use on a Mac system msgid "Mac-Font" msgstr "" # Font to use on a Linux system msgid "Linux-Font" msgstr "" '. s _ s copyReplaceTokens: String cr with: String crlf. ^ s. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'jm 6/2/2008 20:48'! importLanguagesList "Import the list of languages and language codes translated for Scratch by file names from 'Help/Translations'" | dir code lang | ISODict _ Dictionary new. ISODict at: 'en' put: 'English'. dir _ self translationDir. dir fileNames do: [:f | (f endsWith: '.po') ifTrue: [ code _ f copyFrom: 1 to: (f size - 3). lang _ self extractLanguageFromFileNamed: (dir fullNameFor: f). lang ifNil: [lang _ code]. self canRenderUnicode ifTrue: [self insertISOCode: code forLanguage: lang] ifFalse: [self insertISOCode: code forLanguage: code]]]. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 10/14/2007 14:06'! insertISOCode: code forLanguage: lang ISODict at: code put: lang. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 7/9/2008 18:21'! isRTLMath "Returns true if the header 'Language-Direction' is set to 'RTL-math'" ^ IsRTLMath ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'jm 6/1/2008 18:03'! isoCodeForName: aString (ISODict includes: aString) ifFalse:[^ aString] ifTrue:[^ ISODict keyAtValue: aString]. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 10/14/2007 14:08'! isoDict ^ ISODict ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 9/20/2007 18:44'! labelPartsFor: aString "Answer a collection label strings for the translation of given block label. Currently handles one or two-part labels. In a two-part label, the label is split at the field name begginning with a percent sign. For example, 'when %s clicked' would yield the two label parts 'when' and 'clicked'." | s i p1 p2 | s _ ScratchTranslator translationFor: aString. i _ s indexOf: $% ifAbsent: [^ Array with: s with: '']. p1 _ (s copyFrom: 1 to: i - 1) withBlanksTrimmed. p2 _ (s copyFrom: i + 2 to: s size) withBlanksTrimmed. ^ Array with: p1 with: p2 ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 10/24/2007 16:10'! languageNames "Answer a list of language names for the languages menu. These are generally in the native language (e.g. 'Espa–ol') and must match the strings in the setLanguage: method." self importLanguagesList. ^ ISODict values sort ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 10/14/2007 17:22'! midiTranslationSet ^ MIDITranslationSet ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 9/20/2007 18:41'! parameterSpecs: aString "Answer the sequence of parameter specs (e.g. %n) for the given block specification. Used for checking translations." "self parameterSpecs: 'this %b is %s a %c test %n%'" "self parameterSpecs: 'this %b'" "self parameterSpecs: 'this %'" "self parameterSpecs: 'this'" | result i | result _ OrderedCollection new. i _ 1. [i < aString size] whileTrue: [ i _ aString indexOf: $% startingAt: i. i = 0 ifTrue: [i _ aString size]. "% not found" i < aString size ifTrue: [ result addLast: (aString copyFrom: i to: i + 1)]. i _ i + 2]. ^ result asArray ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 5/12/2008 17:08'! resetMIDITranslationSet MIDITranslationSet _ Set new. #('Accordion' 'Acoustic Bass' 'Acoustic Bass Drum' 'Acoustic Grand' 'Acoustic Snare' 'Agogo' 'Alto Sax' 'Applause' 'Bagpipe' 'Banjo' 'Baritone Sax' 'Bass Drum 1' 'Bassoon' 'Bird Tweet' 'Blown Bottle' 'Brass Section' 'Breath Noise' 'Bright Acoustic' 'Cabasa' 'Celesta' 'Cello' 'Chinese Cymbal' 'Choir Aahs' 'Church Organ' 'Clarinet' 'Claves' 'Clavinet' 'Closed Hi-Hat' 'Contrabass' 'Cowbell' 'Crash Cymbal 1' 'Crash Cymbal 2' 'Distortion Guitar' 'Drawbar Organ' 'Dulcimer' 'Electric Bass (finger)' 'Electric Bass (pick)' 'Electric Clean Guitar' 'Electric Grand' 'Electric Jazz Guitar' 'Electric Muted Guitar' 'Electric Piano 1' 'Electric Piano 2' 'Electric Snare' 'English Horn' 'FX 1 (rain)' 'FX 2 (soundtrack)' 'FX 3 (crystal)' 'FX 4 (atmosphere)' 'FX 5 (brightness)' 'FX 6 (goblins)' 'FX 7 (echoes)' 'FX 8 (sci-fi)' 'Fiddle' 'Flute' 'French Horn' 'Fretless Bass' 'Glockenspiel' 'Guitar Fret Noise' 'Guitar Harmonics' 'Gunshot' 'Hand Clap' 'Harmonica' 'Harpsichord' 'Helicopter' 'Hi Bongo' 'Hi Wood Block' 'Hi-Mid Tom' 'High Agogo' 'High Floor Tom' 'High Timbale' 'High Tom' 'Honky-Tonk' 'Kalimba' 'Koto' 'Lead 1 (square)' 'Lead 2 (sawtooth)' 'Lead 3 (calliope)' 'Lead 4 (chiff)' 'Lead 5 (charang)' 'Lead 6 (voice)' 'Lead 7 (fifths)' 'Lead 8 (bass+lead)' 'Long Guiro' 'Long Whistle' 'Low Agogo' 'Low Bongo' 'Low Conga' 'Low Floor Tom' 'Low Timbale' 'Low Tom' 'Low Wood Block' 'Low-Mid Tom' 'Maracas' 'Marimba' 'Melodic Tom' 'Music Box' 'Mute Cuica' 'Mute Hi Conga' 'Mute Triangle' 'Muted Trumpet' 'Nylon String Guitar' 'Oboe' 'Ocarina' 'Open Cuica' 'Open Hi Conga' 'Open Hi-Hat' 'Open Triangle' 'Orchestra Hit' 'Orchestral Strings' 'Overdriven Guitar' 'Pad 1 (new age)' 'Pad 2 (warm)' 'Pad 3 (polysynth)' 'Pad 4 (choir)' 'Pad 5 (bowed)' 'Pad 6 (metallic)' 'Pad 7 (halo)' 'Pad 8 (sweep)' 'Pan Flute' 'Pedal Hi-Hat' 'Percussive Organ' 'Piccolo' 'Pizzicato Strings' 'Recorder' 'Reed Organ' 'Reverse Cymbal' 'Ride Bell' 'Ride Cymbal 1' 'Ride Cymbal 2' 'Rock Organ' 'Seashore' 'Shakuhachi' 'Shamisen' 'Shanai' 'Short Guiro' 'Short Whistle' 'Side Stick' 'Sitar' 'Slap Bass 1' 'Slap Bass 2' 'Soprano Sax' 'Splash Cymbal' 'Steel Drums' 'Steel String Guitar' 'String Ensemble 1' 'String Ensemble 2' 'Synth Bass 1' 'Synth Bass 2' 'Synth Drum' 'Synth Voice' 'SynthBrass 1' 'SynthBrass 2' 'SynthStrings 1' 'SynthStrings 2' 'Taiko Drum' 'Tambourine' 'Tango Accordion' 'Telephone Ring' 'Tenor Sax' 'Timpani' 'Tinkle Bell' 'Tremolo Strings' 'Trombone' 'Trumpet' 'Tuba' 'Tubular Bells' 'Vibraphone' 'Vibraslap' 'Viola' 'Violin' 'Voice Oohs' 'Whistle' 'Woodblock' 'Xylophone') do:[:t | self addMIDITranslation: t]. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'jm 6/29/2009 17:26'! resetUITranslationSet UITranslationSet _ Set new. #('Normal' 'new' 'New' 't' 'New sound:' 'New sprite:' 'No' 'New costume:' 'New Filename:' 'New background:' 'No motion blocks' 'OK' 'New Sprite' '3' 'No variables.' 'no scripts' 'm' 'normal readout' 'No serial ports found' 'No MIDI ports currently available.' 'Open' 'Off' 'f' 'Open Project' 'On' 'Preparing project...' 'or' 'Open a Scratch project' 'only face left-right' '10 ^' 'B' '10' 'play' 'Pictures' 'Password:' 'Project Notes' 'Project notes' 'Paint' 'pop' 'Project uploaded.' 'u' 'Project name' 'Please enter a number' 'Paint Editor' 'Projects' 'Paintbrush' 'pixelate' 'Project author:' 'Paint new sprite' 'Percent? (100 gives original size)' '4' 'n' 'abs' 'atan' 'Position the cross-hair to set the rotation center' 'asin' 'Quit' 'Quit Scratch' 'About Scratch' 'and' '-' 'acos' 'all' 'any' 'Brush size: ' 'g' '1 script' 'add comment' 'Animation' 'Art' 'add comment here' 'About this project:' 'Redo' 'right' 'resistance' 'backgrounds' 'Backgrounds' 'button pressed' 'Reference Guide' 'button' 'save picture of stage...' 'recording' 'Save As' 'record' 'Record' 'right arrow' '<' 'background' 'brightness' 'Remove viewer from stage' 'v' 're-record' 'set translation formatting...' 'reverse' 'Rotate clock-wise' 'Rotation degrees?' 'resize this sprite' 'rotate this sprite' 'Remote sensor connections enabled' 'Reading' 'Rotate counter-clock-wise' '5' 'Clear' 'o' 'space' 'Sounds' 'slider' 'cancel' 'sin' 'Sprite1' 'Sprites' 'connected' 'Stage' 'Could not read' 'stop' 'Choose a folder' 'color' 'Sound Recorder' 'cos' 'Save failed' 'Slider range:' 'costumes' 'sqrt' 'set slider min and max' 'Costumes' 'show ScratchBoard watcher' 'Cancel' 'Single-step speed?' 'costume' 'Computer' 'Sounds compressed' 'Set language' 'Camera' 'Clear canvas' 'Compress Sounds' 'select serial/USB port' 'Sprite' 'e ^' 'close port' 'Close dialog?' 'Compress Images' 'Rectangle tool (draw outlined or filled rectangle or square)' 'Save this project' 'Share this project' 'Scratch' 'Save the current project' 'Share' 'Switch to full stage' 'Show Motor Blocks' 'Could not write file' 'Switch to small stage' 'Stage selected:' 'Switch to presentation mode' 'a' 'Copy' 'tan' 'Set costume center' 'Save changes before quitting?' 'Shrink' 'Story' 'Save Project' 'Close paint editor?' 'thing' 'Support Site' 'Close sound recorder?' 'Select tool (move, modify, or delete selection)' 'Set Single Stepping' 'Start Single Stepping' 'Save the current project?' 'Simulation' 'Set rotation center' 'Create an empty project' 'Share This Project Online' 'Save' 'Stop everything' 'Scripts' 'Close and continue' 'Switch colors' 'Turbo speed' 'h' 'Stamp tool (select area, then stamp copies of it)' 'Done' 'can rotate' 'Tags:' 'Delete a list' 'Don''t Save' 'down arrow' 'Shrink sprite' 'clean up' 'distance' 'disable remote sensor connections' 'Save Stage Shot' 'Save Scripts Snapshot' 'this way' 'delete' 'Delete' 'drag to rotate' 'down' 'Text tool (edit text layer)' 'duplicate' 'Duplicate' 'Documents' 'Delete a variable' 'drag to resize' 'don''t rotate' 'Desktop' 'that way' 'p' 'Delete this background' 'Export failed' 'up' 'That variable name is already in use' 'tilt' 'Delete this sprite' 'enter' 'Eraser' 'Save a copy of the current project' 'Exit presentation mode' 'Export Costume' 'Undelete' 'Examples' 'Could not read project; file may be damaged' 'Updating thumbnails' 'Exit presentation' 'Export Sprite' 'Create account' 'Choose new sprite from file' 'Compress sounds and images' 'Eraser size' 'export this sprite' '/' 'Delete this costume' 'Connecting to ' '=' 'Upload succeeded!!' 'true' 'up arrow' 'enable remote sensor connections' 'export' 'Extras menu' 'Export Sound' 'Eyedropper tool (select a color)' 'Enter presentation mode' 'save picture of scripts' 'Upload to Scratch Server' 'Start green flag scripts' 'Export Background' 'Data sent. Waiting for response...' 'edge' '6' 'Failed:' 'Edit' 'Ellipse tool (draw outlined or filled ellipse or circle)' 'draggable on website?' 'Stop Single Stepping' 'File' 'Delete this sound' 'w' 'Variable name?' 'View on stage' 'For all sprites' 'b' 'enable remote access...' 'false' 'first' 'Undo' 'Flip vertically' 'File not found' 'Flip horizontally' 'i' 'turn into new sprite' 'fisheye' 'For this sprite only' 'Sound quality:' 'The file name already exists. Overwrite existing file?' 'Uploading' 'Error!!' 'export this costume' 'Flash blocks (fast)' 'export this sound' 'Flash blocks (slow)' '!!' '>' 'x' 'Hmm...' 'Fill tool (fill areas with color or gradient)' 'Game' 'Grow' 'Getting Started' 'world' '7' 'whirl' 'ghost' 'What''s your name?' 'q' 'Get the last thing deleted' 'Grow sprite' 'Write Multiple Project Summaries' 'Get surprise sprite' 'Write Project Summary' 'Go To Scratch Website' 'grab screen region for new costume' '0' 'j' 'grab screen region for new sprite' 'help' 'Home' 'Help' 'Help Page' 'Hello!!' 'hello ' 'Help Screens' 'Hide Motor Blocks' 'Host Mesh' 'c' '(empty)' 'High (biggest)' 'Yes' 'y' 'JPEG Quality (10-100)?' 'Import Project' 'import project' 'Import Image' 'Images compressed' 'import' 'Import' 'Import Sound' 'Import Costume' 'Your project is now online at' 'Import Background' '8' 'Incompatible Scratch file format' 'Import List' 'r' 'Your Scratch website login name:' 'Import an image on top of your current canvas' 'Is the folder read-only?' '1' 'k' 'Zoom in' 'Join Mesh' #[ 'Zoom out' '*' 'd' 'kbytes' 'z' '9' 's' 'left' 'light' 'Language' 'ln' 'List name?' 'Low' 'log' '2' 'last' 'l' 'left arrow' 'length' 'large readout' 'Lowest (smallest)' 'Line tool (draw lines)' '+' 'e' 'Music' 'mosaic' 'Message name:' 'meow' 'mod' #] 'Min:' 'more' 'mouse-pointer' 'Move' 'Make a variable' 'Max:' 'More tags:' 'Make a list' 'My Projects' 'mouse x:' 'mouse y:' 'A' 'New file name?') do: [:t | self addUITranslation: t]. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'jm 6/9/2008 14:55'! setLanguage: aString "Set the current language. If the language is not supported, use English (i.e. an empty translation dictionary)." | dict | "default to English" Language _ 'en'. TranslationDict _ Dictionary new. HeaderString _ ''. self setRenderingHints. "clear rendering hints" ScratchTranslator detectRenderPlugin. aString = 'en' ifTrue: [^ self]. dict _ self importTranslation: aString, '.po'. dict ifNotNil: [ Language _ aString. TranslationDict _ dict. HeaderString _ dict at: '' ifAbsent: ['']. dict removeKey: '' ifAbsent: []. self setRenderingHints. RenderWithSqueak ifTrue: [self convertToMacRoman]. self isRTL ifTrue: [self fixAmbigousRTLPunctuation]. self addSensorTranslations]. ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'ee 11/21/2007 15:13'! translationDict ^ TranslationDict ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'jm 7/11/2008 14:36'! translationFor: englishString "Return the translation of the given (English) string for the current language. If there is no entry for the given string, return the original string." | s | s _ TranslationDict at: englishString ifAbsent: [englishString]. s size = 0 ifTrue: [s _ englishString]. RenderWithSqueak ifTrue: [s isUnicode ifTrue: [s _ s asMacRoman]] ifFalse: [s _ s asUTF8]. ^ s ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'jm 10/28/2007 16:29'! uiTranslationSet #('' '?'), ScriptableScratchMorph blockSpecsForTranslation do: [:e | UITranslationSet remove: e ifAbsent: []]. ^ UITranslationSet ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'jm 10/28/2007 16:30'! uiTranslationSetAsSortedArray ^ self uiTranslationSet asArray sort: [:a :b | a asLowercase <= b asLowercase] ! ! !ScratchTranslator class methodsFor: 'language translation' stamp: 'jm 6/12/2008 05:02'! varSpecTranslationFor: spec varName: varName "Return the translation of a variable setter command spec for the given variable." | s i | s _ ScratchTranslator translationFor: spec. i _ s indexOfSubCollection: '%v' startingAt: 1. ^ (s copyFrom: 1 to: i - 1), varName, (s copyFrom: (i + 2) to: s size). ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 6/2/2008 16:43'! canRenderUnicode ^ RenderPlugin notNil ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'ee 6/29/2008 13:33'! centerOffsetForButtonWithFont: aStrikeFont "Answer the vertical offset above the center of a button for the given font. If the translator has provided a render hint string, return an offset that will center the first character of that string. Otherwise, return an offset that will center a lowercase 'x'." "[self centerOffsetForFont: (StrikeFont fontName: 'VerdanaBold' size: 10)] msecs" | f r vOffset | (RenderCenterOffsetCache includesKey: aStrikeFont) ifTrue: [ ^ RenderCenterOffsetCache at: aStrikeFont]. f _ (StringMorph contents: self renderHintString font: aStrikeFont) imageForm. r _ f rectangleEnclosingPixelsNotOfColor: Color transparent. vOffset _ r top + (r height // 2). "offset of string morph above the centerline of a button to center the given letter" r height = 0 ifTrue: [vOffset _ f height // 2]. RenderCenterOffsetCache at: aStrikeFont put: vOffset. ^ vOffset ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'ee 6/29/2008 13:33'! centerOffsetForLabelWithFont: aStrikeFont | co | co _ self centerOffsetForButtonWithFont: aStrikeFont. ^ co - 8. ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 6/9/2008 12:06'! convertToMacRoman "Convert my translations from UTF8 to MacRoman." | s | TranslationDict associationsDo: [:assoc | (assoc key asLowercase ~= 'language-name') ifTrue: [ s _ assoc value. s isUnicode ifTrue: [ assoc value: s asMacRoman]]]. ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 7/2/2008 12:34'! detectRenderPlugin "Determine which plugin is available for rendering Unicode." "self detectRenderPlugin" | hasPlugin | RenderPlugin _ nil. RenderCenterOffsetCache _ IdentityDictionary new. RenderVerticalTrimCache _ IdentityDictionary new. "first try the Uniscribe plugin" hasPlugin _ true. [UnicodePlugin primMeasureString: 'test'] ifError: [hasPlugin _ false]. hasPlugin ifTrue: [RenderPlugin _ UnicodePlugin. ^ self]. "then try the Pango plugin" hasPlugin _ true. [PangoPlugin2 primMeasureString: 'test'] ifError: [hasPlugin _ false]. hasPlugin ifTrue: [RenderPlugin _ PangoPlugin2. ^ self]. "if we get here, we don't have a Unicode rendering plugin" RenderPlugin _ nil. ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'ee 5/23/2008 15:08'! fixAmbigousRTLPunctuation "In a right-to-left language such as Arabic, punctuation characters such as period can be treated as either right-to-left or left-to-right characters. When they appear at the beginning of a string, the directionality is ambiguous. We assume that translation strings that start with punctuation characters are intended to start in left-to-right mode. This method prefixes punctuation characters with the Unicode LTR mark character, U+200E." "Note: Embedded colons are prefixed with the LTR mark everwhere that they appear since block translation strings are broken into component parts that can start with a colon." | punctuation colon prefix utf32 s | punctuation _ '.!!%#' asByteArray. colon _ $: asciiValue. prefix _ UTF32 with: 16r200E. TranslationDict keys do: [:k | ((TranslationDict at: k) isKindOf: UTF8) ifTrue: [ utf32 _ (UTF8 withAll: (TranslationDict at: k)) asUTF32. (utf32 size > 0 and: [punctuation includes: utf32 first]) ifTrue: [ TranslationDict at: k put: (prefix, utf32) asUTF8]. (utf32 includes: colon) ifTrue: [ s _ WriteStream on: (UTF32 new: 100). utf32 do: [:ch | ch = colon ifTrue: [s nextPut: 16r200E]. s nextPut: ch]. TranslationDict at: k put: s contents asUTF8]]]. ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 6/29/2008 11:20'! formFor: aString font: aStrikeFont fgColor: fgColor bgColor: bgColor "Answer a Form containing the given string in the given font and color rendered with the current rendering system. Answer nil if no rendering system is available." "(self formFor: 'Hello, Scratch!!' font: (StrikeFont fontName: 'VerdanaBold' size: 48) fgColor: Color black bgColor: Color blue) display" | f s | RenderPlugin ifNil: [^ nil]. self setFont: aStrikeFont antialias: RenderAntiAliasing. RenderAntiAliasing ifTrue: [RenderPlugin setColorFG: fgColor BG: bgColor bgTransparent: true] ifFalse: [RenderPlugin setColorFG: fgColor BG: Color transparent bgTransparent: true]. s _ aString. s isUnicode ifTrue: [s _ s asUTF8]. "convert from UTF32 to UTF8 if necessary" f _ Form extent: (RenderPlugin primMeasureString: s) depth: 32. RenderPlugin drawString: s on: f. ^ f ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 6/29/2008 11:30'! formFor: aString font: aStrikeFont fgColor: fgColor bgColor: bgColor suppressAntiAliasing: suppressAntiAliasing "Answer a Form containing the given string in the given font and color rendered with the current rendering system. The suppressAntiAliasing flag is useful when the background color is not constant, such as when part of the text is highlighted during editing. Answer nil if no rendering system is available." "(self formFor: 'Hello, Scratch!!' font: (StrikeFont fontName: 'VerdanaBold' size: 48) fgColor: Color black bgColor: Color blue) display" | f s | RenderPlugin ifNil: [^ nil]. self setFont: aStrikeFont antialias: (RenderAntiAliasing & suppressAntiAliasing not). RenderAntiAliasing ifTrue: [RenderPlugin setColorFG: fgColor BG: bgColor bgTransparent: true] ifFalse: [RenderPlugin setColorFG: fgColor BG: Color transparent bgTransparent: true]. s _ aString. s isUnicode ifTrue: [s _ s asUTF8]. "convert from UTF32 to UTF8 if necessary" f _ Form extent: (RenderPlugin primMeasureString: s) depth: 32. RenderPlugin drawString: s on: f. ^ f ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 7/11/2008 17:57'! isRTL: aBoolean IsRTL _ aBoolean. ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 3/14/2009 09:43'! renderHintString "Answer a string to be used as an example button lable to adjust button size and label centering." "self renderHintString" "self showHintString" | result srcs | result _ RenderHintString. result ifNil: [ result _ UTF8 new. srcs _ #('New' 'Open' 'Save' 'Save As' 'Share!!' 'Undo' 'Language' 'Extras' 'Want Help?' 'motion' 'looks' 'sound' 'pen' 'control' 'sensing' 'operators' 'variables'). srcs do: [: s | result _ result, (self translationFor: s)]]. RenderWithSqueak ifTrue: [result _ String withAll: result]. ^ result ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'ee 3/8/2008 18:16'! renderScale ^ RenderScale ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 7/9/2008 23:33'! renderWithSqueak ^ RenderWithSqueak ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 4/8/2009 17:50'! setFont: aStrikeFont antialias: antialiasFlag | font isBold fontName fontSize | font _ aStrikeFont. font ifNil: [font _ StrikeFont fontName: 'Times' size: 14]. fontName _ font name. fontSize _ (font pointSize * RenderScale) rounded. isBold _ false. (fontName asLowercase endsWith: 'bold') ifTrue: [ fontName _ fontName copyFrom: 1 to: (fontName size - 4). isBold _ true]. aStrikeFont isOSFont ifFalse: [ (fontName beginsWith: 'VerdanaBoldNarrowSpace') ifTrue: [ fontName _ 'Verdana'. isBold _ true]. (fontName beginsWith: 'Times') ifTrue: [ "Squeak font; not used by Scratch" fontName _ 'Comic'. fontSize _ font pointSize + 1]. RenderFont ifNotNil: [fontName _ RenderFont]. RenderSuppressBold ifTrue: [isBold _ false]]. RenderPlugin primSetFont: fontName size: fontSize bold: isBold italic: false antialias: antialiasFlag. ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 7/11/2008 14:10'! setRenderingHints "Set optional rendering hints from fields in the translation file header. If a given hint is not explicitly set by the header, set it to its default value." | s | "default values:" IsRTL _ false. IsRTLMath _ false. RenderAntiAliasing _ Smalltalk isMacOSX. RenderFont _ nil. RenderHintString _ nil. RenderScale _ 1. RenderSuppressBold _ false. RenderCenterOffsetCache _ IdentityDictionary new. RenderVerticalTrimCache _ IdentityDictionary new. RenderWithSqueak _ true. TranslationDict isEmpty ifTrue: [^ self]. (TranslationDict includesKey: 'Language-Direction') ifTrue: [ IsRTLMath _ (TranslationDict at: 'Language-Direction') asString asUppercase = 'RTL-MATH'. IsRTLMath ifTrue: [IsRTL _ true] ifFalse: [IsRTL _ (TranslationDict at: 'Language-Direction') asString asUppercase = 'RTL']]. Smalltalk isWindows ifTrue: [ ((TranslationDict includesKey: 'Win-Font') and: [(TranslationDict at: 'Win-Font') size > 0]) ifTrue: [RenderFont _ TranslationDict at: 'Win-Font']]. Smalltalk isMacOSX ifTrue: [ ((TranslationDict includesKey: 'Mac-Font') and: [(TranslationDict at: 'Mac-Font') size > 0]) ifTrue: [RenderFont _ TranslationDict at: 'Mac-Font']]. (Smalltalk isWindows | Smalltalk isMacOSX) ifFalse: [ ((TranslationDict includesKey: 'Linux-Font') and: [(TranslationDict at: 'Linux-Font') size > 0]) ifTrue: [RenderFont _ TranslationDict at: 'Linux-Font']]. s _ TranslationDict at: 'Font-Scale' ifAbsent: ['']. s size > 0 ifTrue: [ RenderScale _ s asString asNumberNoError. RenderScale = 0 ifTrue: [RenderScale _ 1]. "non-number string" RenderScale _ RenderScale within: 0.5 and: 2.5]. s _ TranslationDict at: 'Suppress-Bold' ifAbsent: ['']. s size > 0 ifTrue: [s asString asLowercase = 'true' ifTrue: [RenderSuppressBold _ true]]. "even though we are not actively using the hint string, keep this code in case we need it in the future:" s _ TranslationDict at: 'Layout-Hint' ifAbsent: ['']. s size > 0 ifTrue: [RenderHintString _ s]. RenderWithSqueak _ self useSqueakRendering. ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 6/27/2008 21:58'! showHintString "self showHintString" | f p r | p _ 20@70. f _ (StringMorph contents: self renderHintString font: (StrikeFont fontName: 'Verdana' size: 18)) imageForm. r _ f rectangleEnclosingPixelsNotOfColor: (f colorAt: 0@0). Display fillWhite: (p extent: f extent). f displayOn: Display at: p rule: Form paint. Display border: (r translateBy: p) width: 1 rule: Form over fillColor: Color blue. ^ { f height. r height. f height - r height. self verticalTrimForFont: (StrikeFont fontName: 'VerdanaBold' size: 10) }! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 6/29/2008 11:21'! stringExtent: aString font: aStrikeFont "Answer the extent of the given string using my font under the current font rendering system." "self stringExtent: 'Hello, Scratch!!' font: (StrikeFont fontName: 'Verdana' size: 18)" | s | RenderPlugin ifNil: [ "no renderer; use Squeak font size" ^ (aStrikeFont widthOfString: aString asString) @ aStrikeFont height]. aString size = 0 ifTrue: [^ 5@((aStrikeFont pointSize * RenderScale) rounded)]. s _ aString. s isUnicode ifTrue: [s _ s asUTF8]. "convert from UTF32 to UTF8 if necessary" self setFont: aStrikeFont antialias: RenderAntiAliasing. ^ RenderPlugin primMeasureString: s ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 6/3/2009 17:43'! useSqueakRendering "Answer true if the current langauge can (or must) be rendered using Squeak,based on the availablility of a rendering plugin, the settings in the .po file and the contents of the translations. Assume that the header has already been processed to initialize HeaderDict." self canRenderUnicode ifFalse: [^ true]. IsRTL ifTrue: [^ false]. RenderScale ~= 1 ifTrue: [^ false]. RenderSuppressBold ifTrue: [^ false]. true ifTrue: [^ false]. "disable Squeak rendering, even if language can be represented in MacRoman" TranslationDict associationsDo: [:assoc | ((assoc key endsWith: '-comment') not and: [(assoc key asLowercase = 'language-name') not and: [assoc value isUnicode]]) ifTrue: [ assoc value isMacRoman ifFalse: [^ false]]]. ^ true ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 7/1/2008 10:32'! verticalTrimForFont: aStrikeFont "Answer the number of pixels to trim from a button labeled with the given font. Some rendering systems (e.g. Pango on Mac OS), add excess space below the lowest extent of a font in some languages. This method computes the actual space needed by from the render hints string. It is the translator's responsibility to provide a render hints string that includes the tallest character and the the character with the maximum descent." "[self verticalTrimForFont: (StrikeFont fontName: 'VerdanaBold' size: 10)] msecs" | f r extra | (RenderVerticalTrimCache includesKey: aStrikeFont) ifTrue: [ ^ RenderVerticalTrimCache at: aStrikeFont]. f _ (StringMorph contents: self renderHintString font: aStrikeFont) imageForm. r _ f rectangleEnclosingPixelsNotOfColor: (f colorAt: 0@0). extra _ (f height - r height - 2) max: 0. RenderVerticalTrimCache at: aStrikeFont put: extra. ^ extra ! ! !ScratchTranslator class methodsFor: 'Unicode rendering' stamp: 'jm 6/29/2008 11:21'! xRangesFor: utf8 font: aStrikeFont "Anwer an array of (leftX, rightX) pairs for the given Unicode string. There will be an entry in the resulting array for each UTF character in the input string, even when characters combine. Thus, in general, the x ranges for characters can overlap." RenderPlugin ifNil: [^ Array new: (utf8 asUTF32 size) withAll: #(0 0)]. self setFont: aStrikeFont antialias: RenderAntiAliasing. ^ RenderPlugin xRangesFor: utf8 ! ! !ScratchTranslator class methodsFor: 'Unicode copy/paste' stamp: 'jm 7/11/2008 19:08'! unicodeClipboard "Get the contents of the Unicode clipboard as UTF32." "self unicodeClipboard asArray" "(StringMorph contents: self unicodeClipboard) openInWorld" | n buf raw ch out s ch2 | RenderPlugin ifNil: [ "if there is no plugin, return the normal text clipboard as UTF32" ^ (UTF8 withAll: Smalltalk clipboardText) asUTF32]. n _ RenderPlugin primClipboardSize. n = 0 ifTrue: [ "if unicode clipboard is empty, try the normal one" ^ (UTF8 withAll: Smalltalk clipboardText) asUTF32]. buf _ SoundBuffer new: n. n _ RenderPlugin primGetCliboardInto: buf. raw _ (1 to: n) collect: [:i | ch _ buf at: i. ch < 0 ifTrue: [ch _ ch + 65536]. ch]. out _ WriteStream on: (UTF32 new: n). s _ ReadStream on: raw. [s atEnd] whileFalse: [ ch _ s next. (ch between: 16rD800 and: 16rDBFF) ifTrue: [ (s atEnd not and: [s peek between: 16rDC00 and: 16rDFFF]) ifTrue: [ ch2 _ s next. ch _ ((ch bitAnd: 16r3FF) << 10) + (ch2 bitAnd: 16r3FF) + 16r10000. out nextPut: ch]] ifFalse: [ ((ch = 0) | (ch = 10)) ifFalse: [out nextPut: ch]]]. ^ out contents ! ! !ScratchTranslator class methodsFor: 'Unicode copy/paste' stamp: 'jm 6/30/2008 16:10'! unicodeClipboardPut: unicodeOrString "Store the given string in the Unicode paste buffer of the underlying OS. If the argument is a String, it is assumed to be encoded in MacRoman and is converted to Unicode." "self unicodeClipboardPut: (UTF32 withAll: #(65 0 0 66 67 13 12354 0 27700 119070))" | useCRLF s buf | RenderPlugin ifNil: [ "if there is no plugin, put UTF8 for string into the normal text clipboard" Smalltalk clipboardText: unicodeOrString asUTF8. ^ self]. useCRLF _ Smalltalk isWindows. s _ unicodeOrString. s isUnicode ifFalse: [s _ s asUTF8]. s _ s asUTF32. buf _ WriteStream on: (Array new: 2 * s size). s do: [:ch | ch > 16r10000 ifTrue: [ "extended range character" ch _ ch - 16r10000. buf nextPut: (16rD800 bitOr: ((ch >> 10) bitAnd: 16r3FF)) - 16r10000. buf nextPut: (16rDC00 bitOr: (ch bitAnd: 16r3FF)) - 16r10000] ifFalse: [ ch >= 16r8000 ifTrue: [ "make signed to allow storage in SoundBuffer" ch _ ch - 16r10000]. ch ~= 0 ifTrue: [buf nextPut: ch]. (useCRLF and: [ch = 13]) ifTrue: [buf nextPut: 10]]]. RenderPlugin primCliboardPut: (SoundBuffer withAll: buf contents) size: buf size. ! ! !ScratchTranslator class methodsFor: 'locale' stamp: 'jm 8/8/2008 18:54'! guessLanguage "Try to guess a language setting based on the local." "ScratchTranslator guessLanguage" | lang country myLocale | (lang _ self primLanguage) ifNil: [^ 'en']. lang size > 2 ifTrue: [lang _ lang copyFrom: 1 to: 2]. country _ self primCountry ifNil: [^ lang]. country size > 2 ifTrue: [country _ country copyFrom: 1 to: 2]. "first try lang + country:" myLocale _ lang asLowercase, '_', country asLowercase. ISODict keys do: [:code | code asLowercase = myLocale ifTrue: [^ code]]. "then try just lang:" myLocale _ lang asLowercase. ISODict keys do: [:code | code asLowercase = myLocale ifTrue: [^ code]]. ^ 'en' "if no match, use English" ! ! !ScratchTranslator class methodsFor: 'locale' stamp: 'jm 8/4/2008 18:22'! primCountry "Return a string with ISO 639 country tag for this system, or nil if the primitive fails." "self primCountry" ^ nil ! ! !ScratchTranslator class methodsFor: 'locale' stamp: 'jm 8/4/2008 18:22'! primLanguage "Return a string with ISO 639 language tag for this system, or nil if the primitive fails." "self primLanguage" ^ nil ! ! !ScratchTranslator class methodsFor: 'rendering menu' stamp: 'jm 6/27/2008 22:54'! fontMenu "Present a menu of fonts." | menu choice | RenderPlugin ifNil: [^ self beep]. menu _ CustomMenu new. RenderPlugin getFontList do: [:fn | menu add: fn action: fn]. choice _ menu startUp. choice ifNotNil: [ RenderFont _ choice. self updateScratchUI]. ! ! !ScratchTranslator class methodsFor: 'rendering menu' stamp: 'jm 6/27/2008 22:55'! fontScaleMenu "Present a menu of font scales." | menu choice s | menu _ CustomMenu new. (0.8 to: 2.01 by: 0.1) do: [:n | s _ n printString, 'x'. n = RenderScale ifTrue: [s _ s, ' *']. menu add: s action: n]. choice _ menu startUp. choice ifNotNil: [ RenderScale _ choice. self updateScratchUI]. ! ! !ScratchTranslator class methodsFor: 'rendering menu' stamp: 'jm 7/10/2008 14:04'! renderAntiAliasing ^ RenderAntiAliasing ! ! !ScratchTranslator class methodsFor: 'rendering menu' stamp: 'jm 7/14/2008 22:47'! renderingMenu "Present a menu for experimenting with rendering settings." "self renderingMenu" | menu | World activeHand toolType: nil. Cursor normal show. RenderWithSqueak ifTrue: [^ self beep]. menu _ CustomMenu new. menu add: 'set font scale...' action: #fontScaleMenu. RenderSuppressBold ifTrue: [menu add: 'allow bold' action: #toggleSuppressBold] ifFalse: [menu add: 'suppress bold' action: #toggleSuppressBold]. menu localize. menu invokeOn: self.! ! !ScratchTranslator class methodsFor: 'rendering menu' stamp: 'jm 6/27/2008 22:58'! toggleAntiAliasing RenderAntiAliasing _ RenderAntiAliasing not. self updateScratchUI. ! ! !ScratchTranslator class methodsFor: 'rendering menu' stamp: 'jm 6/27/2008 22:59'! toggleSuppressBold RenderSuppressBold _ RenderSuppressBold not. self updateScratchUI. ! ! !ScratchTranslator class methodsFor: 'rendering menu' stamp: 'jm 6/27/2008 22:54'! updateScratchUI "Update the UI of all ScratchFrameMorphs after changing the font or font scale." RenderCenterOffsetCache _ RenderCenterOffsetCache species new. RenderVerticalTrimCache _ RenderVerticalTrimCache species new. ScratchFrameMorph allInstancesDo: [:m | m rebuildUIForNewLanguage]. ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'jm 6/9/2008 16:32'! export: keyString value: aStringOrUTF8 to: aStream "Write the given string to the given stream as a quoted gettext format. If the string is multiple lines. store it as a sequence of strings in double quotes, each ending with '\n'." | lines | aStream nextPutAll: keyString; space. lines _ (String withAll: aStringOrUTF8) lines. lines size = 1 ifTrue: [aStream nextPut: $"; nextPutAll: lines first; nextPut: $"; crlf] ifFalse: [ aStream nextPutAll: '""'; crlf. lines do: [:s | aStream nextPut: $"; nextPutAll: s; nextPutAll: '\n"'; crlf]]. ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'ee 5/15/2009 14:38'! exportPootleTranslations "Exports translations from the locale folder into a new localeForPootle directory with a different subdirectory structure" "Need to figure out how to overwrite files without getting a permission dialog" "self exportPootleTranslations" | defaultDir localeDir localeForPootleDir fs dirName sdir | defaultDir _ FileDirectory default. (defaultDir directoryNames includes: 'locale') ifTrue: [localeDir _ self translationDir] ifFalse: [^ self "dialog should say no locale folder found"]. localeForPootleDir _ defaultDir directoryNamed: 'translate.scratch.mit.edu'. localeForPootleDir _ localeForPootleDir directoryNamed: 'scratch'. localeDir fileNames do: [:n | (n endsWith: '.po') ifTrue: [ dirName _ (n copyFrom: 1 to: n size - 3). sdir _ localeForPootleDir directoryNamed: dirName. fs _ sdir newFileNamed: n. fs nextPutAll: (localeDir fileNamed: n) contentsOfEntireFile. fs close]]. ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'ee 8/11/2008 17:36'! exportStringsToTranslateFrom: aDictionary toFile: fName "Export the strings to be translated either to Scratch.pot or an existing translation (.po) file." "self resetUITranslationSet. self resetMIDITranslationSet. self exportStringsToTranslateFrom: nil toFile: nil" "self resetMIDITranslationSet. self exportStringsToTranslateFrom: (self importTranslation: 'es.po') toFile: 'exportTest.po'" | dir fn header f sections templateDict title keys comments | self setLanguage: 'en'. header _ HeaderString. dir _ self translationDir. fn _ fName. fn ifNil: [fn _ self templateFilename]. (dir fileExists: fn) ifTrue: [ "extract the existing header, then delete the old .po file" aDictionary ifNotNil: [ header _ aDictionary at: '' ifAbsent: ['']. dir deleteFileNamed: (dir fullNameFor: fn)]]. f _ FileStream newFileNamed: (dir fullNameFor: fn). "f nextPutAll: #(239 187 191) asByteArray asString." "UTF8 byte-order mark - removed for now (Pootle)" "header comments are stored as '-comments' because the header key is the empty string" aDictionary ifNotNil: [ comments _ aDictionary at: '-comments' ifAbsent: [#()]. comments do: [:s | f nextPutAll: s; crlf]]. "header is stored as a multi-line translation entry for the empty string" self export: 'msgid' value: '' to: f. self export: 'msgstr' value: header to: f. f crlf. sections _ { {'FORMATTING'. self formattingHeaderFields}. {'BLOCKS'. ScriptableScratchMorph blockSpecsForTranslation}. {'USER INTERFACE'. self uiTranslationSetAsSortedArray}. {'MIDI INSTRUMENTS'. self midiTranslationSet asArray sort} }. aDictionary ifNotNil: [templateDict _ self importTranslation: self templateFilename]. sections do: [:pair | title _ pair first. keys _ pair second asOrderedCollection. f nextPutAll: '############################################'; crlf. f nextPutAll: '# ', title; crlf. f nextPutAll: '############################################'; crlf. f crlf. ((title = 'FORMATTING') and: [aDictionary isNil]) ifTrue: [ f nextPutAll: self formattingSectionForPOT. keys _ OrderedCollection new]. keys removeAll: self doNotTranslate. keys do: [:k | (aDictionary isNil or: [templateDict includesKey: k]) ifTrue: [ "COMMENTS" comments _ #(). (templateDict notNil and: [templateDict includesKey: k,'-comments']) ifTrue: [ "use the comment from template .pot file if there is one" comments _ (templateDict at: k, '-comments')] ifFalse: [ "else use the comment from the translation file" aDictionary ifNotNil: [ comments _ aDictionary at: k, '-comments' ifAbsent: [#()]]]. comments do: [:s | f nextPutAll: s; crlf]. "FUZZY TAG" (aDictionary notNil and: [aDictionary includesKey: k, '-fuzzy']) ifTrue: [f nextPutAll: '#, fuzzy'; crlf]. "KEY" self export: 'msgid' value: k to: f. "TRANSLATION" (aDictionary notNil and: [aDictionary includesKey: k]) ifTrue: [ self export: 'msgstr' value: (aDictionary at: k) to: f] ifFalse: [ (aDictionary notNil and: [aDictionary includesKey: k asLowercase]) ifTrue: [ "TEMPORARY CAPITALIZATION HACK" self export: 'msgstr' value: (aDictionary at: k asLowercase) to: f] ifFalse: [ (aDictionary notNil and: [(k endsWith: '?') and: [aDictionary includesKey: (k copyFrom: 1 to: k size - 1)]]) ifTrue: [ "TEMPORARY QUESTION HACK" self export: 'msgstr' value: (aDictionary at: (k copyFrom: 1 to: k size - 1)) to: f] ifFalse: [ "BLANK TRANSLATION" self export: 'msgstr' value: '' to: f]]]. f crlf]]]. f close. ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'jm 3/17/2009 16:53'! extractLanguageFromFileNamed: aFilename "Return the UTF8 value of the 'Language-Name:' header from the file with the given name, or nil if the file does not exist or it does not include that header." "self extractLanguageFromFileNamed: (self translationDir fullNameFor: 'Scratch.pot')" | f s i line lang nextLine lineSize | f _ FileStream readOnlyFileNamedOrNil: aFilename. f ifNil: [^ nil]. s _ f contentsOfEntireFile. i _ 0. [true] whileTrue: [ i _ s findString: 'language-name' startingAt: i + 1 caseSensitive: false. i = 0 ifTrue: [^ nil]. line _ (self withoutComment: (self lineOf: s containingIndex: i)) withBlanksTrimmed. lineSize _ line size. ((line size > 0) and: [line first = $m and: [line last = $"]]) ifTrue: [ line _ (self extractQuotedStringFrom: line) withBlanksTrimmed. (line asLowercase beginsWith: 'language-name') ifTrue: [ nextLine _ (self withoutComment: (self lineOf: s containingIndex: (i + lineSize))) withBlanksTrimmed. lang _ (nextLine copyFrom: 9 to: (nextLine size - 1)) withBlanksTrimmed. ^ (UTF8 withAll: lang)]]]. ^ nil ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'ee 8/3/2008 20:01'! importOLPCTranslations "Imports translations from the OLPC Pootle translation directory into the Scratch/locale/ directory and renames the .po files" "First copy the /scratch/ folder from https://dev.laptop.org/~sayamindu/scratch_translations.zip to the local directory, then run this" "self importOLPCTranslations" | dir subDir | dir _ FileDirectory default. (dir directoryNames includes: 'scratch') ifTrue: [dir _ dir directoryNamed: 'scratch'] ifFalse: [^ self "dialog should say no scratch folder found"]. dir directoryNames do: [:n | subDir _ dir directoryNamed: n. subDir rename: 'Scratch.po' toBe: (self translationDir fullNameFor: n,'.po')]. ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'ee 5/22/2009 14:16'! importPootleTranslations "Imports translations from the LLK Pootle translation directory into the Scratch/locale/ directory" "First checkout svn://translate.scratch.mit.edu:52400 to the local directory, then run this" "self importPootleTranslations" | dir subDir fs | dir _ FileDirectory default. (dir directoryNames includes: 'translate.scratch.mit.edu') ifTrue: [dir _ dir directoryNamed: 'translate.scratch.mit.edu'. (dir directoryNames includes: 'scratch') ifTrue: [dir _ dir directoryNamed: 'scratch'] ifFalse: [^ self "dialog should say no scratch folder found"]] ifFalse: [^ self "dialog should say no scratch folder found"]. dir directoryNames do: [:n | (n = '.svn') ifFalse: [ subDir _ dir directoryNamed: n. fs _ self translationDir newFileNamed: n,'.po'. fs nextPutAll: (subDir fileNamed: n,'.po') contentsOfEntireFile. fs close]]. ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'jm 6/10/2008 00:11'! importTranslation: aFilename "Import a translation dictionary." "self importTranslation: 'cs.po'" | f lines lstream result | f _ FileStream readOnlyFileNamedOrNil: (self translationDir fullNameFor: aFilename). f ifNil: [ DialogBoxMorph inform: 'File not found' withDetails: aFilename. ^ nil]. lines _ f contentsOfEntireFile lines. "trim blanks" lines _ lines collect: [:s | s withoutLeadingBlanks]. lstream _ ReadStream on: lines. [result _ self parseTranslationLines: lstream] ifError: [ DialogBoxMorph inform: 'Error reading file: ', aFilename,' at line ', (lstream position asString), '.']. ^ result ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'jm 6/3/2008 14:03'! isRTL "Returns true if the header 'Language-Direction' is set to 'RTL'" ^ IsRTL ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'jm 11/2/2007 18:03'! lineOf: aString containingIndex: anIndex "Answer the line of the given string that contains the given index." | cr lf i j ch | cr _ Character cr. lf _ Character lf. i _ j _ (anIndex within: 1 and: aString size). [(i > 1) and: [((ch _ aString at: i - 1) ~= cr) & (ch ~= lf)]] whileTrue: [i _ i - 1]. [(j < aString size) and: [((ch _ aString at: j + 1) ~= cr) & (ch ~= lf)]] whileTrue: [j _ j + 1]. ^ aString copyFrom: i to: j ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'ee 7/10/2008 09:20'! parseCommandSpec: aCommandSpec "Answer an array of token strings containing my keywords and argument specs." "self parseCommandSpec: '%a of %m'" | result len i j spec | result _ OrderedCollection new. spec _ aCommandSpec. (spec isKindOf: UTF8) ifTrue: [spec _ String withAll: spec]. len _ aCommandSpec size. i _ 1. [(i < len) and: [(spec at: i) isSeparator]] whileTrue: [i _ i + 1]. [i <= len] whileTrue: [ j _ spec indexOf: $% startingAt: i. j > 0 ifTrue: [ j > i ifTrue: [result addLast: (spec copyFrom: i to: j - 1)]. j < len ifTrue: [result addLast: (spec copyFrom: j to: j + 1)] ifFalse: [result addLast: '%']. i _ j + 2] ifFalse: [ result addLast: (spec copyFrom: i to: len). i _ len + 1]]. ^ result asArray collect: [:s | s withBlanksTrimmed]! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'ee 7/9/2008 16:06'! parseTranslationLines: lineStream "Parse a language translation from the given stream of lines and answer the resulting translation dictionary." | result key val comments fuzzy | result _ Dictionary new. "initialComments _ OrderedCollection new." comments _ OrderedCollection new. fuzzy _ nil. "collect all translation diciontary entries" [lineStream atEnd] whileFalse: [ ((lineStream peek beginsWith: '# ') and: [(#('# BLOCKS' '# USER INTERFACE' '# MIDI INSTRUMENTS' '# FORMATTING') includes: (lineStream peek)) not]) ifTrue: [comments add: (UTF8 withAll: (lineStream next))] ifFalse: [(lineStream peek beginsWith: '#, fuzzy') ifTrue: [fuzzy _ lineStream next] ifFalse: [((lineStream peek beginsWith: 'msgid') or: [lineStream peek beginsWith: 'msgid']) "BOM" ifTrue: [ key _ self extractQuotedStringFrom: lineStream next. val _ ''. [lineStream atEnd not and: [lineStream peek beginsWith: 'msgstr']] whileTrue: [ val _ val, (self extractQuotedStringFrom: lineStream next). [lineStream atEnd not and: [lineStream peek beginsWith: '"']] whileTrue:[ val _ val, (self extractQuotedStringFrom: lineStream next)]]. comments size > 0 ifTrue: [result at: key,'-comments' put: comments]. fuzzy ifNotNil: [result at: key,'-fuzzy' put: (UTF8 withAll: fuzzy)]. result at: key put: (UTF8 withAll: val). comments _ OrderedCollection new. fuzzy _ nil] ifFalse: [lineStream next. lineStream atEnd ifTrue: [^ result]]]]]. ^ result ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'ee 10/24/2007 11:38'! templateFilename "Returns the filename for the translation template file to send to users." ^ 'Scratch.pot' ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'jm 10/28/2007 16:17'! translationDir "Returns the directory which contains the translation files and creates it if it doesn't exist." | dir | dir _ FileDirectory default. (dir directoryNames includes: 'locale') ifFalse: [[dir createDirectory: 'locale'] ifError: [^ dir]]. dir _ dir directoryNamed: 'locale'. ^ dir ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'ee 10/25/2007 13:45'! updateTranslationFiles "Takes the existing .po files and updates them to reflect the latest Scratch strings to be translated." "self updateTranslationFiles" | translationDict codes | self importLanguagesList. codes _ self isoDict keys. codes do: [:c | (c = 'en') ifFalse:[ translationDict _ self importTranslation: c,'.po'. self exportStringsToTranslateFrom: translationDict toFile: c,'.po']]. ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'ee 7/10/2008 15:58'! verifyTranslationFiles "Returns a dictionary with each language filename as a key and a dictionary with the header and formatting fields as the key's value" "self verifyTranslationFiles" | verificationDict dict subDict | verificationDict _ Dictionary new. self translationDir fileNames do: [: n | dict _ self importTranslation: n. subDict _ Dictionary new. subDict at: 'header' put: (dict at: '' ifAbsent: ['no header']). self formattingHeaderFields do: [:f | subDict at: f put: (dict at: f ifAbsent: [''])]. verificationDict at: n put: subDict]. ^ verificationDict! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'ee 6/24/2009 10:43'! verifyTranslationFilesArgOrder "Returns a dictionary with any language file with invalid arguments as the key and the invalid arguments as the value" "self verifyTranslationFilesArgOrder2" | verificationDict dict subDict ts argsS argsTS | verificationDict _ Dictionary new. self translationDir fileNames do: [: n | dict _ self importTranslation: n. subDict _ Dictionary new. ScriptableScratchMorph blockSpecsForTranslation do: [:s | ts _ dict at: s ifAbsent: [s]. argsS _ (CommandBlockMorph parseCommandSpec: s) select: [:a | CommandBlockMorph isArgSpec: a]. argsTS _ (CommandBlockMorph parseCommandSpec: ts) select: [:a | CommandBlockMorph isArgSpec: a]. (argsS sort = argsTS sort) ifFalse: [(ts size > 0) ifTrue: [subDict at: s put: ts]]]. (subDict values size > 0) ifTrue: [verificationDict at: n put: subDict]]. ^ verificationDict ! ! !ScratchTranslator class methodsFor: 'import/export' stamp: 'ee 11/13/2007 16:49'! withoutComment: s "Answer the given string without any comment. A hash character (#) begins a comment that runs to the end of the line unless the hash character is inside a double-quoted string." "ScratchTranslator withoutComment: '# full line comment'" "ScratchTranslator withoutComment: 'contents …ffnen # plus s comment'" | inString lastCh ch | (s indexOf: $#) = 0 ifTrue: [^ s]. inString _ false. lastCh _ Character space. 1 to: s size do: [:i | ch _ s at: i. ch = $# ifTrue: [ inString ifFalse: [^ s copyFrom: 1 to: i - 1]]. ch = $" ifTrue: [ (inString and: [lastCh = $\]) ifFalse: [ inString _ inString not]]]. ^ s ! ! I am a Scratch blocks palette viewer. I have a set of ToggleButtons that allow selection of a blocks category to view and a scrollable area showing the blocks in the currently selected category. ! !ScratchViewerMorph methodsFor: 'initialization' stamp: 'ee 1/29/2009 13:44'! initialize super initialize. self initFrontFromForm: (ScratchFrameMorph skinAt: #blocksPaletteFrameTransparent2) topSectionHeight: 120. self middleBarLeftMargin: 5 rightMargin: 0. color _ (Color r: 0.584 g: 0.603 b: 0.623). pageViewer _ ScrollFrameMorph2 new growthFraction: 0.1. self addMorphBack: (pageViewer position: self position + (0@120)). self target: nil. self extent: 214@500. ! ! !ScratchViewerMorph methodsFor: 'initialization' stamp: 'jm 3/14/2009 09:45'! rebuildCategorySelectors | catList maxExtent buttons label offForm onForm overForm b pad leftColumnX rightColumnX x y | catList _ #( motion control looks sensing sound operators pen variables). "First, delete the old category buttons" submorphs do: [:m | (m isKindOf: ResizableToggleButton2) ifTrue: [m delete]]. "Create new buttons, keeping track of the maximum extent." maxExtent _ 75@0. buttons _ catList collect: [:cat | label _ (ScratchTranslator translationFor: cat asString) capitalized. offForm _ (ScratchFrameMorph skinAt: cat). onForm _ (ScratchFrameMorph skinAt: (cat, 'Pressed')). overForm _ (ScratchFrameMorph skinAt: (cat, 'Over')). ScratchTranslator isRTL ifTrue:[ b _ ResizableToggleButton2 new offForm: (offForm flipBy: #horizontal centerAt: offForm center) onForm: (onForm flipBy: #horizontal centerAt: onForm center) overForm: (overForm flipBy: #horizontal centerAt: overForm center)] ifFalse:[ b _ ResizableToggleButton2 new offForm: offForm onForm: onForm overForm: overForm]. b label: label font: (ScratchFrameMorph getFont: #Category); setLabelColor: Color white; target: self; actionSelector: #currentCategory:; arguments: (Array with: cat); toggleButtonMode: true; toggleMode: false. ScratchTranslator isRTL ifTrue:[b rightJustifyInset: 10] ifFalse:[b leftJustifyInset: 10]. maxExtent _ maxExtent max: (b extent + (3 @ -6)). b]. "calculate catButtonsExtent" pad _ 15. "padding on left, right, and betwen the button columns" catButtonsExtent _ ((2 * maxExtent x) + (3 * pad)) @ (((catList size // 2) * (maxExtent y + 6)) + 25). "place the buttons" leftColumnX _ self left + 12 + pad. rightColumnX _ leftColumnX + maxExtent x + pad. x _ leftColumnX. y _ self top + 17. 1 to: buttons size do: [:i | b _ buttons at: i. b extent: maxExtent. self addMorph: (b position: x@y). i even ifTrue: [x _ leftColumnX. y _ y + b height + 6] ifFalse: [x _ rightColumnX]]. self width: catButtonsExtent x. pageViewer position: self position + (0@catButtonsExtent y). topSectionHeight _ catButtonsExtent y - 4. ! ! !ScratchViewerMorph methodsFor: 'accessing' stamp: 'nb 1/24/2008 17:35'! catButtonsExtent ^ catButtonsExtent ! ! !ScratchViewerMorph methodsFor: 'accessing' stamp: 'jm 6/29/2004 14:20'! categoryChanged: aString "The given category has changed (e.g., due to a variable or script add/remove). If it's the current category, update my contents. Otherwise, do nothing." self target ifNil: [^ self]. currentCategory = aString ifTrue: [self updateContents]. ! ! !ScratchViewerMorph methodsFor: 'accessing' stamp: 'jm 2/27/2005 19:04'! currentCategory ^ currentCategory ! ! !ScratchViewerMorph methodsFor: 'accessing' stamp: 'TIS 7/10/2006 11:17'! currentCategory: aString World activeHand newKeyboardFocus: nil. currentCategory _ aString. self lightUpSelectorForCurrentCategory. self updateContents. ! ! !ScratchViewerMorph methodsFor: 'accessing' stamp: 'TIS 7/7/2006 13:33'! pageViewer ^ pageViewer ! ! !ScratchViewerMorph methodsFor: 'accessing' stamp: 'jm 1/28/2009 10:42'! refresh "Refresh the palette for the current category." self currentCategory: currentCategory. ! ! !ScratchViewerMorph methodsFor: 'accessing' stamp: 'jm 5/11/2004 19:36'! target ^ target ! ! !ScratchViewerMorph methodsFor: 'accessing' stamp: 'jm 2/27/2005 19:28'! target: aScratchObject "Start viewing the given object, if not object, return an empty viewer." | categories | target _ aScratchObject. aScratchObject ifNil: [ ^ self currentCategory: 'none']. "keep the same category selected if possible" categories _ target blockCategories. (categories includes: currentCategory) ifTrue: [self currentCategory: currentCategory] ifFalse: [self currentCategory: categories first]. self isInWorld ifTrue: [self world startSteppingSubmorphsOf: self]. ! ! !ScratchViewerMorph methodsFor: 'drawing' stamp: 'jm 10/28/2008 13:07'! drawBackgroundOn: aCanvas "Draw my background." color isTransparent ifTrue: [^ self]. aCanvas fillRectangle: (self topLeft extent: (self width @ catButtonsExtent y)) color: color. ! ! !ScratchViewerMorph methodsFor: 'geometry' stamp: 'jm 2/27/2005 19:36'! extent: aPoint super extent: aPoint. pageViewer ifNotNil: [pageViewer extent: self extent - (pageViewer position - self position)]. ! ! !ScratchViewerMorph methodsFor: 'private' stamp: 'nb 1/14/2008 20:45'! lightUpSelectorForCurrentCategory submorphs do: [:m | (m isKindOf: ResizableToggleButton2) ifTrue: [ m arguments first = currentCategory ifTrue: [m on] ifFalse: [m off]]]. ! ! !ScratchViewerMorph methodsFor: 'private' stamp: 'jm 4/14/2008 17:52'! updateContents | p | self target ifNil: [ pageViewer contents: (Morph new color: ScratchFrameMorph palettePaneColor). ^ self]. p _ self target viewerPageForCategory: currentCategory. p color: ScratchFrameMorph palettePaneColor. pageViewer contents: p. self isInWorld ifTrue: [self world startSteppingSubmorphsOf: p]. p fixLayout. ScratchTranslator isRTL ifTrue: [pageViewer hScrollPixels: (p right)]. ! ! I am the controller for the parts of the display screen that have no view on them. I only provide a standard yellow button menu. I view (a FormView of) an infinite gray form. (ScheduledControllers screenController) is the way to find me.! !ScreenController methodsFor: 'control defaults' stamp: 'sma 3/11/2000 12:21'! controlActivity "Any button opens the screen's menu. If the shift key is down, do find window." sensor leftShiftDown ifTrue: [^ self findWindow]. (self projectScreenMenu invokeOn: self) ifNil: [super controlActivity]! ! !ScreenController methodsFor: 'control defaults' stamp: 'sma 3/11/2000 12:06'! isControlActive ^ self isControlWanted! ! !ScreenController methodsFor: 'control defaults' stamp: 'sma 3/11/2000 12:05'! isControlWanted ^ self viewHasCursor and: [sensor anyButtonPressed]! ! !ScreenController methodsFor: 'menu messages' stamp: 'jm 5/31/2003 17:08'! aboutThisSystem Smalltalk aboutThisSystem. ! ! !ScreenController methodsFor: 'menu messages' stamp: 'sw 6/11/1999 20:25'! bitCachingString ^ StandardSystemView cachingBits ifTrue: ['don''t save bits (compact)'] ifFalse: ['save bits (fast)']! ! !ScreenController methodsFor: 'menu messages' stamp: 'sw 6/11/1999 20:50'! changeWindowPolicy Preferences toggleWindowPolicy! ! !ScreenController methodsFor: 'menu messages' stamp: 'sw 12/10/1999 11:29'! configureFonts Preferences presentMvcFontConfigurationMenu! ! !ScreenController methodsFor: 'menu messages' stamp: 'sw 10/27/1998 14:27'! lookForSlips Smalltalk changes lookForSlips! ! !ScreenController methodsFor: 'menu messages' stamp: 'sw 9/4/1998 10:17'! newChangeSet ChangeSorter newChangeSet! ! !ScreenController methodsFor: 'menu messages' stamp: 'di 7/19/1999 14:56'! openMorphicProject Smalltalk verifyMorphicAvailability ifFalse: [^ self]. ProjectView open: Project newMorphic. ! ! !ScreenController methodsFor: 'menu messages' stamp: 'jm 10/7/2002 06:22'! openProject "Create and schedule a Project." Smalltalk at: #ProjectView ifPresent: [:c | c open: Project new]. ! ! !ScreenController methodsFor: 'menu messages' stamp: 'sw 7/6/1998 18:59'! openSimpleChangeSorter ChangeSorter new open! ! !ScreenController methodsFor: 'menu messages' stamp: 'sma 4/30/2000 10:13'! restoreDisplay "Clear the screen to gray and then redisplay all the scheduled views." Smalltalk isMorphic ifTrue: [^ World restoreDisplay]. Display extent = DisplayScreen actualScreenSize ifFalse: [DisplayScreen startUp. ScheduledControllers unCacheWindows]. ScheduledControllers restore! ! !ScreenController methodsFor: 'menu messages' stamp: 'bf 9/18/1999 20:01'! setDisplayDepth "Let the user choose a new depth for the display. " | result | (result _ (SelectionMenu selections: Display supportedDisplayDepths) startUpWithCaption: 'Choose a display depth (it is currently ' , Display depth printString , ')') == nil ifFalse: [Display newDepth: result]! ! !ScreenController methodsFor: 'menu messages' stamp: 'sw 6/11/1999 20:23'! staggerPolicyString ^ Preferences staggerPolicyString! ! !ScreenController methodsFor: 'menu messages' stamp: 'sw 7/13/1999 20:19'! windowSpecificationPanel Smalltalk hasMorphic ifFalse: [^ self inform: 'Sorry, this feature requires the presence of Morphic.']. Preferences windowSpecificationPanel! ! !ScreenController methodsFor: 'nested menus' stamp: 'jm 5/31/2003 19:03'! appearanceMenu "Answer the appearance menu to be put up as a screen submenu" "ScreenController new appearanceMenu startUp" ^ SelectionMenu labelList: #( 'set display depth...' 'set desktop color...' 'full screen on' 'full screen off' 'window colors...' 'system fonts...' ) lines: #(2 4) selections: #( setDisplayDepth setDesktopColor fullScreenOn fullScreenOff windowSpecificationPanel configureFonts) ! ! !ScreenController methodsFor: 'nested menus' stamp: 'jm 12/2/2003 20:51'! changesMenu "Answer a menu for changes-related items." "ScreenController new changesMenu startUp" ^ SelectionMenu labelList: #( 'create new change set...' 'check change set for slips' 'simple change sorter' 'dual change sorter' 'recover recent changes...' ) lines: #(2 4) selections: #( newChangeSet lookForSlips openSimpleChangeSorter openChangeManager browseRecentLog) ! ! !ScreenController methodsFor: 'nested menus' stamp: 'jm 5/31/2003 17:13'! helpMenu "Answer the help menu to be put up as a screen submenu" "ScreenController new helpMenu startUp" ^ SelectionMenu labelList: #( 'about this system' 'command-key help' 'edit preferences...' 'set author initials...' 'memory statistics' 'space left') lines: #(3) selections: #( aboutThisSystem openCommandKeyHelp editPreferences setAuthorInitials vmStatistics garbageCollect). ! ! !ScreenController methodsFor: 'nested menus' stamp: 'jm 2/9/2004 10:03'! openMenu "ScreenController new openMenu startUp" ^ SelectionMenu labelList: #( 'browser' 'workspace' 'file list' 'transcript' 'simple change sorter' 'dual change sorter' 'mvc project' 'morphic project') lines: #(4 6) selections: #( openBrowser openWorkspace openFileList openTranscript openSimpleChangeSorter openChangeManager openProject openMorphicProject) ! ! !ScreenController methodsFor: 'nested menus' stamp: 'sma 3/11/2000 12:23'! popUpMenuFor: aSymbol (self perform: aSymbol) invokeOn: self! ! !ScreenController methodsFor: 'nested menus' stamp: 'sw 7/13/1999 18:07'! presentAppearanceMenu self popUpMenuFor: #appearanceMenu! ! !ScreenController methodsFor: 'nested menus' stamp: 'sw 7/6/1998 21:14'! presentChangesMenu self popUpMenuFor: #changesMenu! ! !ScreenController methodsFor: 'nested menus' stamp: 'sw 7/6/1998 21:15'! presentHelpMenu self popUpMenuFor: #helpMenu! ! !ScreenController methodsFor: 'nested menus' stamp: 'sw 7/6/1998 21:15'! presentOpenMenu self popUpMenuFor: #openMenu! ! !ScreenController methodsFor: 'nested menus' stamp: 'sw 7/6/1998 21:16'! presentWindowMenu self popUpMenuFor: #windowMenu! ! !ScreenController methodsFor: 'nested menus' stamp: 'jm 5/31/2003 19:08'! projectScreenMenu "Answer the project screen menu." ^ SelectionMenu labelList: #( 'previous project' 'jump to project...' 'restore display' 'open...' 'windows...' 'changes...' 'help...' 'appearance...' 'save' 'save as...' 'save and quit' 'quit') lines: #(2 3 8) selections: #( returnToPreviousProject jumpToProject restoreDisplay presentOpenMenu presentWindowMenu presentChangesMenu presentHelpMenu presentAppearanceMenu snapshot saveAs snapshotAndQuit quit). ! ! !ScreenController methodsFor: 'nested menus' stamp: 'jm 5/31/2003 16:01'! windowMenu "Answer a menu for windows-related items." "ScreenController new windowMenu startUp" ^ SelectionMenu labelList: #( 'find window...' 'find changed browsers...' 'find changed windows...' 'collapse all windows' 'expand all windows' 'close unchanged windows'), (Array with: self bitCachingString with: self staggerPolicyString) lines: #(3 6) selections: #( findWindow chooseDirtyBrowser chooseDirtyWindow collapseAll expandAll closeUnchangedWindows fastWindows changeWindowPolicy) ! ! I am the superclass of all scriptable Scratch morphs. Scriptable morphs: 1. allow the user to add user-defined scripts 2. allow the user to add user-defined variables 3. when they are copied, their scripts and variables are copied; thereafter, script changes to the original and the copy are entirely independent 4. can be exported and imported into another Scratch project. ! !ScriptableScratchMorph methodsFor: 'initialization' stamp: 'ee 8/5/2008 13:57'! initialize super initialize. objName _ self nextInstanceName. vars _ Dictionary new. lists _ Dictionary new. blocksBin _ ScratchScriptsMorph new. isClone _ false. costume _ self defaultImageMedia. media _ OrderedCollection new. costumeChangeMSecs _ 0. visibility _ 100. volume _ 100. tempoBPM _ 60. sceneStates _ Dictionary new. ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 8/2/2004 20:42'! costume ^ costume ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 11/30/2004 16:52'! costumeChangeMSecs "Answer the time of the last costume change. Whenever a costume change (or a filter change) occurs, this value is updated from the current millisecond clock. This value is used to optimize redrawing of thumbnails--if costumeChangeMSecs hasn't changed since the last time the thumbnail was updated we don't need to compute a new thumbnail." ^ costumeChangeMSecs ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 12/9/2005 16:33'! costumeForm "Answer my current costume form, without filtering, rotation or scaling. If my costume is a movie or animation, answer the appropriate frame." ^ costume compositeForm ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 10/31/2006 12:28'! filteredForm filterPack ifNil: [^ self rotatedForm]. filterPack filtersActive ifTrue: [ "filter parameters have changed; apply filters" ScratchFrameMorph useErrorCatcher ifTrue: [[filterPack applyFiltersTo: self rotatedForm] ifError: [filterPack _ nil. ^ self rotatedForm]] ifFalse: [filterPack applyFiltersTo: self rotatedForm]]. ^ filterPack resultForm ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 5/3/2004 14:00'! form "Answer my current costume form, without filtering, rotation, or scaling." ^ self costumeForm ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 11/30/2004 16:33'! form: aForm costume _ (ImageMedia new form: aForm). costume mediaName: (self unusedMediaNameFromBaseName: 'costume'). media addLast: costume. self costumeChanged. self updateMediaCategory. ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 1/8/2006 18:38'! imageForm "Answer a Form showing myself for the purpose of generating a thumbnail." ^ self filteredForm copy offset: 0@0 ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 5/6/2009 17:39'! inPresentationMode ^ (self ownerThatIsA: OffscreenWorldMorph) notNil ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 6/9/2004 09:29'! isClone ^ false ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 5/10/2004 19:46'! isClone: aBoolean "Ignored here. Overridden by my subclasses."! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 4/30/2004 19:05'! isColorable ^ false ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 3/2/2003 12:14'! isScriptable "I am a scriptable Scratch object." ^ true ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 7/3/2008 15:14'! isSprite ^ false ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 6/2/2009 11:43'! isVisible ^ self isHidden not and: [visibility > 0] ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 5/8/2005 11:44'! media ^ media ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 3/15/2003 19:27'! objName ^ objName ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 5/11/2009 11:06'! objName: aString "don't allow renaming a sprite to use a name already in use" (self spriteNameInUse: aString) ifTrue: [^ self]. objName _ aString. objName size = 0 ifTrue: [objName _ self nextInstanceName]. ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'md 11/8/2004 16:39'! renewFilterPack filterPack _ FilterPack new.! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 1/8/2006 18:41'! rotatedForm "By default, just return my costume form. Sprites override this method to do rotation." ^ self costumeForm ! ! !ScriptableScratchMorph methodsFor: 'accessing' stamp: 'jm 5/9/2005 09:05'! rotationCenter: aPoint "Set my costume's rotation center." costume rotationCenter: aPoint. self costumeChanged. ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 7/10/2004 09:55'! changeCostumeIndexBy: aNumber "Change my costume index by the given amount." self costumeIndex: self costumeIndex + aNumber. ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 1/13/2006 09:41'! changeGraphicEffect: effect by: aNumber "Change the given graphic effect by the given value." filterPack ifNil: [filterPack _ FilterPack new]. 'blur' = effect ifTrue: [filterPack blur: filterPack blur + aNumber]. 'brightness' = effect ifTrue: [filterPack brightnessShift: filterPack brightnessShift + aNumber]. 'fisheye' = effect ifTrue: [filterPack fisheye: filterPack fisheye + aNumber]. 'color' = effect ifTrue: [filterPack hueShift: filterPack hueShift + aNumber]. 'mosaic' = effect ifTrue: [filterPack mosaicCount: filterPack mosaicCount + aNumber]. 'pixelate' = effect ifTrue: [filterPack pixelateCount: filterPack pixelateCount + aNumber]. 'pointillize' = effect ifTrue: [filterPack pointillizeSize: filterPack pointillizeSize + aNumber]. 'saturation' = effect ifTrue: [filterPack saturationShift: filterPack saturationShift + aNumber]. ('transparency' = effect) | ('ghost' = effect) ifTrue: [ self transparency: self transparency + aNumber]. 'water ripple' = effect ifTrue: [filterPack waterRippleRate: filterPack waterRippleRate + aNumber]. 'whirl' = effect ifTrue: [filterPack whirl: filterPack whirl + aNumber]. costumeChangeMSecs _ Time millisecondClockValue. self changed. ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 2/18/2008 11:23'! changeVisibilityBy: delta "Obsolete. Do nothing." ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 6/8/2009 10:26'! costumeFromName: aString "If there is a costume by the given name, return it. Otherwise, return nil." | cameraCostume | ((aString = '*** camera ***') and: [CameraPlugin cameraIsAvailable]) ifTrue: [ cameraCostume _ CameraMedia new. (self class = ScratchStageMorph) ifTrue: [cameraCostume frameExtent: self extent]. ^ cameraCostume]. ^ media detect: [:el | el isSound not and: [el mediaName = aString]] ifNone: [nil] ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 7/10/2004 09:49'! costumeIndex "Answer the index of my current costume." ^ (media reject: [:m | m isSound]) indexOf: costume ifAbsent: [1] ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 7/10/2004 09:59'! costumeIndex: aNumber "Set my costume to the costume at the given index modulo my total number of costumes. Costumes are numbered starting at 1." | cList i newC | cList _ media reject: [:m | m isSound]. cList size = 0 ifTrue: [^ self]. "should never happen..." i _ ((aNumber rounded - 1) \\ cList size) + 1. newC _ cList at: i. costume == newC ifFalse: [self lookLike: newC mediaName]. ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 4/30/2007 22:00'! costumeNameFromNumber: aNumber "Return a costume name for the given number--rounded and modulo the number of costumes." | costumeList i | costumeList _ media reject: [:m | m isSound]. costumeList size = 0 ifTrue: [^ 'costume']. i _ ((aNumber rounded - 1) \\ costumeList size) + 1. ^ (costumeList at: i) mediaName ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 5/22/2005 22:54'! filterReset filterPack _ nil. self transparency: 0. self costumeChanged. ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 9/25/2007 11:48'! graphicEffectNames "Answer a collection of graphic effect names." ^ #( 'color' 'fisheye' 'whirl' 'pixelate' 'mosaic' 'brightness' 'ghost') ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 8/10/2008 13:12'! lookLike: costumeNameOrIndex "Change to the costume indicated by the given name, index, or boolean. Noop if there is no costume of the given name in my library." "Note: Costumes can have names that look like numbers, but those numbers have nothing to do with the index of the costume. So, we first try interpreting the argument as a name. If that fails, we try interpreting it as a costume index number." | p newCostume i | p _ self referencePosition. costume suspendPlaying. newCostume _ nil. (costumeNameOrIndex isKindOf: String) ifTrue: [ "try interpreting the argument as a costume name" newCostume _ self costumeFromName: costumeNameOrIndex. "try interpreting the string as a number" newCostume ifNil: [ i _ self interpretStringAsNumberIfPossible: costumeNameOrIndex. i isNumber ifTrue: [ newCostume _ self costumeFromName: (self costumeNameFromNumber: i)] ifFalse: [^ self]]]. "string does not match a costume and cannot be coverted to a number" "treat other types (number, boolean) as a costume index" newCostume ifNil: [ i _ costumeNameOrIndex asNumberNoError. newCostume _ self costumeFromName: (self costumeNameFromNumber: i)]. newCostume ifNil: [^ self]. "should not happen..." costume _ newCostume. costume resumePlaying. self costumeChanged. self referencePosition: p. World displayWorldSafely. ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 11/28/2006 13:39'! nextCostume "Show the next costume in my costumes list." self costumeIndex: self costumeIndex + 1. ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 4/4/2005 13:14'! penDown "Overriden by sprites." ^ false ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 5/19/2009 12:56'! sayNothing "Default behavior does nothing. Overridden in ScratchSpriteMorph." ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'nb 1/7/2008 12:02'! sceneNames ^ (self ownerThatIsA: ScratchStageMorph) sceneNames ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 1/13/2006 09:41'! setGraphicEffect: effect to: aNumber "Set the given graphic effect to the given value." filterPack ifNil: [filterPack _ FilterPack new]. 'blur' = effect ifTrue: [filterPack blur: aNumber]. 'brightness' = effect ifTrue: [filterPack brightnessShift: aNumber]. 'fisheye' = effect ifTrue: [filterPack fisheye: aNumber]. 'color' = effect ifTrue: [filterPack hueShift: aNumber]. 'mosaic' = effect ifTrue: [filterPack mosaicCount: aNumber]. 'pixelate' = effect ifTrue: [filterPack pixelateCount: aNumber]. 'pointillize' = effect ifTrue: [filterPack pointillizeSize: aNumber]. 'saturation' = effect ifTrue: [filterPack saturationShift: aNumber]. ('transparency' = effect) | ('ghost' = effect) ifTrue: [self transparency: aNumber]. 'water ripple' = effect ifTrue: [filterPack waterRippleRate: aNumber]. 'whirl' = effect ifTrue: [filterPack whirl: aNumber]. costumeChangeMSecs _ Time millisecondClockValue. self changed. ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 2/18/2008 11:23'! setVisibilityTo: percent "Obsolete. Do nothing." ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 5/22/2005 22:46'! transparency "Answer my transparency. 0 is opaque; 100 is transparent." ^ 100 - self visibility ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 5/23/2005 08:54'! transparency: aNumber "Set my transparency. 0 is opaque; 100 is transparent." self visibility: (100 - aNumber abs). ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 8/2/2006 18:10'! visibility "Answer my visibility. 0 is invisible. 100 is fully opaque." ^ visibility ! ! !ScriptableScratchMorph methodsFor: 'looks ops' stamp: 'jm 2/18/2008 11:19'! visibility: aNumber "Set my visibility. 0 is invisible. 100 is fully opaque." visibility _ (aNumber max: 0) min: 100. self changed. ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 12/2/2007 22:13'! changeTempoBy: aNumber self setTempoTo: self tempo + aNumber. ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 9/24/2007 17:05'! changeVolumeBy: aNumber self setVolumeTo: volume + aNumber. ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 5/3/2008 09:18'! drum: midiKey duration: beats elapsed: elapsedMSecs from: aNotePlayer | stage player | aNotePlayer ifNil: [ "first call, start playing the drum" (stage _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ ScratchNotePlayer new]. (player _ stage notePlayerFor: self) ifNil: [^ ScratchNotePlayer new]. ^ player copy drumOn: midiKey]. elapsedMSecs >= ((60000 * beats) // self tempo) ifTrue: [aNotePlayer drumOff]. ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 6/3/2009 18:47'! midiDrumMenu "Provides a drop-down menu for setting the drum number." | menu | menu _ CustomMenu new. #( ('Acoustic Bass Drum' 35) ('Bass Drum 1' 36) ('Side Stick' 37) ('Acoustic Snare' 38) ('Hand Clap' 39) ('Electric Snare' 40) ('Low Floor Tom' 41) ('Closed Hi-Hat' 42) ('High Floor Tom' 43) ('Pedal Hi-Hat' 44) ('Low Tom' 45) ('Open Hi-Hat' 46) ('Low-Mid Tom' 47) ('Hi-Mid Tom' 48) ('Crash Cymbal 1' 49) ('High Tom' 50) ('Ride Cymbal 1' 51) ('Chinese Cymbal' 52) ('Ride Bell' 53) ('Tambourine' 54) ('Splash Cymbal' 55) ('Cowbell' 56) ('Crash Cymbal 2' 57) ('Vibraslap' 58) ('Ride Cymbal 2' 59) ('Hi Bongo' 60) ('Low Bongo' 61) ('Mute Hi Conga' 62) ('Open Hi Conga' 63) ('Low Conga' 64) ('High Timbale' 65) ('Low Timbale' 66) ('High Agogo' 67) ('Low Agogo' 68) ('Cabasa' 69) ('Maracas' 70) ('Short Whistle' 71) ('Long Whistle' 72) ('Short Guiro' 73) ('Long Guiro' 74) ('Claves' 75) ('Hi Wood Block' 76) ('Low Wood Block' 77) ('Mute Cuica' 78) ('Open Cuica' 79) ('Mute Triangle' 80) ('Open Triangle' 81) ) do: [:pair | menu add: (self numberInParens: pair second), (pair first localizedMIDI) action: pair second]. ^ menu ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 3/2/2009 13:02'! midiInstrument: aNumber "Old 'set instrument to _' block. Set the MIDI instrument used for note sounds." | stage player | (stage _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ self]. (player _ stage notePlayerFor: self) ifNotNil: [ player instrument: aNumber]. ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 6/3/2009 18:34'! midiInstrumentMenu "Provides a drop-down menu for setting the instrument number." | menu | menu _ CustomMenu new. #( ('Acoustic Grand' 1) ('Bright Acoustic' 2) ('Electric Grand' 3) ('Honky-Tonk' 4) ('Electric Piano 1' 5) ('Electric Piano 2' 6) ('Harpsichord' 7) ('Clavinet' 8) ('Celesta' 9) ('Glockenspiel' 10) ('Music Box' 11) ('Vibraphone' 12) ('Marimba' 13) ('Xylophone' 14) ('Tubular Bells' 15) ('Dulcimer' 16) ('Drawbar Organ' 17) ('Percussive Organ' 18) ('Rock Organ' 19) ('Church Organ' 20) ('Reed Organ' 21) ('Accordion' 22) ('Harmonica' 23) ('Tango Accordion' 24) ('Nylon String Guitar' 25) ('Steel String Guitar' 26) ('Electric Jazz Guitar' 27) ('Electric Clean Guitar' 28) ('Electric Muted Guitar' 29) ('Overdriven Guitar' 30) ('Distortion Guitar' 31) ('Guitar Harmonics' 32) ('Acoustic Bass' 33) ('Electric Bass (finger)' 34) ('Electric Bass (pick)' 35) ('Fretless Bass' 36) ('Slap Bass 1' 37) ('Slap Bass 2' 38) ('Synth Bass 1' 39) ('Synth Bass 2' 40) ('Violin' 41) ('Viola' 42) ('Cello' 43) ('Contrabass' 44) ('Tremolo Strings' 45) ('Pizzicato Strings' 46) ('Orchestral Strings' 47) ('Timpani' 48) ('String Ensemble 1' 49) ('String Ensemble 2' 50) ('SynthStrings 1' 51) ('SynthStrings 2' 52) ('Choir Aahs' 53) ('Voice Oohs' 54) ('Synth Voice' 55) ('Orchestra Hit' 56) ('Trumpet' 57) ('Trombone' 58) ('Tuba' 59) ('Muted Trumpet' 60) ('French Horn' 61) ('Brass Section' 62) ('SynthBrass 1' 63) ('SynthBrass 2' 64) ('Soprano Sax' 65) ('Alto Sax' 66) ('Tenor Sax' 67) ('Baritone Sax' 68) ('Oboe' 69) ('English Horn' 70) ('Bassoon' 71) ('Clarinet' 72) ('Piccolo' 73) ('Flute' 74) ('Recorder' 75) ('Pan Flute' 76) ('Blown Bottle' 77) ('Shakuhachi' 78) ('Whistle' 79) ('Ocarina' 80) ('Lead 1 (square)' 81) ('Lead 2 (sawtooth)' 82) ('Lead 3 (calliope)' 83) ('Lead 4 (chiff)' 84) ('Lead 5 (charang)' 85) ('Lead 6 (voice)' 86) ('Lead 7 (fifths)' 87) ('Lead 8 (bass+lead)' 88) ('Pad 1 (new age)' 89) ('Pad 2 (warm)' 90) ('Pad 3 (polysynth)' 91) ('Pad 4 (choir)' 92) ('Pad 5 (bowed)' 93) ('Pad 6 (metallic)' 94) ('Pad 7 (halo)' 95) ('Pad 8 (sweep)' 96) ('FX 1 (rain)' 97) ('FX 2 (soundtrack)' 98) ('FX 3 (crystal)' 99) ('FX 4 (atmosphere)' 100) ('FX 5 (brightness)' 101) ('FX 6 (goblins)' 102) ('FX 7 (echoes)' 103) ('FX 8 (sci-fi)' 104) ('Sitar' 105) ('Banjo' 106) ('Shamisen' 107) ('Koto' 108) ('Kalimba' 109) ('Bagpipe' 110) ('Fiddle' 111) ('Shanai' 112) ('Tinkle Bell' 113) ('Agogo' 114) ('Steel Drums' 115) ('Woodblock' 116) ('Taiko Drum' 117) ('Melodic Tom' 118) ('Synth Drum' 119) ('Reverse Cymbal' 120) ('Guitar Fret Noise' 121) ('Breath Noise' 122) ('Seashore' 123) ('Bird Tweet' 124) ('Telephone Ring' 125) ('Helicopter' 126) ('Applause' 127) ('Gunshot' 128) ) do: [:pair | menu add: (self numberInParens: pair second), pair first localizedMIDI action: pair second]. ^ menu ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 12/31/2005 12:48'! midiNoteMenu "Provides a drop-down menu for setting the note number." | menu midiKey noteName | menu _ CustomMenu new. midiKey _ 48. #(low middle) do: [:oct | #(C 'C#' D 'Eb' E F 'F#' G 'Ab' A 'Bb' B) do: [:n | n = #C ifTrue: [noteName _ oct, ' ', n] ifFalse: [noteName _ n]. menu add: '(', midiKey printString, ') ', noteName action: midiKey. midiKey _ midiKey + 1]]. menu add: '(72) high C' action: 72. ^ menu ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 5/3/2008 09:17'! noteOn: midiKey duration: beats elapsed: elapsedMSecs from: aNotePlayer | stage player | aNotePlayer ifNil: [ "first call, start playing the note" (stage _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ ScratchNotePlayer new]. (player _ stage notePlayerFor: self) ifNil: [^ ScratchNotePlayer new]. ^ player copy noteOn: midiKey]. elapsedMSecs >= ((60000 * beats) // self tempo) ifTrue: [aNotePlayer noteOff]. ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 5/3/2008 09:19'! noteSelector "Returns an instance of a ScratchNoteSelector for selecting notes. This object behaves like a CustomMenu." | stageMorph notePlayer | stageMorph _ (self ownerThatIsA: ScratchStageMorph). stageMorph ifNil: [^ self midiNoteMenu]. notePlayer _ stageMorph notePlayerFor: self. notePlayer ifNil: [^ self midiNoteMenu]. ^ ScratchNoteSelector new setNotePlayer: notePlayer ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 6/3/2009 18:45'! numberInParens: aNumber ^ (UTF32 with: 16r200E) asUTF8, '(' asUTF8, aNumber printString, ') ' ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 5/20/2006 12:37'! playMidi: fileName "Play the midi file with the given name." | fname score player | fname _ fileName, '.mid'. (FileDirectory default fileExists: fname) ifFalse: [^ self]. score _ (MIDIFileReader scoreFromFileNamed: fname). player _ ScorePlayer onScore: score. player openMIDIPort: 0. player play. ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 5/5/2007 16:49'! playSound: soundName | snd | snd _ self soundNamed: soundName ifAbsent: [^ self]. snd playFromStart. ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 12/4/2007 17:45'! rest: duration elapsed: elapsed from: ignored "Do nothing; just wait for the time interval to elapse." ^ nil ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 5/5/2007 16:50'! rewindSound: soundName "Rewind the given sound." | snd | snd _ self soundNamed: soundName ifAbsent: [^ self]. snd rewindSound. ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 3/19/2009 14:22'! setInstrument: aNumber "New 'set instrument to _' block. Set the instrument used for note sounds. Testing in beta but reverted." | stage map n player | (stage _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ self]. map _ #(1 30 25 35). n _ aNumber asNumberNoError asInteger. (n between: 1 and: map size) ifTrue: [n _ map at: n] ifFalse: [n _ 1]. (player _ stage notePlayerFor: self) ifNotNil: [ player instrument: n]. ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 6/3/2009 18:47'! setInstrumentMenu "Provides a drop-down menu for setting the instrument number." | menu | menu _ CustomMenu new. #( ('Piano' 1) ('Electric Guitar' 2) ('Acoustic Guiar' 3) ('Bass' 4) ) do: [:pair | menu add: (self numberInParens: pair second), (pair first localizedMIDI) action: pair second]. ^ menu ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 11/28/2007 12:56'! setTempoTo: aNumber | stage | (stage _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ self]. stage setTempoTo: aNumber. ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 10/19/2007 12:09'! setVolumeTo: aNumber | stage player | volume _ aNumber within: 0 and: 100. (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [ (player _ stage notePlayerFor: self) ifNotNil: [ player volume: volume]]. media do: [:snd | snd isSound ifTrue: [snd volume: volume]]. ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 8/10/2008 12:14'! soundFromIndex: aNumber "Return a sound for the given number--rounded and modulo the number of costumes. Return nil if there are not sounds." | soundList i | soundList _ media select: [:m | m isSound]. soundList size = 0 ifTrue: [^ nil]. i _ ((aNumber rounded - 1) \\ soundList size) + 1. ^ soundList at: i ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 8/10/2008 12:25'! soundNamed: soundNameOrIndex ifAbsent: absentBlock "Return a SoundMedia with the given name (ignoring case differences) or index. If there is none, return the result of evaluating the given block." | snd i | snd _ nil. (soundNameOrIndex isKindOf: String) ifTrue: [ "first, try using the argument as a sound name" snd _ media detect: [:el | el isSound and: [el mediaName caseInsensitiveEqual: soundNameOrIndex]] ifNone: [nil]. snd ifNil: [ i _ self interpretStringAsNumberIfPossible: soundNameOrIndex. i isNumber ifTrue: [ snd _ self soundFromIndex: i]]. snd ifNil: [^ absentBlock value]]. snd ifNil: [snd _ self soundFromIndex: soundNameOrIndex asNumberNoError]. snd ifNil: [^ absentBlock value] ifNotNil: [^ snd] ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 11/27/2007 11:28'! tempo | stage | (stage _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ 60]. ^ stage tempo ! ! !ScriptableScratchMorph methodsFor: 'sound ops' stamp: 'jm 9/24/2007 17:14'! volume ^ volume ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 2/11/2009 09:48'! answer ^ ScratchPrompterMorph lastAnswer ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 10/25/2007 20:33'! asciiFor: keyName | localizedKey | keyName size = 1 ifTrue: [^ keyName first asciiValue]. localizedKey _ keyName localized. 'space' localized = localizedKey ifTrue: [^ Character space asciiValue]. 'enter' localized = localizedKey ifTrue: [^ Character cr asciiValue]. 'up arrow' localized = localizedKey ifTrue: [^ 30]. 'down arrow' localized = localizedKey ifTrue: [^ 31]. 'right arrow' localized = localizedKey ifTrue: [^ 29]. 'left arrow' localized = localizedKey ifTrue: [^ 28]. "needed to support old projects:" 'up' = keyName ifTrue: [^ 30]. 'down' = keyName ifTrue: [^ 31]. 'right' = keyName ifTrue: [^ 29]. 'left' = keyName ifTrue: [^ 28]. ^ -1 "no match; should never happen" ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 10/2/2007 17:48'! attributeNames ^ vars keys ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 6/8/2009 10:36'! coerceSpriteArg: anObject "Coerce the given object to a sprite reference if necessary. If it is a Symbol (e.g. #mouse), just return it. If it is a String, try to find a Sprite with that name. If it is a Number, coerce it to a string and look for a Sprite with a matching name." | s targetName | (anObject isKindOf: ScriptableScratchMorph) ifTrue: [^ anObject]. (anObject isKindOf: Symbol) ifTrue: [^ anObject]. targetName _ anObject isNumber ifTrue: [anObject printString] ifFalse: [anObject]. (s _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ nil]. s submorphsDo: [:m | ((m isKindOf: ScriptableScratchMorph) and: [targetName = m objName]) ifTrue: [^ m]]. ^ nil ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 10/2/2007 17:47'! getAttribute: attr "Answer the value of my variable or built-in attribute with the given name. Answer zero if I have no attribute or variable with the given name." (vars includesKey: attr) ifTrue: [^ vars at: attr]. ^ 0 ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 4/1/2009 10:38'! getAttribute: attr of: anObject "Answer the variable or built-in attribute value for the given sprite. Answer zero if the given sprite does not have a built-in attribute or variable of the given name." | aSpriteOrStage | aSpriteOrStage _ self coerceSpriteArg: anObject. (aSpriteOrStage isKindOf: ScriptableScratchMorph) ifFalse: [^ 0]. ^ aSpriteOrStage getAttribute: attr asString ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'ee 11/10/2007 19:20'! hookupBooleanSensorNames ^ #( 'button pressed' 'connected' ) ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'ee 6/2/2009 16:29'! hookupSensorNames | sensorNames stage virtualSensors | sensorNames _ #( 'slider' 'light' 'sound' 'resistance' ). sensorNames _ sensorNames, #('-' 'tilt' 'distance'). "WeDo sensors" (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [ stage scratchServer ifNotNil: [ virtualSensors _ stage scratchServer sensorNames. virtualSensors size > 0 ifTrue: [ ^ sensorNames, {'-'}, stage scratchServer sensorNames]]]. ^ sensorNames ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 11/20/2006 12:15'! hookupSensorNumber "Backwards compatibility for old blocks." ^ (1 to: 8) collect: [:n | n printString] ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 4/11/2007 10:50'! indexForSensorName: sensorName "Answer the index for the given sensor name." sensorName startsWithDigit ifTrue: [^ sensorName asNumberNoError]. 'slider' = sensorName ifTrue: [^ 1]. 'light' = sensorName ifTrue: [^ 2]. 'sound' = sensorName ifTrue: [^ 3]. (sensorName includesSubString: 'button') ifTrue: [^ 4]. (sensorName includes: $A) ifTrue: [^ 5]. (sensorName includes: $B) ifTrue: [^ 6]. (sensorName includes: $C) ifTrue: [^ 7]. (sensorName includes: $D) ifTrue: [^ 8]. ^ 1 ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 5/12/2006 09:28'! isLoud ^ self class soundRecorder meterLevel > 30 ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'ee 4/8/2008 12:27'! keyNames "Key names for 'key pressed' block menu. Must keep this list in sync with asciiFor:." ^ #('up arrow' 'down arrow' 'right arrow' 'left arrow' 'space'), ($a to: $z), ($0 to: $9) collect: [:ch | ch asString] ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 10/19/2006 07:57'! keyPressed: keyName | ch | ch _ self asciiFor: keyName. (Sensor keyPressed: ch) ifTrue: [^ true]. "if key is a letter, check the opposite case" (ch between: $a asciiValue and: $z asciiValue) ifTrue: [ ^ Sensor keyPressed: ch - 32]. (ch between: $A asciiValue and: $Z asciiValue) ifTrue: [ ^ Sensor keyPressed: ch + 32]. ^ false ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 8/23/2003 12:22'! mousePressed ^ Sensor redButtonPressed ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 5/6/2009 17:45'! mouseX | s centerX | DoubleSize ifTrue: [^ (Sensor cursorPoint x - ScratchOrigin x) // 2]. self inPresentationMode ifFalse: [ s _ self ownerThatIsA: ScratchStageMorph. (s notNil and: [s isQuarterSize]) ifTrue: [ centerX _ s left + (s width // 4). ^ 2 * (Sensor cursorPoint x - centerX)]]. ^ Sensor cursorPoint x - ScratchOrigin x ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 5/6/2009 17:45'! mouseY | s centerY | DoubleSize ifTrue: [^ (Sensor cursorPoint y - ScratchOrigin y) negated // 2]. self inPresentationMode ifFalse: [ s _ self ownerThatIsA: ScratchStageMorph. (s notNil and: [s isQuarterSize]) ifTrue: [ centerY _ s top + (s height // 4). ^ -2 * (Sensor cursorPoint y - centerY)]]. ^ (Sensor cursorPoint y - ScratchOrigin y) negated ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 6/2/2009 11:27'! promptForInput: questionString | s prompter | (s _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ self]. prompter _ ScratchPrompterMorph new. ((self isKindOf: ScratchStageMorph) | (self isHidden)) ifTrue: [ prompter question: questionString] ifFalse: [ self showQuestion: questionString. prompter sprite: self]. prompter left: s center x - (prompter width // 2). prompter bottom: s bottom - 5. prompter isSticky: true. s addMorphFront: prompter. prompter grabKeyboardFocus. ^ prompter ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 3/24/2009 16:55'! promptInProgress | s | (s _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ false]. s submorphsDo: [:m | (m isKindOf: ScratchPrompterMorph) ifTrue: [^ true]]. ^ false ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 3/23/2009 15:28'! sensor: sensorName "Answer the value of the given sensor, or zero if the sensorboard is not available." | stage v sb | (stage _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ 0]. stage scratchServer ifNotNil: [ v _ stage scratchServer sensorValueFor: sensorName. v ifNotNil: [^ v]]. 'tilt' = sensorName ifTrue: [^ WeDoPlugin tilt]. 'distance' = sensorName ifTrue: [^ WeDoPlugin distance]. sb _ stage sensorBoard. sb tryToOpenPort ifFalse: [^ 0]. "could not open" ^ sb sensor: (self indexForSensorName: sensorName) ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 11/20/2006 12:11'! sensorPressed: sensorName "Answer true if a button connected to the given input of the sensor board is pressed. That is, if the sensor value is less than 10. Answer false if the sensor board cannot be opened." | stage sb | (stage _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ false]. sb _ stage sensorBoard. sb tryToOpenPort ifFalse: [^ false]. "could not open" ^ (sb sensor: (self indexForSensorName: sensorName)) < 10 ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 5/10/2006 11:12'! soundLevel ^ self class soundRecorder meterLevel ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 10/19/2003 09:26'! timer | now | now _ Time millisecondClockValue. TimerStartMSecs ifNil: [TimerStartMSecs _ now]. TimerStartMSecs > now ifTrue: [TimerStartMSecs _ now]. ^ (now - TimerStartMSecs) asFloat / 1000.0 ! ! !ScriptableScratchMorph methodsFor: 'sensing ops' stamp: 'jm 10/19/2003 09:20'! timerReset TimerStartMSecs _ Time millisecondClockValue. ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'ee 6/27/2008 17:46'! addGlobalList | sFrame listName | (sFrame _ self ownerThatIsA: ScratchFrameMorph) ifNil: [^ self beep]. listName _ StringDialog ask: 'List name?'. listName size = 0 ifTrue: [^ self]. sFrame workPane createListNamed: listName. sFrame viewerPane categoryChanged: 'variables'. ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'ee 6/27/2008 17:44'! addList | sFrame result listName | (sFrame _ self ownerThatIsA: ScratchFrameMorph) ifNil: [^ self beep]. result _ NewVariableDialog ask: 'List name?'. result = #cancelled ifTrue: [^ self]. listName _ result first. result second ifTrue: [self createListNamed: listName] ifFalse: [sFrame workPane createListNamed: listName]. sFrame viewerPane categoryChanged: 'variables'. ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 6/3/2008 18:09'! append: anObject toList: listName | list | list _ self listNamed: listName ifNone: [^ 0]. list insertLine: (self asListElement: anObject) at: (list lineCount + 1). ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 8/13/2008 17:04'! asListElement: anObject "Answer the given object converted to an object suitable for storing in a list." (anObject isKindOf: String) ifTrue: [^ anObject]. anObject isUnicode ifTrue: [^ anObject]. (anObject isKindOf: Boolean) ifTrue: [ ^ anObject ifTrue: ['1'] ifFalse: ['0']]. ^ anObject printString ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 5/1/2008 10:51'! contentsOfList: listName | list | list _ self listNamed: listName ifNone: [^ '']. ^ list concatenatedLines ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'ee 8/7/2008 21:12'! createListNamed: listName | list stage n | (self variableNameInUse: listName) ifTrue: [ self beep. DialogBoxMorph warn: 'That variable name is already in use'. ^ self]. lists at: listName put: (list _ ScratchListMorph new listName: listName target: self). (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [ n _ (stage submorphs select: [:m | m isKindOf: ScratchListMorph]) size. stage addMorph: (list position: stage topRight - ((list width + 10)@0) + (0@(10+(20*n)))). list startStepping]. ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 5/1/2008 11:38'! defaultListName | stage | lists size > 0 ifTrue: [^ lists keys asArray sort first]. (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [ stage lists size > 0 ifTrue: [^ stage lists keys asArray sort first]]. ^ '' ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 6/20/2008 11:14'! deleteLine: lineNum ofList: listName | list | list _ self listNamed: listName ifNone: [^ self]. lineNum = 'all' ifTrue: [list clear. ^ self]. list deleteLineAt: (self lineNum: lineNum forList: list) ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 5/5/2008 10:02'! deleteList "Ask the user which list variable to delete, then remove it." | sFrame stage menu choice | sFrame _ self ownerThatIsA: ScratchFrameMorph. sFrame ifNil: [^ self]. stage _ sFrame workPane. (stage listVarNames isEmpty) & (self listVarNames isEmpty) ifTrue: [^ self inform: 'No lists.' localized]. menu _ CustomMenu new. stage listVarNames do: [:v | menu add: v action: (Array with: v with: #global)]. self = stage ifFalse: [ stage listVarNames isEmpty ifFalse: [menu addLine]. self listVarNames do: [:v | menu add: v action: (Array with: v with: #local)]]. choice _ menu startUp. choice ifNil: [^ self]. choice second = #global ifTrue: [stage deleteList: choice first] ifFalse: [self deleteList: choice first]. sFrame viewerPane categoryChanged: 'variables'. ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jens 9/22/2008 11:31'! deleteList: listName "Delete the list variable with the given name. Do nothing if the variable doesn't exist." | stage | stage _ self ownerThatIsA: ScratchStageMorph. lists removeKey: listName asString ifAbsent: []. stage submorphs do: [:m | ((m isKindOf: ScratchListMorph) and: [(m listName = listName) and: [m target = self]]) ifTrue: [m delete]]. ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 5/6/2008 20:36'! getLine: lineNum ofList: listName | list | list _ self listNamed: listName ifNone: [^ '']. ^ list lineAt: (self lineNum: lineNum forList: list) ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 6/3/2008 18:09'! insert: anObject at: lineNum ofList: listName | list i | list _ self listNamed: listName ifNone: [^ self]. i _ self lineNum: lineNum forList: list. #last = lineNum ifTrue: [i _ list lineCount + 1]. list insertLine: (self asListElement: anObject) at: i. ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 5/1/2008 11:18'! lineCountOfList: listName | list | list _ self listNamed: listName ifNone: [^ 0]. ^ list lineCount ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 7/30/2008 16:26'! lineNum: lineNum forList: list | s | lineNum isNumber ifTrue: [^ lineNum asInteger]. s _ lineNum. s isUnicode ifTrue: [s _ String withAll: lineNum]. (s isKindOf: String) ifTrue: [ #first = s ifTrue: [^ 1]. #last = s ifTrue: [^ list lineCount]. #any = s ifTrue: [ list lineCount = 0 ifTrue: [^ 1]. ^ (1 to: list lineCount) atRandom]. ^ s asNumberNoError]. ^ 0 ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 1/28/2009 10:20'! list: listName contains: anObject | list | list _ self listNamed: listName ifNone: [^ false]. ^ list contains: anObject ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 8/10/2008 14:33'! listIndexForDeleteMenu | menu | menu _ CustomMenu new. #('1' last) do: [:s | menu add: s action: s]. menu addLine. menu add: #all action: #all. menu localize. ^ menu ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 8/10/2008 14:31'! listIndexMenu | menu | menu _ CustomMenu new. #('1' last any) do: [:s | menu add: s action: s]. menu localize. ^ menu ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 5/1/2008 10:23'! listNamed: aString ifNone: aBlock "Answer a list with the given name. First check the local list variables, otherwise check the global list variables. If there is no list variable with the given name, return the result of evaluating the given block." | result stage | (result _ lists at: aString ifAbsent: [nil]) ifNotNil: [^ result]. (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [ (result _ stage lists at: aString ifAbsent: [nil]) ifNotNil: [^ result]]. ^ aBlock value ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 7/30/2008 19:38'! listVarMenu "Answer a menu for selecting a list variables." | result stage | result _ #(). ((stage _ self ownerThatIsA: ScratchStageMorph) notNil & (stage ~= self)) ifTrue: [ result _ stage listVarNames]. lists size > 0 ifTrue: [ result size > 0 ifTrue: [result _ result copyWith: '-']. result _ result, self listVarNames]. ^ result ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 5/1/2008 14:48'! listVarNames "Answer a list of list variable names." ^ lists keys asArray sort ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 5/1/2008 10:23'! lists ^ lists ! ! !ScriptableScratchMorph methodsFor: 'list ops' stamp: 'jm 6/3/2008 18:10'! setLine: lineNum ofList: listName to: anObject | list | list _ self listNamed: listName ifNone: [^ '']. ^ list setLineAt: (self lineNum: lineNum forList: list) to: (self asListElement: anObject) ! ! !ScriptableScratchMorph methodsFor: 'string ops' stamp: 'jm 6/22/2009 12:38'! concatenate: arg1 with: arg2 "Concatenate two strings. Arguments are converted to strings." ^ arg1 asString asUTF8, arg2 asString asUTF8 ! ! !ScriptableScratchMorph methodsFor: 'string ops' stamp: 'jm 2/26/2009 20:52'! letter: index of: anObject "Answer the ith letter of the given string. Answer the empty string if the index is out of bounds." | s i | s _ anObject. anObject isUnicode ifTrue: [s _ anObject asUTF32] ifFalse: [s _ anObject asString asUTF32]. i _ self letterNum: index of: s. ((i < 1) | (i > s size)) ifTrue: [^ '']. ^ (UTF32 with: (s at: i)) asUTF8 ! ! !ScriptableScratchMorph methodsFor: 'string ops' stamp: 'jm 2/26/2009 20:40'! letterNum: letterNum of: aString "Answer the ith letter of the given string. Answer the empty string if the index is out of bounds." | s | letterNum isNumber ifTrue: [^ letterNum asInteger]. s _ letterNum. s isUnicode ifTrue: [s _ String withAll: letterNum]. (s isKindOf: String) ifTrue: [ #first = s ifTrue: [^ 1]. #last = s ifTrue: [^ aString size]. #any = s ifTrue: [ aString size = 0 ifTrue: [^ 1]. ^ (1 to: aString size lineCount) atRandom]. ^ s asNumberNoError]. ^ 0 ! ! !ScriptableScratchMorph methodsFor: 'string ops' stamp: 'jm 4/1/2009 10:21'! stringLength: anObject anObject isUnicode ifTrue: [^ anObject asUTF32 size] ifFalse: [^ anObject asString asUTF32 size]. ! ! !ScriptableScratchMorph methodsFor: 'motor ops' stamp: 'jm 4/8/2009 14:40'! allMotorsOff "Turn all motors off." self motorOff: ' '. ! ! !ScriptableScratchMorph methodsFor: 'motor ops' stamp: 'jm 4/14/2009 11:11'! allMotorsOn "Turn all motors off." self motorOn: ' '. ! ! !ScriptableScratchMorph methodsFor: 'motor ops' stamp: 'jm 4/21/2009 21:06'! motor: motorName direction: directionName "Set the direction of the given motor." | dir | dir _ 0. (directionName includesSubString: 'reverse') ifTrue: [dir _ 0]. (directionName includesSubString: 'this way') ifTrue: [dir _ 1]. (directionName includesSubString: 'that way') ifTrue: [dir _ -1]. 'A' = motorName ifTrue: [WeDoPlugin motorADirection: dir]. 'B' = motorName ifTrue: [WeDoPlugin motorBDirection: dir]. ' ' = motorName ifTrue: [ WeDoPlugin motorADirection: dir. WeDoPlugin motorBDirection: dir]. ! ! !ScriptableScratchMorph methodsFor: 'motor ops' stamp: 'jm 3/26/2009 22:13'! motor: motorName power: power "Set the power level of the given motor (0-100)." 'A' = motorName ifTrue: [WeDoPlugin motorAPower: power]. 'B' = motorName ifTrue: [WeDoPlugin motorBPower: power]. ' ' = motorName ifTrue: [ WeDoPlugin motorAPower: power. WeDoPlugin motorBPower: power]. ! ! !ScriptableScratchMorph methodsFor: 'motor ops' stamp: 'ee 6/2/2009 14:34'! motorDirection ^ { 'this way'. 'that way'. 'reverse' } ! ! !ScriptableScratchMorph methodsFor: 'motor ops' stamp: 'jm 3/26/2009 22:10'! motorNames ^ #(' ' A B) ! ! !ScriptableScratchMorph methodsFor: 'motor ops' stamp: 'jm 4/8/2009 15:59'! motorOff: motorName "Turn the given motor off." 'A' = motorName ifTrue: [WeDoPlugin motorAOn: false]. 'B' = motorName ifTrue: [WeDoPlugin motorBOn: false]. ' ' = motorName ifTrue: [ WeDoPlugin motorAOn: false. WeDoPlugin motorBOn: false]. ! ! !ScriptableScratchMorph methodsFor: 'motor ops' stamp: 'jm 3/26/2009 22:13'! motorOn: motorName "Turn the given motor on." 'A' = motorName ifTrue: [WeDoPlugin motorAOn: true]. 'B' = motorName ifTrue: [WeDoPlugin motorBOn: true]. ' ' = motorName ifTrue: [ WeDoPlugin motorAOn: true. WeDoPlugin motorBOn: true]. ! ! !ScriptableScratchMorph methodsFor: 'motor ops' stamp: 'jm 3/20/2009 17:34'! motorOn: motorName duration: secs elapsed: elapsedMSecs from: motorID "Turn the given motor on for the given number of seconds." motorID ifNil: [ "first call, start motor" self motorOn: motorName. ^ motorName]. elapsedMSecs >= (1000 * secs) ifTrue: [self motorOff: motorID]. ! ! !ScriptableScratchMorph methodsFor: 'motor ops' stamp: 'jm 4/8/2009 15:59'! motorOnFor: secs elapsed: elapsedMSecs from: motorID "Turn all motors on for the given number of seconds." motorID ifNil: [ "first call, start motor" self motorOn: ' '. ^ #' ']. elapsedMSecs >= (1000 * secs) ifTrue: [self motorOff: motorID]. ! ! !ScriptableScratchMorph methodsFor: 'motor ops' stamp: 'jm 4/14/2009 11:13'! setMotorDirection: directionName self motor: ' ' direction: directionName. ! ! !ScriptableScratchMorph methodsFor: 'motor ops' stamp: 'jm 4/8/2009 15:59'! startMotorPower: power self motor: ' ' power: power. self motorOn: ' '. ! ! !ScriptableScratchMorph methodsFor: 'other ops' stamp: 'jm 6/23/2004 10:05'! broadcast: name "Broadcast the given event name with zero as its argument." self broadcast: name withArgument: 0. ! ! !ScriptableScratchMorph methodsFor: 'other ops' stamp: 'jm 5/15/2008 13:49'! broadcast: name withArgument: arg "Broadcast the given event with the given argument." | stage | (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [ stage broadcastEventNamed: name asString with: arg]. ! ! !ScriptableScratchMorph methodsFor: 'other ops' stamp: 'jm 10/18/2007 23:09'! computeFunction: functionName of: aNumber "Return the result of computing the given mathematical function on the given number." 'abs' = functionName ifTrue: [^ aNumber abs]. 'sqrt' = functionName ifTrue: [^ aNumber sqrt]. 'sin' = functionName ifTrue: [^ aNumber degreesToRadians sin]. 'cos' = functionName ifTrue: [^ aNumber degreesToRadians cos]. 'tan' = functionName ifTrue: [^ aNumber degreesToRadians tan]. 'asin' = functionName ifTrue: [^ (aNumber within: -1.0 and: 1.0) arcSin radiansToDegrees]. 'acos' = functionName ifTrue: [^ (aNumber within: -1.0 and: 1.0) arcCos radiansToDegrees]. 'atan' = functionName ifTrue: [^ aNumber arcTan radiansToDegrees]. 'ln' = functionName ifTrue: [^ aNumber ln]. 'log' = functionName ifTrue: [^ aNumber log]. 'e ^' = functionName ifTrue: [^ aNumber exp]. '10 ^' = functionName ifTrue: [^ 10.0 raisedTo: aNumber]. ^ 0 ! ! !ScriptableScratchMorph methodsFor: 'other ops' stamp: 'jm 4/27/2007 13:14'! eventReceived: event "Start all non-running stacks with an EventHat matching the given events and answer a collection of the new processes. If a process is already running for a given stack, don't start a new one." | targetScripts newProcs | targetScripts _ #(). event name = 'Scratch-KeyPressedEvent' ifTrue: [ targetScripts _ self scripts select: [:s | (s class == KeyEventHatMorph) and: [s respondsToKeyEvent: event argument]]] ifFalse: [ event name = 'Scratch-MouseClickEvent' ifTrue: [ self isHidden not ifTrue: [ targetScripts _ self scripts select: [:s | s class == MouseClickEventHatMorph]]] ifFalse: [ targetScripts _ self scripts select: [:s | (s class == EventHatMorph) and: [s eventName caseInsensitiveEqual: event name]]]]. newProcs _ targetScripts asArray collect: [:script | script startForEvent: event]. ^ newProcs select: [:p | p notNil] ! ! !ScriptableScratchMorph methodsFor: 'other ops' stamp: 'jm 10/10/2007 11:50'! mathFunctionNames "Answer a collection of math function names." ^ #( 'abs' 'sqrt' 'sin' 'cos' 'tan' 'asin' 'acos' 'atan' 'ln' 'log' 'e ^' '10 ^') ! ! !ScriptableScratchMorph methodsFor: 'other ops' stamp: 'jm 5/18/2006 07:43'! mwait: duration elapsed: elapsed from: ignored "Do nothing; just wait for the time interval to elapse." ^ nil ! ! !ScriptableScratchMorph methodsFor: 'other ops' stamp: 'jm 10/4/2007 15:23'! randomFrom: start to: stop "Answer a random number within the given range. If both min and max are integers, the result is rounded to the nearest integer." | min max result | min _ start min: stop. max _ start max: stop. result _ (RandomGen next * (max - min)) + min. min isInteger & max isInteger ifTrue: [result _ (RandomGen next * ((max + 1) - min)) truncated + min] ifFalse: [result _ (RandomGen next * (max - min)) + min]. ^ result ! ! !ScriptableScratchMorph methodsFor: 'other ops' stamp: 'jm 8/8/2005 12:21'! stopAll "Stop everything!!" | stage | stage _ self ownerThatIsA: ScratchStageMorph. stage ifNotNil: [stage stopAll]. ! ! !ScriptableScratchMorph methodsFor: 'other ops' stamp: 'jm 12/1/2006 19:03'! stopAllSounds "Stop all sounds and MIDI notes/drums." | stage | stage _ self ownerThatIsA: ScratchStageMorph. stage ifNotNil: [stage stopAllSounds]. ! ! !ScriptableScratchMorph methodsFor: 'other ops' stamp: 'jm 8/22/2003 19:29'! wait: duration elapsed: elapsed from: ignored "Do nothing; just wait for the time interval to elapse." ^ nil ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 7/3/2008 16:55'! addGlobalVariable "Ask the user for a variable name, then add a background (global) variable of that name." | sFrame varName | (sFrame _ self ownerThatIsA: ScratchFrameMorph) ifNil: [^ self beep]. varName _ StringDialog ask: 'Variable name?'. varName size = 0 ifTrue: [^ self]. varName _ varName asUTF8. (sFrame workPane variableNameInUse: varName) ifTrue: [ self beep. DialogBoxMorph warn: 'That variable name is already in use'. ^ self]. sFrame workPane addVariable: varName. sFrame viewerPane categoryChanged: 'variables'. self addWatcherForNewVariable: varName withScope: sFrame workPane. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 7/3/2008 16:55'! addVariable "Ask the user for a variable name, then add a user variable of that name. This version is for sprites and supports the option of making the variable specific to the sprite (i.e. local)." | sFrame result varName varOwner | (sFrame _ self ownerThatIsA: ScratchFrameMorph) ifNil: [^ self beep]. result _ NewVariableDialog ask: 'Variable name?'. result = #cancelled ifTrue: [^ self]. varName _ result first asUTF8. varOwner _ result second ifTrue: [self] ifFalse: [sFrame workPane]. (varOwner variableNameInUse: varName) ifTrue: [ self beep. DialogBoxMorph warn: 'That variable name is already in use'. ^ self]. varOwner addVariable: varName. sFrame viewerPane categoryChanged: 'variables'. self addWatcherForNewVariable: varName withScope: varOwner. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 8/21/2006 11:59'! addVariable: varName "Add a new user variable with the given name to this object. Do nothing if the variable already exists or is built in." (vars includesKey: varName asString) ifFalse: [ vars at: varName asString put: 0]. self isClone: false. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 6/3/2009 16:43'! addVariable: varName value: anObject "Add a new user variable with the given name to this object. Do nothing if the variable already exists or is built in." (vars includesKey: varName asString) ifFalse: [ vars at: varName asString put: anObject]. self isClone: false. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 12/1/2007 22:10'! addWatcherForNewVariable: varName withScope: spriteOrStage "Add a watcher on the stage once a variable has been created." | sFrame | (sFrame _ self ownerThatIsA: ScratchFrameMorph) ifNil: [^ self]. sFrame viewerPane pageViewer submorphs do: [:s | (s isKindOf: ScratchBlockPaletteMorph) ifTrue: [ s submorphs do: [:block | (block isKindOf: VariableBlockMorph) ifTrue: [ (block receiver = spriteOrStage) & (block commandSpec = varName) ifTrue: [ (sFrame watcherForBlock: block) ifNil: [ block toggleWatcher]]]]]]. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 7/30/2008 18:52'! allVarNames "Answer a set of all variable and list variable names." | allNames | allNames _ vars keys. allNames addAll: lists keys. ^ allNames ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 7/7/2008 10:56'! changeVar: varName by: increment "Change the value of the given variable of this object by the given amount." | stage n | (vars includesKey: varName asString) ifFalse: [ stage _ self ownerThatIsA: ScratchStageMorph. (stage notNil and: [stage ~= self]) ifTrue: [ stage changeVar: varName by: increment]. ^ self]. n _ vars at: varName asString ifAbsent: [^ self]. self setVar: varName to: n asNumberNoError + increment asNumberNoError. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 10/28/2007 20:53'! deleteVariable "Ask the user which user variable to delete, then remove it." | sFrame stage menu choice | sFrame _ self ownerThatIsA: ScratchFrameMorph. sFrame ifNil: [^ self]. stage _ sFrame workPane. (stage varNames isEmpty) & (self varNames isEmpty) ifTrue: [^ self inform: 'No variables.' localized]. menu _ CustomMenu new. stage varNames do: [:v | menu add: v action: (Array with: v with: #global)]. self = stage ifFalse: [ stage varNames isEmpty ifFalse: [menu addLine]. self varNames do: [:v | menu add: v action: (Array with: v with: #local)]]. choice _ menu startUp. choice ifNil: [^ self]. choice second = #global ifTrue: [stage deleteVariable: choice first] ifFalse: [self deleteVariable: choice first]. sFrame viewerPane categoryChanged: 'variables'. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 12/1/2007 23:55'! deleteVariable: varName "Delete the variable with the given name. Do nothing if the variable doesn't exist." | frame | frame _ self ownerThatIsA: ScratchFrameMorph. frame ifNotNil: [frame deleteWatchersForVar: varName ofSprite: self]. vars removeKey: varName asString ifAbsent: [^ self]. self isClone: false. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 8/6/2008 22:10'! ensureListExists: listName "If a list with the given name is not visible to this object, make one." | stage | (lists includesKey: listName) ifTrue: [^ self]. (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [ (stage listVarNames includes: listName) ifTrue: [^ self]]. "list not found; create it" lists at: listName put: (ScratchListMorph new listName: listName target: self). ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 8/6/2008 21:05'! ensureVariableExists: varName "Make sure that the variable with the given name is visible to this object. Do nothing if the variable already exists." | stage | (vars includesKey: varName) ifTrue: [^ self]. (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [ (stage varNames includes: varName) ifTrue: [^ self]]. "variable not found; create it" vars at: varName asString put: 0. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 11/15/2006 11:32'! getVar: varName "Answer the value of the given user variable of this object, or 0 if the variable has been deleted." ^ vars at: varName asString ifAbsent: [0] ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 7/7/2008 10:28'! hideVariable: varName "Hide the watcher(s) for the given variable. If the receiver is a sprite and both it and and the stage have the given variable, hide both. Do nothing if the variable does not exist or is already hidden." | stage | self showOrHideVariable: varName show: false. stage _ self ownerThatIsA: ScratchStageMorph. stage ~= self ifTrue: [stage showOrHideVariable: varName show: false]. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 6/22/2009 16:40'! setVar: varName to: newValue "Set the value of the given variable of this object to the given value." | vName stage | vName _ varName asString. "convert Symbol to String if needed" (vars includesKey: vName) ifFalse: [ stage _ self ownerThatIsA: ScratchStageMorph. (stage notNil and: [stage ~= self]) ifTrue: [ stage setVar: varName to: newValue]. ^ self]. vars at: vName put: newValue. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 8/3/2008 12:44'! showOrHideVariable: varName show: showFlag "Show the watcher for the given variable. Do nothing if the variable does not exist or is already showing." | frame w b palette | (self varNames includes: varName) ifFalse: [^ self]. frame _ self ownerThatIsA: ScratchFrameMorph. frame ifNil: [ (w _ self ownerThatIsA: OffscreenWorldMorph) ifNil: [^ self]. frame _ w frame]. b _ VariableBlockMorph new commandSpec: varName; receiver: self blockReceiver. w _ frame watcherForBlock: b. showFlag ifTrue: [w ifNil: [frame showWatcher: b createWatcher]] ifFalse: [w ifNotNil: [w delete]]. palette _ frame viewerPane pageViewer contents. palette ifNotNil: [palette updateWatcherButtonsForFrame: frame]. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 7/7/2008 10:17'! showVariable: varName "Show the watcher(s) for the given variable. If the receiver is a sprite and both it and and the stage have the given variable, show both. Do nothing if the variable does not exist or is already showing." | stage | self showOrHideVariable: varName show: true. stage _ self ownerThatIsA: ScratchStageMorph. stage ~= self ifTrue: [stage showOrHideVariable: varName show: true]. ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 11/15/2006 11:22'! varNames "Answer a list of variable names." ^ vars keys asArray sort ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 7/30/2008 18:41'! varNamesMenu "Answer a list of variable names." | varList stage | varList _ #(). (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [varList _ stage varNames]. self == stage ifFalse: [ varList size > 0 ifTrue: [varList _ varList copyWith: #-]. varList _ varList, self varNames]. ^ varList! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 7/30/2008 18:36'! variableBlockColor ^ Color h: 25 s: 0.88 v: 0.95 ! ! !ScriptableScratchMorph methodsFor: 'variables' stamp: 'jm 7/30/2008 18:53'! variableNameInUse: varName "Answer true if the given variable name in the receiver would conflict with an existing variable or list. For example, if the receiver is the Stage and the given variable name is used by any sprite." | stage | (self allVarNames includes: varName) ifTrue: [^ true]. self isSprite ifTrue: [ (stage _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ false]. ^ stage allVarNames includes: varName] ifFalse: [ submorphs do: [:m | (m isKindOf: ScratchSpriteMorph) ifTrue: [ (m allVarNames includes: varName) ifTrue: [^ true]]]]. ^ false ! ! !ScriptableScratchMorph methodsFor: 'scripts' stamp: 'ee 5/14/2008 13:00'! addComment: aScratchCommentMorph "Aligns the newly added script below the lowest script in the pane." | y bottom | y _ 10. blocksBin submorphsDo: [:m | bottom _ (m fullBounds bottom) - (blocksBin position y). (bottom > y) ifTrue: [y _ bottom]]. aScratchCommentMorph position: blocksBin position + (20@(y+10)). blocksBin addMorph: aScratchCommentMorph. ! ! !ScriptableScratchMorph methodsFor: 'scripts' stamp: 'jm 6/26/2006 09:57'! addStack: aBlockStack "Aligns the newly added script below the lowest script in the pane." | y bottom | y _ 10. blocksBin submorphsDo: [:m | bottom _ (m fullBounds bottom) - (blocksBin position y). (bottom > y) ifTrue: [y _ bottom]]. aBlockStack position: blocksBin position + (20@(y+10)). aBlockStack newScriptOwner: self. blocksBin addMorph: aBlockStack. ! ! !ScriptableScratchMorph methodsFor: 'scripts' stamp: 'jm 5/7/2009 15:05'! duplicate | newSprite s | newSprite _ self duplicateNoAttach. self world activeHand attachMorph: newSprite. ((s _ self ownerThatIsA: ScratchStageMorph) notNil and: [s isQuarterSize]) ifTrue: [ self world activeHand drawHalfSize: true]. ^ newSprite ! ! !ScriptableScratchMorph methodsFor: 'scripts' stamp: 'jm 8/10/2007 10:14'! scripts "Answer my scripts, a collection of HatBlockMorphs." (blocksBin isKindOf: Morph) ifFalse: [^ blocksBin]. ^ blocksBin submorphs select: [:m | m isKindOf: HatBlockMorph] ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jm 10/16/2007 15:32'! addEventNamesTo: aSet "Add the names of all events implemented by this morph, excluding keyboard events. Suppress duplicates (ignoring case differences)." (blocksBin isKindOf: Morph) ifFalse: [^ self]. blocksBin allMorphsDo: [:m | (m class = EventTitleMorph) ifTrue: [ (aSet anySatisfy: [:el | el caseInsensitiveEqual: m eventName]) ifFalse: [ aSet add: m eventName]]]. ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'ee 2/3/2009 13:32'! addGenericListBlocksTo: page y: startY "Add the generic list blocks to the given page starting at the given y offset." | addButton deleteButton x y hasLists stage | addButton _ ScratchFrameMorph buttonLabel: 'Make a list' localized selector: #addList. (self isKindOf: ScratchStageMorph) ifTrue: [addButton actionSelector: #addGlobalList]. deleteButton _ ScratchFrameMorph buttonLabel: 'Delete a list' localized selector: #deleteList. x _ 13. y _ startY + 10. page addMorph: (addButton target: self; position: x@y). y _ addButton bottom + 3. hasLists _ self listVarNames size > 0. (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [ stage listVarNames size > 0 ifTrue: [hasLists _ true]]. hasLists ifFalse: [^ self]. page addMorph: (deleteButton target: self; position: x@y). y _ deleteButton bottom + 10. y _ (self addListReportersTo: page x: x y: y) + 10. (self blocksFor: 'list') do: [:blockOrSym | (blockOrSym = #-) | (blockOrSym = #~) ifTrue: [ (blockOrSym = #-) ifTrue: [y _ y + 15]. "insert a full space" (blockOrSym = #~) ifTrue: [y _ y + 5]] "insert a half space" ifFalse: [ y _ self createBlock: blockOrSym atPosition: x@y onPage: page. page submorphs last color: ScriptableScratchMorph listBlockColor]]. ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jm 7/30/2008 19:07'! addGenericVariableBlocksTo: page x: x y: startY "Add the generic variable blocks to the given page starting at the given y offset. Answer the new y." | y vName stage block varBlocks | y _ startY. "pick a default variable name" vName _ nil. stage _ self ownerThatIsA: ScratchStageMorph. (stage notNil and: [stage varNames size > 0]) ifTrue: [ vName _ stage varNames first] ifFalse: [ self varNames size = 0 ifTrue: [^ y]. vName _ self varNames first]. varBlocks _ OrderedCollection new. block _ SetterBlockMorph new initSetterForVar: vName; receiver: self blockReceiver. block expressionArg stringExpression: '0'. varBlocks add: block. block _ SetterBlockMorph new initChangerForVar: vName; receiver: self blockReceiver. block expressionArg numExpression: '1'. varBlocks add: block. (self blocksFor: 'variables') do: [:b | b defaultArgs: (Array with: vName). varBlocks add: b]. varBlocks do: [:b | b color: self variableBlockColor. page addMorph: (b position: x@y). y _ b bottom + 3]. ^ y ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'ee 8/11/2008 19:52'! addListReportersTo: page x: x y: startY "Add the list block reporters to the given page starting at the given y offset. Answer the new y." | y stage b watcherButton yOffset line line2 | y _ startY. stage _ self ownerThatIsA: ScratchStageMorph. (stage notNil and: [stage ~= self]) ifTrue: [ stage listVarNames do: [:listVarName | b _ ListContentsBlockMorph new color: ScriptableScratchMorph listBlockColor; receiver: stage blockReceiver; commandSpec: listVarName; selector: #contentsOfList:. watcherButton _ self createToggleButtonFor: b. yOffset _ (b fullBounds height - watcherButton height) // 2. page addMorph: (watcherButton position: x @ (y + yOffset)). page addMorph: (b position: (x + watcherButton width + 4)@y). y _ y + b height + 3]. (self listVarNames size > 0) ifTrue: [ line _ Morph new. line extent: 90@1; color: Color gray darker darker; position: x@(y+2). line2 _ Morph new. line2 extent: 90@1; color: Color gray lighter; position: x@(y+3). page addMorph: line; addMorph: line2. y _ y + 9]]. self listVarNames do: [:listVarName | b _ ListContentsBlockMorph new color: ScriptableScratchMorph listBlockColor; receiver: self blockReceiver; commandSpec: listVarName; selector: #contentsOfList:. watcherButton _ self createToggleButtonFor: b. yOffset _ (b fullBounds height - watcherButton height) // 2. page addMorph: (watcherButton position: x @ (y + yOffset)). page addMorph: (b position: (x + watcherButton width + 4)@y). y _ y + b height + 3]. ^ y ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'ee 8/11/2008 19:52'! addVariableReportersTo: page x: x y: startY "Add the list block reporters to the given page starting at the given y offset. Answer the new y." | y stage b watcherButton yOffset line line2 | y _ startY. stage _ self ownerThatIsA: ScratchStageMorph. (stage notNil and: [stage ~= self]) ifTrue: [ stage varNames do: [:vName | b _ VariableBlockMorph new commandSpec: vName; receiver: stage blockReceiver. watcherButton _ self createToggleButtonFor: b. yOffset _ (b fullBounds height - watcherButton height) // 2. page addMorph: (watcherButton position: x @ (y + yOffset)). page addMorph: (b position: (x + watcherButton width + 4)@y). y _ y + b height + 3]. (self varNames size > 0) ifTrue: [ line _ Morph new. line extent: 90@1; color: Color gray darker darker; position: x@(y+2). line2 _ Morph new. line2 extent: 90@1; color: Color gray lighter; position: x@(y+3). page addMorph: line; addMorph: line2. y _ y + 9]]. self varNames do: [:vName | b _ VariableBlockMorph new commandSpec: vName; receiver: self blockReceiver. watcherButton _ self createToggleButtonFor: b. yOffset _ (b fullBounds height - watcherButton height) // 2. page addMorph: (watcherButton position: x @ (y + yOffset)). page addMorph: (b position: (x + watcherButton width + 4)@y). y _ y + b height + 3]. ^ y ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jm 7/7/2008 10:34'! blockCategories "Answer a list of block categories." ^ (self class blockSpecs select: [:el | (el isKindOf: String) and: [el ~= '-' and: [el ~= '~']]]) asArray ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'ee 2/27/2009 12:06'! blockFromSpec: spec color: blockColor "Create a block from the given block specification. Answer nil if I don't implement the block spec selector." | blockLabelSpec blockType selector defaultArgs block rcvr argPermutation | blockLabelSpec _ ScratchTranslator translationFor: (spec at: 1). argPermutation _ CommandBlockMorph argPermutationForSpec: (spec at: 1) withTranslation: blockLabelSpec. blockType _ spec at: 2. selector _ (spec at: 3) asSymbol. defaultArgs _ self defaultArgsFor: spec. (#(E K M S W) includes: blockType) ifTrue: [ ^ (self hatBlockType: blockType) color: blockColor]. "basic block type: normal or C-shaped" (blockType includes: $c) ifTrue: [ selector = #doIfElse ifTrue: [block _ IfElseBlockMorph new isSpecialForm: true] ifFalse: [block _ CBlockMorph new isSpecialForm: true]] ifFalse: [ (blockType includes: $r) | (blockType includes: $b) ifTrue: [block _ ReporterBlockMorph new] ifFalse: [block _ CommandBlockMorph new]]. (blockType includes: $b) ifTrue: [block isBoolean: true]. (blockType includes: $s) ifTrue: [block isSpecialForm: true]. (blockType includes: $t) ifTrue: [block isTimed: true]. (ScriptableScratchMorph isSpriteSpecificTarget: self selector: selector) ifTrue: [rcvr _ self] ifFalse: [rcvr _ self ownerThatIsA: ScratchStageMorph]. ^ block argPermutation: argPermutation; color: blockColor; selector: selector; commandSpec: blockLabelSpec; defaultArgs: defaultArgs; receiver: rcvr ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jens 3/6/2009 00:31'! blockFromTuple: tuple receiver: scriptOwner "Answer a new block for the given tuple." | k spec blockColor block argCount arg argBlock | k _ tuple first. (#(readVariable changeVariable) includes: k) ifTrue: [ ^ self variableBlockFromTuple: tuple receiver: scriptOwner]. #contentsOfList: = k ifTrue: [ ^ ListContentsBlockMorph new color: ScriptableScratchMorph listBlockColor; receiver: scriptOwner; commandSpec: tuple second; selector: #contentsOfList:]. (#(EventHatMorph KeyEventHatMorph MouseClickEventHatMorph WhenHatBlockMorph) includes: k) ifTrue: [ block _ self hatBlockFromTuple: tuple receiver: scriptOwner. (block isKindOf: WhenHatBlockMorph) ifTrue: [block color: Color red]. ^ block]. #scratchComment = k ifTrue: [ block _ ScratchCommentMorph new. tuple size > 1 ifTrue: [block commentMorph contents: (tuple at: 2)]. tuple size > 2 ifTrue: [(tuple at: 3) ifFalse: [block toggleShowing]]. tuple size > 3 ifTrue: [block width: (tuple at: 4)]. tuple size > 4 ifTrue: [block anchor: (self blockWithID: (tuple at: 5))]. ^ block]. #comment: = k ifTrue: [ block _ CommentBlockMorph new. tuple size > 1 ifTrue: [block comment: (tuple at: 2)]. tuple size > 2 ifTrue: [(tuple at: 3) ifFalse: [block toggleShowing]]. block color: (Color r: 0.8 g: 0 b: 0). "obsolete" ^ block]. spec _ BlockSpecDict at: k ifAbsent: [nil]. spec ifNil: [ ^ scriptOwner blockFromSpec: #('obsolete!!' - yourself) color: Color red]. blockColor _ BlockColorDict at: k ifAbsent: [Color red]. block _ scriptOwner blockFromSpec: spec color: blockColor. (block isKindOf: CommandBlockMorph) ifTrue: [ argCount _ block argumentCount min: tuple size - 1. 1 to: argCount do: [:i | ((#(+ - / * =) includes: block selector) and: [ScratchTranslator isRTLMath]) "RTLMath operators are RTL" ifTrue: [arg _ tuple at: ((argCount + 1) - (i - 1))] ifFalse: [arg _ tuple at: i + 1]. (arg isKindOf: Array) ifTrue: [ "argument is a block" ((arg size = 1) and: [arg first isKindOf: Array]) ifTrue: [arg _ arg first]. argBlock _ self blockFromTuple: arg receiver: scriptOwner. block replaceArgMorph: (block argumentAt: i) by: argBlock] ifFalse: [ "argument is a value" (block argumentAt: i) defaultValue: arg]]. (block isKindOf: CBlockMorph) ifTrue: [ (tuple last isKindOf: Array) ifTrue: [ block firstBlockList: (self stackFromTupleList: tuple last receiver: scriptOwner)]]. (block isKindOf: IfElseBlockMorph) ifTrue: [ arg _ tuple at: tuple size - 1. (arg isKindOf: Array) ifTrue: [ block trueBlock: (self stackFromTupleList: arg receiver: scriptOwner)]. arg _ tuple at: tuple size. (arg isKindOf: Array) ifTrue: [ block falseBlock: (self stackFromTupleList: arg receiver: scriptOwner)]]. (block isKindOf: ReporterBlockMorph) ifTrue: [ ((spec at: 2) includes: $b) ifTrue: [block isBoolean: true]]]. ^ block ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jm 12/10/2003 18:06'! blockReceiver "Answer the object that is the receiver of my blocks. By default that is myself, but subclasses may re-direct block operations to another object." ^ self ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jm 11/30/2007 09:25'! blockSpecForSelector: aSymbol "Answer a block specification string (in English) for the give selector or nil if there is no spec that has the given selector." self class blockSpecs do: [:spec | ((spec isKindOf: Array) and: [(spec at: 3) = aSymbol]) ifTrue: [^ spec first]]. ^ nil ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jens 3/4/2009 10:52'! blockWithID: id | topBlockList blockList| topBlockList _ (blocksBin submorphs select: [:m | (m isKindOf: BlockMorph) ]) reversed. blockList _ OrderedCollection new. topBlockList do: [:top | (top allMorphs select: [:b| b isKindOf: BlockMorph ]) do: [:m | blockList add: m ]]. ^ blockList at: id ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jm 9/25/2003 12:45'! blocksBin ^ blocksBin ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jm 3/23/2005 17:13'! blocksFor: aCategory "Answer a collection of blocks for the given category. Mixed with the blocks are dash and tilde symbols (#- and #~) indicating where full and half spaces should be inserted when laying out the blocks." | blockColor blocksList category b | blockColor _ self class blockColorFor: aCategory. blocksList _ OrderedCollection new. category _ nil. self class blockSpecs do: [:spec | ((spec isKindOf: String) and: [spec ~= #- and: [spec ~= #~]]) ifTrue: [category _ spec] ifFalse: [ category = aCategory ifTrue: [ (spec = #-) | (spec = #~) ifTrue: [blocksList addLast: spec] ifFalse: [ (b _ self blockFromSpec: spec color: blockColor) ifNotNil: [ blocksList addLast: b]]]]]. ^ blocksList asArray ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jens 3/4/2009 01:08'! convertStacksToTuples "Convert my blocks bin from a morph containing block stack into a collection of (, ) pairs the represent the same stacks in compact, language-independent form." | stacks blocks comments | (blocksBin isKindOf: Array) ifTrue: [^ self]. "already converted" stacks _ (blocksBin submorphs select: [:m | m respondsTo: #tupleSequence]). blocks _ stacks select: [:m | m isKindOf: BlockMorph]. comments _ stacks select: [:m | m isKindOf: ScratchCommentMorph]. blocks _ blocks collect: [:blockM | Array with: blockM position - blocksBin position with: blockM tupleSequence]. comments _ comments collect: [:blockM | Array with: blockM position - blocksBin position with: blockM tupleSequence]. blocksBin _ blocks, comments. ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jens 3/4/2009 00:55'! convertTuplesToStacks "Convert my blocks bin from a collection of (, ) pairs into a morph containing a number of block stacks." | tuplesList stack | (blocksBin isKindOf: Array) ifFalse: [^ self]. "already converted" tuplesList _ blocksBin. blocksBin _ ScratchScriptsMorph new. tuplesList do: [:pair | stack _ self stackFromTupleList: pair second receiver: self. stack position: pair first. blocksBin addMorph: stack]. ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'ee 6/2/2009 14:17'! costumesPage: xOffset "Answer a morph containing thumbnails of my costumes." | bin label m y images n recBut impBut photoBut maxHeight xMargin yMargin separator | xMargin _ 15. yMargin _ 15. bin _ ScratchBlockPaletteMorph new color: ScratchFrameMorph scriptsPaneColor. (self isKindOf: ScratchStageMorph) ifTrue: [label _ 'New background:' localized] ifFalse: [label _ 'New costume:' localized]. m _ StringMorph contents: label font: (ScratchFrameMorph getFont: #CostumesPage). m color: Color white. bin addMorph: m. recBut _ ScratchFrameMorph buttonLabel: 'Paint' localized selector: #drawNewCostume. recBut target: self. bin addMorph: recBut. impBut _ ScratchFrameMorph buttonLabel: 'Import' localized selector: #importImage. impBut target: self. bin addMorph: impBut. photoBut _ ScratchFrameMorph buttonLabel: 'Camera' localized selector: #takePhoto. photoBut target: self. bin addMorph: photoBut. maxHeight _ (m height max: (impBut height max: (recBut height max: photoBut height))). ScratchTranslator isRTL ifTrue: [ photoBut position: xMargin@(yMargin + ((maxHeight - photoBut height) / 2)). impBut position: (photoBut right + 4)@(yMargin + ((maxHeight - impBut height) / 2)). recBut position: (impBut right + 4)@(yMargin + ((maxHeight - recBut height) / 2)). m position: (recBut right + 4)@(yMargin + ((maxHeight - m height) / 2))] ifFalse: [ m position: xMargin@(yMargin + ((maxHeight - m height) / 2)). recBut position: (m right + 4)@(yMargin + ((maxHeight - recBut height) / 2)). impBut position: (recBut right + 4)@(yMargin + ((maxHeight - impBut height) / 2)). photoBut position: (impBut right + 4)@(yMargin + ((maxHeight - impBut height) / 2))]. separator _ TiledImageMorph new tileForm: (ScratchFrameMorph skinAt: #costumeSeparatorTile). bin addMorph: (separator position: (xOffset + 17)@(yMargin + maxHeight + 5)). y _ yMargin + maxHeight + 9. n _ 0. images _ media select: [:item | item isImage]. images do: [:item | m _ MediaItemMorph new. m scratchObj: self media: item; position: xOffset@y. m setNumber: (n _ n + 1). item = costume ifTrue: [m highlight: true]. bin addMorph: m. y _ y + m height - 1.]. bin submorphs size > 0 ifTrue: [separator width: bin firstSubmorph width - 14] ifFalse: [separator width: 240]. ^ bin ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jm 12/1/2007 20:58'! createBlock: block atPosition: pos onPage: page "Creates a block on the given page. If the block is one that can become a watcher, then a toggle button is created as well." | x y changingX toggleButton yOffset frame | x _ pos x. y _ pos y. changingX _ x. block canBecomeWatcher ifTrue: [ toggleButton _ self createToggleButtonFor: block. yOffset _ (block fullBounds height - toggleButton fullBounds height) // 2. page addMorphBack: (toggleButton position: x@(y+yOffset)). changingX _ x + toggleButton fullBounds width + 4]. block fixBlockLayout; position: changingX@y. page addMorphBack: block. block canBecomeWatcher ifTrue: [ frame _ self ownerThatIsA: ScratchFrameMorph. page updateWatcherButtonsForFrame: frame]. ^ y + block height + 3 ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jm 3/21/2008 14:04'! createToggleButtonFor: block "Create the toggle button for each variable that could be viewed on the stage." | toggleButton | toggleButton _ ToggleButton onForm: (ScratchFrameMorph skinAt: #watcherButtonPressed) offForm: (ScratchFrameMorph skinAt: #watcherButton). toggleButton target: block; actionSelector: #toggleWatcher; borderWidth: 0; setProperty: #balloonText toValue: 'View on stage' localized. ^ toggleButton ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'ee 3/28/2009 16:05'! defaultArgsFor: blockSpec "Answer the default argument for the given block specification." | defaultArgs stage sel currentSize list | defaultArgs _ blockSpec copyFrom: 4 to: blockSpec size. "may be empty" stage _ self ownerThatIsA: ScratchStageMorph. sel _ (blockSpec at: 3) asSymbol. #gotoX:y: = sel ifTrue: [ defaultArgs _ Array with: self referencePosition x rounded with: self referencePosition y rounded]. #glideSecs:toX:y:elapsed:from: = sel ifTrue: [ defaultArgs _ Array with: 1 with: self referencePosition x rounded with: self referencePosition y rounded]. #motor:direction: = self ifTrue: [ defaultArgs _ Array with: 'reverse' localized with: 'this way' localized with: 'that way']. #setSizeTo: = sel ifTrue: [ currentSize _ (100.0 * (self scalePoint x max: self scalePoint y)) rounded. defaultArgs _ Array with: currentSize]. #getAttribute:of: = sel ifTrue: [ (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [ list _ stage submorphs select: [:m | m isKindOf: ScratchSpriteMorph]. list sort: [:s1 :s2 | s1 objName asLowercase < s2 objName asLowercase]. list size > 0 ifTrue: [defaultArgs _ Array with: 'x position' with: list first] ifFalse: [defaultArgs _ Array with: 'background #' with: stage]] ifNil:[defaultArgs _ Array with: 'x position' with: self]]. #concatenate:with: = sel ifTrue: [ defaultArgs _ Array with: 'hello ' localized with: 'world' localized]. #doAsk = sel ifTrue: [ defaultArgs _ Array with: 'What''s your name?' localized]. #letter:of: = sel ifTrue: [ defaultArgs _ Array with: 1 with: 'world' localized]. #stringLength: = sel ifTrue: [ defaultArgs _ Array with: 'world' localized]. #say:duration:elapsed:from: = sel ifTrue: [ defaultArgs _ Array with: 'Hello!!' localized with: 2]. #say: = sel ifTrue: [ defaultArgs _ Array with: 'Hello!!' localized]. #think:duration:elapsed:from: = sel ifTrue: [ defaultArgs _ Array with: 'Hmm...' localized with: 2]. #think: = sel ifTrue: [ defaultArgs _ Array with: 'Hmm...' localized]. (#(lookLike: showBackground:) includes: sel) ifTrue: [ defaultArgs _ Array with: self costumeNames last]. (#(playSound: doPlaySoundAndWait) includes: sel) ifTrue: [ list _ self soundNames. defaultArgs _ list size <= 2 ifTrue: [Array with: ''] ifFalse: [Array with: (list at: (list size - 2))]]. (#(broadcast: doBroadcastAndWait) includes: sel) ifTrue: [ stage ifNotNil: [defaultArgs _ Array with: stage defaultEventName]]. (#(append:toList: deleteLine:ofList: insert:at:ofList:) includes: sel) ifTrue: [ defaultArgs size >= 1 ifTrue: [ defaultArgs at: 1 put: (defaultArgs at: 1) localized]]. (#(append:toList: deleteLine:ofList: getLine:ofList: insert:at:ofList: lineCountOfList:) includes: sel) ifTrue: [ defaultArgs _ defaultArgs copyWith: self defaultListName]. #setLine:ofList:to: = sel ifTrue: [ defaultArgs size >= 3 ifTrue: [ defaultArgs at: 2 put: self defaultListName. defaultArgs at: 3 put: (defaultArgs at: 3) localized]]. #appendLettersOf:toList: = sel ifTrue: [ defaultArgs size >= 2 ifTrue: [ defaultArgs at: 1 put: (defaultArgs at: 1) localized. defaultArgs at: 2 put: self defaultListName]]. #list:contains: = sel ifTrue: [ defaultArgs size >= 2 ifTrue: [ defaultArgs at: 1 put: self defaultListName. defaultArgs at: 2 put: (defaultArgs at: 2) localized]]. ^ defaultArgs ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jens 3/6/2009 00:31'! hatBlockFromTuple: tuple receiver: scriptOwner "Answer a new block for the given variable reference tuple." | blockClass block eventName arg | blockClass _ Smalltalk at: tuple first. block _ blockClass new scriptOwner: scriptOwner. blockClass = EventHatMorph ifTrue: [ eventName _ tuple at: 2. eventName = 'Scratch-StartClicked' ifTrue: [block forStartEvent; scriptOwner: scriptOwner] ifFalse: [block eventName: eventName]]. blockClass = KeyEventHatMorph ifTrue: [ block choice: (tuple at: 2)]. blockClass = WhenHatBlockMorph ifTrue: [ (tuple at: 2) ~= false ifTrue: [ arg _ self blockFromTuple: (tuple at: 2) receiver: scriptOwner. block replaceArgMorph: block argMorph by: arg]]. ^ block ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jm 9/27/2007 15:11'! hatBlockType: blockType | stage evtName | 'E' = blockType ifTrue: [ evtName _ ''. (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [evtName _ stage defaultEventName]. ^ EventHatMorph new scriptOwner: self; eventName: evtName]. 'K' = blockType ifTrue: [^ KeyEventHatMorph new scriptOwner: self]. 'M' = blockType ifTrue: [^ MouseClickEventHatMorph new scriptOwner: self]. 'S' = blockType ifTrue: [^ EventHatMorph new forStartEvent scriptOwner: self]. 'W' = blockType ifTrue: [^ WhenHatBlockMorph new scriptOwner: self]. ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jm 8/9/2007 17:31'! scriptsAsTuples: tuples blocksBin _ tuples asArray. self convertTuplesToStacks. ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'ee 7/3/2008 13:05'! soundsPage: xOffset "Answer a morph containing thumbnails of my sounds." | bin m y sounds n recBut impBut maxHeight yMargin xMargin separator | xMargin _ 15. yMargin _ 15. bin _ ScratchBlockPaletteMorph new. m _ StringMorph contents: 'New sound:' localized font: (ScratchFrameMorph getFont: #SoundsPage). m color: Color white. bin addMorph: m. recBut _ ScratchFrameMorph buttonLabel: 'Record' localized selector: #recordSound. recBut target: self. bin addMorph: recBut. impBut _ ScratchFrameMorph buttonLabel: 'Import' localized selector: #importSound. impBut target: self. bin addMorph: impBut. maxHeight _ (m height max: (impBut height max: recBut height)). ScratchTranslator isRTL ifTrue: [impBut position: xMargin@(yMargin + ((maxHeight - impBut height) / 2)). recBut position: (impBut right + 4)@(yMargin + ((maxHeight - recBut height) / 2)). m position: (recBut right + 4)@(yMargin + ((maxHeight - m height) / 2))] ifFalse: [m position: xMargin@(yMargin + ((maxHeight - m height) / 2)). recBut position: (m right + 4)@(yMargin + ((maxHeight - recBut height) / 2)). impBut position: (recBut right + 4)@(yMargin + ((maxHeight - impBut height) / 2))]. separator _ TiledImageMorph new tileForm: (ScratchFrameMorph skinAt: #costumeSeparatorTile). bin addMorph: (separator position: (xOffset + 17)@(yMargin + maxHeight + 5)). n _ 0. y _ yMargin + maxHeight + 9. sounds _ media select: [:item | item isSound]. sounds do: [:item | m _ MediaItemMorph new. m scratchObj: self media: item; position: xOffset@y. m setNumber: (n _ n + 1). bin addMorph: m. y _ y + m height - 1]. bin submorphs size > 0 ifTrue: [separator width: bin firstSubmorph width - 14] ifFalse: [separator width: 240]. ^ bin ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jens 3/4/2009 00:53'! stackFromTupleList: tupleList receiver: scriptOwner "Answer a new block stack from the given sequence of tuples." "self stackFromTupleList: #() receiver: nil" | stackTop previousBlock block | stackTop _ previousBlock _ nil. tupleList do: [:tuple | block _ self blockFromTuple: tuple receiver: scriptOwner. previousBlock ifNil: [stackTop _ block] ifNotNil: [previousBlock nextBlock: block]. previousBlock _ block]. ^ stackTop ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jens 3/6/2009 00:29'! variableBlockFromTuple: tuple receiver: scriptOwner "Answer a new block for the given variable reference tuple." | varName rcvr bg selector block arg argBlock | varName _ tuple at: 2. rcvr _ scriptOwner. (scriptOwner varNames includes: varName) ifFalse: [ bg _ scriptOwner ownerThatIsA: ScratchStageMorph. bg ifNil: [scriptOwner addVariable: varName] ifNotNil: [ bg addVariable: varName. rcvr _ bg]]. tuple first = #readVariable ifTrue: [ ^ VariableBlockMorph new commandSpec: varName; receiver: rcvr]. tuple first = #changeVariable ifTrue: [ selector _ tuple at: 3. "update selector if necessary (backward compatibility):" (selector = #set:to:) ifTrue: [selector _ #setVar:to:]. block _ SetterBlockMorph new receiver: rcvr. selector = #setVar:to: ifTrue: [block initSetterForVar: varName] ifFalse: [block initChangerForVar: varName]. arg _ tuple at: 4. (arg isKindOf: Array) ifTrue: [ "argument is a block" ((arg size = 1) and: [arg first isKindOf: Array]) ifTrue: [arg _ arg first]. argBlock _ self blockFromTuple: arg receiver: scriptOwner. block replaceArgMorph: block expressionArg by: argBlock] ifFalse: [ "argument is a value" block expressionArg defaultValue: arg]. ^ block]. self error: 'unknown variable spec' ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'ee 1/29/2009 12:26'! variablesPage "Answer a morph containing my variables." | page addButton deleteButton hasVars stage x y maxX | page _ ScratchBlockPaletteMorph new color: (Color r: 0.8 g: 0.8 b: 1.0); borderWidth: 0. addButton _ ScratchFrameMorph buttonLabel: 'Make a variable' localized selector: #addGlobalVariable. (self isKindOf: ScratchSpriteMorph) ifTrue: [addButton actionSelector: #addVariable]. deleteButton _ ScratchFrameMorph buttonLabel: 'Delete a variable' localized selector: #deleteVariable. x _ 13. page addMorph: (addButton target: self; position: x@7). y _ addButton bottom + 3. hasVars _ self varNames size > 0. (stage _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [ stage varNames size > 0 ifTrue: [hasVars _ true]]. hasVars ifTrue: [ page addMorph: (deleteButton target: self; position: x@y). y _ deleteButton bottom + 10. y _ self addVariableReportersTo: page x: x y: y. y _ y + 12. y _ self addGenericVariableBlocksTo: page x: x y: y]. self addGenericListBlocksTo: page y: y. page updateWatcherButtonsForFrame: (self ownerThatIsA: ScratchFrameMorph). maxX _ page submorphs inject: 0 into: [:t :m | t max: m right]. page extent: (maxX + 10) @ y. ^ page ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'jm 5/19/2009 22:09'! viewBlocksAndScripts | sFrame editor viewer tabs | self isClone ifTrue: [^ self]. (sFrame _ self ownerThatIsA: ScratchFrameMorph) ifNil: [^ self]. editor _ sFrame scriptsPane. viewer _ sFrame viewerPane. tabs _ sFrame scriptsPane tabPane. sFrame view: self tab: tabs currentTab category: viewer currentCategory. self world ifNotNil: [ self zoomRectFrom: self bounds to: editor bounds]. ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'ee 4/16/2009 17:33'! viewerPageForCategory: aCategoryName "Answer a morph containing blocks for the given category for use in the given ScratchViewer." | bin x y | aCategoryName = 'variables' ifTrue: [^ self variablesPage]. aCategoryName = 'motion' ifTrue: [^ self viewerPageForMotion]. aCategoryName = 'sensing' ifTrue: [^ self viewerPageForSensing]. bin _ ScratchBlockPaletteMorph new. x _ 12. y _ 10. (self blocksFor: aCategoryName) do: [:blockOrSym | (blockOrSym = #-) | (blockOrSym = #~) ifTrue: [ (blockOrSym = #-) ifTrue: [y _ y + 15]. "insert a full space" (blockOrSym = #~) ifTrue: [y _ y + 5]] "insert a half space" ifFalse: [ y _ self createBlock: blockOrSym atPosition: x@y onPage: bin]]. ^ bin ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'ee 4/30/2009 14:26'! viewerPageForMotion "Answer a morph containing mtion blocks for the given category for use in the given ScratchViewer." | bin isStage addMotorBlocks s font x y m | bin _ ScratchBlockPaletteMorph new. (self isKindOf: ScratchStageMorph) ifTrue: [ isStage _ true. addMotorBlocks _ self showMotorBlocks] ifFalse: [ isStage _ false. s _ self ownerThatIsA: ScratchStageMorph. addMotorBlocks _ s notNil and: [s showMotorBlocks]]. (isStage & addMotorBlocks not) ifTrue: [ font _ (ScratchFrameMorph getFont: #ViewerPage). x _ 20. y _ 12. m _ StringMorph contents: 'Stage selected:' localized font: font. bin addMorph: (m color: Color white; position: x@y). m _ StringMorph contents: 'No motion blocks' localized font: font. bin addMorph: (m color: Color white; position: x@(y + 17)). ^ bin]. x _ 12. y _ 10. (self blocksFor: 'motion') do: [:blockOrSym | (blockOrSym = #-) | (blockOrSym = #~) ifTrue: [ (blockOrSym = #-) ifTrue: [y _ y + 15]. "insert a full space" (blockOrSym = #~) ifTrue: [y _ y + 5]] "insert a half space" ifFalse: [ y _ self createBlock: blockOrSym atPosition: x@y onPage: bin]]. addMotorBlocks ifFalse: [^ bin]. isStage ifFalse: [ y _ y + 7. bin addMorph: ((ImageMorph new form: (ScratchFrameMorph skinAt: #connector)) position: x@y). y _ y + 20]. (self blocksFor: 'motor') do: [:blockOrSym | (blockOrSym = #-) | (blockOrSym = #~) ifTrue: [ (blockOrSym = #-) ifTrue: [y _ y + 15]. "insert a full space" (blockOrSym = #~) ifTrue: [y _ y + 5]] "insert a half space" ifFalse: [ y _ self createBlock: blockOrSym atPosition: x@y onPage: bin]]. ^ bin ! ! !ScriptableScratchMorph methodsFor: 'blocks' stamp: 'ee 4/30/2009 14:26'! viewerPageForSensing "Answer a morph containing mtion blocks for the given category for use in the given ScratchViewer." | bin x y | bin _ ScratchBlockPaletteMorph new. x _ 12. y _ 10. (self blocksFor: 'sensing') do: [:blockOrSym | (blockOrSym = #-) | (blockOrSym = #~) ifTrue: [ (blockOrSym = #-) ifTrue:[y _ y + 15]. "insert a full space" (blockOrSym = #~) ifTrue: [ y _ y + 7. bin addMorph: ((ImageMorph new form: (ScratchFrameMorph skinAt: #connector)) position: x@y). y _ y + 20]] ifFalse: [ y _ self createBlock: blockOrSym atPosition: x@y onPage: bin]]. ^ bin ! ! !ScriptableScratchMorph methodsFor: 'dropping/grabbing' stamp: 'jm 12/30/2008 18:37'! justDroppedInto: newOwner event: evt "If I'm dropped into any submorph of a ScratchFrameMorph, reject the drop unless it is onto the work pane. For developers, accept drops into other morphs (e.g. the World). Otherwise, reject the drop." | sFrame | (sFrame _ newOwner ownerThatIsA: ScratchFrameMorph) ifNotNil: [ newOwner = sFrame viewerPane pageViewer contents ifTrue: [ self undoableDeleteSprite. sFrame libraryPane step. ^ self]. newOwner = sFrame workPane ifTrue: [^ self]. "allow drops onto the work pane" ^ self rejectDropEvent: evt]. Preferences noviceMode ifTrue: [^ self rejectDropEvent: evt]. ! ! !ScriptableScratchMorph methodsFor: 'dropping/grabbing' stamp: 'jm 8/3/2003 13:21'! rejectDropEvent: evt "Reject being dropped by the given event." evt hand rejectDropMorph: self event: evt. ! ! !ScriptableScratchMorph methodsFor: 'event handling' stamp: 'jm 11/28/2007 15:07'! click: evt "Start or stop all my clickable scripts." | h clickEvt | h _ World activeHand. h toolType ifNotNil: [^ self handleTool: h toolType hand: h]. clickEvt _ ScratchEvent new name: 'Scratch-MouseClickEvent' argument: 0. self eventReceived: clickEvt. ! ! !ScriptableScratchMorph methodsFor: 'event handling' stamp: 'jm 10/7/2005 15:56'! doubleClick: evt "Show my viewer and script editor." | hand | hand _ self world activeHand. hand toolType ifNotNil: [ ^ self handleTool: hand toolType hand: hand]. self viewBlocksAndScripts. ! ! !ScriptableScratchMorph methodsFor: 'event handling' stamp: 'tis 12/10/2006 23:50'! handleTool: toolName hand: hand | scaleFactor | toolName = 'CopyTool' ifTrue: [ Sensor shiftPressed ifFalse: [hand toolType: nil]. (self isKindOf: ScratchStageMorph) ifFalse: [ ^ self duplicate]]. toolName = 'CutTool' ifTrue: [ Sensor shiftPressed ifFalse: [hand toolType: nil]. (self isKindOf: ScratchStageMorph) ifFalse: [ ^ self undoableDeleteSprite]]. "exit now if I am not scalable" (self respondsTo: #multiplySizeBy:) ifFalse: [ self beep. hand toolType: nil. ^ self]. scaleFactor _ hand lastEvent shiftPressed ifTrue: [1.2] ifFalse: [1.03]. toolName = 'ZoomInTool' ifTrue: [^ self multiplySizeBy: scaleFactor]. toolName = 'ZoomOutTool' ifTrue: [^ self multiplySizeBy: 1.0 / scaleFactor]. ! ! !ScriptableScratchMorph methodsFor: 'event handling' stamp: 'jm 6/2/2009 11:44'! handlesMouseDown: evt ^ self isVisible ! ! !ScriptableScratchMorph methodsFor: 'event handling' stamp: 'jm 11/27/2007 16:04'! mouseDown: evt "Handle a mouse click. Left button either drags or performs click action. Right button brings up a menu." evt hand newKeyboardFocus: nil. evt hand toolType ifNotNil: [ self handleTool: evt hand toolType hand: evt hand. ^ self]. evt rightButtonPressed ifTrue: [Sensor waitNoButton. ^ self rightButtonMenu] ifFalse: [evt hand waitForClicksOrDrag: self event: evt]. ! ! !ScriptableScratchMorph methodsFor: 'event handling' stamp: 'jm 12/19/2008 17:19'! mouseHold: evt self rightButtonMenu. ! ! !ScriptableScratchMorph methodsFor: 'event handling' stamp: 'jm 8/3/2003 23:42'! startDrag: evt "This is a drag gesture; pick me up." | rootForGrab | rootForGrab _ owner rootForGrabOf: self. rootForGrab ifNil: [^ self]. rootForGrab position: evt hand position + (self topLeft - evt cursorPoint). evt hand grabMorph: rootForGrab. ! ! !ScriptableScratchMorph methodsFor: 'right button menu' stamp: 'jm 10/20/2007 18:03'! duplicateNoAttach "Duplicate this sprite, but do not attach to the hand." | newSprite frame | newSprite _ self fullCopy. newSprite position: (newSprite position + 20). frame _ self ownerThatIsA: ScratchFrameMorph. frame ifNotNil: [ frame workPane addMorphFront: newSprite. frame workPane sprites addLast: newSprite]. ^ newSprite ! ! !ScriptableScratchMorph methodsFor: 'right button menu' stamp: 'jm 8/2/2004 21:43'! editDrawing "Edit my current form with the paint editor." self editDrawingOldCostumeName: nil deleteOnCancel: false. ! ! !ScriptableScratchMorph methodsFor: 'right button menu' stamp: 'jm 3/3/2008 11:42'! editDrawingOldCostumeName: oldCostumeName deleteOnCancel: aBoolean "Edit my original form with the paint editor." | sFrame paintEditor | costume isMovie ifTrue: [^ self beep]. (sFrame _ self ownerThatIsA: ScratchFrameMorph) ifNotNil: [ sFrame paintingInProgress ifTrue: [^ self beep]. sFrame stopAll. sFrame paintingInProgress: true]. paintEditor _ PaintFrame new. paintEditor withStartingObject: self; scratchFrame: sFrame; oldCostumeName: oldCostumeName deleteOnCancel: aBoolean. oldCostumeName ifNotNil: [ "When an oldCostumeName is supplied, it means I'm making a new drawing. Clear the initial rotation center." paintEditor clearRotationCenter]. (paintEditor isKindOf: DialogBoxMorph) ifTrue: [paintEditor getUserResponse] ifFalse: [ World addMorphFront: paintEditor. World startSteppingSubmorphsOf: paintEditor]. ! ! !ScriptableScratchMorph methodsFor: 'right button menu' stamp: 'jm 2/26/2009 20:24'! exportObject | fName dir f objToExport | fName _ ScratchFileChooserDialog chooseNewFileDefault: objName title: 'Export Sprite' type: #sprite. fName = #cancelled ifTrue: [^ self]. fName size = 0 ifTrue: [^ self]. (fName endsWith: '.sprite') ifFalse: [fName _ fName, '.sprite']. fName _ FileDirectory localNameFor: fName. "ignore path, if any; save in default directory" dir _ ScratchFileChooserDialog getLastFolderForType: #sprite. (dir fileExists: fName) ifTrue: [ (DialogBoxMorph ask: 'Overwrite existing ', fName, '?') ifFalse: [^ self]. dir deleteFileNamed: fName]. f _ nil. [ f _ (dir newFileNamed: fName) binary. objToExport _ self copyForExport. objToExport objName: fName. ObjStream new storeObj: objToExport on: f showProgress: true. f close. dir setMacFileNamed: fName type: 'STsb' creator: 'MITS'. ] ifError: [ f ifNotNil: [f close]. self inform: 'Could not write file' withDetails: 'Export failed' localized]. ! ! !ScriptableScratchMorph methodsFor: 'right button menu' stamp: 'jm 7/4/2008 19:48'! makeVisible "Make sure that I am entirely on the screen and visible." self isHidden ifTrue: [self isHidden: false]. visibility < 30 ifTrue: [self visibility: 100]. self scale < 10 ifTrue: [self setSizeTo: 100]. (owner notNil and: [owner bounds containsPoint: self center]) ifFalse: [ self referencePosition: 0@0]. self comeToFront. World displayWorldSafely. self viewBlocksAndScripts. ! ! !ScriptableScratchMorph methodsFor: 'right button menu' stamp: 'jm 5/25/2004 14:18'! rightButtonMenu "This default implementation does nothing." ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'tis 8/20/2006 22:45'! addMediaItem: aScratchMedia "Add the given media item to my media." aScratchMedia mediaName: (self unusedMediaNameFromBaseName: aScratchMedia mediaName). media addLast: aScratchMedia. aScratchMedia isSound ifFalse: [self lookLike: aScratchMedia mediaName]. self updateMediaCategory.! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 6/8/2009 10:23'! costumeNames "Answer a list of my costume (costume = non-sound media object) names." | result nm | result _ OrderedCollection new. media do: [:el | el isSound ifFalse: [ nm _ el mediaName. (result includes: nm) ifFalse: [result add: nm]]]. result _ result asArray. CameraPlugin cameraIsAvailable ifTrue: [ result _ result copyWith: '*** camera ***']. ^ result ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 8/7/2008 13:16'! defaultImageMedia "Placeholder image media." ^ ImageMedia new form: (Form extent: 8@8 depth: 8) fillWhite; mediaName: ('costume' localized, '1') ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 6/29/2009 17:14'! deleteMedia: itemToDelete "Delete the given media item." | oldIndex costumesBefore costumesAfter newCostume | oldIndex _ media indexOf: itemToDelete. media remove: itemToDelete ifAbsent: []. itemToDelete isSound ifTrue: [itemToDelete sound pause]. itemToDelete == costume ifTrue: [ "select the costume just before or after the costume being deleted" costumesBefore _ (media copyFrom: 1 to: oldIndex - 1) select: [:item | item isSound not]. costumesAfter _ (media copyFrom: oldIndex to: media size) select: [:item | item isSound not]. costumesBefore size > 0 ifTrue: [newCostume _ costumesBefore last] ifFalse: [ costumesAfter size > 0 ifTrue: [newCostume _ costumesAfter first] ifFalse: [media addLast: (newCostume _ itemToDelete)]]. self lookLike: newCostume mediaName]. self updateMediaCategory. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 10/2/2006 16:25'! drawNewCostume | oldCostumeName newName newImage | oldCostumeName _ costume mediaName. newImage _ ImageMedia new form: (Form extent: 4@4 depth: 8). newName _ (self unusedMediaNameFromBaseName: self defaultImageMedia mediaName). newImage mediaName: newName. media addLast: newImage. self lookLike: newImage mediaName. self updateMediaCategory. self editDrawingOldCostumeName: oldCostumeName deleteOnCancel: false. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 6/22/2004 17:37'! duplicateMedia: itemToDuplicate "Duplicate the given media item." | dup | itemToDuplicate isSound ifTrue: [itemToDuplicate pausePlaying]. dup _ itemToDuplicate copy. dup mediaName: (self unusedMediaNameFromBaseName: itemToDuplicate mediaName). media addLast: dup. self updateMediaCategory. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 1/4/2006 12:56'! editedForm: aForm textBox: aMultilineTextMorph | newImage | (costume isKindOf: ImageMedia) ifFalse: [ newImage _ ImageMedia new form: aForm; mediaName: (self unusedMediaNameFromBaseName: costume mediaName). media addLast: newImage. self lookLike: newImage mediaName]. costume form: aForm. "replace the current costume" costume textBox: aMultilineTextMorph. self lookLike: costume mediaName. self viewBlocksAndScripts. self updateMediaCategory. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 5/6/2009 16:49'! importImage "Import a new image from a file and add it to my media (concatenating the name if it is greater than 16 characters long). If the file is an animated gif, import all frames of the gif as separate costumes." | title result el ext fList isFirst newName type | (self isKindOf: ScratchStageMorph) ifTrue: [type _ #background. title _ 'Import Background'] ifFalse: [type _ #costume. title _ 'Import Costume']. result _ ScratchFileChooserDialog chooseImageFileType: type title: title. result = #cancelled ifTrue: [^ self]. fList _ OrderedCollection new. ext _ FileDirectory extensionFor: result asLowercase. ext = 'gif' ifTrue: [[fList _ (GIFReadWriter new on: (FileStream readOnlyFileNamed: result)) nextImageSet] ifError: [^ self]] ifFalse: [[fList addLast: (Form fromFileNamed: result)] ifError: [^ self]]. isFirst _ true. fList do: [:f | el _ ImageMedia new form: (ScratchFrameMorph scaledFormForPaintEditor: f). newName _ self mediaNameFromFileName: result default: 'costume'. el mediaName: (self unusedMediaNameFromBaseName: (UTF8 withAll: newName)). media addLast: el. isFirst ifTrue: [isFirst _ false. self lookLike: el mediaName.]]. self updateMediaCategory. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 10/5/2006 08:32'! importMedia: fileName "Import a new image from a file and add it to my media." | extension elList baseName fList isFirst | extension _ FileDirectory extensionFor: fileName asLowercase. elList _ OrderedCollection new. (#(aif aiff wav mp3) includes: extension) ifTrue: [ baseName _ self mediaNameFromFileName: fileName default: 'sound'. [elList addLast: (SoundMedia new loadFile: fileName)] ifError: [^ self]]. (#(jpg jpeg gif bmp png) includes: extension) ifTrue: [ baseName _ self mediaNameFromFileName: fileName default: self defaultImageMedia mediaName. fList _ OrderedCollection new. extension = 'gif' ifTrue: [[fList _ (GIFReadWriter new on: (FileStream oldFileNamed: fileName)) nextImageSet] ifError: [^ self]] ifFalse: [[fList addLast: (Form fromFileNamed: fileName)] ifError: [^ self]]. fList do: [:f | elList addLast: (ImageMedia new form: (ScratchFrameMorph scaledFormForPaintEditor: f))]]. elList isEmpty ifTrue: [^ self]. "unknown file type; ignore" isFirst _ true. elList do: [:el | el mediaName: (self unusedMediaNameFromBaseName: baseName). media addLast: el. isFirst ifTrue: [ isFirst _ false. el isSound ifFalse: [self lookLike: el mediaName]]]. self updateMediaCategory. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 8/2/2008 22:55'! importSound "Import a new sound from a file and add it to my media." | result el newName | result _ ScratchFileChooserDialog chooseExistingFileType: #sound extensions: #(aif aiff au wav mp3) title: 'Import Sound'. result = #cancelled ifTrue: [^ self]. el _ [SoundMedia new loadFile: result] ifError: [:err :rcvr | DialogBoxMorph warn: err. nil]. el ifNil: [^ self]. newName _ self mediaNameFromFileName: result default: 'sound'. el mediaName: (UTF8 withAll: (self unusedMediaNameFromBaseName: newName)). media addLast: el. self updateMediaCategory. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 4/3/2005 10:38'! insertMedia: aScratchMedia before: targetOrNil "Shuffle my media list so that the given media item comes immediately before the target media item, or at the end of the list if targetScratchMedia is nil. Do nothing if the given item is the same as the target item." | mediaItem | aScratchMedia == targetOrNil ifTrue: [^ self]. mediaItem _ aScratchMedia. (media includes: aScratchMedia) ifFalse: [ "dropping an undeleted media item" mediaItem _ mediaItem copy. mediaItem mediaName: (self unusedMediaNameFromBaseName: mediaItem mediaName). media addLast: mediaItem]. media remove: mediaItem. targetOrNil ifNil: [media addLast: mediaItem] ifNotNil: [media add: mediaItem before: targetOrNil]. self updateMediaCategory. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 9/7/2006 17:23'! mediaNameFromFileName: fileName default: defaultName "Answer a name for the given media item. If the existing name is '$$squeak$$' then the media was copied via windows drag-and-drop; use the given default name instead." | result | result _ (FileDirectory baseNameFor: (FileDirectory localNameFor: fileName)). (result beginsWith: '$$squeak$$') ifTrue: [result _ defaultName]. result size > 16 ifTrue: [result _ result copyFrom: 1 to: 16]. ^ result ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 12/1/2005 20:04'! onlyCostume: aForm "Remove all my costumes, then make a new costume using the given Form." | el | media _ media select: [:m | m isSound]. el _ ImageMedia new form: aForm. el mediaName: 'costume'. media addLast: el. self lookLike: el mediaName. self updateMediaCategory. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 8/3/2007 08:00'! recordSound | frame | frame _ self ownerThatIsA: ScratchFrameMorph. frame ifNil: [^ self beep]. frame stopAll. World submorphs do: [:m | (m isKindOf: ScratchSoundRecorderDialogMorph) ifTrue: [m delete]]. ScratchSoundRecorderDialogMorph forClient: self. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 8/2/2004 20:38'! revertToCostume: oldCostumeName "Sent by the paint editor if editing of a newly-created costume is cancelled. Delete the new costume and revert to the old costume with the given name." | newlyCreatedCostume | newlyCreatedCostume _ costume. self lookLike: oldCostumeName. media remove: newlyCreatedCostume ifAbsent: []. self updateMediaCategory. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 6/22/2009 15:45'! savePhoto: aForm | n | n _ self unusedMediaNameFromBaseName: 'costume' localized, '1'. self addMediaItem: (ImageMedia new mediaName: n; form: aForm). ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'md 12/11/2004 16:37'! saveSound: aSampledSound name: baseName | sndItem | sndItem _ SoundMedia new. sndItem mediaName: (self unusedMediaNameFromBaseName: baseName). media addLast: sndItem. sndItem sound: aSampledSound. self updateMediaCategory. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 5/12/2004 19:52'! setMedia: mediaCollection "Set my media to the given collection when copying." media _ mediaCollection. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 12/18/2006 10:38'! soleCostume: imageMedia "Make the given image my only costume." media _ media select: [:item | item isImage not]. self addMediaItem: imageMedia. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 4/25/2008 15:58'! soundNames "Answer a list of my sound names. Suppress duplicates (ignoring case differences)." | result nm | result _ OrderedCollection new. media do: [:el | el isSound ifTrue: [ nm _ el mediaName. (result anySatisfy: [:s | s caseInsensitiveEqual: nm]) ifFalse: [result add: nm]]]. result addLast: '-'. result addLast: 'record' localized, ScratchTranslator ellipsesSuffix. ^ result asArray ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 4/30/2009 22:53'! takePhoto "Take a photo." ScratchCameraDialog new client: self; openInWorld; openCamera. ! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'tis 8/20/2006 22:47'! unusedMediaNameFromBaseName: baseName "Answer an unused name for a new media item with the given base name. Strip off the file extension, if any." ^ self unusedMediaNameFromBaseName: baseName forMedia: nil! ! !ScriptableScratchMorph methodsFor: 'media' stamp: 'jm 6/22/2009 16:06'! unusedMediaNameFromBaseName: baseName forMedia: aMedia "Answer an unused name for a new media item with the given base name. Strip off the file extension, if any. med is the sound or costume media which is currently being renamed." | nm i existingNames greatestNum | nm _ baseName. i _ nm indexOf: $.. i > 1 ifTrue: [nm _ nm copyFrom: 1 to: i - 1]. nm size = 0 ifTrue: [ aMedia ifNil: [nm _ 'new' localized asUTF8, '1'] ifNotNil: [ aMedia isImage ifTrue: [nm _ self defaultImageMedia mediaName] ifFalse: [nm _ 'sound' localized asUTF8, '1']]]. existingNames _ Set new. media do: [:m | m = aMedia ifFalse: [existingNames add: (UTF8 withAll: m mediaName)]]. (nm size > 0 and: [nm last isDigit]) ifFalse: [ (existingNames includes: (UTF8 withAll: nm)) ifFalse: [^ nm]]. "remove trailing digits, if any" i _ nm size. [i > 1 and: [(nm at: i) isDigit]] whileTrue: [i _ i - 1]. [i > 1 and: [(nm at: i) = Character space]] whileTrue: [i _ i - 1]. nm _ nm copyFrom: 1 to: i. greatestNum _ 0. existingNames do: [:n | i _ n size. [i > 1 and: [(n at: i) isDigit]] whileTrue: [i _ i - 1]. (n copyFrom: 1 to: i) = nm ifTrue: [ greatestNum _ ((n copyFrom: i+1 to: n size) asNumber) max: greatestNum]]. ^ nm, (greatestNum+1) printString ! ! !ScriptableScratchMorph methodsFor: 'stepping' stamp: 'jm 1/8/2006 18:06'! step costume mediaStep ifTrue: [self costumeChanged]. ! ! !ScriptableScratchMorph methodsFor: 'stepping' stamp: 'jm 5/14/2004 12:22'! stepTime ^ 0 ! ! !ScriptableScratchMorph methodsFor: 'object i/o' stamp: 'jm 6/1/2004 20:19'! initFieldsFrom: anObjStream version: classVersion super initFieldsFrom: anObjStream version: classVersion. self initFieldsNamed: #( objName vars blocksBin isClone media costume ) from: anObjStream. ! ! !ScriptableScratchMorph methodsFor: 'object i/o' stamp: 'jm 3/23/2005 20:17'! storeFieldsOn: anObjStream | oldBlockBinOwner | super storeFieldsOn: anObjStream. (blocksBin isKindOf: Morph) ifTrue: [ oldBlockBinOwner _ blocksBin owner. blocksBin delete]. self storeFieldsNamed: #( objName vars blocksBin isClone media costume ) on: anObjStream. oldBlockBinOwner ifNotNil: [oldBlockBinOwner addMorph: blocksBin]. ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 12/15/2006 11:48'! clearMediaAndCostume "Private!! Used when exporting an empty stage." media _ OrderedCollection new. costume _ nil. ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 1/5/2006 12:40'! copyForExport "Answer a copy of me for exporting." | objToExport | objToExport _ self fullCopy. objToExport objName: objName. objToExport blocksBin allMorphsDo: [:m | (m isKindOf: BlockMorph) ifTrue: [m stop]. (m isKindOf: SpriteArgMorph) ifTrue: [m clearMorphReference]]. objToExport convertStacksToTuples. ^ objToExport ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 6/4/2008 19:45'! copyListsFor: aSprite "Answer a deep copy of my list variables dictionary." | result oldList copiedList | result _ lists species new. lists associationsDo: [:assoc | oldList _ assoc value. copiedList _ ScratchListMorph new listName: oldList listName target: aSprite; newContents: oldList listContents. result at: assoc key put: copiedList]. ^ result ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 6/4/2008 19:45'! copyRecordingIn: dict "Copy my fields and scripts." | newCopy newBlocksBin | (self respondsTo: #sayNothing) ifTrue: [self sayNothing]. "remove talk bubble before copying" newCopy _ super copyRecordingIn: dict. newCopy renewFilterPack. newBlocksBin _ blocksBin fullCopy. newBlocksBin allMorphsDo: [:m | (m isKindOf: HatBlockMorph) ifTrue: [m scriptOwner: newCopy]. (m isKindOf: CommandBlockMorph) ifTrue: [m mapReceiver: self to: newCopy]]. newCopy vars: vars copy lists: (self copyListsFor: newCopy) blocksBin: newBlocksBin. newCopy objName: self nextInstanceName. newCopy setMedia: (media collect: [:el | el copy]). newCopy lookLike: costume mediaName. ^ newCopy ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 11/22/2004 13:43'! costumeChanged costumeChangeMSecs _ Time millisecondClockValue. filterPack ifNotNil: [filterPack clearFilterCaches]. self layoutChanged. self keepOnScreen. ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'ee 4/23/2008 22:01'! deleteSprite "Delete the sprite and all watchers that are associated with it." | frame sprites i | frame _ self ownerThatIsA: ScratchFrameMorph. frame ifNil: [^ self delete]. frame deleteWatchersForSprite: self. frame workPane updateSpritesList. sprites _ frame workPane sprites. i _ sprites indexOf: self. "remember my index" sprites remove: self ifAbsent: []. self delete. frame libraryPane fixLayout. "delete my thumbnail, and re-focus on the thumbnail right after me or the stage if there are no sprites" i > sprites size ifTrue: [i _ i - 1]. i < 1 ifTrue: [frame view: frame workPane tab: 'Scripts' category: 'motion'] ifFalse: [frame view: (sprites at: i) tab: 'Scripts' category: 'motion']. ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 3/14/2009 17:22'! interpretStringAsNumberIfPossible: anObject "If the argument is a string that can be interpreted as a number, such as '123', then return it's value as a number. Otherwise, return the original object." | s digits hasDigit | (anObject isKindOf: String) ifFalse: [^ anObject]. anObject size = 0 ifTrue: [^ anObject]. (anObject first isKindOf: Character) ifTrue: [s _ ReadStream on: anObject asByteArray] ifFalse: [s _ ReadStream on: anObject]. "see if the string can be parsed as a Scratch number:" digits _ '0123456789' asByteArray. hasDigit _ false. (s atEnd not and: [s peek = $- asciiValue]) ifTrue: [s next]. [s atEnd not and: [digits includes: s peek]] whileTrue: [hasDigit _ true. s next]. (s atEnd not and: [s peek = $. asciiValue]) ifTrue: [s next]. [s atEnd not and: [digits includes: s peek]] whileTrue: [hasDigit _ true. s next]. (s atEnd and: [hasDigit]) ifTrue: [^ anObject asNumberNoError] ifFalse: [^ anObject]. ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 6/21/2009 11:53'! nextInstanceName "Answer a name for a new instance. For sprites, an attempt is made to create a unique name of the form 'spriteN'." | stage lastN digits | self = ScratchStageMorph ifTrue: [^ 'Stage' localized]. (stage _ self ownerThatIsA: ScratchStageMorph) ifNil: [^ 'Sprite' localized asUTF8, 1 printString]. lastN _ 0. stage sprites ifNotNil: [ stage sprites do: [:m | (m isKindOf: ScratchSpriteMorph) ifTrue: [ digits _ m objName trailingDigits. ((digits size > 0) and: [m objName beginsWith: ('Sprite' localized)]) ifTrue: [lastN _ lastN max: digits asNumber]]]]. ^ 'Sprite' localized asUTF8, (lastN + 1) printString ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 5/12/2006 14:02'! printSummaryOn: aStream | costumes snds stacks hats otherStacks | aStream nextPutAll: 'Sprite: ', self objName; crlf. costumes _ media select: [:item | item isImage]. aStream nextPutAll: ' Costumes (', costumes size printString, '):'; crlf. costumes do: [:item | aStream nextPutAll: ' ', item mediaName, ' (', item infoString, ')'; crlf]. snds _ media select: [:item | item isSound]. aStream nextPutAll: ' Sounds (', snds size printString, '):'; crlf. snds do: [:item | aStream nextPutAll: ' ', item mediaName, ' (', item infoString, ')'; crlf]. stacks _ blocksBin submorphs select: [:m | m isKindOf: BlockMorph]. stacks size = 0 ifTrue: [ aStream nextPutAll: ' No stacks.'; crlf; crlf. ^ self]. aStream nextPutAll: ' Stacks (', stacks size printString, '):'; crlf. hats _ stacks select: [:m | m isKindOf: HatBlockMorph]. otherStacks _ stacks select: [:m | (m isKindOf: HatBlockMorph) not]. hats, otherStacks do: [:item | item printCodeOn: aStream indent: 1. (item isKindOf: ReporterBlockMorph) ifTrue: [aStream crlf]. aStream crlf]. ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 11/13/2003 20:44'! projectDirectory "Answer the directory containing this Scratch project or the default directory." | frame | (frame _ self ownerThatIsA: ScratchFrameMorph) ifNil: [^ FileDirectory default]. ^ frame projectDirectory ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 9/15/2008 10:36'! serialized "Answer a ByteArray containing this Scratch object in serialized form." | s objToExport | s _ WriteStream on: (ByteArray new: 100000). objToExport _ self copyForExport. ObjStream new storeObj: objToExport on: s showProgress: false. ^ s contents ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 5/11/2009 11:08'! spriteNameInUse: aString | s | (s _ self ownerThatIsA: ScratchStageMorph) ifNotNil: [ s submorphs do: [:m | m == self ifFalse: [ ((m isKindOf: ScriptableScratchMorph) and: [m objName caseInsensitiveEqual: aString]) ifTrue: [ ^ true]]]]. ^ false ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 5/28/2008 16:05'! stopPlaying "Stop playing all movies and sounds." | firstCostume | costume stopPlaying. self filterReset. self setVolumeTo: 100. "reset volume" "exit camera mode" (costume isKindOf: CameraMedia) ifTrue: [ firstCostume _ media detect: [:el | el isImage] ifNone: [^ self]. self lookLike: firstCostume mediaName]. ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'tis 11/8/2006 14:20'! undoableDeleteSprite "Delete the sprite and store it in the clipboard in case of an undo." ScratchFrameMorph putInClipboard: self. self deleteSprite. ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 1/28/2009 10:42'! updateMediaCategory "Update the media category in the viewer, if it is showing." | sFrame | sFrame _ self ownerThatIsA: ScratchFrameMorph. sFrame ifNotNil: [ sFrame projectModified. sFrame updateMediaCategoryFor: self. sFrame viewerPane refresh]. ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 11/17/2006 17:39'! vars: varsDict blocksBin: aBlocksBin "Private!! Set my variables and blocks bin. Used by copyRecordingIn:." vars _ varsDict. blocksBin _ aBlocksBin. ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 6/4/2008 19:34'! vars: varsDict lists: listsDict blocksBin: aBlocksBin "Private!! Set my variables and blocks bin. Used by copyRecordingIn:." vars _ varsDict. lists _ listsDict. blocksBin _ aBlocksBin. ! ! !ScriptableScratchMorph methodsFor: 'private' stamp: 'jm 1/23/2004 10:30'! zoomRectFrom: startRect to: finalRect | steps r p1 p2 | steps _ 8. r _ startRect. Display border: r width: 2 rule: Form reverse fillColor: Color gray. Display forceDisplayUpdate. 0 to: steps do: [:i | Delay waitMSecs: 35. Display border: r width: 2 rule: Form reverse fillColor: Color gray. Display forceDisplayUpdate. p1 _ startRect origin + ((i * (finalRect origin - startRect origin)) // steps). p2 _ startRect corner + ((i * (finalRect corner - startRect corner)) // steps). r _ p1 corner: p2. Display border: r width: 2 rule: Form reverse fillColor: Color gray. Display forceDisplayUpdate]. Delay waitMSecs: 35. Display border: r width: 2 rule: Form reverse fillColor: Color gray. Display forceDisplayUpdate. ! ! !ScriptableScratchMorph class methodsFor: 'class initialization' stamp: 'jm 5/14/2008 18:48'! initialize "self initialize" Experimental _ false. ScratchOrigin _ 0@0. RandomGen _ Random new. TimerStartMSecs _ Time millisecondClockValue. ListBlockColor _ Color h: 18 s: 0.92 v: 0.85. ! ! !ScriptableScratchMorph class methodsFor: 'block specs' stamp: 'jm 8/23/2006 12:58'! blockColorDict ^ BlockColorDict ! ! !ScriptableScratchMorph class methodsFor: 'block specs' stamp: 'jm 4/21/2009 20:53'! blockColorFor: aCategory "Answer the block color for the given category." 'control' = aCategory ifTrue: [^ (Color h: 41 s: 0.85 v: 0.9)]. 'motion' = aCategory ifTrue: [^ (Color h: 225 s: 0.65 v: 0.83)]. 'motor' = aCategory ifTrue: [^ (Color h: 220 s: 0.85 v: 0.725)]. 'looks' = aCategory ifTrue: [^ (Color h: 264 s: 0.62 v: 0.89)]. 'pen' = aCategory ifTrue: [^ (Color h: 165 s: 1 v: 0.63)]. 'operators' = aCategory ifTrue: [^ (Color h: 93 s: 0.9 v: 0.76)]. 'sound' = aCategory ifTrue: [^ (Color h: 296 s: 0.66 v: 0.85)]. 'sensing' = aCategory ifTrue: [^ (Color h: 200 s: 0.98 v: 0.86)]. 'variables' = aCategory ifTrue: [^ (Color h: 25 s: 0.88 v: 0.95)]. 'list' = aCategory ifTrue: [^ ListBlockColor]. ^ (Color h: 0 s: 0.81 v: 0.83) "a shade of red" ! ! !ScriptableScratchMorph class methodsFor: 'block specs' stamp: 'jm 5/26/2005 17:02'! blockSpecDict "Answer my block specification dictionary." BlockSpecDict ifNil: [self buildBlockSpecDictionary]. ^ BlockSpecDict ! ! !ScriptableScratchMorph class methodsFor: 'block specs' stamp: 'jm 5/17/2009 11:32'! blockSpecs "Answer a collection of block specifications for the blocks that are common to all objects. Block specificatons (Arrays) are interspersed with category names (Strings). A block specification is an Array of the form: ( [optional initial argument values]). Explanation of flags: - no flags b boolean reporter c c-shaped block containing a sequence of commands (always special form) r reporter s special form command with its own evaluation rule t timed command, like wait or glide E message event hat K key event hat M mouse-click event hat S start event hat W when hat (obsolete)" | blocks | blocks _ #( 'control' ('when %m clicked' S -) ('when %k key pressed' K -) ('when %m clicked' M -) - ('wait %n secs' t wait:elapsed:from: 1) - ('forever' c doForever) ('repeat %n' c doRepeat 10) - ('broadcast %e' - broadcast:) ('broadcast %e and wait' s doBroadcastAndWait) ('when I receive %e' E -) - ('forever if %b' c doForeverIf) ('if %b' c doIf) ('if %b' c doIfElse) ('wait until %b' s doWaitUntil) ('repeat until %b' c doUntil) - ('stop script' s doReturn) ('stop all' - stopAll) 'operators' ('%n + %n' r + - -) ('%n - %n' r - - -) ('%n * %n' r * - -) ('%n / %n' r / - -) - ('pick random %n to %n' r randomFrom:to: 1 10) - ('%s < %s' b < '' '') ('%s = %s' b = '' '') ('%s > %s' b > '' '') - ('%b and %b' b &) ('%b or %b' b |) ('not %b' b not) - ('join %s %s' r concatenate:with: 'hello ' 'world') ('letter %n of %s' r letter:of: 1 'world') ('length of %s' r stringLength: 'world') - ('%n mod %n' r \\ - -) ('round %n' r rounded -) - ('%f of %n' r computeFunction:of: 'sqrt' 10) 'sound' ('play sound %S' - playSound:) ('play sound %S until done' s doPlaySoundAndWait) ('stop all sounds' - stopAllSounds) - ('play drum %D for %n beats' t drum:duration:elapsed:from: 48 0.2) ('rest for %n beats' t rest:elapsed:from: 0.2) - ('play note %N for %n beats' t noteOn:duration:elapsed:from: 60 0.5) ('set instrument to %I' - midiInstrument: 1) - ('change volume by %n' - changeVolumeBy: -10) ('set volume to %n%' - setVolumeTo: 100) ('volume' r volume) - ('change tempo by %n' - changeTempoBy: 20) ('set tempo to %n bpm' - setTempoTo: 60) ('tempo' r tempo) 'motor' ('motor on for %n secs' t motorOnFor:elapsed:from: 1) ('motor on' - allMotorsOn) ('motor off' - allMotorsOff) ('motor power %n' - startMotorPower: 100) ('motor direction %W' - setMotorDirection: 'this way') 'variables' ('show variable %v' - showVariable:) ('hide variable %v' - hideVariable:) 'list' ('add %s to %L' - append:toList: 'thing') - ('delete %y of %L' - deleteLine:ofList: 1) ('insert %s at %i of %L' - insert:at:ofList: 'thing' 1) ('replace item %i of %L with %s' - setLine:ofList:to: 1 'list' 'thing') - ('item %i of %L' r getLine:ofList: 1) ('length of %L' r lineCountOfList:) ('%L contains %s' b list:contains: 'list' 'thing') ). ^ blocks, self obsoleteBlockSpecs ! ! !ScriptableScratchMorph class methodsFor: 'block specs' stamp: 'jm 4/1/2005 07:25'! buildBlockSpecDictionary "self buildBlockSpecDictionary" | blockColor sel | BlockSpecDict _ IdentityDictionary new: 250. BlockColorDict _ IdentityDictionary new: 250. self withAllSubclassesDo: [:cl | blockColor _ Color blue. cl blockSpecs do: [:spec | ((spec isKindOf: String) and: [spec size > 1]) ifTrue: [ "set color for this category" blockColor _ self blockColorFor: spec]. (spec isMemberOf: Array) ifTrue: [ sel _ spec at: 3. BlockSpecDict at: sel put: spec. BlockColorDict at: sel put: blockColor]]]. ! ! !ScriptableScratchMorph class methodsFor: 'block specs' stamp: 'jm 5/14/2008 18:49'! listBlockColor ^ ListBlockColor ! ! !ScriptableScratchMorph class methodsFor: 'block specs' stamp: 'ee 3/28/2009 15:32'! obsoleteBlockSpecs "Answer a list of specifications for the graphic filter blocks." ^ #( 'obsolete number blocks' ('abs %n' r abs -) ('sqrt %n' r sqrt -) 'obsolete sound blocks' ('rewind sound %S' - rewindSound:) 'obsolete sprite motion blocks' ('point away from edge' - turnAwayFromEdge) ('glide x:%n y:%n in %n secs' t gotoX:y:duration:elapsed:from: 50 50 1) 'obsolete sprite looks blocks' ('change costume by %n' - changeCostumeIndexBy: 1) ('change background by %n' - changeBackgroundIndexBy: 1) - ('change stretch by %n' - changeStretchBy:) ('set stretch to %n%' - setStretchTo: 100) - ('say nothing' - sayNothing) - ('change visibility by %n' - changeVisibilityBy: -10) ('set visibility to %n%' - setVisibilityTo: 100) 'obsolete image effects' ('change color-effect by %n' - changeHueShiftBy: 25) ('set color-effect to %n' - setHueShiftTo: 0) - ('change fisheye by %n' - changeFisheyeBy: 10) ('set fisheye to %n' - setFisheyeTo: 0) ~ ('change whirl by %n' - changeWhirlBy: 30) ('set whirl to %n' - setWhirlTo: 0) - ('change pixelate by %n' - changePixelateCountBy: 1) ('set pixelate to %n' - setPixelateCountTo: 1) ~ ('change mosaic by %n' - changeMosaicCountBy: 1) ('set mosaic to %n' - setMosaicCountTo: 1) - ('change brightness-shift by %n' - changeBrightnessShiftBy: 10) ('set brightness-shift to %n' - setBrightnessShiftTo: 0) ~ ('change saturation-shift by %n' - changeSaturationShiftBy: 10) ('set saturation-shift to %n' - setSaturationShiftTo: 0) - ('change pointillize drop by %n' - changePointillizeSizeBy: 5) ('set pointillize drop to %n' - setPointillizeSizeTo: 0) ~ ('change water ripple by %n' - changeWaterRippleBy: 5) ('set water ripple to %n' - setWaterRippleTo: 0) - ('change blur by %n' - changeBlurBy: 1) ('set blur to %n' - setBlurTo: 0) ) ! ! !ScriptableScratchMorph class methodsFor: 'forms/sounds/icons' stamp: 'jm 11/4/2005 12:36'! defaultBackgroundForm (DefaultBackgroundForm isNil or: [DefaultBackgroundForm extent ~= ScratchFrameMorph workpaneExtent]) ifTrue: [ DefaultBackgroundForm _ (Form extent: ScratchFrameMorph workpaneExtent depth: 1). DefaultBackgroundForm fillWhite]. ^ DefaultBackgroundForm ! ! !ScriptableScratchMorph class methodsFor: 'forms/sounds/icons' stamp: 'jm 5/5/2007 12:57'! meowSound "MeowSound _ SampledSound fromFileNamed: ((FileDirectory default directoryNamed: 'ScratchSkin') fullNameFor: 'kitten.wav')" ^ MeowSound ! ! !ScriptableScratchMorph class methodsFor: 'forms/sounds/icons' stamp: 'jm 5/8/2007 11:03'! oldMeowPrefixReversed ^ OldMeowPrefixReversed ! ! !ScriptableScratchMorph class methodsFor: 'forms/sounds/icons' stamp: 'jm 12/12/2004 11:54'! popSound ^ PopSound ! ! !ScriptableScratchMorph class methodsFor: 'translation support' stamp: 'ee 10/24/2007 09:31'! blockSpecsArrayForTranslation "Answer an array that lists all the blocks and categories to be used as a guide to block translation." "self blockSpecDictForTranslation" | blocks all obsolete currentSpecs | currentSpecs _ Set new. ScratchSpriteMorph new blockCategories sort collect: [:cat | blocks _ (ScratchSpriteMorph new blocksFor: cat) select: [:b | b isKindOf: CommandBlockMorph]. all _ blocks collect: [:b | b commandSpec]. obsolete _ ScriptableScratchMorph obsoleteBlockSpecs collect: [:o | o first]. all _ all select: [:p | (obsolete indexOf: p) = 0]. (cat beginsWith: 'obsolete') ifFalse:[currentSpecs add: cat]. currentSpecs addAll: all]. currentSpecs _ currentSpecs asArray sort. ^ currentSpecs ! ! !ScriptableScratchMorph class methodsFor: 'translation support' stamp: 'jm 11/29/2007 13:20'! blockSpecsForTranslation "Answer a collection blocks for translation. Obsolete blocks are removed." | allSpecs | allSpecs _ Set new. (ScriptableScratchMorph blockSpecs, ScratchSpriteMorph blockSpecs, ScratchStageMorph blockSpecs) do: [:el | (el isKindOf: Array) ifTrue: [allSpecs add: el first] ifFalse: [((el beginsWith: 'obsolete') or:[#('-' '~') includes: el]) ifFalse:[allSpecs add: el]]]. ScriptableScratchMorph obsoleteBlockSpecs do: [:el | (el isKindOf: Array) ifTrue: [allSpecs remove: el first ifAbsent: []]]. allSpecs add: 'else'; add: 'variables'; add: 'set %v to %n'; add: 'change %v by %n'. ^ allSpecs asArray sort ! ! !ScriptableScratchMorph class methodsFor: 'translation support' stamp: 'ee 10/23/2007 13:49'! blockSpecsStringForTranslation "Answer a String that lists all the blocks and categories to be used as a guide to block translation." | out blocks pairs key | out _ WriteStream on: (String new: 10000). ScratchSpriteMorph new blockCategories sort collect: [:cat | out nextPutAll: '- ', cat; cr. blocks _ (ScratchSpriteMorph new blocksFor: cat) select: [:b | b isKindOf: CommandBlockMorph]. pairs _ blocks collect: [:b | key _ b selector collect: [:ch | ch = $: ifTrue: [$_] ifFalse: [ch]]. Array with: key with: b commandSpec]. pairs sort: [:p1 :p2 | p1 first < p2 first]. pairs do: [:p | out nextPutAll: p first; tab; nextPutAll: p last; cr]]. ^ out contents ! ! !ScriptableScratchMorph class methodsFor: 'other' stamp: 'jm 10/21/2005 11:20'! doubleSize: aBoolean DoubleSize _ aBoolean. ! ! !ScriptableScratchMorph class methodsFor: 'other' stamp: 'jm 8/13/2003 17:07'! fromUser "Go into a mode asking the user to select an instance of me, displaying a crossHair cursor while in this mode. The mode ends with the next mouse click. If the click is over an instance of me, return it. Otherwise, return nil. Keep running the world while waiting for the mouse click." | result p m mList | World ifNil: [^ self]. "not in morphic" result _ nil. Cursor crossHair showWhile: [ [Sensor anyButtonPressed] whileFalse: [World doOneCycleNoInput]. p _ Sensor cursorPoint. m _ (World rootMorphsAt: p) first. (m isKindOf: ScratchFrameMorph) ifTrue: [ mList _ m workPane rootMorphsAt: p. mList size > 0 ifTrue: [ m _ (m workPane rootMorphsAt: p) first]]. (m isKindOf: self) ifTrue: [result _ m]. Sensor waitNoButton]. ^ result ! ! !ScriptableScratchMorph class methodsFor: 'other' stamp: 'jm 3/17/2009 15:28'! isSpriteSpecificTarget: anObject selector: selector "I determine whether the given selector is specific to a particular Sprite. ." | i | (anObject isKindOf: ScratchSpriteMorph) ifFalse: [^ false]. i _ selector asString findAnySubStr: #('answer' 'key' 'Loud' 'mouse' 'sensor' 'soundLevel' 'tempo' 'timer') startingAt: 1. ^ i > selector asString size ! ! !ScriptableScratchMorph class methodsFor: 'other' stamp: 'jm 10/23/2007 12:48'! randomInit RandomGen initialize. ! ! !ScriptableScratchMorph class methodsFor: 'other' stamp: 'tis 7/31/2006 16:04'! resetTimer TimerStartMSecs _ Time millisecondClockValue. ! ! !ScriptableScratchMorph class methodsFor: 'other' stamp: 'jm 10/21/2005 11:21'! scratchOrigin: aPoint "Set the origin for the Scratch coordinate system." ScratchOrigin _ aPoint. ! ! !ScriptableScratchMorph class methodsFor: 'other' stamp: 'jm 5/10/2006 11:22'! soundRecorder "Return the shared Scratch SoundRecorder. Create and start it, if necessary." Recorder ifNil: [ Recorder _ SoundRecorder new. Recorder startRecording]. Recorder isActive ifFalse: [Recorder startRecording]. ^ Recorder ! ! !ScriptableScratchMorph class methodsFor: 'other' stamp: 'jm 5/12/2006 10:58'! stopSoundRecorder "Stop the shared sound recorder." "self stopSoundRecorder" Recorder ifNotNil: [Recorder stopRecording; clearRecordedSound]. ! ! Inspired by an oiginal design of Hans-Martin Mosner, this ScrollBar is intended to exercise the handling of input events in Morphic. With sufficient flexibility in this area, all particular behavior can be concentrated in this single class with no need to specialize any other morphs to achieve button, slider and menu-button behavior. Once we have this working, put in logic for horizontal operation as well.! !ScrollBar methodsFor: 'initialize' stamp: 'jm 10/13/2002 10:47'! initialize super initialize. scrollDelta _ 0.02. pageDelta _ 0.2. ! ! !ScrollBar methodsFor: 'initialize' stamp: 'jm 6/15/2003 10:28'! initializeDownButton downButton _ BorderedMorph newBounds: (self innerBounds bottomRight - self buttonExtent extent: self buttonExtent) color: Color lightGray. downButton addMorphCentered: (ImageMorph new form: (UpArrow rotateBy: (bounds isWide ifTrue: [#right] ifFalse: [#pi]) centerAt: 0@0)). downButton setBorderWidth: 1 borderColor: #raised. self addMorph: downButton. ! ! !ScrollBar methodsFor: 'initialize' stamp: 'jm 6/15/2003 10:41'! initializeMenuButton "Preferences disable: #scrollBarsWithoutMenuButton" "Preferences enable: #scrollBarsWithoutMenuButton" hasMenuButton ifNil: [ hasMenuButton _ (Preferences valueOfFlag: #scrollBarsWithoutMenuButton) not]. hasMenuButton ifFalse: [^ self]. menuButton _ BorderedMorph newBounds: (self innerBounds topLeft extent: self buttonExtent) color: Color lightGray. menuButton addMorphCentered: (BorderedMorph newBounds: (0@0 extent: 4@2) color: Color black). menuButton setBorderWidth: 1 borderColor: #raised. self addMorph: menuButton. ! ! !ScrollBar methodsFor: 'initialize' stamp: 'jm 6/15/2003 10:29'! initializePagingArea pagingArea _ BorderedMorph newBounds: self totalSliderArea color: (Color r: 0.6 g: 0.6 b: 0.8). pagingArea borderWidth: 0. self addMorph: pagingArea. ! ! !ScrollBar methodsFor: 'initialize' stamp: 'jm 10/13/2002 11:16'! initializeSlider "Note: we must initialize all the parts before initializing the slider itself." self initializeMenuButton; initializeUpButton; initializeDownButton; initializePagingArea. super initializeSlider. ! ! !ScrollBar methodsFor: 'initialize' stamp: 'jm 6/15/2003 10:29'! initializeUpButton | where | where _ menuButton ifNil: [self innerBounds topLeft] ifNotNil: [ bounds isWide ifTrue: [menuButton bounds topRight] ifFalse: [menuButton bounds bottomLeft]]. upButton _ BorderedMorph newBounds: (where extent: self buttonExtent) color: Color lightGray. upButton addMorphCentered: (ImageMorph new form: (bounds isWide ifTrue: [UpArrow rotateBy: #left centerAt: 0@0] ifFalse: [UpArrow])). upButton setBorderWidth: 1 borderColor: #raised. self addMorph: upButton. ! ! !ScrollBar methodsFor: 'access' stamp: 'jm 2/4/2003 11:49'! hasMenuButton: aBoolean hasMenuButton _ aBoolean. hasMenuButton ifFalse: [menuButton _ nil]. self removeAllMorphs; initializeSlider. ! ! !ScrollBar methodsFor: 'access' stamp: 'dew 2/15/1999 18:25'! pagingArea ^pagingArea! ! !ScrollBar methodsFor: 'access' stamp: 'jm 2/1/2005 14:49'! percentVisible: d "Supply an optional floating fraction so slider can expand to indicate the percent of the content that is visible." interval _ d min: 1.0. self expandSlider. self computeSlider. ! ! !ScrollBar methodsFor: 'geometry' stamp: 'dew 6/9/1999 02:02'! buttonExtent ^ bounds isWide ifTrue: [11 @ self innerBounds height] ifFalse: [self innerBounds width @ 11]! ! !ScrollBar methodsFor: 'geometry' stamp: 'dew 2/27/1999 18:22'! expandSlider "Compute the new size of the slider (use the old sliderThickness as a minimum)." | r | r _ self totalSliderArea. slider extent: (bounds isWide ifTrue: [((r width * interval) asInteger max: self sliderThickness) @ slider height] ifFalse: [slider width @ ((r height * interval) asInteger max: self sliderThickness)])! ! !ScrollBar methodsFor: 'geometry' stamp: 'dew 7/22/1999 19:03'! extent: p p x > p y ifTrue: [super extent: (p max: 42@8)] ifFalse: [super extent: (p max: 8@42)]! ! !ScrollBar methodsFor: 'geometry' stamp: 'dew 2/21/1999 03:08'! sliderExtent "The sliderExtent is now stored in the slider itself, not hardcoded as it is in the superclass." ^slider extent! ! !ScrollBar methodsFor: 'scrolling' stamp: 'di 8/17/1998 09:39'! scrollByPage: event nextPageDirection == nil ifTrue: [nextPageDirection _ event cursorPoint y >= slider center y]. (self waitForDelay1: 300 delay2: 100) ifFalse: [^ self]. nextPageDirection ifTrue: [self setValue: (value + pageDelta min: 1.0)] ifFalse: [self setValue: (value - pageDelta max: 0.0)] ! ! !ScrollBar methodsFor: 'scrolling' stamp: 'di 8/17/1998 09:40'! scrollDown (self waitForDelay1: 300 delay2: 50) ifFalse: [^ self]. self setValue: (value + scrollDelta + 0.000001 min: 1.0)! ! !ScrollBar methodsFor: 'scrolling' stamp: 'jm 10/13/2002 11:01'! scrollDown: count "Used to scroll down by the given number of scrollDeltas in response to arrow keys. A negative count scrolls up." self setValue: ((value + (count * scrollDelta) + 0.000001 min: 1.0) max: 0.0). ! ! !ScrollBar methodsFor: 'scrolling' stamp: 'di 8/17/1998 09:40'! scrollUp (self waitForDelay1: 300 delay2: 50) ifFalse: [^ self]. self setValue: (value - scrollDelta - 0.000001 max: 0.0)! ! !ScrollBar methodsFor: 'scrolling' stamp: 'dew 2/21/1999 03:08'! setValue: newValue "Using roundTo: instead of truncateTo: ensures that scrollUp will scroll the same distance as scrollDown." ^ super setValue: (newValue roundTo: scrollDelta)! ! !ScrollBar methodsFor: 'event handling' stamp: 'jm 10/13/2002 10:21'! handlesMouseDown: evt ^ true ! ! !ScrollBar methodsFor: 'event handling' stamp: 'jm 10/13/2002 11:14'! mouseDown: evt | p | p _ evt cursorPoint. dragOffset _ slider position - p. mouseTarget _ nil. (slider containsPoint: p) ifTrue: [^ self mouseDownInSlider: evt]. (upButton containsPoint: p) ifTrue: [^ self mouseDownInUpButton: evt]. (downButton containsPoint: p) ifTrue: [^ self mouseDownInDownButton: evt]. (pagingArea containsPoint: p) ifTrue: [^ self mouseDownInPagingArea: evt]. (menuButton notNil and: [menuButton containsPoint: p]) ifTrue: [ ^ self mouseDownInMenu: evt]. ! ! !ScrollBar methodsFor: 'event handling' stamp: 'jm 10/13/2002 10:52'! mouseDownInDownButton: evt mouseTarget _ downButton. downButton borderInset. self resetTimer. ! ! !ScrollBar methodsFor: 'event handling' stamp: 'jm 10/13/2002 10:33'! mouseDownInMenu: evt "Send yellowButtonActivity: to my model, if I have one." model ifNil: [^ self]. model yellowButtonActivity: evt shiftPressed. ! ! !ScrollBar methodsFor: 'event handling' stamp: 'jm 10/13/2002 11:10'! mouseDownInPagingArea: evt mouseTarget _ pagingArea. self resetTimer. ! ! !ScrollBar methodsFor: 'event handling' stamp: 'jm 10/13/2002 10:48'! mouseDownInSlider: evt mouseTarget _ slider. super mouseDown: evt. ! ! !ScrollBar methodsFor: 'event handling' stamp: 'jm 10/13/2002 10:53'! mouseDownInUpButton: evt mouseTarget _ upButton. upButton borderInset. self resetTimer. ! ! !ScrollBar methodsFor: 'event handling' stamp: 'jm 10/13/2002 11:11'! mouseMove: evt mouseTarget == slider ifTrue: [^ super mouseMove: evt]. mouseTarget == upButton ifTrue: [^ self scrollUp]. mouseTarget == downButton ifTrue: [^ self scrollDown]. mouseTarget == pagingArea ifTrue: [^ self scrollByPage: evt]. ! ! !ScrollBar methodsFor: 'event handling' stamp: 'jm 10/13/2002 10:51'! mouseUp: evt mouseTarget == slider ifTrue: [^ super mouseUp: evt]. mouseTarget == upButton ifTrue: [^ upButton borderRaised]. mouseTarget == downButton ifTrue: [^ downButton borderRaised]. ! ! !ScrollBar methodsFor: 'scroll timing' stamp: 'di 8/17/1998 09:22'! resetTimer timeOfMouseDown _ Time millisecondClockValue. timeOfLastScroll _ timeOfMouseDown - 1000 max: 0. nextPageDirection _ nil. currentScrollDelay _ nil! ! !ScrollBar methodsFor: 'scroll timing' stamp: 'di 8/17/1998 09:38'! waitForDelay1: delay1 delay2: delay2 "Return true if an appropriate delay has passed since the last scroll operation. The delay decreases exponentially from delay1 to delay2." | now scrollDelay | timeOfLastScroll == nil ifTrue: [self resetTimer]. "Only needed for old instances" now _ Time millisecondClockValue. now < timeOfLastScroll ifTrue: [self resetTimer "rare clock rollover"]. (scrollDelay _ currentScrollDelay) == nil ifTrue: [scrollDelay _ delay1 "initial delay"]. now > (timeOfLastScroll + scrollDelay) ifFalse: [^ false "not time yet"]. currentScrollDelay _ scrollDelay*9//10 max: delay2. "decrease the delay" timeOfLastScroll _ now. ^ true! ! I represent control for scrolling using a scrollBar. I am a MouseMenuController that creates a scrollBar, rather than menus. My subclasses add the button menus. I keep control as long as the cursor is inside the view or the scrollBar area. A scrollBar is a rectangular area representing the length of the information being viewed. It contains an inner rectangle whose top y-coordinate represents the relative position of the information visible on the screen with respect to all of the information, and whose size represents the relative amount of that information visible on the screen. The user controls which part of the information is visible by pressing the red button. If the cursor is to the right of the inner rectangle, the window onto the visible information moves upward, if the cursor is to the left, the window moves downward, and if the cursor is inside, the inner rectangle is grabbed and moved to a desired position.! !ScrollController methodsFor: 'control defaults' stamp: 'sma 3/11/2000 15:17'! controlActivity self scrollByKeyboard ifTrue: [^ self]. self scrollBarContainsCursor ifTrue: [self scroll] ifFalse: [self normalActivity]! ! !ScrollController methodsFor: 'control defaults' stamp: 'ar 3/24/2000 00:45'! isControlActive super isControlActive ifTrue: [^ true]. sensor blueButtonPressed ifTrue: [^ false]. ^ (scrollBar inside merge: view insetDisplayBox) containsPoint: sensor cursorPoint! ! !ScrollController methodsFor: 'control defaults' stamp: 'sma 3/11/2000 15:31'! isControlWanted ^ self viewHasCursor! ! !ScrollController methodsFor: 'control defaults' stamp: 'sma 3/11/2000 15:16'! normalActivity super controlActivity! ! !ScrollController methodsFor: 'scrolling' stamp: 'jm 11/8/2005 12:34'! scroll "Check to see whether the user wishes to jump, scroll up, or scroll down." | savedCursor | savedCursor _ Cursor currentCursor. [self scrollBarContainsCursor] whileTrue: [ self interActivityPause. sensor cursorPoint x <= self downLine ifTrue: [self scrollDown] ifFalse: [ sensor cursorPoint x <= self upLine ifTrue: [self scrollAbsolute] ifFalse: [ sensor cursorPoint x <= self yellowLine ifTrue: [self scrollUp] ifFalse: [ sensor cursorPoint x <= scrollBar right ifTrue: [ "might not be, with touch pen" self changeCursor: Cursor menu. sensor anyButtonPressed ifTrue: [ self changeCursor: savedCursor. self anyButtonActivity]]]]]]. savedCursor show. ! ! !ScrollController methodsFor: 'scrolling' stamp: 'th 12/11/1999 16:57'! scrollByKeyboard | keyEvent | keyEvent _ sensor keyboardPeek. keyEvent ifNil: [^ false]. (sensor controlKeyPressed or:[sensor commandKeyPressed]) ifFalse: [^ false]. keyEvent asciiValue = 30 ifTrue: [sensor keyboard. self scrollViewDown ifTrue: [self moveMarker]. ^ true]. keyEvent asciiValue = 31 ifTrue: [sensor keyboard. self scrollViewUp ifTrue: [self moveMarker]. ^ true]. ^ false! ! !ScrollController methodsFor: 'cursor' stamp: 'jm 11/8/2005 12:30'! changeCursor: aCursor "The current cursor should be set to be aCursor." Cursor currentCursor ~~ aCursor ifTrue: [aCursor show]. ! ! I am a frame for scrollable contents. I can independently show or hide my horizontal and vertical scrollbars. ! !ScrollFrameMorph methodsFor: 'initialization' stamp: 'jm 3/3/2008 22:30'! initialize super initialize. self color: Color red. hbarInset _ 12. vbarInset _ 12. contentsChanged _ false. growthFraction _ 0.6. "amount to grow when contents do not fit as a fraction of the current size" contents _ PasteUpMorph new color: Color red; borderWidth: 0; enableDragNDrop: true. hScrollbar _ ScrollBar new model: self; setValueSelector: #hScrollRelative:; hasMenuButton: false. vScrollbar _ ScrollBar new model: self; setValueSelector: #vScrollRelative:; hasMenuButton: false. cornerMorph _ BorderedMorph new borderRaised; borderWidth: 2; color: hScrollbar color. contents position: self position + borderWidth. self addMorph: contents. self addMorph: hScrollbar. self addMorph: vScrollbar. self addMorph: cornerMorph. self extent: 160@120. ! ! !ScrollFrameMorph methodsFor: 'accessing' stamp: 'jm 8/2/2003 14:34'! contents "Answer my contents morph." ^ contents ! ! !ScrollFrameMorph methodsFor: 'accessing' stamp: 'jm 8/3/2003 10:48'! contents: aMorph "Replace my contents morph. The argument can be any morph. Typically it is a BorderedMorph or a PasteUpMorph." contents ifNotNil: [contents delete]. contents _ aMorph. contents position: self topLeft + borderWidth. self addMorphBack: contents. self extent: self extent. ! ! !ScrollFrameMorph methodsFor: 'accessing' stamp: 'jm 3/3/2008 22:32'! growthFraction: aNumber "Set the amount to grow when contents do not fit, expressed as a fraction of the current size. For example, 0.5 makes the contains pane 50% larger if any submorph extends over the edge." growthFraction _ aNumber max: 0.0. ! ! !ScrollFrameMorph methodsFor: 'accessing' stamp: 'ee 5/1/2008 10:20'! hBarInset: anInteger hbarInset _ anInteger. ! ! !ScrollFrameMorph methodsFor: 'accessing' stamp: 'ee 5/1/2008 10:19'! vBarInset: anInteger vbarInset _ anInteger. ! ! !ScrollFrameMorph methodsFor: 'scrolling' stamp: 'jm 9/9/2003 10:52'! hScrollPixels "Answer the current horizontal scroll offset in pixels." ^ (self left + borderWidth) - contents left ! ! !ScrollFrameMorph methodsFor: 'scrolling' stamp: 'jm 9/10/2003 11:58'! hScrollPixels: hOffset "Scroll to the given horizontal offset in pixels. Zero is scrolled to the left and increasing values scroll right." | delta maxOffset | delta _ (hOffset asInteger min: self maxScroll x) max: 0. contents left: ((self left + borderWidth) - delta) truncated. maxOffset _ self maxScroll x. maxOffset > 0 ifTrue: [hScrollbar value: self hScrollPixels / maxOffset]. ! ! !ScrollFrameMorph methodsFor: 'scrolling' stamp: 'jm 8/2/2003 20:25'! hScrollRelative: aFloat "Sent by the horizontal scrollbar. Scroll to the given relative postion between 0.0 and 1.0." self hScrollPixels: aFloat * self maxScroll x. ! ! !ScrollFrameMorph methodsFor: 'scrolling' stamp: 'jm 8/2/2003 20:25'! maxScroll "Answer a point representing the maximum horizontal and vertical scroll offsets in pixels." ^ contents extent - self visibleExtent ! ! !ScrollFrameMorph methodsFor: 'scrolling' stamp: 'jm 6/23/2005 16:47'! scrollSubmorphIntoView: aMorph | offset | (contents submorphs includes: aMorph) ifFalse: [^ self]. (self bounds containsRect: aMorph fullBounds) ifTrue: [^ self]. (aMorph fullBounds bottom > self bottom) ifTrue: [ offset _ aMorph fullBounds bottom - contents top. contents top: (self bottom - 3) - offset]. (aMorph fullBounds top < self top) ifTrue: [ offset _ aMorph fullBounds top - contents top. contents top: (self top + 3) - offset]. self updateScrollbars. ! ! !ScrollFrameMorph methodsFor: 'scrolling' stamp: 'jm 9/9/2003 10:52'! vScrollPixels "Answer the current vertical scroll offset in pixels." ^ (self top + borderWidth) - contents top ! ! !ScrollFrameMorph methodsFor: 'scrolling' stamp: 'jm 9/10/2003 11:58'! vScrollPixels: vOffset "Scroll to the given vertical offset in pixels. Zero is scrolled to the top and increasing values scroll down." | delta maxOffset | delta _ (vOffset asInteger min: self maxScroll y) max: 0. contents top: ((self top + borderWidth) - delta) truncated. maxOffset _ self maxScroll y. maxOffset > 0 ifTrue: [vScrollbar value: self vScrollPixels / maxOffset]. ! ! !ScrollFrameMorph methodsFor: 'scrolling' stamp: 'jm 8/2/2003 15:25'! vScrollRelative: aFloat "Sent by the vertical scrollbar. Scroll to the given relative postion between 0.0 and 1.0." self vScrollPixels: aFloat * self maxScroll y. ! ! !ScrollFrameMorph methodsFor: 'scrollbar visibility' stamp: 'jm 8/2/2003 14:52'! showHorizontalScrollbar: aBoolean "Show or hide my horizontal scrollbar." aBoolean ifTrue: [ self addMorph: hScrollbar. vScrollbar owner = self ifTrue: [self addMorph: cornerMorph]] ifFalse: [ hScrollbar delete. cornerMorph delete]. self extent: self extent. ! ! !ScrollFrameMorph methodsFor: 'scrollbar visibility' stamp: 'jm 8/2/2003 14:52'! showVerticalScrollbar: aBoolean "Show or hide my vertical scrollbar." aBoolean ifTrue: [ self addMorph: vScrollbar. hScrollbar owner = self ifTrue: [self addMorph: cornerMorph]] ifFalse: [ vScrollbar delete. cornerMorph delete]. self extent: self extent. ! ! !ScrollFrameMorph methodsFor: 'drawing' stamp: 'jm 2/1/2005 12:01'! drawSubmorphsOn: aCanvas "If my contents has changed, fix it's extent and update my scrollbar ranges. Clip submorph drawing to my bounds." contentsChanged ifTrue: [ self updateContentsExtent. self updateScrollbars. contentsChanged _ false]. super drawSubmorphsOn: (aCanvas copyClipRect: self innerBounds). ! ! !ScrollFrameMorph methodsFor: 'layout' stamp: 'jm 8/2/2003 14:26'! hResizing ^ #spaceFill ! ! !ScrollFrameMorph methodsFor: 'layout' stamp: 'jm 8/2/2003 14:24'! isAlignmentMorph "Answer true, since I can be laid out as if I were an AlignmentMorph. I pretend to be an AlignmentMorph so that I can be resized when I'm inside another AlignmentMorph." ^ true ! ! !ScrollFrameMorph methodsFor: 'layout' stamp: 'jm 8/2/2003 14:22'! layoutInWidth: w height: h "Resize myself to the given width and height. Called during when I am in an AlignmentMorph." self extent: w@h. ! ! !ScrollFrameMorph methodsFor: 'layout' stamp: 'jm 7/21/2003 21:06'! minHeight ^ 45 ! ! !ScrollFrameMorph methodsFor: 'layout' stamp: 'jm 7/21/2003 21:06'! minWidth ^ 60 ! ! !ScrollFrameMorph methodsFor: 'layout' stamp: 'jm 8/2/2003 14:26'! vResizing ^ #spaceFill ! ! !ScrollFrameMorph methodsFor: 'geometry' stamp: 'jm 2/23/2005 17:48'! extent: aPoint "After setting my size, position and size my scrollbars and grow box. Also update my contents and scrollbar ranges." | inner w h | super extent: (aPoint truncated max: self minWidth@self minHeight). inner _ self innerBounds. w _ inner width. vScrollbar owner = self ifTrue: [w _ w - vbarInset]. hScrollbar position: inner left@(inner bottom - hbarInset). hScrollbar extent: w@hbarInset. h _ inner height. hScrollbar owner = self ifTrue: [h _ h - hbarInset]. vScrollbar position: (inner right - vbarInset)@inner top. vScrollbar extent: vbarInset@h. cornerMorph position: (inner bottomRight - (vbarInset@hbarInset)). cornerMorph extent: vbarInset@hbarInset. self updateContentsExtent. self updateScrollbars. ! ! !ScrollFrameMorph methodsFor: 'geometry' stamp: 'jm 7/24/2003 18:15'! fullBounds "Overridden to clip submorph hit detection to my bounds." ^ bounds ! ! !ScrollFrameMorph methodsFor: 'geometry' stamp: 'jm 8/3/2003 09:11'! invalidRect: damageRect "Clip damage reports to my bounds, since drawing is clipped to my bounds." | r | r _ damageRect intersect: self bounds. (r width > 0 and: [r height > 0]) ifTrue: [super invalidRect: r]. ! ! !ScrollFrameMorph methodsFor: 'geometry' stamp: 'jm 8/2/2003 14:31'! layoutChanged "If my contents morph's layout has changed, record that fact." super layoutChanged. contents mayNeedLayout ifTrue: [contentsChanged _ true]. ! ! !ScrollFrameMorph methodsFor: 'private' stamp: 'jm 8/2/2003 20:02'! contentsBounds "Answer a rectangle that encloses the bounds of all my submorphs." | offset r | offset _ contents topLeft negated. r _ 0@0 extent: 1@1. contents submorphsDo: [:m | r _ r quickMerge: (m fullBounds translateBy: offset) truncated]. ^ r ! ! !ScrollFrameMorph methodsFor: 'private' stamp: 'ee 3/8/2008 16:28'! updateContentsExtent "Make sure my content morph is large enough to both enclose all it's submorphs and to fill me. Adjust the scroll offsets if my bounds have shrunk." | r delta scrolledOff w h visible | r _ self contentsBounds. ((r left < 0) or: [r top < 0]) ifTrue: [ "contents has morphs that stick out over its top or left edge" delta _ (r topLeft truncated min: (0@0)) negated. contents submorphsDo: [:m | m position: m position + delta]]. "move all submorphs by delta" visible _ self visibleExtent - (8@20). w _ visible x. r width > w ifTrue: [w _ (r width + (growthFraction * visible x)) rounded]. h _ visible y. r height > h ifTrue: [h _ (r height + (growthFraction * visible y)) rounded]. scrolledOff _ ((self topLeft + borderWidth) - contents position) max: 0@0. contents extent: ((w@h) max: (self visibleExtent + scrolledOff)). contentsChanged _ false. ! ! !ScrollFrameMorph methodsFor: 'private' stamp: 'jm 5/12/2008 21:08'! updateScrollbars "Update my scrollbars based on my current contents." | visibleExtent currentScroll maxScroll | contents ifNil: [^ self]. "not fully initialized yet" visibleExtent _ self visibleExtent. hScrollbar percentVisible: (visibleExtent x asFloat / contents width). vScrollbar percentVisible: (visibleExtent y asFloat / contents height). currentScroll _ (self topLeft + borderWidth) - contents position. contents width > visibleExtent x ifTrue: [ maxScroll _ contents width - visibleExtent x. self showHorizontalScrollbar: true. hScrollbar scrollDelta: 4 / maxScroll asFloat pageDelta: (visibleExtent x - 40) / maxScroll asFloat. hScrollbar value: currentScroll x / maxScroll] ifFalse: [ self showHorizontalScrollbar: false. hScrollbar value: 0]. contents height > visibleExtent y ifTrue: [ maxScroll _ contents height - visibleExtent y. self showVerticalScrollbar: true. vScrollbar scrollDelta: 4.0 / maxScroll asFloat pageDelta: (visibleExtent y - 40) / maxScroll asFloat. vScrollbar value: currentScroll y / maxScroll] ifFalse: [ self showVerticalScrollbar: false. vScrollbar value: 0]. ! ! !ScrollFrameMorph methodsFor: 'private' stamp: 'jm 4/6/2005 11:48'! visibleExtent "Answer the extent of my visible area. That is, the area within my borders minus the space used for scrollbars." | clipToScrollBarEdge visibleW visibleH | clipToScrollBarEdge _ false. clipToScrollBarEdge ifFalse: [^ self extent - (2 * borderWidth)]. visibleW _ self width - (2 * borderWidth). vScrollbar owner = self ifTrue: [visibleW _ visibleW - vbarInset]. visibleH _ self height - (2 * borderWidth). hScrollbar owner = self ifTrue: [visibleH _ visibleH - hbarInset]. ^ visibleW@visibleH ! ! This subclass of ScrollFrameMorph uses ScratchScrollBars. ! !ScrollFrameMorph2 methodsFor: 'initialization' stamp: 'jm 5/26/2005 18:22'! initialize super initialize. self borderWidth: 0. self removeAllMorphs. self color: Color blue. hbarInset _ 30. vbarInset _ 25. scrollBarStartInset _ 10. scrollBarEndInset _ 14. contentsChanged _ false. contents _ PasteUpMorph new color: (Color gray: 0.9); borderWidth: 0; enableDragNDrop: true. hScrollbar _ ScratchScrollBar newHorizontal target: self; selector: #hScrollRelative:. vScrollbar _ ScratchScrollBar newVertical target: self; selector: #vScrollRelative:. contents position: self position + borderWidth. self addMorph: contents. self addMorph: hScrollbar. self addMorph: vScrollbar. self extent: 160@120. ! ! !ScrollFrameMorph2 methodsFor: 'geometry' stamp: 'ee 3/6/2008 21:44'! extent: aPoint "After setting my size, position and size my scrollbars and grow box. Also update my contents and scrollbar ranges." | inner w h | super extent: (aPoint truncated max: self minWidth@self minHeight). scrollBarStartInset ifNil: [ scrollBarStartInset _ scrollBarEndInset _ 0]. "needed during initialization" inner _ self innerBounds. w _ inner width - scrollBarStartInset. vScrollbar owner = self ifTrue: [w _ w - vbarInset] ifFalse: [w _ w - scrollBarEndInset]. hScrollbar position: (inner left + scrollBarStartInset)@(inner bottom - hbarInset). hScrollbar width: w. h _ inner height - scrollBarStartInset. hScrollbar owner = self ifTrue: [h _ h - hbarInset] ifFalse: [h _ h - scrollBarEndInset]. ScratchTranslator isRTL ifTrue: [vScrollbar position: (inner left + 9)@(inner top + scrollBarStartInset)] ifFalse: [vScrollbar position: (inner right - vbarInset)@(inner top + scrollBarStartInset)]. vScrollbar height: h. self updateContentsExtent. self updateScrollbars. ! ! !ScrollFrameMorph2 methodsFor: 'geometry' stamp: 'jm 6/28/2008 12:35'! scrollbarStartInset: startInset endInset: endInset scrollBarStartInset _ startInset. scrollBarEndInset _ endInset. ! ! !ScrollFrameMorph2 methodsFor: 'drawing' stamp: 'jm 4/5/2005 21:43'! drawSubmorphsOn: aCanvas "If my contents has changed, fix it's extent and update my scrollbar ranges. Clip submorph drawing to my bounds." | clipR | contentsChanged ifTrue: [ self updateContentsExtent. self updateScrollbars. contentsChanged _ false]. "draw my contents morph clipped to my visible extent" clipR _ self innerBounds topLeft extent: self visibleExtent. (aCanvas copyClipRect: clipR) fullDrawMorph: contents. "draw all my submorphs other than my contents" submorphs reverseDo:[:m | (m ~~ contents) ifTrue: [aCanvas fullDrawMorph: m]]. ! ! !ScrollFrameMorph2 methodsFor: 'scrollbar visibility' stamp: 'jm 4/6/2005 17:07'! showHorizontalScrollbar: aBoolean "Show or hide my horizontal scrollbar." aBoolean = (hScrollbar owner = self) ifTrue: [^ self]. aBoolean ifTrue: [self addMorph: hScrollbar] ifFalse: [hScrollbar delete]. self extent: self extent. ! ! !ScrollFrameMorph2 methodsFor: 'scrollbar visibility' stamp: 'jm 4/6/2005 17:07'! showVerticalScrollbar: aBoolean "Show or hide my vertical scrollbar." aBoolean = (vScrollbar owner = self) ifTrue: [^ self]. aBoolean ifTrue: [self addMorph: vScrollbar] ifFalse: [vScrollbar delete]. self extent: self extent. ! ! !ScrollFrameMorph2 methodsFor: 'scrolling' stamp: 'ee 10/24/2007 19:45'! scrollMorphIntoView: aMorph | offset | (self bounds containsRect: aMorph fullBounds) ifTrue: [^ self]. ((aMorph fullBounds top < self top) and: [(aMorph fullBounds top < contents top) not]) ifTrue: [ offset _ aMorph fullBounds top - contents top. contents top: (self top + 3) - offset]. ((aMorph fullBounds left < self left) and: [(aMorph fullBounds left < contents left) not]) ifTrue: [ offset _ aMorph fullBounds left - contents left. contents left: (self left + 3) - offset]. (aMorph fullBounds right > self right) ifTrue: [ offset _ aMorph fullBounds right - contents left. contents left: (self right - 3) - offset]. (aMorph fullBounds bottom > self bottom) ifTrue: [ offset _ aMorph fullBounds bottom - contents top. contents top: (self bottom - 3) - offset]. self updateContentsExtent. self updateScrollbars. ! ! The scroller (a transform) of a scrollPane is driven by the scrollBar. The scroll values vary from 0.0, meaning zero offset to 1.0 meaning sufficient offset such that the bottom of the scrollable material appears halfway down the pane. The total distance to achieve this range is called the totalScrollRange. ! !ScrollPane methodsFor: 'initialization' stamp: 'tk 8/13/1998 13:05'! fullCopy | copy | self mouseEnter: nil. "Make sure scrollBar is in morphic structure" copy _ super fullCopy. "So that references are updated properly" "Will fail of any Players with scripts are in the ScrollPane" self mouseLeave: nil. ^ copy mouseLeave: nil ! ! !ScrollPane methodsFor: 'initialization' stamp: 'jm 10/10/2002 20:10'! initialize super initialize. hasFocus _ false. borderWidth _ 2. borderColor _ Color black. retractableScrollBar _ (Preferences valueOfFlag: #inboardScrollbars) not. scrollBarOnLeft _ (Preferences valueOfFlag: #scrollBarsOnRight) not. scrollBar _ ScrollBar new model: self. scrollBar borderWidth: 1; borderColor: Color black. scroller _ TransformMorph new color: Color transparent. scroller offset: -3@0. self addMorph: scroller. retractableScrollBar ifFalse: [self addMorph: scrollBar]. self extent: 150@120. ! ! !ScrollPane methodsFor: 'initialization' stamp: 'jm 2/1/2005 14:50'! setScrollDeltas | range delta | self hideOrShowScrollBar. scroller hasSubmorphs ifFalse: [scrollBar percentVisible: 1.0. ^ self]. range _ self leftoverScrollRange. delta _ self scrollDeltaHeight. range = 0 ifTrue: [^ scrollBar scrollDelta: 0.02 pageDelta: 0.2; percentVisible: 1.0]. "Set up for one line (for arrow scrolling), or a full pane less one line (for paging)." scrollBar scrollDelta: (delta / range) asFloat pageDelta: ((self innerBounds height - delta) / range) asFloat. scrollBar percentVisible: ((self innerBounds height - delta) / self totalScrollRange) asFloat. ! ! !ScrollPane methodsFor: 'access' stamp: 'jm 10/4/2002 17:35'! colorForInsets "My submorphs use the surrounding color." (owner color isKindOf: Color) ifTrue: [^ owner color] ifFalse: [^ Color white]. ! ! !ScrollPane methodsFor: 'access' stamp: 'dew 10/17/1999 19:40'! hasFocus "hasFocus is currently set by mouse enter/leave events. This inst var should probably be moved up to a higher superclass." ^ hasFocus ifNil: [false]! ! !ScrollPane methodsFor: 'access' stamp: 'jm 10/3/2002 17:57'! model ^ model ! ! !ScrollPane methodsFor: 'access' stamp: 'jm 10/8/2002 10:43'! model: anObject "Set my model and make me me a dependent of the given object." model ifNotNil: [model removeDependent: self]. anObject ifNotNil: [anObject addDependent: self]. model _ anObject. ! ! !ScrollPane methodsFor: 'geometry' stamp: 'go 4/26/1999 10:06'! extent: newExtent super extent: (newExtent max: (self scrollbarWidth + 20)@16). self resizeScrollBar; resizeScroller; setScrollDeltas! ! !ScrollPane methodsFor: 'geometry' stamp: 'dew 10/17/1999 19:41'! innerBounds | inner w | inner _ super innerBounds. w _ self scrollbarWidth. retractableScrollBar | (submorphs includes: scrollBar) not ifTrue: [^ inner] ifFalse: [^ (scrollBarOnLeft ifTrue: [inner topLeft + ((w-1)@0) corner: inner bottomRight] ifFalse: [inner topLeft corner: inner bottomRight - ((w-2)@0)])]! ! !ScrollPane methodsFor: 'geometry' stamp: 'dew 2/19/1999 18:46'! leftoverScrollRange "Return the entire scrolling range minus the currently viewed area." ^ self totalScrollRange - (bounds height * 3 // 4) max: 0 ! ! !ScrollPane methodsFor: 'geometry' stamp: 'dew 10/17/1999 19:41'! resetExtent "Reset the extent. (may be overridden by subclasses which need to do more than this)" self resizeScroller! ! !ScrollPane methodsFor: 'geometry' stamp: 'di 11/11/1998 09:11'! resizeScrollBar | w topLeft | w _ self scrollbarWidth. topLeft _ scrollBarOnLeft ifTrue: [retractableScrollBar ifTrue: [bounds topLeft - ((w-1)@0)] ifFalse: [bounds topLeft]] ifFalse: [retractableScrollBar ifTrue: [bounds topRight] ifFalse: [bounds topRight - ((w-1)@0)]]. scrollBar bounds: (topLeft extent: w @ bounds height)! ! !ScrollPane methodsFor: 'geometry' stamp: 'di 11/11/1998 09:48'! resizeScroller scroller bounds: self innerBounds! ! !ScrollPane methodsFor: 'geometry' stamp: 'di 8/16/1998 01:09'! scrollBarFills: aRectangle "Return true if a flop-out scrollbar fills the rectangle" ^ (retractableScrollBar and: [submorphs includes: scrollBar]) and: [scrollBar bounds containsRect: aRectangle]! ! !ScrollPane methodsFor: 'geometry' stamp: 'dew 2/19/1999 18:49'! scrollBy: delta "Move the contents in the direction delta." "For now, delta is assumed to have a zero x-component" | newYoffset r | newYoffset _ scroller offset y - delta y max: 0. scroller offset: scroller offset x @ newYoffset. (r _ self leftoverScrollRange) = 0 ifTrue: [scrollBar value: 0.0] ifFalse: [scrollBar value: newYoffset asFloat / r]! ! !ScrollPane methodsFor: 'geometry' stamp: 'dew 2/19/1999 17:08'! scrollDeltaHeight "Return the increment in pixels which this pane should be scrolled (normally a subclass responsibility)." ^ 12 ! ! !ScrollPane methodsFor: 'geometry' stamp: 'di 12/6/1999 08:35'! scrollbarWidth "Includes border" (Preferences valueOfFlag: #scrollBarsNarrow) ifTrue: [^ 12] ifFalse: [^ 16]! ! !ScrollPane methodsFor: 'geometry' stamp: 'dew 2/19/1999 20:24'! totalScrollRange "Return the entire scrolling range." ^ (scroller submorphBounds encompass: 0@0) height ! ! !ScrollPane methodsFor: 'geometry' stamp: 'dew 10/17/1999 19:41'! unadjustedScrollRange "Return the difference between the height extent of the receiver's submorphs and its own height extent (plus an extra 1/2 line height)." scroller submorphBounds ifNil: [^ 0]. ^ self totalScrollRange - bounds height + (self scrollDeltaHeight / 2) max: 0! ! !ScrollPane methodsFor: 'event handling' stamp: 'jm 10/10/2002 20:08'! handlesMouseDown: evt ^ true ! ! !ScrollPane methodsFor: 'event handling' stamp: 'di 1/18/2000 15:29'! handlesMouseOver: evt "Could just ^ true, but this ensures that scroll bars won't flop out if you mouse-over appendages such as connecting pins." | cp | cp _ evt cursorPoint. (bounds containsPoint: cp) ifTrue: [^ true] ifFalse: [self submorphsDo: [:m | (m containsPoint: cp) ifTrue: [m == scrollBar ifTrue: [^ true] ifFalse: [^ false]]]. ^ false]! ! !ScrollPane methodsFor: 'event handling' stamp: 'jm 10/10/2002 20:09'! keyStroke: evt "If pane is not full, pass the event to the last submorph, assuming it is the most appropriate recipient (!!)" (self scrollByKeyboard: evt) ifTrue: [^ self]. scroller submorphs last keyStroke: evt. ! ! !ScrollPane methodsFor: 'event handling' stamp: 'jm 11/27/2007 16:09'! mouseDown: evt evt rightButtonPressed ifTrue: [ "first check for menu click" ^ self yellowButtonActivity: evt shiftPressed]. "If pane is not full, pass the event to the last submorph, assuming it is the most appropriate recipient (!!)" scroller hasSubmorphs ifTrue: [ scroller submorphs last mouseDown: (evt transformedBy: (scroller transformFrom: self))]. ! ! !ScrollPane methodsFor: 'event handling' stamp: 'dew 5/22/2000 15:55'! mouseEnter: event hasFocus _ true. (owner isKindOf: SystemWindow) ifTrue: [owner paneTransition: event]. self hideOrShowScrollBar. ! ! !ScrollPane methodsFor: 'event handling' stamp: 'dew 10/17/1999 19:41'! mouseLeave: event hasFocus _ false. retractableScrollBar ifTrue: [self hideScrollBar]. (owner isKindOf: SystemWindow) ifTrue: [owner paneTransition: event]! ! !ScrollPane methodsFor: 'event handling' stamp: 'jm 10/13/2002 10:58'! scrollByKeyboard: event "If event is ctrl+up/down then scroll and answer true." (event controlKeyPressed or: [event commandKeyPressed]) ifFalse: [^ false]. event keyValue = 30 ifTrue: [scrollBar scrollDown: -3. ^ true]. event keyValue = 31 ifTrue: [scrollBar scrollDown: 3. ^ true]. ^ false ! ! !ScrollPane methodsFor: 'scroll bar events' stamp: 'di 6/26/1998 13:31'! scrollBarMenuButtonPressed: event ^ self yellowButtonActivity: event shiftPressed! ! !ScrollPane methodsFor: 'scroll bar events' stamp: 'dew 2/19/1999 18:48'! scrollBarValue: scrollValue scroller hasSubmorphs ifFalse: [^ self]. scroller offset: -3 @ (self leftoverScrollRange * scrollValue)! ! !ScrollPane methodsFor: 'scroll bar events' stamp: 'di 6/26/1998 13:31'! shiftedYellowButtonActivity ^ self yellowButtonActivity: true! ! !ScrollPane methodsFor: 'scroll bar events' stamp: 'di 6/26/1998 13:32'! unshiftedYellowButtonActivity ^ self yellowButtonActivity: false! ! !ScrollPane methodsFor: 'scroll bar events' stamp: 'jm 12/7/2005 16:38'! yellowButtonActivity: shiftKeyState | menu | (menu _ self getMenu: shiftKeyState) ifNotNil: [ menu setInvokingView: self. menu popUpForHand: self activeHand]. ! ! !ScrollPane methodsFor: 'menu' stamp: 'jm 10/10/2002 20:07'! addCustomMenuItems: aCustomMenu hand: aHandMorph super addCustomMenuItems: aCustomMenu hand: aHandMorph. retractableScrollBar ifTrue: [aCustomMenu add: 'make scrollbar permanent' action: #retractableOrNot] ifFalse: [aCustomMenu add: 'make scrollbar retractable' action: #retractableOrNot]. scrollBarOnLeft ifTrue: [aCustomMenu add: 'scroll bar on right' action: #toggleScrollBarOnLeft] ifFalse: [aCustomMenu add: 'scroll bar on left' action: #toggleScrollBarOnLeft]. ! ! !ScrollPane methodsFor: 'menu' stamp: 'sw 9/23/1998 08:47'! getMenu: shiftKeyState "Answer the menu for this text view, supplying an empty menu to be filled in. If the menu selector takes an extra argument, pass in the current state of the shift key." | menu aMenu aTitle | getMenuSelector == nil ifTrue: [^ nil]. menu _ MenuMorph new defaultTarget: model. aTitle _ getMenuTitleSelector ifNotNil: [model perform: getMenuTitleSelector]. getMenuSelector numArgs = 1 ifTrue: [aMenu _ model perform: getMenuSelector with: menu. aTitle ifNotNil: [aMenu addTitle: aTitle]. ^ aMenu]. getMenuSelector numArgs = 2 ifTrue: [aMenu _ model perform: getMenuSelector with: menu with: shiftKeyState. aTitle ifNotNil: [aMenu addTitle: aTitle]. ^ aMenu]. ^ self error: 'The getMenuSelector must be a 1- or 2-keyword symbol'! ! !ScrollPane methodsFor: 'menu' stamp: 'sw 8/18/1998 12:38'! menuTitleSelector: aSelector getMenuTitleSelector _ aSelector! ! !ScrollPane methodsFor: 'menu' stamp: 'sw 11/5/1998 14:14'! retractable: aBoolean retractableScrollBar == aBoolean ifFalse: [self retractableOrNot "toggles it"]! ! !ScrollPane methodsFor: 'menu' stamp: 'jm 10/10/2002 20:06'! toggleScrollBarOnLeft "Put the scroll bar ont the other side." scrollBarOnLeft _ scrollBarOnLeft not. self extent: self extent. ! ! !ScrollPane methodsFor: 'scrolling' stamp: 'dew 5/22/2000 15:18'! hideOrShowScrollBar "Hide or show the scrollbar depending on if the pane is scrolled/scrollable." "Don't do anything with the retractable scrollbar unless we have focus" retractableScrollBar & self hasFocus not ifTrue: [^self]. self isScrollable not & self isScrolledFromTop not ifTrue: [self hideScrollBar]. self isScrollable | self isScrolledFromTop ifTrue: [self showScrollBar]. ! ! !ScrollPane methodsFor: 'scrolling' stamp: 'RAA 6/8/2000 12:34'! hideScrollBar (submorphs includes: scrollBar) ifFalse: [^self]. self privateRemoveMorph: scrollBar. scrollBar privateOwner: nil. retractableScrollBar ifFalse: [self resetExtent].! ! !ScrollPane methodsFor: 'scrolling' stamp: 'dew 5/22/2000 16:28'! isScrollable (Preferences valueOfFlag: #hiddenScrollBars) ifFalse: [^ true]. "If the contents of the pane are too small to scroll, return false." ^ self unadjustedScrollRange > 0 "treat a single line as non-scrollable" and: [self totalScrollRange > (self scrollDeltaHeight * 3/2)]! ! !ScrollPane methodsFor: 'scrolling' stamp: 'dew 5/22/2000 15:17'! isScrolledFromTop "Have the contents of the pane been scrolled, so that the top of the contents are not visible?" ^scroller offset y > 0 ! ! !ScrollPane methodsFor: 'scrolling' stamp: 'RAA 6/8/2000 12:35'! showScrollBar (submorphs includes: scrollBar) ifTrue: [^self]. self privateAddMorph: scrollBar atIndex: 1. self resizeScrollBar. scrollBar changed. retractableScrollBar ifFalse: [self resetExtent].! ! An editable, multiple-line string in a single font with line wrapping and scrolling. Best for small amounts of text; for longer texts or texts with multiple type styles, see TextMorph. My contents are stored in an array of strings ('lines') with all non-printing characters except cr's and spaces stripped out and tabs replaced by a sequence of spaces. Possible improvements: a. handle tabs correctly b. preserve non-printing characters (could be useful for editing files) ! !ScrollingStringMorph methodsFor: 'initialization' stamp: 'jm 7/19/2008 09:19'! initialize super initialize. borderWidth _ 2. color _ Color transparent. textColor _ Color black. selectionColor _ Color r: 0.353 g: 0.607 b: 0.788. lines _ Array with: self emptyLine. isEditable _ true. hasFocus _ false. selectionStart _ selectionEnd _ 1. blinkState _ true. layoutNeeded _ false. scrollbar _ ScratchScrollBar newVertical width: 16; target: self; selector: #scrollRelative:. firstVisibleLine _ 1. self addMorph: scrollbar. self font: TextStyle defaultFont. self extent: 150@300. ! ! !ScrollingStringMorph methodsFor: 'copying' stamp: 'jm 7/25/2006 10:22'! copy "Return a copy with a copy of my lines array." ^ super copy setLines: lines copy ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 7/19/2008 09:36'! contents "Answer my contents as a string." | sz result i | sz _ 0. lines do: [:s | sz _ sz + s size]. result _ UTF32 new: sz. i _ 1. lines do: [:s | sz _ s size. result replaceFrom: i to: i + sz - 1 with: s startingAt: 1. i _ i + sz]. ^ result asUTF8 ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 6/11/2007 18:03'! contents: aStringOrText "Set my contents to the given String or Text. Break into lines, emove non-printing characters, and replace tabs with sequences of spaces." lines _ Array with: (self replaceTabs: aStringOrText). selectionStart _ selectionEnd _ 1. layoutNeeded _ true. ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 9/16/2005 10:18'! font ^ font ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 6/29/2009 17:49'! font: aFont font _ aFont. renderer _ StringMorph new font: aFont. lineHeight _ renderer stringHeight: ScratchTranslator renderHintString, 'Ag'. lineHeight = 0 ifTrue: [lineHeight _ 10]. "workaround for possible bug in stringHeight:" layoutNeeded _ true. ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 9/16/2005 11:27'! hasFocus ^ hasFocus ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 7/11/2008 06:57'! isEditable "Answer true if I am editable." isEditable ifNil: [isEditable _ true]. ^ isEditable ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 7/11/2008 06:57'! isEditable: aBoolean isEditable _ aBoolean. ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 9/26/2005 19:41'! isEmpty "Answer true if my contents is empty." ^ lines size = 0 or: [lines size = 1 and: [lines first size = 0]] ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 12/8/2006 11:16'! lines ^ lines ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 6/10/2007 17:17'! scrollRelative: aFloat "Sent by the scrollbar. Scroll to the given relative postion between 0.0 and 1.0." | range | range _ lines size - (0.25 * self visibleLineCount). firstVisibleLine _ (aFloat * range) rounded. firstVisibleLine _ firstVisibleLine max: 1. self changed. ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 9/23/2005 10:38'! selectionColor: aColor selectionColor _ aColor. ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 6/10/2007 13:35'! showScrollbar: aBoolean "Show or hide my scrollbar." aBoolean = (scrollbar owner = self) ifTrue: [^ self]. aBoolean ifTrue: [self addMorph: scrollbar] ifFalse: [scrollbar delete]. self extent: self extent. "fix layout" ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 12/9/2005 16:51'! textColor ^ textColor ! ! !ScrollingStringMorph methodsFor: 'accessing' stamp: 'jm 9/23/2005 10:37'! textColor: aColor textColor _ aColor. ! ! !ScrollingStringMorph methodsFor: 'drawing' stamp: 'jm 8/8/2008 21:11'! drawCursor: charIndex line: lineIndex on: aCanvas "Draw a cursor at the given character index on the given line." | line pair x p | line _ lines at: lineIndex. line size = 0 ifTrue: [pair _ #(0 0)] ifFalse: [pair _ (renderer xRangesFor: line) at: (charIndex within: 1 and: line size)]. ScratchTranslator isRTL ifTrue: [x _ charIndex > line size ifTrue: [pair min] ifFalse: [pair max]] ifFalse: [x _ charIndex > line size ifTrue: [pair max] ifFalse: [pair min]]. p _ self offsetForLine: lineIndex. aCanvas fillRectangle: (p + (x@0) extent: 2@lineHeight) color: selectionColor. ! ! !ScrollingStringMorph methodsFor: 'drawing' stamp: 'jm 8/8/2008 21:20'! drawOn: aCanvas | clipC | super drawOn: aCanvas. lineHeight ifNil: [self font: font]. "draw my background, if I have one" backForm ifNotNil: [ backForm position: self topLeft; extent: self extent. backForm drawOn: aCanvas]. clipC _ aCanvas copyOffset: bounds origin clipRect: (bounds insetBy: borderWidth). hasFocus ifTrue: [self drawSelectionOn: clipC]. self drawTextOn: clipC. ! ! !ScrollingStringMorph methodsFor: 'drawing' stamp: 'jm 8/8/2008 21:11'! drawSelectionFrom: startIndex to: endIndex line: lineIndex on: aCanvas "Draw the selection for the given character range of the given line on the given canvas." | line xRanges p pair w | line _ lines at: lineIndex. xRanges _ renderer xRangesFor: line. p _ self offsetForLine: lineIndex. startIndex to: endIndex - 1 do: [:i | pair _ xRanges at: (i within: 1 and: line size). w _ (pair last - pair first) abs. aCanvas fillRectangle: ((p + (pair first@0)) extent: w@lineHeight) color: selectionColor]. ! ! !ScrollingStringMorph methodsFor: 'drawing' stamp: 'jm 8/8/2008 21:04'! drawSelectionLine: lineIndex on: aCanvas "Draw the selection for the entire line with the given index on the given canvas." | p w | p _ self offsetForLine: lineIndex. w _ renderer stringWidth: (lines at: lineIndex). aCanvas fillRectangle: (p extent: w@lineHeight) color: selectionColor. ! ! !ScrollingStringMorph methodsFor: 'drawing' stamp: 'jm 8/8/2008 21:15'! drawSelectionOn: aCanvas "Draw my selection onto the given canvas. The canvas has been offset to my origin." | start end startLine endLine startIndex endIndex | lines size = 0 ifTrue: [^ self]. self ensureSelectionStartPrecedesEnd. start _ self lineAndIndexFor: selectionStart. end _ self lineAndIndexFor: selectionEnd. startLine _ start first. endLine _ end first. startIndex _ start last. endIndex _ end last. startLine = endLine ifTrue: [ startIndex = endIndex ifTrue: [ blinkState ifTrue: [ self drawCursor: startIndex line: startLine on: aCanvas]] ifFalse: [ self drawSelectionFrom: startIndex to: endIndex line: startLine on: aCanvas]. ^ self]. self drawSelectionFrom: startIndex to: (lines at: startLine) size + 1 line: startLine on: aCanvas. startLine + 1 to: endLine - 1 do: [:i | self drawSelectionLine: i on: aCanvas]. self drawSelectionFrom: 1 to: endIndex line: endLine on: aCanvas. ! ! !ScrollingStringMorph methodsFor: 'drawing' stamp: 'jm 7/19/2008 10:25'! drawString: aString at: aPoint on: aCanvas | s bgColor f | s _ aString. s isUnicode ifTrue: [ bgColor _ color. bgColor isTransparent ifTrue: [bgColor _ self backgroundColor]. f _ ScratchTranslator formFor: s font: self font fgColor: textColor bgColor: bgColor suppressAntiAliasing: false. f ifNotNil: [ "have rendering plugin" aCanvas paintImage: f at: aPoint. ^ self]. "no rendering plugin; convert to MacRoman and render with Squeak" s _ s asMacRoman]. aCanvas text: s bounds: (aPoint extent: 10000@10000) font: font kern:0 color: textColor. ! ! !ScrollingStringMorph methodsFor: 'drawing' stamp: 'jm 8/8/2008 22:08'! drawTextOn: aCanvas | iStart iEnd p | iStart _ firstVisibleLine rounded within: 1 and: lines size. iEnd _ (firstVisibleLine + self visibleLineCount) within: 1 and: lines size. iStart to: iEnd do: [:i | p _ self offsetForLine: i. self drawString: (lines at: i) at: p on: aCanvas]. ! ! !ScrollingStringMorph methodsFor: 'stepping' stamp: 'jm 6/10/2007 21:07'! step layoutNeeded ifTrue: [ self wordWrapAll. layoutNeeded _ false]. blinkState _ blinkState not. selectionStart = selectionEnd ifTrue: [self changed]. ! ! !ScrollingStringMorph methodsFor: 'stepping' stamp: 'jm 12/20/2006 16:14'! stepTime ^ 600 ! ! !ScrollingStringMorph methodsFor: 'event handling' stamp: 'jm 6/29/2007 19:15'! autoScroll: mouseY "Autoscroll if necessary for the given mouse y position." | maxScroll | (mouseY between: self top + 3 and: self bottom - 3) ifTrue: [^ self]. maxScroll _ (lines size - (self visibleLineCount // 3)) max: 1. mouseY < (self top + 3) ifTrue: [firstVisibleLine _ (firstVisibleLine - 1) max: 1]. mouseY > (self bottom - 3) ifTrue: [firstVisibleLine _ (firstVisibleLine + 1) min: maxScroll]. self updateScrollbar. Delay waitMSecs: 10. ! ! !ScrollingStringMorph methodsFor: 'event handling' stamp: 'jm 6/10/2007 21:08'! cursorKey: evt "Handle the given cursor control key." | ch pair lineNum line | ch _ evt keyValue. selectionStart = selectionEnd ifTrue: [startLoc _ selectionStart]. ch = 28 ifTrue: [ "left" evt shiftPressed ifTrue: [ selectionEnd > startLoc ifTrue: [selectionEnd _ selectionEnd - 1] ifFalse: [selectionStart _ selectionStart - 1]] ifFalse: [ selectionStart _ selectionEnd _ selectionStart - 1]]. ch = 29 ifTrue: [ "right" evt shiftPressed ifTrue: [ selectionStart < startLoc ifTrue: [selectionStart _ selectionStart + 1] ifFalse: [selectionEnd _ selectionEnd + 1]] ifFalse: [ selectionStart _ selectionEnd _ selectionStart + 1]]. ch = 30 ifTrue: [ "up" pair _ self lineAndIndexFor: selectionStart. lineNum _ (pair first - 1) max: 1. line _ lines at: lineNum. selectionStart _ (self startOfLine: lineNum) + (pair second min: line size). evt shiftPressed ifFalse: [selectionEnd _ selectionStart]]. ch = 31 ifTrue: [ "down" pair _ self lineAndIndexFor: selectionEnd. lineNum _ (pair first + 1) min: lines size. line _ lines at: lineNum. selectionEnd _ (self startOfLine: lineNum) + (pair second min: line size). evt shiftPressed ifFalse: [selectionStart _ selectionEnd]]. blinkState _ true. self ensureSelectionStartPrecedesEnd. self ensureCursorIsOnScreen. self changed. ! ! !ScrollingStringMorph methodsFor: 'event handling' stamp: 'jm 12/18/2006 12:59'! doubleClick: evt "Select a word." | lineAndIndex lineNum s index wordStart wordEnd lineStart | lines size = 0 ifTrue: [^ self]. lineAndIndex _ self lineAndIndexFor: (self indexForPoint: evt cursorPoint). lineNum _ lineAndIndex first. s _ lines at: lineNum. s size = 0 ifTrue: [^ self]. "empty line" index _ lineAndIndex second. index > s size ifTrue: [index _ s size]. "at end of line; try one char back" (s at: index) isSeparator ifTrue: [^ self]. "on separator; do nothing" wordStart _ index. [(wordStart >= 1) and: [(s at: wordStart) isSeparator not]] whileTrue: [wordStart _ wordStart - 1]. wordEnd _ index. [(wordEnd < s size) and: [(s at: wordEnd) isSeparator not]] whileTrue: [wordEnd _ wordEnd + 1]. (s at: wordEnd) isSeparator ifTrue: [wordEnd _ wordEnd - 1]. lineStart _ self startOfLine: lineNum. selectionStart _ lineStart + (wordStart + 1). selectionEnd _ lineStart + (wordEnd + 1). startLoc _ selectionStart. self changed. ! ! !ScrollingStringMorph methodsFor: 'event handling' stamp: 'jm 7/11/2008 06:57'! handlesMouseDown: evt ^ self isEditable & evt hand toolType isNil ! ! !ScrollingStringMorph methodsFor: 'event handling' stamp: 'jm 5/28/2009 21:22'! keyStroke: evt | ch m | ch _ evt unicodeChar. ch = 0 ifTrue: [ch _ evt keyValue]. evt commandKeyPressed ifTrue: [ch _ ch \\ 32]. "map cmd/alt keys to control keys" (ch = 3) & (evt buttons = 0) ifTrue: [ch _ 13]. "map enter key to cr" ch = 9 ifTrue: [ "tab" (m _ self ownerThatIsA: DialogBoxMorph) ifNotNil: [m tabToNextField: evt]. ^ self]. ch = 27 ifTrue: [ "escape key" (m _ self ownerThatIsA: DialogBoxMorph) ifNotNil: [m escapeKeyPressed: evt]. ^ self]. evt controlKeyPressed | evt commandKeyPressed ifTrue: [ "ctrl (or alt) is pressed" ch = 1 ifTrue: [^ self selectAll]. "ctrl-a" ch = 3 ifTrue: [^ self copySelection]. "ctrl-c" ch = 22 ifTrue: [^ self paste]. "ctrl-v" ch = 24 ifTrue: [^ self cutSelection]]. "ctrl-x" evt shiftPressed ifTrue: [ "shift is pressed" ch = 1 ifTrue: [^ self moveCursorHomeAndSelect: true]. "home" ch = 4 ifTrue: [^ self moveCursorEndAndSelect: true]. "end" ch = 11 ifTrue: [^ self moveCursorPageUpAndSelect: true]. "page up" ch = 12 ifTrue: [^ self moveCursorPageDownAndSelect: true]]. "page down" evt buttons = 0 ifTrue: [ "no modifier keys" ch = 8 ifTrue: [^ self backspaceChar]. "backspace" ch = 127 ifTrue: [^ self deleteNextChar]. "delete" ch = 1 ifTrue: [^ self moveCursorHomeAndSelect: false]. "home" ch = 4 ifTrue: [^ self moveCursorEndAndSelect: false]. "end" ch = 11 ifTrue: [^ self moveCursorPageUpAndSelect: false]. "page up" ch = 12 ifTrue: [^ self moveCursorPageDownAndSelect: false]]."page down" (ch between: 28 and: 31) ifTrue: [^ self cursorKey: evt]. "arrow keys" "not a special character--just insert it" self insertString: (UTF32 with: ch). ! ! !ScrollingStringMorph methodsFor: 'event handling' stamp: 'bss 6/23/2007 11:47'! keyboardFocusChange: aBoolean hasFocus = aBoolean ifFalse: [ self changed]. hasFocus _ aBoolean. ! ! !ScrollingStringMorph methodsFor: 'event handling' stamp: 'jm 6/10/2007 21:27'! mouseDown: evt evt hand newKeyboardFocus: self. startLoc _ self indexForPoint: evt cursorPoint. evt shiftPressed ifTrue: [ startLoc < selectionStart ifTrue: [startLoc _ selectionEnd]. startLoc > selectionEnd ifTrue: [startLoc _ selectionStart]] ifFalse: [selectionStart _ selectionEnd _ startLoc]. evt hand waitForClicksOrDrag: self event: evt. self changed. ! ! !ScrollingStringMorph methodsFor: 'event handling' stamp: 'jm 6/10/2007 20:46'! mouseMove: evt | newLoc | self autoScroll: evt cursorPoint y. newLoc _ self indexForPoint: evt cursorPoint. newLoc < startLoc ifTrue: [selectionStart _ newLoc. selectionEnd _ startLoc] ifFalse: [selectionStart _ startLoc. selectionEnd _ newLoc]. self changed. ! ! !ScrollingStringMorph methodsFor: 'event handling' stamp: 'jm 1/5/2006 10:23'! startDrag: evt "Do nothing." ! ! !ScrollingStringMorph methodsFor: 'event handling' stamp: 'jm 9/16/2005 12:02'! wouldAcceptKeyboardFocus ^ true ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 12/9/2006 20:52'! backspaceChar "If there is a selection, delete it. Otherwise delete the character just before the insertion point (i.e. backspace)." selectionStart = selectionEnd ifFalse: [^ self deleteSelection]. selectionStart > 1 ifTrue: [ selectionEnd _ selectionStart. selectionStart _ selectionStart - 1. self deleteSelection]. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 8/10/2008 20:18'! copySelection "Record the current selection in the clipboard." ScratchTranslator unicodeClipboardPut: self currentSelection. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 7/19/2008 09:23'! currentLine "Answer the logical line containing the insertion point (i.e. all the character between two line breaks, independent of word wrap.)" | start end result | start _ end _ (self lineAndIndexFor: selectionStart) first. [(start > 1) and: [(lines at: start - 1) last ~= CR]] whileTrue: [start _ start - 1]. [(end < lines size) and: [(lines at: end) last ~= CR]] whileTrue: [end _ end + 1]. result _ ''. start to: end do: [:i | result _ result, (lines at: i)]. ^ result! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 8/10/2008 20:17'! currentSelection "Answer the current selection as a string." | line result start end startLineNum endLineNum | self ensureSelectionStartPrecedesEnd. start _ self lineAndIndexFor: selectionStart. end _ self lineAndIndexFor: selectionEnd. startLineNum _ start first. endLineNum _ end first. startLineNum = endLineNum ifTrue: [ line _ lines at: startLineNum. ^ line copyFrom: start second to: end second - 1]. result _ WriteStream on: (UTF32 new: 1000). line _ lines at: startLineNum. result nextPutAll: (line copyFrom: start second to: line size). startLineNum + 1 to: endLineNum - 1 do: [:i | result nextPutAll: (lines at: i)]. line _ lines at: endLineNum. result nextPutAll: (line copyFrom: 1 to: end second - 1). ^ result contents ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 9/17/2005 13:41'! cutSelection "Record the current selection in the clipboard, then delete it." selectionStart = selectionEnd ifTrue: [^ self]. "no selection" self copySelection. self deleteSelection. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 12/9/2006 20:53'! deleteNextChar "If there is a selection, delete it. Otherwise delete the character just after the insertion point (i.e. delete)." selectionStart = selectionEnd ifFalse: [^ self deleteSelection]. selectionEnd _ selectionStart + 1. self deleteSelection. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 6/10/2007 21:23'! deleteSelection "Delete the current selection." | start end startLineNum endLineNum s1 s2 mergedLine remainingLines | self ensureSelectionStartPrecedesEnd. selectionStart = selectionEnd ifTrue: [^ self]. "no selection" start _ self lineAndIndexFor: selectionStart. end _ self lineAndIndexFor: selectionEnd. startLineNum _ start first. endLineNum _ end first. "merge the partial lines before and after the selection" s1 _ lines at: startLineNum. s2 _ lines at: endLineNum. mergedLine _ (s1 copyFrom: 1 to: start second - 1), (s2 copyFrom: end second to: s2 size). "combine with following line, if any" endLineNum < lines size ifTrue: [ mergedLine _ mergedLine, (lines at: endLineNum + 1). remainingLines _ lines copyFrom: endLineNum + 2 to: lines size]. "update lines" lines _ (lines copyFrom: 1 to: startLineNum - 1) copyWith: mergedLine. remainingLines ifNotNil: [lines _ lines, remainingLines]. selectionEnd _ selectionStart. self lineWrapFrom: startLineNum. self ensureCursorIsOnScreen. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 6/10/2007 21:14'! insertString: aString "Insert the given string at selectionStart." | start lineNum line prefix postfix | selectionStart = selectionEnd ifFalse: [self cutSelection]. start _ self lineAndIndexFor: selectionStart. lineNum _ start first. line _ lines at: lineNum. prefix _ line copyFrom: 1 to: start second - 1. postfix _ line copyFrom: start second to: line size. lines at: lineNum put: (prefix, (self replaceTabs: aString), postfix). selectionEnd _ selectionStart _ selectionStart + aString size. self lineWrapFrom: lineNum. self ensureCursorIsOnScreen. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 12/12/2006 19:22'! moveCursorContentsEnd "Position the cursor at the end of my contents (i.e. at very end, not the end of the current line)." selectionStart _ selectionEnd _ self charCount + 1. self changed. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 12/30/2006 17:25'! moveCursorEndAndSelect: selectFlag | lineNum line | lineNum _ (self lineAndIndexFor: selectionStart) first. line _ lines at: lineNum. selectionEnd _ (self startOfLine: lineNum) + line size. lineNum = lines size ifTrue: [selectionEnd _ selectionEnd + 1]. selectFlag ifFalse: [selectionStart _ selectionEnd. self ensureCursorIsOnScreen]. startLoc _ selectionStart. self changed. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 12/18/2006 12:57'! moveCursorHomeAndSelect: selectFlag selectionStart _ (self startOfLine: (self lineAndIndexFor: selectionStart) first) + 1. selectFlag ifFalse: [selectionEnd _ selectionStart]. startLoc _ selectionEnd. self changed. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 12/30/2006 17:25'! moveCursorPageDownAndSelect: selectFlag | i lastLine | i _ (self lineAndIndexFor: selectionStart) second. lastLine _ lines at: lines size. selectionEnd _ (self startOfLine: lines size) + (i min: lastLine size + 1). selectFlag ifFalse: [selectionStart _ selectionEnd. self ensureCursorIsOnScreen]. startLoc _ selectionStart. self changed. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 12/18/2006 13:04'! moveCursorPageUpAndSelect: selectFlag | i | i _ (self lineAndIndexFor: selectionStart) second. selectionStart _ i within: 1 and: (lines at: 1) size. selectFlag ifFalse: [selectionEnd _ selectionStart]. startLoc _ selectionEnd. self changed. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 7/19/2008 09:30'! moveCursorToStartOfNextLine "Position the cursor at start of the next line. Insert a CR if this is the last line and it doesn't end in one." | i | self moveCursorEndAndSelect: false. selectionStart _ selectionEnd _ selectionStart + 1. i _ (self lineAndIndexFor: selectionStart) first. i = lines size ifTrue: [ (((lines at: i) size = 0) or: [(lines at: i) last ~= CR]) ifTrue: [ selectionStart _ selectionEnd _ self charCount + 1. self insertString: (UTF32 with: CR)]]. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 8/10/2008 20:23'! paste "Paste the last cut or copied text over the current selection." | s | s _ ScratchTranslator unicodeClipboard.. s size = 0 ifTrue: [^ self]. self deleteSelection. self insertString: s. ! ! !ScrollingStringMorph methodsFor: 'editing' stamp: 'jm 12/9/2006 20:18'! selectAll "Select all of my text." selectionStart _ 1. selectionEnd _ self charCount + 1. self changed. ! ! !ScrollingStringMorph methodsFor: 'geometry' stamp: 'jm 8/10/2008 20:24'! updateScrollbar "Update my scrollbar's position, percent, and deltas based on my current contents." | visibleLines maxScroll | visibleLines _ self visibleLineCount. scrollbar percentVisible: (visibleLines asFloat / lines size). visibleLines >= lines size ifTrue: [ firstVisibleLine _ 1. self changed]. maxScroll _ (lines size - (self visibleLineCount // 3)) max: 1. maxScroll > 0 ifTrue: [ scrollbar scrollDelta: 1.0 / visibleLines pageDelta: (0.6 * visibleLines) / maxScroll. firstVisibleLine > maxScroll ifTrue: [firstVisibleLine _ maxScroll]. scrollbar value: (firstVisibleLine - 1) / maxScroll] ifFalse: [ scrollbar value: 0]. ! ! !ScrollingStringMorph methodsFor: 'menu' stamp: 'jm 9/16/2005 11:29'! addCustomMenuItems: aCustomMenu hand: aHandMorph super addCustomMenuItems: aCustomMenu hand: aHandMorph. aCustomMenu addLine. aCustomMenu add: 'set font' action: #fontMenu. ! ! !ScrollingStringMorph methodsFor: 'menu' stamp: 'ee 7/3/2008 16:25'! backForm: f backForm _ ImageFrameMorph new initFromForm: f. color _ Color r: (211/255) g: (214/255) b: (216/255). ! ! !ScrollingStringMorph methodsFor: 'menu' stamp: 'ee 7/3/2008 11:20'! extent: aPoint "Adjust the position and size of my scrollbar and compute visibleWidth. Text layout is handled by my step method." super extent: (aPoint max: 120@75). backForm ifNotNil: [scrollbar position: (self right - borderWidth - scrollbar width - 7)@(self top + borderWidth + 8). scrollbar height: self height - (2 * borderWidth) - 14] ifNil: [scrollbar position: (self right - borderWidth - scrollbar width - 3)@(self top + borderWidth + 1). scrollbar height: self height - (2 * borderWidth)]. visibleWidth _ self width - (2 * self textInset x) - 1. scrollbar owner = self ifTrue: [visibleWidth _ visibleWidth - scrollbar width]. layoutNeeded _ true. ! ! !ScrollingStringMorph methodsFor: 'menu' stamp: 'jm 8/8/2008 22:12'! fontMenu | menu fName fSize | menu _ CustomMenu new. StrikeFont fontNames do: [:fn | menu add: fn action: fn]. (fName _ menu startUp) ifNil: [^ self]. menu _ CustomMenu new. (StrikeFont sizesForFontName: fName) do: [:sz | menu add: sz printString action: sz]. (fSize _ menu startUp) ifNil: [^ self]. self font: (StrikeFont fontName: fName size: fSize). self changed. ! ! !ScrollingStringMorph methodsFor: 'object i/o' stamp: 'jm 3/5/2008 12:02'! fieldsVersion ^ 1 ! ! !ScrollingStringMorph methodsFor: 'object i/o' stamp: 'jm 3/5/2008 12:40'! initFieldsFrom: anObjStream version: classVersion | fontSpec showScrollbar | super initFieldsFrom: anObjStream version: classVersion. fontSpec _ anObjStream nextField. fontSpec ifNotNil: [ font _ StrikeFont fontName: fontSpec first size: fontSpec second]. showScrollbar _ anObjStream nextField. self initFieldsNamed: #( firstVisibleLine textColor selectionColor lines ) from: anObjStream. self font: font. self showScrollbar: false. self showScrollbar: showScrollbar. ! ! !ScrollingStringMorph methodsFor: 'object i/o' stamp: 'jm 3/5/2008 12:34'! storeFieldsOn: anObjStream | showScrollbar | showScrollbar _ scrollbar owner = self. self showScrollbar: false. super storeFieldsOn: anObjStream. font ifNil: [anObjStream putField: nil] ifNotNil: [anObjStream putField: (Array with: font name with: font pointSize)]. anObjStream putField: showScrollbar. self storeFieldsNamed: #( firstVisibleLine textColor selectionColor lines ) on: anObjStream. self showScrollbar: showScrollbar. ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 8/10/2008 20:28'! breakLine: lineIndex "Break the given line by moving some of it down to the following line. Answer true if the line was broken, false otherwise. (This may cause the next line to become too long, thus propaging the need to word-wrap.) Try the following strategies, in order: a. break at an embedded if that leaves the line short enough b. break at the last space character before a word that hits the edge c. break a word that hits the edge if there is no space before that word" | line breakIndex i lineUpToBreak lineAfterBreak | line _ lines at: lineIndex. breakIndex _ nil. (self fits: line) ifTrue: [^ false]. "line already fits" (i _ line indexOf: CR) ~= 0 ifTrue: [ (self fits: (line copyFrom: 1 to: i - 1)) ifTrue: [breakIndex _ i]]. breakIndex ifNil: [ i _ line indexOfSeparatorStartingAt: 1. [i <= line size] whileTrue: [ (self fits: (line copyFrom: 1 to: i - 1)) ifTrue: [ breakIndex _ i. i _ line indexOfSeparatorStartingAt: i + 1] ifFalse: [i _ line size + 1]]]. breakIndex ifNil: [ i _ line size. [i > 1 and: [(self fits: (line copyFrom: 1 to: i)) not]] whileTrue: [i _ i - 1]. breakIndex _ i]. lineUpToBreak _ line copyFrom: 1 to: breakIndex. lineAfterBreak _ line copyFrom: breakIndex + 1 to: line size. lineIndex = lines size ifTrue: [lines _ lines copyWith: self emptyLine]. "make sure there is a next line" lines at: lineIndex put: lineUpToBreak. lines at: lineIndex + 1 put: lineAfterBreak, (lines at: lineIndex + 1). ^ true ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 12/8/2006 19:35'! charCount | result | result _ 0. lines do: [:s | result _ result + s size]. ^ result ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 7/19/2008 09:19'! emptyLine ^ UTF32 new ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 11/28/2007 12:47'! ensureCursorIsOnScreen | visibleLines i | visibleLines _ self visibleLineCount. visibleLines >= lines size ifTrue: [^ self updateScrollbar]. i _ (self lineAndIndexFor: selectionStart) first. (i between: firstVisibleLine and: firstVisibleLine + visibleLines - 2) ifTrue: [ ^ self updateScrollbar]. i < firstVisibleLine ifTrue: [firstVisibleLine _ (i - 2) max: 1] ifFalse: [firstVisibleLine _ (i + 1 - visibleLines) max: 1]. self updateScrollbar. self changed. ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 12/17/2006 20:17'! ensureSelectionStartPrecedesEnd "If selectionEnd is before selectionStart, swap them." | lastIndex tmp | lastIndex _ self charCount + 1. selectionStart _ selectionStart within: 1 and: lastIndex. selectionEnd _ selectionEnd within: 1 and: lastIndex. selectionEnd < selectionStart ifTrue: [ tmp _ selectionEnd. selectionEnd _ selectionStart. selectionStart _ tmp]. ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 8/10/2008 20:31'! fitContents "Set my extent to just fit around my contents." | w | w _ 10. lines do: [:s | w _ w max: (self widthWithContents: s)]. self extent: (w@(lines size * lineHeight)) + (9@7). ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 8/10/2008 20:36'! fits: aString "Answer true if the given string fits within my width." | i | i _ aString indexOf: CR. ((i > 0) and: [i < aString size]) ifTrue: [^ false]. "line includes an embedded CR; needs to be broken" aString size > 500 ifTrue: [^ false]. ^ (renderer stringWidth: aString) < visibleWidth ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 8/8/2008 21:57'! indexForPoint: aPoint "Answer the character index for the given point in screen coordinates." "Note: This could be speeded up by doing a binary search for the character index, but it seems fast enough." | y lineNum x lineStart line xRanges pair | lines size = 0 ifTrue: [^ 1]. y _ aPoint y - (self top + self textInset y + 2). lineNum _ ((y // lineHeight) + firstVisibleLine) max: 1. lineNum > lines size ifTrue: [^ (self startOfLine: lineNum) + 1]. x _ (aPoint x - self left - (self offsetForLine: lineNum) x) min: self width. x < 0 ifTrue: [ "start of a line" lineNum = 1 ifTrue: [^ 1] ifFalse: [^ self startOfLine: lineNum]]. "search for character index" lineStart _ self startOfLine: lineNum. line _ lines at: lineNum. xRanges _ renderer xRangesFor: line. 1 to: line size do: [:i | pair _ xRanges at: i. (x between: pair first and: pair second) ifTrue: [^ lineStart + i]]. "end of line" lineNum = lines size ifTrue: [^ lineStart + line size + 1] ifFalse: [^ lineStart + line size]. ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 12/8/2006 11:33'! lineAndIndexFor: charPos "Answer an Array containing the line number and the character index within that line for the given character position in my text." | count line | charPos < 1 ifTrue: [^ Array with: 1 with: 1]. count _ 0. 1 to: lines size do: [:lineNum | line _ lines at: lineNum. (charPos between: count + 1 and: count + line size) ifTrue: [^ Array with: lineNum with: charPos - count]. count _ count + line size]. ^ Array with: lines size with: ((lines at: lines size) size + 1) ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 7/19/2008 09:25'! lineWrapFrom: startLine "Fix line wrapping starting at the given line." | lineIndex | lineIndex _ startLine. [lineIndex <= lines size and: [self breakLine: lineIndex]] whileTrue: [lineIndex _ lineIndex + 1]. "if the last line ends with a CR, add a blank line after it" (lines last size > 0 and: [lines last last = CR]) ifTrue: [lines _ lines copyWith: self emptyLine]. self changed. ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 8/8/2008 22:09'! offsetForLine: lineIndex "Answer the offset from my origin to the start of the line with the given index, taking RTL into account." | top w | top _ self textInset y + (lineHeight * (lineIndex - firstVisibleLine)). ScratchTranslator isRTL ifTrue: [ w _ renderer stringWidth: (lines at: lineIndex). ^ (self width - (self textInset x + w + scrollbar width)) @ top] ifFalse: [^ self textInset x @ top]. ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 7/19/2008 09:27'! replaceTabs: aString "Answer a copy of the given UTF32 with each tab replaced by four spaces and all non-printing characters except spaces and line ends removed. Covert LF's to CR's." | s tab lf cr space result | s _ aString asUTF32. tab _ 9. lf _ 10. cr _ 13. space _ 32. result _ WriteStream on: (UTF32 new: s size * 4). s do: [:ch | ch < space ifTrue: [ "non-printing" ch = tab ifTrue: [result nextPutAll: ' ' asUTF32]. (ch = cr) | (ch = lf) ifTrue: [result nextPut: cr]] ifFalse: [ result nextPut: ch]]. ^ result contents ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 7/25/2006 10:07'! setLines: anArray "Private!! Used when copying." lines _ anArray. ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 12/15/2006 13:43'! startOfLine: lineIndex "Answer the character index for the start of the line with the given index. If the line is out of range treat it as if it were the first or last line." | result | lineIndex < 1 ifTrue: [^ 1]. result _ 0. 1 to: ((lineIndex - 1) min: lines size) do: [:i | result _ result + (lines at: i) size]. ^ result ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'ee 7/3/2008 16:25'! textInset backForm ifNotNil: [^ (13@8) + borderWidth] ifNil: [^ (10@8) + borderWidth]. ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 8/8/2008 19:56'! visibleLineCount "Answer the number of full lines that are visible. If less than a full line is visible, answer 1." ^ ((self height - self textInset x) // lineHeight) max: 1! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 8/10/2008 20:34'! widthWithContents: aString "Answer how wide I would need to be to fit the given string on a single line." ^ (renderer stringWidth: aString) + self textInset x + borderWidth + scrollbar width + 3 ! ! !ScrollingStringMorph methodsFor: 'private' stamp: 'jm 6/6/2007 23:12'! wordWrapAll "Redo my wordwrap." self contents: self contents. self lineWrapFrom: 1. self updateScrollbar. ! ! !ScrollingStringMorph class methodsFor: 'class initialization' stamp: 'jm 7/19/2008 09:22'! initialize "self initialize" CR _ 13. ! ! !ScrollingStringMorph class methodsFor: 'instance creation' stamp: 'jm 9/16/2005 10:18'! includeInNewMorphMenu ^ true ! ! !SelectionMenu methodsFor: 'accessing' stamp: 'sma 5/28/2000 11:38'! selections ^ selections! ! !SelectionMenu methodsFor: 'accessing' stamp: 'sma 5/28/2000 11:38'! selections: selectionArray selections _ selectionArray! ! !SelectionMenu methodsFor: 'basic control sequence' stamp: 'sma 5/28/2000 15:28'! invokeOn: targetObject "Pop up this menu and return the result of sending to the target object the selector corresponding to the menu item selected by the user. Return nil if no item is selected." | sel | sel _ self startUp. sel = nil ifFalse: [^ targetObject perform: sel]. ^ nil "Example: (SelectionMenu labels: 'sin cos neg' lines: #() selections: #(sin cos negated)) invokeOn: 0.7"! ! !SelectionMenu methodsFor: 'basic control sequence' stamp: 'jm 2/13/2009 15:35'! invokeOn: targetObject at: aPoint "Pop up this menu at the given point and return the result of sending to the target object the selector corresponding to the menu item selected by the user. Return nil if no item is selected." | sel | sel _ self startUp: nil withCaption: nil at: aPoint. sel ifNotNil: [^ targetObject perform: sel]. ^ nil ! ! !SelectionMenu methodsFor: 'basic control sequence' stamp: 'sma 5/28/2000 15:28'! startUpWithCaption: captionOrNil at: location "Overridden to return value returned by manageMarker." | index | index _ super startUpWithCaption: captionOrNil at: location. (selections = nil or: [(index between: 1 and: selections size) not]) ifTrue: [^ nil]. ^ selections at: index! ! !SelectionMenu class methodsFor: 'instance creation' stamp: 'sw 11/8/1999 17:52'! fromArray: anArray "Construct a menu from anArray. The elements of anArray must be either: * A pair of the form:
') ifTrue: [ buf _ ln copyFrom: 5 to: ln size. [lineStream atEnd | (buf endsWith: '