1 /************************************************************************
2 **
3 **  Copyright (C) 2015-2021 Kevin B. Hendricks, Stratford Ontario Canada
4 **  Copyright (C) 2012      John Schember <john@nachtimwald.com>
5 **  Copyright (C) 2012      Dave Heiland
6 **
7 **  This file is part of Sigil.
8 **
9 **  Sigil is free software: you can redistribute it and/or modify
10 **  it under the terms of the GNU General Public License as published by
11 **  the Free Software Foundation, either version 3 of the License, or
12 **  (at your option) any later version.
13 **
14 **  Sigil is distributed in the hope that it will be useful,
15 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
16 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 **  GNU General Public License for more details.
18 **
19 **  You should have received a copy of the GNU General Public License
20 **  along with Sigil.  If not, see <http://www.gnu.org/licenses/>.
21 **
22 *************************************************************************/
23 
24 #include <QtCore/QFile>
25 #include <QtCore/QFileInfo>
26 #include <QtCore/QHashIterator>
27 #include <QtWidgets/QFileDialog>
28 #include <QtGui/QFont>
29 #include <QtWidgets/QMessageBox>
30 #include <QtWidgets/QPushButton>
31 
32 #include "sigil_exception.h"
33 #include "BookManipulation/FolderKeeper.h"
34 #include "Dialogs/ReportsWidgets/StylesInCSSFilesWidget.h"
35 #include "Parsers/CSSInfo.h"
36 #include "Misc/NumericItem.h"
37 #include "Misc/SettingsStore.h"
38 #include "Misc/Utility.h"
39 #include "ResourceObjects/HTMLResource.h"
40 #include "ResourceObjects/CSSResource.h"
41 
42 static const QString SETTINGS_GROUP = "reports";
43 static const QString DEFAULT_REPORT_FILE = "StyleSelectorsInCSSFilesReport.csv";
44 
StylesInCSSFilesWidget()45 StylesInCSSFilesWidget::StylesInCSSFilesWidget()
46     :
47     m_ItemModel(new QStandardItemModel),
48     m_ContextMenu(new QMenu(this))
49 {
50     ui.setupUi(this);
51     ui.fileTree->setContextMenuPolicy(Qt::CustomContextMenu);
52     CreateContextMenuActions();
53     connectSignalsSlots();
54 }
55 
~StylesInCSSFilesWidget()56 StylesInCSSFilesWidget::~StylesInCSSFilesWidget()
57 {
58     delete m_ItemModel;
59 }
60 
CreateReport(QSharedPointer<Book> book)61 void StylesInCSSFilesWidget::CreateReport(QSharedPointer<Book> book)
62 {
63     m_Book = book;
64     SetupTable();
65     QList<BookReports::StyleData *> css_selector_usage = BookReports::GetAllCSSSelectorsUsed(m_Book, true);
66     AddTableData(css_selector_usage);
67     qDeleteAll(css_selector_usage);
68 
69     for (int i = 0; i < ui.fileTree->header()->count(); i++) {
70         ui.fileTree->resizeColumnToContents(i);
71     }
72 
73     ui.fileTree->sortByColumn(1, Qt::AscendingOrder);
74     ui.fileTree->sortByColumn(0, Qt::AscendingOrder);
75     ui.fileTree->sortByColumn(2, Qt::AscendingOrder);
76 }
77 
SetupTable()78 void StylesInCSSFilesWidget::SetupTable()
79 {
80     m_ItemModel->clear();
81     QStringList header;
82     header.append(tr("CSS File"));
83     header.append(tr("CSS Selector"));
84     header.append(tr("Used In HTML File"));
85     m_ItemModel->setHorizontalHeaderLabels(header);
86     ui.fileTree->setSelectionBehavior(QAbstractItemView::SelectRows);
87     ui.fileTree->setModel(m_ItemModel);
88     ui.fileTree->header()->setSortIndicatorShown(true);
89     ui.fileTree->header()->setToolTip(
90         tr("<p>This is a list of the CSS selectors in all CSS files and whether or not the selector was matched in an HTML file.<p>") %
91         tr("<p>NOTE:</p>") %
92         tr("<p>Due to the complexities of CSS you must check your code manually to be absolutely certain if a selector is used or not.</p>") % tr("<p>Note: Only one HTML File is listed among the many possible matches.</p>")
93     );
94 }
95 
AddTableData(const QList<BookReports::StyleData * > css_selectors_usage)96 void StylesInCSSFilesWidget::AddTableData(const QList<BookReports::StyleData *> css_selectors_usage)
97 {
98     foreach(BookReports::StyleData *selector_usage, css_selectors_usage) {
99         // Write the table entries
100         QList<QStandardItem *> rowItems;
101         // File name
102         QStandardItem *filename_item = new QStandardItem();
103         QString css_short_filename = selector_usage->css_filename;
104         css_short_filename = css_short_filename.right(css_short_filename.length() - css_short_filename.lastIndexOf('/') - 1);
105         filename_item->setText(css_short_filename);
106         filename_item->setData(selector_usage->css_filename);
107         filename_item->setToolTip(selector_usage->css_filename);
108         rowItems << filename_item;
109         // Selector
110         QStandardItem *selector_text_item = new QStandardItem();
111         selector_text_item->setText(selector_usage->css_selector_text);
112         selector_text_item->setData(selector_usage->css_selector_position);
113         rowItems << selector_text_item;
114         // Found in
115         QStandardItem *found_in_item = new QStandardItem();
116         found_in_item->setText(selector_usage->html_filename);
117         rowItems << found_in_item;
118 
119         for (int i = 0; i < rowItems.count(); i++) {
120             rowItems[i]->setEditable(false);
121         }
122 
123         m_ItemModel->appendRow(rowItems);
124     }
125 }
126 
127 
FilterEditTextChangedSlot(const QString & text)128 void StylesInCSSFilesWidget::FilterEditTextChangedSlot(const QString &text)
129 {
130     const QString lowercaseText = text.toLower();
131     QStandardItem *root_item = m_ItemModel->invisibleRootItem();
132     QModelIndex parent_index;
133     // Hide rows that don't contain the filter text
134     int first_visible_row = -1;
135 
136     for (int row = 0; row < root_item->rowCount(); row++) {
137         if (text.isEmpty() || root_item->child(row, 0)->text().toLower().contains(lowercaseText) ||
138             root_item->child(row, 1)->text().toLower().contains(lowercaseText) ||
139             root_item->child(row, 2)->text().toLower().contains(lowercaseText)) {
140             ui.fileTree->setRowHidden(row, parent_index, false);
141 
142             if (first_visible_row == -1) {
143                 first_visible_row = row;
144             }
145         } else {
146             ui.fileTree->setRowHidden(row, parent_index, true);
147         }
148     }
149 
150     if (!text.isEmpty() && first_visible_row != -1) {
151         // Select the first non-hidden row
152         ui.fileTree->setCurrentIndex(root_item->child(first_visible_row, 0)->index());
153     } else {
154         // Clear current and selection, which clears preview image
155         ui.fileTree->setCurrentIndex(QModelIndex());
156     }
157 }
158 
159 
DoubleClick()160 void StylesInCSSFilesWidget::DoubleClick()
161 {
162     QModelIndex index = ui.fileTree->selectionModel()->selectedRows(0).first();
163     QString bookpath = m_ItemModel->itemFromIndex(index)->data().toString();
164     int pos = m_ItemModel->itemFromIndex(index.sibling(index.row(), 1))->data().toInt();
165     emit OpenFileRequest(bookpath, -1, pos);
166 }
167 
Delete()168 void StylesInCSSFilesWidget::Delete()
169 {
170     QString style_names;
171     QHash<QString, QStringList> stylesheet_styles;
172     foreach(QModelIndex index, ui.fileTree->selectionModel()->selectedRows(0)) {
173         QString bookpath = m_ItemModel->itemFromIndex(index)->data().toString();
174         QString name = m_ItemModel->itemFromIndex(index.sibling(index.row(), 1))->text();
175         stylesheet_styles[bookpath].append(name);
176     }
177     int count = 0;
178     QHashIterator<QString, QStringList> it_stylesheet_styles(stylesheet_styles);
179 
180     while (it_stylesheet_styles.hasNext()) {
181         it_stylesheet_styles.next();
182         style_names += "\n\n" + it_stylesheet_styles.key() + ": " "\n";
183         foreach(QString name, it_stylesheet_styles.value()) {
184             style_names += name + ", ";
185             count++;
186         }
187         style_names = style_names.left(style_names.length() - 2);
188     }
189 
190     QList<BookReports::StyleData *> styles_to_delete;
191     foreach(QModelIndex index, ui.fileTree->selectionModel()->selectedRows(0)) {
192         BookReports::StyleData *style = new BookReports::StyleData();
193         style->css_filename = m_ItemModel->itemFromIndex(index)->data().toString();
194         style->css_selector_text = m_ItemModel->itemFromIndex(index.sibling(index.row(), 1))->text();
195         style->css_selector_position = m_ItemModel->itemFromIndex(index.sibling(index.row(), 1))->data().toInt();
196         styles_to_delete.append(style);
197     }
198     emit DeleteStylesRequest(styles_to_delete);
199 }
200 
Save()201 void StylesInCSSFilesWidget::Save()
202 {
203     QStringList report_info;
204     QStringList heading_row;
205 
206     // Get headings
207     for (int col = 0; col < ui.fileTree->header()->count(); col++) {
208         QStandardItem *item = m_ItemModel->horizontalHeaderItem(col);
209         QString text = "";
210         if (item) {
211             text = item->text();
212         }
213         heading_row << text;
214     }
215     report_info << Utility::createCSVLine(heading_row);
216 
217     // Get data from table
218     for (int row = 0; row < m_ItemModel->rowCount(); row++) {
219         QStringList data_row;
220         for (int col = 0; col < ui.fileTree->header()->count(); col++) {
221             QStandardItem *item = m_ItemModel->item(row, col);
222             QString text = "";
223             if (item) {
224                 text = item->text();
225             }
226             data_row << text;
227         }
228         report_info << Utility::createCSVLine(data_row);
229     }
230 
231     QString data = report_info.join('\n') + '\n';
232     // Save the file
233     ReadSettings();
234     QString filter_string = "*.csv;;*.txt;;*.*";
235     QString default_filter = "";
236     QString save_path = m_LastDirSaved + "/" + m_LastFileSaved;
237     QFileDialog::Options options = QFileDialog::Options();
238 #ifdef Q_OS_MAC
239     options = options | QFileDialog::DontUseNativeDialog;
240 #endif
241 
242     QString destination = QFileDialog::getSaveFileName(this,
243                                                        tr("Save Report As Comma Separated File"),
244                                                        save_path,
245                                                        filter_string,
246                                                        &default_filter,
247                                                        options);
248 
249     if (destination.isEmpty()) {
250         return;
251     }
252 
253     try {
254         Utility::WriteUnicodeTextFile(data, destination);
255     } catch (CannotOpenFile&) {
256         QMessageBox::warning(this, tr("Sigil"), tr("Cannot save report file."));
257     }
258 
259     m_LastDirSaved = QFileInfo(destination).absolutePath();
260     m_LastFileSaved = QFileInfo(destination).fileName();
261     WriteSettings();
262 }
263 
CreateContextMenuActions()264 void StylesInCSSFilesWidget::CreateContextMenuActions()
265 {
266     m_Delete    = new QAction(tr("Delete From Stylesheet") + "...",     this);
267     m_Delete->setShortcut(QKeySequence::Delete);
268     // Has to be added to the dialog itself for the keyboard shortcut to work.
269     addAction(m_Delete);
270 }
271 
OpenContextMenu(const QPoint & point)272 void StylesInCSSFilesWidget::OpenContextMenu(const QPoint &point)
273 {
274     SetupContextMenu(point);
275     m_ContextMenu->exec(ui.fileTree->viewport()->mapToGlobal(point));
276     if (!m_ContextMenu.isNull()) {
277         m_ContextMenu->clear();
278     }
279 }
280 
SetupContextMenu(const QPoint & point)281 void StylesInCSSFilesWidget::SetupContextMenu(const QPoint &point)
282 {
283     m_ContextMenu->addAction(m_Delete);
284     // We do not enable the delete option if no rows selected.
285     m_Delete->setEnabled(ui.fileTree->selectionModel()->selectedRows().count() > 0);
286 }
287 
288 
ReadSettings()289 void StylesInCSSFilesWidget::ReadSettings()
290 {
291     SettingsStore settings;
292     settings.beginGroup(SETTINGS_GROUP);
293     // Last file open
294     m_LastDirSaved = settings.value("last_dir_saved").toString();
295     m_LastFileSaved = settings.value("last_file_saved_styles_css_files").toString();
296 
297     if (m_LastFileSaved.isEmpty()) {
298         m_LastFileSaved = DEFAULT_REPORT_FILE;
299     }
300 
301     settings.endGroup();
302 }
303 
WriteSettings()304 void StylesInCSSFilesWidget::WriteSettings()
305 {
306     SettingsStore settings;
307     settings.beginGroup(SETTINGS_GROUP);
308     // Last file open
309     settings.setValue("last_dir_saved", m_LastDirSaved);
310     settings.setValue("last_file_saved_styles_css_files", m_LastFileSaved);
311     settings.endGroup();
312 }
313 
connectSignalsSlots()314 void StylesInCSSFilesWidget::connectSignalsSlots()
315 {
316     connect(ui.Filter,    SIGNAL(textChanged(QString)),
317             this,         SLOT(FilterEditTextChangedSlot(QString)));
318     connect(ui.fileTree, SIGNAL(doubleClicked(const QModelIndex &)),
319             this,          SLOT(DoubleClick()));
320     connect(ui.fileTree,  SIGNAL(customContextMenuRequested(const QPoint &)),
321             this,         SLOT(OpenContextMenu(const QPoint &)));
322     connect(m_Delete,     SIGNAL(triggered()), this, SLOT(Delete()));
323     connect(ui.buttonBox->button(QDialogButtonBox::Close), SIGNAL(clicked()), this, SIGNAL(CloseDialog()));
324     connect(ui.buttonBox->button(QDialogButtonBox::Save), SIGNAL(clicked()), this, SLOT(Save()));
325 }
326