diff --git a/core/src/net/sf/openrocket/appearance/defaults/ResourceDecalImage.java b/core/src/net/sf/openrocket/appearance/defaults/ResourceDecalImage.java index 09243a4a1..6b41636d0 100644 --- a/core/src/net/sf/openrocket/appearance/defaults/ResourceDecalImage.java +++ b/core/src/net/sf/openrocket/appearance/defaults/ResourceDecalImage.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.util.EventListener; import net.sf.openrocket.appearance.DecalImage; import net.sf.openrocket.document.Attachment; @@ -42,4 +43,14 @@ class ResourceDecalImage implements DecalImage { return this.hashCode() - a.hashCode(); } + @Override + public void addChangeListener(EventListener listener) { + //Unimplemented, this can not change + } + + @Override + public void removeChangeListener(EventListener listener) { + //Unimplemented, this can not change + } + } diff --git a/core/src/net/sf/openrocket/document/Attachment.java b/core/src/net/sf/openrocket/document/Attachment.java index a684f1cf7..493a6797c 100644 --- a/core/src/net/sf/openrocket/document/Attachment.java +++ b/core/src/net/sf/openrocket/document/Attachment.java @@ -4,7 +4,9 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -public interface Attachment extends Comparable { +import net.sf.openrocket.util.ChangeSource; + +public interface Attachment extends Comparable, ChangeSource { public abstract String getName(); diff --git a/core/src/net/sf/openrocket/document/DecalRegistry.java b/core/src/net/sf/openrocket/document/DecalRegistry.java index a604941c5..4be1bcbc9 100644 --- a/core/src/net/sf/openrocket/document/DecalRegistry.java +++ b/core/src/net/sf/openrocket/document/DecalRegistry.java @@ -11,6 +11,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.text.MessageFormat; import java.util.Collection; +import java.util.EventListener; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -19,7 +20,11 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sf.openrocket.appearance.DecalImage; +import net.sf.openrocket.document.attachments.BaseAttachment; import net.sf.openrocket.document.attachments.FileSystemAttachment; +import net.sf.openrocket.gui.watcher.FileWatcher; +import net.sf.openrocket.gui.watcher.WatchEvent; +import net.sf.openrocket.gui.watcher.WatchService; import net.sf.openrocket.logging.LogHelper; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.BugException; @@ -28,12 +33,33 @@ import net.sf.openrocket.util.FileUtils; public class DecalRegistry { private static LogHelper log = Application.getLogger(); + private WatchService watchService = Application.getWatchService(); + DecalRegistry() { - } private Map registeredDecals = new HashMap(); + public DecalImage makeUniqueImage(DecalImage original) { + + if (!(original instanceof DecalImageImpl)) { + return original; + } + + DecalImageImpl o = (DecalImageImpl) original; + + DecalImageImpl newDecal = o.clone(); + + String newName = makeUniqueName(o.getName()); + + newDecal.name = newName; + + registeredDecals.put(newName, newDecal); + + return newDecal; + + } + public DecalImage getDecalImage(Attachment attachment) { String decalName = attachment.getName(); DecalImageImpl d; @@ -73,7 +99,7 @@ public class DecalRegistry { return decals; } - public class DecalImageImpl implements DecalImage { + public class DecalImageImpl implements DecalImage, Cloneable { private final Attachment delegate; @@ -94,15 +120,61 @@ public class DecalRegistry { return name != null ? name : delegate.getName(); } + /** + * This function returns an InputStream backed by a byte[] containing the decal pixels. + * If it reads in the bytes from an actual file, the underlying file is closed. + * + * @return InputStream containing byte[] of the image + * @throws FileNotFoundException + * @throws IOException + */ @Override public InputStream getBytes() throws FileNotFoundException, IOException { - return DecalRegistry.getDecal(this); + // First check if the decal is located on the file system + File exportedFile = getFileSystemLocation(); + if (exportedFile != null) { + InputStream rawIs = new FileInputStream(exportedFile); + try { + byte[] bytes = FileUtils.readBytes(rawIs); + return new ByteArrayInputStream(bytes); + } finally { + rawIs.close(); + } + + } + + return delegate.getBytes(); } @Override public void exportImage(File file, boolean watchForChanges) throws IOException { - DecalRegistry.exportDecal(this, file); - this.fileSystemLocation = file; + try { + InputStream is = getBytes(); + OutputStream os = new BufferedOutputStream(new FileOutputStream(file)); + + FileUtils.copy(is, os); + + is.close(); + os.close(); + + this.fileSystemLocation = file; + + if (watchForChanges) { + watchService.register(new FileWatcher(this.fileSystemLocation) { + + @Override + public void handleEvent(WatchEvent evt) { + ((BaseAttachment) DecalImageImpl.this.delegate).fireChangeEvent(); + System.out.println(this.getFile() + " has changed"); + + } + + }); + } + + } catch (IOException iex) { + throw new BugException(iex); + } } File getFileSystemLocation() { @@ -126,51 +198,25 @@ public class DecalRegistry { return getName().compareTo(o.getName()); } - } - - /** - * This function returns an InputStream backed by a byte[] containing the decal pixels. - * If it reads in the bytes from an actual file, the underlying file is closed. - * - * @param name - * @return - * @throws FileNotFoundException - * @throws IOException - */ - private static InputStream getDecal(DecalImageImpl decal) throws FileNotFoundException, IOException { - - // First check if the decal is located on the file system - File exportedFile = decal.getFileSystemLocation(); - if (exportedFile != null) { - InputStream rawIs = new FileInputStream(exportedFile); - try { - byte[] bytes = FileUtils.readBytes(rawIs); - return new ByteArrayInputStream(bytes); - } finally { - rawIs.close(); - } + @Override + protected DecalImageImpl clone() { + DecalImageImpl clone = new DecalImageImpl(this.delegate); + clone.fileSystemLocation = this.fileSystemLocation; + return clone; } - return decal.delegate.getBytes(); - - } - - private static void exportDecal(DecalImageImpl decal, File selectedFile) throws IOException { - - try { - InputStream is = decal.getBytes(); - OutputStream os = new BufferedOutputStream(new FileOutputStream(selectedFile)); - - FileUtils.copy(is, os); - - is.close(); - os.close(); - - } catch (IOException iex) { - throw new BugException(iex); + @Override + public void addChangeListener(EventListener listener) { + delegate.addChangeListener(listener); } + @Override + public void removeChangeListener(EventListener listener) { + delegate.removeChangeListener(listener); + } + + } private DecalImageImpl findDecalForFile(File file) { @@ -203,14 +249,17 @@ public class DecalRegistry { * group(3) = "null" * group(4) = "png" */ - private static final Pattern fileNamePattern = Pattern.compile("(.*?)( \\((\\d+)\\)\\)?+)?\\.(\\w*)"); + private static final Pattern fileNamePattern = Pattern.compile("(.*?)( \\((\\d+)\\)+)?\\.(\\w*)"); private static final int BASE_NAME_INDEX = 1; private static final int NUMBER_INDEX = 3; private static final int EXTENSION_INDEX = 4; private String makeUniqueName(String name) { - String newName = "decals/" + name; + String newName = name; + if (!newName.startsWith("decals/")) { + newName = "decals/" + name; + } String basename = ""; String extension = ""; Matcher nameMatcher = fileNamePattern.matcher(newName); diff --git a/core/src/net/sf/openrocket/document/OpenRocketDocument.java b/core/src/net/sf/openrocket/document/OpenRocketDocument.java index 8af24ae50..12709d20a 100644 --- a/core/src/net/sf/openrocket/document/OpenRocketDocument.java +++ b/core/src/net/sf/openrocket/document/OpenRocketDocument.java @@ -3,11 +3,14 @@ package net.sf.openrocket.document; import java.io.File; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; +import net.sf.openrocket.appearance.Appearance; +import net.sf.openrocket.appearance.Decal; import net.sf.openrocket.appearance.DecalImage; import net.sf.openrocket.document.events.DocumentChangeEvent; import net.sf.openrocket.document.events.DocumentChangeListener; @@ -18,6 +21,7 @@ import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; import net.sf.openrocket.rocketcomponent.ComponentChangeListener; import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.simulation.listeners.SimulationListener; @@ -206,6 +210,31 @@ public class OpenRocketDocument implements ComponentChangeListener { } + public int countDecalUsage(DecalImage img) { + int count = 0; + + Iterator it = rocket.iterator(); + while (it.hasNext()) { + RocketComponent comp = it.next(); + Appearance a = comp.getAppearance(); + if (a == null) { + continue; + } + Decal d = a.getTexture(); + if (d == null) { + continue; + } + if (img.equals(d.getImage())) { + count++; + } + } + return count; + } + + public DecalImage makeUniqueDecal(DecalImage img) { + return decalRegistry.makeUniqueImage(img); + } + public DecalImage getDecalImage(Attachment a) { return decalRegistry.getDecalImage(a); } diff --git a/core/src/net/sf/openrocket/document/attachments/BaseAttachment.java b/core/src/net/sf/openrocket/document/attachments/BaseAttachment.java index 549b5800d..f8d53c897 100644 --- a/core/src/net/sf/openrocket/document/attachments/BaseAttachment.java +++ b/core/src/net/sf/openrocket/document/attachments/BaseAttachment.java @@ -5,8 +5,9 @@ import java.io.IOException; import java.io.InputStream; import net.sf.openrocket.document.Attachment; +import net.sf.openrocket.util.AbstractChangeSource; -public abstract class BaseAttachment implements Attachment { +public abstract class BaseAttachment extends AbstractChangeSource implements Attachment { private final String name; @@ -36,5 +37,9 @@ public abstract class BaseAttachment implements Attachment { return getName(); } + @Override + public void fireChangeEvent() { + super.fireChangeEvent(); + } } diff --git a/core/src/net/sf/openrocket/file/AttachmentUtils.java b/core/src/net/sf/openrocket/file/AttachmentUtils.java new file mode 100644 index 000000000..5d76d2036 --- /dev/null +++ b/core/src/net/sf/openrocket/file/AttachmentUtils.java @@ -0,0 +1,25 @@ +package net.sf.openrocket.file; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import net.sf.openrocket.document.Attachment; +import net.sf.openrocket.util.FileUtils; + +public abstract class AttachmentUtils { + + public static void exportAttachment(Attachment a, File outFile) throws IOException { + InputStream is = a.getBytes(); + OutputStream os = new BufferedOutputStream(new FileOutputStream(outFile)); + + FileUtils.copy(is, os); + + is.close(); + os.close(); + + } +} diff --git a/core/src/net/sf/openrocket/gui/ExportDecalDialog.java b/core/src/net/sf/openrocket/gui/ExportDecalDialog.java index 3357c2af7..948c45d3c 100644 --- a/core/src/net/sf/openrocket/gui/ExportDecalDialog.java +++ b/core/src/net/sf/openrocket/gui/ExportDecalDialog.java @@ -16,6 +16,7 @@ import javax.swing.JPanel; import net.miginfocom.swing.MigLayout; import net.sf.openrocket.appearance.DecalImage; import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.file.AttachmentUtils; import net.sf.openrocket.gui.util.SwingPreferences; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.startup.Application; @@ -78,10 +79,10 @@ public class ExportDecalDialog extends JDialog { private void export(DecalImage decal, File selectedFile) { try { - decal.exportImage(selectedFile, false); + AttachmentUtils.exportAttachment(decal, selectedFile); } catch (IOException iex) { + // FIXME - probably want a simple user dialog here since FileIO is not really a bug. throw new BugException(iex); } } - } diff --git a/core/src/net/sf/openrocket/gui/configdialog/AppearancePanel.java b/core/src/net/sf/openrocket/gui/configdialog/AppearancePanel.java index b6be923d5..b88b3b661 100644 --- a/core/src/net/sf/openrocket/gui/configdialog/AppearancePanel.java +++ b/core/src/net/sf/openrocket/gui/configdialog/AppearancePanel.java @@ -244,7 +244,7 @@ public class AppearancePanel extends JPanel { ab.addChangeListener(new StateChangeListener() { @Override public void stateChanged(EventObject e) { - editBtn.setEnabled(ab.getImage() == null); + editBtn.setEnabled(ab.getImage() != null); } }); editBtn.addActionListener(new ActionListener() { @@ -252,7 +252,7 @@ public class AppearancePanel extends JPanel { @Override public void actionPerformed(ActionEvent e) { try { - EditDecalHelper.editDecal(SwingUtilities.getWindowAncestor(AppearancePanel.this), ab.getImage()); + EditDecalHelper.editDecal(SwingUtilities.getWindowAncestor(AppearancePanel.this), document, c, ab.getImage()); } catch (IOException ex) { throw new BugException(ex); } diff --git a/core/src/net/sf/openrocket/gui/dialogs/EditDecalDialog.java b/core/src/net/sf/openrocket/gui/dialogs/EditDecalDialog.java index 2b478b1e5..59ee62ce7 100644 --- a/core/src/net/sf/openrocket/gui/dialogs/EditDecalDialog.java +++ b/core/src/net/sf/openrocket/gui/dialogs/EditDecalDialog.java @@ -24,9 +24,9 @@ import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.startup.Application; public class EditDecalDialog extends JDialog { - + private static final Translator trans = Application.getTranslator(); - + private JRadioButton systemRadio; private JRadioButton commandRadio; private JTextArea commandText; @@ -34,88 +34,111 @@ public class EditDecalDialog extends JDialog { private JCheckBox savePref; private boolean isCancel = false; + private boolean editOne = true; - public EditDecalDialog(final Window owner) { + public EditDecalDialog(final Window owner, boolean promptForEditor, int usageCount) { super(owner, trans.get("EditDecalDialog.title"), Dialog.ModalityType.APPLICATION_MODAL); - - JPanel panel = new JPanel(new MigLayout("fill, ins para")); - - JLabel selectLbl = new JLabel(trans.get("EditDecalDialog.lbl.select")); - panel.add(selectLbl, "gapright, wrap"); - - ButtonGroup execGroup = new ButtonGroup(); - if (Desktop.getDesktop().isSupported(Desktop.Action.EDIT) ) { - - systemRadio = new JRadioButton(trans.get("EditDecalDialog.lbl.system")); - systemRadio.setSelected(false); - panel.add(systemRadio,"wrap"); - execGroup.add(systemRadio); + JPanel panel = new JPanel(new MigLayout("fill, ins para")); + + if (promptForEditor) { + JLabel selectLbl = new JLabel(trans.get("EditDecalDialog.lbl.select")); + panel.add(selectLbl, "gapright, wrap"); - commandRadio = new JRadioButton(trans.get("EditDecalDialog.lbl.cmdline")); - commandRadio.setSelected(false); - panel.add(commandRadio,"wrap"); - execGroup.add(commandRadio); + ButtonGroup execGroup = new ButtonGroup(); - commandText = new JTextArea(); - commandText.setEnabled(false); - panel.add(commandText, "growx, wrap"); - - final JButton chooser = new JButton(trans.get("EditDecalDialog.btn.chooser")); - chooser.setEnabled(false); - chooser.addActionListener( new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - JFileChooser fc = new JFileChooser(); - int action = fc.showOpenDialog(owner); - if ( action == JFileChooser.APPROVE_OPTION) { - commandText.setText(fc.getSelectedFile().getAbsolutePath()); + if (Desktop.getDesktop().isSupported(Desktop.Action.EDIT)) { + + systemRadio = new JRadioButton(trans.get("EditDecalDialog.lbl.system")); + systemRadio.setSelected(true); + panel.add(systemRadio, "wrap"); + execGroup.add(systemRadio); + + commandRadio = new JRadioButton(trans.get("EditDecalDialog.lbl.cmdline")); + commandRadio.setSelected(false); + panel.add(commandRadio, "wrap"); + execGroup.add(commandRadio); + + commandText = new JTextArea(); + commandText.setEnabled(false); + panel.add(commandText, "growx, wrap"); + + final JButton chooser = new JButton(trans.get("EditDecalDialog.btn.chooser")); + chooser.setEnabled(false); + chooser.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + JFileChooser fc = new JFileChooser(); + int action = fc.showOpenDialog(owner); + if (action == JFileChooser.APPROVE_OPTION) { + commandText.setText(fc.getSelectedFile().getAbsolutePath()); + } + } - } + }); + panel.add(chooser, "growx, wrap"); - }); - panel.add(chooser, "growx, wrap"); - - - commandRadio.addChangeListener( new ChangeListener() { - - @Override - public void stateChanged(ChangeEvent e) { - boolean enabled = commandRadio.isSelected(); - commandText.setEnabled(enabled); - chooser.setEnabled(enabled); - } - }); - - } else { - commandText = new JTextArea(); - commandText.setEnabled(false); - panel.add(commandText, "growx, wrap"); - - final JButton chooser = new JButton(trans.get("EditDecalDialog.btn.chooser")); - chooser.setEnabled(false); - chooser.addActionListener( new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - JFileChooser fc = new JFileChooser(); - int action = fc.showOpenDialog(owner); - if ( action == JFileChooser.APPROVE_OPTION) { - commandText.setText(fc.getSelectedFile().getAbsolutePath()); + commandRadio.addChangeListener(new ChangeListener() { + + @Override + public void stateChanged(ChangeEvent e) { + boolean enabled = commandRadio.isSelected(); + commandText.setEnabled(enabled); + chooser.setEnabled(enabled); } - } + }); - }); - panel.add(chooser, "growx, wrap"); - + } else { + commandText = new JTextArea(); + commandText.setEnabled(false); + panel.add(commandText, "growx, wrap"); + + final JButton chooser = new JButton(trans.get("EditDecalDialog.btn.chooser")); + chooser.setEnabled(false); + chooser.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + JFileChooser fc = new JFileChooser(); + int action = fc.showOpenDialog(owner); + if (action == JFileChooser.APPROVE_OPTION) { + commandText.setText(fc.getSelectedFile().getAbsolutePath()); + } + + } + + }); + panel.add(chooser, "growx, wrap"); + + } } - savePref = new JCheckBox(trans.get("EditDecalDialog.lbl.always")); - panel.add(savePref,"wrap"); + if (usageCount > 1) { + ButtonGroup bg = new ButtonGroup(); + final JRadioButton justThisOne = new JRadioButton("just this one", true); + justThisOne.addChangeListener(new ChangeListener() { + + @Override + public void stateChanged(ChangeEvent e) { + EditDecalDialog.this.editOne = justThisOne.isSelected(); + } + + }); + panel.add(justThisOne, "left"); + bg.add(justThisOne); + JRadioButton all = new JRadioButton("all", false); + panel.add(all, "gapleft para, right, wrap"); + bg.add(all); + } + + if (promptForEditor) { + savePref = new JCheckBox(trans.get("EditDecalDialog.lbl.always")); + panel.add(savePref, "wrap"); + } // OK / Cancel buttons JButton okButton = new JButton(trans.get("dlg.but.ok")); @@ -141,10 +164,9 @@ public class EditDecalDialog extends JDialog { GUIUtil.rememberWindowSize(this); GUIUtil.setDisposableDialogOptions(this, okButton); - } - + public boolean isCancel() { return isCancel; } @@ -154,22 +176,25 @@ public class EditDecalDialog extends JDialog { } public boolean isUseSystemEditor() { - return systemRadio!= null && systemRadio.isSelected(); + return systemRadio != null && systemRadio.isSelected(); } public String getCommandLine() { return commandText.getText(); } + public boolean isEditOne() { + return editOne; + } + public void ok() { isCancel = false; this.setVisible(false); } - + public void close() { isCancel = true; this.setVisible(false); } } - diff --git a/core/src/net/sf/openrocket/gui/figure3d/FigureRenderer.java b/core/src/net/sf/openrocket/gui/figure3d/FigureRenderer.java index 66fc4cf00..dbacece61 100644 --- a/core/src/net/sf/openrocket/gui/figure3d/FigureRenderer.java +++ b/core/src/net/sf/openrocket/gui/figure3d/FigureRenderer.java @@ -123,6 +123,10 @@ public class FigureRenderer extends RocketRenderer { cr.renderGeometry(gl, c); } + @Override + public void flushTextureCache(GLAutoDrawable drawable) { + } + private static int getShine(RocketComponent c) { if (c instanceof ExternalComponent) { switch (((ExternalComponent) c).getFinish()) { diff --git a/core/src/net/sf/openrocket/gui/figure3d/RealisticRenderer.java b/core/src/net/sf/openrocket/gui/figure3d/RealisticRenderer.java index 71b8494fb..257778fff 100644 --- a/core/src/net/sf/openrocket/gui/figure3d/RealisticRenderer.java +++ b/core/src/net/sf/openrocket/gui/figure3d/RealisticRenderer.java @@ -190,6 +190,13 @@ public class RealisticRenderer extends RocketRenderer { } + @Override + public void flushTextureCache(GLAutoDrawable drawable) { + // Flush the cache twice to get rid of old images. + clearCaches(drawable.getGL().getGL2()); + clearCaches(drawable.getGL().getGL2()); + } + private void clearCaches(GL2 gl) { log.debug("ClearCaches"); for (Map.Entry e : oldTexCache.entrySet()) { diff --git a/core/src/net/sf/openrocket/gui/figure3d/RocketFigure3d.java b/core/src/net/sf/openrocket/gui/figure3d/RocketFigure3d.java index fdb27c55a..ec4a70d4e 100644 --- a/core/src/net/sf/openrocket/gui/figure3d/RocketFigure3d.java +++ b/core/src/net/sf/openrocket/gui/figure3d/RocketFigure3d.java @@ -107,6 +107,16 @@ public class RocketFigure3d extends JPanel implements GLEventListener { } } + public void flushTextureCaches() { + canvas.invoke(true, new GLRunnable() { + @Override + public boolean run(GLAutoDrawable drawable) { + rr.flushTextureCache(drawable); + return false; + } + }); + } + /** * Return true if 3d view is enabled. This may be toggled by the user at * launch time. diff --git a/core/src/net/sf/openrocket/gui/figure3d/RocketRenderer.java b/core/src/net/sf/openrocket/gui/figure3d/RocketRenderer.java index 0696b435f..dc2b5c449 100644 --- a/core/src/net/sf/openrocket/gui/figure3d/RocketRenderer.java +++ b/core/src/net/sf/openrocket/gui/figure3d/RocketRenderer.java @@ -48,6 +48,8 @@ public abstract class RocketRenderer { public abstract boolean isDrawnTransparent(RocketComponent c); + public abstract void flushTextureCache(GLAutoDrawable drawable); + public RocketComponent pick(GLAutoDrawable drawable, Configuration configuration, Point p, Set ignore) { final GL2 gl = drawable.getGL().getGL2(); diff --git a/core/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java b/core/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java index 51272987b..f5c76648e 100644 --- a/core/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java +++ b/core/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java @@ -61,6 +61,7 @@ import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.masscalc.BasicMassCalculator; import net.sf.openrocket.masscalc.MassCalculator; import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; +import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; @@ -214,6 +215,13 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change @Override public void stateChanged(EventObject e) { // System.out.println("Configuration changed, calling updateFigure"); + if (is3d) { + if (e instanceof ComponentChangeEvent) { + if (((ComponentChangeEvent) e).isTextureChange()) { + figure3d.flushTextureCaches(); + } + } + } updateExtras(); updateFigures(); } diff --git a/core/src/net/sf/openrocket/gui/util/EditDecalHelper.java b/core/src/net/sf/openrocket/gui/util/EditDecalHelper.java index d3f863264..d58a28b33 100644 --- a/core/src/net/sf/openrocket/gui/util/EditDecalHelper.java +++ b/core/src/net/sf/openrocket/gui/util/EditDecalHelper.java @@ -5,91 +5,107 @@ import java.awt.Window; import java.io.File; import java.io.IOException; +import net.sf.openrocket.appearance.AppearanceBuilder; import net.sf.openrocket.appearance.DecalImage; +import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.gui.dialogs.EditDecalDialog; +import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.startup.Application; public class EditDecalHelper { - + // FIXME - need to have a specific set of localizable exceptions come out of this instead of generic IOException; // perhaps - unable to create file, // unable to open system editor // unable to fork process - - private static final SwingPreferences prefs = ((SwingPreferences)Application.getPreferences()); - public static void editDecal( Window parent, DecalImage decal ) throws IOException { - + private static final SwingPreferences prefs = ((SwingPreferences) Application.getPreferences()); + + public static void editDecal(Window parent, OpenRocketDocument doc, RocketComponent component, DecalImage decal) throws IOException { + + boolean sysPrefSet = prefs.isDecalEditorPreferenceSet(); + int usageCount = doc.countDecalUsage(decal); + + //First Check preferences + if (sysPrefSet && usageCount == 1) { + + launchEditor(prefs.isDecalEditorPreferenceSystem(), prefs.getDecalEditorCommandLine(), decal); + return; + } + + EditDecalDialog dialog = new EditDecalDialog(parent, !sysPrefSet, usageCount); + dialog.setVisible(true); + + if (dialog.isCancel()) { + return; + } + + // Do we use the System Preference Editor or from the dialog? + boolean useSystemEditor = false; + String commandLine = ""; + + if (sysPrefSet) { + useSystemEditor = prefs.isDecalEditorPreferenceSystem(); + commandLine = prefs.getDecalEditorCommandLine(); + } else { + useSystemEditor = dialog.isUseSystemEditor(); + commandLine = dialog.getCommandLine(); + // Do we need to save the preferences? + if (dialog.isSavePreferences()) { + prefs.setDecalEditorPreference(useSystemEditor, commandLine); + } + } + + if (dialog.isEditOne()) { + decal = makeDecalUnique(doc, component, decal); + } + + launchEditor(useSystemEditor, commandLine, decal); + + } + + private static DecalImage makeDecalUnique(OpenRocketDocument doc, RocketComponent component, DecalImage decal) { + + DecalImage newImage = doc.makeUniqueDecal(decal); + + AppearanceBuilder appearanceBuilder = new AppearanceBuilder(component.getAppearance()); + appearanceBuilder.setImage(newImage); + + component.setAppearance(appearanceBuilder.getAppearance()); + + return newImage; + } + + private static void launchEditor(boolean useSystemEditor, String commandTemplate, DecalImage decal) throws IOException { + String decalId = decal.getName(); // Create Temp File. int dotlocation = decalId.lastIndexOf('.'); String extension = "tmp"; - if ( dotlocation > 0 && dotlocation < decalId.length() ) { + if (dotlocation > 0 && dotlocation < decalId.length()) { extension = decalId.substring(dotlocation); } File tmpFile = File.createTempFile("OR_graphics", extension); decal.exportImage(tmpFile, true); - //First Check preferences - if ( prefs.isDecalEditorPreferenceSet() ) { + + if (useSystemEditor) { + Desktop.getDesktop().edit(tmpFile); + } else { - // FIXME - need this one or all dialog. + String filename = tmpFile.getAbsolutePath(); - if ( prefs.isDecalEditorPreferenceSystem() ) { - launchSystemEditor( tmpFile ); + String command; + if (commandTemplate.contains("%%")) { + command = commandTemplate.replace("%%", filename); } else { - String commandTemplate = prefs.getDecalEditorCommandLine(); - launchCommandEditor(commandTemplate, tmpFile); + command = commandTemplate + " " + filename; } - return; + + Runtime.getRuntime().exec(command); + } - - // Preference not set, launch dialog - EditDecalDialog dialog = new EditDecalDialog(parent); - dialog.setVisible(true); - - if( dialog.isCancel() ) { - // FIXME - delete tmpfile? - return; - } - - boolean saveToPrefs = dialog.isSavePreferences(); - - if ( dialog.isUseSystemEditor() ) { - if ( saveToPrefs ) { - prefs.setDecalEditorPreference(true, null); - } - launchSystemEditor( tmpFile ); - } else { - String commandLine = dialog.getCommandLine(); - if( saveToPrefs ) { - prefs.setDecalEditorPreference(false, commandLine); - } - launchCommandEditor( commandLine, tmpFile ); - } - - } - - private static void launchSystemEditor( File tmpFile ) throws IOException { - - Desktop.getDesktop().edit(tmpFile); - - } - - private static void launchCommandEditor( String commandTemplate, File tmpFile ) throws IOException { - - String filename = tmpFile.getAbsolutePath(); - - String command; - if( commandTemplate.contains("%%")) { - command = commandTemplate.replace("%%", filename); - } else { - command = commandTemplate + " " + filename; - } - - Runtime.getRuntime().exec(command); - } } diff --git a/core/src/net/sf/openrocket/gui/watcher/FileWatcher.java b/core/src/net/sf/openrocket/gui/watcher/FileWatcher.java new file mode 100644 index 000000000..555df2b8b --- /dev/null +++ b/core/src/net/sf/openrocket/gui/watcher/FileWatcher.java @@ -0,0 +1,37 @@ +package net.sf.openrocket.gui.watcher; + +import java.io.File; + +public abstract class FileWatcher implements Watchable { + + private final File file; + private long lastModifiedTimestamp = 0L; + + public FileWatcher(File file) { + this.file = file; + } + + protected File getFile() { + return file; + } + + @Override + public WatchEvent monitor() { + + long modified = file.lastModified(); + if (modified == 0L) { + // check for removal? + return null; + } + if (modified > lastModifiedTimestamp) { + long oldTimestamp = lastModifiedTimestamp; + lastModifiedTimestamp = modified; + return (oldTimestamp == 0L) ? null : WatchEvent.MODIFIED; + } + return null; + } + + @Override + public abstract void handleEvent(WatchEvent evt); + +} diff --git a/core/src/net/sf/openrocket/gui/watcher/WatchEvent.java b/core/src/net/sf/openrocket/gui/watcher/WatchEvent.java new file mode 100644 index 000000000..dea1f45c0 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/watcher/WatchEvent.java @@ -0,0 +1,8 @@ +package net.sf.openrocket.gui.watcher; + +public enum WatchEvent { + + MODIFIED, + REMOVED; + +} diff --git a/core/src/net/sf/openrocket/gui/watcher/WatchKey.java b/core/src/net/sf/openrocket/gui/watcher/WatchKey.java new file mode 100644 index 000000000..d00b291e9 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/watcher/WatchKey.java @@ -0,0 +1,7 @@ +package net.sf.openrocket.gui.watcher; + +public interface WatchKey { + + public void cancel(); + +} diff --git a/core/src/net/sf/openrocket/gui/watcher/WatchService.java b/core/src/net/sf/openrocket/gui/watcher/WatchService.java new file mode 100644 index 000000000..df279d826 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/watcher/WatchService.java @@ -0,0 +1,7 @@ +package net.sf.openrocket.gui.watcher; + +public interface WatchService { + + public abstract WatchKey register(Watchable w); + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/watcher/WatchServiceImpl.java b/core/src/net/sf/openrocket/gui/watcher/WatchServiceImpl.java new file mode 100644 index 000000000..03dd9bb36 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/watcher/WatchServiceImpl.java @@ -0,0 +1,72 @@ +package net.sf.openrocket.gui.watcher; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class WatchServiceImpl implements WatchService { + + private final static int INTERVAL_MS = 1000; + + private static AtomicInteger threadcount = new AtomicInteger(0); + + private ScheduledExecutorService executor = Executors.newScheduledThreadPool(2, new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setName("WatchService-" + threadcount.getAndIncrement()); + return t; + } + }); + + public WatchServiceImpl() { + } + + /* (non-Javadoc) + * @see net.sf.openrocket.gui.watcher.WatchService#register(net.sf.openrocket.gui.watcher.Watchable) + */ + @Override + public WatchKey register(Watchable w) { + ScheduledFuture future = executor.scheduleWithFixedDelay(new WatchableRunner(w), 0, INTERVAL_MS, TimeUnit.MILLISECONDS); + + return new WatchKeyImpl(future); + } + + public class WatchKeyImpl implements WatchKey { + + ScheduledFuture future; + + private WatchKeyImpl(ScheduledFuture future) { + this.future = future; + } + + @Override + public void cancel() { + future.cancel(true); + } + + } + + private class WatchableRunner implements Runnable { + + private Watchable w; + + private WatchableRunner(Watchable w) { + this.w = w; + } + + @Override + public void run() { + + WatchEvent evt = w.monitor(); + if (evt != null) { + w.handleEvent(evt); + } + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/watcher/Watchable.java b/core/src/net/sf/openrocket/gui/watcher/Watchable.java new file mode 100644 index 000000000..9ac590910 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/watcher/Watchable.java @@ -0,0 +1,9 @@ +package net.sf.openrocket.gui.watcher; + +public interface Watchable { + + public WatchEvent monitor(); + + public void handleEvent(WatchEvent evt); + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java b/core/src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java index 5a2a2e24f..ff5d6c8b1 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java +++ b/core/src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java @@ -5,7 +5,7 @@ import java.util.EventObject; public class ComponentChangeEvent extends EventObject { private static final long serialVersionUID = 1L; - + /** A change that does not affect simulation results in any way (name, color, etc.) */ public static final int NONFUNCTIONAL_CHANGE = 1; /** A change that affects the mass properties of the rocket */ @@ -23,6 +23,8 @@ public class ComponentChangeEvent extends EventObject { public static final int MOTOR_CHANGE = 32; /** A change that affects the events occurring during flight. */ public static final int EVENT_CHANGE = 64; + /** A change to the 3D texture assigned to a component*/ + public static final int TEXTURE_CHANGE = 128; /** A bit-field that contains all possible change types. */ public static final int ALL_CHANGE = 0xFFFFFFFF; @@ -47,6 +49,9 @@ public class ComponentChangeEvent extends EventObject { return (RocketComponent) super.getSource(); } + public boolean isTextureChange() { + return (type & TEXTURE_CHANGE) != 0; + } public boolean isAerodynamicChange() { return (type & AERODYNAMIC_CHANGE) != 0; diff --git a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java index 898d1ad38..e2110b45a 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java +++ b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java @@ -2,11 +2,13 @@ package net.sf.openrocket.rocketcomponent; import java.util.Collection; import java.util.EventListener; +import java.util.EventObject; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import net.sf.openrocket.appearance.Appearance; +import net.sf.openrocket.appearance.Decal; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.startup.Application; @@ -20,6 +22,7 @@ import net.sf.openrocket.util.LineStyle; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.SafetyMutex; import net.sf.openrocket.util.SimpleStack; +import net.sf.openrocket.util.StateChangeListener; import net.sf.openrocket.util.UniqueID; @@ -423,6 +426,17 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab */ public void setAppearance(Appearance appearance) { this.appearance = appearance; + Decal d = this.appearance.getTexture(); + if (d != null) { + d.getImage().addChangeListener(new StateChangeListener() { + + @Override + public void stateChanged(EventObject e) { + fireComponentChangeEvent(ComponentChangeEvent.TEXTURE_CHANGE); + } + + }); + } fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } diff --git a/core/src/net/sf/openrocket/startup/Application.java b/core/src/net/sf/openrocket/startup/Application.java index c4b206567..16bf4ed4a 100644 --- a/core/src/net/sf/openrocket/startup/Application.java +++ b/core/src/net/sf/openrocket/startup/Application.java @@ -3,6 +3,7 @@ package net.sf.openrocket.startup; import net.sf.openrocket.database.ComponentPresetDao; import net.sf.openrocket.database.motor.MotorDatabase; import net.sf.openrocket.database.motor.ThrustCurveMotorSetDatabase; +import net.sf.openrocket.gui.watcher.WatchService; import net.sf.openrocket.l10n.ClassBasedTranslator; import net.sf.openrocket.l10n.DebugTranslator; import net.sf.openrocket.l10n.ExceptionSuppressingTranslator; @@ -68,7 +69,9 @@ public final class Application { Application.logger = logger; } - + public static WatchService getWatchService() { + return Application.injector.getInstance(WatchService.class); + } /** * Return the log buffer. diff --git a/core/src/net/sf/openrocket/startup/ApplicationModule.java b/core/src/net/sf/openrocket/startup/ApplicationModule.java index 6b775a94a..20590d57c 100644 --- a/core/src/net/sf/openrocket/startup/ApplicationModule.java +++ b/core/src/net/sf/openrocket/startup/ApplicationModule.java @@ -1,20 +1,23 @@ package net.sf.openrocket.startup; -import com.google.inject.AbstractModule; - +import net.sf.openrocket.gui.watcher.WatchService; +import net.sf.openrocket.gui.watcher.WatchServiceImpl; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.LogHelper; import net.sf.openrocket.util.watcher.DirectoryChangeReactor; import net.sf.openrocket.util.watcher.DirectoryChangeReactorImpl; -public class ApplicationModule extends AbstractModule { +import com.google.inject.AbstractModule; +public class ApplicationModule extends AbstractModule { + @Override protected void configure() { bind(LogHelper.class).toInstance(Application.getLogger()); bind(Preferences.class).toInstance(Application.getPreferences()); bind(Translator.class).toInstance(Application.getTranslator()); - bind(DirectoryChangeReactor.class).to(DirectoryChangeReactorImpl.class); + bind(DirectoryChangeReactor.class).to(DirectoryChangeReactorImpl.class); + bind(WatchService.class).to(WatchServiceImpl.class); } - + }