001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.ui;
030
031import static java.awt.Color.GRAY;
032
033import static javax.swing.BorderFactory.createBevelBorder;
034import static javax.swing.GroupLayout.Alignment.LEADING;
035import static javax.swing.border.BevelBorder.RAISED;
036
037import static ucar.unidata.util.GuiUtils.getImageIcon;
038import static ucar.unidata.util.LayoutUtil.inset;
039import static ucar.unidata.util.LayoutUtil.topCenter;
040
041import java.awt.Cursor;
042import java.awt.Font;
043import java.awt.BorderLayout;
044import java.awt.event.KeyAdapter;
045import java.awt.event.KeyEvent;
046import java.awt.event.MouseAdapter;
047import java.awt.event.MouseEvent;
048
049import java.net.MalformedURLException;
050import java.net.URL;
051
052import java.util.Objects;
053import java.util.concurrent.atomic.AtomicBoolean;
054
055import javax.swing.GroupLayout;
056import javax.swing.JEditorPane;
057import javax.swing.JFrame;
058import javax.swing.JLabel;
059import javax.swing.JPanel;
060import javax.swing.JScrollPane;
061import javax.swing.JTabbedPane;
062import javax.swing.JTextArea;
063import javax.swing.JTextField;
064import javax.swing.SwingUtilities;
065import javax.swing.WindowConstants;
066import javax.swing.event.ChangeEvent;
067import javax.swing.event.ChangeListener;
068import javax.swing.event.HyperlinkEvent;
069import javax.swing.text.DefaultCaret;
070
071import org.slf4j.Logger;
072import org.slf4j.LoggerFactory;
073
074import ucar.unidata.ui.TextSearcher;
075import ucar.unidata.util.GuiUtils;
076import ucar.unidata.util.Misc;
077
078import edu.wisc.ssec.mcidasv.Constants;
079import edu.wisc.ssec.mcidasv.McIDASV;
080import edu.wisc.ssec.mcidasv.StateManager;
081import edu.wisc.ssec.mcidasv.util.SystemState;
082
083/**
084 * Class that represents the {@literal "Help>About McIDAS-V"} window.
085 */
086class AboutFrame extends JFrame implements ChangeListener {
087
088    /** Logging object. */
089    private static final Logger logger =
090        LoggerFactory.getLogger(AboutFrame.class);
091
092    /**
093     * Initial message in text area within {@literal "System Information"} tab.
094     */
095    private static final String PLEASE_WAIT =
096        "Please wait, collecting system information...";
097
098    /** Text used as the title for this window. */
099    private static final String WINDOW_TITLE = "About McIDAS-V";
100
101    /** Name of the first tab. */
102    private static final String MCV_TAB_TITLE = "McIDAS-V";
103
104    /** Name of the second tab. */
105    private static final String SYS_TAB_TITLE = "System Information";
106
107    /** Reference to the main McIDAS-V object. */
108    private final McIDASV mcv;
109
110    /**
111     * Text area within the {@literal "System Information"} tab.
112     * Value may be {@code null}.
113     */
114    private JTextArea sysTextArea;
115
116    /** Whether the system information has been collected. */
117    private final AtomicBoolean hasSysInfo;
118
119    /** the text searching widget */
120    private TextSearcher textSearcher;
121
122    /**
123     * Creates new form AboutFrame
124     *
125     * @param mcv McIDAS-V object. Cannot be {@code null}.
126     *
127     * @throws NullPointerException if {@code mcv} is {@code null}.
128     */
129    AboutFrame(final McIDASV mcv) {
130        Objects.requireNonNull(mcv,"mcv reference cannot be null");
131        this.mcv = mcv;
132        this.hasSysInfo = new AtomicBoolean(false);
133        initComponents();
134    }
135
136    /**
137     * Convenience method for calling {@link SystemState#getStateAsString(McIDASV, boolean)}.
138     *
139     * @return <i>All</i> of the relevant McIDAS-V system properties, stuffed into a single {@code String}.
140     */
141    private String getSystemInformation() {
142        return SystemState.getStateAsString(mcv, true);
143    }
144
145    /**
146     * Called by the constructor to initialize the {@literal "About"} window.
147     */
148    private void initComponents() {
149
150        JTabbedPane tabbedPanel = new JTabbedPane();
151        JPanel mcvTab = new JPanel();
152        JPanel mcvPanel = buildAboutMcv();
153        JPanel sysTab = new JPanel();
154        JScrollPane sysScrollPane = new JScrollPane();
155        sysTextArea = new JTextArea();
156
157        textSearcher = new TextSearcher(sysTextArea);
158
159        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
160        setTitle(WINDOW_TITLE);
161
162        GroupLayout mcvTabLayout = new GroupLayout(mcvTab);
163        mcvTab.setLayout(mcvTabLayout);
164        mcvTabLayout.setHorizontalGroup(
165            mcvTabLayout.createParallelGroup(LEADING)
166            .addComponent(mcvPanel)
167        );
168        mcvTabLayout.setVerticalGroup(
169            mcvTabLayout.createParallelGroup(LEADING)
170            .addComponent(mcvPanel)
171        );
172
173        tabbedPanel.addTab(MCV_TAB_TITLE, mcvTab);
174
175        sysTextArea.setText(PLEASE_WAIT);
176
177        sysTextArea.setEditable(false);
178        sysTextArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
179        sysTextArea.setCaretPosition(0);
180        sysTextArea.setLineWrap(false);
181        sysTextArea.addKeyListener(new KeyAdapter() {
182            @Override public void keyPressed(KeyEvent e) {
183                if (McIDASV.isMac() && e.isMetaDown() && e.getKeyCode() == KeyEvent.VK_F) {
184                    textSearcher.getFindFld().requestFocusInWindow();
185                } else if (!McIDASV.isMac() && GuiUtils.isControlKey(e, KeyEvent.VK_F)) {
186                    textSearcher.getFindFld().requestFocusInWindow();
187                } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
188                    JTextField field = textSearcher.getFindFld();
189                    boolean highlights = textSearcher.getTextWrapper().hasHighlights();
190                    if (!field.getText().isEmpty() && highlights) {
191                        textSearcher.getTextWrapper().removeHighlights();
192                        field.setText("");
193                    }
194                }
195            }
196        });
197
198        sysScrollPane.setViewportView(sysTextArea);
199
200        sysTab.setLayout(new BorderLayout());
201        sysTab.add(sysScrollPane);
202        sysTab.add(textSearcher, BorderLayout.SOUTH);
203
204        tabbedPanel.addTab(SYS_TAB_TITLE, sysTab);
205
206        GroupLayout layout = new GroupLayout(getContentPane());
207        getContentPane().setLayout(layout);
208        layout.setHorizontalGroup(
209            layout.createParallelGroup(LEADING)
210            .addComponent(tabbedPanel)
211        );
212        layout.setVerticalGroup(
213            layout.createParallelGroup(LEADING)
214            .addComponent(tabbedPanel)
215        );
216
217        tabbedPanel.addChangeListener(this);
218
219        pack();
220        setSize(600, 600);
221        setLocationRelativeTo(mcv.getIdvUIManager().getFrame());
222    }
223
224    /**
225     * Populate the regular "About McIDAS-V" tab.
226     * <p>
227     * Contains information like build date, a link to our website, etc.
228     * </p>
229     *
230     * @return Panel suitable for using inside a {@link JTabbedPane}.
231     */
232    private JPanel buildAboutMcv() {
233        StateManager stateManager = (StateManager)mcv.getStateManager();
234
235        JEditorPane editor = new JEditorPane();
236        editor.setEditable(false);
237        editor.setContentType("text/html");
238        String html = stateManager.getMcIdasVersionAbout();
239        editor.setText(html);
240        editor.setBackground(new JPanel().getBackground());
241        editor.addHyperlinkListener(mcv);
242
243        String splashIcon = mcv.getProperty(Constants.PROP_SPLASHICON, "");
244        final JLabel iconLbl = new JLabel(getImageIcon(splashIcon));
245
246        iconLbl.setToolTipText("McIDAS-V Homepage");
247        iconLbl.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
248        iconLbl.addMouseListener(new MouseAdapter() {
249            public void mouseClicked(MouseEvent evt) {
250                String url = mcv.getProperty(Constants.PROP_HOMEPAGE, "");
251                try {
252                    HyperlinkEvent link = new HyperlinkEvent(
253                        iconLbl,
254                        HyperlinkEvent.EventType.ACTIVATED,
255                        new URL(url)
256                    );
257                    mcv.hyperlinkUpdate(link);
258                } catch (MalformedURLException e) {
259                    logger.warn("Malformed URL: '"+url+"'", e);
260                }
261            }
262        });
263
264        JPanel contents = topCenter(inset(iconLbl, 5), inset(editor, 5));
265        contents.setBorder(createBevelBorder(RAISED, GRAY, GRAY));
266        return contents;
267    }
268
269    /**
270     * Populates the {@literal "System Information"} tab.
271     * <p>
272     * The system information is collected on a separate thread, and when done,
273     * the results are added to the tab (on the Event Dispatch Thread).
274     */
275    private void populateSystemTab() {
276        Misc.runInABit(500, () -> {
277            if (!hasSysInfo.get()) {
278                String sysInfo = getSystemInformation();
279                SwingUtilities.invokeLater(() -> {
280                    // the caret manipulation is done so that the "append"
281                    // call doesn't result in the text area being
282                    // auto-scrolled to the end. however, it's also nice to
283                    // have the caret available so that keystroke navigation
284                    // of the text area still works.
285                    DefaultCaret caret = (DefaultCaret)sysTextArea.getCaret();
286                    caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
287                    sysTextArea.setText(sysInfo);
288                    caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
289                    hasSysInfo.set(true);
290                    sysTextArea.requestFocus();
291                });
292            }
293        });
294    }
295
296    /**
297     * Respond to a change in the {@link JTabbedPane}.
298     * <p>
299     * If the user has decided to make the {@literal "System Information"} tab
300     * visible, this method will call {@link #populateSystemTab()}.
301     *
302     * @param e Event that represents the state change. Cannot be {@code null}.
303     */
304    public void stateChanged(ChangeEvent e) {
305        JTabbedPane tabPane = (JTabbedPane)e.getSource();
306        int newIndex = tabPane.getSelectedIndex();
307        if (newIndex == 1) {
308            populateSystemTab();
309        }
310    }
311}