1 package tim.prune.function;
3 import java.awt.BorderLayout;
4 import java.awt.Component;
5 import java.awt.Dimension;
6 import java.awt.FlowLayout;
7 import java.awt.GridLayout;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.awt.event.FocusAdapter;
11 import java.awt.event.FocusEvent;
12 import java.awt.event.FocusListener;
13 import java.awt.event.KeyAdapter;
14 import java.awt.event.KeyEvent;
15 import java.util.ArrayList;
16 import java.util.TimeZone;
17 import java.util.TreeSet;
19 import javax.swing.BorderFactory;
20 import javax.swing.BoxLayout;
21 import javax.swing.ButtonGroup;
22 import javax.swing.JButton;
23 import javax.swing.JDialog;
24 import javax.swing.JLabel;
25 import javax.swing.JPanel;
26 import javax.swing.JRadioButton;
27 import javax.swing.JScrollPane;
28 import javax.swing.event.ListSelectionEvent;
29 import javax.swing.event.ListSelectionListener;
32 import tim.prune.DataSubscriber;
33 import tim.prune.GenericFunction;
34 import tim.prune.I18nManager;
35 import tim.prune.UpdateMessageBroker;
36 import tim.prune.config.Config;
37 import tim.prune.gui.CombinedListAndModel;
38 import tim.prune.gui.GuiGridLayout;
41 * Class to provide the gui for selecting an alternative timezone
43 public class SelectTimezoneFunction extends GenericFunction
45 /** Arraylist of timezone infos */
46 private ArrayList<TimezoneDetails> _zoneInfo;
48 private JDialog _dialog = null;
49 /** Radio button to select system timezone instead of using listboxes */
50 private JRadioButton _systemRadio = null;
51 /** Radio button to select timezone using listboxes */
52 private JRadioButton _customRadio = null;
53 /** Array of list boxes */
54 private CombinedListAndModel[] _listBoxes = null;
55 /** Label for selected zone */
56 private JLabel _selectedZoneLabel = null;
57 /** Label for offset of selected zone */
58 private JLabel _selectedOffsetLabel = null;
59 /** OK button for finishing */
60 private JButton _okButton = null;
62 private static final int LIST_REGIONS = 0;
63 private static final int LIST_OFFSETS = 1;
64 private static final int LIST_GROUPS = 2;
65 private static final int LIST_NAMES = 3;
68 * Inner class for listening to list clicks
70 class ListListener implements ListSelectionListener
74 ListListener(int inKey) {_key = inKey;}
75 /** Listen for selection changes */
76 public void valueChanged(ListSelectionEvent inEvent) {
77 if (!inEvent.getValueIsAdjusting()) {
78 processListClick(_key);
83 /** Inner class to hold categorisation info for a timezone */
87 public String _region;
95 * @param inApp App object
97 public SelectTimezoneFunction(App inApp)
102 /** Get the name key */
103 public String getNameKey() {
104 return "function.selecttimezone";
114 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
115 _dialog.setLocationRelativeTo(_parentFrame);
116 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
117 _dialog.getContentPane().add(makeDialogComponents());
120 collectTimezoneInfo();
121 _systemRadio.setText(I18nManager.getText("dialog.settimezone.system") + " ("
122 + TimeZone.getDefault().getID() + ")");
123 // Set up dialog according to current config
124 String selectedTimezone = Config.getConfigString(Config.KEY_TIMEZONE_ID);
125 if (selectedTimezone == null || selectedTimezone.equals(""))
127 _systemRadio.setSelected(true);
131 _customRadio.setSelected(true);
133 _dialog.setVisible(true);
137 * Create dialog components
138 * @return Panel containing all gui elements in dialog
140 private Component makeDialogComponents()
142 JPanel dialogPanel = new JPanel();
143 dialogPanel.setLayout(new BorderLayout(5, 5));
144 // Listener for radio buttons
145 ActionListener radioListener = new ActionListener() {
146 public void actionPerformed(ActionEvent inEvent) {
147 radioSelected(_systemRadio.isSelected());
150 FocusListener radioFocusListener = new FocusAdapter() {
151 public void focusGained(FocusEvent inEvent) {
152 radioSelected(_systemRadio.isSelected());
157 JPanel topPanel = new JPanel();
158 topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
159 JLabel topLabel = new JLabel(I18nManager.getText("dialog.settimezone.intro"));
160 topLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
161 topPanel.add(topLabel);
162 _systemRadio = new JRadioButton(I18nManager.getText("dialog.settimezone.system"));
163 _systemRadio.addActionListener(radioListener);
164 _systemRadio.addFocusListener(radioFocusListener);
165 topPanel.add(_systemRadio);
166 _customRadio = new JRadioButton(I18nManager.getText("dialog.settimezone.custom"));
167 _customRadio.addActionListener(radioListener);
168 _customRadio.addFocusListener(radioFocusListener);
169 topPanel.add(_customRadio);
170 ButtonGroup radioGroup = new ButtonGroup();
171 radioGroup.add(_systemRadio); radioGroup.add(_customRadio);
172 dialogPanel.add(topPanel, BorderLayout.NORTH);
174 // Main panel with box layout, list Panel with four lists in a grid
175 JPanel mainPanel = new JPanel();
176 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
178 JPanel listsPanel = new JPanel();
179 listsPanel.setLayout(new GridLayout(1, 4));
180 _listBoxes = new CombinedListAndModel[4];
181 // First list for regions
182 _listBoxes[LIST_REGIONS] = new CombinedListAndModel(0);
183 // Add listener for list selection changes
184 _listBoxes[LIST_REGIONS].addListSelectionListener(new ListListener(LIST_REGIONS));
185 JScrollPane scrollPane = new JScrollPane(_listBoxes[LIST_REGIONS]);
186 scrollPane.setPreferredSize(new Dimension(100, 200));
187 scrollPane.setMinimumSize(new Dimension(100, 200));
188 scrollPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
189 listsPanel.add(scrollPane);
191 // second list for offsets
192 _listBoxes[LIST_OFFSETS] = new CombinedListAndModel(1);
193 _listBoxes[LIST_OFFSETS].setMaxNumEntries(24);
194 _listBoxes[LIST_OFFSETS].addListSelectionListener(new ListListener(LIST_OFFSETS));
195 scrollPane = new JScrollPane(_listBoxes[LIST_OFFSETS]);
196 scrollPane.setPreferredSize(new Dimension(100, 200));
197 scrollPane.setMinimumSize(new Dimension(100, 200));
198 scrollPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
199 listsPanel.add(scrollPane);
201 // third list for groups
202 _listBoxes[LIST_GROUPS] = new CombinedListAndModel(2);
203 _listBoxes[LIST_GROUPS].setMaxNumEntries(20);
204 _listBoxes[LIST_GROUPS].addListSelectionListener(new ListListener(LIST_GROUPS));
205 scrollPane = new JScrollPane(_listBoxes[LIST_GROUPS]);
206 scrollPane.setPreferredSize(new Dimension(100, 200));
207 scrollPane.setMinimumSize(new Dimension(100, 200));
208 scrollPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
209 listsPanel.add(scrollPane);
211 // fourth list for names
212 _listBoxes[LIST_NAMES] = new CombinedListAndModel(3);
213 _listBoxes[LIST_NAMES].setMaxNumEntries(20);
214 _listBoxes[LIST_NAMES].addListSelectionListener(new ListListener(LIST_NAMES));
215 scrollPane = new JScrollPane(_listBoxes[LIST_NAMES]);
216 scrollPane.setPreferredSize(new Dimension(100, 200));
217 scrollPane.setMinimumSize(new Dimension(100, 200));
218 scrollPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
219 listsPanel.add(scrollPane);
220 mainPanel.add(listsPanel);
222 // Details labels underneath lists - description and offset
223 JPanel detailsPanel = new JPanel();
224 GuiGridLayout grid = new GuiGridLayout(detailsPanel);
225 grid.add(new JLabel(I18nManager.getText("dialog.settimezone.selectedzone") + " :"));
226 _selectedZoneLabel = new JLabel("");
227 grid.add(_selectedZoneLabel);
228 grid.add(new JLabel(I18nManager.getText("dialog.settimezone.offsetfromutc") + " :"));
229 _selectedOffsetLabel = new JLabel("");
230 grid.add(_selectedOffsetLabel);
231 mainPanel.add(detailsPanel);
232 dialogPanel.add(mainPanel, BorderLayout.CENTER);
234 // close window if escape pressed
235 KeyAdapter escListener = new KeyAdapter() {
236 public void keyReleased(KeyEvent inE) {
237 if (inE.getKeyCode() == KeyEvent.VK_ESCAPE) {
242 _listBoxes[LIST_REGIONS].addKeyListener(escListener);
243 _listBoxes[LIST_OFFSETS].addKeyListener(escListener);
245 // button panel at bottom
246 JPanel buttonPanel = new JPanel();
247 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
249 _okButton = new JButton(I18nManager.getText("button.ok"));
250 _okButton.addActionListener(new ActionListener() {
251 public void actionPerformed(ActionEvent e) {
252 finishSelectTimezone();
255 buttonPanel.add(_okButton);
256 _okButton.addKeyListener(escListener);
258 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
259 cancelButton.addActionListener(new ActionListener() {
260 public void actionPerformed(ActionEvent e) {
264 cancelButton.addKeyListener(new KeyAdapter() {
265 public void keyPressed(KeyEvent inE) {
266 if (inE.getKeyCode() == KeyEvent.VK_ESCAPE) {_dialog.dispose();}
269 buttonPanel.add(cancelButton);
270 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
275 * React to changes in the radio buttons
276 * @param inUseSystem true for system, false for custom
278 private void radioSelected(boolean inUseSystem)
280 for (int i=0; i<_listBoxes.length; i++)
284 _listBoxes[i].clear();
286 _listBoxes[i].setEnabled(!inUseSystem);
290 populateTimezoneRegions();
291 populateTimezoneOffsets(null);
292 preselectTimezone(Config.getConfigString(Config.KEY_TIMEZONE_ID));
294 showTimezoneDetails();
298 * React to a selection change on one of our lists
299 * @param inKey key of list which was clicked
301 private void processListClick(int inKey)
303 final boolean offsetSelected = _listBoxes[LIST_OFFSETS].getSelectedItem() != null;
304 final boolean groupSelected = _listBoxes[LIST_GROUPS].getSelectedItem() != null;
306 if (inKey == LIST_REGIONS)
308 populateTimezoneOffsets(_listBoxes[LIST_REGIONS].getSelectedItem());
311 if (inKey == LIST_OFFSETS
312 || (inKey == LIST_REGIONS && !offsetSelected))
314 populateTimezoneGroups(_listBoxes[LIST_REGIONS].getSelectedItem(), _listBoxes[LIST_OFFSETS].getSelectedItem());
317 if (inKey == LIST_GROUPS
318 || (inKey <= LIST_OFFSETS && !groupSelected))
320 populateTimezoneNames(_listBoxes[LIST_REGIONS].getSelectedItem(), _listBoxes[LIST_OFFSETS].getSelectedItem(),
321 _listBoxes[LIST_GROUPS].getSelectedItem());
323 // Show the details of the selected timezone
324 showTimezoneDetails();
328 * Use the system information to populate the list of available timezones
330 private void collectTimezoneInfo()
332 _zoneInfo = new ArrayList<TimezoneDetails>();
333 for (String id : TimeZone.getAvailableIDs())
335 String region = getRegion(id);
338 TimeZone tz = TimeZone.getTimeZone(id);
339 TimezoneDetails details = new TimezoneDetails();
341 details._region = region;
342 details._offset = tz.getOffset(System.currentTimeMillis()) / 1000 / 60;
343 details._group = tz.getDisplayName();
344 details._name = getNameWithoutRegion(id);
345 _zoneInfo.add(details);
351 * Populate the timezone regions into the region list
353 private void populateTimezoneRegions()
355 _listBoxes[LIST_REGIONS].clear();
356 TreeSet<String> regions = new TreeSet<String>();
357 for (TimezoneDetails currZone : _zoneInfo)
359 regions.add(currZone._region);
361 for (String region : regions)
363 _listBoxes[LIST_REGIONS].addItem(region);
368 * Extract the timezone region from the id
370 private static String getRegion(String inId)
372 final int slashPos = (inId == null ? -1 : inId.indexOf('/'));
375 return inId.substring(0, slashPos);
381 * Populate the second listbox with the offsets for the given region
382 * @param inRegion selected region, or null if none selected
384 private void populateTimezoneOffsets(String inRegion)
386 _listBoxes[LIST_OFFSETS].clear();
387 TreeSet<Integer> offsetsinMinutes = new TreeSet<Integer>();
388 for (TimezoneDetails currZone : _zoneInfo)
390 String region = currZone._region;
391 if (inRegion == null || region.equals(inRegion))
393 offsetsinMinutes.add(currZone._offset);
396 for (Integer offset : offsetsinMinutes)
398 _listBoxes[LIST_OFFSETS].addItem(makeOffsetString(offset));
403 * @return String containing offset for display
405 private static String makeOffsetString(int inOffsetInMinutes)
407 if (inOffsetInMinutes == 0) return "0";
408 final boolean isWholeHours = (inOffsetInMinutes % 60) == 0;
411 return (inOffsetInMinutes > 0 ? "+" : "") + (inOffsetInMinutes / 60);
413 final double numHours = inOffsetInMinutes / 60.0;
414 return (inOffsetInMinutes > 0 ? "+" : "") + numHours;
418 * Populate the group list using the specified region and offset
419 * @param inRegion selected region (if any) from the first list
420 * @param inOffset selected offset (if any) from the second list
422 private void populateTimezoneGroups(String inRegion, String inOffset)
424 _listBoxes[LIST_GROUPS].clear();
425 // Convert given offset string (in hours) into numeric offset (in minutes)
426 final int offsetMins = convertToMinutes(inOffset);
428 TreeSet<String> zoneGroups = new TreeSet<String>();
429 for (TimezoneDetails currZone : _zoneInfo)
431 if (inRegion == null || currZone._region.equals(inRegion))
433 if (offsetMins == -1 || offsetMins == currZone._offset)
435 zoneGroups.add(currZone._group);
439 // If the region and offset were given, then list is unlimited
440 _listBoxes[LIST_GROUPS].setUnlimited(inRegion != null && inOffset != null);
441 // Add all the found names to the listbox
442 for (String group : zoneGroups)
444 _listBoxes[LIST_GROUPS].addItem(group);
449 * Populate the group list using the specified region, offset and group
450 * @param inRegion selected region (if any) from the first list
451 * @param inOffset selected offset (if any) from the second list
452 * @param inGroup selected group (if any) from the third list
454 private void populateTimezoneNames(String inRegion, String inOffset, String inGroup)
456 CombinedListAndModel nameList = _listBoxes[LIST_NAMES];
458 // Convert given offset string (in hours) into numeric offset (in minutes)
459 final int offsetMins = convertToMinutes(inOffset);
461 TreeSet<String> zoneNames = new TreeSet<String>();
462 for (TimezoneDetails currZone : _zoneInfo)
464 if ((inRegion == null || currZone._region.equals(inRegion))
465 && (offsetMins == -1 || currZone._offset == offsetMins)
466 && (inGroup == null || currZone._group.equals(inGroup)))
468 zoneNames.add(currZone._name);
471 // If the region and offset were given, then list is unlimited
472 nameList.setUnlimited(inRegion != null && inOffset != null && inRegion != null);
473 // Add all the found names to the listbox
474 for (String name : zoneNames)
476 nameList.addItem(name);
481 * Convert the given String from hours to minutes
482 * @param inOffsetInHours String from listbox in +/- hours
483 * @return offset in minutes, or -1
485 private static int convertToMinutes(String inOffsetInHours)
489 offsetMins = (int) (60 * Double.parseDouble(inOffsetInHours));
491 catch (NumberFormatException nfe) {} // offset stays -1
492 catch (NullPointerException npe) {} // offset stays -1
497 * Remove the timezone region from the id to just leave the name after the slash
499 private static String getNameWithoutRegion(String inId)
501 final int slashPos = (inId == null ? -1 : inId.indexOf('/'));
504 return inId.substring(slashPos + 1);
510 * Get the selected timezone, or null if none selected
512 private TimeZone getSelectedTimezone()
514 if (_systemRadio.isSelected())
516 return TimeZone.getDefault();
519 String chosenRegion = _listBoxes[LIST_REGIONS].getSelectedItem();
520 // Convert given offset string (in hours) into numeric offset (in minutes)
521 final int offsetMins = convertToMinutes(_listBoxes[LIST_OFFSETS].getSelectedItem());
522 String chosenGroup = _listBoxes[LIST_GROUPS].getSelectedItem();
523 String chosenName = _listBoxes[LIST_NAMES].getSelectedItem();
525 TreeSet<String> zoneIds = new TreeSet<String>();
526 for (TimezoneDetails currZone : _zoneInfo)
528 if ((chosenRegion == null || currZone._region.equals(chosenRegion))
529 && (offsetMins == -1 || currZone._offset == offsetMins)
530 && (chosenGroup == null || currZone._group.equals(chosenGroup))
531 && (chosenName == null || currZone._name.equals(chosenName)))
533 zoneIds.add(currZone._id);
534 if (zoneIds.size() > 1) {
535 break; // exit loop now, we've got too many
539 // Should have exactly one result now
540 if (zoneIds.size() == 1)
542 return TimeZone.getTimeZone(zoneIds.first());
545 // none selected (yet)
550 * Show the details of the selected timezone
552 private void showTimezoneDetails()
554 TimeZone selectedTimezone = getSelectedTimezone();
555 if (selectedTimezone == null)
557 // Clear details labels
558 _selectedZoneLabel.setText("");
559 _selectedOffsetLabel.setText("");
563 // Fill results in labels
564 String desc = selectedTimezone.getID() + " - " + selectedTimezone.getDisplayName();
565 _selectedZoneLabel.setText(desc);
566 String offsets = getOffsetDescription(selectedTimezone);
567 _selectedOffsetLabel.setText(offsets);
569 _okButton.setEnabled(selectedTimezone != null);
573 * @param inTimezone selected timezone
574 * @return String describing the time offset(s) of this zone including winter/summer time
576 private static String getOffsetDescription(TimeZone inTimezone)
578 if (inTimezone == null)
582 TreeSet<Integer> offsetsinMinutes = new TreeSet<Integer>();
583 long testTimeMillis = System.currentTimeMillis();
584 final long testPeriodInMillis = 1000L * 60 * 60 * 24 * 30 * 2;
585 for (int i=0; i<5; i++)
587 offsetsinMinutes.add(inTimezone.getOffset(testTimeMillis) / 1000 / 60);
588 testTimeMillis += testPeriodInMillis;
590 // Make String describing the sorted set
591 StringBuffer buff = new StringBuffer();
592 for (Integer offset : offsetsinMinutes)
594 if (buff.length() > 0)
598 buff.append(makeOffsetString(offset));
600 return buff.toString();
604 * On entry to the dialog, select the items in each listbox
605 * according to the given preselected timezone id
606 * @param zoneId id of zone to select
608 private void preselectTimezone(String zoneId)
610 TimeZone tz = (zoneId == null ? TimeZone.getDefault() : TimeZone.getTimeZone(zoneId));
613 _listBoxes[LIST_REGIONS].selectItem(getRegion(zoneId));
614 _listBoxes[LIST_OFFSETS].selectItem(makeOffsetString(tz.getOffset(System.currentTimeMillis()) / 1000 / 60));
615 _listBoxes[LIST_GROUPS].selectItem(tz.getDisplayName());
616 _listBoxes[LIST_NAMES].selectItem(getNameWithoutRegion(zoneId));
621 * Finish the dialog by setting the config according to the selected zone
623 private void finishSelectTimezone()
625 TimeZone selectedTimezone = getSelectedTimezone();
626 if (_systemRadio.isSelected() || selectedTimezone == null)
628 // Clear config, use default system timezone instead
629 Config.setConfigString(Config.KEY_TIMEZONE_ID, null);
633 // Get selected timezone, set in config
634 Config.setConfigString(Config.KEY_TIMEZONE_ID, selectedTimezone.getID());
637 // Make sure listeners know to update themselves
638 UpdateMessageBroker.informSubscribers(DataSubscriber.UNITS_CHANGED);