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