jsugar

Window.java: Added the createComboBox() method.

Author
Vngngdn
Date
Aug. 10, 2016, 6:20 p.m.
Hash
7539cb4c37ba221b16cc1a5963ef58a3c6349e9b
Parent
58a44db6025ff84c6a3a4d23ea0fbc8894143f80
Modified file
Window.java

Window.java

39 additions and 1 deletion.

View changes Hide changes
1
1
 * Window.java - Module to create a new window with JSugar.
2
2
 * Copyright © 2016 Maarten "Vngngdn" Vangeneugden
3
3
 * 
4
4
 * This program is free software: you can redistribute it and/or modify
5
5
 * it under the terms of the GNU General Public License as published by
6
6
 * the Free Software Foundation, either version 3 of the License, or
7
7
 * (at your option) any later version.
8
8
 * 
9
9
 * This program is distributed in the hope that it will be useful,
10
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
12
 * GNU General Public License for more details.
13
13
 * 
14
14
 * You should have received a copy of the GNU General Public License
15
15
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
16
 */
17
17
18
18
/*
19
19
 * TODO list:
20
20
 * - JList
21
21
 * - JSlider
22
22
 * - JTable (And a JScrollBar to accompany it)
23
23
 * - JComboBox
24
-
 * - JFileChooser (?)
25
24
 * DONE list:
26
25
 * - JLabel
27
26
 * - JText
28
27
 * - JButton
29
28
 * - JDialogBoxes (you know, everything dialog related)
30
29
 * - JCheckbox
31
30
 * - JRadioButton (properly grouping them has been taken care of as well)
32
31
 * - JSpinner
33
32
 */
+
33
 */
34
34
35
35
/**
36
36
 * Window class for the program.
37
37
 *
38
38
 * Window contains the necessary data and methods to present the user with what
39
39
 * he's familiar with as being a "window". To make it functional, the developer
40
40
 * can make use of a series of methods to add components to said window, remove
41
41
 * components, and so on.
42
42
 * Currently, Window also contains methods to show dialogs. This will be cleaned
43
43
 * in the near future.
44
44
 * @author Maarten Vangeneugden
45
45
 */
46
46
import javax.swing.*; // FIXME: Maybe namespacing it to "javax.swing;" is a better idea.
47
47
import java.util.NoSuchElementException;
48
48
import java.lang.reflect.Method;
49
49
50
50
class Window {
51
51
	private JPanel panel; // The panel that contains all the components.
52
52
	private JFrame frame; // The "window" being presented to the user.
53
53
54
54
	/**
55
55
	 * Constructor of Window.
56
56
	 * By creating a new Window instance, this constructor will automatically
57
57
	 * start the initialization of the GUI. After doing so, the caller can
58
58
	 * start adding components to the window as pleased.
59
59
	 * @param title The title to be shown in the window's title bar.
60
60
	 */
61
61
	public Window() {
62
62
		this.panel = new JPanel();
63
63
		// TODO: The current title is "Hello world!" but that will become caller
64
64
		// defined soon.
65
65
		JFrame frame = new JFrame("Hello world!");
66
66
		// Makes it so that if the user clicks the X in the titlebar, the window
67
67
		// closes:
68
68
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
69
69
		//frame.getContentPane().add(lblHelloWorld); // So you use a get() in order to set() data? #JavaWTF
70
70
		frame.setContentPane(this.panel); // Connecting the component panel to the window.
71
71
		// Makes the window fit to the necessary width and height, so it can show all "subcomponents".
72
72
		frame.pack(); 	
73
73
		frame.setVisible(true); // Makes the window visible to the user.
74
74
		this.frame = frame;
75
75
	}
76
76
77
77
	/**
78
78
	 * Resizes the window to fit all components.
79
79
	 * By calling this method, the window will evaluate the currently visible
80
80
	 * components, and resize itself so that all components become properly
81
81
	 * visible.
82
82
	 */
83
83
	private void updateWindow() {
84
84
		this.frame.pack();
85
85
	}
86
86
87
87
	/**
88
88
	 * A series of tests for method and class handling.
89
89
	 * When a caller presents certain methods with data concerning reflection,
90
90
	 * the Java classes you need to use for that are quite opaque, and don't
91
91
	 * offer much safety in any way.
92
92
	 * The solution therefore, is run some validation checks, but these take up
93
93
	 * a decent amount of space in terms of LoC.
94
94
	 * This method takes care of all that. Call this function whenever data
95
95
	 * needs to be validated.
96
96
	 * @param methodName The name of the method, as it is declared in object.
97
97
	 * @param object The class instance in where this method will be called.
98
98
	 * @return The method that could be derived from the supplied data, or null
99
99
	 * if that wasn't possible.
100
100
	 * @throws NullPointerException if either methodName or object are null
101
101
	 * pointers.
102
102
	 * @throws IllegalArgumentException if methodName is empty, or the method
103
103
	 * does not appear to be declared in the given object, or object is not a
104
104
	 * class.
105
105
	 */
106
106
	// All unchecked typecasts are safe, and the use of raw types is taken care
107
107
	// of.
108
108
	@SuppressWarnings({"unchecked","rawtypes"})
109
109
	private Method handleReflectionData(String methodName, Object object) {
110
110
		// Null pointer checking:
111
111
		if (methodName == null || object == null) {
112
112
			throw new NullPointerException("One or more of the given parameters are null pointers.");
113
113
		}
114
114
115
115
		// XXX: Some might say the next line should be in an else{} block. But
116
116
		// Scoping rules require that I'd then have to wrap the rest of the
117
117
		// method in the same else to use it.
118
118
		Class methodClass = object.getClass(); 
119
119
		if (methodName.equals("")) {
120
120
			throw new IllegalArgumentException("The given methodName was empty.");
121
121
		}
122
122
		Method method;
123
123
		try { // First: Look if there's a method without parameters.
124
124
			method = methodClass.getMethod(methodName, null);
125
125
		}
126
126
		catch (NoSuchMethodException exception) {
127
127
			try {
128
128
				// It's possible that the method requires an event parameter, so
129
129
				// check for that as well:
130
130
				Class<?>[] parameters = {java.awt.event.ActionEvent.class};
131
131
				method = methodClass.getMethod(methodName, parameters);
132
132
			}
133
133
			catch (NoSuchMethodException e) {
134
134
				throw new IllegalArgumentException("The given method does not appear in the given class. Be aware that the given method mustn't have any parameters, or only 1 parameter, which has to be of type java.awt.event.ActionEvent.");
135
135
			}
136
136
		}
137
137
		// At this stage, the given data has been validated, and we've been able
138
138
		// to retrieve the method itself.
139
139
		return method;
140
140
	}
141
141
142
142
	/**
143
143
	 * Creates a button in the GUI for interaction.
144
144
	 * This function offers a convenient way to create a button, that can be
145
145
	 * directly interacted with by the user. After creation, the button itself
146
146
	 * is returned to the caller, if he wishes to do something else with it.
147
147
	 * @param text The text that will be displayed in the button.
148
148
	 * @param action The action that will be returned to the action listener.
149
149
	 * @param methodName The name of the method that will be called when an
150
150
	 * action is triggered.
151
151
	 * @param objectInstance The object instance that contains the given method.
152
152
	 * This may only be a null pointer if triggerMethod is not an instance
153
153
	 * method.
154
154
	 * performed. This method may accept an ActionEvent parameter as its only
155
155
	 * parameter, or no parameters at all.
156
156
	 * @throws NullPointerException if triggerMethod is a null pointer, or
157
157
	 * the empty String was given.
158
158
	 * @throws IllegalArgumentException if triggerMethod has more than 1
159
159
	 * parameter, or the 1 required parameter is not of type ActionEvent.
160
160
	 * @return The button that was created.
161
161
	 * @see java.awt.event.ActionEvent
162
162
	 * @see java.lang.reflect.Method.invoke()
163
163
	 */
164
164
	public JButton createButton(String text, String action, String methodName, Object triggerObject) {
165
165
		Method triggerMethod = this.handleReflectionData(methodName, triggerObject);
166
166
167
167
		// For starters, we first assert that the given parameters are valid:
168
168
		if (text == null) {
169
169
			text = "";
170
170
		}
171
171
		if (action == null) {
172
172
			action = "";
173
173
		}
174
174
		
175
175
		// When the method gets here, everything's been validated correctly.
176
176
		JButton button = new JButton(text);
177
177
		button.setActionCommand(action);
178
178
		button.addActionListener(
179
179
				new java.awt.event.ActionListener() {
180
180
					public void actionPerformed(java.awt.event.ActionEvent event) {
181
181
						try {
182
182
							triggerMethod.setAccessible(true);
183
183
							if (triggerMethod.getParameterTypes().length == 0) {
184
184
								// FIXME: Next line throws a warning?
185
185
								triggerMethod.invoke(triggerObject, null);
186
186
							}
187
187
							else {
188
188
								triggerMethod.invoke(triggerObject, new Object[]{event});
189
189
							}
190
190
						}
191
191
						catch (Exception useless) {
192
192
							/*
193
193
							 * XXX: Some info on why I don't just throw said
194
194
							 * Exception to the caller:
195
195
							 * Java has this awful language constraint, which
196
196
							 * forces every damn exception that isn't a subclass
197
197
							 * of RuntimeException, to be declared in the method
198
198
							 * declaration. This tends to infect all underlying
199
199
							 * methods as well, and all that for reasons I can't
200
200
							 * comprehend. In order to keep JSugar a simple and
201
201
							 * clean library, I'll rather just handle it here,
202
202
							 * and throw a RuntimeException with appropriate
203
203
							 * details.
204
204
							 */
205
205
							throw new IllegalArgumentException("triggerMethod is not accessible from this context.");
206
206
						}
207
207
					}
208
208
				});
209
209
		this.addComponent(button);
210
210
		return button;
211
211
	}
212
212
213
213
	/**
214
214
	 * Ask the user for input through a dialog box.
215
215
	 * This method presents the user with an input field, that can accept
216
216
	 * textual input. The method will return the given input after the user's
217
217
	 * clicked a button to send.
218
218
	 * @param text The text/question to be asked to the user.
219
219
	 * @return A String, equal to what the user entered.
220
220
	 * @throws NullPointerException if text is a null pointer.
221
221
	 */
222
222
	public String inputDialog(String text) {
223
223
		if (text == null) {
224
224
			throw new NullPointerException("The given text/question was a null pointer.");
225
225
		}
226
226
		return JOptionPane.showInputDialog(text);
227
227
	}
228
228
229
229
	/**
230
230
	 * Give the user a dialog box.
231
231
	 * This method can be used to provide a simple dialog to the user.
232
232
	 * This will show the user the given question, after which a boolean value
233
233
	 * is returned, holding the choice.
234
234
	 * @param text The text/question to be asked to the user.
235
235
	 * @return True if the user confirms, False if he denies.
236
236
	 * @throws NullPointerException if text is a null pointer.
237
237
	 */
238
238
	public boolean confirmDialog(String text) {
239
239
		if (text == null) {
240
240
			throw new NullPointerException("The given text/question was a null pointer.");
241
241
		}
242
242
		final int ACCEPTED = 0;
243
243
		//final int DENIED = 1; // Not used in the current context.
244
244
		int result = this.choiceDialog(text, new String[]{"Confirm", "Deny"});
245
245
		if (result == ACCEPTED) {
246
246
			return true;
247
247
		}
248
248
		else {
249
249
			return false;
250
250
		}
251
251
	}
252
252
253
253
	/**
254
254
	 * Give the user a choice dialog box.
255
255
	 * This method gives the user a simple dialog with predefined choices.
256
256
	 * These choices are to be provided by the caller in a simple array.
257
257
	 *
258
258
	 * Tip: This method works extremely well with arbitrary created choices.
259
259
	 * That is: if the outcome of the dialog is trivial (e.g. Only 1 choice),
260
260
	 * then that value is immediately returned.
261
261
	 * @param text The text/question to be asked to the user.
262
262
	 * @param choices An array of Strings, containing the choices the user can
263
263
	 * pick.
264
264
	 * @return The index value of the picked choice, or -1 if no choices were
265
265
	 * given.
266
266
	 * @throws NullPointerException if text is a null pointer.
267
267
	 */
268
268
	public int choiceDialog(String text, String[] choices) {
269
269
		if (text == null) {
270
270
			throw new NullPointerException("The given text/question was a null pointer.");
271
271
		}
272
272
		// First: handling the trivial cases:
273
273
		if (choices.length == 0) {
274
274
			return -1;
275
275
		}
276
276
		else if (choices.length == 1) {
277
277
			return 0;
278
278
		}
279
279
		int answer = JOptionPane.CLOSED_OPTION;
280
280
		// The dialog needs to be shown again until the user has made a possible
281
281
		// choice, i.e. Chickening out using the close button is not possible
282
282
		// (Because that returns CLOSED_OPTION).
283
283
		while (answer == JOptionPane.CLOSED_OPTION) {
284
284
				JOptionPane.showOptionDialog(
285
285
					null, // The parent component. May become the panel?
286
286
					text, // The text/question to describe the goal
287
287
					"Dialog", // The text in the title bar
288
288
					JOptionPane.DEFAULT_OPTION, // The kind of available options
289
289
					JOptionPane.QUESTION_MESSAGE, // The type of message
290
290
					null, // The icon to show
291
291
					choices, // The possible choices
292
292
					choices[0] // The standard choice
293
293
					);
294
294
		}
295
295
		return answer;
296
296
	}
297
297
		
298
298
299
299
	/**
300
300
	 * Creates a label in the GUI for interaction.
301
301
	 * This function offers a convenient way to create a label, that can be
302
302
	 * directly interacted with by the user. After creation, the label itself
303
303
	 * is returned to the caller, if he wishes to do something else with it.
304
304
	 * @param text The text that will be displayed in the label.
305
305
	 * @return The label that was created.
306
306
	 */
307
307
	public JLabel createLabel(String text) {
308
308
		JLabel label = new JLabel(text);
309
309
		this.addComponent(label);
310
310
		return label;
311
311
	}
312
312
313
313
	/**
314
314
	 * Adds a checkbox to the window.
315
315
	 * By providing a String, you can use this method to easily
316
316
	 * create a checkbox, and add it to the window. 
317
317
	 */
318
318
	public JCheckBox createCheckbox(String text) {
319
319
		JCheckBox checkbox = new JCheckBox(text);
320
320
		this.addComponent(checkbox);
321
321
		return checkbox;
322
322
	}
323
323
324
324
	/**
325
325
	 * Adds radio buttons to the window.
326
326
	 * Given a list of Strings, this method will create the same amount of radio
327
327
	 * buttons.
328
328
	 *
329
329
	 * The radio buttons will silently be grouped in a ButtonGroup object,
330
330
	 * making them automatically disable each other, so only 1 radio button can
331
331
	 * be enabled. This ButtonGroup is immutable.
332
332
	 *
333
333
	 * If you need a mutable ButtonGroup, create your own, and use the {@link
334
334
	 * #addComponent()} method to add the radio buttons manually.
335
335
	 * @param text An array of Strings. The length of the array will determine
336
336
	 * the amount of radio buttons that will be created.
337
337
	 * @return An array of radio buttons, in the same order as text.
338
338
	 */
339
339
	public JRadioButton[] createRadioButtons(String text[]) {
340
340
		JRadioButton[] radioButtons = new JRadioButton[text.length];
341
341
		ButtonGroup buttonGroup = new ButtonGroup();
342
342
		for (int i=0; i<radioButtons.length; i++) {
343
343
			radioButtons[i].setText(text[i]);
344
344
			buttonGroup.add(radioButtons[i]);
345
345
			this.addComponent(radioButtons[i]);
346
346
		}
347
347
348
348
		assert radioButtons.length == buttonGroup.getButtonCount() : "The amount of radio buttons ("+ radioButtons.length +") differs from the amount of buttons in buttonGroup ("+ buttonGroup.getButtonCount() +").";
349
349
		return radioButtons;
350
350
	}
351
351
352
352
	/**
353
353
	 * Adds a number spinner component to the GUI.
354
354
	 * This method allows the caller to create a so-called "spinner component"
355
355
	 * to the window. This spinner is an input box, in which only integers can
356
356
	 * be put.
357
357
	 *
358
358
	 * The caller can set a range, a start value, and a step size.
359
359
	 *
360
360
	 * The spinner created with this method may modify itself based on the
361
361
	 * parameters.
362
362
	 * For example: If the minimum and maximum value are equal, the spinner will
363
363
	 * be disabled.
364
364
	 *
365
365
	 * @param minimum The minimum value that can be selected.
366
366
	 * @param maximum The maximum value that can be selected.
367
367
	 * @param start The value that will initially be shown in the component.
368
368
	 * @param stepSize The step size when the user increases/decreases the
369
369
	 * value.
370
370
	 * @throws IllegalArgumentException if minimum is larger than maximum, 
371
371
	 * start is not in the range of the selectable values, or stepsize is not a
372
372
	 * natural number.
373
373
	 * @return The JSpinner that was added to the window.
374
374
	 */
375
375
	public JSpinner createSpinner(int minimum, int maximum, int start, int stepSize) {
376
376
		// As usual, we begin with checking the contract:
377
377
		if(minimum > maximum) {
378
378
			throw new IllegalArgumentException("The minimum value ("+ minimum +") was larger than the maximum value ("+ maximum +")");
379
379
		}
380
380
		// The "start ∉ [minimum, maximum]" is done by the SpinnerNumberModel
381
381
		// constructor, which will be constructed later.
382
382
		if(stepSize <= 0) { // stepSize ∉ ℕ¹ (In Belgium: ℕ₀)
383
383
			throw new IllegalArgumentException("The stepSize ("+ stepSize +") is not a natural number (excluding 0).");
384
384
		}
385
385
		// If the contract is valid, we can begin:
386
386
		/*
387
387
		 * I'd like to interject here, because this is a nice example of why
388
388
		 * JSugar was a good idea:
389
389
		 * If you want a spinner, you'll typically want an integer spinner. But
390
390
		 * in Swing, when you create a JSpinner, it creates a JSpinner, with a
391
391
		 * predefined 'SpinnerNumberModel' attached to it.
392
392
		 * It's this model you then have to extract from the created spinner, on
393
393
		 * which you need to apply the configuration.
394
394
		 * What you actually have to do, is create a SpinnerNumberModel
395
395
		 * yourself, put your settings on that, and then, create a JSpinner to
396
396
		 * which you give that SpinnerNumberModel.
397
397
		 * In essence: The entire Java framework is shit.
398
398
		 */
399
399
		SpinnerNumberModel spinnerSettings = new SpinnerNumberModel(
400
400
				start,
401
401
				minimum,
402
402
				maximum,
403
403
				stepSize
404
404
				);
405
405
		JSpinner spinner = new JSpinner(spinnerSettings);
406
406
		if(minimum == maximum) { // Trivial value is set already, --> disable.
407
407
			spinner.setEnabled(false);
408
408
		}
409
409
		this.addComponent(spinner);
410
410
		return spinner;
411
411
	}
412
412
413
413
	/**
414
414
	 * Adds a number spinner component to the GUI.
415
415
	 * This method allows the caller to create a so-called "spinner component"
416
416
	 * to the window. This spinner is an input box, in which only integers can
417
417
	 * be put.
418
418
	 * 
419
419
	 * Tip: This method is a convenience method, and works extremely well with
420
420
	 * arbitrary data.
421
421
	 * For example: The start value is automatically set to the minimal possible
422
422
	 * value, and the step size defaults to 1.
423
423
	 * If the minimum and maximum are equal, the component will be disabled, and
424
424
	 * thus, be locked on the only (trivially) possible value.
425
425
	 * If minimum is larger than maximum, the method will notify you of this,
426
426
	 * but also swap the values. So you can rest assured that the spinner will
427
427
	 * handle errors, but also, inform you about it.
428
428
	 * @param minimum The minimum value that can be selected.
429
429
	 * @param maximum The maximum value that can be selected.
430
430
	 * @return The JSpinner component that was added to the window.
431
431
	 */
432
432
	public JSpinner createSpinner(int minimum, int maximum) {
433
433
		// The disabling of equal values is done in the full createSpinner(), so
434
434
		// this is merely switching values if they need to be swapped.
435
435
		if(minimum > maximum) {
436
436
			System.err.println("minimum ("+ minimum +") was larger than maximum ("+ maximum +").");
437
437
			// FIXME: Consider whether it's appropriate to print a stacktrace
438
438
			// here, which may be convenient for debugging.
439
439
			
440
440
			// XXX: I know you don't need the help variable when swapping
441
441
			// integers, because you can also do basic arithmetics. Change it if
442
442
			// it causes too much eye burn for you.
443
443
			int swapValue = minimum;
444
444
			minimum = maximum;
445
445
			maximum = swapValue;
446
446
		}
447
447
448
448
		// Yeah, these 2 variables make you cringe huh, performance addicts?
449
449
		// Drown me in the tears of your useless performance-related opinions.
450
450
		int startValue = minimum;
451
451
		int stepSize = 1;
452
452
		return this.createSpinner(minimum, maximum, startValue, stepSize);
453
453
	}
454
454
455
455
	/**
+
456
	 * Adds a combobox to the GUI.
+
457
	 * Allows the caller to create a combobox by providing the values that
+
458
	 * should be put in it.
+
459
	 *
+
460
	 * This method can only be used for String values. If that is not what you
+
461
	 * need, consider creating your own combobox and adding it manually. Or, if
+
462
	 * you need a combobox for integers, consider {@link #createSpinner()}.
+
463
	 *
+
464
	 * WARNING: {@see JComboBox#getSelectedItem()} returns an object, not a
+
465
	 * String. You need to manually typecast this. This is a constraint of the
+
466
	 * Swing framework.
+
467
	 * @param items An array of Strings that will be put in the combobox.
+
468
	 * @throws NullPointerException if one of the values in items is a null
+
469
	 * pointer.
+
470
	 * @throws IllegalArgumentException if items is empty.
+
471
	 * @return The JCombobox that was added to the window.
+
472
	 */
+
473
	public JComboBox<String> addComboBox(String[] items) {
+
474
		// Contract validation:
+
475
		if(items.length == 0) {
+
476
			throw new IllegalArgumentException("The given array of items was empty.");
+
477
		}
+
478
		for(String item : items) {
+
479
			if(item == null) {
+
480
				throw new NullPointerException("One of the given Strings is a null pointer.");
+
481
			}
+
482
		}
+
483
		// Contract validated, create the component:
+
484
		JComboBox<String> comboBox = new JComboBox<String>(items);
+
485
		comboBox.setSelectedIndex(0);
+
486
		if(comboBox.getItemCount() == 1) { // Trivial selection
+
487
			comboBox.setEnabled(false);
+
488
		}
+
489
		this.addComponent(comboBox);
+
490
		return comboBox;
+
491
	}
+
492
+
493
	/**
456
494
	 * Adds the given component to the GUI.
457
495
	 * This method allows its caller to give a pre-made component, so that it
458
496
	 * can be added to the GUI. Even though its main use is for the Window class
459
497
	 * itself, the user of JSugar can also use it to create components himself,
460
498
	 * and then add them. As such, this method doesn't provide parameters for
461
499
	 * reflection/action triggering purposes.
462
500
	 * @param component The component to be added to the window.
463
501
	 * @throws NullPointerException if the given component is a null pointer.
464
502
	 */
465
503
	public void addComponent(JComponent component) {
466
504
		int originalSize = this.panel.getComponentCount();
467
505
		this.panel.add(component); // Throws the exception if null.
468
506
		this.updateWindow();
469
507
470
508
		assert originalSize == this.panel.getComponentCount()-1 : "A component was supposed to be added to the window, but the total amount of components was unchanged after the addition.";
471
509
	}
472
510
473
511
	/**
474
512
	 * Removes the given component from the GUI.
475
513
	 * This method allows its caller to remove a component from the GUI.
476
514
	 * @param component The component to be removed.
477
515
	 * @throws NoSuchElementException if the given component does not exist in
478
516
	 * the GUI.
479
517
	 * @throws NullPointerException if the given component is a null pointer.
480
518
	 */
481
519
	public void removeComponent(JComponent component) {
482
520
		int originalSize = this.panel.getComponentCount();
483
521
		this.panel.remove(component);
484
522
		int newSize = this.panel.getComponentCount();
485
523
		if (originalSize != newSize+1) {
486
524
			throw new NoSuchElementException("The given component does not exist in the GUI.");
487
525
		}
488
526
		this.updateWindow();
489
527
	}
490
528
}
491
529