Create an add or edit domain settings action.
[PrivacyBrowserPC.git] / src / dialogs / DomainSettingsDialog.cpp
1 /*
2  * Copyright © 2022 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser PC <https://www.stoutner.com/privacy-browser-pc>.
5  *
6  * Privacy Browser PC is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser PC is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser PC.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 // Application headers.
21 #include "DomainSettingsDialog.h"
22 #include "Settings.h"
23 #include "ui_DomainSettingsDialog.h"
24 #include "helpers/DomainsDatabaseHelper.h"
25 #include "helpers/UserAgentHelper.h"
26
27 // Qt toolkit headers.
28 #include <QInputDialog>
29 #include <QMessageBox>
30 #include <QPushButton>
31
32 // Define the public static int constants.
33 const int DomainSettingsDialog::SHOW_ALL_DOMAINS = 0;
34 const int DomainSettingsDialog::ADD_DOMAIN = 1;
35 const int DomainSettingsDialog::EDIT_DOMAIN = 2;
36
37 DomainSettingsDialog::DomainSettingsDialog(const int &startType, const QString &domainName) : QDialog(nullptr)
38 {
39     // Instantiate the domain settings view UI.
40     Ui::DomainSettingsDialog domainSettingsDialogUi;
41
42     // Setup the UI.
43     domainSettingsDialogUi.setupUi(this);
44
45     // Get handles for the views.
46     domainsListViewPointer = domainSettingsDialogUi.domainsListView;
47     domainSettingsWidgetPointer = domainSettingsDialogUi.domainSettingsWidget;
48     domainNameLineEditPointer = domainSettingsDialogUi.domainNameLineEdit;
49     javaScriptComboBoxPointer = domainSettingsDialogUi.javaScriptComboBox;
50     javaScriptLabelPointer = domainSettingsDialogUi.javaScriptLabel;
51     localStorageComboBoxPointer = domainSettingsDialogUi.localStorageComboBox;
52     localStorageLabelPointer = domainSettingsDialogUi.localStorageLabel;
53     userAgentComboBoxPointer = domainSettingsDialogUi.userAgentComboBox;
54     userAgentLabelPointer = domainSettingsDialogUi.userAgentLabel;
55     zoomFactorComboBoxPointer = domainSettingsDialogUi.zoomFactorComboBox;
56     customZoomFactorSpinBoxPointer = domainSettingsDialogUi.customZoomFactorSpinBox;
57     QPushButton *addDomainButtonPointer = domainSettingsDialogUi.addDomainButton;
58     deleteDomainButtonPointer = domainSettingsDialogUi.deleteDomainButton;
59     QDialogButtonBox *dialogButtonBoxPointer = domainSettingsDialogUi.dialogButtonBox;
60     applyButtonPointer = dialogButtonBoxPointer->button(QDialogButtonBox::StandardButton::Apply);
61     resetButtonPointer = dialogButtonBoxPointer->button(QDialogButtonBox::StandardButton::Reset);
62
63     // Create a table model.
64     domainsTableModelPointer = new QSqlTableModel(nullptr, QSqlDatabase::database(DomainsDatabaseHelper::CONNECTION_NAME));
65
66     // Set the table for the model.
67     domainsTableModelPointer->setTable(DomainsDatabaseHelper::DOMAINS_TABLE);
68
69     // Set the edit strategy to be manual.
70     domainsTableModelPointer->setEditStrategy(QSqlTableModel::EditStrategy::OnManualSubmit);
71
72     // Sort the output alphabetically.
73     domainsTableModelPointer->setSort(1, Qt::SortOrder::AscendingOrder);
74
75     // Set the model for the list view.
76     domainsListViewPointer->setModel(domainsTableModelPointer);
77
78     // Set the visible column to be the domain name.
79     domainsListViewPointer->setModelColumn(1);
80
81     // Disable editing of the list view.
82     domainsListViewPointer->setEditTriggers(QAbstractItemView::NoEditTriggers);
83
84     // Read the data from the database and apply it to the table model.
85     domainsTableModelPointer->select();
86
87     // Setup the dialog according to the start type.
88     switch (startType)
89     {
90         case SHOW_ALL_DOMAINS:
91         {
92             // Select the first entry in the list view.
93             domainsListViewPointer->setCurrentIndex(domainsTableModelPointer->index(0, domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::DOMAIN_NAME)));
94
95             // Populate the domain settings.
96             domainSelected(domainsListViewPointer->selectionModel()->currentIndex());
97
98             break;
99         }
100
101         case ADD_DOMAIN:
102         {
103             // Add the new domain.
104             addDomain(domainName);
105
106             break;
107         }
108
109         case EDIT_DOMAIN:
110         {
111             // Find the index for the new domain.  `1` returns the first match.
112             QModelIndexList newDomainIndex = domainsTableModelPointer->match(domainsTableModelPointer->index(0, domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::DOMAIN_NAME)),
113                                                                         Qt::DisplayRole, domainName, 1, Qt::MatchWrap);
114
115             // Move to the new domain.
116             domainsListViewPointer->setCurrentIndex(newDomainIndex[0]);
117
118             // Populate the domain settings.
119             domainSelected(domainsListViewPointer->selectionModel()->currentIndex());
120         }
121     }
122
123     // Handle clicks on the domains.
124     connect(domainsListViewPointer, SIGNAL(activated(QModelIndex)), this, SLOT(domainSelected(QModelIndex)));
125
126     // Connect the domain settings.
127     connect(domainNameLineEditPointer, SIGNAL(textEdited(QString)), this, SLOT(domainNameChanged(QString)));
128     connect(javaScriptComboBoxPointer, SIGNAL(currentIndexChanged(int)), this, SLOT(javaScriptChanged(int)));
129     connect(localStorageComboBoxPointer, SIGNAL(currentIndexChanged(int)), this, SLOT(localStorageChanged(int)));
130     connect(userAgentComboBoxPointer, SIGNAL(currentTextChanged(QString)), this, SLOT(userAgentChanged(QString)));
131     connect(zoomFactorComboBoxPointer, SIGNAL(currentIndexChanged(int)), this, SLOT(zoomFactorComboBoxChanged(int)));
132     connect(customZoomFactorSpinBoxPointer, SIGNAL(valueChanged(double)), this, SLOT(customZoomFactorChanged(double)));
133
134     // Connect the buttons.
135     connect(addDomainButtonPointer, SIGNAL(released()), this, SLOT(showAddMessageBox()));
136     connect(deleteDomainButtonPointer, SIGNAL(released()), this, SLOT(showDeleteMessageBox()));
137     connect(resetButtonPointer, SIGNAL(released()), this, SLOT(reset()));
138     connect(dialogButtonBoxPointer, SIGNAL(accepted()), this, SLOT(ok()));
139     connect(applyButtonPointer, SIGNAL(released()), this, SLOT(apply()));
140     connect(dialogButtonBoxPointer, SIGNAL(rejected()), this, SLOT(cancel()));
141
142     // Update the UI.
143     updateUi();
144 }
145
146 void DomainSettingsDialog::addDomain(const QString &domainName) const
147 {
148     // Create a new domain record.
149     QSqlRecord newDomainRecord = QSqlDatabase::database(DomainsDatabaseHelper::CONNECTION_NAME).record(DomainsDatabaseHelper::DOMAINS_TABLE);
150
151     // Set the values for the new domain.
152     newDomainRecord.setValue(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::DOMAIN_NAME), domainName);
153     newDomainRecord.setValue(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::JAVASCRIPT), DomainsDatabaseHelper::SYSTEM_DEFAULT);
154     newDomainRecord.setValue(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::LOCAL_STORAGE), DomainsDatabaseHelper::SYSTEM_DEFAULT);
155     newDomainRecord.setValue(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::USER_AGENT), UserAgentHelper::SYSTEM_DEFAULT_DATABASE);
156     newDomainRecord.setValue(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::ZOOM_FACTOR), DomainsDatabaseHelper::SYSTEM_DEFAULT);
157     newDomainRecord.setValue(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::CUSTOM_ZOOM_FACTOR), 1.0);
158
159     // Insert the new domain.  `-1` appends it to the end.
160     domainsTableModelPointer->insertRecord(-1, newDomainRecord);
161
162     // Submit all pending changes.
163     domainsTableModelPointer->submitAll();
164
165     // Find the index for the new domain.  `-1` allows for multiple entries to be returned.
166     QModelIndexList newDomainIndex = domainsTableModelPointer->match(domainsTableModelPointer->index(0, domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::DOMAIN_NAME)),
167                                                                         Qt::DisplayRole, domainName, -1, Qt::MatchWrap);
168
169     // Move to the new domain.  If there are multiple domains with the same name, the new one should be the last in the list.
170     domainsListViewPointer->setCurrentIndex(newDomainIndex[newDomainIndex.size() - 1]);
171
172     // Populate the domain settings.
173     domainSelected(domainsListViewPointer->selectionModel()->currentIndex());
174
175     // Update the UI.
176     updateUi();
177 }
178
179
180 void DomainSettingsDialog::apply() const
181 {
182     // Get the current index.
183     QModelIndex currentIndex = domainsListViewPointer->currentIndex();
184
185     // Get the ID of the current index row.
186     QVariant currentId = currentIndex.siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::_ID)).data();
187
188     // Submit all pending changes.
189     domainsTableModelPointer->submitAll();
190
191     // Find the new index for the selected id.  The `1` keeps searching after the first match.
192     QModelIndexList newIndexList = domainsTableModelPointer->match(currentIndex.siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::_ID)), Qt::DisplayRole, currentId,
193                                                                    1, Qt::MatchWrap);
194
195     // Select the new index.
196     domainsListViewPointer->setCurrentIndex(newIndexList[0].siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::DOMAIN_NAME)));
197
198     // Update the UI.
199     updateUi();
200
201     // Emit the domain settings updated signal.
202     emit domainSettingsUpdated();
203 }
204
205 void DomainSettingsDialog::cancel()
206 {
207     // Revert all pending changes.
208     domainsTableModelPointer->revertAll();
209
210     // Close the dialog.
211     reject();
212 }
213
214 void DomainSettingsDialog::customZoomFactorChanged(const double &newValue) const
215 {
216     // Update the domains table model.
217     domainsTableModelPointer->setData(domainsListViewPointer->selectionModel()->currentIndex().siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::CUSTOM_ZOOM_FACTOR)),
218                                       newValue);
219
220     // Update the UI.
221     updateUi();
222 }
223
224 void DomainSettingsDialog::domainNameChanged(const QString &updatedDomainName) const
225 {
226     // Update the domains table model.
227     domainsTableModelPointer->setData(domainsListViewPointer->selectionModel()->currentIndex(), updatedDomainName);
228
229     // Update the UI.
230     updateUi();
231 }
232
233 void DomainSettingsDialog::domainSelected(const QModelIndex &modelIndex) const
234 {
235     // Populate the domain name line edit pointer.
236     domainNameLineEditPointer->setText(modelIndex.data().toString());
237
238     // Populate the JavaScript combo box.
239     javaScriptComboBoxPointer->setCurrentIndex(modelIndex.siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::JAVASCRIPT)).data().toInt());
240
241     // Populate the local storage combo box.
242     localStorageComboBoxPointer->setCurrentIndex(modelIndex.siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::LOCAL_STORAGE)).data().toInt());
243
244     // Get the user agent string.
245     QString userAgent = modelIndex.siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::USER_AGENT)).data().toString();
246
247     // Get the user agent index.
248     int userAgentIndex = UserAgentHelper::getDomainSettingsUserAgentIndex(userAgent);
249
250     // Set the user agent combo box index.
251     userAgentComboBoxPointer->setCurrentIndex(userAgentIndex);
252
253     // Set the custom user agent if specified.
254     if (userAgentIndex == -1) userAgentComboBoxPointer->setCurrentText(userAgent);
255
256     // Get the zoom factor combo box index.
257     int zoomFactorComboBoxIndex = modelIndex.siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::ZOOM_FACTOR)).data().toInt();
258
259     // Populate the zoom factor combo box.
260     zoomFactorComboBoxPointer->setCurrentIndex(zoomFactorComboBoxIndex);
261
262     // Populate the custom zoom factor spin box according to the zoom factor combo box.
263     if (zoomFactorComboBoxIndex == 0)  // System default zoom factor is selected.
264     {
265         // Display the default zoom factor.
266         customZoomFactorSpinBoxPointer->setValue(Settings::zoomFactor());
267     }
268     else  // Custom zoom factor is selected.
269     {
270         // Display the custom zoom factor from the domain settings.
271         customZoomFactorSpinBoxPointer->setValue(modelIndex.siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::CUSTOM_ZOOM_FACTOR)).data().toDouble());
272     }
273
274     // Set the initial status of the custom zoom factor spin box.
275     customZoomFactorSpinBoxPointer->setEnabled(zoomFactorComboBoxIndex);
276
277     // Populate the labels.
278     populateJavaScriptLabel();
279     populateLocalStorageLabel();
280     populateUserAgentLabel(userAgentComboBoxPointer->currentText());
281
282     // Update the UI.
283     updateUi();
284 }
285
286 void DomainSettingsDialog::javaScriptChanged(const int &newIndex) const
287 {
288     // Update the domains table model.
289     domainsTableModelPointer->setData(domainsListViewPointer->selectionModel()->currentIndex().siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::JAVASCRIPT)),
290                                       newIndex);
291
292     // Populate the JavaScript label.
293     populateJavaScriptLabel();
294
295     // Update the UI.
296     updateUi();
297 }
298
299 void DomainSettingsDialog::localStorageChanged(const int &newIndex) const
300 {
301     // Update the domains table model.
302     domainsTableModelPointer->setData(domainsListViewPointer->selectionModel()->currentIndex().siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::LOCAL_STORAGE)),
303                                       newIndex);
304
305     // Populate the local storage label.
306     populateLocalStorageLabel();
307
308     // Update the UI.
309     updateUi();
310 }
311
312 void DomainSettingsDialog::ok()
313 {
314     // Submit all pending changes.
315     domainsTableModelPointer->submitAll();
316
317     // Emit the domain settings updated signal.
318     domainSettingsUpdated();
319
320     // Close the dialog.
321     accept();
322 }
323
324 void DomainSettingsDialog::populateJavaScriptLabel() const
325 {
326     // Populate the label according to the currently selected index.
327     switch (javaScriptComboBoxPointer->currentIndex())
328     {
329         case (DomainsDatabaseHelper::SYSTEM_DEFAULT):
330         {
331             // Set the text according to the system default.
332             if (Settings::javaScript()) javaScriptLabelPointer->setText(i18nc("Domains settings label", "JavaScript enabled"));
333             else javaScriptLabelPointer->setText(i18nc("Domain settings label", "JavaScript disabled"));
334
335             break;
336         }
337
338         case (DomainsDatabaseHelper::DISABLED):
339         {
340             // Set the label text in bold.
341             javaScriptLabelPointer->setText(i18nc("Domain settings label.  The <strong> tags should be retained.", "<strong>JavaScript disabled</strong>"));
342
343             break;
344         }
345
346         case (DomainsDatabaseHelper::ENABLED):
347         {
348             // Set the label text in bold.
349             javaScriptLabelPointer->setText(i18nc("Domains settings label.  The <strong> tags should be retained.", "<strong>JavaScript enabled</strong>"));
350
351             break;
352         }
353     }
354 }
355
356 void DomainSettingsDialog::populateLocalStorageLabel() const
357 {
358     // Populate the label according to the currently selected index.
359     switch (localStorageComboBoxPointer->currentIndex())
360     {
361         case (DomainsDatabaseHelper::SYSTEM_DEFAULT):
362         {
363             // Set the text according to the system default.
364             if (Settings::localStorage()) localStorageLabelPointer->setText(i18nc("Local storage label", "Local storage enabled"));
365             else localStorageLabelPointer->setText(i18nc("Local storage label", "Local storage disabled"));
366
367             break;
368         }
369
370         case (DomainsDatabaseHelper::DISABLED):
371         {
372             // Set the label text in bold.
373             localStorageLabelPointer->setText(i18nc("Local storage label.  The <string> tags should be retained.", "<strong>Local storage disabled</strong>"));
374
375             break;
376         }
377
378         case (DomainsDatabaseHelper::ENABLED):
379         {
380             // Set the label text in bold.
381             localStorageLabelPointer->setText(i18nc("Local storage label.  The <strong> tags should be retained.", "<strong>Local storage enabled</strong>"));
382
383             break;
384         }
385     }
386 }
387
388 void DomainSettingsDialog::populateUserAgentLabel(const QString &userAgentName) const
389 {
390     // Populate the label according to the type.
391     if (userAgentName == UserAgentHelper::SYSTEM_DEFAULT_TRANSLATED)
392     {
393         // Display the system default user agent name.
394         userAgentLabelPointer->setText(UserAgentHelper::getTranslatedUserAgentName(Settings::userAgent()));
395     }
396     else
397     {
398         // Display the user agent name in bold.
399         userAgentLabelPointer->setText("<strong>" + userAgentName + "</strong>");
400     }
401 }
402
403 void DomainSettingsDialog::reset() const
404 {
405     // Cancel all pending changes.
406     domainsTableModelPointer->revertAll();
407
408     // Repopulate the domain settings.
409     domainSelected(domainsListViewPointer->currentIndex());
410
411     // Update the UI.
412     updateUi();
413 }
414
415 void DomainSettingsDialog::showAddMessageBox()
416 {
417     // Create an OK flag.
418     bool okClicked;
419
420     // Display a dialog to request the new domain name from the user.
421     QString newDomainName = QInputDialog::getText(this, i18nc("Add domain dialog title", "Add Domain"),
422                                                   i18nc("Add domain message.  The \n\n are newline codes that should be retained",
423                                                         "Add a new domain.  Doing so will also save any pending changes that have been made to other domains.\n\n"
424                                                         "*. may be prepended to a domain to include all subdomains (eg. *.stoutner.com)."),
425                                                   QLineEdit::Normal, QString(), &okClicked);
426
427     // Add the new domain if the user clicked OK.
428     if (okClicked) addDomain(newDomainName);
429 }
430
431 void DomainSettingsDialog::showDeleteMessageBox() const
432 {
433     // Instantiate a delete dialog message box.
434     QMessageBox deleteDialogMessageBox;
435
436     // Set the icon.
437     deleteDialogMessageBox.setIcon(QMessageBox::Warning);
438
439     // Set the window title.
440     deleteDialogMessageBox.setWindowTitle(i18nc("Delete domain dialog title", "Delete Domain"));
441
442     // Set the text.
443     deleteDialogMessageBox.setText(i18nc("Delete domain main message", "Delete the current domain?"));
444
445     // Set the informative text.
446     deleteDialogMessageBox.setInformativeText(i18nc("Delete domain secondary message", "Doing so will also save any pending changes that have been made to other domains."));
447
448     // Set the standard buttons.
449     deleteDialogMessageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
450
451     // Set the default button.
452     deleteDialogMessageBox.setDefaultButton(QMessageBox::No);
453
454     // Display the dialog and capture the return value.
455     int returnValue = deleteDialogMessageBox.exec();
456
457     if (returnValue == QMessageBox::Yes)
458     {
459         // Get the current index.
460         QModelIndex currentIndex = domainsListViewPointer->currentIndex();
461
462         // Delete the current row.
463         domainsTableModelPointer->removeRow(domainsListViewPointer->selectionModel()->currentIndex().row());
464
465         // Submit all pending changes.
466         domainsTableModelPointer->submitAll();
467
468         // Select the row next to the deleted item if one exists.
469         if (domainsTableModelPointer->rowCount() > 0)
470         {
471             // Check the row of the deleted item.
472             if (currentIndex.row() == 0)  // The first row was deleted.
473             {
474                 // Reselect the current index.
475                 domainsListViewPointer->setCurrentIndex(currentIndex);
476             }
477             else  // A subsequent row was deleted.
478             {
479                 // Select the crow above the deleted itemm.
480                 domainsListViewPointer->setCurrentIndex(currentIndex.siblingAtRow(currentIndex.row() - 1));
481             }
482
483             // Populate the domain settings.
484             domainSelected(domainsListViewPointer->currentIndex());
485         }
486
487         // Update the Ui.
488         updateUi();
489     }
490 }
491
492 void DomainSettingsDialog::updateUi() const
493 {
494     // Update the delete button status.
495     deleteDomainButtonPointer->setEnabled(domainsListViewPointer->selectionModel()->hasSelection());
496
497     // Update the apply button status.
498     applyButtonPointer->setEnabled(domainsTableModelPointer->isDirty());
499
500     // Update the reset button status.
501     resetButtonPointer->setEnabled(domainsTableModelPointer->isDirty());
502
503     // Display the domain settings if there is at least one domain.
504     domainSettingsWidgetPointer->setVisible(domainsTableModelPointer->rowCount() > 0);
505 }
506
507 void DomainSettingsDialog::userAgentChanged(const QString &updatedUserAgent) const
508 {
509     // Update the domains table model.
510     domainsTableModelPointer->setData(domainsListViewPointer->selectionModel()->currentIndex().siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::USER_AGENT)),
511                                       UserAgentHelper::getDatabaseUserAgentName(updatedUserAgent));
512
513     // Populate the user agent label.
514     populateUserAgentLabel(updatedUserAgent);
515
516     // Update the UI.
517     updateUi();
518 }
519
520 void DomainSettingsDialog::zoomFactorComboBoxChanged(const int &newIndex) const
521 {
522     // Get the current model index.
523     QModelIndex modelIndex = domainsListViewPointer->selectionModel()->currentIndex();
524
525     // Update the domains table model.
526     domainsTableModelPointer->setData(modelIndex.siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::ZOOM_FACTOR)), newIndex);
527
528     // Populate the custom zoom factor spin box according to the zoom factor combo box.
529     if (newIndex == 0)  // System default zoom factor is selected.
530     {
531         // Display the default zoom factor.
532         customZoomFactorSpinBoxPointer->setValue(Settings::zoomFactor());
533     }
534     else  // Custom zoom factor is selected.
535     {
536         // Display the custom zoom factor from the domain settings.
537         customZoomFactorSpinBoxPointer->setValue(modelIndex.siblingAtColumn(domainsTableModelPointer->fieldIndex(DomainsDatabaseHelper::CUSTOM_ZOOM_FACTOR)).data().toDouble());
538     }
539
540     // Update the status of the custom zoom factor spin box.
541     customZoomFactorSpinBoxPointer->setEnabled(newIndex);
542
543     // Update the UI.
544     updateUi();
545 }