guided tours implementation
authorplaa <plaa@180e2498-e6e9-4542-8430-84ac67f01cd8>
Mon, 19 Dec 2011 05:00:30 +0000 (05:00 +0000)
committerplaa <plaa@180e2498-e6e9-4542-8430-84ac67f01cd8>
Mon, 19 Dec 2011 05:00:30 +0000 (05:00 +0000)
git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@235 180e2498-e6e9-4542-8430-84ac67f01cd8

21 files changed:
datafiles/tours/designed_rocket.png [new file with mode: 0644]
datafiles/tours/left_design.png [new file with mode: 0644]
datafiles/tours/main_window.png [new file with mode: 0644]
datafiles/tours/rocket_configuration.png [new file with mode: 0644]
datafiles/tours/style.css [new file with mode: 0644]
datafiles/tours/test.tour [new file with mode: 0644]
datafiles/tours/test2.tour [new file with mode: 0644]
datafiles/tours/test_de.tour [new file with mode: 0644]
datafiles/tours/tours.txt [new file with mode: 0644]
l10n/messages.properties
src/net/sf/openrocket/gui/components/ImageDisplayComponent.java [new file with mode: 0644]
src/net/sf/openrocket/gui/help/tours/GuidedTourSelectionDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/help/tours/Slide.java [new file with mode: 0644]
src/net/sf/openrocket/gui/help/tours/SlideSet.java [new file with mode: 0644]
src/net/sf/openrocket/gui/help/tours/SlideSetLoader.java [new file with mode: 0644]
src/net/sf/openrocket/gui/help/tours/SlideSetManager.java [new file with mode: 0644]
src/net/sf/openrocket/gui/help/tours/SlideShowComponent.java [new file with mode: 0644]
src/net/sf/openrocket/gui/help/tours/SlideShowDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/help/tours/TextLineReader.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/BasicFrame.java
src/net/sf/openrocket/util/Chars.java

diff --git a/datafiles/tours/designed_rocket.png b/datafiles/tours/designed_rocket.png
new file mode 100644 (file)
index 0000000..0accfa2
Binary files /dev/null and b/datafiles/tours/designed_rocket.png differ
diff --git a/datafiles/tours/left_design.png b/datafiles/tours/left_design.png
new file mode 100644 (file)
index 0000000..4c496d5
Binary files /dev/null and b/datafiles/tours/left_design.png differ
diff --git a/datafiles/tours/main_window.png b/datafiles/tours/main_window.png
new file mode 100644 (file)
index 0000000..d6f7f70
Binary files /dev/null and b/datafiles/tours/main_window.png differ
diff --git a/datafiles/tours/rocket_configuration.png b/datafiles/tours/rocket_configuration.png
new file mode 100644 (file)
index 0000000..e2ddd7e
Binary files /dev/null and b/datafiles/tours/rocket_configuration.png differ
diff --git a/datafiles/tours/style.css b/datafiles/tours/style.css
new file mode 100644 (file)
index 0000000..63c3c04
--- /dev/null
@@ -0,0 +1,15 @@
+
+/*
+ * This stylesheet is used to style all guided tours.
+ * Use only simple styling.
+ *
+ * NOTE:  The Sun JRE does not support "em" widths, use only pixels.
+ */
+
+div.base {
+  font-family: Arial, sans-serif;
+}
+
+p {
+  margin: 0 0 5px;
+}
diff --git a/datafiles/tours/test.tour b/datafiles/tours/test.tour
new file mode 100644 (file)
index 0000000..e5c2004
--- /dev/null
@@ -0,0 +1,18 @@
+
+# Foo bar
+
+A test tour
+
+This is the <i>description</i>.
+
+[left_design.png]
+
+This is the left_design file.
+
+It's nifty. It's also <a href="foobar">economical</a>.
+
+
+
+[main_window.png]
+This is the next one.
+
diff --git a/datafiles/tours/test2.tour b/datafiles/tours/test2.tour
new file mode 100644 (file)
index 0000000..aff6262
--- /dev/null
@@ -0,0 +1,16 @@
+Another test tour
+
+<p>This is the second tour.
+<p>You get it?
+
+[left_design.png]
+
+This is the second tour.
+
+It's nifty. It's also <a href="foobar">economical</a>.
+
+
+
+[main_window.png]
+This is the next one.
+
diff --git a/datafiles/tours/test_de.tour b/datafiles/tours/test_de.tour
new file mode 100644 (file)
index 0000000..615dd84
--- /dev/null
@@ -0,0 +1,6 @@
+
+Das test Tour
+
+[left_design.png]
+
+Das is ein test tour.
diff --git a/datafiles/tours/tours.txt b/datafiles/tours/tours.txt
new file mode 100644 (file)
index 0000000..79242c0
--- /dev/null
@@ -0,0 +1,6 @@
+
+# This file lists all the available tours.
+
+test.tour
+test2.tour
+
index cbda872235faf675763328825eebc10a95f35fe1..3a51de44702e779f612f91ebda6affee4cddd9ee 100644 (file)
@@ -999,6 +999,8 @@ main.menu.analyze.optimization.desc = General rocket design optimization
 
 main.menu.help = Help
 main.menu.help.desc = Information about OpenRocket
+main.menu.help.tours = Guided tours
+main.menu.help.tours.desc = Take guided tours on OpenRocket
 main.menu.help.license = License
 main.menu.help.license.desc = OpenRocket license information
 main.menu.help.bugReport = Bug report
@@ -1531,3 +1533,14 @@ CompassSelectionButton.lbl.SW = SW
 CompassSelectionButton.lbl.W = W
 CompassSelectionButton.lbl.NW = NW
 
+
+SlideShowDialog.btn.next = Next
+SlideShowDialog.btn.prev = Previous
+
+GuidedTourSelectionDialog.title = Guided tours
+GuidedTourSelectionDialog.lbl.selectTour = Select guided tour:
+GuidedTourSelectionDialog.lbl.description = Tour description:
+GuidedTourSelectionDialog.lbl.length = Number of slides:
+GuidedTourSelectionDialog.btn.start = Start tour!
+
+
diff --git a/src/net/sf/openrocket/gui/components/ImageDisplayComponent.java b/src/net/sf/openrocket/gui/components/ImageDisplayComponent.java
new file mode 100644 (file)
index 0000000..aaf8292
--- /dev/null
@@ -0,0 +1,124 @@
+package net.sf.openrocket.gui.components;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
+import javax.imageio.ImageIO;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.util.MathUtil;
+
+/**
+ * Draws a BufferedImage centered and scaled to fit to the component.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ImageDisplayComponent extends JPanel {
+       
+       private BufferedImage image;
+       
+       public ImageDisplayComponent() {
+               this(null);
+       }
+       
+       public ImageDisplayComponent(BufferedImage image) {
+               this.image = image;
+       }
+       
+       
+       @Override
+       protected void paintComponent(Graphics g) {
+               super.paintComponent(g);
+               
+               if (image == null) {
+                       return;
+               }
+               
+               final int width = Math.max(this.getWidth(), 1);
+               final int height = Math.max(this.getHeight(), 1);
+               
+               final int origWidth = Math.max(image.getWidth(), 1);
+               final int origHeight = Math.max(image.getHeight(), 1);
+               
+
+               // Determine scaling factor
+               double scaleX = ((double) width) / origWidth;
+               double scaleY = ((double) height) / origHeight;
+               
+               double scale = MathUtil.min(scaleX, scaleY);
+               
+               if (scale >= 1) {
+                       scale = 1.0;
+               }
+               
+
+               // Center in the middle of the component
+               int finalWidth = (int) Math.round(origWidth * scale);
+               int finalHeight = (int) Math.round(origHeight * scale);
+               
+               int posX = (width - finalWidth) / 2;
+               int posY = (height - finalHeight) / 2;
+               
+
+               // Draw the image
+               int dx1 = posX;
+               int dy1 = posY;
+               int dx2 = posX + finalWidth;
+               int dy2 = posY + finalHeight;
+               int sx1 = 0;
+               int sy1 = 0;
+               int sx2 = origWidth;
+               int sy2 = origHeight;
+               
+               Graphics2D g2 = (Graphics2D) g;
+               g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
+               g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+               g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+               g2.drawImage(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null);
+               
+       }
+       
+       
+       public BufferedImage getImage() {
+               return image;
+       }
+       
+       
+       public void setImage(BufferedImage image) {
+               this.image = image;
+               this.repaint();
+       }
+       
+       
+       public static void main(String[] args) throws Exception {
+               final BufferedImage image = ImageIO.read(new File("test.png"));
+               
+               SwingUtilities.invokeAndWait(new Runnable() {
+                       @Override
+                       public void run() {
+                               
+                               JFrame frame = new JFrame();
+                               
+                               JPanel panel = new JPanel(new MigLayout("fill"));
+                               panel.setBackground(Color.red);
+                               frame.add(panel);
+                               
+                               ImageDisplayComponent c = new ImageDisplayComponent(image);
+                               panel.add(c, "grow");
+                               
+                               frame.setSize(500, 500);
+                               frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+                               frame.setVisible(true);
+                               
+                       }
+               });
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/help/tours/GuidedTourSelectionDialog.java b/src/net/sf/openrocket/gui/help/tours/GuidedTourSelectionDialog.java
new file mode 100644 (file)
index 0000000..c2ede5e
--- /dev/null
@@ -0,0 +1,197 @@
+package net.sf.openrocket.gui.help.tours;
+
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.List;
+
+import javax.swing.AbstractListModel;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JEditorPane;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ListSelectionModel;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.text.html.HTMLDocument;
+import javax.swing.text.html.StyleSheet;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.components.StyledLabel;
+import net.sf.openrocket.gui.components.StyledLabel.Style;
+import net.sf.openrocket.gui.util.GUIUtil;
+import net.sf.openrocket.l10n.Translator;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.BugException;
+import net.sf.openrocket.util.Named;
+
+public class GuidedTourSelectionDialog extends JDialog {
+       
+       private static final Translator trans = Application.getTranslator();
+       
+       private static final String TOURS_BASE_DIR = "datafiles/tours";
+       
+
+       private final SlideSetManager slideSetManager;
+       private final List<String> tourNames;
+       
+       private SlideShowDialog slideShowDialog;
+       
+       private JList tourList;
+       private JEditorPane tourDescription;
+       private JLabel tourLength;
+       
+       
+       public GuidedTourSelectionDialog(Window parent) {
+               super(parent, trans.get("title"), ModalityType.MODELESS);
+               
+               try {
+                       
+                       slideSetManager = new SlideSetManager(TOURS_BASE_DIR);
+                       slideSetManager.load();
+                       
+                       tourNames = slideSetManager.getSlideSetNames();
+                       if (tourNames.isEmpty()) {
+                               throw new FileNotFoundException("No tours found.");
+                       }
+                       
+               } catch (IOException e) {
+                       throw new BugException(e);
+               }
+               
+
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               
+               panel.add(new StyledLabel(trans.get("lbl.selectTour"), Style.BOLD), "spanx, wrap rel");
+               
+               tourList = new JList(new TourListModel());
+               tourList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               tourList.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+                       @Override
+                       public void valueChanged(ListSelectionEvent e) {
+                               updateText();
+                       }
+               });
+               tourList.addMouseListener(new MouseAdapter() {
+                       @Override
+                       public void mouseClicked(MouseEvent e) {
+                               if (e.getClickCount() == 2) {
+                                       startTour();
+                               }
+                       }
+               });
+               panel.add(new JScrollPane(tourList), "grow, gapright unrel, w 200lp, h 150lp");
+               
+
+
+               //  Sub-panel containing description and start button
+               JPanel sub = new JPanel(new MigLayout("fill, ins 0"));
+               sub.add(new StyledLabel(trans.get("lbl.description"), -1), "wrap rel");
+               
+               tourDescription = new JEditorPane("text/html", "");
+               tourDescription.setEditable(false);
+               StyleSheet ss = slideSetManager.getSlideSet(tourNames.get(0)).getStyleSheet();
+               ((HTMLDocument) tourDescription.getDocument()).getStyleSheet().addStyleSheet(ss);
+               sub.add(new JScrollPane(tourDescription), "grow, wrap rel");
+               
+               tourLength = new StyledLabel(-1);
+               sub.add(tourLength, "wrap unrel");
+               
+               JButton start = new JButton(trans.get("btn.start"));
+               start.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               startTour();
+                       }
+               });
+               sub.add(start, "growx");
+               
+               panel.add(sub, "grow, wrap para, w 200lp, h 150lp");
+               
+
+
+               JButton close = new JButton(trans.get("button.close"));
+               close.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               GuidedTourSelectionDialog.this.dispose();
+                       }
+               });
+               panel.add(close, "spanx, right");
+               
+               this.add(panel);
+               GUIUtil.setDisposableDialogOptions(this, close);
+               tourList.setSelectedIndex(0);
+       }
+       
+       
+       private void startTour() {
+               SlideSet ss = getSelectedSlideSet();
+               if (ss == null) {
+                       return;
+               }
+               
+               if (slideShowDialog != null && !slideShowDialog.isVisible()) {
+                       closeTour();
+               }
+               
+               if (slideShowDialog == null) {
+                       slideShowDialog = new SlideShowDialog(this);
+               }
+               
+               slideShowDialog.setSlideSet(ss, 0);
+               slideShowDialog.setVisible(true);
+       }
+       
+       
+       private void closeTour() {
+               if (slideShowDialog != null) {
+                       slideShowDialog.dispose();
+                       slideShowDialog = null;
+               }
+       }
+       
+       
+       private void updateText() {
+               SlideSet ss = getSelectedSlideSet();
+               if (ss != null) {
+                       tourDescription.setText(ss.getDescription());
+                       tourLength.setText(trans.get("lbl.length") + " " + ss.getSlideCount());
+               } else {
+                       tourDescription.setText("");
+                       tourLength.setText(trans.get("lbl.length"));
+               }
+       }
+       
+       
+       @SuppressWarnings("unchecked")
+       private SlideSet getSelectedSlideSet() {
+               return ((Named<SlideSet>) tourList.getSelectedValue()).get();
+       }
+       
+       private class TourListModel extends AbstractListModel {
+               
+               @Override
+               public Object getElementAt(int index) {
+                       String name = tourNames.get(index);
+                       SlideSet set = slideSetManager.getSlideSet(name);
+                       return new Named<SlideSet>(set, set.getTitle());
+               }
+               
+               @Override
+               public int getSize() {
+                       return tourNames.size();
+               }
+               
+       }
+       
+
+
+}
diff --git a/src/net/sf/openrocket/gui/help/tours/Slide.java b/src/net/sf/openrocket/gui/help/tours/Slide.java
new file mode 100644 (file)
index 0000000..7ac4212
--- /dev/null
@@ -0,0 +1,73 @@
+package net.sf.openrocket.gui.help.tours;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.lang.ref.SoftReference;
+import java.net.URL;
+
+import javax.imageio.ImageIO;
+
+/**
+ * An individual slide in a guided tour.  It contains a image (or reference to an
+ * image file) plus a text description (in HTML).
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class Slide {
+       
+       private final String imageFile;
+       private SoftReference<BufferedImage> imageReference = null;
+       
+       private final String text;
+       
+       
+
+       public Slide(String imageFile, String text) {
+               this.imageFile = imageFile;
+               this.text = text;
+       }
+       
+       
+
+       public BufferedImage getImage() {
+               
+               // Check the cache
+               if (imageReference != null) {
+                       BufferedImage image = imageReference.get();
+                       if (image != null) {
+                               return image;
+                       }
+               }
+               
+               // Otherwise load and cache
+               BufferedImage image = loadImage();
+               imageReference = new SoftReference<BufferedImage>(image);
+               
+               return image;
+       }
+       
+       public String getText() {
+               return text;
+       }
+       
+       
+
+       private BufferedImage loadImage() {
+               BufferedImage img;
+               
+               try {
+                       URL url = ClassLoader.getSystemResource(imageFile);
+                       if (url != null) {
+                               img = ImageIO.read(url);
+                       } else {
+                               //FIXME
+                               img = null;
+                       }
+               } catch (IOException e) {
+                       // FIXME
+                       img = null;
+               }
+               
+               return img;
+       }
+}
diff --git a/src/net/sf/openrocket/gui/help/tours/SlideSet.java b/src/net/sf/openrocket/gui/help/tours/SlideSet.java
new file mode 100644 (file)
index 0000000..459bda2
--- /dev/null
@@ -0,0 +1,62 @@
+package net.sf.openrocket.gui.help.tours;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.text.html.StyleSheet;
+
+/**
+ * A set of slides that composes a tour.
+ * 
+ * A slide set contains a (localized, plain-text) title for the tour, a (possibly
+ * multiline, HTML-formatted) description and a number of slides.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SlideSet {
+       
+       private String title = "";
+       private String description = "";
+       private final List<Slide> slides = new ArrayList<Slide>();
+       private StyleSheet styleSheet = new StyleSheet();
+       
+       
+
+       public String getTitle() {
+               return title;
+       }
+       
+       public void setTitle(String name) {
+               this.title = name;
+       }
+       
+       public String getDescription() {
+               return description;
+       }
+       
+       public void setDescription(String description) {
+               this.description = description;
+       }
+       
+       
+       public Slide getSlide(int index) {
+               return this.slides.get(index);
+       }
+       
+       public void addSlide(Slide slide) {
+               this.slides.add(slide);
+       }
+       
+       public int getSlideCount() {
+               return this.slides.size();
+       }
+       
+       public StyleSheet getStyleSheet() {
+               return styleSheet;
+       }
+       
+       public void setStyleSheet(StyleSheet styleSheet) {
+               this.styleSheet = styleSheet;
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/help/tours/SlideSetLoader.java b/src/net/sf/openrocket/gui/help/tours/SlideSetLoader.java
new file mode 100644 (file)
index 0000000..1a32cb4
--- /dev/null
@@ -0,0 +1,173 @@
+package net.sf.openrocket.gui.help.tours;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import net.sf.openrocket.util.BugException;
+
+/**
+ * Class that loads a slide set from a file.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SlideSetLoader {
+       
+       private static final Pattern NEW_SLIDE_PATTERN = Pattern.compile("^\\[(.*)\\]$");
+       
+       private final String baseDir;
+       private TextLineReader source;
+       private Locale locale;
+       
+       
+
+
+       /**
+        * Constructor.
+        * 
+        * @param baseDir       The base directory from which to load from.  It is prepended to the loaded
+        *                                      file names and image file names.
+        */
+       public SlideSetLoader(String baseDir) {
+               this(baseDir, Locale.getDefault());
+       }
+       
+       
+       /**
+        * Constructor.
+        * 
+        * @param baseDir       The base directory from which to load from.  It is prepended to the loaded
+        *                                      file names and image file names.
+        * @param locale        The locale for which the files are loaded.
+        */
+       public SlideSetLoader(String baseDir, Locale locale) {
+               if (baseDir.length() > 0 && !baseDir.endsWith("/")) {
+                       baseDir = baseDir + "/";
+               }
+               this.baseDir = baseDir;
+               this.locale = locale;
+       }
+       
+       
+       /**
+        * Load a slide set from a file.  The base directory is prepended to the
+        * file name first.
+        * 
+        * @param filename              the file to read in the base directory.
+        * @return                              the slide set
+        */
+       public SlideSet load(String filename) throws IOException {
+               String file = baseDir + filename;
+               InputStream in = getLocalizedFile(file);
+               
+               try {
+                       InputStreamReader reader = new InputStreamReader(in, "UTF-8");
+                       return load(reader);
+               } finally {
+                       in.close();
+               }
+       }
+       
+       
+       private InputStream getLocalizedFile(String filename) throws IOException {
+               for (String file : generateLocalizedFiles(filename)) {
+                       InputStream in = ClassLoader.getSystemResourceAsStream(file);
+                       if (in != null) {
+                               return in;
+                       }
+               }
+               throw new FileNotFoundException("File '" + filename + "' not found.");
+       }
+       
+       private List<String> generateLocalizedFiles(String filename) {
+               String base, ext;
+               int index = filename.lastIndexOf('.');
+               if (index >= 0) {
+                       base = filename.substring(0, index);
+                       ext = filename.substring(index);
+               } else {
+                       base = filename;
+                       ext = "";
+               }
+               
+
+               List<String> list = new ArrayList<String>();
+               list.add(base + "_" + locale.getLanguage() + "_" + locale.getCountry() + "_" + locale.getVariant() + ext);
+               list.add(base + "_" + locale.getLanguage() + "_" + locale.getCountry() + ext);
+               list.add(base + "_" + locale.getLanguage() + ext);
+               list.add(base + ext);
+               return list;
+       }
+       
+       
+       /**
+        * Load slide set from a reader.
+        * 
+        * @param reader        the reader to read from.
+        * @return                      the slide set.
+        */
+       public SlideSet load(Reader reader) throws IOException {
+               source = new TextLineReader(reader);
+               
+               // Read title and description
+               String title = source.next();
+               StringBuilder desc = new StringBuilder();
+               while (!nextLineStartsSlide()) {
+                       if (desc.length() > 0) {
+                               desc.append('\n');
+                       }
+                       desc.append(source.next());
+               }
+               
+               // Create the slide set
+               SlideSet set = new SlideSet();
+               set.setTitle(title);
+               set.setDescription(desc.toString());
+               
+
+               // Read the slides
+               while (source.hasNext()) {
+                       Slide s = readSlide();
+                       set.addSlide(s);
+               }
+               
+               return set;
+       }
+       
+       
+       private Slide readSlide() {
+               
+               String imgLine = source.next();
+               Matcher matcher = NEW_SLIDE_PATTERN.matcher(imgLine);
+               if (!matcher.matches()) {
+                       throw new BugException("Line did not match new slide pattern: " + imgLine);
+               }
+               
+               String imageFile = matcher.group(1);
+               
+               StringBuffer desc = new StringBuffer();
+               while (source.hasNext() && !nextLineStartsSlide()) {
+                       if (desc.length() > 0) {
+                               desc.append('\n');
+                       }
+                       desc.append(source.next());
+               }
+               
+               return new Slide(baseDir + imageFile, desc.toString());
+       }
+       
+       
+
+       private boolean nextLineStartsSlide() {
+               return NEW_SLIDE_PATTERN.matcher(source.peek()).matches();
+       }
+       
+
+}
diff --git a/src/net/sf/openrocket/gui/help/tours/SlideSetManager.java b/src/net/sf/openrocket/gui/help/tours/SlideSetManager.java
new file mode 100644 (file)
index 0000000..abf5e74
--- /dev/null
@@ -0,0 +1,134 @@
+package net.sf.openrocket.gui.help.tours;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.text.html.StyleSheet;
+
+/**
+ * A manager that loads a number of slide sets from a defined base directory
+ * and provides access to them.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SlideSetManager {
+       
+       private static final String TOURS_FILE = "tours.txt";
+       private static final String STYLESHEET_FILE = "style.css";
+       
+
+       private final String baseDir;
+       private final Map<String, SlideSet> slideSets = new LinkedHashMap<String, SlideSet>();
+       
+       
+       /**
+        * Sole constructor.
+        * 
+        * @param baseDir       the base directory containing the tours and style files.
+        */
+       public SlideSetManager(String baseDir) {
+               if (baseDir.length() > 0 && !baseDir.endsWith("/")) {
+                       baseDir = baseDir + "/";
+               }
+               this.baseDir = baseDir;
+       }
+       
+       
+       /**
+        * Load all the tours.
+        */
+       public void load() throws IOException {
+               slideSets.clear();
+               
+               List<String> tours = loadTourList();
+               StyleSheet styleSheet = loadStyleSheet();
+               
+               for (String file : tours) {
+                       
+                       String base = baseDir + file;
+                       int index = base.lastIndexOf('/');
+                       if (index >= 0) {
+                               base = base.substring(0, index);
+                       } else {
+                               base = "";
+                       }
+                       
+                       SlideSetLoader loader = new SlideSetLoader(base);
+                       SlideSet set = loader.load(file);
+                       set.setStyleSheet(styleSheet);
+                       slideSets.put(file, set);
+               }
+               
+       }
+       
+       
+       /**
+        * Return a set containing all the slide set names.
+        */
+       public List<String> getSlideSetNames() {
+               return new ArrayList<String>(slideSets.keySet());
+       }
+       
+       /**
+        * Retrieve an individual slide set.
+        * 
+        * @param name  the name of the slide set to retrieve.
+        * @return              the slide set (never null)
+        * @throws IllegalArgumentException             if the slide set with the name does not exist.
+        */
+       public SlideSet getSlideSet(String name) {
+               SlideSet s = slideSets.get(name);
+               if (s == null) {
+                       throw new IllegalArgumentException("Slide set with name '" + name + "' not found.");
+               }
+               return s;
+       }
+       
+       
+       private List<String> loadTourList() throws IOException {
+               InputStream in = ClassLoader.getSystemResourceAsStream(baseDir + TOURS_FILE);
+               if (in == null) {
+                       throw new FileNotFoundException("File '" + baseDir + TOURS_FILE + "' not found.");
+               }
+               
+               try {
+                       
+                       List<String> tours = new ArrayList<String>();
+                       TextLineReader reader = new TextLineReader(in);
+                       while (reader.hasNext()) {
+                               tours.add(reader.next());
+                       }
+                       return tours;
+                       
+               } finally {
+                       in.close();
+               }
+       }
+       
+       
+       private StyleSheet loadStyleSheet() throws IOException {
+               InputStream in = ClassLoader.getSystemResourceAsStream(baseDir + STYLESHEET_FILE);
+               if (in == null) {
+                       throw new FileNotFoundException("File '" + baseDir + STYLESHEET_FILE + "' not found.");
+               }
+               
+               try {
+                       
+                       StyleSheet ss = new StyleSheet();
+                       InputStreamReader reader = new InputStreamReader(in, "UTF-8");
+                       ss.loadRules(reader, null);
+                       return ss;
+                       
+               } finally {
+                       in.close();
+               }
+               
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/help/tours/SlideShowComponent.java b/src/net/sf/openrocket/gui/help/tours/SlideShowComponent.java
new file mode 100644 (file)
index 0000000..4e11d25
--- /dev/null
@@ -0,0 +1,72 @@
+package net.sf.openrocket.gui.help.tours;
+
+import java.awt.Dimension;
+
+import javax.swing.JEditorPane;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.event.HyperlinkListener;
+import javax.swing.text.html.HTMLDocument;
+import javax.swing.text.html.StyleSheet;
+
+import net.sf.openrocket.gui.components.ImageDisplayComponent;
+
+/**
+ * Component that displays a single slide, with the image on top and
+ * text below it.  The portions are resizeable.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SlideShowComponent extends JSplitPane {
+       
+       private final ImageDisplayComponent imageDisplay;
+       private final JEditorPane textPane;
+       
+       
+       public SlideShowComponent() {
+               super(VERTICAL_SPLIT);
+               
+               imageDisplay = new ImageDisplayComponent();
+               imageDisplay.setPreferredSize(new Dimension(600, 350));
+               this.setLeftComponent(imageDisplay);
+               
+               textPane = new JEditorPane("text/html", "");
+               textPane.setEditable(false);
+               textPane.setPreferredSize(new Dimension(600, 100));
+               
+               JScrollPane scrollPanel = new JScrollPane(textPane);
+               this.setRightComponent(scrollPanel);
+               
+               this.setResizeWeight(0.7);
+       }
+       
+       
+
+       public void setSlide(Slide slide) {
+               this.imageDisplay.setImage(slide.getImage());
+               this.textPane.setText(slide.getText());
+       }
+       
+       
+       /**
+        * Replace the current HTML style sheet with a new style sheet.
+        */
+       public void setStyleSheet(StyleSheet newStyleSheet) {
+               HTMLDocument doc = (HTMLDocument) textPane.getDocument();
+               StyleSheet base = doc.getStyleSheet();
+               StyleSheet[] linked = base.getStyleSheets();
+               if (linked != null) {
+                       for (StyleSheet ss : linked) {
+                               base.removeStyleSheet(ss);
+                       }
+               }
+               
+               base.addStyleSheet(newStyleSheet);
+       }
+       
+       
+       public void addHyperlinkListener(HyperlinkListener listener) {
+               textPane.addHyperlinkListener(listener);
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/help/tours/SlideShowDialog.java b/src/net/sf/openrocket/gui/help/tours/SlideShowDialog.java
new file mode 100644 (file)
index 0000000..ca97e2e
--- /dev/null
@@ -0,0 +1,160 @@
+package net.sf.openrocket.gui.help.tours;
+
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Locale;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+import javax.swing.event.HyperlinkEvent;
+import javax.swing.event.HyperlinkListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.util.GUIUtil;
+import net.sf.openrocket.l10n.Translator;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.BugException;
+import net.sf.openrocket.util.Chars;
+
+public class SlideShowDialog extends JDialog {
+       
+       private static final Translator trans = Application.getTranslator();
+       
+       private SlideShowComponent slideShowComponent;
+       private SlideSet slideSet;
+       private int position;
+       
+       private JButton nextButton;
+       private JButton prevButton;
+       private JButton closeButton;
+       
+       
+       public SlideShowDialog(Window parent) {
+               super(parent, ModalityType.MODELESS);
+               
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               
+               slideShowComponent = new SlideShowComponent();
+               panel.add(slideShowComponent, "spanx, grow, wrap para");
+               
+
+               JPanel sub = new JPanel(new MigLayout("ins 0, fill"));
+               
+               prevButton = new JButton(Chars.LEFT_ARROW + " " + trans.get("btn.prev"));
+               prevButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setPosition(position - 1);
+                       }
+               });
+               sub.add(prevButton, "left");
+               
+
+
+               nextButton = new JButton(trans.get("btn.next") + " " + Chars.RIGHT_ARROW);
+               nextButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setPosition(position + 1);
+                       }
+               });
+               sub.add(nextButton, "left, gapleft para");
+               
+
+               sub.add(new JPanel(), "growx");
+               
+
+               closeButton = new JButton(trans.get("button.close"));
+               closeButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               SlideShowDialog.this.dispose();
+                       }
+               });
+               sub.add(closeButton, "right");
+               
+
+               panel.add(sub, "growx");
+               
+               this.add(panel);
+               updateEnabled();
+               GUIUtil.setDisposableDialogOptions(this, nextButton);
+               this.setAlwaysOnTop(true);
+       }
+       
+       public void setSlideSet(SlideSet slideSet, int position) {
+               this.slideSet = slideSet;
+               this.setTitle(slideSet.getTitle() + " " + Chars.EMDASH + " OpenRocket");
+               slideShowComponent.setStyleSheet(slideSet.getStyleSheet());
+               setPosition(position);
+       }
+       
+       public void setPosition(int position) {
+               if (this.slideSet == null) {
+                       throw new BugException("setPosition called when slideSet is null");
+               }
+               
+               if (position < 0 || position >= slideSet.getSlideCount()) {
+                       throw new BugException("position exceeds slide count, position=" + position +
+                                       " slideCount=" + slideSet.getSlideCount());
+               }
+               
+               this.position = position;
+               slideShowComponent.setSlide(slideSet.getSlide(position));
+               updateEnabled();
+       }
+       
+       
+       private void updateEnabled() {
+               if (slideSet == null) {
+                       prevButton.setEnabled(false);
+                       nextButton.setEnabled(false);
+                       return;
+               }
+               
+               prevButton.setEnabled(position > 0);
+               nextButton.setEnabled(position < slideSet.getSlideCount() - 1);
+       }
+       
+       
+       public static void main(String[] args) throws Exception {
+               
+               Locale.setDefault(new Locale("de", "DE", ""));
+               
+               SlideSetManager manager = new SlideSetManager("datafiles/tours");
+               manager.load();
+               
+               final SlideSet set = manager.getSlideSet("test.tour");
+               
+               SwingUtilities.invokeAndWait(new Runnable() {
+                       @Override
+                       public void run() {
+                               
+                               SlideShowDialog ssd = new SlideShowDialog(null);
+                               
+                               ssd.slideShowComponent.addHyperlinkListener(new HyperlinkListener() {
+                                       @Override
+                                       public void hyperlinkUpdate(HyperlinkEvent e) {
+                                               System.out.println("Hyperlink event: " + e);
+                                               System.out.println("Event type: " + e.getEventType());
+                                               System.out.println("Description: " + e.getDescription());
+                                               System.out.println("URL: " + e.getURL());
+                                               System.out.println("Source element: " + e.getSourceElement());
+                                               
+                                       }
+                               });
+                               
+                               ssd.setSize(500, 500);
+                               ssd.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+                               ssd.setVisible(true);
+                               
+                               ssd.setSlideSet(set, 0);
+                       }
+               });
+       }
+       
+
+}
diff --git a/src/net/sf/openrocket/gui/help/tours/TextLineReader.java b/src/net/sf/openrocket/gui/help/tours/TextLineReader.java
new file mode 100644 (file)
index 0000000..fd3ddaf
--- /dev/null
@@ -0,0 +1,120 @@
+package net.sf.openrocket.gui.help.tours;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import net.sf.openrocket.util.BugException;
+
+/**
+ * Read from a Reader object one line at a time, ignoring blank lines,
+ * preceding and trailing whitespace and comment lines starting with '#'.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class TextLineReader implements Iterator<String> {
+       
+       private static final Charset UTF8 = Charset.forName("UTF-8");
+       
+
+
+       private final BufferedReader reader;
+       
+       private String next = null;
+       
+       /**
+        * Read from an input stream with UTF-8 character encoding.
+        */
+       public TextLineReader(InputStream inputStream) {
+               this(new InputStreamReader(inputStream, UTF8));
+       }
+       
+       
+       /**
+        * Read from a reader.
+        */
+       public TextLineReader(Reader reader) {
+               if (reader instanceof BufferedReader) {
+                       this.reader = (BufferedReader) reader;
+               } else {
+                       this.reader = new BufferedReader(reader);
+               }
+       }
+       
+       
+       /**
+        * Test whether the file has more lines available.
+        */
+       @Override
+       public boolean hasNext() {
+               if (next != null) {
+                       return true;
+               }
+               
+               try {
+                       next = readLine();
+               } catch (IOException e) {
+                       throw new BugException(e);
+               }
+               
+               return next != null;
+       }
+       
+       
+       /**
+        * Retrieve the next non-blank, non-comment line.
+        */
+       @Override
+       public String next() {
+               if (hasNext()) {
+                       String ret = next;
+                       next = null;
+                       return ret;
+               }
+               
+               throw new NoSuchElementException("End of file reached");
+       }
+       
+       
+       /**
+        * Peek what the next line would be.
+        */
+       public String peek() {
+               if (hasNext()) {
+                       return next;
+               }
+               
+               throw new NoSuchElementException("End of file reached");
+       }
+       
+       
+       private String readLine() throws IOException {
+               
+               while (true) {
+                       // Read the next line
+                       String line = reader.readLine();
+                       if (line == null) {
+                               return null;
+                       }
+                       
+                       // Check whether to accept the line
+                       line = line.trim();
+                       if (line.length() > 0 && line.charAt(0) != '#') {
+                               return line;
+                       }
+               }
+               
+       }
+       
+       
+       @Override
+       public void remove() {
+               throw new UnsupportedOperationException("Remove not supported");
+       }
+       
+}
index cb7bd5009d1cf92c104f48672db471cb9b0f6a45..417b67e64110876932e2ea63a86dc2b01f492015 100644 (file)
@@ -81,14 +81,15 @@ import net.sf.openrocket.gui.dialogs.SwingWorkerDialog;
 import net.sf.openrocket.gui.dialogs.WarningDialog;
 import net.sf.openrocket.gui.dialogs.optimization.GeneralOptimizationDialog;
 import net.sf.openrocket.gui.dialogs.preferences.PreferencesDialog;
+import net.sf.openrocket.gui.help.tours.GuidedTourSelectionDialog;
 import net.sf.openrocket.gui.main.componenttree.ComponentTree;
 import net.sf.openrocket.gui.scalefigure.RocketPanel;
 import net.sf.openrocket.gui.util.FileHelper;
 import net.sf.openrocket.gui.util.GUIUtil;
 import net.sf.openrocket.gui.util.Icons;
 import net.sf.openrocket.gui.util.OpenFileWorker;
-import net.sf.openrocket.gui.util.SwingPreferences;
 import net.sf.openrocket.gui.util.SaveFileWorker;
+import net.sf.openrocket.gui.util.SwingPreferences;
 import net.sf.openrocket.l10n.Translator;
 import net.sf.openrocket.logging.LogHelper;
 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
@@ -672,6 +673,24 @@ public class BasicFrame extends JFrame {
                menubar.add(menu);
                
 
+               // Guided tours
+               
+               item = new JMenuItem(trans.get("main.menu.help.tours"), KeyEvent.VK_L);
+               // TODO: Icon
+               item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.tours.desc"));
+               item.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               log.user("Guided tours selected");
+                               // FIXME:  Singleton
+                               new GuidedTourSelectionDialog(BasicFrame.this).setVisible(true);
+                       }
+               });
+               menu.add(item);
+               
+               menu.addSeparator();
+               
+
                //// License
                item = new JMenuItem(trans.get("main.menu.help.license"), KeyEvent.VK_L);
                item.setIcon(Icons.HELP_LICENSE);
index 1920bfa87c99403695b8d0181608359bd365e292..eeba22bcaf2ae7daff79ce9d8d4e4387ba942992 100644 (file)
@@ -35,6 +35,9 @@ public class Chars {
        /** Zero-width space */
        public static final char ZWSP = '\u200B';
        
+       /** Em dash */
+       public static final char EMDASH = '\u2014';
+       
        /** Micro sign (Greek letter mu) */
        public static final char MICRO = '\u00B5';