All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.eteks.sweethome3d.viewcontroller.HelpController Maven / Gradle / Ivy

Go to download

Sweet Home 3D is a free interior design application that helps you draw the plan of your house, arrange furniture on it and visit the results in 3D.

There is a newer version: 5.4.0.4
Show newest version
/*
 * HelpController.java 20 juil. 07
 *
 * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks 
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.viewcontroller;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.text.BadLocationException;
import javax.swing.text.ChangedCharSetException;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTML.Tag;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;

import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.ResourceURLContent;

/**
 * A MVC controller for Sweet Home 3D help view.
 * @author Emmanuel Puybaret
 */
public class HelpController implements Controller {
  /**
   * The properties that may be edited by the view associated to this controller. 
   */
  public enum Property {HELP_PAGE, BROWSER_PAGE, 
      PREVIOUS_PAGE_ENABLED, NEXT_PAGE_ENABLED, HIGHLIGHTED_TEXT}

  private static final String SEARCH_RESULT_PROTOCOL = "search";
  
  private final UserPreferences       preferences;
  private final ViewFactory           viewFactory;
  private final PropertyChangeSupport propertyChangeSupport;
  private final List             history;
  private int                         historyIndex;
  private HelpView                    helpView;
  
  private URL helpPage;
  private URL browserPage;
  private boolean previousPageEnabled;
  private boolean nextPageEnabled;
  private String  highlightedText;
  
  public HelpController(UserPreferences preferences, 
                        ViewFactory viewFactory) {
    this.preferences = preferences;
    this.viewFactory = viewFactory;    
    this.propertyChangeSupport = new PropertyChangeSupport(this);
    this.history = new ArrayList();
    this.historyIndex = -1;
    showPage(getHelpIndexPageURL());
  }

  /**
   * Returns the view associated with this controller.
   */
  public HelpView getView() {
    if (this.helpView == null) {
      this.helpView = this.viewFactory.createHelpView(this.preferences, this);
      addLanguageListener(this.preferences);
    }
    return this.helpView;
  }

  /**
   * Displays the help view controlled by this controller. 
   */
  public void displayView() {
    getView().displayView();
  }

  /**
   * Adds the property change listener in parameter to this controller.
   */
  public void addPropertyChangeListener(Property property, PropertyChangeListener listener) {
    this.propertyChangeSupport.addPropertyChangeListener(property.name(), listener);
  }

  /**
   * Removes the property change listener in parameter from this controller.
   */
  public void removePropertyChangeListener(Property property, PropertyChangeListener listener) {
    this.propertyChangeSupport.removePropertyChangeListener(property.name(), listener);
  }

  /**
   * Sets the current page.
   */
  private void setHelpPage(URL helpPage) {
    if (helpPage != this.helpPage) {
      URL oldHelpPage = this.helpPage;
      this.helpPage = helpPage;
      this.propertyChangeSupport.firePropertyChange(Property.HELP_PAGE.name(), oldHelpPage, helpPage);
    }
  }
  
  /**
   * Returns the current page.
   */
  public URL getHelpPage() {
    return this.helpPage;
  }

  /**
   * Sets the browser page.
   */
  private void setBrowserPage(URL browserPage) {
    if (browserPage != this.browserPage) {
      URL oldBrowserPage = this.browserPage;
      this.browserPage = browserPage;
      this.propertyChangeSupport.firePropertyChange(Property.BROWSER_PAGE.name(), oldBrowserPage, browserPage);
    }
  }
  
  /**
   * Returns the browser page.
   */
  public URL getBrowserPage() {
    return this.browserPage;
  }

  /**
   * Sets whether a previous page is available or not.
   */
  private void setPreviousPageEnabled(boolean previousPageEnabled) {
    if (previousPageEnabled != this.previousPageEnabled) {
      this.previousPageEnabled = previousPageEnabled;
      this.propertyChangeSupport.firePropertyChange(Property.PREVIOUS_PAGE_ENABLED.name(), 
          !previousPageEnabled, previousPageEnabled);
    }
  }
  
  /**
   * Returns whether a previous page is available or not.
   */
  public boolean isPreviousPageEnabled() {
    return this.previousPageEnabled;
  }

  /**
   * Sets whether a next page is available or not.
   */
  private void setNextPageEnabled(boolean nextPageEnabled) {
    if (nextPageEnabled != this.nextPageEnabled) {
      this.nextPageEnabled = nextPageEnabled;
      this.propertyChangeSupport.firePropertyChange(Property.NEXT_PAGE_ENABLED.name(), 
          !nextPageEnabled, nextPageEnabled);
    }
  }
  
  /**
   * Returns whether a next page is available or not.
   */
  public boolean isNextPageEnabled() {
    return this.nextPageEnabled;
  }

  /**
   * Sets the highlighted text.
   */
  public void setHighlightedText(String highlightedText) {
    if (highlightedText != this.highlightedText
        && (highlightedText == null || !highlightedText.equals(this.highlightedText))) {
      String oldHighlightedText = this.highlightedText;
      this.highlightedText = highlightedText;
      this.propertyChangeSupport.firePropertyChange(Property.HIGHLIGHTED_TEXT.name(), 
          oldHighlightedText, highlightedText);
    }
  }
  
  /**
   * Returns the highlighted text.
   */
  public String getHighlightedText() {
    return getHelpPage() == null || SEARCH_RESULT_PROTOCOL.equals(getHelpPage().getProtocol()) 
        ? null
        : this.highlightedText;
  }

  /**
   * Adds a property change listener to preferences to update
   * displayed page when language changes.
   */
  private void addLanguageListener(UserPreferences preferences) {
    preferences.addPropertyChangeListener(UserPreferences.Property.LANGUAGE, 
        new LanguageChangeListener(this));
  }

  /**
   * Preferences property listener bound to this component with a weak reference to avoid
   * strong link between preferences and this component.  
   */
  private static class LanguageChangeListener implements PropertyChangeListener {
    private WeakReference helpController;

    public LanguageChangeListener(HelpController helpController) {
      this.helpController = new WeakReference(helpController);
    }
    
    public void propertyChange(PropertyChangeEvent ev) {
      // If help controller was garbage collected, remove this listener from preferences
      HelpController helpController = this.helpController.get();
      if (helpController == null) {
        ((UserPreferences)ev.getSource()).removePropertyChangeListener(
            UserPreferences.Property.LANGUAGE, this);
      } else {
        // Updates home page from current default locale
        helpController.history.clear();
        helpController.historyIndex = -1;
        helpController.showPage(helpController.getHelpIndexPageURL());
      }
    }
  }
  
  /**
   * Controls the display of previous page.
   */
  public void showPrevious() {
    setHelpPage(this.history.get(--this.historyIndex));
    setPreviousPageEnabled(this.historyIndex > 0);
    setNextPageEnabled(true);
  }

  /**
   * Controls the display of next page.
   */
  public void showNext() {
    setHelpPage(this.history.get(++this.historyIndex));
    setPreviousPageEnabled(true);
    setNextPageEnabled(this.historyIndex < this.history.size() - 1);
  }

  /**
   * Controls the display of the given page.
   */
  public void showPage(URL page) {
    if (isBrowserPage(page)) {
      setBrowserPage(page);
    } else if (this.historyIndex == -1
            || !this.history.get(this.historyIndex).equals(page)) {
      setHelpPage(page);
      for (int i = this.history.size() - 1; i > this.historyIndex; i--) {
        this.history.remove(i);
      }
      this.history.add(page);
      setPreviousPageEnabled(++this.historyIndex > 0);
      setNextPageEnabled(false);
    }
  }
  
  /**
   * Returns true if the given page should be displayed 
   * by the system browser rather than by the help view.
   * By default, it returns true if the page protocol is http or https.
   */
  protected boolean isBrowserPage(URL page) {
    String protocol = page.getProtocol();
    return protocol.equals("http") || protocol.equals("https");
  }
  
  /**
   * Returns the URL of the help index page.
   */
  private URL getHelpIndexPageURL() {
    String helpIndex = this.preferences.getLocalizedString(HelpController.class, "helpIndex");
    try {
      // Try first to interpret contentFile as an absolute URL 
      return new URL(helpIndex);
    } catch (MalformedURLException ex) {
      String classPackage = HelpController.class.getName();
      classPackage = classPackage.substring(0, classPackage.lastIndexOf(".")).replace('.', '/');
      String helpIndexWithoutLeadingSlash = helpIndex.startsWith("/") 
          ? helpIndex.substring(1) 
          : classPackage + '/' + helpIndex;
      for (ClassLoader classLoader : this.preferences.getResourceClassLoaders()) {
        try {
          return new ResourceURLContent(classLoader, helpIndexWithoutLeadingSlash).getURL();
        } catch (IllegalArgumentException ex2) {
          // Try next class loader 
        }
      }
      try {
        // Build URL of index page with ResourceURLContent because of Java bug #6746185 
        return new ResourceURLContent(HelpController.class, helpIndex).getURL();
      } catch (IllegalArgumentException ex2) {
        ex2.printStackTrace();
        // Return English help by default
        return new ResourceURLContent(HelpController.class, "resources/help/en/index.html").getURL();
      }
    }
  }
  
  /**
   * Searches searchedText in help documents and displays 
   * the result.
   */
  public void search(String searchedText) {
    URL helpIndex = getHelpIndexPageURL();
    String [] searchedWords = getLowerCaseSearchedWords(searchedText);
    List helpDocuments = searchInHelpDocuments(helpIndex, searchedWords);
    URL applicationIconUrl = null;
    try {
      applicationIconUrl = new ResourceURLContent(HelpController.class, "resources/help/images/applicationIcon32.png").getURL();
    } catch (Exception ex) {
      // Ignore icon
    }
    // Build dynamically the search result page
    final StringBuilder htmlText = new StringBuilder(
        "\n"
        + ""
        + "
" + " " + " " + " " + (applicationIconUrl != null ? "" : "") + " " + " " + " " + " " + "
   " + this.preferences.getLocalizedString(HelpController.class, "searchResult") + "
 
" + " "); if (helpDocuments.size() == 0) { String searchNotFound = this.preferences.getLocalizedString(HelpController.class, "searchNotFound", searchedText); htmlText.append(""); } else { String searchFound = this.preferences.getLocalizedString(HelpController.class, "searchFound", searchedText); htmlText.append(""); URL searchRelevanceImage = new ResourceURLContent(HelpController.class, "resources/searchRelevance.gif").getURL(); for (HelpDocument helpDocument : helpDocuments) { // Add hyperlink to help document found htmlText.append(""); } } htmlText.append("

" + searchNotFound + "

" + searchFound + "

" + helpDocument.getTitle() + ""); // Add relevance image for (int i = 0; i < helpDocument.getRelevance() && i < 50; i++) { htmlText.append(""); } htmlText.append("
"); try { // Show built HTML text as a page read from an URL showPage(new URL(null, SEARCH_RESULT_PROTOCOL + "://" + htmlText.hashCode(), new URLStreamHandler() { @Override protected URLConnection openConnection(URL url) throws IOException { return new URLConnection(url) { @Override public void connect() throws IOException { // Don't need to connect } @Override public InputStream getInputStream() throws IOException { return new ByteArrayInputStream( htmlText.toString().getBytes("UTF-8")); } }; } })); } catch (MalformedURLException ex) { // Can't happen } } /** * Returns the searched words in the given text. */ private String [] getLowerCaseSearchedWords(String searchedText) { String [] searchedWords = searchedText.split("\\s"); for (int i = 0; i < searchedWords.length; i++) { searchedWords [i] = searchedWords [i].toLowerCase().trim(); } return searchedWords; } /** * Searches searchedWords in help documents and returns * the list of matching documents sorted from the most relevant to the least relevant. * This method uses some Swing classes for their HTML parsing capabilities * and not to create components. */ private List searchInHelpDocuments(URL helpIndex, String [] searchedWords) { List parsedDocuments = new ArrayList(); parsedDocuments.add(helpIndex); List helpDocuments = new ArrayList(); // Parse all the URLs added to parsedDocuments at each loop for (int i = 0; i < parsedDocuments.size(); i++) { try { // Parse a HTML document URL helpDocumentUrl = parsedDocuments.get(i); HelpDocument helpDocument = new HelpDocument(helpDocumentUrl, searchedWords); helpDocument.parse(); // If searched text was found add it to returned documents list if (helpDocument.getRelevance() > 0) { helpDocuments.add(helpDocument); } // Check if the HTML file contains new URLs to parse for (URL url : helpDocument.getReferencedDocuments()) { String lowerCaseFile = url.getFile().toLowerCase(); if (lowerCaseFile.endsWith(".html") && !parsedDocuments.contains(url)) { parsedDocuments.add(url); } } } catch (IOException ex) { // Ignore unknown documents (their URLs should be checked outside of Sweet Home 3D) } } // Sort by relevance Collections.sort(helpDocuments, new Comparator() { public int compare(HelpDocument document1, HelpDocument document2) { return document2.getRelevance() - document1.getRelevance(); } }); return helpDocuments; } /** * A help HTML document parsed with HTMLEditorKit. */ private class HelpDocument extends HTMLDocument { // Documents set referenced in this file private Set referencedDocuments = new HashSet(); private String [] searchedWords; private int relevance; private String title = ""; public HelpDocument(URL helpDocument, String [] searchedWords) { this.searchedWords = searchedWords; // Store HTML file base setBase(helpDocument); } /** * Parses this document. */ public void parse() throws IOException { HTMLEditorKit html = new HTMLEditorKit(); Reader urlReader = null; try { urlReader = new InputStreamReader(getBase().openStream(), "ISO-8859-1"); // Parse HTML file first without ignoring charset directive putProperty("IgnoreCharsetDirective", Boolean.FALSE); try { html.read(urlReader, this, 0); } catch (ChangedCharSetException ex) { // Retrieve document real encoding String mimeType = ex.getCharSetSpec(); String encoding = mimeType.substring(mimeType.indexOf("=") + 1).trim(); // Restart reading document with its real encoding urlReader.close(); urlReader = new InputStreamReader(getBase().openStream(), encoding); putProperty("IgnoreCharsetDirective", Boolean.TRUE); html.read(urlReader, this, 0); } } catch (BadLocationException ex) { } finally { if (urlReader != null) { try { urlReader.close(); } catch (IOException ex) { } } } } public Set getReferencedDocuments() { return this.referencedDocuments; } public int getRelevance() { return this.relevance; } public String getTitle() { return this.title; } private void addReferencedDocument(String referencedDocument) { try { URL url = new URL(getBase(), referencedDocument); if (!isBrowserPage(url)) { URL urlWithNoAnchor = new URL( url.getProtocol(), url.getHost(), url.getPort(), url.getFile()); this.referencedDocuments.add(urlWithNoAnchor); } } catch (MalformedURLException e) { // Ignore malformed URLs (they should be checked outside of Sweet Home 3D) } } @Override public HTMLEditorKit.ParserCallback getReader(int pos) { // Change default callback reader return new HelpReader(); } // Reader that tracks all tags in current HTML document private class HelpReader extends HTMLEditorKit.ParserCallback { private boolean inTitle; @Override public void handleStartTag(HTML.Tag tag, MutableAttributeSet att, int pos) { if (tag.equals(HTML.Tag.A)) { // tag String attribute = (String)att.getAttribute(HTML.Attribute.HREF); if (attribute != null) { addReferencedDocument(attribute); } } else if (tag.equals(HTML.Tag.TITLE)) { this.inTitle = true; } } @Override public void handleEndTag(Tag tag, int pos) { if (tag.equals(HTML.Tag.TITLE)) { this.inTitle = false; } } @Override public void handleSimpleTag(Tag tag, MutableAttributeSet att, int pos) { if (tag.equals(HTML.Tag.META)) { String nameAttribute = (String)att.getAttribute(HTML.Attribute.NAME); String contentAttribute = (String)att.getAttribute(HTML.Attribute.CONTENT); if ("keywords".equalsIgnoreCase(nameAttribute) && contentAttribute != null) { searchWords(contentAttribute); } } } @Override public void handleText(char [] data, int pos) { String text = new String(data); if (this.inTitle) { title += text; } searchWords(text); } private void searchWords(String text) { String lowerCaseText = text.toLowerCase(); for (String searchedWord : searchedWords) { for (int index = 0; index < lowerCaseText.length(); index += searchedWord.length() + 1) { index = lowerCaseText.indexOf(searchedWord, index); if (index == -1) { break; } else { relevance++; // Give more relevance to searchedWord when it's found in title if (this.inTitle) { relevance++; } } } } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy