I. Introduction▲
Si, comme moi, vous avez déjà dû réaliser des fichiers d'aide, vous êtes sûrement tombé sur la spécification JavaHelp (version 1.0 et version 2.0). Vous avez probablement immédiatement apprécié son intégration à Swing, son support pour l'internationalisation, l'indexation, la possibilité de réaliser vos fichiers d'aide en pur HTML, de pouvoir fusionner des fichiers d'aide au vol et la possibilité de mise à jour par réseau de ces fichiers.
Et puis, après quelques essais, vous avez déchanté. Vous avez vu à quoi ressemblait un fichier HTML affiché avec JavaHelp et vous avez vraiment hésité avant d'intégrer cette technologie dans votre application. Parce qu'il faut vraiment l'admettre, malgré toutes ses promesses, JavaHelp a visuellement l'air coincé dans les années 1990 !
Note préliminaire : le code est en dernière page et ce document technique explique comment améliorer la visionneuse de JavaHelp. Il n'a pas pour but de vous expliquer comment utiliser JavaHelp ou comment rédiger votre documentation.
II. Pourquoi ?▲
Si l'on s'intéresse de près à la spécification JavaHelp, on remarquera inévitablement une petite note précisant que, par défaut, la visionneuse repose sur le composant Swing JEditorPane. Et que savons-nous, par expérience, sur ce composant lorsqu'il affiche du HTML ?
- Pas de support pour l'antialiasing des polices.
- Pas de support pour le JavaScript.
- Pas de support pour le XHTML. N'écrivez jamais <br/>, écrivez toujours <BR>.
- Un support CSS quasi inexistant.
- En général, un rendu à des lieues du rendu d'un navigateur moderne.
En fait, historiquement, le JEditorPane date des années 1990, au tout début de l'ère Swing. Ainsi, lorsqu'il prétend supporter le format RTF de Microsoft, ce n'est que partiellement et uniquement les fichiers tels que Word 97 les créait. Inutile de faire un fichier RTF avec Office 2003 et d'espérer l'afficher dans le JEditorPane.
Il en va de même pour le HTML. La norme supportée est le « HTML 3.2/CSS1 », paire de standards qui datent de …1997 ! Et comme avec le RTF, le support n'est que partiel. Par exemple, les propriétés CSS doivent être écrites entièrement en minuscules.
Le problème de cette norme, c'est qu'on ne peut pas en faire grand-chose de visuellement attractif. Les documentations que vous allez générer avec cette norme seront assez austères. Je suis par ailleurs tombé sur plusieurs billets sur le net maudissant JavaHelp pour cette raison.
Comment faire un logiciel professionnel aujourd'hui quand on est à peine capable de changer la police dans son fichier d'aide ? Et que dire quand les images s'affichent à la mauvaise taille ou sont déformées dans l'aide ? J'ai par ailleurs trouvé des outils commerciaux de rédaction de fichiers d'aide qui avaient arrêté de supporter JavaHelp comme format de sortie, vu le nombre d'horreurs graphiques que cela générait d'après eux.
Il faut cependant poser un petit bémol à ce tableau bien noir. J'ai fait des tests sous Java 6 et cette version semble être capable d'afficher des fichiers XHTML4 avec du CSS un peu avancé, comme vous le verrez dans les captures d'écran plus loin. Mais il reste de nombreux bogues qui peuvent vite devenir un cauchemar pour vos rédacteurs de documentation.
III. Un pétard mouillé JavaHelp ? Pas vraiment▲
Il existe heureusement des solutions pour contourner ce problème d'affichage. Toutes passent par l'API de JavaHelp (c'est donc standard) pour changer la visionneuse.
JavaHelp a eu le bon goût d'être une spécification très modulaire. De nombreux éléments de l'interface peuvent être reconfigurés. Ainsi, vous trouverez dans ce billet de Roger Brinkley, qui date de 2004, une méthode très simple pour remplacer le JEditorPane par une visionneuse utilisant un navigateur natif. Au travers de Java Desktop Integration Component (JDIC), JavaHelp va ainsi demander à votre navigateur préféré (Mozilla, Internet Explorer) de faire le rendu au sein même de votre application Java.
Le code est on ne peut plus simple. Avant d'invoquer la première fois JavaHelp, il suffit de faire ceci :
import
javax.help.SwingHelpUtilities;
...
SwingHelpUtilities.setContentViewerUI
(
"javax.help.plaf.basic.BasicNativeContentViewerUI"
);
Cette solution est merveilleuse, n'est-ce pas ? Rien n'est plus apte à afficher une page HTML que votre fidèle navigateur. Que c'est beau ! Allez, on applaudit et on va boire un café.
Ce serait même encore plus beau si ça fonctionnait correctement. En effet, pour intégrer le navigateur de l'OS dans votre application Java, JDIC passe par une librairie native. Or, si comme moi vous êtes sous Linux 64 bits, ça commence à sentir tout doucement le sapin. Aucune librairie native n'est fournie pour Linux 64 bits. Pas de librairie Windows 64 bits non plus ni de librairie Mac OS X, quelle que soit la version.
De plus, JDIC utilise soit Internet Explorer, soit Mozilla. Or, Mozilla n'est ni Firefox, ni Chrome. Bien que basé sur du code commun à Mozilla, Firefox est une version allégée. Il n'intègre pas les librairies permettant l'inclusion du moteur de rendu dans une autre application. Quant à Chrome, inutilisable avec JDIC, trop récent.
En résumé, les utilisateurs qui ne pourront pas lire votre aide dans ce cas seront :
- les utilisateurs Linux 64 bits ;
- les utilisateurs Windows 64 bits ;
- les utilisateurs Mac OS X ;
- les utilisateurs sous d'autres OS moins répandus ;
- les utilisateurs Linux 32 bits ayant installé Firefox ou Opera, mais pas Mozilla ;
- a priori, les futurs utilisateurs européens de Windows Seven ayant pris autre chose qu'« Internet Explorer » comme navigateur.
Vous laisseriez beaucoup d'utilisateurs sur le carreau avec une telle solution ! Pour une application professionnelle, cette situation n'est pas acceptable.
IV. Rusé comme le serpent▲
Nous avons donc un système d'aide dont la visionneuse est interchangeable via une factory. Nous avons deux implémentations fournies en base. L'une ne fonctionne que la moitié du temps et l'autre fonctionne toujours, mais ne donne pas les résultats escomptés. On peut maintenant retrousser ses manches et créer sa propre visionneuse. Il « suffirait » de coder de A à Z un moteur HTML en Java. Il « suffirait » d'être un peu tapé du ciboulot et d'avoir du temps libre devant soi. Beaucoup de temps libre !
Eh bien mes amis, des fous sur le net, on en trouve. Je vous présente le projet Lobo qui fournit un navigateur Internet 100 % Java. Sans la moindre librairie native, ce projet vise au support du HTML4, CSS2 et JavaScript. Ce n'est pas encore parfait, mais les tests que vous allez voir sont assez concluants. De plus, cette librairie est séparée clairement en deux modules : le navigateur (menu, préférences, etc.) d'un côté et le moteur HTML/CSS/JavaScript de l'autre. Ce dernier est appelé « Cobra ».
Dans le billet de Roger Brinkley que j'ai mentionné à la page précédente, on trouve aussi des suggestions pour réutiliser la visionneuse JDIC avec n'importe quelle pseudoclasse « Browser ». Celle-ci aurait juste besoin d'une méthode « setUrl ». C'est donc ce que j'ai réalisé. Le temps de développement, incluant le téléchargement de Cobra : 1h. Ce petit cours aura été plus long à écrire !
Le principe est de reprendre le code source de javax.help.plaf.basic.BasicNativeContentViewerUI, d'en retirer ce qui concerne JDIC et d'y mettre à la place ce qui concerne Cobra. Rien de bien dur en soi. Voici le genre de remplacements à faire dans le code :
// ancien code
//html = new WebBrowser();
//html.setUrl(location);
// nouveau code
html =
new
HtmlPanel
(
);
UserAgentContext ucontext =
new
SimpleUserAgentContext
(
);
rcontext =
new
SimpleHtmlRendererContext
(
html, ucontext);
rcontext.navigate
(
url,"_top"
);
Des changements similaires sont à faire dans le reste de la classe. À noter que HtmlPanel, contrairement au « browser » de JDIC, ne doit pas être placé dans un JScrollPane (il intègre déjà le sien). Le code complet est disponible en dernière page.
Notez aussi que la modification de BasicNativeContentViewerUI nécessite d'en respecter la licence qui est une GPL. Si cela vous pose problème, vous devrez implémenter vous-même une classe qui étend HelpContentViewerUI et utiliser uniquement la distribution binaire de JavaHelp. Rien d'énorme, mais un peu plus de travail quand même.
V. Le résultat▲
Une fois les changements effectués, vous pouvez visionner votre aide en utilisant le moteur Cobra. Voici des pages HTML affichées respectivement (de gauche à droite) dans la visionneuse de base, dans la visionneuse Cobra et finalement dans Firefox (pour référence).
V-A. Pages de base▲
Voici une page HTML assez simple avec des tables, des listes, un peu de CSS et un div flottant. J'avoue avoir été surpris par la qualité de la visionneuse de base sur le coup ! Pour une mise en page basique de l'aide, changer de visionneuse pourrait même s'avérer inutile. Cependant, comme vous le voyez, le div flottant pose un petit problème et le choix des polices dans la visionneuse de base est assez douteux.
V-B. Site des forums de developpez.net▲
Le site des forums de developpez.net est largement fourni en CSS et tables en tout genre. Un vrai défi à afficher pour un navigateur ;). Je n'ai donc pas résisté à comparer son affichage. Pour le rendu de base, pas besoin de capture, votre navigateur fera le boulot :D.
Là aussi, j'ai été surpris par la qualité de la visionneuse de base. Le texte est certes petit, mal dimensionné et les zones « input » sont inutilisables, mais dans l'ensemble le rendu n'a pas l'air pire qu'avec Cobra. Il faut cependant laisser à Cobra un avantage qui ne se voit pas ici. Cobra gère le JavaScript, le rendu est animé et les « selector CSS » du type :hover sont gérés correctement.
V-C. Css Zen Garden▲
Le site de « Css Zen Garden » a pour but de démontrer toute la puissance du CSS. Ici, Cobra va prendre de l'avance. On a du CSS, en veux-tu ? En voilà. Comme on peut le voir, la visionneuse de base a du mal avec les CSS complexes et le choix des tailles de police. Le rendu Cobra est loin d'être parfait, mais il est très utilisable.
V-D. Css Zen Garden, version « sous la mer »▲
Un deuxième test, avec le même site, mais un CSS complètement différent. La différence est encore plus clairement marquée entre Cobra et la visionneuse de base !
À voir de tels rendus, vous pouvez imaginer la qualité des fichiers d'aide que vous pourrez réaliser en utilisant Cobra ou JDIC.
VI. Conclusion▲
Alors que faut-il faire ? Utiliser JDIC ? Utiliser Cobra ? Utiliser la visionneuse de base ? J'aurais tendance à vous suggérer de commencer par la visionneuse de base, mais de rédiger vos documents sans trop en tenir compte. N'oubliez pas qu'il s'agit de documents HTML et qu'ils seront vraisemblablement intégrés dans d'autres moyens d'affichage (site web, documentation pdf, etc.). Vous allez aussi probablement éditer votre aide dans un autre langage que le HTML. Peut-être un parent plus ou moins proche des wikis qui peuvent générer plein de formats différents (HTML, PDF, Word, WinHelp…).
C'est lors du rendu final dans la visionneuse (ou en cours de projet) que vous pourrez décider de votre stratégie.
- Soit tout passe bien ou les corrections à effectuer dans vos fichiers seront mineures. Dans ce cas, aucune raison de vous embêter avec une visionneuse autre que celle de base. Ne touchez pas au ContentViewerUI.
-
Soit beaucoup de corrections sont nécessaires pour atteindre un résultat acceptable ou les besoins au niveau du design de l'aide ne peuvent pas être rencontrés par la visionneuse de base. Dans ce cas, vous aurez de nouveau deux possibilités :
- soit Cobra vous donne entièrement satisfaction ;
- soit vous n'avez plus d'autre choix que de vous orienter vers le navigateur de l'OS. Dans ce dernier cas, attendez-vous à devoir fournir du support supplémentaire pour tous les cas ou les librairies natives poseront problème.
En tout cas, retenez que la visionneuse n'est pas un absolu, et il ne faut pas vous focaliser dessus. Les solutions existent, et je vous en ai proposé quelques-unes. Vous pouvez très bien en imaginer d'autres si vous le désirez.
Si vraiment vous utilisez JDIC et que vous avez des clients sur des OS non supportés, il faudra recompiler les librairies natives pour ces clients et les embarquer dans l'application. Et bien sûr, mentionner dans la documentation que pour visionner l'aide, il faut installer un navigateur.
Dernier détail qui ne se voit pas ici. Pour les pages complexes, le temps de rendu a son importance, et j'ai pu constater que, pour des pages comme le « Css Zen Garden », Cobra est nettement plus rapide à faire le rendu que la visionneuse de base.
VII. Le code▲
Voici le code de la visionneuse basée sur Cobra. Vous pouvez télécharger Cobra ici et la visionneuse s'utilise avec cette commande : SwingHelpUtilities.setContentViewerUI (CobraContentViewerUI.class.getName());
package
be.meteo.sample.javahelp.ui;
import
java.awt.BorderLayout;
import
java.awt.Dimension;
import
java.awt.Font;
import
java.beans.PropertyChangeEvent;
import
java.beans.PropertyChangeListener;
import
java.io.Serializable;
import
java.net.MalformedURLException;
import
java.net.URL;
import
java.util.Locale;
import
javax.help.HelpSet;
import
javax.help.HelpUtilities;
import
javax.help.JHelpContentViewer;
import
javax.help.JHelpNavigator;
import
javax.help.Map;
import
javax.help.TextHelpModel;
import
javax.help.Map.ID;
import
javax.help.event.HelpModelEvent;
import
javax.help.event.HelpModelListener;
import
javax.help.event.TextHelpModelEvent;
import
javax.help.event.TextHelpModelListener;
import
javax.help.plaf.HelpContentViewerUI;
import
javax.swing.JComponent;
import
javax.swing.JViewport;
import
javax.swing.plaf.ComponentUI;
import
org.lobobrowser.html.UserAgentContext;
import
org.lobobrowser.html.gui.HtmlPanel;
import
org.lobobrowser.html.parser.DocumentBuilderImpl;
import
org.lobobrowser.html.test.SimpleHtmlRendererContext;
import
org.lobobrowser.html.test.SimpleUserAgentContext;
//import org.jdesktop.jdic.browser.WebBrowser;
public
class
CobraContentViewerUI extends
HelpContentViewerUI
implements
HelpModelListener, TextHelpModelListener, PropertyChangeListener, Serializable {
protected
JHelpContentViewer theViewer;
private
static
Dimension PREF_SIZE =
new
Dimension
(
200
, 300
);
private
static
Dimension MIN_SIZE =
new
Dimension
(
80
, 80
);
private
HtmlPanel html;
private
JViewport vp;
private
DocumentBuilderImpl dbi;
private
SimpleHtmlRendererContext rcontext;
public
static
ComponentUI createUI
(
JComponent x) {
debug
(
"createUI"
);
return
new
CobraContentViewerUI
((
JHelpContentViewer) x);
}
public
CobraContentViewerUI
(
JHelpContentViewer b) {
debug
(
"createUI - sort of"
);
}
public
void
installUI
(
JComponent c) {
debug
(
"installUI"
);
theViewer =
(
JHelpContentViewer)c;
theViewer.setLayout
(
new
BorderLayout
(
));
// listen to property changes...
theViewer.addPropertyChangeListener
(
this
);
TextHelpModel model =
theViewer.getModel
(
);
if
(
model !=
null
) {
// listen to id changes...
model.addHelpModelListener
(
this
);
// listen to highlight changes...
model.addTextHelpModelListener
(
this
);
}
html =
new
HtmlPanel
(
);
// This panel should be added to a JFrame or
// another Swing component.
UserAgentContext ucontext =
new
SimpleUserAgentContext
(
);
rcontext =
new
SimpleHtmlRendererContext
(
html, ucontext);
// Note that document builder should receive both contexts.
dbi =
new
DocumentBuilderImpl
(
ucontext, rcontext);
// A documentURI should be provided to resolve relative URIs.
/**
* html future additions
* add any listeners here
*/
// if the model has a current URL then set it
if
(
model !=
null
) {
URL url =
model.getCurrentURL
(
);
if
(
url !=
null
) {
try
{
rcontext.navigate
(
url.toExternalForm
(
));
}
catch
(
MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace
(
);
}
}
}
theViewer.add
(
"Center"
, html);
}
public
void
uninstallUI
(
JComponent c) {
debug
(
"uninstallUI"
);
JHelpContentViewer viewer =
(
JHelpContentViewer) c;
viewer.removePropertyChangeListener
(
this
);
/**
* html future additions
* remove all html listeners here - if we add any
*/
TextHelpModel model =
viewer.getModel
(
);
if
(
model !=
null
) {
model.removeHelpModelListener
(
this
);
model.removeTextHelpModelListener
(
this
);
}
viewer.setLayout
(
null
);
viewer.removeAll
(
);
}
public
Dimension getPreferredSize
(
JComponent c) {
return
PREF_SIZE;
}
public
Dimension getMinimumSize
(
JComponent c) {
return
MIN_SIZE;
}
public
Dimension getMaximumSize
(
JComponent c) {
// This doesn't seem right. But I'm not sure what to do for now
return
new
Dimension
(
Integer.MAX_VALUE, Integer.MAX_VALUE);
}
public
void
idChanged
(
HelpModelEvent e) {
ID id =
e.getID
(
);
URL url =
e.getURL
(
);
TextHelpModel model =
theViewer.getModel
(
);
debug
(
"idChanged("
+
e+
")"
);
debug
(
" = "
+
id +
" "
+
url);
debug
(
" my helpModel: "
+
model);
model.setDocumentTitle
(
null
);
/**
* html future additions
* if we were doing any highlighting of the search text
* code would be needed here remove all the highlights before
* the new page is displayed
*/
rcontext.navigate
(
url,"_top"
);
debug
(
"done with idChanged"
);
}
private
void
rebuild
(
) {
debug
(
"rebuild"
);
TextHelpModel model =
theViewer.getModel
(
);
if
(
model ==
null
) {
debug
(
"rebuild-end: model is null"
);
return
;
}
/**
* html future additions
* if we were doing any highlighting the highlights would need
* to be removed here
*/
HelpSet hs =
model.getHelpSet
(
);
// for glossary - not set homeID page - glossary is not synchronized
if
(
theViewer.getSynch
(
)){
try
{
Map.ID homeID =
hs.getHomeID
(
);
Locale locale =
hs.getLocale
(
);
String name =
HelpUtilities.getString
(
locale, "history.homePage"
);
model.setCurrentID
(
homeID, name, (
JHelpNavigator)null
);
rcontext.navigate
(
model.getCurrentURL
(
),"_top"
);
}
catch
(
Exception e) {
// ignore
}
}
debug
(
"rebuild-end"
);
}
public
void
propertyChange
(
PropertyChangeEvent event) {
debug
(
"propertyChange: "
+
event.getPropertyName
(
) +
"
\n\t
oldValue:"
+
event.getOldValue
(
) +
"
\n\t
newValue:"
+
event.getNewValue
(
));
if
(
event.getSource
(
) ==
theViewer) {
String changeName =
event.getPropertyName
(
);
if
(
changeName.equals
(
"helpModel"
)) {
TextHelpModel oldModel =
(
TextHelpModel) event.getOldValue
(
);
TextHelpModel newModel =
(
TextHelpModel) event.getNewValue
(
);
if
(
oldModel !=
null
) {
oldModel.removeHelpModelListener
(
this
);
oldModel.removeTextHelpModelListener
(
this
);
}
if
(
newModel !=
null
) {
newModel.addHelpModelListener
(
this
);
newModel.addTextHelpModelListener
(
this
);
}
rebuild
(
);
}
else
if
(
changeName.equals
(
"font"
)) {
debug
(
"font changed"
);
Font newFont =
(
Font)event.getNewValue
(
);
/**
* ~~
* Put font change handling code here
*/
}
else
if
(
changeName.equals
(
"clear"
)) {
/**
* html future additions
* do not know how to do this at the current time
*/
// a~~ html.setText("");
}
else
if
(
changeName.equals
(
"reload"
)) {
rcontext.reload
(
);
}
}
}
/**
* Determines if highlights have changed.
* Collects all the highlights and marks the presentation.
*
*
@param
e
The TextHelpModelEvent.
*/
public
void
highlightsChanged
(
TextHelpModelEvent e) {
debug
(
"highlightsChanged "
+
e);
// if we do anything with highlighting it would need to
// be handled here.
}
/**
* For printf debugging.
*/
private
final
static
boolean
debug =
false
;
private
static
void
debug
(
String str) {
if
(
debug) {
System.out.println
(
"NativeContentViewerUI: "
+
str);
}
}
}
VIII. Notes et remerciements du gabarisateur▲
Cet article a été mis au gabarit de developpez.com.
Le gabarisateur remercie Malick SECK pour sa correction orthographique.