Vidalia  0.2.21
HelpBrowser.cpp
Go to the documentation of this file.
1 /*
2 ** This file is part of Vidalia, and is subject to the license terms in the
3 ** LICENSE file, found in the top level directory of this distribution. If you
4 ** did not receive the LICENSE file with this file, you may obtain it from the
5 ** Vidalia source package distributed by the Vidalia Project at
6 ** http://www.torproject.org/projects/vidalia.html. No part of Vidalia,
7 ** including this file, may be copied, modified, propagated, or distributed
8 ** except according to the terms described in the LICENSE file.
9 */
10 
11 /*
12 ** \file HelpBrowser.cpp
13 ** \brief Displays a list of help topics and content
14 */
15 
16 #include "HelpBrowser.h"
17 #include "Vidalia.h"
18 
19 #include <QDomDocument>
20 #include <QDir>
21 
22 #define LEFT_PANE_INDEX 0
23 #define NO_STRETCH 0
24 #define MINIMUM_PANE_SIZE 1
25 
26 /* Names of elements and attributes in the XML file */
27 #define ELEMENT_CONTENTS "Contents"
28 #define ELEMENT_TOPIC "Topic"
29 #define ATTRIBUTE_TOPIC_ID "id"
30 #define ATTRIBUTE_TOPIC_HTML "html"
31 #define ATTRIBUTE_TOPIC_NAME "name"
32 #define ATTRIBUTE_TOPIC_SECTION "section"
33 
34 /* Define two roles used to store data associated with a topic item */
35 #define ROLE_TOPIC_ID Qt::UserRole
36 #define ROLE_TOPIC_QRC_PATH (Qt::UserRole+1)
37 
38 
39 /** Constuctor. This will probably do more later */
40 HelpBrowser::HelpBrowser(QWidget *parent)
41  : VidaliaWindow("HelpBrowser", parent)
42 {
43  VidaliaSettings settings;
44 
45  /* Invoke Qt Designer generated QObject setup routine */
46  ui.setupUi(this);
47 #if defined(Q_WS_MAC)
48  ui.actionHome->setShortcut(QString("Shift+Ctrl+H"));
49 #endif
50 
51  /* Pressing 'Esc' or 'Ctrl+W' will close the window */
52  ui.actionClose->setShortcut(QString("Esc"));
53  Vidalia::createShortcut("Ctrl+W", this, ui.actionClose, SLOT(trigger()));
54 
55  /* Hide Search frame */
56  ui.frmFind->setHidden(true);
57 
58  /* Set the splitter pane sizes so that only the txtBrowser pane expands
59  * and set to arbitrary sizes (the minimum sizes will take effect */
60  QList<int> sizes;
61  sizes.append(MINIMUM_PANE_SIZE);
62  sizes.append(MINIMUM_PANE_SIZE);
63  ui.splitter->setSizes(sizes);
64  ui.splitter->setStretchFactor(LEFT_PANE_INDEX, NO_STRETCH);
65 
66  connect(ui.treeContents,
67  SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)),
68  this, SLOT(contentsItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)));
69 
70  connect(ui.treeSearch,
71  SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)),
72  this, SLOT(searchItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)));
73 
74  /* Connect the navigation actions to their slots */
75  connect(ui.actionHome, SIGNAL(triggered()), ui.txtBrowser, SLOT(home()));
76  connect(ui.actionBack, SIGNAL(triggered()), ui.txtBrowser, SLOT(backward()));
77  connect(ui.actionForward, SIGNAL(triggered()), ui.txtBrowser, SLOT(forward()));
78  connect(ui.txtBrowser, SIGNAL(backwardAvailable(bool)),
79  ui.actionBack, SLOT(setEnabled(bool)));
80  connect(ui.txtBrowser, SIGNAL(forwardAvailable(bool)),
81  ui.actionForward, SLOT(setEnabled(bool)));
82  connect(ui.btnFindNext, SIGNAL(clicked()), this, SLOT(findNext()));
83  connect(ui.btnFindPrev, SIGNAL(clicked()), this, SLOT(findPrev()));
84  connect(ui.btnSearch, SIGNAL(clicked()), this, SLOT(search()));
85 
86  /* Load the help topics from XML */
87  loadContentsFromXml(":/help/" + language() + "/contents.xml");
88 
89  /* Show the first help topic in the tree */
90  ui.treeContents->setCurrentItem(ui.treeContents->topLevelItem(0));
91  ui.treeContents->setItemExpanded(ui.treeContents->topLevelItem(0), true);
92 }
93 
94 /** Called when the user changes the UI translation. */
95 void
97 {
98  ui.retranslateUi(this);
99  ui.treeContents->clear();
100  loadContentsFromXml(":/help/" + language() + "/contents.xml");
101  ui.treeContents->setItemExpanded(ui.treeContents->topLevelItem(0), true);
102  ui.treeContents->setCurrentItem(ui.treeContents->topLevelItem(0));
103  ui.treeContents->setItemExpanded(ui.treeContents->topLevelItem(0), true);
104 }
105 
106 /** Returns the language in which help topics should appear, or English
107  * ("en") if no translated help files exist for the current GUI language. */
108 QString
110 {
111  QString lang = Vidalia::language();
112  if (!QDir(":/help/" + lang).exists())
113  lang = "en";
114  return lang;
115 }
116 
117 /** Load the contents of the help topics tree from the specified XML file. */
118 void
120 {
121  QString errorString;
122  QFile file(xmlFile);
123  QDomDocument document;
124 
125  /* Load the XML contents into the DOM document */
126  if (!document.setContent(&file, true, &errorString)) {
127  ui.txtBrowser->setPlainText(tr("Error Loading Help Contents: ")+errorString);
128  return;
129  }
130  /* Load the DOM document contents into the tree view */
131  if (!loadContents(&document, errorString)) {
132  ui.txtBrowser->setPlainText(tr("Error Loading Help Contents: ")+errorString);
133  return;
134  }
135 }
136 
137 /** Load the contents of the help topics tree from the given DOM document. */
138 bool
139 HelpBrowser::loadContents(const QDomDocument *document, QString &errorString)
140 {
141  /* Grab the root document element and make sure it's the right one */
142  QDomElement root = document->documentElement();
143  if (root.tagName() != ELEMENT_CONTENTS) {
144  errorString = tr("Supplied XML file is not a valid Contents document.");
145  return false;
146  }
147  _elementList << root;
148 
149  /* Create the home item */
150  QTreeWidgetItem *home = createTopicTreeItem(root, 0);
151  ui.treeContents->addTopLevelItem(home);
152 
153  /* Process all top-level help topics */
154  QDomElement child = root.firstChildElement(ELEMENT_TOPIC);
155  while (!child.isNull()) {
156  parseHelpTopic(child, home);
157  child = child.nextSiblingElement(ELEMENT_TOPIC);
158  }
159  return true;
160 }
161 
162 /** Parse a Topic element and handle all its children recursively. */
163 void
164 HelpBrowser::parseHelpTopic(const QDomElement &topicElement,
165  QTreeWidgetItem *parent)
166 {
167  /* Check that we have a valid help topic */
168  if (isValidTopicElement(topicElement)) {
169  /* Save this element for later (used for searching) */
170  _elementList << topicElement;
171 
172  /* Create and populate the new topic item in the tree */
173  QTreeWidgetItem *topic = createTopicTreeItem(topicElement, parent);
174 
175  /* Process all its child elements */
176  QDomElement child = topicElement.firstChildElement(ELEMENT_TOPIC);
177  while (!child.isNull()) {
178  parseHelpTopic(child, topic);
179  child = child.nextSiblingElement(ELEMENT_TOPIC);
180  }
181  }
182 }
183 
184 /** Returns true if the given Topic element has the necessary attributes. */
185 bool
186 HelpBrowser::isValidTopicElement(const QDomElement &topicElement)
187 {
188  return (topicElement.hasAttribute(ATTRIBUTE_TOPIC_ID) &&
189  topicElement.hasAttribute(ATTRIBUTE_TOPIC_NAME) &&
190  topicElement.hasAttribute(ATTRIBUTE_TOPIC_HTML));
191 }
192 
193 /** Builds a resource path to an html file associated with the given help
194  * topic. If the help topic needs an achor, the anchor will be formatted and
195  * appended. */
196 QString
197 HelpBrowser::getResourcePath(const QDomElement &topicElement)
198 {
199  QString link = language() + "/" + topicElement.attribute(ATTRIBUTE_TOPIC_HTML);
200  if (topicElement.hasAttribute(ATTRIBUTE_TOPIC_SECTION)) {
201  link += "#" + topicElement.attribute(ATTRIBUTE_TOPIC_SECTION);
202  }
203  return link;
204 }
205 
206 /** Creates a new element to be inserted into the topic tree. */
207 QTreeWidgetItem*
208 HelpBrowser::createTopicTreeItem(const QDomElement &topicElement,
209  QTreeWidgetItem *parent)
210 {
211  QTreeWidgetItem *topic = new QTreeWidgetItem(parent);
212  QString label = topicElement.attribute(ATTRIBUTE_TOPIC_NAME);
213 
214  topic->setText(0, label);
215  topic->setToolTip(0, label);
216  topic->setData(0, ROLE_TOPIC_ID, topicElement.attribute(ATTRIBUTE_TOPIC_ID));
217  topic->setData(0, ROLE_TOPIC_QRC_PATH, getResourcePath(topicElement));
218 
219  return topic;
220 }
221 
222 /** Called when the user selects a different item in the content topic tree */
223 void
224 HelpBrowser::contentsItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *prev)
225 {
226  QList<QTreeWidgetItem *> selected = ui.treeSearch->selectedItems();
227  /* Deselect the selection in the search tree */
228  if (!selected.isEmpty()) {
229  ui.treeSearch->setItemSelected(selected[0], false);
230  }
231  currentItemChanged(current, prev);
232 }
233 
234 /** Called when the user selects a different item in the content topic tree */
235 void
236 HelpBrowser::searchItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *prev)
237 {
238  QList<QTreeWidgetItem *> selected = ui.treeContents->selectedItems();
239  /* Deselect the selection in the contents tree */
240  if (!selected.isEmpty()) {
241  ui.treeContents->setItemSelected(selected[0], false);
242  }
243 
244  /* Change to selected page */
245  currentItemChanged(current, prev);
246 
247  /* Highlight search phrase */
248  QTextCursor found;
249  QTextDocument::FindFlags flags = QTextDocument::FindWholeWords;
250  found = ui.txtBrowser->document()->find(_lastSearch, 0, flags);
251  if (!found.isNull()) {
252  ui.txtBrowser->setTextCursor(found);
253  }
254 }
255 
256 /** Called when the user selects a different item in the tree. */
257 void
258 HelpBrowser::currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *prev)
259 {
260  Q_UNUSED(prev);
261  if (current) {
262  ui.txtBrowser->setSource(QUrl(current->data(0,
263  ROLE_TOPIC_QRC_PATH).toString()));
264  }
265  _foundBefore = false;
266 }
267 
268 /** Searches for a topic in the topic tree. Returns a pointer to that topics
269  * item in the topic tree if it is found, 0 otherwise. */
270 QTreeWidgetItem*
271 HelpBrowser::findTopicItem(QTreeWidgetItem *startItem, QString topic)
272 {
273  /* If startItem is null, then we don't know where to start searching. */
274  if (!startItem)
275  return 0;
276 
277  /* Parse the first subtopic in the topic id. */
278  QString subtopic = topic.mid(0, topic.indexOf(".")).toLower();
279 
280  /* Search through all children of startItem and look for a subtopic match */
281  for (int i = 0; i < startItem->childCount(); i++) {
282  QTreeWidgetItem *item = startItem->child(i);
283 
284  if (subtopic == item->data(0, ROLE_TOPIC_ID).toString().toLower()) {
285  /* Found a subtopic match, so expand this item */
286  ui.treeContents->setItemExpanded(item, true);
287  if (!topic.contains(".")) {
288  /* Found the exact topic */
289  return item;
290  }
291  /* Search recursively for the next subtopic */
292  return findTopicItem(item, topic.mid(topic.indexOf(".")+1));
293  }
294  }
295  return 0;
296 }
297 
298 /** Shows the help browser. If a sepcified topic was given, then search for
299  * that topic's ID (e.g., "log.basic") and display the appropriate page. */
300 void
302 {
303  /* Search for the topic in the contents tree */
304  QTreeWidgetItem *item =
305  findTopicItem(ui.treeContents->topLevelItem(0), topic);
306  QTreeWidgetItem *selected = 0;
307 
308  if (item) {
309  /* Item was found, so show its location in the hierarchy and select its
310  * tree item. */
311  if (ui.treeContents->selectedItems().size()) {
312  selected = ui.treeContents->selectedItems()[0];
313  if (selected)
314  ui.treeContents->setItemSelected(selected, false);
315  }
316  ui.treeContents->setItemExpanded(ui.treeContents->topLevelItem(0), true);
317  ui.treeContents->setItemSelected(item, true);
318  currentItemChanged(item, selected);
319  }
320 }
321 
322 /** Called when the user clicks "Find Next". */
323 void
325 {
326  find(true);
327 }
328 
329 /** Called when the user clicks "Find Previous". */
330 void
332 {
333  find(false);
334 }
335 
336 /** Searches the current page for the phrase in the Find box.
337  * Highlights the first instance found in the document
338  * \param forward true search forward if true, backward if false
339  **/
340 void
341 HelpBrowser::find(bool forward)
342 {
343  /* Don't bother searching if there is no search phrase */
344  if (ui.lineFind->text().isEmpty()) {
345  return;
346  }
347 
348  QTextDocument::FindFlags flags = 0;
349  QTextCursor cursor = ui.txtBrowser->textCursor();
350  QString searchPhrase = ui.lineFind->text();
351 
352  /* Clear status bar */
353  this->statusBar()->clearMessage();
354 
355  /* Set search direction and other flags */
356  if (!forward) {
357  flags |= QTextDocument::FindBackward;
358  }
359  if (ui.chkbxMatchCase->isChecked()) {
360  flags |= QTextDocument::FindCaseSensitively;
361  }
362  if (ui.chkbxWholePhrase->isChecked()) {
363  flags |= QTextDocument::FindWholeWords;
364  }
365 
366  /* Check if search phrase is the same as the previous */
367  if (searchPhrase != _lastFind) {
368  _foundBefore = false;
369  }
370  _lastFind = searchPhrase;
371 
372  /* Set the cursor to the appropriate start location if necessary */
373  if (!cursor.hasSelection()) {
374  if (forward) {
375  cursor.movePosition(QTextCursor::Start);
376  } else {
377  cursor.movePosition(QTextCursor::End);
378  }
379  ui.txtBrowser->setTextCursor(cursor);
380  }
381 
382  /* Search the page */
383  QTextCursor found;
384  found = ui.txtBrowser->document()->find(searchPhrase, cursor, flags);
385 
386  /* If found, move the cursor to the location */
387  if (!found.isNull()) {
388  ui.txtBrowser->setTextCursor(found);
389  /* If not found, display appropriate error message */
390  } else {
391  if (_foundBefore) {
392  if (forward)
393  this->statusBar()->showMessage(tr("Search reached end of document"));
394  else
395  this->statusBar()->showMessage(tr("Search reached start of document"));
396  } else {
397  this->statusBar()->showMessage(tr("Text not found in document"));
398  }
399  }
400 
401  /* Even if not found this time, may have been found previously */
402  _foundBefore |= !found.isNull();
403 }
404 
405 /** Searches all help pages for the phrase the Search box.
406  * Fills treeSearch with documents containing matches and sets the
407  * status bar text appropriately.
408  */
409 void
411 {
412  /* Clear the list */
413  ui.treeSearch->clear();
414 
415  /* Don't search if invalid document or blank search phrase */
416  if (ui.lineSearch->text().isEmpty()) {
417  return;
418  }
419 
420  HelpTextBrowser browser;
421  QTextCursor found;
422  QTextDocument::FindFlags flags = QTextDocument::FindWholeWords;
423 
424  _lastSearch = ui.lineSearch->text();
425 
426  /* Search through all the pages looking for the phrase */
427  for (int i=0; i < _elementList.size(); ++i) {
428  /* Load page data into browser */
429  browser.setSource(QUrl(getResourcePath(_elementList[i])));
430 
431  /* Search current document */
432  found = browser.document()->find(ui.lineSearch->text(), 0, flags);
433 
434  /* If found, add page to tree */
435  if (!found.isNull()) {
436  ui.treeSearch->addTopLevelItem(createTopicTreeItem(_elementList[i], 0));
437  }
438  }
439 
440  /* Set the status bar text */
441  this->statusBar()->showMessage(tr("Found %1 results")
442  .arg(ui.treeSearch->topLevelItemCount()));
443 }
444 
445 /** Overrides the default show method */
446 void
448 {
449 
450  /* Bring the window to the top */
452 
453  /* If a topic was specified, then go ahead and display it. */
454  if (!topic.isEmpty()) {
455  showTopic(topic);
456  }
457 }
458