001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static java.awt.GridBagConstraints.HORIZONTAL;
005import static javax.swing.SwingConstants.CENTER;
006import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
007import static org.openstreetmap.josm.tools.I18n.tr;
008import static org.openstreetmap.josm.tools.Utils.getSystemProperty;
009
010import java.awt.Color;
011import java.awt.Dimension;
012import java.awt.FlowLayout;
013import java.awt.Font;
014import java.awt.GridBagLayout;
015import java.awt.event.ActionEvent;
016import java.awt.event.KeyEvent;
017import java.io.BufferedReader;
018import java.io.File;
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.InputStreamReader;
022import java.nio.charset.StandardCharsets;
023import java.util.Map.Entry;
024
025import javax.swing.AbstractAction;
026import javax.swing.Action;
027import javax.swing.BorderFactory;
028import javax.swing.JButton;
029import javax.swing.JLabel;
030import javax.swing.JPanel;
031import javax.swing.JScrollPane;
032import javax.swing.JTabbedPane;
033import javax.swing.JTextArea;
034
035import org.openstreetmap.josm.data.Preferences;
036import org.openstreetmap.josm.data.Version;
037import org.openstreetmap.josm.gui.ExtendedDialog;
038import org.openstreetmap.josm.gui.MainApplication;
039import org.openstreetmap.josm.gui.util.GuiHelper;
040import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
041import org.openstreetmap.josm.gui.widgets.JosmTextArea;
042import org.openstreetmap.josm.gui.widgets.UrlLabel;
043import org.openstreetmap.josm.plugins.PluginHandler;
044import org.openstreetmap.josm.spi.preferences.Config;
045import org.openstreetmap.josm.tools.GBC;
046import org.openstreetmap.josm.tools.ImageProvider;
047import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
048import org.openstreetmap.josm.tools.Logging;
049import org.openstreetmap.josm.tools.OpenBrowser;
050import org.openstreetmap.josm.tools.Shortcut;
051import org.openstreetmap.josm.tools.Utils;
052
053/**
054 * Nice about screen.
055 *
056 * The REVISION resource is read and if present, it shows the revision information of the jar-file.
057 *
058 * @author imi
059 */
060public final class AboutAction extends JosmAction {
061
062    /**
063     * Constructs a new {@code AboutAction}.
064     */
065    public AboutAction() {
066        super(tr("About"), "logo", tr("Display the about screen."),
067            Shortcut.registerShortcut("system:about", tr("Help: {0}", tr("About")),
068            KeyEvent.VK_F1, Shortcut.SHIFT), true, false);
069    }
070
071    JPanel buildAboutPanel() {
072        final JTabbedPane about = new JTabbedPane();
073
074        Version version = Version.getInstance();
075
076        JosmTextArea readme = new JosmTextArea();
077        readme.setFont(GuiHelper.getMonospacedFont(readme));
078        readme.setEditable(false);
079        setTextFromResourceFile(readme, "/README");
080        readme.setCaretPosition(0);
081
082        JosmTextArea revision = new JosmTextArea();
083        revision.setFont(GuiHelper.getMonospacedFont(revision));
084        revision.setEditable(false);
085        revision.setText(version.getReleaseAttributes());
086        revision.setCaretPosition(0);
087
088        JosmTextArea contribution = new JosmTextArea();
089        contribution.setEditable(false);
090        setTextFromResourceFile(contribution, "/CONTRIBUTION");
091        contribution.setCaretPosition(0);
092
093        JosmTextArea license = new JosmTextArea();
094        license.setEditable(false);
095        setTextFromResourceFile(license, "/LICENSE");
096        license.setCaretPosition(0);
097
098        JPanel info = new JPanel(new GridBagLayout());
099        final JMultilineLabel label = new JMultilineLabel("<html>" +
100                "<h1>" + "JOSM – " + tr("Java OpenStreetMap Editor") + "</h1>" +
101                "<p style='font-size:75%'></p>" +
102                "<p>" + tr("Version {0}", version.getVersionString()) + "</p>" +
103                "<p style='font-size:50%'></p>" +
104                "<p>" + tr("Last change at {0}", version.getTime()) + "</p>" +
105                "<p style='font-size:50%'></p>" +
106                "<p>" + tr("Java Version {0}", getSystemProperty("java.version")) + "</p>" +
107                "<p style='font-size:50%'></p>" +
108                "</html>");
109        info.add(label, GBC.eol().fill(HORIZONTAL).insets(10, 0, 0, 10));
110        info.add(new JLabel(tr("Homepage")), GBC.std().insets(10, 0, 10, 0));
111        info.add(new UrlLabel(Config.getUrls().getJOSMWebsite(), 2), GBC.eol());
112        info.add(new JLabel(tr("Translations")), GBC.std().insets(10, 0, 10, 0));
113        info.add(new UrlLabel("https://translations.launchpad.net/josm", 2), GBC.eol());
114        info.add(new JLabel(tr("Follow us on")), GBC.std().insets(10, 10, 10, 0));
115        JPanel logos = new JPanel(new FlowLayout());
116        logos.add(createImageLink("OpenStreetMap", /* ICON(dialogs/about/) */ "openstreetmap",
117                "https://www.openstreetmap.org/user/josmeditor/diary"));
118        logos.add(createImageLink("Twitter", /* ICON(dialogs/about/) */ "twitter-square", "https://twitter.com/josmeditor"));
119        logos.add(createImageLink("Facebook", /* ICON(dialogs/about/) */ "facebook-square", "https://www.facebook.com/josmeditor"));
120        logos.add(createImageLink("GitHub", /* ICON(dialogs/about/) */ "github-square", "https://github.com/JOSM"));
121        info.add(logos, GBC.eol().insets(0, 10, 0, 0));
122        info.add(GBC.glue(0, 5), GBC.eol());
123
124        JPanel inst = new JPanel(new GridBagLayout());
125        inst.add(new JLabel(tr("Preferences are stored in {0}", getPathToPreferences())), GBC.eol().insets(0, 0, 0, 10));
126        inst.add(new JLabel(tr("Symbolic names for directories and the actual paths:")),
127                GBC.eol().insets(0, 0, 0, 10));
128        for (Entry<String, String> entry : ShowStatusReportAction.getAnonimicDirectorySymbolMap().entrySet()) {
129            addInstallationLine(inst, entry.getValue(), entry.getKey());
130        }
131
132        about.addTab(tr("Info"), info);
133        about.addTab(tr("Readme"), createScrollPane(readme));
134        about.addTab(tr("Revision"), createScrollPane(revision));
135        about.addTab(tr("Contribution"), createScrollPane(contribution));
136        about.addTab(tr("License"), createScrollPane(license));
137        about.addTab(tr("Plugins"), new JScrollPane(PluginHandler.getInfoPanel()));
138        about.addTab(tr("Installation Details"), inst);
139
140        // Get the list of Launchpad contributors using customary msgid “translator-credits”
141        String translators = tr("translator-credits");
142        if (!Utils.isEmpty(translators) && !"translator-credits".equals(translators)) {
143            about.addTab(tr("Translators"), createScrollPane(new JosmTextArea(translators)));
144        }
145
146        // Intermediate panel to allow proper optionPane resizing
147        JPanel panel = new JPanel(new GridBagLayout());
148        panel.setPreferredSize(new Dimension(890, 300));
149        panel.add(new JLabel("", ImageProvider.get("logo.svg", ImageSizes.ABOUT_LOGO), CENTER), GBC.std().insets(0, 5, 0, 0));
150        panel.add(about, GBC.std().fill());
151        return panel;
152    }
153
154    private static String getPathToPreferences() {
155        File preferenceFile = Preferences.main().getPreferenceFile();
156        try {
157            return ShowStatusReportAction.paramCleanup(preferenceFile.getAbsolutePath());
158        } catch (SecurityException e) {
159            Logging.warn(e);
160            return ShowStatusReportAction.paramCleanup(preferenceFile.getPath());
161        }
162    }
163
164    @Override
165    public void actionPerformed(ActionEvent e) {
166        JPanel panel = buildAboutPanel();
167
168        GuiHelper.prepareResizeableOptionPane(panel, panel.getPreferredSize());
169        ExtendedDialog dlg = new ExtendedDialog(MainApplication.getMainFrame(), tr("About JOSM..."), tr("OK"), tr("Report bug"));
170        int ret = dlg.setButtonIcons("ok", "bug")
171                .configureContextsensitiveHelp(ht("Action/About"), true)
172                .setContent(panel, false)
173                .showDialog().getValue();
174        if (2 == ret) {
175            MainApplication.getMenu().reportbug.actionPerformed(null);
176        }
177        GuiHelper.destroyComponents(panel, false);
178        dlg.dispose();
179    }
180
181    private static class OpenDirAction extends AbstractAction {
182        final String dir;
183
184        OpenDirAction(String dir) {
185            putValue(Action.NAME, "...");
186            this.dir = dir;
187            try {
188                setEnabled(dir != null && new File(dir).isDirectory());
189            } catch (SecurityException e) {
190                setEnabled(false);
191                Logging.warn(e);
192            }
193        }
194
195        @Override
196        public void actionPerformed(ActionEvent e) {
197            OpenBrowser.displayUrl(new File(dir).toURI());
198        }
199    }
200
201    /**
202     * Add line to installation details showing symbolic name used in status report and actual directory.
203     * @param inst the panel
204     * @param dir the actual path represented by a symbol
205     * @param source source for symbol
206     */
207    private static void addInstallationLine(JPanel inst, String dir, String source) {
208        if (source == null)
209            return;
210        JLabel symbol = new JLabel(source);
211        symbol.setFont(GuiHelper.getMonospacedFont(symbol));
212        JosmTextArea dirLabel = new JosmTextArea();
213        if (!Utils.isEmpty(dir)) {
214            dirLabel.setText(dir);
215            dirLabel.setEditable(false);
216        } else {
217            dirLabel.setText("<" + tr("unset") + ">");
218            dirLabel.setFont(dirLabel.getFont().deriveFont(Font.ITALIC));
219            dirLabel.setEditable(false);
220        }
221        inst.add(symbol, GBC.std().insets(5, 0, 0, 0));
222        inst.add(GBC.glue(10, 0), GBC.std());
223        dirLabel.setFont(GuiHelper.getMonospacedFont(dirLabel));
224        dirLabel.setOpaque(false);
225        inst.add(dirLabel, GBC.std().fill(HORIZONTAL));
226        JButton btn = new JButton(new OpenDirAction(dir));
227        btn.setToolTipText(tr("Open directory"));
228        inst.add(btn, GBC.eol().insets(0, 0, 5, 0));
229    }
230
231    private static JLabel createImageLink(String tooltip, String icon, final String link) {
232        return new UrlLabel(link, tooltip, ImageProvider.get("dialogs/about", icon, ImageSizes.LARGEICON));
233    }
234
235    /**
236     * Reads the contents of the resource file that is described by the {@code filePath}-attribute and puts that text
237     * into the {@link JTextArea} given by the {@code ta}-attribute.
238     * @param ta the {@link JTextArea} to put the files contents into
239     * @param filePath the path where the resource file to read resides
240     */
241    private void setTextFromResourceFile(JTextArea ta, String filePath) {
242        InputStream is = Utils.getResourceAsStream(getClass(), filePath);
243        if (is == null) {
244            displayErrorMessage(ta, tr("Failed to locate resource ''{0}''.", filePath));
245        } else {
246            try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
247                String line;
248                while ((line = br.readLine()) != null) {
249                    ta.append(line+'\n');
250                }
251            } catch (IOException e) {
252                Logging.warn(e);
253                displayErrorMessage(ta, tr("Failed to load resource ''{0}'', error is {1}.", filePath, e.toString()));
254            }
255        }
256    }
257
258    private static void displayErrorMessage(JTextArea ta, String msg) {
259        Logging.warn(msg);
260        ta.setForeground(new Color(200, 0, 0));
261        ta.setText(msg);
262    }
263
264    private static JScrollPane createScrollPane(JosmTextArea area) {
265        area.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
266        area.setOpaque(false);
267        JScrollPane sp = new JScrollPane(area);
268        sp.setBorder(null);
269        sp.setOpaque(false);
270        return sp;
271    }
272}