// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.resource.other; import tv.porst.jhexview.DataChangedEvent; import tv.porst.jhexview.IDataChangedListener; import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; // import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; import java.nio.file.Path; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; import javax.swing.SwingWorker; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import org.infinity.NearInfinity; import org.infinity.gui.ButtonPanel; import org.infinity.gui.InfinityTextArea; import org.infinity.gui.ViewerUtil; import org.infinity.gui.WindowBlocker; import org.infinity.gui.hexview.GenericHexViewer; import org.infinity.resource.Closeable; import org.infinity.resource.Profile; import org.infinity.resource.Resource; import org.infinity.resource.ResourceFactory; import org.infinity.resource.ViewableContainer; import org.infinity.resource.Writeable; import org.infinity.resource.key.BIFFResourceEntry; import org.infinity.resource.key.FileResourceEntry; import org.infinity.resource.key.ResourceEntry; import org.infinity.util.Misc; import org.infinity.util.io.FileManager; import org.infinity.util.io.StreamUtils; public final class UnknownResource implements Resource, Closeable, Writeable, ActionListener, ChangeListener, DocumentListener, CaretListener, IDataChangedListener { private static final int TAB_VIEW = 0; private static final int TAB_TEXT = 1; private static final int TAB_RAW = 2; private static final int MIN_SIZE_WARN = 4 * 1024 * 1024; // Show warning for files >= size private static final int MIN_SIZE_BLOCK_TEXT = 128 * 1024 * 1024; // Block text edit >= size private static final int MIN_SIZE_BLOCK_RAW = 256 * 1024 * 1024; // Block raw edit >= size private final ResourceEntry entry; private final long entrySize; private final ButtonPanel buttonPanel = new ButtonPanel(); private JTabbedPane tabbedPane; private InfinityTextArea editor; private GenericHexViewer hexViewer; private JButton bShowEditor; private JPanel panelMain, panelRaw; private boolean textModified, dataSynced; private Charset textCharset; public UnknownResource(ResourceEntry entry) throws Exception { this.entry = entry; int[] data = this.entry.getResourceInfo(); if (data != null && data.length > 0) { entrySize = (data.length == 1) ? data[0] : (data[0] * data[1]); } else { entrySize = 0L; } } //--------------------- Begin Interface Closeable --------------------- @Override public void close() throws Exception { if (isTextModified() || isRawModified()) { Path output = null; if (entry instanceof BIFFResourceEntry) { output = FileManager.query(Profile.getRootFolders(), Profile.getOverrideFolderName(), entry.toString()); } else if (entry instanceof FileResourceEntry) { output = entry.getActualPath(); } if (output != null) { final String options[] = {"Save changes", "Discard changes", "Cancel"}; int result = JOptionPane.showOptionDialog(panelMain, "Save changes to " + output.toString(), "Resource changed", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); if (result == 0) { ResourceFactory.saveResource(this, panelMain.getTopLevelAncestor()); } else if (result != 1) { throw new Exception("Save aborted"); } } } } //--------------------- End Interface Closeable --------------------- // --------------------- Begin Interface ChangeListener --------------------- @Override public void stateChanged(ChangeEvent e) { if (e.getSource() == tabbedPane) { synchronizeData(tabbedPane.getSelectedIndex()); updateStatusBar(); } } // --------------------- End Interface ChangeListener --------------------- // --------------------- Begin Interface ActionListener --------------------- @Override public void actionPerformed(ActionEvent event) { if (event.getSource() == bShowEditor) { openTextEditor(true); } else if (event.getSource() == buttonPanel.getControlByType(ButtonPanel.Control.EXPORT_BUTTON)) { ResourceFactory.exportResource(entry, panelMain.getTopLevelAncestor()); } else if (event.getSource() == buttonPanel.getControlByType(ButtonPanel.Control.SAVE)) { if (ResourceFactory.saveResource(this, panelMain.getTopLevelAncestor())) { setTextModified(false); setRawModified(false); } } } // --------------------- End Interface ActionListener --------------------- // --------------------- Begin Interface DocumentListener --------------------- @Override public void insertUpdate(DocumentEvent e) { setTextModified(true); } @Override public void removeUpdate(DocumentEvent e) { setTextModified(true); } @Override public void changedUpdate(DocumentEvent e) { setTextModified(true); } // --------------------- End Interface DocumentListener --------------------- // --------------------- Begin Interface CaretListener --------------------- @Override public void caretUpdate(CaretEvent e) { if (e.getSource() == editor) { updateStatusBar(); } } // --------------------- End Interface CaretListener --------------------- // --------------------- Begin Interface IDataChangedListener --------------------- @Override public void dataChanged(DataChangedEvent event) { setRawModified(true); } // --------------------- End Interface IDataChangedListener --------------------- // --------------------- Begin Interface Resource --------------------- @Override public ResourceEntry getResourceEntry() { return entry; } // --------------------- End Interface Resource --------------------- // --------------------- Begin Interface Viewable --------------------- @Override public JComponent makeViewer(ViewableContainer container) { ((JButton)buttonPanel.addControl(ButtonPanel.Control.EXPORT_BUTTON)).addActionListener(this); ((JButton)buttonPanel.addControl(ButtonPanel.Control.SAVE)).addActionListener(this); buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(false); GridBagConstraints gbc = new GridBagConstraints(); // creating View tab JPanel panelView = new JPanel(new GridBagLayout()); panelView.setBorder(BorderFactory.createLoweredBevelBorder()); JLabel label = new JLabel("Unsupported file format", JLabel.CENTER); bShowEditor = new JButton("Edit as text"); bShowEditor.addActionListener(this); gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0); panelView.add(new JLabel(), gbc); gbc = ViewerUtil.setGBC(gbc, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0); panelView.add(label, gbc); gbc = ViewerUtil.setGBC(gbc, 0, 2, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(8, 0, 0, 0), 0, 0); panelView.add(bShowEditor, gbc); gbc = ViewerUtil.setGBC(gbc, 0, 3, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0); panelView.add(new JLabel(), gbc); // creating (empty) Edit tab editor = new InfinityTextArea(true); editor.getDocument().addDocumentListener(this); editor.addCaretListener(this); // creating Raw tab (stub) panelRaw = new JPanel(new BorderLayout()); if (getEntrySize() >= MIN_SIZE_BLOCK_RAW) { label = new JLabel("File is too big for the hex editor (" + getEntrySize() + " bytes).", JLabel.CENTER); panelRaw.add(label, BorderLayout.CENTER); } tabbedPane = new JTabbedPane(); tabbedPane.addTab("View", panelView); tabbedPane.addTab("Edit", new JScrollPane(editor)); tabbedPane.addTab("Raw", panelRaw); tabbedPane.setEnabledAt(TAB_TEXT, false); tabbedPane.addChangeListener(this); panelMain = new JPanel(new BorderLayout()); panelMain.add(tabbedPane, BorderLayout.CENTER); panelMain.add(buttonPanel, BorderLayout.SOUTH); return panelMain; } // --------------------- End Interface Viewable --------------------- //--------------------- Begin Interface Writeable --------------------- @Override public void write(OutputStream os) throws IOException { if (tabbedPane.getSelectedIndex() == TAB_TEXT) { StreamUtils.writeString(os, editor.getText(), editor.getText().length(), textCharset); } else { StreamUtils.writeBytes(os, hexViewer.getData()); } } //--------------------- End Interface Writeable --------------------- private boolean isEditorActive() { return tabbedPane.isEnabledAt(TAB_TEXT); } private void setEditorActive(boolean activate) { if (tabbedPane.isEnabledAt(TAB_TEXT) != activate) { if (!activate && tabbedPane.getSelectedIndex() == TAB_TEXT) { tabbedPane.setSelectedIndex(TAB_VIEW); } tabbedPane.setEnabledAt(TAB_TEXT, activate); } } // Returns file size of ResourceEntry private long getEntrySize() { return entrySize; } private boolean isRawActive() { return (hexViewer != null); } private boolean isTextModified() { return (isEditorActive() && textModified); } private void setTextModified(boolean modified) { if (isEditorActive()) { textModified = modified; setDataInSync(!modified); setSaveButtonEnabled(isTextModified()); } } private boolean isRawModified() { return (isRawActive() && hexViewer.isModified()); } private void setRawModified(boolean modified) { if (isRawActive()) { if (!modified) { hexViewer.clearModified(); } setDataInSync(!modified); setSaveButtonEnabled(isRawModified()); } } // Returns true if content in one of the editors has been modified by the user private boolean isDataInSync() { if (isEditorActive() && isRawActive()) { return dataSynced; } else { return true; } } // Mark data as (not) synchronized private void setDataInSync(boolean b) { dataSynced = b; } // Synchronizes data in text and raw tabs. Sync target is specified by the tabIndex parameter. private void synchronizeData(int tabIndex) { switch (tabIndex) { case TAB_TEXT: { if (!isDataInSync()) { int pos = editor.getCaretPosition(); editor.setText(hexViewer.getText(textCharset)); editor.setCaretPosition(Math.min(editor.getDocument().getLength(), pos)); editor.discardAllEdits(); setDataInSync(true); } editor.requestFocusInWindow(); break; } case TAB_RAW: { // lazy initialization if (getEntrySize() < MIN_SIZE_BLOCK_RAW) { if (!isRawActive()) { try { WindowBlocker.blockWindow(true); hexViewer = new GenericHexViewer(entry); hexViewer.addDataChangedListener(this); hexViewer.setCurrentOffset(0L); panelRaw.add(hexViewer, BorderLayout.CENTER); } catch (Exception e) { e.printStackTrace(); } finally { WindowBlocker.blockWindow(false); } } if (!isDataInSync()) { hexViewer.setText(editor.getText(), textCharset); setDataInSync(true); } hexViewer.requestFocusInWindow(); } break; } } } private void setSaveButtonEnabled(boolean enable) { buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(enable); } private void updateStatusBar() { if (isEditorActive() && tabbedPane.getSelectedIndex() == TAB_TEXT) { int row = editor.getCaretLineNumber() + 1; int col = editor.getCaretOffsetFromLineStart() + 1; NearInfinity.getInstance().getStatusBar().setCursorText(Integer.toString(row) + ":" + Integer.toString(col)); } else if (isRawActive() && tabbedPane.getSelectedIndex() == TAB_RAW) { hexViewer.updateStatusBar(); } else { NearInfinity.getInstance().getStatusBar().setCursorText(""); } } // Opens resource in text editor, optionally trigger safeguard mechanisms if file is big private void openTextEditor(boolean confirmSize) { if (isEditorActive()) { tabbedPane.setSelectedIndex(TAB_TEXT); return; } // Confirm loading big files if (confirmSize && entry instanceof FileResourceEntry) { if (getEntrySize() >= MIN_SIZE_BLOCK_TEXT) { JOptionPane.showMessageDialog(panelMain, "File is too big for the text editor (" + getEntrySize() + " bytes).", "File size error", JOptionPane.ERROR_MESSAGE); return; } else if (getEntrySize() >= MIN_SIZE_WARN) { if (JOptionPane.showConfirmDialog(panelMain, "File size is " + getEntrySize() + " bytes. " + "Do you really want to load the file into the text editor?", "Show as text?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) != JOptionPane.YES_OPTION) { return; } } } WindowBlocker.blockWindow(true); new SwingWorker<Void, Void>() { @Override protected Void doInBackground() throws Exception { try { boolean success = false; try { // try to determine character encoding format of text data final byte[] data = isRawModified() ? hexViewer.getData() : StreamUtils.toArray(entry.getResourceBuffer()); textCharset = Misc.detectCharset(data); editor.setText(new String(data, textCharset)); editor.setCaretPosition(0); editor.discardAllEdits(); // don't undo loading operation success = true; } catch (Exception e) { e.printStackTrace(); } if (success) { setEditorActive(true); setTextModified(false); tabbedPane.setSelectedIndex(TAB_TEXT); editor.requestFocusInWindow(); } else { JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Error reading file data.", "Error", JOptionPane.ERROR_MESSAGE); } } finally { WindowBlocker.blockWindow(false); } return null; } }.execute(); } }