'From Squeak3.7alpha of ''11 September 2003'' [latest update: #5623] on 4 January 2004 at 8:21:31 pm'! "Change Set: FormEditor-fixes Date: 12 December 2003 Author: Boris Gaertner This is a revision of a proposed fix from Dec 6, 2003. This change set contains fixes for the FormEditor, an MVC application that was not properly maintained for a long time. The FormEditor is very old stuff. A detailed description of the editor and its use can be found in 'Adele Goldberg: Smalltalk-80 The Interactive Programming Environment', in chapter 7, pages 120 - 135. "! MouseMenuController subclass: #FormEditor instanceVariableNames: 'form tool grid togglegrid mode previousTool color unNormalizedColor xgridOn ygridOn hasUnsavedChanges ' classVariableNames: 'BitEditKey BlackKey BlockKey ChangeGridsKey CurveKey DarkGrayKey EraseKey FlashCursor GrayKey InKey LightGrayKey LineKey OutKey OverKey RepeatCopyKey ReverseKey SelectKey SingleCopyKey TogglexGridKey ToggleyGridKey UnderKey WhiteKey YellowButtonMenu YellowButtonMessages YgridKey ' poolDictionaries: '' category: 'ST80-Editors'! !FormEditor commentStamp: 'BG 12/5/2003 22:40' prior: 0! I represent the basic editor for creating and modifying Forms. This is intended to be an easy to use general-purpose picture (bitMap) editor. I am a kind of MouseMenuController that creates a yellow button menu for accepting and canceling edits. My instances give up control if the cursor is outside the FormView or if a key on the keyboard is pressed. The form to be edited is stored in instance variable model. The instance variable form references the paint brush.! !BitEditor methodsFor: 'menu messages' stamp: 'BG 12/5/2003 13:53'! getCurrentColor | formExtent form c | c := Color colorFromPixelValue: color depth: Display depth. formExtent _ 30@30" min: 10@ 10//(2+1@2)". "compute this better" form _ Form extent: formExtent depth: Display depth. form borderWidth: 5. form border: form boundingBox width: 4 fillColor: Color white. form fill: form boundingBox fillColor: c. ^form! ! !BitEditor methodsFor: 'menu messages' stamp: 'BG 12/5/2003 13:21'! setColor: aColor "Set the color that the next edited dots of the model to be the argument, aSymbol. aSymbol can be any color changing message understood by a Form, such as white or black." color _ aColor pixelValueForDepth: Display depth. squareForm fillColor: aColor. self changed: #getCurrentColor! ! !BitEditor class methodsFor: 'private' stamp: 'BG 12/4/2003 10:18'! bitEdit: aForm at: magnifiedFormLocation scale: scaleFactor remoteView: remoteView "Create a BitEditor on aForm. That is, aForm is a small image that will change as a result of the BitEditor changing a second and magnified view of me. magnifiedFormLocation is where the magnified form is to be located on the screen. scaleFactor is the amount of magnification. This method implements a scheduled view containing both a small and magnified view of aForm. Upon accept, aForm is updated." | aFormView scaledFormView bitEditor topView extent menuView lowerRightExtent | scaledFormView _ FormHolderView new model: aForm. scaledFormView scaleBy: scaleFactor. bitEditor _ self new. scaledFormView controller: bitEditor. bitEditor setColor: Color black. topView _ ColorSystemView new. remoteView == nil ifTrue: [topView label: 'Bit Editor']. topView borderWidth: 2. topView addSubView: scaledFormView. remoteView == nil ifTrue: "If no remote view, then provide a local view of the form" [aFormView _ FormView new model: scaledFormView workingForm. aFormView controller: NoController new. aForm height < 50 ifTrue: [aFormView borderWidthLeft: 0 right: 2 top: 2 bottom: 2] ifFalse: [aFormView borderWidthLeft: 0 right: 2 top: 2 bottom: 0]. topView addSubView: aFormView below: scaledFormView] ifFalse: "Otherwise, the remote one should view the same form" [remoteView model: scaledFormView workingForm]. lowerRightExtent _ remoteView == nil ifTrue: [(scaledFormView viewport width - aFormView viewport width) @ (aFormView viewport height max: 50)] ifFalse: [scaledFormView viewport width @ 50]. menuView _ self buildColorMenu: lowerRightExtent colorCount: 1. menuView model: bitEditor. menuView borderWidthLeft: 0 right: 0 top: 2 bottom: 0. topView addSubView: menuView align: menuView viewport topRight with: scaledFormView viewport bottomRight. extent _ scaledFormView viewport extent + (0 @ lowerRightExtent y) + (4 @ 4). "+4 for borders" topView minimumSize: extent. topView maximumSize: extent. topView translateBy: magnifiedFormLocation. topView insideColor: Color white. ^topView! ! !BitEditor class methodsFor: 'private' stamp: 'BG 12/5/2003 13:40'! buildColorMenu: extent colorCount: nColors "See BitEditor magnifyWithSmall." | menuView form aSwitchView button formExtent highlightForm color leftOffset | menuView _ FormMenuView new. menuView window: (0@0 corner: extent). formExtent _ 30@30 min: extent//(nColors*2+1@2). "compute this better" leftOffset _ extent x-(nColors*2-1*formExtent x)//2. highlightForm _ Form extent: formExtent. highlightForm borderWidth: 4. 1 to: nColors do: [:index | color _ (nColors = 1 ifTrue: [#(black)] ifFalse: [#(black gray)]) at: index. form _ Form extent: formExtent. form fill: form boundingBox fillColor: (Color perform: color). form borderWidth: 5. form border: form boundingBox width: 4 fillColor: Color white. button _ Button new. aSwitchView _ PluggableButtonView on: button getState: #isOn action: #turnOn label: #getCurrentColor. index = 1 ifTrue: [button onAction: [menuView model setColor: Color fromUser. aSwitchView label: menuView model getCurrentColor; displayView ] ] ifFalse: [button onAction: [menuView model setTransparentColor]]. aSwitchView shortcutCharacter: ((nColors=3 ifTrue: ['xvn'] ifFalse: ['xn']) at: index); label: form; window: (0@0 extent: form extent); translateBy: (((index - 1) * 2 * form width) + leftOffset)@(form height // 2); borderWidth: 1. menuView addSubView: aSwitchView]. ^ menuView ! ! !FormEditor methodsFor: 'editing tools' stamp: 'BG 12/5/2003 23:00'! block "Allow the user to fill a rectangle with the gray tone and mode currently selected." | rectangle originRect | originRect := (Sensor cursorPoint grid: grid) extent: 2 @ 2. rectangle := Cursor corner showWhile: [originRect newRectFrom: [:f | f origin corner: (Sensor cursorPoint grid: grid)]]. rectangle isNil ifFalse: [sensor waitNoButton. Display fill: (rectangle intersect: view insetDisplayBox) rule: mode fillColor: color. hasUnsavedChanges contents: true.]! ! !FormEditor methodsFor: 'editing tools' stamp: 'BG 12/10/2003 16:21'! curve "Conic-section specified by three points designated by: first point--press red button second point--release red button third point--click red button. The resultant curve on the display is displayed according to the current form and mode." | firstPoint secondPoint thirdPoint curve drawForm | "sensor noButtonPressed ifTrue: [^self]." firstPoint _ self cursorPoint. secondPoint _ self rubberBandFrom: firstPoint until: [sensor noButtonPressed]. thirdPoint _ self rubberBandFrom: secondPoint until: [sensor redButtonPressed]. Display depth > 1 ifTrue: [self deleteRubberBandFrom: secondPoint to: thirdPoint. self deleteRubberBandFrom: firstPoint to: secondPoint]. curve _ CurveFitter new. curve firstPoint: firstPoint. curve secondPoint: secondPoint. curve thirdPoint: thirdPoint. drawForm := form asFormOfDepth: Display depth. Display depth > 1 ifTrue: [drawForm mapColor: Color white to: Color transparent; mapColor: Color black to: color]. curve form: drawForm. curve displayOn: Display at: 0 @ 0 clippingBox: view insetDisplayBox rule: (Display depth > 1 ifTrue: [mode ~= Form erase ifTrue: [Form paint] ifFalse: [mode]] ifFalse: [mode]) fillColor: (Display depth = 1 ifTrue: [color] ifFalse: [nil]). sensor waitNoButton. hasUnsavedChanges contents: true.! ! !FormEditor methodsFor: 'editing tools' stamp: 'BG 12/12/2003 15:51'! line "Line is specified by two points from the mouse: first point--press red button; second point--release red button. The resultant line is displayed according to the current form and mode." | firstPoint endPoint drawForm | drawForm := form asFormOfDepth: Display depth. Display depth > 1 ifTrue: [drawForm mapColor: Color white to: Color transparent; mapColor: Color black to: color]. firstPoint _ self cursorPoint. endPoint _ self rubberBandFrom: firstPoint until: [sensor noButtonPressed]. endPoint isNil ifTrue: [^self]. Display depth > 1 ifTrue: [self deleteRubberBandFrom: firstPoint to: endPoint.]. (Line from: firstPoint to: endPoint withForm: drawForm) displayOn: Display at: 0 @ 0 clippingBox: view insetDisplayBox rule: (Display depth > 1 ifTrue: [mode ~= Form erase ifTrue: [Form paint] ifFalse: [mode]] ifFalse: [mode]) fillColor: (Display depth = 1 ifTrue: [color] ifFalse: [nil]). hasUnsavedChanges contents: true.! ! !FormEditor methodsFor: 'editing tools' stamp: 'jm 6/30/1999 15:46'! newSourceForm "Allow the user to define a new source form for the FormEditor. Copying the source form onto the display is the primary graphical operation. Resets the tool to be repeatCopy." | dForm interiorPoint interiorColor | dForm _ Form fromUser: grid. "sourceForm must be only 1 bit deep" interiorPoint _ dForm extent // 2. interiorColor _ dForm colorAt: interiorPoint. form _ (dForm makeBWForm: interiorColor) reverse findShapeAroundSeedBlock: [:f | f pixelValueAt: interiorPoint put: 1]. form _ form trimBordersOfColor: Color white. tool _ previousTool! ! !FormEditor methodsFor: 'editing tools' stamp: 'BG 12/10/2003 15:59'! repeatCopy "As long as the red button is pressed, copy the source form onto the display screen." | drawingWasChanged | drawingWasChanged := false. [sensor redButtonPressed] whileTrue: [(BitBlt current destForm: Display sourceForm: form halftoneForm: color combinationRule: (Display depth > 1 ifTrue: [mode ~= Form erase ifTrue: [Form paint] ifFalse: [mode]] ifFalse: [mode]) destOrigin: self cursorPoint sourceOrigin: 0@0 extent: form extent clipRect: view insetDisplayBox) colorMap: (Bitmap with: 0 with: 16rFFFFFFFF); copyBits. drawingWasChanged := true. ]. drawingWasChanged ifTrue: [hasUnsavedChanges contents: true.]! ! !FormEditor methodsFor: 'editing tools' stamp: 'BG 2/25/2001 21:36'! setColor: aColor "Set the mask (color) to aColor. Hacked to invoke color chooser if not B/W screen. Leaves the tool set in its previous state." self normalizeColor: (unNormalizedColor := Display depth > 1 ifTrue: [Color fromUser] ifFalse: [aColor]). tool _ previousTool! ! !FormEditor methodsFor: 'editing tools' stamp: 'BG 12/10/2003 16:00'! singleCopy "If the red button is clicked, copy the source form onto the display screen." (BitBlt destForm: Display sourceForm: form halftoneForm: color combinationRule: (Display depth > 1 ifTrue: [mode ~= Form erase ifTrue: [Form paint] ifFalse: [mode]] ifFalse: [mode]) destOrigin: self cursorPoint sourceOrigin: 0@0 extent: form extent clipRect: view insetDisplayBox) colorMap: (Bitmap with: 0 with: 16rFFFFFFFF); copyBits. sensor waitNoButton. hasUnsavedChanges contents: true.! ! !FormEditor methodsFor: 'menu messages' stamp: 'BG 12/5/2003 22:59'! accept "The edited information should now be accepted by the view." view updateDisplay. view accept. hasUnsavedChanges contents: false.! ! !FormEditor methodsFor: 'menu messages' stamp: 'BG 12/5/2003 22:59'! cancel "The edited information should be forgotten by the view." view cancel. hasUnsavedChanges contents: false.! ! !FormEditor methodsFor: 'private' stamp: 'BG 12/10/2003 17:02'! deleteRubberBandFrom: startPoint to: endPoint (Line from: startPoint to: endPoint withForm: form) displayOn: Display at: 0 @ 0 clippingBox: view insetDisplayBox rule: Form reverse fillColor: (Display depth = 1 ifTrue: [Color black] ifFalse: [Color gray]).! ! !FormEditor methodsFor: 'private' stamp: 'BG 12/10/2003 16:47'! rubberBandFrom: startPoint until: aBlock | endPoint previousEndPoint | previousEndPoint _ startPoint. [aBlock value] whileFalse: [(endPoint _ self cursorPoint) = previousEndPoint ifFalse: [(Line from: startPoint to: previousEndPoint withForm: form) displayOn: Display at: 0 @ 0 clippingBox: view insetDisplayBox rule: Form reverse fillColor: Color gray. (Line from: startPoint to: endPoint withForm: form) displayOn: Display at: 0 @ 0 clippingBox: view insetDisplayBox rule: Form reverse fillColor: Color gray. previousEndPoint _ endPoint]]. (Line from: startPoint to: previousEndPoint withForm: form) displayOn: Display at: 0 @ 0 clippingBox: view insetDisplayBox rule: Form reverse fillColor: (Display depth = 1 ifTrue: [Color gray] ifFalse: [Color black]). ^endPoint! ! !FormEditor methodsFor: 'private' stamp: 'BG 12/5/2003 22:58'! setVariables tool _ #repeatCopy. previousTool _ tool. grid _ 1 @ 1. togglegrid _ 8 @ 8. xgridOn _ false. ygridOn _ false. mode _ Form over. form _ Form extent: 8 @ 8. form fillBlack. unNormalizedColor _ color _ Color black. hasUnsavedChanges := ValueHolder new contents: false. ! ! !FormEditor methodsFor: 'private' stamp: 'BG 12/12/2003 15:50'! trackFormUntil: aBlock | previousPoint cursorPoint displayForm | previousPoint _ self cursorPoint. displayForm := Form extent: form extent depth: form depth. displayForm copy: (0 @ 0 extent: form extent) from: form to: 0 @ 0 rule: Form over. Display depth > 1 ifTrue: [displayForm reverse]. displayForm displayOn: Display at: previousPoint rule: Form reverse. [aBlock value] whileFalse: [cursorPoint _ self cursorPoint. (FlashCursor or: [cursorPoint ~= previousPoint]) ifTrue: [displayForm displayOn: Display at: previousPoint rule: Form reverse. displayForm displayOn: Display at: cursorPoint rule: Form reverse. previousPoint _ cursorPoint]]. displayForm displayOn: Display at: previousPoint rule: Form reverse. ^previousPoint! ! !FormEditor methodsFor: 'window support' stamp: 'BG 12/5/2003 23:23'! okToChange ^hasUnsavedChanges contents not ifFalse: [PopUpMenu confirm: 'This drawing was not saved.\Is it OK to close this window?' withCRs ] ifTrue: [true] ! ! !FormEditor class methodsFor: 'examples' stamp: 'BG 12/5/2003 22:39'! newForm "Create an instance of me on a new form at a location designated by the user. " (Form extent: 400 @ 200 depth: Display depth) fillWhite; edit "FormEditor newForm"! ! !FormEditor class methodsFor: 'private' stamp: 'BG 12/5/2003 23:18'! createOnForm: aForm "Create a StandardSystemView for a FormEditor on the form aForm." | formView formEditor menuView aView topView extent topViewBorder | topViewBorder _ 2. formView _ FormHolderView new model: aForm. formEditor _ formView controller. menuView _ FormMenuView new makeFormEditorMenu model: formEditor. formEditor model: aForm. aView _ View new. aView model: aForm. aView addSubView: formView. aView addSubView: menuView align: menuView viewport topCenter with: formView viewport bottomCenter + (0@16). aView window: ((formView viewport merge: (menuView viewport expandBy: (16 @ 0 corner: 16@16))) expandBy: (0@topViewBorder corner: 0@0)). topView _ "ColorSystemView" FormEditorView new. topView model: formEditor. topView backgroundColor: #veryLightGray. topView addSubView: aView. topView label: 'Form Editor'. topView borderWidth: topViewBorder. extent _ topView viewport extent. topView minimumSize: extent. topView maximumSize: extent. ^topView! ! !FormMenuView methodsFor: 'private' stamp: 'BG 12/4/2003 14:22'! makeButton: index | buttonCache button | buttonCache _ (FormButtons at: index) shallowCopy. buttonCache form: (FormButtons at: index) form copy. button _ Button newOff. button onAction: [model changeTool: buttonCache value]. self makeViews: buttonCache for: button. ! ! !FormMenuView methodsFor: 'private' stamp: 'BG 12/4/2003 14:23'! makeColorConnections: indexInterval | connector buttonCache button aSwitchView | connector _ Object new. "a dummy model for connecting dependents" indexInterval do: [:index | buttonCache _ (FormButtons at: index) shallowCopy. buttonCache form: (FormButtons at: index) form copy. buttonCache initialState = #true ifTrue: [button _ OneOnSwitch newOn] ifFalse: [button _ OneOnSwitch newOff]. button onAction: [model changeTool: buttonCache value]. button connection: connector. aSwitchView _ self makeViews: buttonCache for: button. aSwitchView borderWidthLeft: 1 right: 0 top: 1 bottom: 1; action: #turnOn]. aSwitchView borderWidth: 1. ! ! !FormMenuView methodsFor: 'private' stamp: 'BG 12/4/2003 14:23'! makeConnections: indexInterval | connector buttonCache button aSwitchView | connector _ Object new. "a dummy model for connecting dependents." indexInterval do: [:index | buttonCache _ (FormButtons at: index) shallowCopy. buttonCache form: (FormButtons at: index) form copy. buttonCache initialState = #true ifTrue: [button _ OneOnSwitch newOn] ifFalse: [button _ OneOnSwitch newOff]. button onAction: [model changeTool: buttonCache value]. button connection: connector. aSwitchView _ self makeViews: buttonCache for: button. aSwitchView borderWidthLeft: 1 right: 0 top: 1 bottom: 1; action: #turnOn]. aSwitchView borderWidth: 1. ! ! !FormMenuView methodsFor: 'private' stamp: 'BG 12/4/2003 15:24'! makeGridSwitch: index | buttonCache button | buttonCache _ FormButtons at: index. buttonCache form: (FormButtons at: index) form copy. buttonCache initialState = #true ifTrue: [button _ Switch newOn] ifFalse: [button _ Switch newOff]. button onAction: [model changeTool: buttonCache value]. button offAction: [model changeTool: buttonCache value]. self makeViews: buttonCache for: button. ! ! !FormMenuView methodsFor: 'private' stamp: 'BG 12/4/2003 14:23'! makeSwitch: index | buttonCache button | buttonCache _ (FormButtons at: index) shallowCopy. buttonCache form: (FormButtons at: index) form copy. buttonCache initialState = #true ifTrue: [button _ Switch newOn] ifFalse: [button _ Switch newOff]. button onAction: [model changeTool: buttonCache value]. self makeViews: buttonCache for: button. ! ! !FormMenuView class methodsFor: 'accessing' stamp: 'BG 12/4/2003 12:11'! formButtons ^FormButtons! ! !FormView class methodsFor: 'examples' stamp: 'BG 12/5/2003 14:45'! open: aForm named: aString "FormView open: ((Form extent: 100@100) borderWidth: 1) named: 'Squeak' " "Open a window whose model is aForm and whose label is aString." | topView aView | topView _ StandardSystemView new. topView model: aForm. topView label: aString. topView minimumSize: aForm extent; maximumSize: aForm extent. aView _ FormView new. aView model: aForm. aView window: (aForm boundingBox expandBy: 2). aView borderWidthLeft: 2 right: 2 top: 2 bottom: 2. topView addSubView: aView. topView controller open! ! !PluggableButtonView methodsFor: 'private' stamp: 'BG 12/4/2003 13:26'! centerAlignLabelWith: aPoint "Align the center of the label with aPoint." | alignPt | alignPt _ label boundingBox center. (label isKindOf: Paragraph) ifTrue: [alignPt _ alignPt + (0@(label textStyle leading))]. (label isKindOf: Form) ifTrue: [label offset: 0 @ 0]. label align: alignPt with: aPoint ! ! !StandardSystemView methodsFor: 'framing' stamp: 'BG 12/4/2003 13:14'! reframeTo: newFrame "Reframe the receiver to the given screen rectangle. Repaint difference after the change. " | oldBox newBox portRect | self uncacheBits. oldBox _ self windowBox. portRect _ newFrame topLeft + self labelOffset corner: newFrame corner. self setWindow: nil. self resizeTo: portRect. self setLabelRegion. newBox _ self windowBox. (oldBox areasOutside: newBox) do: [:rect | ScheduledControllers restore: rect]. self displayEmphasized! ! !StandardSystemView methodsFor: 'clipping box access' stamp: 'BG 12/5/2003 11:13'! constrainFrame: aRectangle "Constrain aRectangle, to the minimum and maximum size for this window" | adjustmentForLabel | adjustmentForLabel := 0 @ (labelFrame height - labelFrame borderWidth). ^ aRectangle origin extent: ((aRectangle extent max: minimumSize + adjustmentForLabel) min: maximumSize + adjustmentForLabel).! ! MouseMenuController subclass: #FormEditor instanceVariableNames: 'form tool grid togglegrid mode previousTool color unNormalizedColor xgridOn ygridOn hasUnsavedChanges' classVariableNames: 'BitEditKey BlackKey BlockKey ChangeGridsKey CurveKey DarkGrayKey EraseKey FlashCursor GrayKey InKey LightGrayKey LineKey OutKey OverKey RepeatCopyKey ReverseKey SelectKey SingleCopyKey TogglexGridKey ToggleyGridKey UnderKey WhiteKey YellowButtonMenu YellowButtonMessages YgridKey' poolDictionaries: '' category: 'ST80-Editors'!