diff --git a/src/Spec2-Adapters-Morphic/LayoutPolicy.extension.st b/src/Spec2-Adapters-Morphic/LayoutPolicy.extension.st new file mode 100644 index 00000000..d7462c6c --- /dev/null +++ b/src/Spec2-Adapters-Morphic/LayoutPolicy.extension.st @@ -0,0 +1,6 @@ +Extension { #name : 'LayoutPolicy' } + +{ #category : '*Spec2-Adapters-Morphic' } +LayoutPolicy >> isFromEnd [ + ^ false +] diff --git a/src/Spec2-Adapters-Morphic/SpMorphicBackend.class.st b/src/Spec2-Adapters-Morphic/SpMorphicBackend.class.st index 2a72f769..ac73d31c 100644 --- a/src/Spec2-Adapters-Morphic/SpMorphicBackend.class.st +++ b/src/Spec2-Adapters-Morphic/SpMorphicBackend.class.st @@ -62,7 +62,7 @@ SpMorphicBackend >> inform: aString [ "If we are not in interactive mode, we log in the stdout. Else we delegate to the Morphic notification" Smalltalk isInteractive - ifTrue: [ GrowlMorph openWithLabel: 'Information' translated contents: aString ] + ifTrue: [ GrowlMorph openWithLabel: 'Information' contents: aString ] ifFalse: [ | logBlock | logBlock := [ :logger | @@ -99,7 +99,7 @@ SpMorphicBackend >> notifyError: aSpecNotification [ SpMorphicBackend >> notifyInfo: aSpecNotification [ GrowlMorph - openWithLabel: 'Information' translated + openWithLabel: 'Information' contents: aSpecNotification message ] diff --git a/src/Spec2-Adapters-Morphic/SpMorphicPanedLayout.class.st b/src/Spec2-Adapters-Morphic/SpMorphicPanedLayout.class.st index 70be4c3f..22ed9f4e 100644 --- a/src/Spec2-Adapters-Morphic/SpMorphicPanedLayout.class.st +++ b/src/Spec2-Adapters-Morphic/SpMorphicPanedLayout.class.st @@ -1,22 +1,21 @@ " -I'm a table layout for PanedLayouts. -I add some posibilities to layouting (like defining the initial position in proportional -numbers instead a fixed number). - -This is needed because sometimes I want to express a position like: - -SpecPanedLayout new - position: 80 percent; - ... - -instead a fixed number I may change in other places. - +I am a specialized TableLayout for PanedLayouts in the Morphic adapter. + +I manage rigid and spaceFill resizing constraints for the first and second child submorphs based on `#position`: +- Fixed values (|position| >= 1): + Positive (e.g. 200): First submorph is rigid with specified width/height; last submorph is spaceFill. + Negative (e.g. -200): Last submorph is rigid with absolute width/height; first submorph is spaceFill. +- Proportional values (|position| < 1): + Submorph sizes are updated proportionally on layout passes. +- Splitter interaction: + When the splitter is dragged, the sizing mode (fixed vs proportional, start vs end) is preserved rather than being forcibly coerced into a percentage. " Class { #name : 'SpMorphicPanedLayout', #superclass : 'TableLayout', #instVars : [ - 'position' + 'position', + 'isResizing' ], #category : 'Spec2-Adapters-Morphic-Layout', #package : 'Spec2-Adapters-Morphic', @@ -24,40 +23,87 @@ Class { } { #category : 'private' } -SpMorphicPanedLayout >> ensureHeightPosition: aMorph in: aRect [ - | firstMorph height | +SpMorphicPanedLayout >> computePreservedPositionOn: layoutedMorph [ + | targetMorph newDimension delta | + targetMorph := self isFromEnd + ifTrue: [ layoutedMorph submorphs last ] + ifFalse: [ layoutedMorph submorphs first ]. + + newDimension := self isHorizontal + ifTrue: [ targetMorph width ] + ifFalse: [ targetMorph height ]. + + self isFixed ifTrue: [ + ^ self isFromEnd + ifTrue: [ newDimension asInteger negated ] + ifFalse: [ newDimension asInteger ] ]. + + delta := self isHorizontal + ifTrue: [ (newDimension * 100 / layoutedMorph width) asInteger percent ] + ifFalse: [ (newDimension * 100 / layoutedMorph height) asInteger percent ]. + + ^ self isFromEnd + ifTrue: [ delta negated ] + ifFalse: [ delta ] +] +{ #category : 'private' } +SpMorphicPanedLayout >> ensureHeightPosition: aMorph in: aRect [ + | targetMorph otherMorph height | self position ifNil: [ ^ self ]. + self isResizing ifTrue: [ ^ self ]. + aMorph submorphs size < 3 ifTrue: [ ^ self ]. + + targetMorph := self isFromEnd + ifTrue: [ aMorph submorphs last ] + ifFalse: [ aMorph submorphs first ]. + otherMorph := self isFromEnd + ifTrue: [ aMorph submorphs first ] + ifFalse: [ aMorph submorphs last ]. + + targetMorph vResizing = #rigid ifFalse: [ targetMorph vResizing: #rigid ]. + otherMorph vResizing = #spaceFill ifFalse: [ otherMorph vResizing: #spaceFill ]. - firstMorph := aMorph submorphs first. - firstMorph vResizing = #rigid - ifFalse: [ firstMorph vResizing: #rigid ]. height := self heightPositionIn: aRect extent. - firstMorph height = height - ifFalse: [ firstMorph height: height ] + targetMorph height = height ifFalse: [ targetMorph height: height ] ] { #category : 'private' } SpMorphicPanedLayout >> ensureWidthPosition: aMorph in: aRect [ - | firstMorph width | - + | targetMorph otherMorph width | self position ifNil: [ ^ self ]. + self isResizing ifTrue: [ ^ self ]. + aMorph submorphs size < 3 ifTrue: [ ^ self ]. + + targetMorph := self isFromEnd + ifTrue: [ aMorph submorphs last ] + ifFalse: [ aMorph submorphs first ]. + otherMorph := self isFromEnd + ifTrue: [ aMorph submorphs first ] + ifFalse: [ aMorph submorphs last ]. + + targetMorph hResizing = #rigid ifFalse: [ targetMorph hResizing: #rigid ]. + otherMorph hResizing = #spaceFill ifFalse: [ otherMorph hResizing: #spaceFill ]. - firstMorph := aMorph submorphs first. - firstMorph hResizing = #rigid - ifFalse: [ firstMorph hResizing: #rigid ]. width := self widthPositionIn: aRect extent. - firstMorph width = width - ifFalse: [ firstMorph width: width ]. - + targetMorph width = width ifFalse: [ targetMorph width: width ] ] { #category : 'private' } -SpMorphicPanedLayout >> heightPositionIn: aPoint [ +SpMorphicPanedLayout >> heightPositionIn: aPoint [ + self position ifNil: [ ^ (aPoint y * 0.5) asInteger ]. + self isFixed ifTrue: [ ^ self position abs asInteger ]. + ^ (aPoint y * self position abs) asInteger +] - ^ self position < 1 - ifTrue: [ (aPoint y * self position) asInteger ] - ifFalse: [ self position ] +{ #category : 'testing' } +SpMorphicPanedLayout >> isFixed [ + ^ self position isNotNil and: [ self position abs >= 1 ] +] + +{ #category : 'testing' } +SpMorphicPanedLayout >> isFromEnd [ + ^ self position isNotNil and: [ self position < 0 ] ] { #category : 'testing' } @@ -67,14 +113,19 @@ SpMorphicPanedLayout >> isHorizontal [ or: [ properties listDirection == #bottomToTop ]) not ] -{ #category : 'private' } +{ #category : 'testing' } +SpMorphicPanedLayout >> isResizing [ + ^ isResizing ifNil: [ false ] +] + +{ #category : 'layout' } SpMorphicPanedLayout >> layoutLeftToRight: aMorph in: newBounds [ self ensureWidthPosition: aMorph in: newBounds. super layoutLeftToRight: aMorph in: newBounds ] -{ #category : 'private' } +{ #category : 'layout' } SpMorphicPanedLayout >> layoutTopToBottom: aMorph in: newBounds [ self ensureHeightPosition: aMorph in: newBounds. @@ -88,29 +139,28 @@ SpMorphicPanedLayout >> position [ { #category : 'accessing' } SpMorphicPanedLayout >> position: anObject [ - position := anObject + position := anObject. + isResizing := false ] -{ #category : 'accessing' } +{ #category : 'actions' } SpMorphicPanedLayout >> preservePositionProportionOn: layoutedMorph [ + isResizing := false. + layoutedMorph submorphs size < 3 ifTrue: [ ^ self ]. - | referenceMorph | - referenceMorph := layoutedMorph submorphs first. - position := self isHorizontal - ifTrue: [ (referenceMorph width * 100 / layoutedMorph width) asInteger percent ] - ifFalse: [ (referenceMorph height * 100 / layoutedMorph height) asInteger percent ] + position := self computePreservedPositionOn: layoutedMorph. + layoutedMorph layoutChanged ] -{ #category : 'accessing' } +{ #category : 'actions' } SpMorphicPanedLayout >> resetPosition [ - - position := nil + "Flag that splitter dragging is active so intermediate relayout passes do not override manual adjustment." + isResizing := true ] { #category : 'private' } SpMorphicPanedLayout >> widthPositionIn: aPoint [ - - ^ self position < 1 - ifTrue: [ (aPoint x * self position) asInteger ] - ifFalse: [ self position ] + self position ifNil: [ ^ (aPoint x * 0.5) asInteger ]. + self isFixed ifTrue: [ ^ self position abs asInteger ]. + ^ (aPoint x * self position abs) asInteger ] diff --git a/src/Spec2-Adapters-Morphic/SpNotebookMorph.class.st b/src/Spec2-Adapters-Morphic/SpNotebookMorph.class.st index ae4a8b52..e6a3bbd6 100644 --- a/src/Spec2-Adapters-Morphic/SpNotebookMorph.class.st +++ b/src/Spec2-Adapters-Morphic/SpNotebookMorph.class.st @@ -104,7 +104,7 @@ SpNotebookMorph >> newCloseButtonFor: aSpNotebookPage [ newCloseControlIn: self for: aSpNotebookPage action: [ aSpNotebookPage removeFromNotebook ] - help: 'Close this page' translated + help: 'Close this page' ] { #category : 'factory' } diff --git a/src/Spec2-Adapters-Morphic/SpPanedResizerHorizontalMorph.class.st b/src/Spec2-Adapters-Morphic/SpPanedResizerHorizontalMorph.class.st index 343cff4e..5198a48b 100644 --- a/src/Spec2-Adapters-Morphic/SpPanedResizerHorizontalMorph.class.st +++ b/src/Spec2-Adapters-Morphic/SpPanedResizerHorizontalMorph.class.st @@ -44,16 +44,21 @@ SpPanedResizerHorizontalMorph >> setLayoutSizing [ { #category : 'private' } SpPanedResizerHorizontalMorph >> setTargetBounds [ - | target | - - target := self target. - target bounds: (target bounds withRight: self left - 1) + self target ifNotNil: [ :t | + t bounds: (t bounds withRight: self left) ]. + self targetAfter ifNotNil: [ :ta | + ta bounds: (self right @ ta top corner: ta bottomRight) ] ] { #category : 'private' } SpPanedResizerHorizontalMorph >> setTargetLayoutSizing [ - - (self target hResizing = #rigid) ifTrue: [ ^ self ]. - self target hResizing: #rigid. - self targetAfter hResizing: #spaceFill. + self isFromEnd + ifTrue: [ + self targetAfter hResizing = #rigid ifTrue: [ ^ self ]. + self targetAfter hResizing: #rigid. + self target hResizing: #spaceFill ] + ifFalse: [ + self target hResizing = #rigid ifTrue: [ ^ self ]. + self target hResizing: #rigid. + self targetAfter hResizing: #spaceFill ] ] diff --git a/src/Spec2-Adapters-Morphic/SpPanedResizerMorph.class.st b/src/Spec2-Adapters-Morphic/SpPanedResizerMorph.class.st index 8b2e8688..764570b6 100644 --- a/src/Spec2-Adapters-Morphic/SpPanedResizerMorph.class.st +++ b/src/Spec2-Adapters-Morphic/SpPanedResizerMorph.class.st @@ -129,6 +129,13 @@ SpPanedResizerMorph >> initialize [ extent: self defaultWidth @ self defaultHeight ] +{ #category : 'testing' } +SpPanedResizerMorph >> isFromEnd [ + ^ self owner + ifNotNil: [ :o | o layoutPolicy ifNotNil: [ :lp | lp isFromEnd ] ] + ifNil: [ false ] +] + { #category : 'testing' } SpPanedResizerMorph >> isHorizontal [ "Answer true if the receiver has a horizontal layout." diff --git a/src/Spec2-Adapters-Morphic/SpPanedResizerVerticalMorph.class.st b/src/Spec2-Adapters-Morphic/SpPanedResizerVerticalMorph.class.st index 8e40c7e0..e1377853 100644 --- a/src/Spec2-Adapters-Morphic/SpPanedResizerVerticalMorph.class.st +++ b/src/Spec2-Adapters-Morphic/SpPanedResizerVerticalMorph.class.st @@ -43,16 +43,21 @@ SpPanedResizerVerticalMorph >> setLayoutSizing [ { #category : 'private' } SpPanedResizerVerticalMorph >> setTargetBounds [ - - | target | - target := self target. - target bounds: (target position corner: (target right @ (self top - 1))) + self target ifNotNil: [ :t | + t bounds: (t position corner: (t right @ self top)) ]. + self targetAfter ifNotNil: [ :ta | + ta bounds: (ta left @ self bottom corner: ta bottomRight) ] ] { #category : 'private' } SpPanedResizerVerticalMorph >> setTargetLayoutSizing [ - - (self target vResizing = #rigid) ifTrue: [ ^ self ]. - self target vResizing: #rigid. - self targetAfter vResizing: #spaceFill + self isFromEnd + ifTrue: [ + self targetAfter vResizing = #rigid ifTrue: [ ^ self ]. + self targetAfter vResizing: #rigid. + self target vResizing: #spaceFill ] + ifFalse: [ + self target vResizing = #rigid ifTrue: [ ^ self ]. + self target vResizing: #rigid. + self targetAfter vResizing: #spaceFill ] ] diff --git a/src/Spec2-Backend-Tests/SpAbstractListAdapterSingleSelectionTest.class.st b/src/Spec2-Backend-Tests/SpAbstractListAdapterSingleSelectionTest.class.st index 5fdc6179..1617344f 100644 --- a/src/Spec2-Backend-Tests/SpAbstractListAdapterSingleSelectionTest.class.st +++ b/src/Spec2-Backend-Tests/SpAbstractListAdapterSingleSelectionTest.class.st @@ -182,5 +182,6 @@ SpAbstractListAdapterSingleSelectionTest >> testUnselectWidgetIndexRaisesSelecti presenter selection whenChangedDo: [ :selection | selectedIndex := selection selectedIndex ]. self adapter selectIndex: 0. + backendForTest waitUntilUIRedrawed. self assert: selectedIndex equals: 0 ] diff --git a/src/Spec2-Backend-Tests/SpPanedLayoutAdapterTest.class.st b/src/Spec2-Backend-Tests/SpPanedLayoutAdapterTest.class.st index 8d323707..75d83a74 100644 --- a/src/Spec2-Backend-Tests/SpPanedLayoutAdapterTest.class.st +++ b/src/Spec2-Backend-Tests/SpPanedLayoutAdapterTest.class.st @@ -80,6 +80,21 @@ SpPanedLayoutAdapterTest >> testAddSecondTwiceSetsChildrenSingleTime [ self assert: self adapter children size equals: 1 ] +{ #category : 'tests' } +SpPanedLayoutAdapterTest >> testBuildWithVariousSliderPositions [ + layout positionOfSlider: -200. + layout first: SpButtonPresenter new. + layout second: SpButtonPresenter new. + self openInstance. + self assert: self adapter children size equals: 2. + + layout positionOfSlider: 300. + self assert: layout positionOfSlider equals: 300. + + layout positionOfSlider: -0.3. + self assert: layout positionOfSlider closeTo: -0.3 +] + { #category : 'tests' } SpPanedLayoutAdapterTest >> testLayoutWithFirstElementIsNotEmpty [ diff --git a/src/Spec2-Backend-Tests/SpTreeAdapterSingleSelectionTest.class.st b/src/Spec2-Backend-Tests/SpTreeAdapterSingleSelectionTest.class.st index 6d848c83..fba08f57 100644 --- a/src/Spec2-Backend-Tests/SpTreeAdapterSingleSelectionTest.class.st +++ b/src/Spec2-Backend-Tests/SpTreeAdapterSingleSelectionTest.class.st @@ -172,5 +172,6 @@ SpTreeAdapterSingleSelectionTest >> testUnselectWidgetIndexRaisesSelectionIndexC presenter selection whenChangedDo: [ :selection | selectedPath := selection ]. self adapter selectPath: #(). + backendForTest waitUntilUIRedrawed. self assertEmpty: selectedPath ] diff --git a/src/Spec2-Backend-Tests/SpTreeTableAdapterSingleSelectionTest.class.st b/src/Spec2-Backend-Tests/SpTreeTableAdapterSingleSelectionTest.class.st index 35147888..843799ee 100644 --- a/src/Spec2-Backend-Tests/SpTreeTableAdapterSingleSelectionTest.class.st +++ b/src/Spec2-Backend-Tests/SpTreeTableAdapterSingleSelectionTest.class.st @@ -191,5 +191,6 @@ SpTreeTableAdapterSingleSelectionTest >> testUnselectWidgetIndexRaisesSelectionI presenter selection whenChangedDo: [ :selection | selectedPath := selection ]. self adapter selectPath: #(). + backendForTest waitUntilUIRedrawed. self assertEmpty: selectedPath ] diff --git a/src/Spec2-Code/SpCodePresenter.class.st b/src/Spec2-Code/SpCodePresenter.class.st index 06eb8517..12dc44d1 100644 --- a/src/Spec2-Code/SpCodePresenter.class.st +++ b/src/Spec2-Code/SpCodePresenter.class.st @@ -331,11 +331,12 @@ SpCodePresenter >> doBrowseSenders [ | variableOrClassName variable | variableOrClassName := self selectedSelector ifNil: [ ^ nil ]. - variable := self lookupEnvironment lookupVar: variableOrClassName. "For global and class variables, sender-of shows intead the using methods" + variable := self lookupEnvironment lookupVar: variableOrClassName. + + "For global and class variables, sender-of shows intead the using methods" (variable isNotNil and: [ variable isGlobalVariable or: [ variable isClassVariable ] ]) ifTrue: [ self systemNavigation openBrowserFor: variable withMethods: variable usingMethods ] - ifFalse: [ - (Smalltalk tools toolNamed: #messageList) browseSendersOfAll: { variableOrClassName } from: self interactionModel behavior ] + ifFalse: [ self systemNavigation browseAllSendersOf: variableOrClassName ] ] { #category : 'private - bindings' } diff --git a/src/Spec2-Examples/SpPanedLayout.extension.st b/src/Spec2-Examples/SpPanedLayout.extension.st index 83d93978..b909b759 100644 --- a/src/Spec2-Examples/SpPanedLayout.extension.st +++ b/src/Spec2-Examples/SpPanedLayout.extension.st @@ -1,5 +1,41 @@ Extension { #name : 'SpPanedLayout' } +{ #category : '*Spec2-Examples' } +SpPanedLayout class >> exampleFixedFromEnd [ + + | presenter text form | + presenter := SpPresenter new. + presenter layout: (SpPanedLayout newLeftToRight + positionOfSlider: -260; + add: (text := presenter newText); + add: (form := presenter newList); + yourself). + + text text: 'Fixed end layout (-260 px). +The right properties panel keeps its fixed width (260 px) when the window is resized. +Dragging the splitter updates the fixed width from the right edge.'. + form items: #( 'Property 1: True' 'Property 2: #auto' 'Property 3: 42' ). + presenter openWithTitle: 'SpPanedLayout - Fixed Right (-260 px)' +] + +{ #category : '*Spec2-Examples' } +SpPanedLayout class >> exampleFixedFromStart [ + + | presenter list text | + presenter := SpPresenter new. + presenter layout: (SpPanedLayout newLeftToRight + positionOfSlider: 220; + add: (list := presenter newList); + add: (text := presenter newText); + yourself). + + list items: #( 'Project Tree' 'Packages' 'Classes' 'Methods' ). + text text: 'Fixed start layout (220 px). +The left list keeps its fixed width when the window is resized. +Dragging the splitter updates the fixed pixel width.'. + presenter openWithTitle: 'SpPanedLayout - Fixed Left (220 px)' +] + { #category : '*Spec2-Examples' } SpPanedLayout class >> exampleNotResizingStartChild [ @@ -19,3 +55,21 @@ SpPanedLayout class >> exampleNotResizingStartChild [ presenter open ] + +{ #category : '*Spec2-Examples' } +SpPanedLayout class >> exampleProportional [ + + | presenter list text | + presenter := SpPresenter new. + presenter layout: (SpPanedLayout newLeftToRight + positionOfSlider: 30 percent; + add: (list := presenter newList); + add: (text := presenter newText); + yourself). + + list items: (1 to: 50). + text text: 'Proportional layout (30% / 70%). +Both panels resize proportionally when the window is resized. +Dragging the splitter keeps proportional behavior.'. + presenter openWithTitle: 'SpPanedLayout - Proportional (30%)' +] diff --git a/src/Spec2-Examples/SpPanedLayoutExamplePage.class.st b/src/Spec2-Examples/SpPanedLayoutExamplePage.class.st new file mode 100644 index 00000000..4065966f --- /dev/null +++ b/src/Spec2-Examples/SpPanedLayoutExamplePage.class.st @@ -0,0 +1,25 @@ +" +I am the demo page descriptor for SpPanedLayoutExamplePresenter in the Spec2 demo browser. +" +Class { + #name : 'SpPanedLayoutExamplePage', + #superclass : 'SpDemoPage', + #category : 'Spec2-Examples-Demo-Layouts', + #package : 'Spec2-Examples', + #tag : 'Demo-Layouts' +} + +{ #category : 'initialization' } +SpPanedLayoutExamplePage class >> pageName [ + ^ 'Paned Layouts' +] + +{ #category : 'initialization' } +SpPanedLayoutExamplePage class >> priority [ + ^ 2150 +] + +{ #category : 'initialization' } +SpPanedLayoutExamplePage >> pageClass [ + ^ SpPanedLayoutExamplePresenter +] diff --git a/src/Spec2-Examples/SpPanedLayoutExamplePresenter.class.st b/src/Spec2-Examples/SpPanedLayoutExamplePresenter.class.st new file mode 100644 index 00000000..335443b4 --- /dev/null +++ b/src/Spec2-Examples/SpPanedLayoutExamplePresenter.class.st @@ -0,0 +1,71 @@ +" +I am an interactive demo presenter showcasing SpPanedLayout capabilities. + +I provide controls to switch between fixed-start (+200 px), fixed-end (-200 px), and proportional (30%, -30%) modes, demonstrating window resizing and splitter dragging behavior. +" +Class { + #name : 'SpPanedLayoutExamplePresenter', + #superclass : 'SpPresenter', + #instVars : [ + 'textPresenter', + 'listPresenter', + 'modeLabel', + 'btnPropStart', + 'btnPropEnd', + 'btnFixStart', + 'btnFixEnd' + ], + #category : 'Spec2-Examples-Demo-Layouts', + #package : 'Spec2-Examples', + #tag : 'Demo-Layouts' +} + +{ #category : 'actions' } +SpPanedLayoutExamplePresenter >> applyLayout: aPosition label: aDescription [ + modeLabel label: 'Current mode: ', aDescription. + self layout: (SpBoxLayout newTopToBottom + add: (SpBoxLayout newLeftToRight + add: btnFixStart; + add: btnFixEnd; + add: btnPropStart; + add: btnPropEnd; + yourself) expand: false; + add: modeLabel expand: false; + add: (SpPanedLayout newLeftToRight + positionOfSlider: aPosition; + add: listPresenter; + add: textPresenter; + yourself); + yourself) +] + +{ #category : 'layout' } +SpPanedLayoutExamplePresenter >> defaultLayout [ + ^ SpBoxLayout newTopToBottom +] + +{ #category : 'initialization' } +SpPanedLayoutExamplePresenter >> initializePresenters [ + textPresenter := self newText + text: 'Resize this window or drag the splitter to observe panel resizing.'; + yourself. + listPresenter := self newList + items: (1 to: 100); + yourself. + modeLabel := self newLabel. + + btnPropStart := self newButton + label: '30% Start'; + action: [ self applyLayout: 30 percent label: 'Proportional 30% from start (0.3)' ]. + btnPropEnd := self newButton + label: '30% End'; + action: [ self applyLayout: -30 percent label: 'Proportional 30% from end (-0.3)' ]. + btnFixStart := self newButton + label: '200px Start'; + action: [ self applyLayout: 200 label: 'Fixed 200px from start (+200)' ]. + btnFixEnd := self newButton + label: '200px End'; + action: [ self applyLayout: -200 label: 'Fixed 200px from end (-200)' ]. + + self applyLayout: 200 label: 'Fixed 200px from start (+200)'. +] diff --git a/src/Spec2-Layout/SpPanedLayout.class.st b/src/Spec2-Layout/SpPanedLayout.class.st index 74c375b7..fe0a2164 100644 --- a/src/Spec2-Layout/SpPanedLayout.class.st +++ b/src/Spec2-Layout/SpPanedLayout.class.st @@ -1,8 +1,13 @@ " -A layout with two **adjustable** panels. -A paned layout manages two and only two children, a first and a second one. +A layout with two adjustable panels separated by a draggable splitter. -From that perspective, a paned layout is like a `SpBoxLayout`: it places childen in vertical or horizontal fashion, but it will add a splitter in between, that user can drag to resize the panel. +A paned layout manages exactly two children (first and second). +The position and sizing behavior of the panes is controlled by `#positionOfSlider:`: + +- Values with absolute magnitude < 1 (e.g. 0.3, 30 percent, -0.2) define proportional splits that resize dynamically when the container is resized. +- Values with absolute magnitude >= 1 (e.g. 200, -200) define fixed pixel sizes that do not resize with the window. +- Positive values anchor the fixed/proportional constraint to the start pane (left or top). +- Negative values anchor the fixed/proportional constraint to the end pane (right or bottom). " Class { #name : 'SpPanedLayout', @@ -145,13 +150,21 @@ SpPanedLayout >> positionOfSlider [ { #category : 'api' } SpPanedLayout >> positionOfSlider: aNumber [ - "Position of the slider. - - `aNumber` can be a fixed number indicating the exact position where the slider will - be placed in the pane. - - `aFloat` can also be a fraction or a percentage, indicating the percentage position - of the slider, e.g. 30 percent (0.3) indicates the slider will be place at 1/3 of the panel." + "Sets the position and sizing mode of the paned splitter: + + - Proportional sizing (|aNumber| < 1): + Positive (e.g. 0.3 or 30 percent): Splitted proportionally from the start (left or top). + Negative (e.g. -0.3 or -30 percent): Splitted proportionally from the end (right or bottom). + Both panes resize proportionally when the container is resized. + + - Fixed sizing (|aNumber| >= 1): + Positive integer (e.g. 200): Fixed offset in pixels from the start (left or top). + The start pane retains its fixed width/height when the window is resized. + Negative integer (e.g. -200): Fixed offset in pixels from the end (right or bottom). + The end pane retains its fixed width/height when the window is resized. - positionOfSlider := aNumber + When the user drags the splitter, the sizing mode (fixed vs proportional, start vs end) is preserved." + positionOfSlider := aNumber ] { #category : 'api' } diff --git a/src/Spec2-Morphic-Backend-Tests/SpMorphicPanedLayoutTest.class.st b/src/Spec2-Morphic-Backend-Tests/SpMorphicPanedLayoutTest.class.st index 35db91fd..43abba71 100644 --- a/src/Spec2-Morphic-Backend-Tests/SpMorphicPanedLayoutTest.class.st +++ b/src/Spec2-Morphic-Backend-Tests/SpMorphicPanedLayoutTest.class.st @@ -17,6 +17,55 @@ SpMorphicPanedLayoutTest >> tearDown [ super tearDown ] +{ #category : 'tests' } +SpMorphicPanedLayoutTest >> testFixedPositionFromBottom [ + | panel m1 m2 layout | + layout := SpMorphicPanedLayout new. + layout position: -150. + panel := PanelMorph new + layoutPolicy: layout; + listDirection: #topToBottom; + bounds: (0@0 extent: 600@800); + addMorphBack: (m1 := Morph new extent: 100@100; hResizing: #spaceFill; vResizing: #spaceFill; yourself); + addMorphBack: SpPanedResizerMorph newVertical; + addMorphBack: (m2 := Morph new extent: 100@100; hResizing: #spaceFill; vResizing: #spaceFill; yourself); + yourself. + panel fullBounds. + + self assert: m2 height equals: 150. + self assert: m2 vResizing equals: #rigid. + self assert: m1 vResizing equals: #spaceFill. + + panel extent: 600@1000. + panel fullBounds. + self assert: m2 height equals: 150. +] + +{ #category : 'tests' } +SpMorphicPanedLayoutTest >> testFixedPositionFromRight [ + | panel m1 m2 layout | + layout := SpMorphicPanedLayout new. + layout position: -200. + panel := PanelMorph new + layoutPolicy: layout; + listDirection: #leftToRight; + bounds: (0@0 extent: 800@600); + addMorphBack: (m1 := Morph new extent: 100@100; hResizing: #spaceFill; vResizing: #spaceFill; yourself); + addMorphBack: SpPanedResizerMorph newHorizontal; + addMorphBack: (m2 := Morph new extent: 100@100; hResizing: #spaceFill; vResizing: #spaceFill; yourself); + yourself. + panel fullBounds. + + self assert: m2 width equals: 200. + self assert: m2 hResizing equals: #rigid. + self assert: m1 hResizing equals: #spaceFill. + + "Resizing window: the right panel must retain its 200 px fixed width" + panel extent: 1000@600. + panel fullBounds. + self assert: m2 width equals: 200 +] + { #category : 'tests' } SpMorphicPanedLayoutTest >> testListTakesAssignedSpace [ @@ -38,6 +87,33 @@ SpMorphicPanedLayoutTest >> testListTakesAssignedSpace [ equals: list adapter widget width ] +{ #category : 'tests' } +SpMorphicPanedLayoutTest >> testPresenterWithFixedPositionFromRight [ + | presenter leftBtn rightBtn | + presenter := SpPresenter new. + presenter application: SpApplication new. + presenter layout: (SpPanedLayout newLeftToRight + positionOfSlider: -200; + first: (leftBtn := presenter newButton label: 'Left'); + second: (rightBtn := presenter newButton label: 'Right'); + yourself). + + [ + presenter open. + presenter window extent: 800@600. + presenter window adapter widget fullBounds. + + self assert: rightBtn adapter widget width equals: 200. + self assert: rightBtn adapter widget hResizing equals: #rigid. + self assert: leftBtn adapter widget hResizing equals: #spaceFill. + + "Resizing window: the right button must retain its 200 px fixed width" + presenter window extent: 1000@600. + presenter window adapter widget fullBounds. + self assert: rightBtn adapter widget width equals: 200. + ] ensure: [ presenter window close ] +] + { #category : 'tests' } SpMorphicPanedLayoutTest >> testReplacePresenter [ @@ -60,3 +136,110 @@ SpMorphicPanedLayoutTest >> testReplacePresenter [ self assert: widget submorphs size equals: 3. "one plus for the slider" self assert: widget submorphs first equals: p3 adapter widget ] + +{ #category : 'tests' } +SpMorphicPanedLayoutTest >> testSplitterDragPreservesFixedPositionFromLeft [ + | panel resizer m1 m2 layout dragDelta startCenter endCenter | + layout := SpMorphicPanedLayout new. + layout position: 200. + panel := PanelMorph new + layoutPolicy: layout; + listDirection: #leftToRight; + bounds: (0@0 extent: 800@600); + addMorphBack: (m1 := Morph new extent: 100@100; hResizing: #spaceFill; vResizing: #spaceFill; yourself); + addMorphBack: (resizer := SpPanedResizerMorph newHorizontal); + addMorphBack: (m2 := Morph new extent: 100@100; hResizing: #spaceFill; vResizing: #spaceFill; yourself); + yourself. + panel fullBounds. + + dragDelta := 50. + startCenter := resizer center. + endCenter := (startCenter x + dragDelta) @ startCenter y. + + resizer mouseDown: (MouseButtonEvent new + setType: #mouseDown + position: startCenter + which: MouseButtonEvent redButton + buttons: MouseButtonEvent redButton + hand: HandMorph new + stamp: Time millisecondClockValue). + + resizer mouseMove: (MouseMoveEvent new + setType: #mouseMove + startPoint: startCenter + endPoint: endCenter + trail: { startCenter. endCenter } + buttons: MouseButtonEvent redButton + hand: HandMorph new + stamp: Time millisecondClockValue). + + resizer mouseUp: (MouseButtonEvent new + setType: #mouseUp + position: endCenter + which: MouseButtonEvent redButton + buttons: 0 + hand: HandMorph new + stamp: Time millisecondClockValue). + + self assert: layout position isInteger. + self assert: layout position equals: 250. + self assert: m1 width equals: 250. + + "Resizing window: panel must retain 250 px and not convert to percentage" + panel extent: 1000@600. + panel fullBounds. + self assert: m1 width equals: 250 +] + +{ #category : 'tests' } +SpMorphicPanedLayoutTest >> testSplitterDragPreservesFixedPositionFromRight [ + | panel resizer m1 m2 layout dragDelta startCenter endCenter | + layout := SpMorphicPanedLayout new. + layout position: -200. + panel := PanelMorph new + layoutPolicy: layout; + listDirection: #leftToRight; + bounds: (0@0 extent: 800@600); + addMorphBack: (m1 := Morph new extent: 100@100; hResizing: #spaceFill; vResizing: #spaceFill; yourself); + addMorphBack: (resizer := SpPanedResizerMorph newHorizontal); + addMorphBack: (m2 := Morph new extent: 100@100; hResizing: #spaceFill; vResizing: #spaceFill; yourself); + yourself. + panel fullBounds. + + dragDelta := -50. "Dragging left expands the right morph m2" + startCenter := resizer center. + endCenter := (startCenter x + dragDelta) @ startCenter y. + + resizer mouseDown: (MouseButtonEvent new + setType: #mouseDown + position: startCenter + which: MouseButtonEvent redButton + buttons: MouseButtonEvent redButton + hand: HandMorph new + stamp: Time millisecondClockValue). + + resizer mouseMove: (MouseMoveEvent new + setType: #mouseMove + startPoint: startCenter + endPoint: endCenter + trail: { startCenter. endCenter } + buttons: MouseButtonEvent redButton + hand: HandMorph new + stamp: Time millisecondClockValue). + + resizer mouseUp: (MouseButtonEvent new + setType: #mouseUp + position: endCenter + which: MouseButtonEvent redButton + buttons: 0 + hand: HandMorph new + stamp: Time millisecondClockValue). + + self assert: layout position isInteger. + self assert: layout position equals: -250. + self assert: m2 width equals: 250. + + panel extent: 1000@600. + panel fullBounds. + self assert: m2 width equals: 250 +] diff --git a/src/Spec2-Morphic-Tests/SpWorldPresenterTest.class.st b/src/Spec2-Morphic-Tests/SpWorldPresenterTest.class.st index 73fe3740..f95926e9 100644 --- a/src/Spec2-Morphic-Tests/SpWorldPresenterTest.class.st +++ b/src/Spec2-Morphic-Tests/SpWorldPresenterTest.class.st @@ -33,44 +33,38 @@ SpWorldPresenterTest >> tearDown [ { #category : 'tests' } SpWorldPresenterTest >> testOpenPresenterInWorldDisplayInFullWorld [ + | buttonPresenter | - - buttonPresenter := (application newPresenter: SpButtonPresenter) - label: 'test'. - + buttonPresenter := (application instantiate: SpButtonPresenter) label: 'test'. + (SpTestWorldPresenter presenter: buttonPresenter) open. - + self currentWorld addMorph: worldMorph. - - self - assert: worldMorph submorphs first submorphs first extent - equals: worldMorph extent - 8 "border" + + self assert: worldMorph submorphs first submorphs first extent equals: worldMorph extent - 8 "border" ] { #category : 'tests' } SpWorldPresenterTest >> testOpenPresenterInWorldDisplayToolbarDoesNotHidePresenter [ + | windowPresenter worldPanel | - windowPresenter := application - newPresenter: SpTestPresenterWithToolbar. - + windowPresenter := application instantiate: SpTestPresenterWithToolbar. + (SpTestWorldPresenter presenter: windowPresenter) open. - + worldPanel := worldMorph submorphs first. self - assert: - (worldPanel submorphs first bounds - intersect: worldPanel submorphs second bounds) - = (0 @ 0 extent: 0 @ 0) + assert: (worldPanel submorphs first bounds intersect: worldPanel submorphs second bounds) = (0 @ 0 extent: 0 @ 0) description: 'toolbar should not intersect with presenter when opening in world' ] { #category : 'tests' } SpWorldPresenterTest >> testOpenPresenterInWorldRemoveExisitingMorphsInWorld [ + | buttonPresenter | worldMorph addMorph: CircleMorph new. - buttonPresenter := (application newPresenter: SpButtonPresenter) - label: 'testOpenPresenterInWorldRemoveExisitingMorphsInWorld'. - + buttonPresenter := (application instantiate: SpButtonPresenter) label: 'testOpenPresenterInWorldRemoveExisitingMorphsInWorld'. + (SpTestWorldPresenter presenter: buttonPresenter) open. self deny: (worldMorph submorphs anySatisfy: [ :morph | morph class = CircleMorph ]). diff --git a/src/Spec2-Morphic/SpRGBSlidersPresenter.class.st b/src/Spec2-Morphic/SpRGBSlidersPresenter.class.st index 7fecf863..03c3ac52 100644 --- a/src/Spec2-Morphic/SpRGBSlidersPresenter.class.st +++ b/src/Spec2-Morphic/SpRGBSlidersPresenter.class.st @@ -56,13 +56,13 @@ SpRGBSlidersPresenter >> initializePresenters [ blueSlider := self instantiate: SpSliderInputPresenter. redSlider min: 0; - max: 255; label: 'Red' translated. + max: 255; label: 'Red'. greenSlider min: 0; - max: 255; label: 'Green' translated. + max: 255; label: 'Green'. blueSlider min: 0; - max: 255; label: 'Blue' translated. + max: 255; label: 'Blue'. self focusOrder add: redSlider; add: greenSlider; diff --git a/src/Spec2-Tests/SpPanedLayoutTest.class.st b/src/Spec2-Tests/SpPanedLayoutTest.class.st index f1cf1db1..15bbeeff 100644 --- a/src/Spec2-Tests/SpPanedLayoutTest.class.st +++ b/src/Spec2-Tests/SpPanedLayoutTest.class.st @@ -62,6 +62,15 @@ SpPanedLayoutTest >> testLayoutWithOneSecondElementIsNotEmpty [ self deny: layout isEmpty ] +{ #category : 'tests' } +SpPanedLayoutTest >> testPositionOfSliderAllowsNegativeValues [ + layout positionOfSlider: -200. + self assert: layout positionOfSlider equals: -200. + + layout positionOfSlider: -30 percent. + self assert: layout positionOfSlider equals: -30 percent +] + { #category : 'running' } SpPanedLayoutTest >> testRemoveFirstElementFromLayoutTakesItOut [