X-Git-Url: http://gitweb.fperrin.net/?p=GpsPrune.git;a=blobdiff_plain;f=tim%2Fprune%2Ffunction%2FSelectTimezoneFunction.java;fp=tim%2Fprune%2Ffunction%2FSelectTimezoneFunction.java;h=8f5f778ed1a8481db6161a90ab5071caf21abe99;hp=0000000000000000000000000000000000000000;hb=92dad5df664287acb51728e9ea599f150765d34a;hpb=81843c3d8d0771bf00d0f26034a13aa515465c78 diff --git a/tim/prune/function/SelectTimezoneFunction.java b/tim/prune/function/SelectTimezoneFunction.java new file mode 100644 index 0000000..8f5f778 --- /dev/null +++ b/tim/prune/function/SelectTimezoneFunction.java @@ -0,0 +1,640 @@ +package tim.prune.function; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.TimeZone; +import java.util.TreeSet; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import tim.prune.App; +import tim.prune.DataSubscriber; +import tim.prune.GenericFunction; +import tim.prune.I18nManager; +import tim.prune.UpdateMessageBroker; +import tim.prune.config.Config; +import tim.prune.gui.CombinedListAndModel; +import tim.prune.gui.GuiGridLayout; + +/** + * Class to provide the gui for selecting an alternative timezone + */ +public class SelectTimezoneFunction extends GenericFunction +{ + /** Arraylist of timezone infos */ + private ArrayList _zoneInfo; + /** Dialog */ + private JDialog _dialog = null; + /** Radio button to select system timezone instead of using listboxes */ + private JRadioButton _systemRadio = null; + /** Radio button to select timezone using listboxes */ + private JRadioButton _customRadio = null; + /** Array of list boxes */ + private CombinedListAndModel[] _listBoxes = null; + /** Label for selected zone */ + private JLabel _selectedZoneLabel = null; + /** Label for offset of selected zone */ + private JLabel _selectedOffsetLabel = null; + /** OK button for finishing */ + private JButton _okButton = null; + + private static final int LIST_REGIONS = 0; + private static final int LIST_OFFSETS = 1; + private static final int LIST_GROUPS = 2; + private static final int LIST_NAMES = 3; + + /** + * Inner class for listening to list clicks + */ + class ListListener implements ListSelectionListener + { + private int _key = 0; + /** Constructor */ + ListListener(int inKey) {_key = inKey;} + /** Listen for selection changes */ + public void valueChanged(ListSelectionEvent inEvent) { + if (!inEvent.getValueIsAdjusting()) { + processListClick(_key); + } + } + } + + /** Inner class to hold categorisation info for a timezone */ + class TimezoneDetails + { + public String _id; + public String _region; + public int _offset; + public String _group; + public String _name; + } + + /** + * Constructor + * @param inApp App object + */ + public SelectTimezoneFunction(App inApp) + { + super(inApp); + } + + /** Get the name key */ + public String getNameKey() { + return "function.selecttimezone"; + } + + /** + * Begin the function + */ + public void begin() + { + if (_dialog == null) + { + _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true); + _dialog.setLocationRelativeTo(_parentFrame); + _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + _dialog.getContentPane().add(makeDialogComponents()); + _dialog.pack(); + } + collectTimezoneInfo(); + _systemRadio.setText(I18nManager.getText("dialog.settimezone.system") + " (" + + TimeZone.getDefault().getID() + ")"); + // Set up dialog according to current config + String selectedTimezone = Config.getConfigString(Config.KEY_TIMEZONE_ID); + if (selectedTimezone == null || selectedTimezone.equals("")) + { + _systemRadio.setSelected(true); + } + else + { + _customRadio.setSelected(true); + } + _dialog.setVisible(true); + } + + /** + * Create dialog components + * @return Panel containing all gui elements in dialog + */ + private Component makeDialogComponents() + { + JPanel dialogPanel = new JPanel(); + dialogPanel.setLayout(new BorderLayout(5, 5)); + // Listener for radio buttons + ActionListener radioListener = new ActionListener() { + public void actionPerformed(ActionEvent inEvent) { + radioSelected(_systemRadio.isSelected()); + } + }; + FocusListener radioFocusListener = new FocusAdapter() { + public void focusGained(FocusEvent inEvent) { + radioSelected(_systemRadio.isSelected()); + } + }; + + // Panel at top + JPanel topPanel = new JPanel(); + topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS)); + JLabel topLabel = new JLabel(I18nManager.getText("dialog.settimezone.intro")); + topLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + topPanel.add(topLabel); + _systemRadio = new JRadioButton(I18nManager.getText("dialog.settimezone.system")); + _systemRadio.addActionListener(radioListener); + _systemRadio.addFocusListener(radioFocusListener); + topPanel.add(_systemRadio); + _customRadio = new JRadioButton(I18nManager.getText("dialog.settimezone.custom")); + _customRadio.addActionListener(radioListener); + _customRadio.addFocusListener(radioFocusListener); + topPanel.add(_customRadio); + ButtonGroup radioGroup = new ButtonGroup(); + radioGroup.add(_systemRadio); radioGroup.add(_customRadio); + dialogPanel.add(topPanel, BorderLayout.NORTH); + + // Main panel with box layout, list Panel with four lists in a grid + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); + + JPanel listsPanel = new JPanel(); + listsPanel.setLayout(new GridLayout(1, 4)); + _listBoxes = new CombinedListAndModel[4]; + // First list for regions + _listBoxes[LIST_REGIONS] = new CombinedListAndModel(0); + // Add listener for list selection changes + _listBoxes[LIST_REGIONS].addListSelectionListener(new ListListener(LIST_REGIONS)); + JScrollPane scrollPane = new JScrollPane(_listBoxes[LIST_REGIONS]); + scrollPane.setPreferredSize(new Dimension(100, 200)); + scrollPane.setMinimumSize(new Dimension(100, 200)); + scrollPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + listsPanel.add(scrollPane); + + // second list for offsets + _listBoxes[LIST_OFFSETS] = new CombinedListAndModel(1); + _listBoxes[LIST_OFFSETS].setMaxNumEntries(24); + _listBoxes[LIST_OFFSETS].addListSelectionListener(new ListListener(LIST_OFFSETS)); + scrollPane = new JScrollPane(_listBoxes[LIST_OFFSETS]); + scrollPane.setPreferredSize(new Dimension(100, 200)); + scrollPane.setMinimumSize(new Dimension(100, 200)); + scrollPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + listsPanel.add(scrollPane); + + // third list for groups + _listBoxes[LIST_GROUPS] = new CombinedListAndModel(2); + _listBoxes[LIST_GROUPS].setMaxNumEntries(20); + _listBoxes[LIST_GROUPS].addListSelectionListener(new ListListener(LIST_GROUPS)); + scrollPane = new JScrollPane(_listBoxes[LIST_GROUPS]); + scrollPane.setPreferredSize(new Dimension(100, 200)); + scrollPane.setMinimumSize(new Dimension(100, 200)); + scrollPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + listsPanel.add(scrollPane); + + // fourth list for names + _listBoxes[LIST_NAMES] = new CombinedListAndModel(3); + _listBoxes[LIST_NAMES].setMaxNumEntries(20); + _listBoxes[LIST_NAMES].addListSelectionListener(new ListListener(LIST_NAMES)); + scrollPane = new JScrollPane(_listBoxes[LIST_NAMES]); + scrollPane.setPreferredSize(new Dimension(100, 200)); + scrollPane.setMinimumSize(new Dimension(100, 200)); + scrollPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + listsPanel.add(scrollPane); + mainPanel.add(listsPanel); + + // Details labels underneath lists - description and offset + JPanel detailsPanel = new JPanel(); + GuiGridLayout grid = new GuiGridLayout(detailsPanel); + grid.add(new JLabel(I18nManager.getText("dialog.settimezone.selectedzone") + " :")); + _selectedZoneLabel = new JLabel(""); + grid.add(_selectedZoneLabel); + grid.add(new JLabel(I18nManager.getText("dialog.settimezone.offsetfromutc") + " :")); + _selectedOffsetLabel = new JLabel(""); + grid.add(_selectedOffsetLabel); + mainPanel.add(detailsPanel); + dialogPanel.add(mainPanel, BorderLayout.CENTER); + + // close window if escape pressed + KeyAdapter escListener = new KeyAdapter() { + public void keyReleased(KeyEvent inE) { + if (inE.getKeyCode() == KeyEvent.VK_ESCAPE) { + _dialog.dispose(); + } + } + }; + _listBoxes[LIST_REGIONS].addKeyListener(escListener); + _listBoxes[LIST_OFFSETS].addKeyListener(escListener); + + // button panel at bottom + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + // OK button + _okButton = new JButton(I18nManager.getText("button.ok")); + _okButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + finishSelectTimezone(); + } + }); + buttonPanel.add(_okButton); + _okButton.addKeyListener(escListener); + // Cancel button + JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + _dialog.dispose(); + } + }); + cancelButton.addKeyListener(new KeyAdapter() { + public void keyPressed(KeyEvent inE) { + if (inE.getKeyCode() == KeyEvent.VK_ESCAPE) {_dialog.dispose();} + } + }); + buttonPanel.add(cancelButton); + dialogPanel.add(buttonPanel, BorderLayout.SOUTH); + return dialogPanel; + } + + /** + * React to changes in the radio buttons + * @param inUseSystem true for system, false for custom + */ + private void radioSelected(boolean inUseSystem) + { + for (int i=0; i<_listBoxes.length; i++) + { + if (inUseSystem) + { + _listBoxes[i].clear(); + } + _listBoxes[i].setEnabled(!inUseSystem); + } + if (!inUseSystem) + { + populateTimezoneRegions(); + populateTimezoneOffsets(null); + preselectTimezone(Config.getConfigString(Config.KEY_TIMEZONE_ID)); + } + showTimezoneDetails(); + } + + /** + * React to a selection change on one of our lists + * @param inKey key of list which was clicked + */ + private void processListClick(int inKey) + { + final boolean offsetSelected = _listBoxes[LIST_OFFSETS].getSelectedItem() != null; + final boolean groupSelected = _listBoxes[LIST_GROUPS].getSelectedItem() != null; + // Update offsets? + if (inKey == LIST_REGIONS) + { + populateTimezoneOffsets(_listBoxes[LIST_REGIONS].getSelectedItem()); + } + // Update groups? + if (inKey == LIST_OFFSETS + || (inKey == LIST_REGIONS && !offsetSelected)) + { + populateTimezoneGroups(_listBoxes[LIST_REGIONS].getSelectedItem(), _listBoxes[LIST_OFFSETS].getSelectedItem()); + } + // Update names? + if (inKey == LIST_GROUPS + || (inKey <= LIST_OFFSETS && !groupSelected)) + { + populateTimezoneNames(_listBoxes[LIST_REGIONS].getSelectedItem(), _listBoxes[LIST_OFFSETS].getSelectedItem(), + _listBoxes[LIST_GROUPS].getSelectedItem()); + } + // Show the details of the selected timezone + showTimezoneDetails(); + } + + /** + * Use the system information to populate the list of available timezones + */ + private void collectTimezoneInfo() + { + _zoneInfo = new ArrayList(); + for (String id : TimeZone.getAvailableIDs()) + { + String region = getRegion(id); + if (region != null) + { + TimeZone tz = TimeZone.getTimeZone(id); + TimezoneDetails details = new TimezoneDetails(); + details._id = id; + details._region = region; + details._offset = tz.getOffset(System.currentTimeMillis()) / 1000 / 60; + details._group = tz.getDisplayName(); + details._name = getNameWithoutRegion(id); + _zoneInfo.add(details); + } + } + } + + /** + * Populate the timezone regions into the region list + */ + private void populateTimezoneRegions() + { + _listBoxes[LIST_REGIONS].clear(); + TreeSet regions = new TreeSet(); + for (TimezoneDetails currZone : _zoneInfo) + { + regions.add(currZone._region); + } + for (String region : regions) + { + _listBoxes[LIST_REGIONS].addItem(region); + } + } + + /** + * Extract the timezone region from the id + */ + private static String getRegion(String inId) + { + final int slashPos = (inId == null ? -1 : inId.indexOf('/')); + if (slashPos > 0) + { + return inId.substring(0, slashPos); + } + return null; + } + + /** + * Populate the second listbox with the offsets for the given region + * @param inRegion selected region, or null if none selected + */ + private void populateTimezoneOffsets(String inRegion) + { + _listBoxes[LIST_OFFSETS].clear(); + TreeSet offsetsinMinutes = new TreeSet(); + for (TimezoneDetails currZone : _zoneInfo) + { + String region = currZone._region; + if (inRegion == null || region.equals(inRegion)) + { + offsetsinMinutes.add(currZone._offset); + } + } + for (Integer offset : offsetsinMinutes) + { + _listBoxes[LIST_OFFSETS].addItem(makeOffsetString(offset)); + } + } + + /** + * @return String containing offset for display + */ + private static String makeOffsetString(int inOffsetInMinutes) + { + if (inOffsetInMinutes == 0) return "0"; + final boolean isWholeHours = (inOffsetInMinutes % 60) == 0; + if (isWholeHours) + { + return (inOffsetInMinutes > 0 ? "+" : "") + (inOffsetInMinutes / 60); + } + final double numHours = inOffsetInMinutes / 60.0; + return (inOffsetInMinutes > 0 ? "+" : "") + numHours; + } + + /** + * Populate the group list using the specified region and offset + * @param inRegion selected region (if any) from the first list + * @param inOffset selected offset (if any) from the second list + */ + private void populateTimezoneGroups(String inRegion, String inOffset) + { + _listBoxes[LIST_GROUPS].clear(); + // Convert given offset string (in hours) into numeric offset (in minutes) + final int offsetMins = convertToMinutes(inOffset); + + TreeSet zoneGroups = new TreeSet(); + for (TimezoneDetails currZone : _zoneInfo) + { + if (inRegion == null || currZone._region.equals(inRegion)) + { + if (offsetMins == -1 || offsetMins == currZone._offset) + { + zoneGroups.add(currZone._group); + } + } + } + // If the region and offset were given, then list is unlimited + _listBoxes[LIST_GROUPS].setUnlimited(inRegion != null && inOffset != null); + // Add all the found names to the listbox + for (String group : zoneGroups) + { + _listBoxes[LIST_GROUPS].addItem(group); + } + } + + /** + * Populate the group list using the specified region, offset and group + * @param inRegion selected region (if any) from the first list + * @param inOffset selected offset (if any) from the second list + * @param inGroup selected group (if any) from the third list + */ + private void populateTimezoneNames(String inRegion, String inOffset, String inGroup) + { + CombinedListAndModel nameList = _listBoxes[LIST_NAMES]; + nameList.clear(); + // Convert given offset string (in hours) into numeric offset (in minutes) + final int offsetMins = convertToMinutes(inOffset); + + TreeSet zoneNames = new TreeSet(); + for (TimezoneDetails currZone : _zoneInfo) + { + if ((inRegion == null || currZone._region.equals(inRegion)) + && (offsetMins == -1 || currZone._offset == offsetMins) + && (inGroup == null || currZone._group.equals(inGroup))) + { + zoneNames.add(currZone._name); + } + } + // If the region and offset were given, then list is unlimited + nameList.setUnlimited(inRegion != null && inOffset != null && inRegion != null); + // Add all the found names to the listbox + for (String name : zoneNames) + { + nameList.addItem(name); + } + } + + /** + * Convert the given String from hours to minutes + * @param inOffsetInHours String from listbox in +/- hours + * @return offset in minutes, or -1 + */ + private static int convertToMinutes(String inOffsetInHours) + { + int offsetMins = -1; + try { + offsetMins = (int) (60 * Double.parseDouble(inOffsetInHours)); + } + catch (NumberFormatException nfe) {} // offset stays -1 + catch (NullPointerException npe) {} // offset stays -1 + return offsetMins; + } + + /** + * Remove the timezone region from the id to just leave the name after the slash + */ + private static String getNameWithoutRegion(String inId) + { + final int slashPos = (inId == null ? -1 : inId.indexOf('/')); + if (slashPos > 0) + { + return inId.substring(slashPos + 1); + } + return null; + } + + /** + * Get the selected timezone, or null if none selected + */ + private TimeZone getSelectedTimezone() + { + if (_systemRadio.isSelected()) + { + return TimeZone.getDefault(); + } + + String chosenRegion = _listBoxes[LIST_REGIONS].getSelectedItem(); + // Convert given offset string (in hours) into numeric offset (in minutes) + final int offsetMins = convertToMinutes(_listBoxes[LIST_OFFSETS].getSelectedItem()); + String chosenGroup = _listBoxes[LIST_GROUPS].getSelectedItem(); + String chosenName = _listBoxes[LIST_NAMES].getSelectedItem(); + + TreeSet zoneIds = new TreeSet(); + for (TimezoneDetails currZone : _zoneInfo) + { + if ((chosenRegion == null || currZone._region.equals(chosenRegion)) + && (offsetMins == -1 || currZone._offset == offsetMins) + && (chosenGroup == null || currZone._group.equals(chosenGroup)) + && (chosenName == null || currZone._name.equals(chosenName))) + { + zoneIds.add(currZone._id); + if (zoneIds.size() > 1) { + break; // exit loop now, we've got too many + } + } + } + // Should have exactly one result now + if (zoneIds.size() == 1) + { + return TimeZone.getTimeZone(zoneIds.first()); + } + + // none selected (yet) + return null; + } + + /** + * Show the details of the selected timezone + */ + private void showTimezoneDetails() + { + TimeZone selectedTimezone = getSelectedTimezone(); + if (selectedTimezone == null) + { + // Clear details labels + _selectedZoneLabel.setText(""); + _selectedOffsetLabel.setText(""); + } + else + { + // Fill results in labels + String desc = selectedTimezone.getID() + " - " + selectedTimezone.getDisplayName(); + _selectedZoneLabel.setText(desc); + String offsets = getOffsetDescription(selectedTimezone); + _selectedOffsetLabel.setText(offsets); + } + _okButton.setEnabled(selectedTimezone != null); + } + + /** + * @param inTimezone selected timezone + * @return String describing the time offset(s) of this zone including winter/summer time + */ + private static String getOffsetDescription(TimeZone inTimezone) + { + if (inTimezone == null) + { + return ""; + } + TreeSet offsetsinMinutes = new TreeSet(); + long testTimeMillis = System.currentTimeMillis(); + final long testPeriodInMillis = 1000L * 60 * 60 * 24 * 30 * 2; + for (int i=0; i<5; i++) + { + offsetsinMinutes.add(inTimezone.getOffset(testTimeMillis) / 1000 / 60); + testTimeMillis += testPeriodInMillis; + } + // Make String describing the sorted set + StringBuffer buff = new StringBuffer(); + for (Integer offset : offsetsinMinutes) + { + if (buff.length() > 0) + { + buff.append(" / "); + } + buff.append(makeOffsetString(offset)); + } + return buff.toString(); + } + + /** + * On entry to the dialog, select the items in each listbox + * according to the given preselected timezone id + * @param zoneId id of zone to select + */ + private void preselectTimezone(String zoneId) + { + TimeZone tz = (zoneId == null ? TimeZone.getDefault() : TimeZone.getTimeZone(zoneId)); + if (tz != null) + { + _listBoxes[LIST_REGIONS].selectItem(getRegion(zoneId)); + _listBoxes[LIST_OFFSETS].selectItem(makeOffsetString(tz.getOffset(System.currentTimeMillis()) / 1000 / 60)); + _listBoxes[LIST_GROUPS].selectItem(tz.getDisplayName()); + _listBoxes[LIST_NAMES].selectItem(getNameWithoutRegion(zoneId)); + } + } + + /** + * Finish the dialog by setting the config according to the selected zone + */ + private void finishSelectTimezone() + { + TimeZone selectedTimezone = getSelectedTimezone(); + if (_systemRadio.isSelected() || selectedTimezone == null) + { + // Clear config, use default system timezone instead + Config.setConfigString(Config.KEY_TIMEZONE_ID, null); + } + else + { + // Get selected timezone, set in config + Config.setConfigString(Config.KEY_TIMEZONE_ID, selectedTimezone.getID()); + } + _dialog.dispose(); + // Make sure listeners know to update themselves + UpdateMessageBroker.informSubscribers(DataSubscriber.UNITS_CHANGED); + } +}