]> gitweb.stoutner.com Git - PrivacyBrowserPC.git/blob - src/dialogs/BookmarksDialog.cpp
Add bookmark folders.
[PrivacyBrowserPC.git] / src / dialogs / BookmarksDialog.cpp
1 /*
2  * Copyright 2023 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 "BookmarksDialog.h"
22 #include "ui_BookmarksDialog.h"
23 #include "databases/BookmarksDatabase.h"
24 #include "dialogs/AddBookmarkDialog.h"
25 #include "dialogs/AddFolderDialog.h"
26 #include "dialogs/EditBookmarkDialog.h"
27 #include "dialogs/EditFolderDialog.h"
28
29 // KDE Frameworks headers.
30 #include <KLocalizedString>
31
32 // Qt toolkit headers.
33 #include <QDebug>
34 #include <QStandardItemModel>
35
36 // Construct the class.
37 BookmarksDialog::BookmarksDialog(QString currentWebsiteTitle, QString currentWebsiteUrl, QIcon currentWebsiteFavorieIcon) :
38                                  QDialog(nullptr), websiteFavoriteIcon(currentWebsiteFavorieIcon), websiteTitle(currentWebsiteTitle), websiteUrl(currentWebsiteUrl)
39 {
40     // Set the dialog window title.
41     setWindowTitle(i18nc("The bookmarks dialog window title", "Bookmarks"));
42
43     // Set the window modality.
44     setWindowModality(Qt::WindowModality::ApplicationModal);
45
46     // Instantiate the bookmarks settings dialog UI.
47     Ui::BookmarksDialog bookmarksDialogUi;
48
49     // Setup the UI.
50     bookmarksDialogUi.setupUi(this);
51
52     // Get a handle for the draggable tree view.
53     draggableTreeViewPointer = bookmarksDialogUi.draggableTreeView;
54
55     // Initialize the tree model.
56     treeModelPointer = new QStandardItemModel();
57
58     // Auto resize the headers.
59     draggableTreeViewPointer->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
60
61     // Indicate that all the rows are the same height, which improves performance.
62     draggableTreeViewPointer->setUniformRowHeights(true);
63
64     // Set the selection mode to allow multiple rows to be selected at once.
65     draggableTreeViewPointer->setSelectionMode(QAbstractItemView::ExtendedSelection);
66
67     // Allow dragging of bookmarks to reorder.
68     draggableTreeViewPointer->setDragDropMode(QAbstractItemView::InternalMove);
69
70     // Set the tree model.
71     draggableTreeViewPointer->setModel(treeModelPointer);
72
73     // Get a handle for the tree selection model.
74     treeSelectionModelPointer = draggableTreeViewPointer->selectionModel();
75
76     // Listen for selection changes.
77     connect(treeSelectionModelPointer, SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(updateSelection()));
78
79     // Repopulate the bookmarks when they are moved.
80     connect(draggableTreeViewPointer, SIGNAL(bookmarksMoved()), this, SLOT(refreshBookmarks()));
81
82     // Get handles for the buttons.
83     QPushButton *addBookmarkButtonPointer = bookmarksDialogUi.addBookmarkButton;
84     QPushButton *addFolderButtonPointer = bookmarksDialogUi.addFolderButton;
85     editButtonPointer = bookmarksDialogUi.editButton;
86     deleteItemsButtonPointer = bookmarksDialogUi.deleteItemsButton;
87     QDialogButtonBox *dialogButtonBoxPointer = bookmarksDialogUi.dialogButtonBox;
88     QPushButton *closeButtonPointer = dialogButtonBoxPointer->button(QDialogButtonBox::Close);
89
90     // Connect the buttons.
91     connect(addBookmarkButtonPointer, SIGNAL(clicked()), this, SLOT(showAddBookmarkDialog()));
92     connect(addFolderButtonPointer, SIGNAL(clicked()), this, SLOT(showAddFolderDialog()));
93     connect(editButtonPointer, SIGNAL(clicked()), this, SLOT(showEditDialog()));
94     connect(deleteItemsButtonPointer, SIGNAL(clicked()), this, SLOT(deleteItems()));
95     connect(dialogButtonBoxPointer, SIGNAL(rejected()), this, SLOT(reject()));
96
97     // Set the close button to be the default.
98     closeButtonPointer->setDefault(true);
99
100     // Monitor editing of data in the tree model.
101     connect(treeModelPointer, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(updateBookmarkFromTree(QStandardItem*)));
102
103     // Populate the bookmarks.
104     populateBookmarks();
105 }
106
107 void BookmarksDialog::deleteItems() const
108 {
109     // Create a parent folder ID standard C++ list (which can be sorted and from which duplicates can be removed).
110     std::list<double> parentFolderIdList;
111
112     // Get the list of selected model indexes.
113     QList<QModelIndex> selectedModelIndexList = treeSelectionModelPointer->selectedRows(DATABASE_ID_COLUMN);
114
115     // Delete each of the bookmarks.
116     for (const QModelIndex &modelIndex : selectedModelIndexList)
117     {
118         // Add the parent folder ID to the list.
119         parentFolderIdList.push_back(modelIndex.parent().siblingAtColumn(FOLDER_ID_COLUMN).data().toDouble());
120
121         // Delete the bookmark.
122         BookmarksDatabase::deleteBookmark(modelIndex.data().toInt());
123     }
124
125     // Sort the parent folder ID.
126     parentFolderIdList.sort();
127
128     // Remove duplicates from the parent folder ID list.
129     parentFolderIdList.unique();
130
131     // Update the folder contents display order for each folder with a deleted bookmark.
132     for (const double parentFolderId : parentFolderIdList)
133         BookmarksDatabase::updateFolderContentsDisplayOrder(parentFolderId);
134
135     // Repopulate the bookmarks in this dialog
136     populateBookmarks();
137
138     // Emit the bookmark updated signal to redraw the bookmarks in the menu and toolbar.
139     emit bookmarkUpdated();
140 }
141
142 void BookmarksDialog::populateBookmarks() const
143 {
144     // Clear the current contents of the tree model.
145     treeModelPointer->clear();
146
147     // Set the column count.
148     treeModelPointer->setColumnCount(6);
149
150     // Set the tree header data.
151     treeModelPointer->setHeaderData(NAME_COLUMN, Qt::Horizontal, i18nc("The bookmark Name header.", "Name"));
152     treeModelPointer->setHeaderData(URL_COLUMN, Qt::Horizontal, i18nc("The bookmark URL header.", "URL"));
153
154     // Hide the backend columns.
155     draggableTreeViewPointer->setColumnHidden(DATABASE_ID_COLUMN, true);
156     draggableTreeViewPointer->setColumnHidden(DISPLAY_ORDER_COLUMN, true);
157     draggableTreeViewPointer->setColumnHidden(IS_FOLDER_COLUMN, true);
158     draggableTreeViewPointer->setColumnHidden(FOLDER_ID_COLUMN, true);
159
160     // Create a bookmarks root item list.
161     QList<QStandardItem*> bookmarksRootItemList;
162
163     // Create the root items.
164     QStandardItem *rootItemNamePointer = new QStandardItem(QIcon::fromTheme("bookmarks"), i18nc("The bookmarks root tree widget name", "Bookmarks"));
165     QStandardItem *rootItemUrlPointer = new QStandardItem(QLatin1String(""));
166     QStandardItem *rootItemDatabaseIdPonter = new QStandardItem(QLatin1String("-1"));  // The root item doesn't have a database ID.
167     QStandardItem *rootItemDisplayOrderPointer = new QStandardItem(QLatin1String("-1"));  // The root item doesn't have a display order.
168     QStandardItem *rootItemIsFolderPointer = new QStandardItem(QLatin1String("1"));
169     QStandardItem *rootItemFolderIdPointer = new QStandardItem(QLatin1String("0"));
170
171     // Enable dropping on the root name column.
172     rootItemNamePointer->setDropEnabled(true);
173
174     // Disable dropping on the URL.
175     rootItemUrlPointer->setDropEnabled(false);
176
177     // Disable dragging of the bookmarks root item.
178     rootItemNamePointer->setDragEnabled(false);
179     rootItemUrlPointer->setDragEnabled(false);
180
181     // Disable selecting the URL.
182     rootItemUrlPointer->setSelectable(false);
183
184     // Populate the bookmarks root item list.
185     bookmarksRootItemList.append(rootItemNamePointer);
186     bookmarksRootItemList.append(rootItemUrlPointer);
187     bookmarksRootItemList.append(rootItemDatabaseIdPonter);
188     bookmarksRootItemList.append(rootItemDisplayOrderPointer);
189     bookmarksRootItemList.append(rootItemIsFolderPointer);
190     bookmarksRootItemList.append(rootItemFolderIdPointer);
191
192     // Add the bookmarks root item to the tree.
193     treeModelPointer->appendRow(bookmarksRootItemList);
194
195     // Populate the subfolders, starting with the root folder ID (`0`).
196     populateSubfolders(rootItemNamePointer, 0);
197
198     // Expand all the folder.
199     draggableTreeViewPointer->expandAll();
200
201     // Update the UI.
202     updateUi();
203 }
204
205 void BookmarksDialog::populateSubfolders(QStandardItem *folderItemNamePointer, const double folderId) const
206 {
207     // Get the folder contents.
208     QList<BookmarkStruct> *folderContentsListPointer = BookmarksDatabase::getFolderContents(folderId);
209
210     // Populate the bookmarks tree view.
211     for (const BookmarkStruct &bookmarkStruct : *folderContentsListPointer)
212     {
213         // Create a bookmark item list.
214         QList<QStandardItem*> bookmarkItemList;
215
216         // Create the bookmark items.
217         QStandardItem *nameItemPointer = new QStandardItem(bookmarkStruct.favoriteIcon, bookmarkStruct.name);
218         QStandardItem *urlItemPointer = new QStandardItem(bookmarkStruct.url);
219         QStandardItem *databaseIdItemPointer = new QStandardItem(QString::number(bookmarkStruct.databaseId));
220         QStandardItem *displayOrderItemPointer = new QStandardItem(QString::number(bookmarkStruct.displayOrder));
221         QStandardItem *isFolderItemPointer = new QStandardItem(QString::number(bookmarkStruct.isFolder));
222         QStandardItem *folderIdItemPointer = new QStandardItem(QString::number(bookmarkStruct.folderId, 'f', 0));  // Format the folder ID as a floating point with no trailing zeros.
223
224         // Enable dragging and dropping of the name column.
225         nameItemPointer->setDragEnabled(true);
226
227         // Only allow dropping on the name if this is a folder.
228         nameItemPointer->setDropEnabled(bookmarkStruct.isFolder);
229
230         // Disable dragging and dropping on the URL.
231         urlItemPointer->setDragEnabled(false);
232         urlItemPointer->setDropEnabled(false);
233
234         // Disable selecting the URL.
235         urlItemPointer->setSelectable(false);
236
237         // Populate the bookmark item list.
238         bookmarkItemList.append(nameItemPointer);
239         bookmarkItemList.append(urlItemPointer);
240         bookmarkItemList.append(databaseIdItemPointer);
241         bookmarkItemList.append(displayOrderItemPointer);
242         bookmarkItemList.append(isFolderItemPointer);
243         bookmarkItemList.append(folderIdItemPointer);
244
245         // Add the bookmark to the parent folder.
246         folderItemNamePointer->appendRow(bookmarkItemList);
247
248         // Populate subfolders if this item is a folder.
249         if (bookmarkStruct.isFolder)
250             populateSubfolders(nameItemPointer, bookmarkStruct.folderId);
251     }
252 }
253
254 void BookmarksDialog::refreshBookmarks() const
255 {
256     // Repopulate the bookmarks in this dialog
257     populateBookmarks();
258
259     // Emit the bookmark updated signal to redraw the bookmarks in the menu and toolbar.
260     emit bookmarkUpdated();
261 }
262
263 void BookmarksDialog::selectSubfolderContents(const QModelIndex &parentModelIndex) const
264 {
265     // Get the index model.
266     const QAbstractItemModel *modelIndexAbstractItemPointer = parentModelIndex.model();
267
268     // Get the number of items in the folder.
269     int numberOfChildrenInFolder = modelIndexAbstractItemPointer->rowCount(parentModelIndex);
270
271     // Select any child items.
272     if (numberOfChildrenInFolder > 0)
273     {
274         // Select the contents of any subfolders.
275         for (int i = 0; i < numberOfChildrenInFolder; ++i)
276         {
277             // Get the child model index.
278             QModelIndex childModelIndex = modelIndexAbstractItemPointer->index(i, 0, parentModelIndex);
279
280             // Select the subfolder contents if it is a folder.
281             if (childModelIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toBool())
282                 selectSubfolderContents(childModelIndex);
283         }
284
285         // Get the first and last child model indexes.
286         QModelIndex firstChildModelIndex = modelIndexAbstractItemPointer->index(0, 0, parentModelIndex);
287         QModelIndex lastChildModelIndex = modelIndexAbstractItemPointer->index((numberOfChildrenInFolder - 1), 0, parentModelIndex);
288
289         // Create an item selection that includes all the child items.
290         QItemSelection folderChildItemsSelection = QItemSelection(firstChildModelIndex, lastChildModelIndex);
291
292         // Get the current selection.
293         QItemSelection currentSelection = treeSelectionModelPointer->selection();
294
295         // Combine the current selection and the folder child items selection.
296         currentSelection.merge(folderChildItemsSelection, QItemSelectionModel::SelectCurrent);
297
298         // Selected the updated list of items.
299         treeSelectionModelPointer->select(currentSelection, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
300     }
301 }
302
303 void BookmarksDialog::showAddBookmarkDialog() const
304 {
305     // Return the most recently selected index.
306     QModelIndex currentIndex = treeSelectionModelPointer->currentIndex();
307
308     // Instantiate a parent folder ID.
309     double parentFolderId;
310
311     // Get the parent folder ID.
312     if (currentIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toInt() == 1)  // The current index is a folder.
313     {
314         // Store the parent folder ID.
315         parentFolderId = currentIndex.siblingAtColumn(FOLDER_ID_COLUMN).data().toDouble();
316     }
317     else  // The current index is not a folder.
318     {
319         // Store the parent folder ID of the folder that contains the bookmark.
320         parentFolderId = currentIndex.parent().siblingAtColumn(FOLDER_ID_COLUMN).data().toDouble();
321     }
322
323     // Instantiate an add bookmark dialog.
324     AddBookmarkDialog *addBookmarkDialogPointer = new AddBookmarkDialog(websiteTitle, websiteUrl, websiteFavoriteIcon, parentFolderId);
325
326     // Update the displayed bookmarks when a new one is added.
327     connect(addBookmarkDialogPointer, SIGNAL(bookmarkAdded()), this, SLOT(refreshBookmarks()));
328
329     // Show the dialog.
330     addBookmarkDialogPointer->show();
331 }
332
333 void BookmarksDialog::showAddFolderDialog() const
334 {
335     // Get the most recently selected index.
336     QModelIndex currentIndex = treeSelectionModelPointer->currentIndex();
337
338     // Instantiate a parent folder ID.
339     double parentFolderId;
340
341     // Get the parent folder ID.
342     if (currentIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toInt() == 1)  // The current index is a folder.
343     {
344         // Store the parent folder ID.
345         parentFolderId = currentIndex.siblingAtColumn(FOLDER_ID_COLUMN).data().toDouble();
346     }
347     else  // The current index is not a folder.
348     {
349         // Store the parent folder ID of the folder that contains the bookmark.
350         parentFolderId = currentIndex.parent().siblingAtColumn(FOLDER_ID_COLUMN).data().toDouble();
351     }
352
353     // Instantiate an add folder dialog.
354     AddFolderDialog *addFolderDialogPointer = new AddFolderDialog(websiteFavoriteIcon, parentFolderId);
355
356     // Update the displayed bookmarks when a folder is added.
357     connect(addFolderDialogPointer, SIGNAL(folderAdded()), this, SLOT(refreshBookmarks()));
358
359     // Show the dialog.
360     addFolderDialogPointer->show();
361 }
362
363 void BookmarksDialog::showEditDialog()
364 {
365     // Get the current model index.
366     QModelIndex currentIndex = treeSelectionModelPointer->currentIndex();
367
368     // Check to see if the selected item is a folder.
369     if (currentIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toInt() == 1)  // The selected item is a folder.
370     {
371         // Instantiate an edit folder dialog.
372         QDialog *editFolderDialogPointer = new EditFolderDialog(currentIndex.siblingAtColumn(DATABASE_ID_COLUMN).data().toInt(), websiteFavoriteIcon);
373
374         // Show the dialog.
375         editFolderDialogPointer->show();
376
377         // Update the bookmarks UI.
378         connect(editFolderDialogPointer, SIGNAL(folderSaved()), this, SLOT(refreshBookmarks()));
379     }
380     else  // The selected item is a bookmark.
381     {
382         // Instantiate an edit bookmark dialog.
383         QDialog *editBookmarkDialogPointer = new EditBookmarkDialog(currentIndex.siblingAtColumn(DATABASE_ID_COLUMN).data().toInt(), websiteFavoriteIcon);
384
385         // Show the dialog.
386         editBookmarkDialogPointer->show();
387
388         // Update the bookmarks UI.
389         connect(editBookmarkDialogPointer, SIGNAL(bookmarkSaved()), this, SLOT(refreshBookmarks()));
390     }
391 }
392
393 void BookmarksDialog::updateBookmarkFromTree(QStandardItem *modifiedStandardItem)
394 {
395     // Get the model index of the modified item.
396     QModelIndex modifiedItemModelIndex = modifiedStandardItem->index();
397
398     // Get the model index of the database ID.
399     QModelIndex databaseIdModelIndex = modifiedItemModelIndex.siblingAtColumn(DATABASE_ID_COLUMN);
400
401     // Get the database ID.
402     int databaseId = databaseIdModelIndex.data().toInt();
403
404     // Check to see if the bookmark name or the URL was edited.
405     if (modifiedStandardItem->column() == NAME_COLUMN)  // The bookmark name was edited.
406     {
407         // Update the bookmark name.
408         BookmarksDatabase::updateBookmarkName(databaseId, modifiedStandardItem->text());
409     }
410     else  // The bookmark URL was edited.
411     {
412         // Update the bookmark URL.
413         BookmarksDatabase::updateBookmarkUrl(databaseId, modifiedStandardItem->text());
414     }
415
416     // Emit the bookmark updated signal.
417     emit bookmarkUpdated();
418 }
419
420 void BookmarksDialog::updateSelection() const
421 {
422     // Set the status of the buttons.
423     if (treeSelectionModelPointer->hasSelection())  // A bookmark or folder is selected.
424     {
425         // Get the list of selected model indexes.
426         QModelIndexList selectedRowsModelIndexList = treeSelectionModelPointer->selectedRows();
427
428         // Check to see if each selected item is a folder.
429         for(QModelIndex modelIndex : selectedRowsModelIndexList)
430         {
431             // If it is a folder, select all the children bookmarks.
432             if (modelIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toBool())
433                 selectSubfolderContents(modelIndex);
434         }
435     }
436
437     // Update the UI.
438     updateUi();
439 }
440
441 void BookmarksDialog::updateUi() const
442 {
443     // Set the status of the buttons.
444     if (treeSelectionModelPointer->hasSelection())  // A bookmark or folder is selected.
445     {
446         // Get the currently selected index.
447         QModelIndex currentSelectedIndex = treeSelectionModelPointer->currentIndex();
448
449         // Get the list of selected model indexes.
450         QModelIndexList selectedRowsModelIndexList = treeSelectionModelPointer->selectedRows();
451
452         // Get the number of selected rows.
453         int numberOfSelectedRows = selectedRowsModelIndexList.count();
454
455         // Enable the edit button if a folder or only one bookmark is selected.
456         editButtonPointer->setEnabled(currentSelectedIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toBool() || (numberOfSelectedRows == 1));
457
458         // Check if the root folder is selected.
459         if (treeSelectionModelPointer->isRowSelected(0))
460         {
461             // Disable the edit button.
462             editButtonPointer->setEnabled(false);
463
464             // Decrease the number of selected rows by 1.
465             --numberOfSelectedRows;
466         }
467
468         // Enabled the delete button if at least one real bookmark or folder is selected.
469         deleteItemsButtonPointer->setEnabled(numberOfSelectedRows > 0);
470
471         // Update the delete items button text.
472         if (numberOfSelectedRows > 0)
473             deleteItemsButtonPointer->setText(i18ncp("Delete items button populated text.", "Delete %1 item", "Delete %1 items", numberOfSelectedRows));
474         else
475             deleteItemsButtonPointer->setText(i18nc("Delete items button default text", "Delete items"));
476     }
477     else  // Nothing is selected.
478     {
479         // Disable the buttons.
480         editButtonPointer->setEnabled(false);
481         deleteItemsButtonPointer->setEnabled(false);
482
483         // Update the delete items button text.
484         deleteItemsButtonPointer->setText(i18nc("Delete items button default text", "Delete items"));
485     }
486 }