Vidalia  0.2.15
HelpBrowser.cpp
Go to the documentation of this file.
00001 /*
00002 **  This file is part of Vidalia, and is subject to the license terms in the
00003 **  LICENSE file, found in the top level directory of this distribution. If you
00004 **  did not receive the LICENSE file with this file, you may obtain it from the
00005 **  Vidalia source package distributed by the Vidalia Project at
00006 **  http://www.torproject.org/projects/vidalia.html. No part of Vidalia, 
00007 **  including this file, may be copied, modified, propagated, or distributed 
00008 **  except according to the terms described in the LICENSE file.
00009 */
00010 
00011 /*
00012 ** \file HelpBrowser.cpp
00013 ** \brief Displays a list of help topics and content
00014 */
00015 
00016 #include "HelpBrowser.h"
00017 #include "Vidalia.h"
00018 
00019 #include <QDomDocument>
00020 #include <QDir>
00021 
00022 #define LEFT_PANE_INDEX     0
00023 #define NO_STRETCH          0
00024 #define MINIMUM_PANE_SIZE   1
00025 
00026 /* Names of elements and attributes in the XML file */
00027 #define ELEMENT_CONTENTS        "Contents"
00028 #define ELEMENT_TOPIC           "Topic"
00029 #define ATTRIBUTE_TOPIC_ID      "id"
00030 #define ATTRIBUTE_TOPIC_HTML    "html"
00031 #define ATTRIBUTE_TOPIC_NAME    "name"
00032 #define ATTRIBUTE_TOPIC_SECTION "section"
00033 
00034 /* Define two roles used to store data associated with a topic item */
00035 #define ROLE_TOPIC_ID        Qt::UserRole
00036 #define ROLE_TOPIC_QRC_PATH (Qt::UserRole+1)
00037 
00038 
00039 /** Constuctor. This will probably do more later */
00040 HelpBrowser::HelpBrowser(QWidget *parent)
00041  : VidaliaWindow("HelpBrowser", parent)
00042 {
00043   VidaliaSettings settings;
00044 
00045   /* Invoke Qt Designer generated QObject setup routine */
00046   ui.setupUi(this);
00047 #if defined(Q_WS_MAC)
00048   ui.actionHome->setShortcut(QString("Shift+Ctrl+H"));
00049 #endif
00050 
00051   /* Pressing 'Esc' or 'Ctrl+W' will close the window */
00052   ui.actionClose->setShortcut(QString("Esc"));
00053   Vidalia::createShortcut("Ctrl+W", this, ui.actionClose, SLOT(trigger()));
00054 
00055   /* Hide Search frame */
00056   ui.frmFind->setHidden(true);
00057  
00058   /* Set the splitter pane sizes so that only the txtBrowser pane expands
00059    * and set to arbitrary sizes (the minimum sizes will take effect */
00060   QList<int> sizes;
00061   sizes.append(MINIMUM_PANE_SIZE); 
00062   sizes.append(MINIMUM_PANE_SIZE);
00063   ui.splitter->setSizes(sizes);
00064   ui.splitter->setStretchFactor(LEFT_PANE_INDEX, NO_STRETCH);
00065 
00066   connect(ui.treeContents,
00067           SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)),
00068           this, SLOT(contentsItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)));
00069 
00070   connect(ui.treeSearch,
00071           SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)),
00072           this, SLOT(searchItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)));
00073 
00074   /* Connect the navigation actions to their slots */
00075   connect(ui.actionHome, SIGNAL(triggered()), ui.txtBrowser, SLOT(home()));
00076   connect(ui.actionBack, SIGNAL(triggered()), ui.txtBrowser, SLOT(backward()));
00077   connect(ui.actionForward, SIGNAL(triggered()), ui.txtBrowser, SLOT(forward()));
00078   connect(ui.txtBrowser, SIGNAL(backwardAvailable(bool)), 
00079           ui.actionBack, SLOT(setEnabled(bool)));
00080   connect(ui.txtBrowser, SIGNAL(forwardAvailable(bool)),
00081           ui.actionForward, SLOT(setEnabled(bool)));
00082   connect(ui.btnFindNext, SIGNAL(clicked()), this, SLOT(findNext()));
00083   connect(ui.btnFindPrev, SIGNAL(clicked()), this, SLOT(findPrev()));
00084   connect(ui.btnSearch, SIGNAL(clicked()), this, SLOT(search()));
00085   
00086   /* Load the help topics from XML */
00087   loadContentsFromXml(":/help/" + language() + "/contents.xml");
00088 
00089   /* Show the first help topic in the tree */
00090   ui.treeContents->setCurrentItem(ui.treeContents->topLevelItem(0));
00091   ui.treeContents->setItemExpanded(ui.treeContents->topLevelItem(0), true);
00092 }
00093 
00094 /** Called when the user changes the UI translation. */
00095 void
00096 HelpBrowser::retranslateUi()
00097 {
00098   ui.retranslateUi(this);
00099   ui.treeContents->clear();
00100   loadContentsFromXml(":/help/" + language() + "/contents.xml");
00101   ui.treeContents->setItemExpanded(ui.treeContents->topLevelItem(0), true);
00102   ui.treeContents->setCurrentItem(ui.treeContents->topLevelItem(0));
00103   ui.treeContents->setItemExpanded(ui.treeContents->topLevelItem(0), true);
00104 }
00105 
00106 /** Returns the language in which help topics should appear, or English
00107  * ("en") if no translated help files exist for the current GUI language. */
00108 QString
00109 HelpBrowser::language()
00110 {
00111   QString lang = Vidalia::language();
00112   if (!QDir(":/help/" + lang).exists())
00113     lang = "en";
00114   return lang;
00115 }
00116 
00117 /** Load the contents of the help topics tree from the specified XML file. */
00118 void
00119 HelpBrowser::loadContentsFromXml(QString xmlFile)
00120 {
00121   QString errorString;
00122   QFile file(xmlFile);
00123   QDomDocument document;
00124   
00125   /* Load the XML contents into the DOM document */
00126   if (!document.setContent(&file, true, &errorString)) {
00127     ui.txtBrowser->setPlainText(tr("Error Loading Help Contents: ")+errorString);
00128     return;
00129   }
00130   /* Load the DOM document contents into the tree view */
00131   if (!loadContents(&document, errorString)) {
00132     ui.txtBrowser->setPlainText(tr("Error Loading Help Contents: ")+errorString);
00133     return;
00134   }
00135 }
00136 
00137 /** Load the contents of the help topics tree from the given DOM document. */
00138 bool
00139 HelpBrowser::loadContents(const QDomDocument *document, QString &errorString)
00140 {
00141   /* Grab the root document element and make sure it's the right one */
00142   QDomElement root = document->documentElement();
00143   if (root.tagName() != ELEMENT_CONTENTS) {
00144     errorString = tr("Supplied XML file is not a valid Contents document.");
00145     return false;
00146   }
00147   _elementList << root;
00148 
00149   /* Create the home item */
00150   QTreeWidgetItem *home = createTopicTreeItem(root, 0);
00151   ui.treeContents->addTopLevelItem(home);
00152   
00153   /* Process all top-level help topics */
00154   QDomElement child = root.firstChildElement(ELEMENT_TOPIC);
00155   while (!child.isNull()) {
00156     parseHelpTopic(child, home);
00157     child = child.nextSiblingElement(ELEMENT_TOPIC);
00158   }
00159   return true;
00160 }
00161 
00162 /** Parse a Topic element and handle all its children recursively. */
00163 void
00164 HelpBrowser::parseHelpTopic(const QDomElement &topicElement, 
00165                             QTreeWidgetItem *parent)
00166 {
00167   /* Check that we have a valid help topic */
00168   if (isValidTopicElement(topicElement)) {
00169     /* Save this element for later (used for searching) */
00170     _elementList << topicElement;
00171 
00172     /* Create and populate the new topic item in the tree */
00173     QTreeWidgetItem *topic = createTopicTreeItem(topicElement, parent);
00174 
00175     /* Process all its child elements */
00176     QDomElement child = topicElement.firstChildElement(ELEMENT_TOPIC);
00177     while (!child.isNull()) {
00178       parseHelpTopic(child, topic);
00179       child = child.nextSiblingElement(ELEMENT_TOPIC);
00180     }
00181   }
00182 }
00183 
00184 /** Returns true if the given Topic element has the necessary attributes. */
00185 bool
00186 HelpBrowser::isValidTopicElement(const QDomElement &topicElement)
00187 {
00188   return (topicElement.hasAttribute(ATTRIBUTE_TOPIC_ID) &&
00189           topicElement.hasAttribute(ATTRIBUTE_TOPIC_NAME) &&
00190           topicElement.hasAttribute(ATTRIBUTE_TOPIC_HTML));
00191 }
00192 
00193 /** Builds a resource path to an html file associated with the given help
00194  * topic. If the help topic needs an achor, the anchor will be formatted and
00195  * appended. */
00196 QString
00197 HelpBrowser::getResourcePath(const QDomElement &topicElement)
00198 {
00199   QString link = language() + "/" + topicElement.attribute(ATTRIBUTE_TOPIC_HTML);
00200   if (topicElement.hasAttribute(ATTRIBUTE_TOPIC_SECTION)) {
00201     link += "#" + topicElement.attribute(ATTRIBUTE_TOPIC_SECTION);
00202   }
00203   return link;
00204 }
00205 
00206 /** Creates a new element to be inserted into the topic tree. */
00207 QTreeWidgetItem*
00208 HelpBrowser::createTopicTreeItem(const QDomElement &topicElement, 
00209                                  QTreeWidgetItem *parent)
00210 {
00211   QTreeWidgetItem *topic = new QTreeWidgetItem(parent);
00212   QString label = topicElement.attribute(ATTRIBUTE_TOPIC_NAME);
00213 
00214   topic->setText(0, label);
00215   topic->setToolTip(0, label);
00216   topic->setData(0, ROLE_TOPIC_ID, topicElement.attribute(ATTRIBUTE_TOPIC_ID));
00217   topic->setData(0, ROLE_TOPIC_QRC_PATH, getResourcePath(topicElement));
00218 
00219   return topic;
00220 }
00221 
00222 /** Called when the user selects a different item in the content topic tree */
00223 void
00224 HelpBrowser::contentsItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *prev)
00225 {
00226   QList<QTreeWidgetItem *> selected = ui.treeSearch->selectedItems();
00227   /* Deselect the selection in the search tree */
00228   if (!selected.isEmpty()) {
00229     ui.treeSearch->setItemSelected(selected[0], false);
00230   }
00231   currentItemChanged(current, prev);
00232 }
00233 
00234 /** Called when the user selects a different item in the content topic tree */
00235 void
00236 HelpBrowser::searchItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *prev)
00237 {
00238   QList<QTreeWidgetItem *> selected = ui.treeContents->selectedItems();
00239   /* Deselect the selection in the contents tree */
00240   if (!selected.isEmpty()) {
00241     ui.treeContents->setItemSelected(selected[0], false);
00242   }
00243 
00244   /* Change to selected page */
00245   currentItemChanged(current, prev);
00246 
00247   /* Highlight search phrase */
00248   QTextCursor found;
00249   QTextDocument::FindFlags flags = QTextDocument::FindWholeWords;
00250   found = ui.txtBrowser->document()->find(_lastSearch, 0, flags);
00251   if (!found.isNull()) {
00252     ui.txtBrowser->setTextCursor(found);
00253   }
00254 }
00255 
00256 /** Called when the user selects a different item in the tree. */
00257 void
00258 HelpBrowser::currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *prev)
00259 {
00260   Q_UNUSED(prev);
00261   if (current) {
00262     ui.txtBrowser->setSource(QUrl(current->data(0, 
00263                                               ROLE_TOPIC_QRC_PATH).toString()));
00264   }
00265   _foundBefore = false;
00266 }
00267 
00268 /** Searches for a topic in the topic tree. Returns a pointer to that topics
00269  * item in the topic tree if it is found, 0 otherwise. */
00270 QTreeWidgetItem*
00271 HelpBrowser::findTopicItem(QTreeWidgetItem *startItem, QString topic)
00272 {
00273   /* If startItem is null, then we don't know where to start searching. */
00274   if (!startItem)
00275     return 0;
00276 
00277   /* Parse the first subtopic in the topic id. */
00278   QString subtopic = topic.mid(0, topic.indexOf(".")).toLower();
00279 
00280   /* Search through all children of startItem and look for a subtopic match */
00281   for (int i = 0; i < startItem->childCount(); i++) {
00282     QTreeWidgetItem *item = startItem->child(i);
00283     
00284     if (subtopic == item->data(0, ROLE_TOPIC_ID).toString().toLower()) {
00285       /* Found a subtopic match, so expand this item */
00286       ui.treeContents->setItemExpanded(item, true);
00287       if (!topic.contains(".")) {
00288         /* Found the exact topic */
00289         return item;
00290       }
00291       /* Search recursively for the next subtopic */
00292       return findTopicItem(item, topic.mid(topic.indexOf(".")+1));
00293     }
00294   }
00295   return 0;
00296 }
00297 
00298 /** Shows the help browser. If a sepcified topic was given, then search for
00299  * that topic's ID (e.g., "log.basic") and display the appropriate page. */
00300 void
00301 HelpBrowser::showTopic(QString topic)
00302 {
00303   /* Search for the topic in the contents tree */
00304   QTreeWidgetItem *item =
00305     findTopicItem(ui.treeContents->topLevelItem(0), topic);
00306   QTreeWidgetItem *selected = 0;
00307 
00308   if (item) {
00309     /* Item was found, so show its location in the hierarchy and select its
00310      * tree item. */
00311     if (ui.treeContents->selectedItems().size()) {
00312       selected = ui.treeContents->selectedItems()[0];
00313       if (selected)
00314         ui.treeContents->setItemSelected(selected, false);
00315     }
00316     ui.treeContents->setItemExpanded(ui.treeContents->topLevelItem(0), true);
00317     ui.treeContents->setItemSelected(item, true);
00318     currentItemChanged(item, selected);
00319   }
00320 }
00321 
00322 /** Called when the user clicks "Find Next". */
00323 void
00324 HelpBrowser::findNext()
00325 {
00326   find(true);
00327 }
00328 
00329 /** Called when the user clicks "Find Previous". */
00330 void
00331 HelpBrowser::findPrev()
00332 {
00333   find(false);
00334 }
00335 
00336 /** Searches the current page for the phrase in the Find box.
00337  *  Highlights the first instance found in the document
00338  *  \param forward true search forward if true, backward if false
00339  **/
00340 void
00341 HelpBrowser::find(bool forward)
00342 {
00343   /* Don't bother searching if there is no search phrase */
00344   if (ui.lineFind->text().isEmpty()) {
00345     return;
00346   }
00347   
00348   QTextDocument::FindFlags flags = 0;
00349   QTextCursor cursor = ui.txtBrowser->textCursor();
00350   QString searchPhrase = ui.lineFind->text();
00351   
00352   /* Clear status bar */
00353   this->statusBar()->clearMessage();
00354   
00355   /* Set search direction and other flags */
00356   if (!forward) {
00357     flags |= QTextDocument::FindBackward;
00358   }
00359   if (ui.chkbxMatchCase->isChecked()) {
00360     flags |= QTextDocument::FindCaseSensitively;
00361   }
00362   if (ui.chkbxWholePhrase->isChecked()) {
00363     flags |= QTextDocument::FindWholeWords;
00364   }
00365   
00366   /* Check if search phrase is the same as the previous */
00367   if (searchPhrase != _lastFind) {
00368     _foundBefore = false;
00369   }
00370   _lastFind = searchPhrase;
00371   
00372   /* Set the cursor to the appropriate start location if necessary */
00373   if (!cursor.hasSelection()) {
00374     if (forward) {
00375       cursor.movePosition(QTextCursor::Start);
00376     } else {
00377       cursor.movePosition(QTextCursor::End);
00378     }
00379     ui.txtBrowser->setTextCursor(cursor);
00380   }
00381 
00382   /* Search the page */
00383   QTextCursor found;
00384   found = ui.txtBrowser->document()->find(searchPhrase, cursor, flags);
00385   
00386   /* If found, move the cursor to the location */
00387   if (!found.isNull()) {
00388     ui.txtBrowser->setTextCursor(found);
00389   /* If not found, display appropriate error message */
00390   } else {
00391     if (_foundBefore) {
00392       if (forward) 
00393         this->statusBar()->showMessage(tr("Search reached end of document"));
00394       else 
00395         this->statusBar()->showMessage(tr("Search reached start of document"));
00396     } else {
00397       this->statusBar()->showMessage(tr("Text not found in document"));
00398     }
00399   }
00400   
00401   /* Even if not found this time, may have been found previously */
00402   _foundBefore |= !found.isNull();
00403 }
00404  
00405 /** Searches all help pages for the phrase the Search box.
00406  *  Fills treeSearch with documents containing matches and sets the
00407  *  status bar text appropriately.
00408  */
00409 void
00410 HelpBrowser::search()
00411 {
00412   /* Clear the list */
00413   ui.treeSearch->clear();
00414   
00415   /* Don't search if invalid document or blank search phrase */
00416   if (ui.lineSearch->text().isEmpty()) {
00417     return;
00418   }
00419     
00420   HelpTextBrowser browser;
00421   QTextCursor found;
00422   QTextDocument::FindFlags flags = QTextDocument::FindWholeWords;
00423 
00424   _lastSearch = ui.lineSearch->text();
00425 
00426   /* Search through all the pages looking for the phrase */
00427   for (int i=0; i < _elementList.size(); ++i) {
00428     /* Load page data into browser */
00429     browser.setSource(QUrl(getResourcePath(_elementList[i])));
00430       
00431     /* Search current document */
00432     found = browser.document()->find(ui.lineSearch->text(), 0, flags);
00433 
00434     /* If found, add page to tree */
00435     if (!found.isNull()) {
00436       ui.treeSearch->addTopLevelItem(createTopicTreeItem(_elementList[i], 0));
00437     }
00438   }
00439 
00440   /* Set the status bar text */
00441   this->statusBar()->showMessage(tr("Found %1 results")
00442                                 .arg(ui.treeSearch->topLevelItemCount()));
00443 }
00444 
00445 /** Overrides the default show method */
00446 void
00447 HelpBrowser::showWindow(QString topic)
00448 {
00449   
00450   /* Bring the window to the top */
00451   VidaliaWindow::showWindow();
00452 
00453   /* If a topic was specified, then go ahead and display it. */
00454   if (!topic.isEmpty()) {
00455     showTopic(topic);
00456   }
00457 }
00458