Améliorer le rendu de JavaHelp

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 attractive. 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 cauchemard 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 :

 
Sélectionnez
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. Hors, 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. Hors, 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 pseudo-classe « 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 :

 
Sélectionnez
// 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.

Image non disponible
Visionneuse de base
Image non disponible
Visonneuse Cobra
Image non disponible
Mozilla

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.

Image non disponible
Visionneuse de base
Image non disponible
Visonneuse Cobra

Rendu de référence

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.

Rendu de référence

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 !

Rendu de référence

À 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());

 
Sélectionnez
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\toldValue:" + event.getOldValue() + "\n\tnewValue:" + 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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2009 David Delbecq. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.