From: plaa Date: Mon, 19 Dec 2011 05:00:30 +0000 (+0000) Subject: guided tours implementation X-Git-Tag: upstream/12.03~1^2~235 X-Git-Url: https://git.gag.com/?a=commitdiff_plain;h=586d0f517492a33faaa77912a5a40d1272534602;p=debian%2Fopenrocket guided tours implementation git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@235 180e2498-e6e9-4542-8430-84ac67f01cd8 --- diff --git a/datafiles/tours/designed_rocket.png b/datafiles/tours/designed_rocket.png new file mode 100644 index 00000000..0accfa23 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 index 00000000..4c496d50 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 index 00000000..d6f7f70c 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 index 00000000..e2ddd7e1 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 index 00000000..63c3c04d --- /dev/null +++ b/datafiles/tours/style.css @@ -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 index 00000000..e5c20045 --- /dev/null +++ b/datafiles/tours/test.tour @@ -0,0 +1,18 @@ + +# Foo bar + +A test tour + +This is the description. + +[left_design.png] + +This is the left_design file. + +It's nifty. It's also economical. + + + +[main_window.png] +This is the next one. + diff --git a/datafiles/tours/test2.tour b/datafiles/tours/test2.tour new file mode 100644 index 00000000..aff6262a --- /dev/null +++ b/datafiles/tours/test2.tour @@ -0,0 +1,16 @@ +Another test tour + +

This is the second tour. +

You get it? + +[left_design.png] + +This is the second tour. + +It's nifty. It's also economical. + + + +[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 index 00000000..615dd84d --- /dev/null +++ b/datafiles/tours/test_de.tour @@ -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 index 00000000..79242c07 --- /dev/null +++ b/datafiles/tours/tours.txt @@ -0,0 +1,6 @@ + +# This file lists all the available tours. + +test.tour +test2.tour + diff --git a/l10n/messages.properties b/l10n/messages.properties index cbda8722..3a51de44 100644 --- a/l10n/messages.properties +++ b/l10n/messages.properties @@ -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 index 00000000..aaf82928 --- /dev/null +++ b/src/net/sf/openrocket/gui/components/ImageDisplayComponent.java @@ -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 + */ +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 index 00000000..c2ede5e2 --- /dev/null +++ b/src/net/sf/openrocket/gui/help/tours/GuidedTourSelectionDialog.java @@ -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 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) 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(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 index 00000000..7ac4212b --- /dev/null +++ b/src/net/sf/openrocket/gui/help/tours/Slide.java @@ -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 + */ +public class Slide { + + private final String imageFile; + private SoftReference 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(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 index 00000000..459bda23 --- /dev/null +++ b/src/net/sf/openrocket/gui/help/tours/SlideSet.java @@ -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 + */ +public class SlideSet { + + private String title = ""; + private String description = ""; + private final List slides = new ArrayList(); + 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 index 00000000..1a32cb47 --- /dev/null +++ b/src/net/sf/openrocket/gui/help/tours/SlideSetLoader.java @@ -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 + */ +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 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 list = new ArrayList(); + 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 index 00000000..abf5e74b --- /dev/null +++ b/src/net/sf/openrocket/gui/help/tours/SlideSetManager.java @@ -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 + */ +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 slideSets = new LinkedHashMap(); + + + /** + * 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 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 getSlideSetNames() { + return new ArrayList(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 loadTourList() throws IOException { + InputStream in = ClassLoader.getSystemResourceAsStream(baseDir + TOURS_FILE); + if (in == null) { + throw new FileNotFoundException("File '" + baseDir + TOURS_FILE + "' not found."); + } + + try { + + List tours = new ArrayList(); + 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 index 00000000..4e11d252 --- /dev/null +++ b/src/net/sf/openrocket/gui/help/tours/SlideShowComponent.java @@ -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 + */ +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 index 00000000..ca97e2e6 --- /dev/null +++ b/src/net/sf/openrocket/gui/help/tours/SlideShowDialog.java @@ -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 index 00000000..fd3ddaf0 --- /dev/null +++ b/src/net/sf/openrocket/gui/help/tours/TextLineReader.java @@ -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 + */ +public class TextLineReader implements Iterator { + + 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"); + } + +} diff --git a/src/net/sf/openrocket/gui/main/BasicFrame.java b/src/net/sf/openrocket/gui/main/BasicFrame.java index cb7bd500..417b67e6 100644 --- a/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -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); diff --git a/src/net/sf/openrocket/util/Chars.java b/src/net/sf/openrocket/util/Chars.java index 1920bfa8..eeba22bc 100644 --- a/src/net/sf/openrocket/util/Chars.java +++ b/src/net/sf/openrocket/util/Chars.java @@ -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';