/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.tutorial.gui; import static java.nio.charset.StandardCharsets.UTF_8; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; import java.awt.Point; import java.awt.event.ActionEvent; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JEditorPane; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import javax.swing.text.Document; import javax.swing.text.html.HTMLEditorKit; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.ls.DOMImplementationLS; import org.w3c.dom.ls.LSSerializer; import org.xml.sax.InputSource; import com.rapidminer.Process; import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.tools.ExtendedJScrollPane; import com.rapidminer.gui.tools.Ionicon; import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.ResourceDockKey; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.components.LinkLocalButton; import com.rapidminer.repository.MalformedRepositoryLocationException; import com.rapidminer.studio.internal.NoStartupDialogRegistreredException; import com.rapidminer.studio.internal.StartupDialogProvider.ToolbarButton; import com.rapidminer.studio.internal.StartupDialogRegistry; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.Observable; import com.rapidminer.tools.Observer; import com.rapidminer.tools.RMUrlHandler; import com.rapidminer.tools.Tools; import com.rapidminer.tools.XMLException; import com.rapidminer.tools.usagestats.ActionStatisticsCollector; import com.rapidminer.tutorial.Tutorial; import com.rapidminer.tutorial.TutorialManager; import com.vlsolutions.swing.docking.DockKey; import com.vlsolutions.swing.docking.Dockable; import com.vlsolutions.swing.docking.RelativeDockablePosition; /** * A browser for the {@link Tutorial} steps file, see {@link Tutorial#getSteps()} . * * @since 7.0.0 * @author Marcel Michel */ public class TutorialBrowser extends JPanel implements Dockable { private static final long serialVersionUID = 1L; public static final String TUTORIAL_BROWSER_DOCK_KEY = "tutorial_browser"; private static final DockKey DOCK_KEY = new ResourceDockKey(TUTORIAL_BROWSER_DOCK_KEY); { DOCK_KEY.setDockGroup(MainFrame.DOCK_GROUP_ROOT); } public static final RelativeDockablePosition POSITION = new RelativeDockablePosition(0, 1, 0.15, 1); private static final String EXPLANATION_BACKGROUND_NAME = "tutorial/explanation_background.png"; private static final String EXPLANATION_HEADER_NAME = "tutorial/explanation_header.png"; private static final String CHALLENGE_BACKGROUND_NAME = "tutorial/challenge_background.png"; private static final String CHALLENGE_HEADER_NAME = "tutorial/challenge_header.png"; private static final String ACTIVITY_BACKGROUND_NAME = "tutorial/activity_background.png"; private static final String ACTIVITY_HEADER_NAME = "tutorial/activity_header.png"; private static final URL EXPLANATION_BACKGROUND_RESOURCE = Tools.getResource("tutorial/explanation_background.png"); private static final URL EXPLANATION_HEADER_RESOURCE = Tools.getResource("tutorial/explanation_header.png"); private static final URL CHALLENGE_BACKGROUND_RESOURCE = Tools.getResource("tutorial/challenge_background.png"); private static final URL CHALLENGE_HEADER_RESOURCE = Tools.getResource("tutorial/challenge_header.png"); private static final URL ACTIVITY_BACKGROUND_RESOURCE = Tools.getResource("tutorial/activity_background.png"); private static final URL ACTIVITY_HEADER_RESOURCE = Tools.getResource("tutorial/activity_header.png"); private static final String XSL_FILE_PATH = "tutorial/tutorial_browser.xsl"; private static final String XML_ATTRIBUTE_BACKGROUND_IMAGE = "background-image"; private static final String XML_ATTRIBUTE_TOTAL = "total"; private static final String XML_ATTRIBUTE_INDEX = "index"; private static final String XML_ATTRIBUTE_IMAGE = "image"; private static final String XML_TAG_STEP = "step"; private static final String XML_TAG_ICON = "icon"; private static final String XML_TAG_TASKS = "tasks"; private static final String XML_TAG_QUESTIONS = "questions"; private static final String XML_TAG_INFO = "info"; private static final String PROGRESS_THREAD_ID = "tutorial_browser.load_tutorial"; private static final String INFO_TEMPLATE = "<html><div style=\"margin:10px;color:#555555;font-family:'Open Sans';font-size:14;text-align:center;\">%s</div></html>"; private static final String HEADLINE_TEMPLATE = "<html><span style=\"font-family:'Open Sans';font-size: 13;color:#333333;\">%s</html>"; private static final String ICON_TEMPLATE = "<html><div style=\"font-size: 12; color: %s;\">%s</div></html>"; private static final String LINK_COLOR_HEX = SwingTools.getColorHexValue(Colors.LINKBUTTON_LOCAL); private static final String LINK_STYLE = "a {color:" + LINK_COLOR_HEX + ";font-family:'Open Sans';font-size:13;}"; private static final String NEXT_TUTORIAL_URL = "tutorial://next"; private static final String NO_TUTORIAL_SELECTED = I18N.getGUILabel("tutorial_browser.no_tutorial_selected"); private static final String NO_STEPS_AVAILABLE = I18N.getGUILabel("tutorial_browser.no_steps_available"); private static final String DISPLAY_TUTORIAL_STEPS = I18N.getGUILabel("tutorial_browser.displaying_steps"); private static final String TRANSFORMATION_FAILURE = I18N.getGUILabel("tutorial_browser.transformation_failure"); private JLabel tutorialNameLabel; private JButton previousStepButton; private JButton nextStepButton; private JEditorPane jEditorPane; private JScrollPane scrollPane; private Tutorial selectedTutorial; private Tutorial nextTutorial; private List<String> steps = new ArrayList<>(); private int stepIndex = -1; private TutorialSelector tutorialSelector; private Observer<Tutorial> tutorialObserver; /** * Creates a new browser which is linked to the given {@link TutorialSelector}. * * @param tutorialSelector * the selector which should be observed */ public TutorialBrowser(TutorialSelector tutorialSelector) { this.tutorialSelector = tutorialSelector; initGUI(); tutorialObserver = new Observer<Tutorial>() { @Override public void update(Observable<Tutorial> observable, Tutorial tutorial) { selectedTutorial = tutorial; nextTutorial = null; // find next tutorial if (selectedTutorial != null) { List<Tutorial> tutorials = selectedTutorial.getGroup().getTutorials(); int currIndex = tutorials.indexOf(selectedTutorial); if (currIndex != -1 && currIndex + 1 < tutorials.size()) { nextTutorial = tutorials.get(currIndex + 1); } } updateTutorial(tutorial); } }; tutorialSelector.addObserver(tutorialObserver, true); } @Override public DockKey getDockKey() { return DOCK_KEY; } @Override public Component getComponent() { return this; } private void initGUI() { setLayout(new BorderLayout()); setBackground(Color.WHITE); add(createHeaderPanel(), BorderLayout.NORTH); add(createContentPanel(), BorderLayout.CENTER); add(createFooterPanel(), BorderLayout.SOUTH); } private Component createHeaderPanel() { JPanel buttonPanel = new JPanel(new GridBagLayout()); buttonPanel.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Colors.TAB_BORDER)); GridBagConstraints gbc = new GridBagConstraints(); gbc.insets = new Insets(10, 10, 10, 0); gbc.gridx = 0; // tutorial title { tutorialNameLabel = new JLabel(" "); buttonPanel.add(tutorialNameLabel, gbc); } // filler { gbc.gridx += 1; gbc.weightx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; buttonPanel.add(new JLabel(), gbc); gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0; } // back to tutorials button { gbc.gridx += 1; gbc.insets = new Insets(10, 0, 10, 0); JLabel backToTutorialsIcon = new JLabel(String.format(ICON_TEMPLATE, LINK_COLOR_HEX, Ionicon.HOME.getHtml())); buttonPanel.add(backToTutorialsIcon, gbc); LinkLocalButton backToTutorialsButton = new LinkLocalButton(new ResourceAction("tutorials.view_all_tutorials") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { try { StartupDialogRegistry.INSTANCE.showStartupDialog(ToolbarButton.TUTORIAL); } catch (NoStartupDialogRegistreredException e1) { SwingTools.showVerySimpleErrorMessage("tutorials_not_available"); } } }); HTMLEditorKit htmlKit = (HTMLEditorKit) backToTutorialsButton.getEditorKit(); htmlKit.getStyleSheet().addRule(LINK_STYLE); gbc.gridx += 1; gbc.insets = new Insets(10, 0, 10, 10); buttonPanel.add(backToTutorialsButton, gbc); } return buttonPanel; } private Component createFooterPanel() { JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 5, 5)); buttonPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); previousStepButton = new JButton(); previousStepButton.setAction(new ResourceAction("tutorial_browser.previous_step") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { ActionStatisticsCollector.INSTANCE.log(ActionStatisticsCollector.TYPE_GETTING_STARTED, "tutorial:" + selectedTutorial.getIdentifier(), "step_" + (stepIndex + 1) + "_previous"); displayStep(--stepIndex); } }); previousStepButton.setEnabled(false); buttonPanel.add(previousStepButton); nextStepButton = new JButton(); nextStepButton.setAction(new ResourceAction("tutorial_browser.next_step") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { ActionStatisticsCollector.INSTANCE.log(ActionStatisticsCollector.TYPE_GETTING_STARTED, "tutorial:" + selectedTutorial.getIdentifier(), "step_" + (stepIndex + 1) + "_next"); displayStep(++stepIndex); } }); nextStepButton.setEnabled(false); nextStepButton.setHorizontalTextPosition(SwingConstants.LEFT); buttonPanel.add(nextStepButton); JPanel footer = new JPanel(new BorderLayout()); footer.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Colors.TAB_BORDER)); footer.add(buttonPanel, BorderLayout.CENTER); return footer; } private Component createContentPanel() { JPanel contentPanel = new JPanel(); contentPanel.setOpaque(false); jEditorPane = new JEditorPane(); jEditorPane.setEditable(false); scrollPane = new ExtendedJScrollPane(jEditorPane); scrollPane.setBorder(null); HTMLEditorKit kit = new HTMLEditorKit(); jEditorPane.setEditorKit(kit); jEditorPane.setMargin(new Insets(0, 0, 0, 0)); Document doc = kit.createDefaultDocument(); jEditorPane.setDocument(doc); jEditorPane.setText(String.format(INFO_TEMPLATE, NO_TUTORIAL_SELECTED)); jEditorPane.addHyperlinkListener(new HyperlinkListener() { @Override public void hyperlinkUpdate(HyperlinkEvent e) { if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { if (NEXT_TUTORIAL_URL.equals(e.getDescription())) { ActionStatisticsCollector.INSTANCE.log(ActionStatisticsCollector.TYPE_GETTING_STARTED, "tutorial_browser", "next_tutorial"); if (nextTutorial == null) { try { StartupDialogRegistry.INSTANCE.showStartupDialog(ToolbarButton.TUTORIAL); } catch (NoStartupDialogRegistreredException e1) { SwingTools.showVerySimpleErrorMessage("tutorials_not_available"); } } else { try { MainFrame mainFrame = RapidMinerGUI.getMainFrame(); Process tutorialProcess = nextTutorial.makeProcess(); mainFrame.setOpenedProcess(tutorialProcess); TutorialManager.INSTANCE.completedTutorial(nextTutorial.getIdentifier()); tutorialSelector.setSelectedTutorial(nextTutorial); } catch (RuntimeException | MalformedRepositoryLocationException | IOException | XMLException e1) { SwingTools.showSimpleErrorMessage("cannot_open_tutorial", e1, nextTutorial.getTitle(), e1.getMessage()); } } } else { ActionStatisticsCollector.INSTANCE.log(ActionStatisticsCollector.TYPE_GETTING_STARTED, "tutorial_browser", "open_remote_url"); RMUrlHandler.openInBrowser(e.getURL()); } } } }); return scrollPane; } private void updateTutorial(final Tutorial tutorial) { if (tutorial == null) { steps = new ArrayList<>(); tutorialNameLabel.setText(null); jEditorPane.setText(String.format(INFO_TEMPLATE, NO_TUTORIAL_SELECTED)); updateStepButtons(); } else { tutorialNameLabel.setText(String.format(HEADLINE_TEMPLATE, tutorial.getTitle())); jEditorPane.setText(String.format(INFO_TEMPLATE, DISPLAY_TUTORIAL_STEPS)); ProgressThread thread = new ProgressThread(PROGRESS_THREAD_ID) { @Override public void run() { try (InputStream stepFileStream = tutorial.getSteps()) { if (stepFileStream == null) { SwingTools.invokeLater(new Runnable() { @Override public void run() { displaySteps(null); } }); return; } final List<String> steps = new ArrayList<>(); DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); org.w3c.dom.Document doc = dBuilder.parse(new InputSource(stepFileStream)); doc.getDocumentElement().normalize(); // enrich document information enrichNodeList(doc.getElementsByTagName(XML_TAG_ICON)); enrichNodeList(doc.getElementsByTagName(XML_TAG_INFO)); enrichNodeList(doc.getElementsByTagName(XML_TAG_TASKS)); enrichNodeList(doc.getElementsByTagName(XML_TAG_QUESTIONS)); TransformerFactory tFactory = TransformerFactory.newInstance(); Transformer transformer = tFactory .newTransformer(new StreamSource(Tools.getResourceInputStream(XSL_FILE_PATH))); NodeList stepList = doc.getElementsByTagName(XML_TAG_STEP); for (int i = 0; i < stepList.getLength(); i++) { Node nNode = stepList.item(i); // enrich step node ((Element) nNode).setAttribute(XML_ATTRIBUTE_INDEX, String.valueOf(i + 1)); ((Element) nNode).setAttribute(XML_ATTRIBUTE_TOTAL, String.valueOf(stepList.getLength())); DOMImplementationLS lsImpl = (DOMImplementationLS) nNode.getOwnerDocument().getImplementation() .getFeature("LS", "3.0"); LSSerializer lsSerializer = lsImpl.createLSSerializer(); lsSerializer.getDomConfig().setParameter("xml-declaration", false); String currentStep = lsSerializer.writeToString(nNode); ByteArrayOutputStream output = new ByteArrayOutputStream(); org.w3c.dom.Document document = dBuilder.parse(new InputSource(new StringReader(currentStep))); transformer.transform(new DOMSource(document), new StreamResult(output)); steps.add(output.toString(UTF_8.name())); } SwingTools.invokeLater(new Runnable() { @Override public void run() { displaySteps(steps); } }); } catch (Exception e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.tutorial.gui.TutorialBrowser.failed_to_load_stepfile", new Object[] { tutorial.getTitle(), e }); SwingTools.invokeLater(new Runnable() { @Override public void run() { jEditorPane.setText(String.format(INFO_TEMPLATE, TRANSFORMATION_FAILURE)); } }); } } }; thread.setIndeterminate(true); thread.addDependency(PROGRESS_THREAD_ID); thread.start(); } } private void enrichNodeList(NodeList nList) { for (int i = 0; i < nList.getLength(); i++) { Node nNode = nList.item(i); switch (nNode.getNodeName()) { case XML_TAG_ICON: ((Element) nNode).setAttribute(XML_ATTRIBUTE_IMAGE, SwingTools.getIconPath(nNode.getTextContent())); break; case XML_TAG_INFO: ((Element) nNode).setAttribute(XML_ATTRIBUTE_IMAGE, EXPLANATION_HEADER_RESOURCE == null ? SwingTools.getIconPath(EXPLANATION_HEADER_NAME) : EXPLANATION_HEADER_RESOURCE.toExternalForm()); ((Element) nNode).setAttribute(XML_ATTRIBUTE_BACKGROUND_IMAGE, EXPLANATION_BACKGROUND_RESOURCE == null ? SwingTools.getIconPath(EXPLANATION_BACKGROUND_NAME) : EXPLANATION_BACKGROUND_RESOURCE.toExternalForm()); break; case XML_TAG_TASKS: ((Element) nNode).setAttribute(XML_ATTRIBUTE_IMAGE, ACTIVITY_HEADER_RESOURCE == null ? SwingTools.getIconPath(ACTIVITY_HEADER_NAME) : ACTIVITY_HEADER_RESOURCE.toExternalForm()); ((Element) nNode).setAttribute(XML_ATTRIBUTE_BACKGROUND_IMAGE, ACTIVITY_BACKGROUND_RESOURCE == null ? SwingTools.getIconPath(ACTIVITY_BACKGROUND_NAME) : ACTIVITY_BACKGROUND_RESOURCE.toExternalForm()); break; case XML_TAG_QUESTIONS: ((Element) nNode).setAttribute(XML_ATTRIBUTE_IMAGE, CHALLENGE_HEADER_RESOURCE == null ? SwingTools.getIconPath(CHALLENGE_HEADER_NAME) : CHALLENGE_HEADER_RESOURCE.toExternalForm()); ((Element) nNode).setAttribute(XML_ATTRIBUTE_BACKGROUND_IMAGE, CHALLENGE_BACKGROUND_RESOURCE == null ? SwingTools.getIconPath(CHALLENGE_BACKGROUND_NAME) : CHALLENGE_BACKGROUND_RESOURCE.toExternalForm()); break; default: break; } } } private void displaySteps(List<String> steps) { this.steps = steps; this.stepIndex = -1; if (steps != null && steps.size() > 0) { displayStep(0); } else { jEditorPane.setText(String.format(INFO_TEMPLATE, NO_STEPS_AVAILABLE)); updateStepButtons(); } } private void displayStep(final int index) { final Runnable changeStep = new Runnable() { @Override public void run() { jEditorPane.setText(steps.get(index)); stepIndex = index; updateStepButtons(); } }; new Thread(new Runnable() { @Override public void run() { try { SwingUtilities.invokeAndWait(changeStep); } catch (InvocationTargetException | InterruptedException e) { SwingUtilities.invokeLater(changeStep); } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { scrollPane.getViewport().setViewPosition(new Point(0, 0)); } }); } }).start(); } private void updateStepButtons() { if (steps == null) { nextStepButton.setEnabled(false); previousStepButton.setEnabled(false); } else { nextStepButton.setEnabled(stepIndex < steps.size() - 1); previousStepButton.setEnabled(stepIndex > 0); } } }