/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ package illarion.easynpc.gui; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.util.ContextInitializer; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.util.StatusPrinter; import illarion.common.bug.CrashReporter; import illarion.common.bug.ReportDialogFactorySwing; import illarion.common.util.DirectoryManager; import illarion.common.util.DirectoryManager.Directory; import illarion.easynpc.EasyNpcScript; import illarion.easynpc.Lang; import illarion.easynpc.crash.AWTCrashHandler; import illarion.easynpc.gui.syntax.EasyNpcTokenMakerFactory; import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; import org.pushingpixels.flamingo.api.common.JCommandButton; import org.pushingpixels.flamingo.api.common.RichTooltip; import org.pushingpixels.flamingo.api.ribbon.JRibbonFrame; import org.pushingpixels.flamingo.api.ribbon.RibbonTask; import org.pushingpixels.substance.api.SubstanceLookAndFeel; import org.pushingpixels.substance.api.tabbed.BaseTabCloseListener; import org.pushingpixels.substance.api.tabbed.VetoableTabCloseListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.bridge.SLF4JBridgeHandler; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeListener; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; /** * This is the Main Frame of the display of the easyNPC editor. * * @author Martin Karing <nitram@illarion.org> */ public final class MainFrame extends JRibbonFrame { // NO_UCD /** * The instance of the MainFrame that is used by other parts of the GUI. */ private static MainFrame instance; /** * The logger instance that takes care for the logging output of this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(MainFrame.class); /** * The serialization UID of this main frame. */ private static final long serialVersionUID = 1L; /** * The area where the error messages are displayed. */ @Nonnull private final ErrorPane errorArea; /** * The main splitted panel. In the upper part the editor is displayed, the * lower part shows the error list. */ @Nonnull private final JSplitPane mainPanel; /** * The Tab Pane the editors are displayed in. */ @Nonnull private final JTabbedPane tabbedEditorArea; /** * This is the instance of the documentation browser that is ready for usage. */ @Nullable private DocuBrowser docuBrowser; @Nonnull private final UndoMonitor undoMonitor; /** * Default constructor that creates the window and builds all required * elements into this window. */ private MainFrame() { super("easyNPC Scripteditor"); TokenMakerFactory.setDefaultInstance(new EasyNpcTokenMakerFactory()); undoMonitor = new UndoMonitor(this); RibbonTask startTask = new RibbonTask(Lang.getMsg(getClass(), "ribbonTaskStart"), new ClipboardBand(), new SearchBand(this), new CompileBand()); getRibbon().addTask(startTask); JCommandButton saveButton = new JCommandButton(Utils.getResizableIconFromResource("filesave.png")); saveButton.setActionRichTooltip(new RichTooltip(Lang.getMsg(getClass(), "saveButtonTooltipTitle"), Lang.getMsg(getClass(), "saveButtonTooltip"))); saveButton.addActionListener(e -> Utils.saveEasyNPC(this, getCurrentScriptEditor())); getRibbon().addTaskbarComponent(saveButton); getRibbon().addTaskbarComponent(undoMonitor.getUndoButton()); getRibbon().addTaskbarComponent(undoMonitor.getRedoButton()); getRibbon().setApplicationMenu(new MainMenu(this)); getRibbon().configureHelp(Utils.getResizableIconFromResource("help.png"), e -> showDocuBrowser()); JPanel rootPanel = new JPanel(new BorderLayout()); mainPanel = new JSplitPane(JSplitPane.VERTICAL_SPLIT); tabbedEditorArea = new JTabbedPane(SwingConstants.TOP); tabbedEditorArea.addChangeListener(undoMonitor); JTabbedPane footerPane = new JTabbedPane(SwingConstants.TOP); errorArea = new ErrorPane(); footerPane.addTab(Lang.getMsg(getClass(), "errorTab"), errorArea); //$NON-NLS-1$ mainPanel.setBottomComponent(footerPane); mainPanel.setTopComponent(tabbedEditorArea); footerPane.setPreferredSize(errorArea.getPreferredSize()); rootPanel.add(mainPanel, BorderLayout.CENTER); getContentPane().add(rootPanel); pack(); Config.getInstance().getLastWindowValue(this); setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { dispose(); } @Override public void windowClosing(WindowEvent e) { closeWindow(); } }); validate(); getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke("F1"), "displayHelpWindow"); getRootPane().getActionMap().put("displayHelpWindow", new Action() { @Override public void actionPerformed(ActionEvent e) { showDocuBrowser(); } @Override public void addPropertyChangeListener( PropertyChangeListener listener) { // nothing } @Nonnull @Override public Object getValue(String key) { return "displayHelpWindow"; } @Override public boolean isEnabled() { return true; } @Override public void putValue(String key, Object value) { // nothing } @Override public void removePropertyChangeListener( PropertyChangeListener listener) { // nothing } @Override public void setEnabled(boolean b) { // nothing } }); mainPanel.setDividerLocation(Config.getInstance().getSplitPaneState()); /* The listener that listens the editor tabs to be closed and ask the user to save the content of the tab in case its needed. */ BaseTabCloseListener editorTabListener = new VetoableTabCloseListener() { @Override public void tabClosed( @Nonnull JTabbedPane pane, @Nonnull Component component) { ((Editor) component).cleanup(); if (pane.getTabCount() == 0) { addNewScript(); } } @Override public void tabClosing( JTabbedPane pane, Component component) { // nothing } @Override public boolean vetoTabClosing( JTabbedPane pane, Component component) { Editor editor = (Editor) component; if (!editor.changedSinceSave()) { return false; } Object[] options = {Lang.getMsg(MainFrame.class, "UnsavedChanges.saveButton"), Lang.getMsg(MainFrame.class, "UnsavedChanges.discardButton"), Lang.getMsg(MainFrame.class, "UnsavedChanges.cancelButton")}; int result = JOptionPane .showOptionDialog(MainFrame.this, Lang.getMsg(MainFrame.class, "UnsavedChanges.message"), Lang.getMsg(MainFrame.class, "UnsavedChanges.title"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); if (result == JOptionPane.YES_OPTION) { Utils.saveEasyNPC(MainFrame.this, editor); return false; } return result == JOptionPane.CANCEL_OPTION; } }; SubstanceLookAndFeel.registerTabCloseChangeListener(tabbedEditorArea, editorTabListener); setApplicationIcon(Utils.getResizableIconFromResource("easynpc256.png")); Collection<String> lastFiles = Config.getInstance().getOldFiles(); if (lastFiles.isEmpty()) { addNewScript(); } else { for (String file : lastFiles) { if (file.length() < 3) { continue; } if ("new".equals(file)) { //$NON-NLS-1$ continue; } try { EasyNpcScript easyScript = new EasyNpcScript(Paths.get(file)); addNewScript().loadScript(easyScript); setCurrentTabTitle(easyScript.getSourceScriptFile().getFileName().toString()); } catch (@Nonnull IOException e1) { LOGGER.warn("Originally opened file: {} could not be opened.", file); } } } if (getOpenTabs() == 0) { addNewScript(); } } /** * This function should be used to shutdown the entire editor instantly. * Only do this in case there is no other way. All data will be lost. * * @param message the error message that is displayed. */ public static void crashEditor(@Nullable String message) { if (message != null) { JOptionPane.showMessageDialog(null, message, Lang.getMsg(MainFrame.class, "crashEditor.Title"), JOptionPane.ERROR_MESSAGE); LOGGER.error("Editor crashed! Fatal error: {}", message); } else { LOGGER.error("Editor crashed!"); } System.exit(-1); } /** * Start the GUI of the parser. * * @param args start arguments */ public static void main(String... args) { initLogging(); Config.getInstance().init(); JFrame.setDefaultLookAndFeelDecorated(Config.getInstance().getUseWindowDecoration()); JDialog.setDefaultLookAndFeelDecorated(Config.getInstance().getUseWindowDecoration()); CrashReporter.getInstance().setConfig(Config.getInstance().getInternalCfg()); CrashReporter.getInstance().setMessageSource(Lang.getInstance()); CrashReporter.getInstance().setDialogFactory(new ReportDialogFactorySwing()); AWTCrashHandler.init(); SwingUtilities.invokeLater(() -> { try { SubstanceLookAndFeel.setSkin(Config.getInstance().getLookAndFeel()); } catch (@Nonnull Exception e) { SubstanceLookAndFeel.setSkin(Config.DEFAULT_LOOK_AND_FEEL); } instance = new MainFrame(); instance.setVisible(true); }); } private static void initLogging() { //noinspection UseOfSystemOutOrSystemErr System.out.println("Startup done."); SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); Path userDir = DirectoryManager.getInstance().getDirectory(Directory.User); if (userDir == null) { return; } System.setProperty("log_dir", userDir.toAbsolutePath().toString()); //Reload: LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); ContextInitializer ci = new ContextInitializer(lc); lc.reset(); try { ci.autoConfig(); } catch (JoranException ignored) { } StatusPrinter.printInCaseOfErrorsOrWarnings(lc); } /** * Get the singleton instance of this class. * * @return the singleton instance */ static MainFrame getInstance() { return instance; } /** * Get the script editor that is currently activated. * * @return the currently activated script editor */ @Nonnull public Editor getCurrentScriptEditor() { return getScriptEditor(tabbedEditorArea.getSelectedIndex()); } /** * Get the index of the current tab. * * @return the index of the current tab */ public int getCurrentTab() { return tabbedEditorArea.getSelectedIndex(); } /** * Get the area the errors are displayed in. * * @return the area the errors are displayed in */ @Nonnull public ErrorPane getErrorArea() { return errorArea; } /** * Get the amount of currently open tabs. * * @return the amount of currently open tabs */ public int getOpenTabs() { return tabbedEditorArea.getTabCount(); } /** * Set the title of the currently selected tab. * * @param title the title of the currently selected tab */ public void setCurrentTabTitle(String title) { setTabTitle(tabbedEditorArea.getSelectedIndex(), title); } /** * Add a new, empty script to the editors. * * @return the editor that was now just created */ @Nonnull Editor addNewScript() { return addNewScript(null); } /** * Add a new, empty script to the editors. * * @return the editor that was now just created */ @Nonnull Editor addNewScript(@Nullable String templateText) { Editor editor = new Editor(this, undoMonitor); editor.putClientProperty(SubstanceLookAndFeel.TABBED_PANE_CLOSE_BUTTONS_PROPERTY, Boolean.TRUE); tabbedEditorArea .insertTab(Lang.getMsg(getClass(), "newScriptTab"), null, editor, null, tabbedEditorArea.getTabCount()); tabbedEditorArea.setSelectedIndex(tabbedEditorArea.getTabCount() - 1); if (templateText != null) { editor.setTemplateText(templateText); } return editor; } /** * Check if a script file is already opened in a editor and return the index * of the editor in case it is. * * @param file the file that is to be opened * @return the index of the editor that opened this file or -1 in case its * not opened yet */ int alreadyOpen(Path file) { int count = tabbedEditorArea.getComponentCount(); for (int i = 0; i < count; i++) { Editor currentComp = (Editor) tabbedEditorArea.getComponent(i); if ((currentComp.getScriptFile() != null) && currentComp.getScriptFile().equals(file)) { return i; } } return -1; } /** * Close this window. This also causes to check the changed and opened * editors and save them in case its needed. */ void closeWindow() { int tabCount = getOpenTabs(); Collection<Path> fileList = new ArrayList<>(); for (int i = 0; i < tabCount; i++) { Editor editor = getScriptEditor(i); if (editor.changedSinceSave()) { Object[] options = {Lang.getMsg(MainFrame.class, "UnsavedChanges.saveButton"), Lang.getMsg(MainFrame.class, "UnsavedChanges.discardButton"), Lang.getMsg(MainFrame.class, "UnsavedChanges.cancelButton")}; int result = JOptionPane.showOptionDialog(this, String.format(Lang.getMsg(MainFrame.class, "UnsavedChanges.message2"), editor.getFileName() ), Lang.getMsg(MainFrame.class, "UnsavedChanges.title"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0] ); if (result == JOptionPane.YES_OPTION) { Utils.saveEasyNPC(this, editor); fileList.add(editor.getScriptFile()); } } else { if (editor.getScriptFile() != null) { fileList.add(editor.getScriptFile()); } } } if (!fileList.isEmpty()) { Config.getInstance().setOldFiles(fileList); } Config.getInstance().setLastWindowValues(this); Config.getInstance().setSplitPaneState((double) mainPanel.getDividerLocation() / mainPanel.getHeight()); setVisible(false); dispose(); Config.getInstance().save(); System.exit(0); } /** * Get the script editor that is attached to a specified tab. * * @param index the tab index * @return the editor attached to this tab index */ @Nonnull Editor getScriptEditor(int index) { return (Editor) tabbedEditorArea.getComponentAt(index); } /** * Set the tab that should be displayed now by its index. * * @param index the index of the tab to display */ void setCurrentEditorTab(int index) { tabbedEditorArea.setSelectedIndex(index); undoMonitor.updateUndoRedo(getScriptEditor(index)); } /** * Set the title of a tab that holds a specified component. * * @param component the component in the tab * @param title the new title of the tab */ void setTabTitle(Editor component, String title) { int count = tabbedEditorArea.getComponentCount(); for (int i = 0; i < count; i++) { Editor currentComp = (Editor) tabbedEditorArea.getComponent(i); if (currentComp.equals(component)) { setTabTitle(i, title); } } } /** * Set the title of a tab at the specified index. * * @param index the index of the tab thats title shall change * @param title the new title for the tab */ private void setTabTitle(int index, String title) { tabbedEditorArea.setTitleAt(index, title); } private void showDocuBrowser() { if (docuBrowser == null) { docuBrowser = new DocuBrowser(this); } docuBrowser.setVisible(true); } }