A simpleadditional condition that tests the current element type will take care of it in the mousePressedmethod: public void mousePressedMouseEvent e {// Code to handle mouse button pres
Trang 1}g2D.draw(tempElement.getShape()); // and draw it}
}The change to the mouseReleased()method is exactly the same as for mousePressed(), so go aheadand modify the ifcondition in that method, too The only other change you need is to make the statusbar respond to the TEXTelement type being set To do this you can make a small addition to the defini-tion of the setTypePane()method in the StatusBarclass:
public void setTypePane(int elementType) {
String text; // Text for the type paneswitch(elementType) {
// case labels as before
How It Works
The mouseClicked()handler responds to mouse button 1 being clicked when the element type is TEXT.This method will be called after the mouseReleased()method has been called Within the ifstatementthat determines that the current element is of type TEXT, you create a dialog to receive the text input bycalling the static showInputDialog()in the JOptionPaneclass If the Cancel button is clicked in thedialog, textwill be null, so in this case you do nothing If text is not null, you create an Element.Textobject at the current cursor position containing the text string that was entered in the dialog You then addthis to the model, as long as it’s not null It’s important to reset the startand tempElementmembersback to null; otherwise, subsequent event-handling operations will be confused
Incidentally, although there isn’t a method to detect double-clicks on the mouse button, it’s easy toimplement The getClickCount()method for the MouseEventobject that is passed to
mouseClicked()returns the click count To respond to a double-click, you could write the followingstatements:
if(e.getClickCount() == 2) {
//Response to double-click
}
The other event-handling methods behave as before so far as the geometric elements are concerned, and
do nothing if the element type is TEXT You can try it out
Chapter 20
Trang 2theApp.getWindow().getElementColor(), // The text colornew java.awt.font.TextLayout(text, font, // The bounding rectangleg2D.getFontRenderContext()).getBounds().getBounds()
);
if(tempElement != null) { // If we created onetheApp.getModel().add(tempElement); // add it to the modeltempElement = null; // and reset the field}
g2D.dispose(); // Release context resourcesg2D = null;
start = null;
}The bounding rectangle for the text is produced by the rather fearsome looking expression for the lastargument to the Element.Textconstructor It’s much easier than it looks so let’s take it apart
The TextLayoutconstructor you are using expects three arguments: the text string, the font, and aFontRenderContextobject for the context in which the text is to be displayed You call thegetBounds()method for the TextLayoutobject, which returns a reference to a rectangle of typeRectangle2D Since you want a rectangle of type Rectangleto pass to the Element.Textconstructor,you call the getBounds()method for the Rectangle2Dobject, hence the repetition in the code
Once the element has been created, you just add it to the model, and clean up the variables that youwere using
You must now make sure that the other mouse event handlers do nothing when the current element isTEXT You don’t want the XORmode set when you are just creating text elements, for example A simpleadditional condition that tests the current element type will take care of it in the mousePressed()method:
public void mousePressed(MouseEvent e) {// Code to handle mouse button press
start = e.getPoint(); // Save the cursor position in startif((button1Down = (e.getButton()==MouseEvent.BUTTON1)) &&
(theApp.getWindow().getElementType() != TEXT)) {g2D = (Graphics2D)getGraphics(); // Get graphics contextg2D.setXORMode(getBackground()); // Set XOR mode
} } The ifexpression will be trueonly if button 1 was pressed and the current element type is not TEXT.You can update the mouseDragged()method in a similar way:
public void mouseDragged(MouseEvent e) {last = e.getPoint(); // Save cursor positionif(button1Down && (theApp.getWindow().getElementType() != TEXT)) {
if(tempElement == null) { // Is there an element?tempElement = createElement(start, last); // No, so create one} else {
tempElement.draw(g2D); // Yes – draw to erase it
Extending the GUI
Trang 3Try It Out Testing the TextDialog Class
All you need to do now is recompile Sketcher and run it again To open the text dialog, select the newtoolbar button or the menu item and click in the view where you want the text to appear
You just type the text that you want and click the OK button The text will be displayed starting at thepoint in the view where you clicked the mouse button You can draw text in any of the colors — just likethe geometric elements The application window may look something like that in Figure 20-9 when thetext dialog is displayed
Figure 20-9
A Font Selection Dialog
You don’t really want to be stuck with a 12-point SansSerif font You need to be able to release your ativity so your sketches will astound and delight! A font dialog that pops up in response to a click on asuitable menu item should enable you to change the font for text elements to any of those available onthe system It will also give you a chance to see how you can get at and process the fonts that are avail-able You’ll also learn more about how to add components to a dialog window The first step is to estab-lish what the font dialog will do
cre-You want to be able to choose the font name from those available on the system on which the application
is executing You’ll also want to select the style of the font, whether plain, bold, or italic, as well as thepoint size It would also be nice to see what a font looks like before you decide to use it The dialog willtherefore need to obtain a list of the fonts available and display them in a component It will also need acomponent to allow the point size to be selected and some means for choosing the style for the font
Extending the GUI
Trang 4This is not going to be a wimpy pathetic excuse for a dialog like those you have seen so far This is going
to be a real chunky Java programmer’s dialog You’ll drag in a diversity of components here, just for theexperience, and you’ll be building it step-by-step, as it involves quite a lot of code Just so that you knowwhere you’re headed, the finished dialog is shown in Figure 20-10
Figure 20-10
The component that provides the choice of font in the dialog is a Swing component of type
javax.swing.JListthat can display a list of any type of component Below that is a panel holding aJLabelobject, which displays a sample of the current font The list of font names and the panel beloware displayed in a split pane defined by the JSplitPaneclass Here the pane is split vertically, but aJSplitPaneobject can also hold two panels side by side The point size is displayed in another Swingcomponent called a spinner, which is an object of type javax.swing.JSpinner The choice for the fontstyle options is provided by two radio buttons, and either, neither, or both may be selected Finally, youhave two buttons to close the dialog
You can set the foundations by defining the FontDialogclass with its data members and its constructor,and then build on that
Try It Out A FontDialog Class
The major work will be in the dialog class constructor That will set up all the GUI elements as well asthe necessary listeners to respond to operations with the dialog The dialog object will need to know thatthe SketchFrameobject that represents the Sketcher application window is the parent, so you’ll pass aSketchFramereference to the constructor
Chapter 20
Trang 5Here’s the code for the outline of the FontDialogclass:
// Class to define a dialog to choose a fontimport java.awt.Font;
// Code to create buttons and the button panel
// Code to create the data input panel
// Code to create the font choice and add it to the input panel
// Code to create the font size choice and add it to the input panel
// Code to create the font style checkboxes and add them to the input panel // and then some!
}private Font font; // Currently selected fontprivate int fontStyle; // Font style – Plain,Bold,Italicprivate int fontSize; // Font point size
}You’ll be adding a few more data members shortly, but at least you know you’ll need the three that areshown here The code to initialize the data members within the FontDialogconstructor is easy You caninitialize the fontmember and the associated fontStyleand fontSizemembers from the current fontthat is stored in the application window:
public FontDialog(SketchFrame window) {// Call the base constructor to create a modal dialogsuper(window, “Font Selection”, true);
font = window.getCurrentFont(); // Get the current fontfontStyle = font.getStyle(); // style
fontSize = font.getSize(); // and size// Plus the code for the rest of the constructor
}You call the base class constructor and pass the windowobject to it as the parent The second argument is the title for the dialog, and the third argument determines that the dialog is modal ThegetCurrentFont()method returns the font stored in the windowobject, and you use this to initializethe fontStyleand fontSizemembers; therefore, the first time you open the dialog this will be thedefault setting
Extending the GUI
Trang 6Creating the Font Dialog Buttons
Next you can add the code to the constructor that will create the button panel with the OK and Cancelbuttons You can place the button panel at the bottom of the content pane for the dialog using the defaultBorderLayoutmanager:
public FontDialog(SketchFrame window) {
// Initialization as before
// Create the dialog button panel
JPanel buttonPane = new JPanel(); // Create a panel to hold buttons// Create and add the buttons to the buttonPane
buttonPane.add(ok = createButton(“OK”)); // Add the OK button
buttonPane.add(cancel = createButton(“Cancel”)); // Add the Cancel buttongetContentPane().add(buttonPane, BorderLayout.SOUTH);// Add pane to content pane// Plus the code for the rest of the constructor
}
The buttonPaneobject will have a FlowLayoutmanager by default, so this will take care of positioningthe buttons You add the button pane to the dialog content pane using the BorderLayout.SOUTHspeci-fication to place it at the bottom of the window Because creating each button involves several steps thatare the same for both buttons, you are using a helper method, createButton(), that requires only thebutton label as an argument You can see that you store each button reference in a class field, so youmust add these as members of the FontDialogclass:
private JButton ok; // OK button
private JButton cancel; // Cancel button
You’ll use these fields in the listener for the button events, as you’ll see in a moment
You can code the createButton()method as a member of the FontDialogclass as follows:
JButton createButton(String label) {
JButton button = new JButton(label); // Create the buttonbutton.setPreferredSize(new Dimension(80,20)); // Set the sizebutton.addActionListener(this); // Listener is the dialogreturn button; // Return the button}
You set the preferred size of the button here to ensure that the buttons are all of the same size Withoutthis call, each button would be sized to fit its label, so the dialog would look a bit untidy The listener isthe FontDialogclass object, so the FontDialogclass must implement the ActionListenerinterface,which implies that an actionPerformed()method must be defined in the class:
Trang 7}The getSource()member of the ActionEventobject ereturns a reference to the object that originatedthe event, so you can use this to determine the button for which the method is being called You justcompare the sourceobject (which is holding the reference to the object to which the event applies) tothe OK button object to determine whether it was clicked If it is the OK button, you call the
setCurrentFont()method in the SketchFrameobject that is the parent for this dialog to set the font.You then just hide the dialog so Sketcher can continue This will be the sole action when the Cancel but-ton is selected for the dialog
Of course, you must add the definition of setCurrentFont()to the SketchFrameclass:
// Method to set the current fontpublic void setCurrentFont(Font font) { this.font = font;
}Let’s now get back to the FontDialogconstructor
Adding the Data PaneYou can now add a panel to contain the components that will receive input You’ll be using a JListobject for the font names, a JSpinnerobject for the point size of the font, and two JRadioButtonobjects for selecting the font style You can add the code to create the panel first:
public FontDialog(SketchFrame window) {// Initialization as before
// Button panel code as before
// Code to create the data input panelJPanel dataPane = new JPanel(); // Create the data entry paneldataPane.setBorder(BorderFactory.createCompoundBorder( // Create pane border
BorderFactory.createLineBorder(Color.BLACK),BorderFactory.createEmptyBorder(5, 5, 5, 5)));
GridBagLayout gbLayout = new GridBagLayout(); // Create the layoutdataPane.setLayout(gbLayout); // Set the pane layoutGridBagConstraints constraints = new GridBagConstraints();
// Plus the code for the rest of the constructor
}
Extending the GUI
Trang 8Here you use a GridBagLayoutmanager so you can set constraints for each component that you add tothe dataPanecontainer You also set a black line border for dataPanewith an inset empty border 5 pix-els wide This uses the BorderFactorystatic methods that you have seen before You have many otherpossible layout managers that you could use here BoxLayoutmanagers are very easy to use to lay outcomponents in vertical columns and horizontal rows.
The first component that you’ll add to dataPanewill be a label that prompts for the font selection:public FontDialog(SketchFrame window) {
// Initialization as before
// Button panel code as before
// Set up the data input panel to hold all input components as before
// Code to create the font choice and add it to the input panelJLabel label = new JLabel(“Choose a Font”);
With the fillconstraint set as HORIZONTAL, the components in a row will fill the width of the
dataPanecontainer, but without affecting the height With the width constraint set to REMAINDER, thelabelcomponent will fill the width of the row
You need a few more importstatements in the FontDialogsource file, so add the following statements:import java.awt.Color;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import javax.swing.JLabel;
import javax.swing.BorderFactory;
Implementing the Font List
You’ll add the JListobject that displays the list of fonts next, but you won’t add this directly to thedataPanepanel because the list is likely to be long enough to need scrolling capability The list of fontswill have to be obtained using the GraphicsEnvironmentobject that encapsulates information aboutthe system in which the application is running You’ll recall that you call a staticmethod in theGraphicsEnvironmentclass to get the GraphicsEnvironmentobject Here’s the code to create the list
of font names:
public FontDialog(SketchFrame window) {
// Initialization as before
// Button panel code as before
// Set up the data input panel to hold all input components as before
// Add the font choice prompt label as before
Chapter 20
Trang 9// Code to set up font list choice componentGraphicsEnvironment e = GraphicsEnvironment.getLocalGraphicsEnvironment();String[] fontNames = e.getAvailableFontFamilyNames(); // Get the font namesfontList = new JList(fontNames); // Create list of font namesfontList.setValueIsAdjusting(true); // single event selectionfontList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);// Choose 1 fontfontList.setSelectedValue(font.getFamily(),true);
private JList fontList; // Font listThe fontNamesarray holds Stringobjects, but you can create a JListobject for any kind of object —images, for example You can also create a JListobject by passing a Vector<>object that contains theobjects you want in the list to the constructor It is possible to allow multiple entries from a list to beselected, in which case the selection process may cause multiple events — when you drag the cursor overseveral list items, for example You can make certain that there is only one event for a selection, eventhough multiple items are selected, by calling the setValueIsAdjusting()method with the argumenttrue Calling setSelectionMode()with the argument SINGLE_SELECTIONensures that only one fontname can be selected
You have two possible multiple selections that you can enable for a JListobject Passing the valueSINGLE_INTERVAL_SELECTIONto the setSelectionMode()method allows a series of consecutiveitems to be selected Passing MULTIPLE_SELECTION_INTERVALprovides you with total flexibility andallows any number of items anywhere to be selected The initial selection in the list is set by thesetSelectedValue()call You pass the family name for the current font as the argument specifying theinitial selection There is a complementary method, getSelectedValue(), that you’ll be using in theevent handler
There’s a special kind of listener for JListselection events that is an object of a class type that ments the ListSelectionListenerinterface Since you set the FontDialogobject as the listener forthe list in the call to the addListSelectionListener()method, you had better make sure theFontDialogclass implements the interface:
imple-class FontDialog extends JDialog
implements ActionListener, // For buttons etc
ListSelectionListener { // For list box
Extending the GUI
Trang 10the FontDialogconstructor AJScrollPaneobject creates a pane with scrollbars — either vertical, zontal, or both — as necessary for whatever it contains You set a minimum size for the JScrollPaneobject to limit how small it can be made in the split pane into which you’ll insert it in a moment Notehow easy it is to get the mouse wheel supported for scrolling here You just call the
hori-setWheelScrollingEnabled() method for the scroll pane with the argument as true, and it’s done.The new code that you’ve added requires a few more importstatements:
public FontDialog(SketchFrame window) {// Initialization as before
// Button panel code as before
// Set up the data input panel to hold all input components as before
// Add the font choice prompt label as before
// Set up font list choice component as before
// Panel to display font sampleJPanel display = new JPanel();
fontDisplay = new JLabel(“Sample Size: x X y Y z Z”);
fontDisplay.setPreferredSize(new Dimension(300,100));
display.add(fontDisplay);
// Plus the code for the rest of the constructor
}You create the JPanelobject displayand add the JLabelobject fontDisplayto it Remember, youupdate this object in the valueChanged()handler for selections from the list of font names You’ll also
be updating it when the font size or style is changed The fontDisplayobject just represents some ple text You can choose something different if you like
sam-It’s not strictly necessary here but just for the experience you’ll use a split pane to hold the scroll panecontaining the list, chooseFont, and the displaypanel
Using a Split Pane
AJSplitPaneobject represents a pane with a movable horizontal or vertical split, so that it can holdtwo components The split pane divider can be adjusted by dragging it with the mouse Here’s the code
Trang 11There’s only one method in the ListSelectionListenerinterface, and you can implement it in theFontDialogclass like this:
// List selection listener method
public void valueChanged(ListSelectionEvent e) {
if(!e.getValueIsAdjusting()) {font = new Font((String)fontList.getSelectedValue(), fontStyle, fontSize);fontDisplay.setFont(font);
fontDisplay.repaint();
}}
This method will be called when you select an item in the list You have only one list, so you don’t need
to check which object was the source of the event If you were handling events from several lists, youcould call the getSource()method for the event object that is passed to valueChanged(), and com-pare it with the references to the JListobjects being used
The ListSelectionEventobject that is passed to the valueChanged()method contains records
of the index positions of the list items that changed You can obtain these as a range by calling the
getFirstIndex()method for the event object to get the first in the range, and the getLastIndex()method will return the last in the range You don’t need to worry about any of this in the FontDialogclassbecause you have disallowed multiple selections and you just want the newly selected item in the list
You have to be careful though Since you start out with an item already selected, selecting another fontname from the list will cause two events — one for deselecting the original font name and the other forselecting the new name You make sure that you deal only with the last event by calling the
getValueIsAdjusting()method for the event object in the ifexpression This returns falsefor theevent when all changes due to a selection are complete, and trueif things are still changing when theevent occurred Thus your implementation of the valueChanged()method will do nothing when thegetValueIsAdjusting()method returns true
Once you are sure nothing further is changing, which will be when getValueIsAdjusting()returnsfalse, you retrieve the selected font name from the list by calling its getSelectedValue()method.The item is returned as type Objectso you have to cast it to type Stringbefore using it You create anew Fontobject using the selected family name and the current values for fontStyleand fontSize.You store the new font in the data member fontand also call the setFont()member of a data member,fontDisplay, that you haven’t added to the FontDialogclass yet This will be a JLabelobject dis-playing a sample of the current font After you’ve set the new font, you call repaint()for the labelfontDisplayto get it redrawn
If you allow multiple selections on the list with the SINGLE_SELECTION_INTERVALmethod, you can use the getFirstIndex()and getLastIndex()methods to get the range of index values for the item that may have changed If on the other hand you employ the MULTIPLE_SELECTION_INTERVAL
option, you would need to figure out which items in the range were actually selected You could do this
by calling the getSelectedIndices()method or the getSelectedValues()method for the
list object The first of these returns an array of index values of type intfor the selected items, and the second returns an array of elements of type Objectthat reference the selected items.
AJListobject doesn’t support scrolling directly, but it is scrolling “aware.” To get a scrollable list, onewith scrollbars, you just need to pass the JListobject to the JScrollPaneconstructor, as you have inChapter 20
Trang 12// Button panel code as before
// Set up the data input panel to hold all input components as before
// Add the font choice prompt label as before
// Set up font list choice component as before
// Panel to display font sample as before
//Create a split pane with font choice at the top// and font display at the bottom
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
true,chooseFont,display);
gbLayout.setConstraints(splitPane, constraints); // Split pane constraintsdataPane.add(splitPane); // Add to the data pane// Plus the code for the rest of the constructor
}
You’ll need an importstatement for javax.swing.JSplitPanein FontDialog.java
The constructor does it all The first argument specifies that the pane supports two components, oneabove the other You can probably guess that for side-by-side components you would specify
JSplitPane.HORIZONTAL_SPLIT If the second constructor argument is true, the components areredrawn continuously as the divider is dragged If it is falsethe components are not redrawn until youstop dragging the divider
The third argument is the component to go at the top, or to the left for HORIZONTAL_SPLIT, and thefourth argument is the component to go at the bottom, or to the right, as the case may be
You don’t need to do it here, but you can change the components in a split pane You have four methods
to do this: setLeftComponent()and setRightComponent()for a horizontal arrangement of the panes and setTopComponent()and setBottomComponent()for a vertical arrangement of the
panes You just pass a reference to the component you want to set to whichever method you want to use There are also corresponding get methods to retrieve the components in a split pane You can even
change the orientation by calling the setOrientation()method and passing either
JSplitPane.HORIZONTAL_SPLITor JSplitPane.VERTICAL_SPLITto it.
There is a facility to provide a widget on the divider to collapse and restore either pane You don’t need
it, but if you want to try this here, you can add the following statement after the JSplitPanetor call:
construc-splitPane.setOneTouchExpandable(true);
Calling this method with the argument as falsewill remove the widget
Once you have created the splitPaneobject, you add it to the dataPanepanel with constraints thatmake it fill the full width of the container
Next you can add the font size selection mechanism
Chapter 20
Trang 13Using a SpinnerYou could use another list for this, but to broaden your horizons you’ll use another Swing component, ajavax.swing.JSpinnerobject AJSpinnerobject displays a sequence of numbers or objects and theuser can select any one from the set The spinner displays up and down arrows at the side of the spinnerfor stepping through the list You can also use the keyboard up and down arrow keys for this.
The sequence of choices in a spinner is managed by a javax.swing.SpinnerModelobject There arethree concrete spinner model classes defined in the javax.swingpackage The one you use depends onwhat sort of items you are choosing from:
SpinnerNumberModel A model for a sequence of numbers Numbers are stored internally
and returned as type Number, which is the superclass of the classesencapsulating the primitive numerical types —Integer, Long, Dou-ble, etc Numberis also the superclass of other classes such asBigDecimal, but only the classes corresponding to the primitivetypes are supported
SpinnerListModel A model for a sequence defined by an array of objects of any type, or
by a java.util.List<>object You could use this to use a sequence
of strings as the choices in the spinner
SpinnerDateModel A model for a sequence of dates specified as java.util.Date
Thus the smallest point size that can be chosen is 8, the largest is 24, and the step from 8 onwards is 2
You create a JSpinnerobject by passing a SpinnerModelreference to it For example:
JSpinner spinner = new JSpinner(spinnerModel);
In the font dialog, the spinner model will be of type SpinnerNumberModel, and the constructor you’lluse to create the object expects four arguments: a current value that will be the one displayed initially, aminimum value, a maximum value, and the step size Here’s how you can create that for the font dialog:
public FontDialog(SketchFrame window) {// Initialization as before
// Button panel code as before
Extending the GUI
Trang 14// Set up the data input panel to hold all input components as before
// Add the font choice prompt label as before
// Set up font list choice component as before
// Panel to display font sample as before
// Create a split pane with font choice at the top as before
// Set up the size choice using a spinnerJPanel sizePane = new JPanel(); // Pane for size choiceslabel = new JLabel(“Choose point size”); // Prompt for point sizesizePane.add(label); // Add the promptchooseSize = new JSpinner(new SpinnerNumberModel(fontSize,
pointSizeMin, pointSizeMax, pointSizeStep));chooseSize.addChangeListener(this); sizePane.add(chooseSize);
// Add spinner to panegbLayout.setConstraints(sizePane, constraints); // Set pane constraintsdataPane.add(sizePane); // Add the pane
// Plus the code for the rest of the constructor
import static Constants.SketcherConstants.*;
You again create a panel to contain the spinner and its associated prompt, as it makes the layout easier.The default FlowLayoutin the panel is fine for what you want You had better add a couple more mem-bers to the FontDialogclass to store the references to the chooseSizeand fontDisplayobjects:private JSpinner chooseSize; // Font size options
private JLabel fontDisplay; // Font sample
A spinner generates an event of type ChangeEventwhen an item is selected that will be sent to listeners
of type ChangeListener The listener for our spinner is the FontDialogobject so you need to specifythat it implements the ChangeListenerinterface:
class FontDialog extends JDialog
implements ActionListener, // For buttons etc
ListSelectionListener, // For list boxChangeListener { // For the spinnerThe ChangeListenerinterface defines one method, stateChanged(), which has a parameter of typeChangeEvent You obtain a reference to the source of the event by calling getSource()for the eventobject You then need to cast the reference to the type of the source — in this case, JSpinner For exam-ple, you could code it like this:
public void stateChanged(ChangeEvent e) {
JSpinner source = (JSpinner)e.getSource();
// plus code to deal with the spinner event for source
Chapter 20
Trang 15gbLayout.setConstraints(stylePane, constraints); // Set pane constraintsdataPane.add(stylePane); // Add the pane
FontDialogin a moment Note that you pass the style constant that corresponds to the set state of thebutton to the constructor for the listener
The stylePaneobject presents the buttons using the default FlowLayoutmanager, and this pane isadded as the last row to dataPane The final step is to add the dataPaneobject as the central pane inthe content pane for the dialog The call to pack()lays out the dialog components with their preferredsizes if possible, and the setVisible()call with the argument falsemeans that the dialog is initiallyhidden Since this is a complex dialog, you won’t want to create a new object each time you want to dis-play the font dialog You’ll just call the setVisible()method for the dialog object with the argumenttruewhen you want to display it
Listening for Radio Buttons
The inner class, StyleListener, in the FontDialogclass will work on principles that you have seenbefore A radio button (or a checkbox) generates events of type java.awt.ItemEvent, and the listenerclass must implement the java.awt.ItemListenerinterface:
class StyleListener implements ItemListener {
public StyleListener(int style) { this.style = style;
}public void itemStateChanged(ItemEvent e) {if(e.getStateChange()==ItemEvent.SELECTED) { // If style was selectedfontStyle |= style; // turn it on in the font style} else {
fontStyle &= ~style; // otherwise turn it off}
font = font.deriveFont(fontStyle); // Get a new fontfontDisplay.setFont(font); // Change the label fontfontDisplay.repaint(); // repaint
} private int style; // Style for this listener}
Chapter 20
Trang 16Of course, you want the value that is now selected in the spinner, and the getValue()method willreturn a reference to this as type Object Since you are using a SpinnerNumberModelobject as the spin-ner model, the object encapsulating the value will actually be of type Number, so you can cast the refer-ence returned by getValue()to this type You can get a little closer to what you want by amending ourstateChanged()method to:
public void stateChanged(ChangeEvent e) {Number value = (Number)((JSpinner)e.getSource()).getValue();
}You’re not really interested in a Numberobject though What you want is the integer value it contains, soyou can store it in the fontSizemember of the dialog and then derive a new font The intValue()method for the Numberobject will produce that You can therefore arrive at the final version ofsetChanged()that does what you want:
public void stateChanged(ChangeEvent e) {fontSize = ((Number)(((JSpinner)e.getSource()).getValue())).intValue();
font = font.deriveFont((float)fontSize);
fontDisplay.setFont(font);
fontDisplay.repaint();
}That first statement looks quite daunting but since you put it together one step at a time, you should seethat it isn’t really difficult — there are just a lot of parentheses to keep in sync You now need to addimportstatements for the ChangeListenerinterface and the ChangeEventclass:
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
Using Radio Buttons to Select the Font StyleTwo JRadioButtonobjects will provide the means for selecting the font style One will select bold ornot, and the other will select italic or not A plain font is simply one that is neither bold nor italic Youcould use JCheckBoxobjects here if you prefer — they would work just as well Here’s the code:
public FontDialog(SketchFrame window) {// Initialization as before
// Button panel code as before
// Set up the data input panel to hold all input components as before
// Add the font choice prompt label as before
// Set up font list choice component as before
// Panel to display font sample as before
// Create a split pane with font choice at the top as before
// Set up the size choice using a spinner as before
// Set up style options using radio buttonsJRadioButton bold = new JRadioButton(“Bold”, (fontStyle & Font.BOLD) > 0);JRadioButton italic = new JRadioButton(“Italic”,
(fontStyle & Font.ITALIC) > 0);bold.addItemListener(new StyleListener(Font.BOLD)); // Add button listenersitalic.addItemListener(new StyleListener(Font.ITALIC));
JPanel stylePane = new JPanel(); // Create style panestylePane.add(bold); // Add buttons
Extending the GUI
Trang 17The constructor accepts an argument that is the style for the button, so the value of the member, style,will be the value you want to set in the fontStylemember that you use to create a new Fontobject,either Font.BOLDor Font.ITALIC Since the listener for a particular button already contains the corre-sponding style, the itemStateChanged()method that is called when an item event occurs just switchesthe value of stylein the fontStylemember of FontDialogeither on or off, depending on whetherthe radio button was selected or deselected It then derives a font with the new style, sets it in thefontDisplaylabel, and repaints it.
This code calls for two more importstatements in the FontDialogsource file:
import java.awt.event.ItemListener;
import java.awt.event.ItemEvent;
You have now completed the FontDialogclass If you have been creating the code yourself, now would
be a good time to try compiling the class to see what missing —importstatements usually All you neednow is some code in the SketchFrameclass to make use of it
Try It Out Using the Font Dialog
To get the font dialog operational in Sketcher, you’ll add a new menu, Options, to the menu bar with aChoose font menu item and install a listener for the menu item To keeps things vaguely shipshape itwould be best to add the fragments of code in the SketchFrameconstructor in the places where you dosimilar things
Create the Options menu with the following code in the SketchFrameconstructor:
JMenu fileMenu = new JMenu(“File”); // Create File menuJMenu elementMenu = new JMenu(“Elements”); // Create Elements menuJMenu optionsMenu = new JMenu(“Options”); // Create options menuJMenu helpMenu = new JMenu(“Help”); // Create Help menufileMenu.setMnemonic(‘F’); // Create shortcutelementMenu.setMnemonic(‘E’); // Create shortcutoptionsMenu.setMnemonic(‘O’); // Create shortcuthelpMenu.setMnemonic(‘H’); // Create shortcutYou can add the menu item like this somewhere in the constructor:
// Add the font choice item to the options menufontItem = new JMenuItem(“Choose font ”);
fontItem.addActionListener(this);
optionsMenu.add(fontItem);
You can add a declaration for the fontItemmember of the SketchFrameclass by adding it to the ing declaration for the aboutItem, which is probably somewhere near the end in your file It may takesome hunting to find it with the amount of code you now have in this class:
exist-private JMenuItem aboutItem, fontItem;
Extending the GUI
Trang 18You need to add the Options menu to the menu bar before the Help menu to be consistent with convention:
menuBar.add(fileMenu); // Add the file menumenuBar.add(elementMenu); // Add the element menumenuBar.add(optionsMenu); // Add the options menuYou can create a FontDialogobject by adding a statement to the end of the SketchFrameconstructor:
fontDlg = new FontDialog(this); // Create the font dialogYou can reuse the FontDialogobject as often as you want When you need to display it, you simply callits setVisible()method Of course, you’ll declare fontDlgas a member of the SketchFrameclass:private FontDialog fontDlg; // The font dialog
You can modify the actionPerformed()method in the SketchFrameclass to handle the events for thenew menu item:
public void actionPerformed(ActionEvent e) {
if(e.getSource() == aboutItem) {// Create about dialog with the menu item as parentJOptionPane.showMessageDialog(this, // Parent
“Sketcher Copyright Ivor Horton 2000”,// Message
“About Sketcher”, // TitleJOptionPane.INFORMATION_MESSAGE); // Message type} else if(e.getSource() == fontItem) { // Set the dialog window positionRectangle bounds = getBounds();
fontDlg.setLocation(bounds.x + bounds.width/3, bounds.y + bounds.height/3);fontDlg.setVisible(true); // Show the dialog
}}
The new else ifblock makes the dialog visible after setting its location in relation to the applicationwindow You’ll need an importstatement for the Rectangleclass name in the source file for
top-Chapter 20
Trang 19Figure 20-11
Pop-Up Menus
The javax.swingpackage defines the JPopupMenuclass, which represents a menu that you can pop up
at any position within a component, but conventionally you display it at the current mouse cursor tion when a particular mouse button is pressed, usually button 2 There are two constructors in thePopupMenuclass: one to which you pass a Stringobject that defines a name for the menu, and a defaultconstructor that defines a menu without a name If you specify a name for a pop-up menu with a state-ment such as
posi-generalPopup = new PopupMenu(“General”);
the name you supply is primarily for identification purposes and is not always displayed when themenu is popped up: it depends on your environment Under MS Windows, for example, it doesn’tappear This is different from a menu on a menu bar where the string you pass to the constructor is whatappears on the menu bar Don’t forget to add an importstatement for javax.swing.JPopupMenu.Let’s add a pop-up menu to the SketchFrameclass by adding a data member of type JPopupMenu:private JPopupMenu popup = new JPopupMenu(“General”); // Window pop-up
Extending the GUI
Trang 20To populate a pop-up menu with menu items, you add JMenuItemobjects by passing each of them tothe add()method for the JPopupMenuobject If you’re using Actionobjects because you also want toimplement toolbar buttons, you can create the JMenuItemobject using a constructor that accepts a refer-ence of type Actionand then pass it to the add()method for the pop-up menu object You can also pass
a Stringobject to add(),which will create a JMenuItemobject and add it to the pop-up A reference tothe menu item object is always returned by the various overloaded add()methods Handling the eventsfor the menu items is an identical process to that for regular menu items, and Actionobjects handletheir own events, as you have seen
You’ll now add menu items to the pop-up that you have created as a member of a SketchFrameobject
by adding the following code to the SketchFrameclass constructor:
// Create pop-up menupopup.add(new JMenuItem(lineAction));
Displaying a Pop-Up Menu
You can display a pop-up within the coordinate system of any component, by calling the show()method for the JPopupMenuobject The method requires three arguments to be specified: a reference to
the parent component that is the context for the pop-up, and the x and y coordinates where the menu is
to be displayed, relative to the origin of the parent For example:
generalPopup.show(view, xCoord, yCoord);
This displays the pop-up at position (xCoord, yCoord) in the coordinate system for the component, view
A pop-up menu is usually implemented as a context menu The principal idea of a context menu is that
it’s not just a single menu: It displays a different set of menu items depending on the context — that is,what is under the mouse cursor when the button is clicked The mouse button that you press to display
a context menu is sometimes called a pop-up trigger, simply because pressing it triggers the display of
the pop-up On systems that support the notion of a pop-up trigger, the pop-up trigger is fixed, but itcan be different between systems It is usually the right mouse button on a two- or three-button mousefor right-handed users On systems with a one-button mouse, you typically have to hold down a modi-fier key while pressing the mouse button to fire the pop-up trigger
Chapter 20
Trang 21The MouseEventclass has a special method, isPopupTrigger(), that returns truewhen the eventshould display a pop-up menu This method will return trueonly in the mousePressed()ormouseReleased()methods It will always return falsein methods corresponding to other mouseevents This method helps solve the problem of different mouse buttons being used on different systems
to display a pop-up If you use this method to decide when to display a pop-up, you’ve got them ered — well, almost You would typically use this with the following code to display a pop-up:
cov-public void mouseReleased(MouseEvent e) {if(e.isPopupTrigger()) {
// Code to display the pop-up menu
}}
I have shown conceptual code for the mouseReleased()method here This would be fine for Windows,but unfortunately it may not work on some other systems — Solaris, for example This is because insome operating system environments, the isPopupTrigger()returns trueonly when the button ispressed, not when it is released The pop-up trigger is not just a particular button — it is a either amouse-pressed event or a mouse-released event associated with a particular button This implies that ifyou want your code to work on a variety of systems using the “standard” mouse button to trigger thepop-up in every case, you must implement the code to call isPopupTrigger()and pop the menu inboth the mousePressed()and mouseReleased()methods The method will return trueonly in one
or the other Of course, you could always circumvent this by ignoring convention and pop the menu for
a specific button press with code like this:
if((e.getButton() == e.BUTTON3) {// Code to display the pop-up menu
}Now the pop-up would operate only with button 3, regardless of the convention for the underlyingoperating system, but the user may not be particularly happy about having to use a different pop-uptrigger for your Java program compared to other applications on the same system
Try It Out Displaying a Pop-Up Menu
In Sketcher, the pop-up menu would sensibly operate in the area where the sketch is displayed — inother words, triggering the pop-up menu has to happen in the view Assuming you have already addedthe code to SketchFramethat will create the pop-up menu as I discussed earlier, you just need to add amethod to SketchFrameto make the pop-up available to the view:
// Retrieve the pop-up menupublic JPopupMenu getPopup() { return popup;
}Now a SketchViewobject can get a reference to the pop-up in the SketchFrameobject by using theapplication object to get to this method
Extending the GUI
Trang 22To maintain proper cross-platform operation for Sketcher, you’ll implement the pop-up triggering inboth the mousePressed()and the mouseReleased()methods To make it easier, you can implementthe processing of the pop-up trigger event in a separate method in the MouseHandlerinner class toSketchView:
// Process pop-up trigger eventpublic void processPopupTrigger(MouseEvent e) {start = e.getPoint(); // Save the cursor position in starttheApp.getWindow().getPopup().show((Component)e.getSource(),
start.x, start.y); start = null;
}The response to the event is to display the pop-up menu at the position defined by start You obtain areference to the JPopupMenuobject from the application window object, which you access via the appli-cation object reference that is stored in the view You then call the show()method for the pop-up menuobject, passing a reference to the source of the event as the parent Note that the method retrieves thecursor position from the MouseEventobject You could use the position stored in startby the
mousePressed()method, but if the event is associated with the mouse released event and the userdrags the cursor before releasing the button, the menu will appear at a different position from where thebutton is released
Here’s how mousePressed()should be implemented in the MouseHandlerinner class to SketchView:
public void mousePressed(MouseEvent e) {if(e.isPopupTrigger()) {
processPopupTrigger(e);
} else if((button1Down = (e.getButton() == MouseEvent.BUTTON1)) &&
(theApp.getWindow().getElementType() != TEXT)) {start = e.getPoint(); // Save the cursor position in startg2D = (Graphics2D)getGraphics(); // Get graphics contextg2D.setXORMode(getBackground()); // Set XOR mode
}}The method checks for a pop-up trigger event If that’s what it is, the method calls the
processPopupTrigger()method to handle the event If it’s not the pop-up trigger event, things cess exactly as before The cursor position is now retrieved from the event object inside the body of thesecond ifstatement
pro-Implementing the mouseReleased()method is just as easy:
public void mouseReleased(MouseEvent e) {if(e.isPopupTrigger()) {
processPopupTrigger(e);
} else if(button1Down = (e.getButton()==MouseEvent.BUTTON1) &&
(theApp.getWindow().getElementType() != TEXT)) {button1Down = false; // Reset the button 1 flagif(tempElement != null) { // If there is an element theApp.getModel().add(tempElement); // add it to the model tempElement = null; // and reset the fieldChapter 20
Trang 23}if(g2D != null) { // If there’s a graphics contextg2D.dispose(); // release the resource g2D = null; // and reset field to null}
start = last = null; // Remove the points}
}
If you recompile Sketcher and run it again, the pop-up menu should appear in response to a right-buttonclick, or whatever button triggers a context menu on your system The way it looks on my system isshown in Figure 20-12
Figure 20-12
Note how you get the icons and the label for each of the menu items This is because both are defined inthe Actionobjects that were used to generate the menu, and they have not been set to nullby callingthe setIcon()method for the menu items
How It WorksThe isPopupTrigger()method for the MouseEventobject returns truewhen the button correspond-ing to a context menu is pressed or released In this case you call the processPopupTrigger()methodthat you implemented to display the pop-up menu When you click on a menu item in the pop-up, orclick elsewhere, the pop-up menu is automatically hidden Now any element type or color is a couple ofclicks away
This is just a pop-up menu, not a context menu A context menu should be different depending onwhat’s under the cursor You’ll now look more closely at how you could implement a proper contextmenu capability in Sketcher
Extending the GUI
Trang 24Implementing a Context Menu
As a context menu displays a different menu depending on the context, it follows that the programneeds to know what is under the cursor at the time the pop-up trigger button is pressed Let’s take thespecific instance of the view in Sketcher where you are listening for mouse events You could define twocontexts for the cursor in the view — one when an already drawn element is under the cursor andanother when there is no element under the cursor In the first context, you could display a special pop-
up menu that provides operations that apply specifically to the element under the cursor — with menuitems to delete or move the element, for example In the second context, when there is no element underthe cursor, you could display the pop-up menu that you created in the previous example The contextmenu that will be displayed when an element is under the cursor is going to look like that shown inFigure 20-13
Figure 20-13
That’s where you’re headed, but there are a few bridges to be crossed on the way For starters, if the text menu is to be really useful, users will need to know which element is under the cursor before theypop up the context menu, otherwise they can’t be sure to which element the pop-up menu operationswill apply, particularly when elements overlap on the screen Deleting the wrong element could be irri-tating to say the least
con-What you need is some visual feedback to show when an element is under the cursor — highlighting theelement under the cursor by changing its color, for example
Try It Out Highlighting an Element
You could draw an element in magenta rather than its normal color to highlight that it’s the one underthe mouse cursor Every element will need a booleanfield to indicate whether it is highlighted or not sothe object will know which color to use in the draw()method when drawing the element You can addthis variable as a field in the Elementclass:
Chapter 20
Trang 25You can add this line immediately following the statement for the other data members in the Elementclass definition The highlightedfield will be inherited by all of the subclasses of Element.
You’ll need a method to set the highlightedflag in the Elementclass:
// Set or reset highlight colorpublic void setHighlighted(boolean highlighted) {this.highlighted = highlighted;
}This method will also be inherited by all of the subclasses of Element
To implement the basis for getting highlighting to work, you need to change one line in the draw()method for each of the subclasses of Element— that is, Element.Line, Element.Circle,
Element.Curve, Element.Rectangle, and Element.Text The line to change is the one that sets thedrawing color — it’s the first line in each of the draw()methods You should change it to:
g2D.setPaint(highlighted ? Color.MAGENTA : color);
Now each element can potentially be highlighted
How It WorksThe setHighlighted()method accepts a booleanvalue as an argument and stores it in the data mem-ber highlighted When you want an element to be highlighted, you just call this method with the argu-ment as true To switch highlighting off for an element, you call this method with the argument false.Previously, the setPaint()statement just set the color stored in the data member coloras the drawingcolor Now, if highlightedis true, the color will be set to magenta, and if highlightedis false, thecolor stored in the data member colorwill be used
To make use of highlighting to provide the visual feedback necessary for a user-friendly implementation
of the context menu, you need to determine at all times what is under the cursor This means you must
track and analyze all mouse moves all the time!
Tracking Mouse MovesWhenever the mouse is moved, the mouseMoved()method in the MouseMotionListenerinterface iscalled You can therefore track mouse moves by implementing this method in the MouseHandlerclass,which is an inner class to the SketchViewclass Before I get into that, I need to define what I mean by anelement being under the cursor, and more crucially, how you are going to find out to which element, ifany, this applies
It’s not going to be too difficult You can arbitrarily decide that an element is under the cursor when thecursor position is inside the bounding rectangle for an element This is not too precise a method, but ithas the great attraction that it is extremely simple Precise hit-testing on an element would carry consid-erably more processing overhead Electing to add any greater complexity will not help you to under-stand the principles here, so you’ll stick with the simple approach
Extending the GUI
Trang 26So what is going to be the methodology for finding the element under the cursor? Brute force basically:Whenever the mouse is moved, you can just search through the bounding rectangles for each of the ele-ments in the model until you find one that encloses the current cursor position You’ll then arrange forthe first element that you find to be highlighted If you check all the elements in the model without find-ing a bounding rectangle that encloses the cursor, then there isn’t an element under the cursor Themechanism for the various geometric elements is illustrated in Figure 20-14.
Figure 20-14
To record a reference to the element that is under the cursor, you’ll add a data member of type Element
to the SketchViewclass If there isn’t an element under the cursor, you’ll make sure that this data ber is null
mem-Try It Out Referring to Elements
Add the following statement after the statement that declares the theAppdata member in the
SketchViewclass definition:
private Element highlightElement; // Highlighted elementThe mouseMoved()method is going to be called very frequently, so you must make sure it executes asquickly as possible This means that for any given set of conditions, you execute the minimum amount
of code Here’s the implementation of the mouseMoved()method in the MouseHandlerclass inSketchView:
Circle is under thecursor here
Curve is under thecursor here
Line is under thecursor here
Rectangle is underthe cursor here
Nothing is under thecursor here
boundingrectangles
Element.Circle
Element.Curve
Element.Rectangle
Rectangle bounds = element.getBounds(); // Gets bounding rectangle
bounds.contains(cursor) returns true if cursor is in bounds
Element.Line
Chapter 20
Trang 27// Handle mouse movespublic void mouseMoved(MouseEvent e) {Point currentCursor = e.getPoint(); // Get current cursor positionfor(Element element : theApp.getModel()) { // Go through the listif(element.getBounds().contains(currentCursor)) { // Under the cursor?if(element==highlightElement) { // If it’s already highlightedreturn; // we are done
}// The element under the cursor is not highlightedg2D = (Graphics2D)getGraphics(); // Get graphics context// Un-highlight any old highlighted element
if(highlightElement!=null) { // If an element is highlightedhighlightElement.setHighlighted(false);// un-highlight it and
highlightElement.draw(g2D); // draw it normal color}
element.setHighlighted(true); // Set highlight for new elementhighlightElement = element; // Store new highlighted elementelement.draw(g2D); // Draw it highlighted
g2D.dispose(); // Release graphic context resourcesg2D = null;
return;
}}// Here there is no element under the cursor so
if(highlightElement!=null) { // If an element is highlightedg2D = (Graphics2D)getGraphics(); // Get graphics context
highlightElement.setHighlighted(false);// turn off highlightinghighlightElement.draw(g2D); // Redraw the elementhighlightElement = null; // No element highlightedg2D.dispose(); // Release graphic context resourcesg2D = null;
}}
To check that highlighting works, recompile Sketcher and run it again If you draw a few elements, youshould see them change color as the cursor moves over them
How It WorksThis method is a fair amount of code so let’s work through it step by step The first statement saves thecurrent cursor position in the local variable currentCursor You use a collection-based forloop to iter-ate over all the elements in the model In the loop, you obtain the bounding rectangle for each element
by calling its getBounds()method, and then call the contains()method for the rectangle that isreturned with the current cursor position as the argument This will return trueif the rectangle enclosesthe point, and falseif it doesn’t When you find an element under the cursor, it is quite possible thatthe element is already highlighted because the element was found the last time the mouseMoved()method was called This will occur when you move the cursor within the rectangle bounding an ele-ment In this case you don’t need to do anything, so you return from the method
Extending the GUI
Trang 28If the element found is not the same as last time, you obtain a graphics context object for the viewbecause you definitely need it to draw the new element you have found under the cursor in the high-light color You then check that the variable highlightElementis not null— it will be nullif the cur-sor newly entered the rectangle for an element and previously none were highlighted If
highlightElementis not null, you must restore the normal color to the old element before you light the new one To do this you call its setHighlighted()method with the argument false, andthen call its draw()method You don’t need to involve the paint()method for the view here since youare not adding or removing elements — you are simply redrawing an element that is already displayed
high-To highlight the new element, you call its setHighlighted()method with the argument true, andthen store a reference to the element in highlightElementand call its draw()method to get it drawn
in the highlight color Finally, you release the graphics context resources by calling the dispose()method for g2D,set the variable back to null, and return
The next block of code in the method executes if you exit the forloop because no element is under thecursor In this case you must check if there was an element highlighted last time around If there was,you un-highlight it, redraw it in its normal color, and reset highlightElementto null
Defining the Other Context Menu
You already have the menu defined in SketchFramefor when the cursor is not over an element It’s sible to keep it there because the menu items are a subset of those from the application windows menus.The context menu when the cursor is over an element will have a new set of menu items, specific tooperating on individual elements, so it can be defined in the view All you need is the code to define thenew context menu — plus the code to decide which menu to display when isPopupTrigger()returnstruefor a mouse event
sen-You already know that you will have four menu items in the element context menu:
❑ Move— This moves the element under the cursor to a new position This operation works bydragging it with the left mouse button down (button 1)
❑ Delete— This operation will delete the element under the cursor
❑ Rotate— This operation will allow you to rotate the element under the cursor about the top-leftcorner of its bounding rectangle by dragging it while holding button 1 (normally the left mousebutton) down
❑ Send-to-back— This operation overcomes the problem of an element not being accessible,never highlighted that is, because it is masked by the bounding rectangle of another element
Since you highlight an element by searching the list from the beginning, an element towards the endmay never be highlighted if the rectangle for an earlier element completely encloses it Moving the ear-lier element that is hogging the highlighting to the end of the list will allow the formerly masked ele-ment to be highlighted, so this is what the Send-to-back operation will do
Try It Out Creating Context Menus
First, add the data members to the SketchViewclass that will store the element pop-up reference andthe JMenuItemobjects that will be the pop-up menu items:
private JPopupMenu elementPopup = new JPopupMenu(“Element”);
private JMenuItem moveItem, deleteItem,rotateItem, sendToBackItem;
Chapter 20
Trang 29// Process a rotate} else if(source == sendToBackItem) {// Process a send-to-back
}}
Of course, you’ll need two more importstatements in the SketchView.javafile:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
To pop the menu you need to modify the code in the processPopupTrigger()method of the
MouseHandlerinner class a little:
public void processPopupTrigger(MouseEvent e) {start = e.getPoint(); // Save the cursor position in startif(highlightElement == null) {
theApp.getWindow().getPopup().show((Component)e.getSource(),
start.x, start.y);
} else {elementPopup.show((Component)e.getSource(), start.x, start.y);
} start = null;
}This just adds an if–elseto display the element dialog when an element is highlighted If you recom-pile Sketcher you should get a different context menu depending on whether an element is under thecursor or not
How It Works
The processPopupTrigger()method in the MouseHandlerinner class now pops one or other of thetwo pop-ups you have, depending on whether the reference in highlightElementis nullor not TheprocessPopupTrigger()method will be called either by the mousePressed()method or themouseReleased()method, depending on how the pop-up trigger is defined in your environment Youcan select items from the general pop-up to set the color or the element type, but the element pop-upmenu does nothing at present It just needs a few lines of code somewhere to do moves and rotationsand stuff Don’t worry — it’ll be like falling off a log — but not so painful
Deleting Elements
Let’s take the easiest one first — deleting an element All that’s involved here is calling remove()for themodel object from the actionPerformed()method in SketchView Let’s give it a try
Try It Out Deleting Elements
The code you need to add to actionPerformed()in the SketchViewclass looks like this:
public void actionPerformed(ActionEvent e) {
Object source = e.getSource();
Chapter 20
Trang 30You must also add importstatements for JPopupMenuand JMenuItem:import javax.swing.JPopupMenu;
import javax.swing.JMenuItem;
You can create the elementPopupcontext menu in the SketchViewconstructor:
public SketchView(Sketcher theApp) {this.theApp = theApp;
MouseHandler handler = new MouseHandler(); // create the mouse listeneraddMouseListener(handler); // Listen for button eventsaddMouseMotionListener(handler); // Listen for motion events// Add the pop-up menu items
moveItem = elementPopup.add(new JMenuItem(“Move”));
deleteItem = elementPopup.add(new JMenuItem(“Delete”));
rotateItem = elementPopup.add(new JMenuItem(“Rotate”));
sendToBackItem = elementPopup.add(new JMenuItem(“Send-to-back”));
// Add the menu item listenersmoveItem.addActionListener(this);
deleteItem.addActionListener(this);
rotateItem.addActionListener(this);
sendToBackItem.addActionListener(this);
}You add the menu items using the add()method that accepts a JMenuItemargument, and returns a ref-erence to the JMenuItemobject that it creates You create each JMenuItemobject in the expression that isthe argument to the add()method You then use the references to the JMenuItemobjects to add theview object as the listener for all the menu items in the pop-up
You must make the SketchViewclass implement the ActionListenerinterface:
class SketchView extends JComponent
implements Observer, ActionListener {You can add to SketchViewthe actionPerformed()method, which will handle action events from themenu items:
// Handle context menu eventspublic void actionPerformed(ActionEvent e ) {Object source = e.getSource();
if(source == moveItem) {// Process a move
} else if(source == deleteItem) {// Process a delete
As with the new data members above, be careful to add this to the SketchViewclass and not inside the inner MouseHandler class by mistake!
Extending the GUI
Trang 31if(source == moveItem) {// Process a move
} else if(source == deleteItem) {if(highlightElement != null) { // If there’s an elementtheApp.getModel().remove(highlightElement); // then remove ithighlightElement = null; // Remove the reference}
} else if(source == rotateItem) {// Process a rotate
} else if(source == sendToBackItem) {// Process a send-to-back
}}This is a cheap operation requiring only six lines Recompile, create a few elements, and then watchthem disappear before your very eyes with a right button click
How It WorksAfter verifying in the actionPerformed()method that highlightElementis not null, you call theremove()method that you added in the SketchModelclass way back This will delete the element fromthe list, so when the view is repainted, it will no longer be displayed The repaint occurs automaticallybecause the update()method for the view — the method that you implemented for the Observerinter-face — will be called because the model has changed Of course, you must remember to set
highlightElementto nulltoo; otherwise, it could get drawn by a mouse handler even though it is nolonger in the model
Let’s do another easy one — send-to-back
Implementing the Send-to-Back OperationThe send-to-back operation is really an extension of the delete operation You can move an element fromwherever it is in the list by deleting it and then adding it again at the end of the list
Try It Out The Send-to-Back Operation
The actionPerformed()method in the SketchViewclass has the job of removing the highlighted ment from wherever it is in the model and then adding it back at the end:
ele-public void actionPerformed(ActionEvent e) {Object source = e.getSource();
if(source == moveItem) {// (Process a move )} else if(source == deleteItem) {// Code as inserted here earlier
} else if(source == rotateItem) {// (Process a rotate)
} else if(source == sendToBackItem) {
Extending the GUI
Trang 32if(highlightElement != null) {theApp.getModel().remove(highlightElement);
A little harder this time — eight lines of code You can try this by drawing a few concentric circles, withthe outermost drawn first An outer circle will prevent an inner circle from being highlighted, but apply-ing send-to-back to the outer circle will make the inner circle accessible
How It Works
This uses the remove()method in SketchModelto remove the highlighted element, and then calls theadd()method to put it back — it will automatically be added to the end of the elements in the list Youswitch off the highlighting of the element to indicate that it’s gone to the back of the queue, and resethighlightElementback to null You call repaint()for the view object to get the highlighted elementthat you have reset drawn in its normal color
You have run out of easy operations You must now deal with a not quite so easy one — the move tion To handle this you must look into a new topic — transforming the user coordinate system If youare not of a mathematical bent, some of what I’ll discuss here can sound complicated But even if yourmath is very rusty, you should not have too many problems Like a lot of things, it’s the unfamiliarity ofthe jargon that makes it seem more difficult than it is
opera-Transforming the User Coordinate System
I said when you started learning how to draw on a component that the drawing operations are specified
in a user coordinate system, and the user coordinates are converted to a device coordinate system Theconversion of coordinates from the user system to the device system is taken care of by the methods in
the graphics context object that you use to do the drawing, and they do this by applying a tionto the user coordinates The term transformation refers to the computational operations that perform
transforma-the conversion
By default, the origin, the (0, 0) point in the user coordinate system, corresponds to the (0, 0) point in the
device coordinate system The axes are also coincident, too, with positive x heading from left to right, and positive y from top to bottom However, you can move the origin of the user coordinate system rela-
tive to its default position Such a move is called a translation, and this is illustrated in Figure 20-15.
A fixed value, deltaX, say, is added to each x coordinate, and another value, deltaY, say, is added to
every y coordinate, and the effect of this is to move the origin of the user coordinate system relative to
the device coordinate system: Everything will be shifted to the right and down compared to where itwould have been without the translation Of course, the deltaXand deltaYvalues can be negative, inwhich case it would shift things to the left and up
Chapter 20
Trang 33Figure 20-15
A translation is one kind of affine transformation (Affine is a funny word Some say it goes back to
Laurel and Hardy where Ollie says, “This is affine mess you’ve got us into,” but I don’t subscribe tothat.) An affine transformation is actually a linear transformation that leaves straight lines still straightand parallel lines still parallel As well as translations, there are other kinds of affine transformation thatyou can define:
❑ Rotation— The user coordinates system is rotated through a given angle about its origin
❑ Scale— The x and y coordinates are each multiplied by a scaling factor, and the multipliers for x and y can be different This enables you to enlarge or reduce something in size If the scale factor
for one coordinate axis is negative, then objects will be reflected in the other axis Setting the
scale factor for x coordinates to –1, for example, will make all positive coordinates negative and vice versa, so everything is reflected in the y axis.
❑ Shear— This is perhaps a less familiar operation It adds to each x coordinate a value that depends on the y coordinate, and adds to each y coordinate a value that depends on the x coor-
dinate You supply two values to specify a shear, sXand sY, say, and they change the nates in the following way:
coordi-Each x coordinate becomes (x + sX * y)
Each y coordinate becomes (y + sY * x)
y
x
User CoordinatesDefault mapping
This appears…This appears…
Default Mapping
Extending the GUI
Trang 34The effect of this can be visualized most easily if you first imagine a rectangle that is drawn mally A shearing transform can squash it by tilting the sides — rather like when you flatten acarton — but keep opposite sides straight and parallel Figure 20-16 illustrates the three affinetransformations that I’ve just described.
nor-Figure 20-16
Figure 20-16 shows:
❑ A rotation of -π/4 radians, which is the same as a rotation of –45 degrees Rotation angles are
expressed in radians, and a positive angle rotates everything from the positive x-axis toward the positive y-axis — therefore clockwise The rotation in the illustration is negative and therefore
counterclockwise
❑ A scaling transformation corresponding to an x scale of 2.5 and a y scale of 1.5
❑ A shearing operation where only the x coordinates have a shear factor The factor for the y
coor-dinates is 0 so they are unaffected, and the transformed shape is the same height as the original
The AffineTransform Class
In Java, the AffineTransformclass in the java.awt.geompackage represents an affine transformation.Every Graphics2Dgraphics context has one The default AffineTransformobject in a graphics context
is the identity transform, which leaves user coordinates unchanged It is applied to the user coordinate
system anyway for everything you draw, but all the coordinates for an entity that is displayed are tered by default You can retrieve a copy of the current transform for a graphics context object by callingits getTransform()method For example:
What was like thiscould appear likethis
What was here
could appear likethis
Trang 35AffineTransform at = g2D.getTransform(); // Get current transformWhile this retrieves a copy of the current transform for a graphics context, you can also replace it byanother transform object:
g2D.setTransform(at);
You can retrieve the transform currently in effect with getTransform(), set it to some other operationbefore you draw some shapes, and then restore the original transform later with setTransform()whenyou’re finished The fact that getTransform()returns a reference to a copy, rather than a reference tothe original transform object, is important It means you can alter the existing transform and then restorethe copy later
Although the default transform object for a graphics context leaves everything unchanged, you could set
it to do something by calling one of its member functions All of these have a return type of void, sonone of them return anything:
Transform Default Description
setToTranslation( This method makes the transform a translation of deltaXin x and
double deltaX, deltaYin y This replaces whatever the previous transform was
double deltaY) for the graphics context You could apply this to the transform for
a graphics context with the statements:
// Save current transform and set a new oneAffineTransform at = g2D.getTransform();at.setToTranslation(5.0, 10.0);
The effect of the new transform will be to shift everything that isdrawn in the graphics context g2D5.0 to the right and down by10.0 This will apply to everything that is drawn in g2Dsubse-quent to the statement that sets the new transform
setToRotation( You call this method for a transform object to make it a rotation of double angle) angleradians about the origin This replaces the previous trans-
form To rotate the axes 30 degrees clockwise, you could write:
g2D.getTransform().setToRotation(30*Math.PI/180);This statement gets the current transform object for g2Dand sets it
to be the rotation specified by the expression 30*Math.PI/180.Since ( radians is 180 degrees, this expression produces the equiv-alent of 30 degrees measured in radians
Table continued on following page
Extending the GUI
Trang 36Transform Default Description
setToRotation( This method defines a rotation of angleradians about the point double angle, deltaX,deltaY It is equivalent to three successive transform double deltaX, operations — a translation by deltaX, deltaY, then a rotation double deltaY) through angle radians about the new position of the origin, and
then a translation back by -deltaX,-deltaYto restore the ous origin point
previ-You could use this to draw a shape rotated about the shape’s erence point For example, if the reference point for a shape were
ref-at shapeX,shapeY, you could draw the shape rotated through (/3radians with the following:
g2D.getTransform().setToRotation(Math.PI/3,
shapeX, shapeY);// Draw the shape
The coordinate system has been rotated about the pointshapeX,shapeYand will remain so until you change the transfor-mation in effect You would probably want to restore the originaltransform after drawing the shape rotated
setToScale( This method sets the transform object to scale the x coordinates by
double scaleX, scaleX, and the y coordinates by scaleY To draw everything double scaleY) half scale you could set the transformation with the following
statement:
g2D.getTransform().setToScale(0.5, 0.5);
setToShear( The x coordinates are converted to x+shearX*y,and the
double shearX, y coordinates are converted to y+shearY*x.double shearY)
All of the methods that I’ve discussed here replace the transform in an AffineTransformobject Youcan modify the existing transform object in a graphics context, too
Modifying the Transformation for a Graphics Context
Modifying the current transform for a Graphics2Dobject involves calling a method for the Graphics2D
object The effect in each case is to add whatever transform you are applying to whatever the transform
did before You can add each of the four kinds of transforms that I discussed before by using the ing methods that are defined in the Graphics2Dclass:
follow-translate(double deltaX, double deltaY)
translate(int deltaX, int deltaY)
rotate(double angle)
rotate(double angle, double deltaX, double deltaY)
Chapter 20
Trang 37scale(double scaleX, double scaleY)shear(double shearX, double shearY)
Each of these adds or concatenates the transform specified to the existing transform object for a
Graphics2Dobject Therefore, you can cause a translation of the coordinate system followed by a tion about the new origin position with the following statements:
rota-g2D.translate(5, 10); // Translate the origing2D.rotate(Math.PI/3); // Clockwise rotation 60 degreesg2D.draw(line); // Draw in translate and rotated space
Of course, you can apply more than two transforms to the user coordinate system — as many as you like.However, it is important to note that the order in which you apply the transforms matters To see why,look at the example shown in Figure 20-17
Figure 20-17
This shows just two transforms in effect, but it should be clear that the sequence in which they areapplied makes a big difference This is because the second transform is always applied relative to thenew position of the coordinate system after the first transform has been applied If you need more con-vincing that the order in which you apply transforms matters, you can apply some transforms to your-self Stand with your back to any wall in the room Now apply a translation — take three steps forward.Next apply a rotation — turn through 45 degrees clockwise Make a mental note of where you are If younow go back and stand with your back to the wall in the original position and first turn through 45degrees before you take the three steps forward, you will clearly be in quite a different place in the roomfrom the first time around
Next on your affine tour — how you can create completely new AffineTransformobjects
Device Coordinates
Translate by 0,deltaY Rotate –pi/4
x
y
x
yUser Coordinates
Device Coordinates
Rotate –pi/4 Translate by 0,deltaY
x
y
x
yUser CoordinatesExtending the GUI
Trang 38Creating AffineTransform Objects
Of course, there are constructors for AffineTransformobjects: the default “identity” constructor and anumber of other constructors, but I don’t have space to go into them here The easiest way to createtransform objects is to call a staticmember of the AffineTransformclass There are four static meth-ods corresponding to the four kinds of transforms that I discussed earlier:
getTranslateInstance(double deltaX, double deltaY)
getRotateInstance(double angle)
getScaleInstance(double scaleX, double scaleY)
getShearInstance(double shearX, double shearY)
Each of these returns an AffineTransformobject containing the transform that you specify by the ments To create a transform to rotate the user space by 90 degrees, you could write:
argu-AffineTransform at = argu-AffineTransform.getRotateInstance(Math.PI/2);
Once you have an AffineTransformobject, you can apply it to a graphics context by passing it as anargument to the setTransform()method It has another use, too: You can use it to transform a Shapeobject The createTransformedShape()method for the AffineTransformobject does this Supposeyou define a Rectangleobject with the following statement:
Rectangle rect = new Rectangle(10, 10, 100, 50);
You now have a rectangle that is 100 wide by 50 high, at position (10, 10) You can create a transformobject with the statement:
AffineTransform at = getTranslateInstance(25, 30);
This is a translation in x of 25, and a translation in y of 30 You can create a new Shapeobject from theoriginal rectangle with the statement:
Shape transRect = at.createTransformedShape(rect);
The new transRectobject will look the same as the original rectangle but translated by 25 in x and 30
in y, so its top-left corner will now be at (35, 40) Figure 20-18 illustrates this operation.
However, although it will still look like a rectangle, it will not be a Rectangleobject The
createTransformedShape()method always returns a GeneralPathobject since it has to work withany transform This is because some transformations will deform a shape — applying a shear to a rectan-gle, for example, results in a shape that is no longer a rectangle The method also has to be able to applyany transform to any Shapeobject, and returning a GeneralPathshape makes this possible
Let’s try some of this out A good place to do this is with the Sketcher shape classes At the moment youdraw each shape or text element in the place where the cursor happens to be Let’s use a translation tochange how this works You can redefine each nested class to Elementso that it translates the user coor-dinate system to where the shape should be and then draws the shape that it represent at the origin,(0, 0) You could try to implement this yourself as an exercise before reading on You just need to applysome of the transform methods I have been discussing
Chapter 20
Trang 39// Get the current position of the element
public Point getPosition() {
return position;
}
public abstract java.awt.Rectangle getBounds();
public abstract void modify(Point start, Point last);
public abstract void draw(Graphics2D g2D);
protected Color color; // Color of a shape
protected boolean highlighted = false; // Highlight flag
final static Point origin = new Point(); // Point 0,0
protected Point position; // Element position
// Definitions for the shape classes
}
You might consider passing the start point to the Elementconstructor, but this wouldn’t always work.This is because you need to figure out what the reference point is in some cases — for rectangles, forexample The position of a rectangle will always be the top-left corner, but this is not necessarily the startpoint A method to retrieve the position of an element has been added, as I’m sure you are going to need
it You also have added another member, origin, which is the point (0, 0) This will be useful in all thederived classes, as you’ll now draw every element at that point Since you only need one, it is static,and since you won’t want to change it, it is final
Let’s start with the nested class, Line
Translating Lines
You need to update the constructor first of all:
public Line(Point start, Point end, Color color) {super(color);
position = start;
line = new Line2D.Double(origin, new Point(end.x – position.x,
end.y – position.y));}
You’ve saved the point start in positionand created the Line2D.Doubleshape as the origin Ofcourse, you have to adjust the coordinates of the end point so that it is relative to (0, 0)
You can now implement the draw()method to use a transform to move the coordinate system to wherethe line should be drawn You can economize on the code in the element classes a little by thinking aboutthis because a lot of the code is essentially the same Here’s how you would implement the method forthe Element.Lineclass directly:
public void draw(Graphics2D g2D) {
g2D.setPaint(highlighted ? Color.MAGENTA : color); // Set the line color
AffineTransform old = g2D.getTransform(); // Save the current transformg2D.translate(position.x, position.y); // Translate to position
g2D.draw(line); // Draw the line
g2D.setTransform(old); // Restore original transform}
Chapter 20
Trang 40Figure 20-18
Try It Out Translation
To make this work you’ll need to save the position for each element that is passed to the elementconstructor — this is the start point recorded in the mousePressed()method — and use this to create atranslation transform in the draw() method for the element Since you are going to store the position ofevery class object that has Elementas a base, you might as well store the location in a data member ofthe base class You can modify the Elementclass to do this:
// import statements as before
public abstract class Element {public Element(Color color) { this.color = color;
}public Color getColor(){
return color;
}// Set or reset highlight colorpublic void setHighlighted(boolean highlighted) {this.highlighted = highlighted;