From 6f0cf6826a59566f060612f17468fa06460a32dd Mon Sep 17 00:00:00 2001 From: kruland2607 Date: Fri, 18 Jan 2013 09:34:53 -0600 Subject: [PATCH] Added edit decal with file change notifications. Added super simple WatchService which integrates better with our code by activly making the callbacks. --- .../sf/openrocket/document/Attachment.java | 4 +- .../sf/openrocket/document/DecalRegistry.java | 143 ++++--- .../document/OpenRocketDocument.java | 29 ++ .../document/attachments/BaseAttachment.java | 7 +- .../sf/openrocket/file/AttachmentUtils.java | 25 ++ .../sf/openrocket/gui/ExportDecalDialog.java | 5 +- .../gui/configdialog/AppearancePanel.java | 4 +- .../gui/dialogs/EditDecalDialog.java | 175 ++++---- .../gui/figure3d/FigureRenderer.java | 38 +- .../gui/figure3d/RealisticRenderer.java | 7 + .../gui/figure3d/RocketFigure3d.java | 10 + .../gui/figure3d/RocketRenderer.java | 2 + .../gui/scalefigure/RocketPanel.java | 380 +++++++++--------- .../openrocket/gui/util/EditDecalHelper.java | 136 ++++--- .../openrocket/gui/watcher/FileWatcher.java | 37 ++ .../sf/openrocket/gui/watcher/WatchEvent.java | 8 + .../sf/openrocket/gui/watcher/WatchKey.java | 7 + .../openrocket/gui/watcher/WatchService.java | 7 + .../gui/watcher/WatchServiceImpl.java | 72 ++++ .../sf/openrocket/gui/watcher/Watchable.java | 9 + .../rocketcomponent/ComponentChangeEvent.java | 7 +- .../rocketcomponent/RocketComponent.java | 14 + .../sf/openrocket/startup/Application.java | 5 +- .../openrocket/startup/ApplicationModule.java | 13 +- 24 files changed, 746 insertions(+), 398 deletions(-) create mode 100644 core/src/net/sf/openrocket/file/AttachmentUtils.java create mode 100644 core/src/net/sf/openrocket/gui/watcher/FileWatcher.java create mode 100644 core/src/net/sf/openrocket/gui/watcher/WatchEvent.java create mode 100644 core/src/net/sf/openrocket/gui/watcher/WatchKey.java create mode 100644 core/src/net/sf/openrocket/gui/watcher/WatchService.java create mode 100644 core/src/net/sf/openrocket/gui/watcher/WatchServiceImpl.java create mode 100644 core/src/net/sf/openrocket/gui/watcher/Watchable.java 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 2399a883d..493b9710d 100644 --- a/core/src/net/sf/openrocket/gui/configdialog/AppearancePanel.java +++ b/core/src/net/sf/openrocket/gui/configdialog/AppearancePanel.java @@ -228,7 +228,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() { @@ -236,7 +236,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 f6c230099..6c9c47605 100644 --- a/core/src/net/sf/openrocket/gui/figure3d/FigureRenderer.java +++ b/core/src/net/sf/openrocket/gui/figure3d/FigureRenderer.java @@ -29,9 +29,9 @@ public class FigureRenderer extends RocketRenderer { GL2 gl = drawable.getGL().getGL2(); - gl.glLightModelfv(GL2ES1.GL_LIGHT_MODEL_AMBIENT, - new float[] { 0,0,0 }, 0); - + gl.glLightModelfv(GL2ES1.GL_LIGHT_MODEL_AMBIENT, + new float[] { 0, 0, 0 }, 0); + float amb = 0.3f; float dif = 1.0f - amb; float spc = 1.0f; @@ -41,21 +41,21 @@ public class FigureRenderer extends RocketRenderer { new float[] { dif, dif, dif, 1 }, 0); gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_SPECULAR, new float[] { spc, spc, spc, 1 }, 0); - + gl.glEnable(GLLightingFunc.GL_LIGHT1); gl.glEnable(GLLightingFunc.GL_LIGHTING); gl.glShadeModel(GLLightingFunc.GL_SMOOTH); - + gl.glEnable(GLLightingFunc.GL_NORMALIZE); } - - - + + + @Override public boolean isDrawn(RocketComponent c) { return true; } - + @Override public boolean isDrawnTransparent(RocketComponent c) { if (c instanceof BodyTube) @@ -74,7 +74,7 @@ public class FigureRenderer extends RocketRenderer { } private static final HashMap, Color> defaultColorCache = new HashMap, Color>(); - + @Override public void renderComponent(GL2 gl, RocketComponent c, float alpha) { @@ -88,13 +88,13 @@ public class FigureRenderer extends RocketRenderer { defaultColorCache.put(c.getClass(), figureColor); } } - + // Set up the front A&D color convertColor(figureColor, color); color[3] = alpha; gl.glMaterialfv(GL.GL_FRONT, GLLightingFunc.GL_DIFFUSE, color, 0); gl.glMaterialfv(GL.GL_FRONT, GLLightingFunc.GL_AMBIENT, color, 0); - + // Set up the Specular color & Shine convertColor(figureColor, color); float d = 0.9f; @@ -102,13 +102,13 @@ public class FigureRenderer extends RocketRenderer { color[0] = Math.max(color[0], d) * m; color[1] = Math.max(color[1], d) * m; color[2] = Math.max(color[2], d) * m; - + gl.glMaterialfv(GL.GL_FRONT, GLLightingFunc.GL_SPECULAR, color, 0); gl.glMateriali(GL.GL_FRONT, GLLightingFunc.GL_SHININESS, getShine(c)); color[0] = color[1] = color[2] = 0; gl.glMaterialfv(GL.GL_BACK, GLLightingFunc.GL_SPECULAR, color, 0); - + //Back A&D convertColor(figureColor, color); color[0] = color[0] * 0.4f; @@ -117,10 +117,14 @@ public class FigureRenderer extends RocketRenderer { color[3] = alpha; gl.glMaterialfv(GL.GL_BACK, GLLightingFunc.GL_DIFFUSE, color, 0); gl.glMaterialfv(GL.GL_BACK, GLLightingFunc.GL_AMBIENT, color, 0); - + cr.renderGeometry(gl, c); } + @Override + public void flushTextureCache(GLAutoDrawable drawable) { + } + private static int getShine(RocketComponent c) { if (c instanceof ExternalComponent) { switch (((ExternalComponent) c).getFinish()) { @@ -140,9 +144,9 @@ public class FigureRenderer extends RocketRenderer { } return 20; } - + protected static void convertColor(Color color, float[] out) { - if ( color == null ){ + if (color == null) { out[0] = 1; out[1] = 1; out[2] = 0; diff --git a/core/src/net/sf/openrocket/gui/figure3d/RealisticRenderer.java b/core/src/net/sf/openrocket/gui/figure3d/RealisticRenderer.java index 5bb27a385..0e3bbb102 100644 --- a/core/src/net/sf/openrocket/gui/figure3d/RealisticRenderer.java +++ b/core/src/net/sf/openrocket/gui/figure3d/RealisticRenderer.java @@ -169,6 +169,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 e5f13f322..6c0d6ffbf 100644 --- a/core/src/net/sf/openrocket/gui/figure3d/RocketFigure3d.java +++ b/core/src/net/sf/openrocket/gui/figure3d/RocketFigure3d.java @@ -106,6 +106,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 3da885fed..c5707ed70 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 c71237a1d..3a590c5f3 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; @@ -87,71 +88,71 @@ import net.sf.openrocket.util.StateChangeListener; */ public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource { private static final long serialVersionUID = 1L; - + private static final Translator trans = Application.getTranslator(); - + /*RocketPanel.FigTypeAct.Sideview = Side view RocketPanel.FigTypeAct.Backview = Back view RocketPanel.FigViewAct.3DFigure = 3D Figure RocketPanel.FigViewAct.3DRealistic = 3D Realistic*/ - + private static enum VIEW_TYPE { Sideview, Backview, Figure3D, Realistic3D; @Override - public String toString(){ + public String toString() { return trans.get("RocketPanel.FigTypeAct." + super.toString()); } } - + private boolean is3d; private final RocketFigure figure; private final RocketFigure3d figure3d; - - + + private final ScaleScrollPane scrollPane; - + private final JPanel figureHolder; - + private JLabel infoMessage; - + private TreeSelectionModel selectionModel = null; - + private BasicSlider rotationSlider; private ScaleSelector scaleSelector; - - + + /* Calculation of CP and CG */ private AerodynamicCalculator aerodynamicCalculator; private MassCalculator massCalculator; - - + + private final OpenRocketDocument document; private final Configuration configuration; - + private Caret extraCP = null; private Caret extraCG = null; private RocketInfo extraText = null; - - + + private double cpAOA = Double.NaN; private double cpTheta = Double.NaN; private double cpMach = Double.NaN; private double cpRoll = Double.NaN; - + // The functional ID of the rocket that was simulated private int flightDataFunctionalID = -1; private String flightDataMotorID = null; - - + + private SimulationWorker backgroundSimulationWorker = null; - + private List listeners = new ArrayList(); - - + + /** * The executor service used for running the background simulations. * This uses a fixed-sized thread pool for all background simulations @@ -161,37 +162,37 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change static { backgroundSimulationExecutor = Executors.newFixedThreadPool(SwingPreferences.getMaxThreadCount(), new ThreadFactory() { - private ThreadFactory factory = Executors.defaultThreadFactory(); - - @Override - public Thread newThread(Runnable r) { - Thread t = factory.newThread(r); - t.setDaemon(true); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); + private ThreadFactory factory = Executors.defaultThreadFactory(); + + @Override + public Thread newThread(Runnable r) { + Thread t = factory.newThread(r); + t.setDaemon(true); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); } - - + + public RocketPanel(OpenRocketDocument document) { - + this.document = document; configuration = document.getDefaultConfiguration(); - + // TODO: FUTURE: calculator selection aerodynamicCalculator = new BarrowmanCalculator(); massCalculator = new BasicMassCalculator(); - + // Create figure and custom scroll pane figure = new RocketFigure(configuration); figure3d = new RocketFigure3d(document, configuration); - + figureHolder = new JPanel(new BorderLayout()); - + scrollPane = new ScaleScrollPane(figure) { private static final long serialVersionUID = 1L; - + @Override public void mouseClicked(MouseEvent event) { handleMouseClick(event); @@ -199,21 +200,28 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change }; scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE); scrollPane.setFitting(true); - + createPanel(); - + is3d = true; go2D(); - + configuration.addChangeListener(new StateChangeListener() { @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(); } }); - + figure3d.addComponentSelectionListener(new RocketFigure3d.ComponentSelectionListener() { @Override public void componentClicked(RocketComponent clicked[], MouseEvent event) { @@ -221,14 +229,14 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change } }); } - + private void updateFigures() { if (!is3d) figure.updateFigure(); else figure3d.updateFigure(); } - + private void go3D() { if (is3d) return; @@ -237,13 +245,13 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change figureHolder.add(figure3d, BorderLayout.CENTER); rotationSlider.setEnabled(false); scaleSelector.setEnabled(false); - + revalidate(); figureHolder.revalidate(); - + figure3d.repaint(); } - + private void go2D() { if (!is3d) return; @@ -256,16 +264,16 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change figureHolder.revalidate(); figure.repaint(); } - + /** * Creates the layout and components of the panel. */ private void createPanel() { setLayout(new MigLayout("", "[shrink][grow]", "[shrink][shrink][grow][shrink]")); - + setPreferredSize(new Dimension(800, 300)); - - + + // View Type Dropdown ComboBoxModel cm = new DefaultComboBoxModel(VIEW_TYPE.values()) { @Override @@ -295,84 +303,84 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change add(new JLabel("View Type:"), "spanx, split"); add(new JComboBox(cm)); - + // Zoom level selector scaleSelector = new ScaleSelector(scrollPane); add(scaleSelector); - - - + + + // Stage selector StageSelector stageSelector = new StageSelector(configuration); add(stageSelector); - - - + + + // Flight configuration selector //// Flight configuration: JLabel label = new JLabel(trans.get("RocketPanel.lbl.Flightcfg")); label.setHorizontalAlignment(JLabel.RIGHT); add(label, "growx, right"); add(new JComboBox(new FlightConfigurationModel(configuration)), ""); - + //// Edit button JButton button = new JButton(trans.get("RocketPanel.but.FlightcfgEdit")); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - JDialog configDialog = new FlightConfigurationDialog(document.getRocket(),SwingUtilities.windowForComponent(RocketPanel.this)); + JDialog configDialog = new FlightConfigurationDialog(document.getRocket(), SwingUtilities.windowForComponent(RocketPanel.this)); configDialog.show(); } }); add(button, "wrap"); - - - - + + + + // Create slider and scroll pane - + DoubleModel theta = new DoubleModel(figure, "Rotation", UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI); UnitSelector us = new UnitSelector(theta, true); us.setHorizontalAlignment(JLabel.CENTER); add(us, "alignx 50%, growx"); - + // Add the rocket figure add(figureHolder, "grow, spany 2, wmin 300lp, hmin 100lp, wrap"); - - + + // Add rotation slider // Minimum size to fit "360deg" JLabel l = new JLabel("360" + Chars.DEGREE); Dimension d = l.getPreferredSize(); - + add(rotationSlider = new BasicSlider(theta.getSliderModel(0, 2 * Math.PI), JSlider.VERTICAL, true), "ax 50%, wrap, width " + (d.width + 6) + "px:null:null, growy"); - - + + //// Click to select    Shift+click to select other    Double-click to edit    Click+drag to move infoMessage = new JLabel(trans.get("RocketPanel.lbl.infoMessage")); infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9)); add(infoMessage, "skip, span, gapleft 25, wrap"); - - + + addExtras(); } - - - + + + public RocketFigure getFigure() { return figure; } - + public AerodynamicCalculator getAerodynamicCalculator() { return aerodynamicCalculator; } - + public Configuration getConfiguration() { return configuration; } - + /** * Get the center of pressure figure element. * @@ -381,7 +389,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change public Caret getExtraCP() { return extraCP; } - + /** * Get the center of gravity figure element. * @@ -390,7 +398,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change public Caret getExtraCG() { return extraCG; } - + /** * Get the extra text figure element. * @@ -399,7 +407,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change public RocketInfo getExtraText() { return extraText; } - + public void setSelectionModel(TreeSelectionModel m) { if (selectionModel != null) { selectionModel.removeTreeSelectionListener(this); @@ -408,9 +416,9 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change selectionModel.addTreeSelectionListener(this); valueChanged((TreeSelectionEvent) null); // updates FigureParameters } - - - + + + /** * Return the angle of attack used in CP calculation. NaN signifies the default value * of zero. @@ -419,7 +427,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change public double getCPAOA() { return cpAOA; } - + /** * Set the angle of attack to be used in CP calculation. A value of NaN signifies that * the default AOA (zero) should be used. @@ -434,11 +442,11 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change updateFigures(); fireChangeEvent(); } - + public double getCPTheta() { return cpTheta; } - + public void setCPTheta(double theta) { if (MathUtil.equals(theta, cpTheta) || (Double.isNaN(theta) && Double.isNaN(cpTheta))) @@ -450,11 +458,11 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change updateFigures(); fireChangeEvent(); } - + public double getCPMach() { return cpMach; } - + public void setCPMach(double mach) { if (MathUtil.equals(mach, cpMach) || (Double.isNaN(mach) && Double.isNaN(cpMach))) @@ -464,11 +472,11 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change updateFigures(); fireChangeEvent(); } - + public double getCPRoll() { return cpRoll; } - + public void setCPRoll(double roll) { if (MathUtil.equals(roll, cpRoll) || (Double.isNaN(roll) && Double.isNaN(cpRoll))) @@ -478,31 +486,31 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change updateFigures(); fireChangeEvent(); } - - - + + + @Override public void addChangeListener(EventListener listener) { listeners.add(0, listener); } - + @Override public void removeChangeListener(EventListener listener) { listeners.remove(listener); } - + protected void fireChangeEvent() { EventObject e = new EventObject(this); for (EventListener l : listeners) { - if ( l instanceof StateChangeListener ) { - ((StateChangeListener)l).stateChanged(e); + if (l instanceof StateChangeListener) { + ((StateChangeListener) l).stateChanged(e); } } } - - - - + + + + /** * Handle clicking on figure shapes. The functioning is the following: * @@ -513,7 +521,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change * the next component. Otherwise select the first component in the list. */ public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK; - + private void handleMouseClick(MouseEvent event) { if (event.getButton() != MouseEvent.BUTTON1) return; @@ -521,20 +529,20 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change Point p1 = scrollPane.getViewport().getViewPosition(); int x = p0.x + p1.x; int y = p0.y + p1.y; - + RocketComponent[] clicked = figure.getComponentsByPoint(x, y); - + handleComponentClick(clicked, event); } - - private void handleComponentClick(RocketComponent[] clicked, MouseEvent event){ - + + private void handleComponentClick(RocketComponent[] clicked, MouseEvent event) { + // If no component is clicked, do nothing - if (clicked.length == 0){ + if (clicked.length == 0) { selectionModel.setSelectionPath(null); return; } - + // Check whether the currently selected component is in the clicked components. TreePath path = selectionModel.getSelectionPath(); if (path != null) { @@ -551,7 +559,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change } } } - + // Currently selected component not clicked if (path == null) { if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) { @@ -560,34 +568,34 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change path = ComponentTreeModel.makeTreePath(clicked[0]); } } - + // Set selection and check for double-click selectionModel.setSelectionPath(path); if (event.getClickCount() == 2) { RocketComponent component = (RocketComponent) path.getLastPathComponent(); - + ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this), document, component); } } - - - - + + + + /** * Updates the extra data included in the figure. Currently this includes * the CP and CG carets. */ private WarningSet warnings = new WarningSet(); - + private void updateExtras() { Coordinate cp, cg; double cpx, cgx; - + // TODO: MEDIUM: User-definable conditions FlightConditions conditions = new FlightConditions(configuration); warnings.clear(); - + if (!Double.isNaN(cpMach)) { conditions.setMach(cpMach); extraText.setMach(cpMach); @@ -595,20 +603,20 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change conditions.setMach(Application.getPreferences().getDefaultMach()); extraText.setMach(Application.getPreferences().getDefaultMach()); } - + if (!Double.isNaN(cpAOA)) { conditions.setAOA(cpAOA); } else { conditions.setAOA(0); } extraText.setAOA(cpAOA); - + if (!Double.isNaN(cpRoll)) { conditions.setRollRate(cpRoll); } else { conditions.setRollRate(0); } - + if (!Double.isNaN(cpTheta)) { conditions.setTheta(cpTheta); cp = aerodynamicCalculator.getCP(configuration, conditions, warnings); @@ -616,24 +624,24 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change cp = aerodynamicCalculator.getWorstCP(configuration, conditions, warnings); } extraText.setTheta(cpTheta); - - + + cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS); // System.out.println("CG computed as "+cg+ " CP as "+cp); - + if (cp.weight > 0.000001) cpx = cp.x; else cpx = Double.NaN; - + if (cg.weight > 0.000001) cgx = cg.x; else cgx = Double.NaN; - + figure3d.setCG(cg); figure3d.setCP(cp); - + // Length bound is assumed to be tight double length = 0, diameter = 0; Collection bounds = configuration.getBounds(); @@ -647,7 +655,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change } length = maxX - minX; } - + for (RocketComponent c : configuration) { if (c instanceof SymmetricComponent) { double d1 = ((SymmetricComponent) c).getForeRadius() * 2; @@ -655,31 +663,31 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change diameter = MathUtil.max(diameter, d1, d2); } } - + extraText.setCG(cgx); extraText.setCP(cpx); extraText.setLength(length); extraText.setDiameter(diameter); extraText.setMass(cg.weight); extraText.setWarnings(warnings); - - + + if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) { - + // TODO: LOW: Y-coordinate and rotation extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0); extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0); - + } else { - + extraCP.setPosition(Double.NaN, Double.NaN); extraCG.setPosition(Double.NaN, Double.NaN); - + } - - + + //////// Flight simulation in background - + // Check whether to compute or not if (!((SwingPreferences) Application.getPreferences()).computeFlightInBackground()) { extraText.setFlightData(null); @@ -687,38 +695,38 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change stopBackgroundSimulation(); return; } - + // Check whether data is already up to date if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() && flightDataMotorID == configuration.getFlightConfigurationID()) { return; } - + flightDataFunctionalID = configuration.getRocket().getFunctionalModID(); flightDataMotorID = configuration.getFlightConfigurationID(); - + // Stop previous computation (if any) stopBackgroundSimulation(); - + // Check that configuration has motors if (!configuration.hasMotors()) { extraText.setFlightData(FlightData.NaN_DATA); extraText.setCalculatingData(false); return; } - + // Start calculation process extraText.setCalculatingData(true); - + Rocket duplicate = (Rocket) configuration.getRocket().copy(); - Simulation simulation = ((SwingPreferences)Application.getPreferences()).getBackgroundSimulation(duplicate); + Simulation simulation = ((SwingPreferences) Application.getPreferences()).getBackgroundSimulation(duplicate); simulation.getOptions().setMotorConfigurationID( configuration.getFlightConfigurationID()); - + backgroundSimulationWorker = new BackgroundSimulationWorker(document, simulation); backgroundSimulationExecutor.execute(backgroundSimulationWorker); } - + /** * Cancels the current background simulation worker, if any. */ @@ -728,26 +736,26 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change backgroundSimulationWorker = null; } } - - + + /** * A SimulationWorker that simulates the rocket flight in the background and * sets the results to the extra text when finished. The worker can be cancelled * if necessary. */ private class BackgroundSimulationWorker extends SimulationWorker { - + private final CustomExpressionSimulationListener exprListener; - + public BackgroundSimulationWorker(OpenRocketDocument doc, Simulation sim) { super(sim); List exprs = doc.getCustomExpressions(); exprListener = new CustomExpressionSimulationListener(exprs); } - + @Override protected FlightData doInBackground() { - + // Pause a little while to allow faster UI reaction try { Thread.sleep(300); @@ -755,38 +763,38 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change } if (isCancelled() || backgroundSimulationWorker != this) return null; - + return super.doInBackground(); } - + @Override protected void simulationDone() { // Do nothing if cancelled if (isCancelled() || backgroundSimulationWorker != this) return; - + backgroundSimulationWorker = null; extraText.setFlightData(simulation.getSimulatedData()); extraText.setCalculatingData(false); figure.repaint(); figure3d.repaint(); } - + @Override protected SimulationListener[] getExtraListeners() { return new SimulationListener[] { InterruptListener.INSTANCE, ApogeeEndListener.INSTANCE, - exprListener}; - + exprListener }; + } - + @Override protected void simulationInterrupted(Throwable t) { // Do nothing on cancel, set N/A data otherwise if (isCancelled() || backgroundSimulationWorker != this) // Double-check return; - + backgroundSimulationWorker = null; extraText.setFlightData(FlightData.NaN_DATA); extraText.setCalculatingData(false); @@ -794,9 +802,9 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change figure3d.repaint(); } } - - - + + + /** * Adds the extra data to the figure. Currently this includes the CP and CG carets. */ @@ -805,21 +813,21 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change extraCP = new CPCaret(0, 0); extraText = new RocketInfo(configuration); updateExtras(); - + figure.clearRelativeExtra(); figure.addRelativeExtra(extraCP); figure.addRelativeExtra(extraCG); figure.addAbsoluteExtra(extraText); - - + + figure3d.clearRelativeExtra(); //figure3d.addRelativeExtra(extraCP); //figure3d.addRelativeExtra(extraCG); figure3d.addAbsoluteExtra(extraText); - + } - - + + /** * Updates the selection in the FigureParameters and repaints the figure. * Ignores the event itself. @@ -832,17 +840,17 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change figure3d.setSelection(null); return; } - + RocketComponent[] components = new RocketComponent[paths.length]; for (int i = 0; i < paths.length; i++) components[i] = (RocketComponent) paths[i].getLastPathComponent(); figure.setSelection(components); - + figure3d.setSelection(components); } - - - + + + /** * An Action that shows whether the figure type is the type * given in the constructor. @@ -852,13 +860,13 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change private class FigureTypeAction extends AbstractAction implements StateChangeListener { private static final long serialVersionUID = 1L; private final int type; - + public FigureTypeAction(int type) { this.type = type; stateChanged(null); figure.addChangeListener(this); } - + @Override public void actionPerformed(ActionEvent e) { boolean state = (Boolean) getValue(Action.SELECTED_KEY); @@ -870,11 +878,11 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change } stateChanged(null); } - + @Override public void stateChanged(EventObject e) { putValue(Action.SELECTED_KEY, figure.getType() == type && !is3d); } } - + } 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); } - + }