|
1
|
|
|
package org.gannacademy.cdf.graphics.ui; |
|
2
|
|
|
|
|
3
|
|
|
import org.gannacademy.cdf.graphics.Drawable; |
|
4
|
|
|
|
|
5
|
|
|
import javax.imageio.ImageIO; |
|
6
|
|
|
import javax.swing.*; |
|
7
|
|
|
import java.awt.*; |
|
8
|
|
|
import java.awt.image.BufferedImage; |
|
9
|
|
|
import java.io.File; |
|
10
|
|
|
import java.io.IOException; |
|
11
|
|
|
import java.util.Stack; |
|
12
|
|
|
|
|
13
|
|
|
/** |
|
14
|
|
|
* <p>A drawing panel receives and displays all drawing instructions</p> |
|
15
|
|
|
* |
|
16
|
|
|
* <p>In the default configuration (in which {@link AppWindow} is extended to construct a drawing), the drawing panel is |
|
17
|
|
|
* contained by an {@code AppWindow} object — an {@code AppWindow} has-a {@code DrawingPanel}, in the parlance of |
|
18
|
|
|
* the Advanced Placement exam, and a {@code DrawingPanel} has-many {@code Drawable} components.</p> |
|
19
|
|
|
* |
|
20
|
|
|
* @author <a href="https://github.com/gann-cdf/graphics/issues" target="_blank">Seth Battis</a> |
|
21
|
|
|
*/ |
|
22
|
|
|
public class DrawingPanel extends JPanel { |
|
23
|
|
|
|
|
24
|
|
|
/** |
|
25
|
|
|
* {@value #DEFAULT_WIDTH} pixels |
|
26
|
|
|
*/ |
|
27
|
|
|
public static final int DEFAULT_WIDTH = 600; |
|
28
|
|
|
|
|
29
|
|
|
/** |
|
30
|
|
|
* {@value #DEFAULT_HEIGHT} pixels |
|
31
|
|
|
*/ |
|
32
|
|
|
public static final int DEFAULT_HEIGHT = 400; |
|
33
|
|
|
|
|
34
|
|
|
/** |
|
35
|
|
|
* {@link Color#WHITE} |
|
36
|
|
|
*/ |
|
37
|
|
|
public static final Color DEFAULT_BACKGROUND = Color.WHITE; |
|
38
|
|
|
|
|
39
|
|
|
/** |
|
40
|
|
|
* {@value #DEFAULT_IMAGE_FORMAT} |
|
41
|
|
|
*/ |
|
42
|
|
|
public static final String DEFAULT_IMAGE_FORMAT = "PNG"; |
|
43
|
|
|
|
|
44
|
|
|
/* |
|
45
|
|
|
* TODO Not 100% confident that the stack is the right structure here -- it would be nice to let someone reorder the |
|
46
|
|
|
* components to effectively set their Z-depth (which could technically be done with a stack, but is not really in the |
|
47
|
|
|
* spirit of it all) |
|
48
|
|
|
*/ |
|
49
|
|
|
private Stack<Drawable> components; |
|
50
|
|
|
|
|
51
|
|
|
/** |
|
52
|
|
|
* Construct a drawing panel of default dimensions and background color |
|
53
|
|
|
*/ |
|
54
|
|
|
public DrawingPanel() { |
|
55
|
|
|
this(new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT), DEFAULT_BACKGROUND); |
|
56
|
|
|
} |
|
57
|
|
|
|
|
58
|
|
|
/** |
|
59
|
|
|
* <p>Construct a drawing panel with custom arguments</p> |
|
60
|
|
|
* |
|
61
|
|
|
* <p>Of potential interest: while setting a background color that is transparent reveals the light gray color of of |
|
62
|
|
|
* the enclosing {@link JFrame} underneath the drawing panel, it will save as a transparent PNG using |
|
63
|
|
|
* {@link #saveAs(String)}.</p> |
|
64
|
|
|
* |
|
65
|
|
|
* @param dimension in pixels |
|
66
|
|
|
* @param background color |
|
67
|
|
|
*/ |
|
68
|
|
|
public DrawingPanel(Dimension dimension, Color background) { |
|
69
|
|
|
super(); |
|
70
|
|
|
setPreferredSize(dimension); |
|
71
|
|
|
setBackground(background); |
|
72
|
|
|
components = new Stack<>(); |
|
73
|
|
|
} |
|
74
|
|
|
|
|
75
|
|
|
/** |
|
76
|
|
|
* <p>Add a drawable component to the drawing panel</p> |
|
77
|
|
|
* |
|
78
|
|
|
* <p>While available for use, this method does not usually need to be called manually -- it is automatically called |
|
79
|
|
|
* by all {@link Drawable} component constructors. The {@link Drawable#setDrawingPanel(DrawingPanel)} method is |
|
80
|
|
|
* meant to be used to move a {@code Drawable} component from one drawing panel to another transparently.</p> |
|
81
|
|
|
* |
|
82
|
|
|
* @param component to be added |
|
83
|
|
|
*/ |
|
84
|
|
|
public synchronized void add(Drawable component) { |
|
85
|
|
|
if (!components.contains(component)) { |
|
86
|
|
|
components.push(component); |
|
87
|
|
|
} |
|
88
|
|
|
} |
|
89
|
|
|
|
|
90
|
|
|
/** |
|
91
|
|
|
* <p>Remove a drawable component from the drawing panel</p> |
|
92
|
|
|
* |
|
93
|
|
|
* <p>While available for use, this method dos not usually need to be called manually -- it is automatically called |
|
94
|
|
|
* by {@link Drawable#removeFromDrawingPanel()} and, if necessary, by |
|
95
|
|
|
* {@link Drawable#setDrawingPanel(DrawingPanel)}.</p> |
|
96
|
|
|
* |
|
97
|
|
|
* @param component to be removed |
|
98
|
|
|
* @return {@code true} if the component was present and removed, {@code false} otherwise |
|
99
|
|
|
*/ |
|
100
|
|
|
public synchronized boolean remove(Drawable component) { |
|
101
|
|
|
return components.remove(component); |
|
102
|
|
|
} |
|
103
|
|
|
|
|
104
|
|
|
/** |
|
105
|
|
|
* <p>Clear all drawable components from the drawing panel</p> |
|
106
|
|
|
* |
|
107
|
|
|
* <p>This does not destroy the drawable components — any references to these components outside of the drawing |
|
108
|
|
|
* panel will still be valid, but the components will no longer refer to this drawing panel.</p> |
|
109
|
|
|
*/ |
|
110
|
|
|
public void clear() { |
|
111
|
|
|
while (!components.isEmpty()) { |
|
112
|
|
|
components.peek().removeFromDrawingPanel(); |
|
113
|
|
|
} |
|
114
|
|
|
} |
|
115
|
|
|
|
|
116
|
|
|
/** |
|
117
|
|
|
* <p>Repaint the contents of the drawing panel (drawable components) as-needed</p> |
|
118
|
|
|
* |
|
119
|
|
|
* <p>This method is called automatically by the enclosing {@link JFrame} to repaint the drawing panel as needed. It |
|
120
|
|
|
* is not meant to be called manually. If the drawing panel needs to be updated, a {@link #repaint()} request will |
|
121
|
|
|
* schedule the update.</p> |
|
122
|
|
|
* |
|
123
|
|
|
* <p>This method calls the {@link #draw(Graphics2D)} method to perform the actual drawing instructions</p> |
|
124
|
|
|
* |
|
125
|
|
|
* @param graphics context for drawing instructions |
|
126
|
|
|
* @see #draw(Graphics2D) |
|
127
|
|
|
*/ |
|
128
|
|
|
@Override |
|
129
|
|
|
public void paintComponent(Graphics graphics) { |
|
130
|
|
|
super.paintComponent(graphics); |
|
131
|
|
|
draw((Graphics2D) graphics); |
|
132
|
|
|
} |
|
133
|
|
|
|
|
134
|
|
|
/** |
|
135
|
|
|
* <p>Request drawing instructions from all contained drawing components</p> |
|
136
|
|
|
* |
|
137
|
|
|
* <p>This method encapsulates all of the drawing instructions necessary to display the current drawable components. |
|
138
|
|
|
* It is used by both the {@link #paintComponent(Graphics)} method to update the display and the {@link #saveAs(String)} |
|
139
|
|
|
* method to write the current drawable components to a file.</p> |
|
140
|
|
|
* |
|
141
|
|
|
* <p>This method calls the {@link #preDraw(Graphics2D)} method prior to making drawing instructions to |
|
142
|
|
|
* set any rendering hints or other configuration for the drawing.</p> |
|
143
|
|
|
* |
|
144
|
|
|
* @param graphics context for drawing instructions |
|
145
|
|
|
* @see Drawable#draw(Graphics2D) |
|
146
|
|
|
* @see #preDraw(Graphics2D) |
|
147
|
|
|
* @see #paintComponent(Graphics) |
|
148
|
|
|
* @see #saveAs(String, String) |
|
149
|
|
|
*/ |
|
150
|
|
|
protected synchronized void draw(Graphics2D graphics) { |
|
151
|
|
|
Graphics2D graphics2D = graphics; |
|
152
|
|
|
preDraw(graphics2D); |
|
153
|
|
|
for (Drawable component : components) { |
|
154
|
|
|
component.draw(graphics2D); |
|
155
|
|
|
} |
|
156
|
|
|
} |
|
157
|
|
|
|
|
158
|
|
|
/** |
|
159
|
|
|
* <p>Override this method to set custom graphics configurations</p> |
|
160
|
|
|
* <p>This method is called by {@link #draw(Graphics2D)} to configure any desired rendering hints or other |
|
161
|
|
|
* preconfiguration before executing the actual drawing instructions.</p> |
|
162
|
|
|
* |
|
163
|
|
|
* @param graphics context for drawing commands |
|
164
|
|
|
*/ |
|
165
|
|
|
public void preDraw(Graphics2D graphics) { |
|
166
|
|
|
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
|
167
|
|
|
} |
|
168
|
|
|
|
|
169
|
|
|
/** |
|
170
|
|
|
* Save the current drawing panel as a file using the default format |
|
171
|
|
|
* |
|
172
|
|
|
* @param path relative to the current working directory |
|
173
|
|
|
* @return {@code true} if the file was successfully written, {@code false} otherwise |
|
174
|
|
|
*/ |
|
175
|
|
|
public boolean saveAs(String path) { |
|
176
|
|
|
return saveAs(path, DEFAULT_IMAGE_FORMAT); |
|
177
|
|
|
} |
|
178
|
|
|
|
|
179
|
|
|
/** |
|
180
|
|
|
* Save the current drawing panel as a file |
|
181
|
|
|
* |
|
182
|
|
|
* @param path relative to the current working directory |
|
183
|
|
|
* @param format of the image file (e.g. {@value #DEFAULT_IMAGE_FORMAT} |
|
184
|
|
|
* @return {@code true} if the file was successfully written, {@code false} otherwise |
|
185
|
|
|
*/ |
|
186
|
|
|
public boolean saveAs(String path, String format) { |
|
187
|
|
|
try { |
|
188
|
|
|
BufferedImage image = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); |
|
189
|
|
|
Graphics2D context = image.createGraphics(); |
|
190
|
|
|
context.setPaint(getBackground()); |
|
191
|
|
|
context.fillRect(0, 0, image.getWidth(), image.getHeight()); |
|
192
|
|
|
draw(context); |
|
193
|
|
|
ImageIO.write(image, format, new File(path)); |
|
194
|
|
|
} catch (IOException e) { |
|
195
|
|
|
System.err.println("There was an error trying to create the DrawingPanel image file"); |
|
196
|
|
|
e.printStackTrace(); |
|
|
|
|
|
|
197
|
|
|
return false; |
|
198
|
|
|
} |
|
199
|
|
|
return true; |
|
200
|
|
|
} |
|
201
|
|
|
} |
|
202
|
|
|
|