CS456 Widget Set
Program - Widget Set
goal
To use the set of mechanisms that we have implemented so far to create a simple set of button, scrollbar and text widgets.
Application
Modify your application from the Selection program so that your panel component now uses the Interactable interface in addition to Selectable.
Interactable
You should create a Java interface called Interactable that contains the methods:
  • boolean mouseDown( double x, double y, AffineTransform myTransform) This is called whenever a mouse down event is received. It returns true if the object handled this event, false otherwise.
  • boolean mouseMove( double x, double y, AffineTransform myTransform) This is called whenever a mouse move event is received. It returns true if the object handled this event, false otherwise.
  • boolean mouseUp( double x, double y, AffineTransform myTransform) This is called whenever a mouse up event is received. It returns true if the object handled this event, false otherwise.
  • boolean key( char key ) This is called whenever a keyboard event is received. It returns true if the object handled this event, false otherwise.
  • Root getPanel() If the object is Root then it returns itself. Otherwise this method is called on the parent().
Root
The Root object is the top object in the tree and is Drawable and Interactable. It mostly behaves like a Group. It has two additional methods:
  • void setKeyFocus(Interactable focus) When this method is called on a Root object it saves the pointer to the focus object, but not as a SPARK attribute. Whenever the Root receives a key() event it will call key() on the focus object if there is one.
  • void releaseKeyFocus() Sets the key focus to null.
It also has the additional attribute of "model" that is any Spark data structure. This is the model that your widgets will manipulate.

Example:

	Root{ contents:[ all the presentation objects ],
		model:Employee{ name:"George", address:"1414 Mockingbird Lane", age:42 }
		}
		
For purposes of being able to see the effects of the various widgets, this object should write its model attribute to the console after processing every mouseUp() event. This will make it easy to see the impact of the various widgets on the model.
Interactable objects
All of your Selectable objects from the previous assignment should now implement Interactable as well. For many of these such as Line and Ellipse their Interactable methods will just return false.

The objects should respond to the Interactable methods in the following ways:

  • Group{ contents:[ ... ], sx:1.0, sy:1.0, rotate:0.0, tx:0.0, ty:0.0 } This should forward each of the mouse events to the objects in "contents" in front to back order. If any of them return true, stop forwarding the event to the remaining children and return true. Otherwise return false. This object should ignore key() events.
  • Button{ label:"my label", contents:[...],state:"idle", idle:{r:0,g:0,b:0}, hover:{r:100,g:100,b:100}, active:{r:255,g:255,b:0}, model:[...], value:10 } This behaves like a group. Each of the idle, hover, and active attributes contains a color. Some of the objects in the contents should have the attribute 'class:"active"'. When the button enters the idle state set all 'class:"active"' objects should be changed to have the background color of idle. Do similarly when the button enters the hover and active states. This will cause your button to react visually to the mouse.

    The state logic is:

    • idle if none of the objects in the contents list are selected by the current mouse event.
    • hover if one of the objects in the contents list is selected and the mouse button is not pressed.
    • active if one of the objects in the contents list is selected and the mouse button is pressed.
    If contents has a Text object that has an attribute 'class:"label"' then replace the text attribute of that object with the label attribute of the button.

    If a mouseUp() occurs while the button is in the active state then the model attribute should be used as a path from the Root's model to identify an attribute to be changed. The model attribute should be changed to the value of the Button's value attribute.

    Example:

    		Button{ label:"Hit", 
    			contents:[
    					Ellipse{ class:"active", left:100, top:200, width:100, height:50 }
    					Ellipse{ left:110, top:210, width:80, height:30,
    							fill:{r:255, g:255, b:255 }
    						},
    					Text{ class:"label", x:120, y:230, font:"sans-serif", size:15}
    				],
    			state:"idle"
    		}
    				
    The Text object will have its text attribute replaced by "Hit" and the color of the first Ellipse object will change in response to the mouse.
  • ScrollV{ state:"idle",contents:[...], idle:{r:0,g:0,b:0}, hover:{r:100,g:100,b:100}, active:{r:255,g:255,b:0}, model:[...], max:1.0, min:0.0, step:0.1} This is a vertical scroll bar. It's three states work the same as with button.

    Within contents there should be at least one object 'class:"up"'. If a mouseUp() event occurs while such an object is selected then the current value should be changed by value+step, provided it does not exceed max.

    Within contents there should be at least one object 'class:"down"'. If a mouseUp() event occurs while such an object is selected then the current value should be changed by value-step, provided it does not go below min.

    Within contents there should be at least one object 'class:"slide"'. If a mouseUp() event occurs while such an object is selected then it is saved and the slider becomes active. mouseMove() events while the mouse button is down should move the slider vertically but not horizontally. The location of the slider should change the current value so that when the slider is at the top, the value is max, and when the slider is at the bottom its value is min. There should also be an object with 'class:"range"'. This is generally a Rect or Line. This object defines the range of movement for the slide object.

    The model attribute contains a path from the Root object's model to the value controlled by this slider. Whenever the current value of the slider is changes, the model attribute and the Root object should be used to find the correct location in the model and change it to the new value. Whenever the model value changes the ScrollV object should change the location of the slider.

    Example:

    		ScrollV{ state:"idle", idle:{r:0,g:0,b:0}, hover:{r:100,g:100,b:100}, 
    			active:{r:255,g:255,b:0}, model:["age"], 
    			max:100, min:0, step:1,
    			contents:[
    					Rect{ class:"active", left:10,top:10, width:20, height:190, fill:{r:255,g:255,b:255}},
    					Rect{ class:"range", left:10, top:32, width:20, height:136,
    						fill:{r:200,g:200,b:200} },
    					Polygon{ class:"up", points:[{x:12,y:30},{x:20,y:12},{x:28,y:30}],
    						fill:{r:0,g:0,b:0} },
    					Polygon{ class:"down", points:[{x:12,y:170},{x:20,y:198},{x:28,y:170}],
    						fill:{r:0,g:0,b:0} },
    					Rect{ class:"slide", left:12, top:100, width:26, height:30,fill:{r:0,g:0,b:0} }
    				]
    			}
    				
  • ScrollH{ state:"idle", contents:[...],idle:{r:0,g:0,b:0}, hover:{r:100,g:100,b:100}, active:{r:255,g:255,b:0}, model:[...], max:1.0, min:0.0, step:0.1} Works the same as ScrollV except that the slider moves horizontally with max on the right.
  • Text{ ... same as before ..., edit:true, cursor:0} This is the same text object that you had before except that we added the "edit" and "cursor" attributes. If edit=false, then this behaves the same as the old Text object. If edit=true and select() is called and succeeds, then the cursor attribute is set to the character position where typing insertions should occur. When cursor is set to any number greater than or equal to zero, the cursor is painted at the insertion point and the Root object has its key focus set to this object.

    On key() events any normal events should add a character to text at the point in the string indicated by "cursor" and "cursor" should be incremented. If the key is backspace then the preceding character should be removed from the text string and cursor decremented.

  • TextBox{ state:"idle", contents:[...], idle:{r:0,g:0,b:0}, hover:{r:100,g:100,b:100}, active:{r:255,g:255,b:0}, model:[...] } There should be a Text object somewhere in the active list that has the attribute 'class:"content"'. Whenever this object's text attribute is changed, the model value indicated by the model: path should be changed to match. Whenever the object indicated by the model: path changes then the text attribute should be changed to match.

    Example:

    		TextBox{ state:"idle", idle:{r:0,g:0,b:0}, hover:{r:100,g:100,b:100}, 
    			active:{r:255,g:255,b:0}, model:["name"], edit:true, cursor:-1,
    			contents: [
    					Rect{ class:"active", x:10,y:300, width:200, height:30 },
    					Text{ class:"content", x:20,y:320,font:"serif", size:15 }
    				]
    			}
    				
Previous objects
All of your Selectable objects from the previous assignment except for Group should implement Interactable by returning false from each of the methods.
hint
Set up a listening mechanism so that your widget objects can listen to various model settings. It must be possible for multiple widgets to reference the same model values. One one changes the model all other widgets watching that same model value must update.
grading
__ 2) Button correctly responds to hover and active.
__ 2) ScrollV correctly sets its slider to the value in the model
__ 2) TextBox correctly updates its contents when the model value changes.
__ 2) Button correctly changes the model value.
__ 2) ScrollV can step the model value up and down correctly within max/min
__ 2) Dragging the slider in ScrollV will correctly change the model value.
__ 2) ScrollH can do everything that ScrollV can do.
__ 2) Clicking in a TextBox will correctly display the cursor
__ 2) Typing in a TextBox will correctly change the text, including backspace.
__ 2) ScrollV correctly responds to hover and active states
test 1
Make sure that whenever the button is pressed, the text box contents get reset to the button's value. Make sure the text box edits correctly and initially has the correct model contents.
Root{
	contents:[
		Button{ label:"20", state:"idle", idle:{r:255,g:255,b:255}, hover:{r:200,g:200,b:255},
			active:{r:255,g:255,b:200}, model:["location", "street"], value:"Farmington",
			contents:[
				Rect{ class:"active", left:10, top:10, width:300, height:200, thickness:2, 
					border:{r:0,g:0,b:100}
				},
				Text{ class:"label", text:"wrong", x:120,y:250, font:"sans-serif",size:30 }
			]
		},

		TextBox{state:"idle", idle:{r:200,g:200,b:200}, hover:{r:200,g:200,b:255},
			active:{r:255,g:255,b:200}, model:["location","street"],
			contents:[
				Rect{ class:"active",left:100, top:400, width:400, height:200, fill:{r:255,g:255,b:255}},
				Text{ edit:true, class:"content", text:"wrong", x:120,y:550, font:"serif",size:10 }
			]
		}
	],
	model:{ size:10, location:{ number:144, street:"Inglewood" }, age:55 }
}
test 2
Make sure that all of your widgets get the correct starting model value. Make sure that the two vertical scroll bars watch the same model value and stay synchronized with each other.
Root{
	contents:[
		Button{ label:"20", state:"idle", idle:{r:255,g:255,b:255}, hover:{r:200,g:200,b:255},
			active:{r:255,g:255,b:200}, model:["size"], value:20,
			contents:[
				Rect{ class:"active", left:10, top:10, width:300, height:200, thickness:2, 
					border:{r:0,g:0,b:100}
				},
				Text{ class:"label", text:"wrong", x:120,y:250, font:"sans-serif",size:30 }
			]
		},
		ScrollV{ state:"idle", idle:{r:200,g:200,b:200}, hover:{r:200,g:200,b:255},
			active:{r:255,g:255,b:200}, model:["size"], max:50, min:0, step:1,model:["size"],
			contents:[
				Rect{ class:"active",left:600, top:100, width:100, height:600, fill:{r:255,g:255,b:255}},
				Rect{ class:"range",left:600, top:200, width:100, height:400, thickness:1,
					border:{r:0,g:0,b:0}},
				Rect{ class:"up", left:600,top:100,width:100,height:100,fill:{r:125,g:125,b:125} },
				Rect{ class:"down", left:600,top:600,width:100,height:100,fill:{r:125,g:125,b:125} },
				Rect{ class:"slide", left:600,top:300,width:100,height:100,fill:{r:125,g:125,b:125} }
			]
		},
		ScrollV{ state:"idle", idle:{r:200,g:200,b:200}, hover:{r:200,g:200,b:255},
			active:{r:255,g:255,b:200}, model:["size"], max:50, min:0, step:1,model:["size"],
			contents:[
				Rect{ class:"active",left:800, top:100, width:100, height:600, fill:{r:255,g:255,b:255}},
				Rect{ class:"range",left:800, top:200, width:100, height:400, thickness:1,
					border:{r:0,g:0,b:0}},
				Rect{ class:"up", left:800,top:100,width:100,height:100,fill:{r:125,g:125,b:125} },
				Rect{ class:"down", left:800,top:600,width:100,height:100,fill:{r:125,g:125,b:125} },
				Rect{ class:"slide", left:800,top:300,width:100,height:100,fill:{r:125,g:125,b:125} }
			]
		},
		ScrollH{state:"idle", idle:{r:200,g:200,b:200}, hover:{r:200,g:200,b:255},
			active:{r:255,g:255,b:200}, model:["size"], max:100, min:0, step:1, model:["age"],
			contents:[
				Rect{ class:"active",left:100, top:800, width:700, height:100, fill:{r:255,g:255,b:255}},
				Rect{ class:"range",left:200, top:800, width:500, height:100, thickness:1,
					border:{r:0,g:0,b:0}},
				Rect{ class:"up", left:700,top:800,width:100,height:100,fill:{r:125,g:125,b:125} },
				Rect{ class:"down", left:100,top:800,width:100,height:100,fill:{r:125,g:125,b:125} },
				Rect{ class:"slide", left:600,top:800,width:100,height:100,fill:{r:125,g:125,b:125} }
			]
		},

		TextBox{state:"idle", idle:{r:200,g:200,b:200}, hover:{r:200,g:200,b:255},
			active:{r:255,g:255,b:200}, model:["location","street"],
			contents:[
				Rect{ class:"active",left:100, top:400, width:400, height:200, fill:{r:255,g:255,b:255}},
				Text{ edit:true, class:"content", text:"wrong", x:120,y:550, font:"serif",size:10 }
			]
		}
	],
	model:{ size:10, location:{ number:144, street:"Inglewood" }, age:55 }
}