com.eteks.sweethome3d.viewcontroller.HelpController Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sweethome3d Show documentation
Show all versions of sweethome3d Show documentation
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.
/*
* 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("" + searchNotFound + "
");
} else {
String searchFound = this.preferences.getLocalizedString(HelpController.class, "searchFound", searchedText);
htmlText.append("" + searchFound + "
");
URL searchRelevanceImage = new ResourceURLContent(HelpController.class, "resources/searchRelevance.gif").getURL();
for (HelpDocument helpDocument : helpDocuments) {
// Add hyperlink to help document found
htmlText.append(""
+ helpDocument.getTitle() + " ");
// Add relevance image
for (int i = 0; i < helpDocument.getRelevance() && i < 50; i++) {
htmlText.append("");
}
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++;
}
}
}
}
}
}
}
}
"
+ "