OOP2

Window.java

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