// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.opendata.core.gui;
import static org.openstreetmap.josm.tools.I18n.tr;
import static org.openstreetmap.josm.tools.I18n.trn;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.gui.HelpAwareOptionPane;
import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
import org.openstreetmap.josm.gui.help.HelpUtil;
import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting;
import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
import org.openstreetmap.josm.plugins.opendata.core.OdConstants;
import org.openstreetmap.josm.plugins.opendata.core.modules.ModuleDownloadTask;
import org.openstreetmap.josm.plugins.opendata.core.modules.ModuleInformation;
import org.openstreetmap.josm.plugins.opendata.core.modules.ReadLocalModuleInformationTask;
import org.openstreetmap.josm.plugins.opendata.core.modules.ReadRemoteModuleInformationTask;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.ImageProvider;
public class ModulePreference implements SubPreferenceSetting {
/*public static class Factory implements PreferenceSettingFactory {
public PreferenceSetting createPreferenceSetting() {
return new ModulePreference();
}
}*/
public static String buildDownloadSummary(ModuleDownloadTask task) {
Collection<ModuleInformation> downloaded = task.getDownloadedModules();
Collection<ModuleInformation> failed = task.getFailedModules();
StringBuilder sb = new StringBuilder();
if (!downloaded.isEmpty()) {
sb.append(trn(
"The following module has been downloaded <strong>successfully</strong>:",
"The following {0} modules have been downloaded <strong>successfully</strong>:",
downloaded.size(),
downloaded.size()
));
sb.append("<ul>");
for (ModuleInformation pi : downloaded) {
sb.append("<li>").append(pi.name).append(" (").append(pi.version).append(")").append("</li>");
}
sb.append("</ul>");
}
if (!failed.isEmpty()) {
sb.append(trn(
"Downloading the following module has <strong>failed</strong>:",
"Downloading the following {0} modules has <strong>failed</strong>:",
failed.size(),
failed.size()
));
sb.append("<ul>");
for (ModuleInformation pi : failed) {
sb.append("<li>").append(pi.name).append("</li>");
}
sb.append("</ul>");
}
return sb.toString();
}
private JTextField tfFilter;
private ModuleListPanel pnlModulePreferences;
private ModulePreferencesModel model;
private JScrollPane spModulePreferences;
/**
* is set to true if this preference pane has been selected
* by the user
*/
private boolean modulePreferencesActivated = false;
protected JPanel buildSearchFieldPanel() {
JPanel pnl = new JPanel(new GridBagLayout());
pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
GridBagConstraints gc = new GridBagConstraints();
gc.anchor = GridBagConstraints.NORTHWEST;
gc.fill = GridBagConstraints.HORIZONTAL;
gc.weightx = 0.0;
gc.insets = new Insets(0, 0, 0, 3);
pnl.add(new JLabel(tr("Search:")), gc);
gc.gridx = 1;
gc.weightx = 1.0;
pnl.add(tfFilter = new JTextField(), gc);
tfFilter.setToolTipText(tr("Enter a search expression"));
SelectAllOnFocusGainedDecorator.decorate(tfFilter);
tfFilter.getDocument().addDocumentListener(new SearchFieldAdapter());
return pnl;
}
protected JPanel buildActionPanel() {
JPanel pnl = new JPanel(new GridLayout(1, 3));
pnl.add(new JButton(new DownloadAvailableModulesAction()));
pnl.add(new JButton(new UpdateSelectedModulesAction()));
pnl.add(new JButton(new ConfigureSitesAction()));
return pnl;
}
protected JPanel buildModuleListPanel() {
JPanel pnl = new JPanel(new BorderLayout());
pnl.add(buildSearchFieldPanel(), BorderLayout.NORTH);
model = new ModulePreferencesModel();
spModulePreferences = new JScrollPane(pnlModulePreferences = new ModuleListPanel(model));
spModulePreferences.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
spModulePreferences.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
spModulePreferences.getVerticalScrollBar().addComponentListener(
new ComponentAdapter() {
@Override
public void componentShown(ComponentEvent e) {
spModulePreferences.setBorder(UIManager.getBorder("ScrollPane.border"));
}
@Override
public void componentHidden(ComponentEvent e) {
spModulePreferences.setBorder(null);
}
}
);
pnl.add(spModulePreferences, BorderLayout.CENTER);
pnl.add(buildActionPanel(), BorderLayout.SOUTH);
return pnl;
}
@Override
public void addGui(final PreferenceTabbedPane gui) {
GridBagConstraints gc = new GridBagConstraints();
gc.weightx = 1.0;
gc.weighty = 1.0;
gc.anchor = GridBagConstraints.NORTHWEST;
gc.fill = GridBagConstraints.BOTH;
OdPreferenceSetting settings = gui.getSetting(OdPreferenceSetting.class);
settings.tabPane.addTab(tr("Modules"), buildModuleListPanel());
pnlModulePreferences.refreshView();
gui.addChangeListener(new ModulePreferenceActivationListener(settings.masterPanel));
}
private void configureSites() {
ButtonSpec[] options = new ButtonSpec[] {
new ButtonSpec(
tr("OK"),
ImageProvider.get("ok"),
tr("Accept the new module sites and close the dialog"),
null /* no special help topic */
),
new ButtonSpec(
tr("Cancel"),
ImageProvider.get("cancel"),
tr("Close the dialog"),
null /* no special help topic */
)
};
ModuleConfigurationSitesPanel pnl = new ModuleConfigurationSitesPanel();
int answer = HelpAwareOptionPane.showOptionDialog(
pnlModulePreferences,
pnl,
tr("Configure Module Sites"),
JOptionPane.QUESTION_MESSAGE,
null,
options,
options[0],
null /* no help topic */
);
if (answer != 0 /* OK */)
return;
List<String> sites = pnl.getUpdateSites();
OdPreferenceSetting.setModuleSites(sites);
}
/**
* Replies the list of modules waiting for update or download
*
* @return the list of modules waiting for update or download
*/
public List<ModuleInformation> getModulesScheduledForUpdateOrDownload() {
return model != null ? model.getModulesScheduledForUpdateOrDownload() : null;
}
@Override
public boolean ok() {
if (!modulePreferencesActivated)
return false;
if (model.isActiveModulesChanged()) {
LinkedList<String> l = new LinkedList<>(model.getSelectedModuleNames());
Collections.sort(l);
Main.pref.putCollection(OdConstants.PREF_MODULES, l);
return true;
}
return false;
}
/**
* Reads locally available information about modules from the local file system.
* Scans cached module lists from module download sites and locally available
* module jar files.
*
*/
public void readLocalModuleInformation() {
final ReadLocalModuleInformationTask task = new ReadLocalModuleInformationTask();
Runnable r = new Runnable() {
@Override
public void run() {
if (task.isCanceled()) return;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
model.setAvailableModules(task.getAvailableModules());
pnlModulePreferences.refreshView();
}
});
}
};
Main.worker.submit(task);
Main.worker.submit(r);
}
/**
* The action for downloading the list of available modules
*
*/
class DownloadAvailableModulesAction extends AbstractAction {
DownloadAvailableModulesAction() {
putValue(NAME, tr("Download list"));
putValue(SHORT_DESCRIPTION, tr("Download the list of available modules"));
putValue(SMALL_ICON, ImageProvider.get("download"));
}
@Override
public void actionPerformed(ActionEvent e) {
final ReadRemoteModuleInformationTask task = new ReadRemoteModuleInformationTask(OdPreferenceSetting.getModuleSites());
Runnable continuation = new Runnable() {
@Override
public void run() {
if (task.isCanceled()) return;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
model.updateAvailableModules(task.getAvailableModules());
pnlModulePreferences.refreshView();
}
});
}
};
Main.worker.submit(task);
Main.worker.submit(continuation);
}
}
/**
* The action for downloading the list of available modules
*
*/
class UpdateSelectedModulesAction extends AbstractAction {
UpdateSelectedModulesAction() {
putValue(NAME, tr("Update modules"));
putValue(SHORT_DESCRIPTION, tr("Update the selected modules"));
putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
}
protected void notifyDownloadResults(ModuleDownloadTask task) {
Collection<ModuleInformation> downloaded = task.getDownloadedModules();
Collection<ModuleInformation> failed = task.getFailedModules();
StringBuilder sb = new StringBuilder();
sb.append("<html>");
sb.append(buildDownloadSummary(task));
if (!downloaded.isEmpty()) {
sb.append(tr("Please restart JOSM to activate the downloaded modules."));
}
sb.append("</html>");
HelpAwareOptionPane.showOptionDialog(
pnlModulePreferences,
sb.toString(),
tr("Update modules"),
!failed.isEmpty() ? JOptionPane.WARNING_MESSAGE : JOptionPane.INFORMATION_MESSAGE,
HelpUtil.ht("/Preferences/Modules")
);
}
protected void alertNothingToUpdate() {
GuiHelper.runInEDTAndWait(new Runnable() {
@Override
public void run() {
HelpAwareOptionPane.showOptionDialog(
pnlModulePreferences,
tr("All installed modules are up to date. JOSM does not have to download newer versions."),
tr("Modules up to date"),
JOptionPane.INFORMATION_MESSAGE,
null // FIXME: provide help context
);
}
});
}
@Override
public void actionPerformed(ActionEvent e) {
final List<ModuleInformation> toUpdate = model.getSelectedModules();
// the async task for downloading modules
final ModuleDownloadTask moduleDownloadTask = new ModuleDownloadTask(
pnlModulePreferences,
toUpdate,
tr("Update modules")
);
// the async task for downloading module information
final ReadRemoteModuleInformationTask moduleInfoDownloadTask =
new ReadRemoteModuleInformationTask(OdPreferenceSetting.getModuleSites());
// to be run asynchronously after the module download
//
final Runnable moduleDownloadContinuation = new Runnable() {
@Override
public void run() {
if (moduleDownloadTask.isCanceled())
return;
notifyDownloadResults(moduleDownloadTask);
model.refreshLocalModuleVersion(moduleDownloadTask.getDownloadedModules());
model.clearPendingModules(moduleDownloadTask.getDownloadedModules());
GuiHelper.runInEDT(new Runnable() {
@Override
public void run() {
pnlModulePreferences.refreshView();
}
});
}
};
// to be run asynchronously after the module list download
//
final Runnable moduleInfoDownloadContinuation = new Runnable() {
@Override
public void run() {
if (moduleInfoDownloadTask.isCanceled())
return;
model.updateAvailableModules(moduleInfoDownloadTask.getAvailableModules());
// select modules which actually have to be updated
//
Iterator<ModuleInformation> it = toUpdate.iterator();
while (it.hasNext()) {
ModuleInformation pi = it.next();
if (!pi.isUpdateRequired()) {
it.remove();
}
}
if (toUpdate.isEmpty()) {
alertNothingToUpdate();
return;
}
moduleDownloadTask.setModulesToDownload(toUpdate);
Main.worker.submit(moduleDownloadTask);
Main.worker.submit(moduleDownloadContinuation);
}
};
Main.worker.submit(moduleInfoDownloadTask);
Main.worker.submit(moduleInfoDownloadContinuation);
}
}
/**
* The action for configuring the module download sites
*
*/
class ConfigureSitesAction extends AbstractAction {
ConfigureSitesAction() {
putValue(NAME, tr("Configure sites..."));
putValue(SHORT_DESCRIPTION, tr("Configure the list of sites where modules are downloaded from"));
putValue(SMALL_ICON, ImageProvider.get("dialogs", "settings"));
}
@Override
public void actionPerformed(ActionEvent e) {
configureSites();
}
}
/**
* Listens to the activation of the module preferences tab. On activation it
* reloads module information from the local file system.
*
*/
class ModulePreferenceActivationListener implements ChangeListener {
private Component pane;
ModulePreferenceActivationListener(Component preferencesPane) {
pane = preferencesPane;
}
@Override
public void stateChanged(ChangeEvent e) {
JTabbedPane tp = (JTabbedPane) e.getSource();
if (tp.getSelectedComponent() == pane) {
readLocalModuleInformation();
modulePreferencesActivated = true;
}
}
}
/**
* Applies the current filter condition in the filter text field to the
* model
*/
class SearchFieldAdapter implements DocumentListener {
public void filter() {
String expr = tfFilter.getText().trim();
if (expr.equals("")) {
expr = null;
}
model.filterDisplayedModules(expr);
pnlModulePreferences.refreshView();
}
@Override
public void changedUpdate(DocumentEvent arg0) {
filter();
}
@Override
public void insertUpdate(DocumentEvent arg0) {
filter();
}
@Override
public void removeUpdate(DocumentEvent arg0) {
filter();
}
}
private static class ModuleConfigurationSitesPanel extends JPanel {
private DefaultListModel<String> model;
protected void build() {
setLayout(new GridBagLayout());
add(new JLabel(tr("Add Open Data Module description URL.")), GBC.eol());
model = new DefaultListModel<>();
for (String s : OdPreferenceSetting.getModuleSites()) {
model.addElement(s);
}
final JList<String> list = new JList<>(model);
add(new JScrollPane(list), GBC.std().fill());
JPanel buttons = new JPanel(new GridBagLayout());
buttons.add(new JButton(new AbstractAction(tr("Add")) {
@Override
public void actionPerformed(ActionEvent e) {
String s = JOptionPane.showInputDialog(
JOptionPane.getFrameForComponent(ModuleConfigurationSitesPanel.this),
tr("Add Open Data Module description URL."),
tr("Enter URL"),
JOptionPane.QUESTION_MESSAGE
);
if (s != null) {
model.addElement(s);
}
}
}), GBC.eol().fill(GBC.HORIZONTAL));
buttons.add(new JButton(new AbstractAction(tr("Edit")) {
@Override
public void actionPerformed(ActionEvent e) {
if (list.getSelectedValue() == null) {
JOptionPane.showMessageDialog(
JOptionPane.getFrameForComponent(ModuleConfigurationSitesPanel.this),
tr("Please select an entry."),
tr("Warning"),
JOptionPane.WARNING_MESSAGE
);
return;
}
String s = (String) JOptionPane.showInputDialog(
Main.parent,
tr("Edit Open Data Module description URL."),
tr("Open Data Module description URL"),
JOptionPane.QUESTION_MESSAGE,
null,
null,
list.getSelectedValue()
);
if (s != null) {
model.setElementAt(s, list.getSelectedIndex());
}
}
}), GBC.eol().fill(GBC.HORIZONTAL));
buttons.add(new JButton(new AbstractAction(tr("Delete")) {
@Override
public void actionPerformed(ActionEvent event) {
if (list.getSelectedValue() == null) {
JOptionPane.showMessageDialog(
JOptionPane.getFrameForComponent(ModuleConfigurationSitesPanel.this),
tr("Please select an entry."),
tr("Warning"),
JOptionPane.WARNING_MESSAGE
);
return;
}
model.removeElement(list.getSelectedValue());
}
}), GBC.eol().fill(GBC.HORIZONTAL));
add(buttons, GBC.eol());
}
ModuleConfigurationSitesPanel() {
build();
}
public List<String> getUpdateSites() {
if (model.getSize() == 0) return Collections.emptyList();
List<String> ret = new ArrayList<>(model.getSize());
for (int i = 0; i < model.getSize(); i++) {
ret.add(model.get(i));
}
return ret;
}
}
@Override
public boolean isExpert() {
return false;
}
@Override
public TabPreferenceSetting getTabPreferenceSetting(PreferenceTabbedPane gui) {
return gui.getSetting(OdPreferenceSetting.class);
}
}