001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools.bugreport;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Frame;
008import java.awt.GridBagConstraints;
009import java.awt.GridBagLayout;
010import java.awt.event.ActionEvent;
011
012import javax.swing.AbstractAction;
013import javax.swing.BorderFactory;
014import javax.swing.Icon;
015import javax.swing.JButton;
016import javax.swing.JCheckBox;
017import javax.swing.JDialog;
018import javax.swing.JLabel;
019import javax.swing.JOptionPane;
020import javax.swing.JPanel;
021import javax.swing.UIManager;
022
023import org.openstreetmap.josm.Main;
024import org.openstreetmap.josm.actions.ExpertToggleAction;
025import org.openstreetmap.josm.gui.preferences.plugin.PluginPreference;
026import org.openstreetmap.josm.gui.util.GuiHelper;
027import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
028import org.openstreetmap.josm.gui.widgets.UrlLabel;
029import org.openstreetmap.josm.plugins.PluginDownloadTask;
030import org.openstreetmap.josm.plugins.PluginHandler;
031import org.openstreetmap.josm.tools.GBC;
032import org.openstreetmap.josm.tools.ImageProvider;
033import org.openstreetmap.josm.tools.InputMapUtils;
034import org.openstreetmap.josm.tools.bugreport.BugReportQueue.SuppressionMode;
035
036/**
037 * This is a dialog that can be used to display a bug report.
038 * <p>
039 * It displays the bug to the user and asks the user to submit a bug report.
040 * @author Michael Zangl
041 * @since 10649
042 */
043public class BugReportDialog extends JDialog {
044    private static final int MAX_MESSAGE_SIZE = 500;
045    // This is explicitly not an ExtendedDialog - we still want to be able to display bug reports if there are problems with preferences/..
046    private final JPanel content = new JPanel(new GridBagLayout());
047    private final BugReport report;
048    private final DebugTextDisplay textPanel;
049    private JCheckBox cbSuppressSingle;
050    private JCheckBox cbSuppressAll;
051
052    /**
053     * Create a new dialog.
054     * @param report The report to display the dialog for.
055     */
056    public BugReportDialog(BugReport report) {
057        super(findParent(), tr("You have encountered a bug in JOSM"));
058        this.report = report;
059        textPanel = new DebugTextDisplay(report);
060        setContentPane(content);
061
062        addMessageSection();
063
064        addUpToDateSection();
065        // TODO: Notify user about plugin updates, then remove that notification that is displayed before this dialog is displayed.
066
067        addCreateTicketSection();
068
069        if (ExpertToggleAction.isExpert()) {
070            addDebugTextSection();
071        }
072
073        addIgnoreButton();
074
075        pack();
076        setModal(true);
077        setDefaultCloseOperation(DISPOSE_ON_CLOSE);
078
079        InputMapUtils.addEscapeAction(getRootPane(), new AbstractAction() {
080            @Override
081            public void actionPerformed(ActionEvent e) {
082                closeDialog();
083            }
084        });
085    }
086
087    /**
088     * The message informing the user what happened.
089     */
090    private void addMessageSection() {
091        String message = tr(
092                "An unexpected exception occurred.\n" + "This is always a coding error. If you are running the latest "
093                        + "version of JOSM, please consider being kind and file a bug report.");
094        Icon icon = UIManager.getIcon("OptionPane.errorIcon");
095
096        JPanel panel = new JPanel(new GridBagLayout());
097
098        panel.add(new JLabel(icon), GBC.std().insets(0, 0, 10, 0));
099        JMultilineLabel messageLabel = new JMultilineLabel(message);
100        messageLabel.setMaxWidth(MAX_MESSAGE_SIZE);
101        panel.add(messageLabel, GBC.eol().fill());
102        content.add(panel, GBC.eop().fill(GBC.HORIZONTAL).insets(20, 10, 10, 10));
103    }
104
105    private void addDebugTextSection() {
106        JPanel panel = new JPanel(new GridBagLayout());
107        addBorder(panel, tr("Debug information"));
108        panel.add(textPanel, GBC.eop().fill());
109
110        panel.add(new JLabel(tr("Manually report at:")+' '), GBC.std());
111        panel.add(new UrlLabel(Main.getJOSMWebsite() + "/newticket"), GBC.std().fill(GBC.HORIZONTAL));
112        JButton copy = new JButton("Copy to clipboard");
113        copy.addActionListener(e -> textPanel.copyToClipboard());
114        panel.add(copy, GBC.eol().anchor(GBC.EAST));
115        content.add(panel, GBC.eop().fill());
116    }
117
118    private void addUpToDateSection() {
119        JPanel panel = new JosmUpdatePanel();
120        addBorder(panel, tr("Is JOSM up to date?"));
121        content.add(panel, GBC.eop().fill(GBC.HORIZONTAL));
122    }
123
124    private void addCreateTicketSection() {
125        JPanel panel = new JPanel(new GridBagLayout());
126        addBorder(panel, tr("Send bug report"));
127
128        JMultilineLabel helpText = new JMultilineLabel(
129                tr("If you are running the latest version of JOSM and the plugins, "
130                        + "please file a bug report in our bugtracker.\n"
131                        + "There the error information should already be "
132                        + "filled in for you. Please include information on how to reproduce "
133                        + "the error and try to supply as much detail as possible."));
134        helpText.setMaxWidth(MAX_MESSAGE_SIZE);
135        panel.add(helpText, GBC.eop().fill(GridBagConstraints.HORIZONTAL));
136
137        Component settings = GBC.glue(0, 0);
138        if (ExpertToggleAction.isExpert()) {
139            // The default settings should be fine in most situations.
140            settings = new BugReportSettingsPanel(report);
141        }
142        panel.add(settings);
143
144        JButton sendBugReportButton = new JButton(tr("Report Bug"), ImageProvider.get("bug"));
145        sendBugReportButton.addActionListener(e -> sendBug());
146        panel.add(sendBugReportButton, GBC.eol().insets(0, 0, 0, 0).anchor(GBC.SOUTHEAST));
147        content.add(panel, GBC.eop().fill(GBC.HORIZONTAL));
148    }
149
150    private static void addBorder(JPanel panel, String title) {
151        panel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(title), BorderFactory
152                .createEmptyBorder(5, 5, 5, 5)));
153    }
154
155    private void addIgnoreButton() {
156        JPanel panel = new JPanel(new GridBagLayout());
157        cbSuppressSingle = new JCheckBox(tr("Suppress this error for this session."));
158        cbSuppressSingle.setVisible(false);
159        panel.add(cbSuppressSingle, GBC.std(0, 0).fill(GBC.HORIZONTAL));
160        cbSuppressAll = new JCheckBox(tr("Suppress further error dialogs for this session."));
161        cbSuppressAll.setVisible(false);
162        panel.add(cbSuppressAll, GBC.std(0, 1).fill(GBC.HORIZONTAL));
163        JButton ignore = new JButton(tr("Ignore this error."));
164        ignore.addActionListener(e -> closeDialog());
165        panel.add(ignore, GBC.std(1, 0).span(1, 2).anchor(GBC.CENTER));
166        content.add(panel, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 10, 10));
167    }
168
169    /**
170     * Shows or hides the suppress errors button
171     * @param showSuppress <code>true</code> to show the suppress errors checkbox.
172     */
173    public void setShowSuppress(boolean showSuppress) {
174        cbSuppressSingle.setVisible(showSuppress);
175        pack();
176    }
177
178    /**
179     * Shows or hides the suppress all errors button
180     * @param showSuppress <code>true</code> to show the suppress errors checkbox.
181     * @since 10819
182     */
183    public void setShowSuppressAll(boolean showSuppress) {
184        cbSuppressAll.setVisible(showSuppress);
185        pack();
186    }
187
188    /**
189     * Check if the checkbox to suppress further errors was selected
190     * @return <code>true</code> if the user wishes to suppress errors.
191     */
192    public SuppressionMode shouldSuppressFurtherErrors() {
193        if (cbSuppressAll.isSelected()) {
194            return SuppressionMode.ALL;
195        } else if (cbSuppressSingle.isSelected()) {
196            return SuppressionMode.SAME;
197        } else {
198            return SuppressionMode.NONE;
199        }
200    }
201
202    private void closeDialog() {
203        setVisible(false);
204    }
205
206    private void sendBug() {
207        BugReportSender.reportBug(textPanel.getCodeText());
208    }
209
210    /**
211     * A safe way to find a matching parent frame.
212     * @return The parent frame.
213     */
214    private static Frame findParent() {
215        Component current = Main.parent;
216        try {
217            // avoid cycles/invalid hirarchies
218            for (int i = 0; i < 20 && current != null; i++) {
219                if (current instanceof Frame) {
220                    return (Frame) current;
221                }
222                current = current.getParent();
223            }
224        } catch (RuntimeException e) {
225            BugReport.intercept(e).put("current", current).warn();
226        }
227        return null;
228    }
229
230    /**
231     * Show the bug report for a given exception
232     * @param e The exception to display
233     * @param exceptionCounter A counter of how many exceptions have already been worked on
234     * @return The new suppression status
235     * @since 10819
236     */
237    public static SuppressionMode showFor(ReportedException e, int exceptionCounter) {
238        if (e.isOutOfMemory()) {
239            // do not translate the string, as translation may raise an exception
240            JOptionPane.showMessageDialog(Main.parent, "JOSM is out of memory. " +
241                    "Strange things may happen.\nPlease restart JOSM with the -Xmx###M option,\n" +
242                    "where ### is the number of MB assigned to JOSM (e.g. 256).\n" +
243                    "Currently, " + Runtime.getRuntime().maxMemory()/1024/1024 + " MB are available to JOSM.",
244                    "Error",
245                    JOptionPane.ERROR_MESSAGE
246                    );
247            return SuppressionMode.NONE;
248        } else {
249            return GuiHelper.runInEDTAndWaitAndReturn(() -> {
250                PluginDownloadTask downloadTask = PluginHandler.updateOrdisablePluginAfterException(e);
251                if (downloadTask != null) {
252                    // Ask for restart to install new plugin
253                    PluginPreference.notifyDownloadResults(
254                            Main.parent, downloadTask, !downloadTask.getDownloadedPlugins().isEmpty());
255                    return SuppressionMode.NONE;
256                }
257
258                BugReport report = new BugReport(e);
259                BugReportDialog dialog = new BugReportDialog(report);
260                dialog.setShowSuppress(exceptionCounter > 0);
261                dialog.setShowSuppressAll(exceptionCounter > 1);
262                dialog.setVisible(true);
263                return dialog.shouldSuppressFurtherErrors();
264            });
265        }
266    }
267}