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