1 /****************************************************************************
2 **
3 **  Copyright (C) 2019-2021 Kevin B. Hendricks, Stratford Ontario Canada
4 **
5 **  This file is part of Sigil.
6 **
7 **  Sigil is free software: you can redistribute it and/or modify
8 **  it under the terms of the GNU General Public License as published by
9 **  the Free Software Foundation, either version 3 of the License, or
10 **  (at your option) any later version.
11 **
12 **  Sigil is distributed in the hope that it will be useful,
13 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
14 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 **  GNU General Public License for more details.
16 **
17 **  You should have received a copy of the GNU General Public License
18 **  along with Sigil.  If not, see <http://www.gnu.org/licenses/>.
19 **
20 *************************************************************************/
21 
22 #include <QDialog>
23 #include <QFileDialog>
24 #include <QInputDialog>
25 #include <QMenu>
26 #include <QAction>
27 #include <QtGui>
28 #include <QString>
29 #include <QFileInfo>
30 #include <QTextStream>
31 #include <QDate>
32 #include <QFileSystemModel>
33 #include <QTreeView>
34 #include <QModelIndex>
35 #include <QDesktopWidget>
36 #include <QDir>
37 #include <QApplication>
38 #include <QListWidget>
39 #include <QObject>
40 #include <QAbstractButton>
41 #include <QShortcut>
42 #include <QKeySequence>
43 #include <QMessageBox>
44 #include <QDebug>
45 
46 #include "Misc/SettingsStore.h"
47 #include "Misc/Utility.h"
48 
49 #include "Dialogs/EmptyLayout.h"
50 
51 
52 static const QString SETTINGS_GROUP = "empty_epub_layout";
53 
54 
EmptyLayout(const QString & epubversion,QWidget * parent)55 EmptyLayout::EmptyLayout(const QString &epubversion, QWidget *parent)
56   : QDialog(parent),
57     m_MainFolder(QDir::cleanPath(m_TempFolder.GetPath())),
58     m_EpubVersion(epubversion),
59     m_BookPaths(QStringList()),
60     m_hasOPF(false),
61     m_hasNCX(false),
62     m_hasNAV(false)
63 {
64     setupUi(this);
65     m_filemenu = new QMenu(this);
66 
67     // make target root folder
68     QDir folder(m_MainFolder);
69     folder.mkdir("EpubRoot");
70 
71     // initialize QFileSystemModel to point to our TempFolder
72     m_fsmodel = new QFileSystemModel();
73     m_fsmodel->setReadOnly(false);
74     m_fsmodel->setFilter(QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Files);
75     m_fsmodel->setRootPath(m_MainFolder);
76 
77     // initialize QTreeView for our model
78     view->setModel(m_fsmodel);
79     const QModelIndex rootIndex = m_fsmodel->index(m_MainFolder);
80     if (rootIndex.isValid()) {
81         view->setRootIndex(rootIndex);
82     }
83     view->setAnimated(false);
84     view->setIndentation(20);
85     view->setSortingEnabled(true);
86     const QSize availableSize = QApplication::desktop()->availableGeometry(view).size();
87     view->resize(availableSize / 2);
88     view->setColumnWidth(0, view->width() / 3);
89     view->setWindowTitle(QObject::tr("Custom Epub Layout Designer"));
90     view->setRootIsDecorated(true);
91     // column 0 is name, 1 is size, 2 is kind, 3 is date modified
92     view->hideColumn(1);
93     view->hideColumn(3);
94     view->setHeaderHidden(false);
95     // do not allow inline file folder name editing
96     view->setEditTriggers(QAbstractItemView::NoEditTriggers);
97 
98     ReadSettings();
99 
100     // Set up a popup menu with allowed file types
101     setupMarkersMenu();
102 
103     // the button takes over management of this qmenu
104     addFileButton->setMenu(m_filemenu);
105 
106     // connect signals to slots
107     connect(loadButton,    SIGNAL(clicked()),           this, SLOT(loadDesign()));
108     connect(saveButton,    SIGNAL(clicked()),           this, SLOT(saveDesign()));
109     connect(delButton,     SIGNAL(clicked()),           this, SLOT(deleteCurrent()));
110     connect(addButton,     SIGNAL(clicked()),           this, SLOT(addFolder()));
111     connect(renameButton,  SIGNAL(clicked()),           this, SLOT(renameCurrent()));
112     connect(buttonBox,     SIGNAL(accepted()),          this, SLOT(saveData()));
113     connect(buttonBox,     SIGNAL(rejected()),          this, SLOT(reject()));
114     connect(m_filemenu,    SIGNAL(triggered(QAction*)), this, SLOT(addFile(QAction*)));
115 
116     connect(view->selectionModel(),
117             SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
118             this, SLOT(updateActions()));
119 
120     // assign basic shortcuts
121     delButton->     setShortcut(QKeySequence(Qt::ControlModifier + Qt::ShiftModifier + Qt::Key_Delete));
122     addButton->     setShortcut(QKeySequence("Ctrl+Shift+D"));
123     renameButton->  setShortcut(QKeySequence("Ctrl+Shift+F2"));
124     addFileButton-> setShortcut(QKeySequence("Ctrl+Shift+F"));
125 
126     view->show();
127     view->setCurrentIndex(m_fsmodel->index(m_MainFolder + "/EpubRoot"));
128     updateActions();
129 }
130 
131 
~EmptyLayout()132 EmptyLayout::~EmptyLayout()
133 {
134     // to prevent errors with Windows fs watchers
135     // delete the model first *before*
136     // m_TmpFolder destructor is invoked.
137     if (m_fsmodel) delete m_fsmodel;
138 }
139 
140 
setupMarkersMenu()141 void EmptyLayout::setupMarkersMenu()
142 {
143     // ftypes and fmarks should be kept in sync
144     QStringList FTypes = QStringList() << QT_TR_NOOP("Xhtml files") << QT_TR_NOOP("Style files")
145                                     << QT_TR_NOOP("Image files") << QT_TR_NOOP("Font files")
146                                     << QT_TR_NOOP("Audio files") << QT_TR_NOOP("Video files")
147                                     << QT_TR_NOOP("Javascript files") << QT_TR_NOOP("Misc files")
148                                     << QT_TR_NOOP("OPF file") << QT_TR_NOOP("NCX file")
149                                     << QT_TR_NOOP("Nav file");
150 
151     QStringList FMarks = QStringList() << "marker.xhtml" << "marker.css"
152                                     << "marker.jpg" << "marker.otf" << "marker.mp3"
153                                     << "marker.mp4" << "marker.js" << "marker.xml"
154                                     << "content.opf" <<"toc.ncx" << "nav.xhtml";
155     QAction * act;
156     int i = 0;
157     foreach(QString filetype, FTypes) {
158         QString mark = FMarks.at(i++);
159         if (!m_EpubVersion.startsWith("3") && ((mark == "marker.js") || (mark == "nav.xhtml"))) continue;
160         act = m_filemenu->addAction(tr(filetype.toUtf8().constData()));
161         act->setData(mark);
162     }
163 }
164 
165 
GetInput(const QString & title,const QString & prompt,const QString & initvalue)166 QString EmptyLayout::GetInput(const QString& title, const QString& prompt, const QString& initvalue)
167 {
168     QString result;
169     QInputDialog dinput;
170     dinput.setWindowTitle(title);
171     dinput.setLabelText(prompt);
172     dinput.setTextValue(initvalue);
173     if (dinput.exec()) {
174         result = dinput.textValue();
175     }
176     return result;
177 }
178 
179 
cleanEpubRoot()180 bool EmptyLayout::cleanEpubRoot()
181 {
182     // first hide the view
183     view->hide();
184 
185     disconnect(view->selectionModel(),
186             SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
187             this, SLOT(updateActions()));
188 
189     QItemSelectionModel *m = view->selectionModel();
190     // Using NULL here sets the model to QAbstractItemModelPrivate::staticEmptyModel() (see source)
191     view->setModel(NULL);
192     delete m;
193 
194     delete m_fsmodel;
195     m_fsmodel = NULL;
196 
197     // Delete the EpubRoot
198     QString adir = m_MainFolder + "/EpubRoot";
199     QDir eroot(adir);
200     bool success = eroot.removeRecursively();
201     if (!success) qDebug() << "Error:: Attempt to remove EpubRoot failed";
202 
203     // remake Epubroot
204     QDir mfolder(m_MainFolder);
205     mfolder.mkdir("EpubRoot");
206 
207     // initialize to empty state
208     m_hasOPF = false;
209     m_hasNCX = false;
210     m_hasNAV = false;
211     return success;
212 }
213 
214 
loadDesign()215 void EmptyLayout::loadDesign()
216 {
217     QFileDialog::Options options = QFileDialog::Options();
218 #ifdef Q_OS_MAC
219     options = options | QFileDialog::DontUseNativeDialog;
220 #endif
221     QString inipath = QFileDialog::getOpenFileName(this,
222                                                    tr("Select previously saved layout design ini File"),
223                                                    m_LastDirSaved,
224                                                    tr("Settings Files (*.ini)"),
225                                                    NULL,
226                                                    options);
227 
228     if (inipath.isEmpty()) return;
229     if (!QFile::exists(inipath)) return;
230 
231     QStringList bookpaths;
232     {
233         SettingsStore ss(inipath);
234         const QString SETTINGS_GROUP = "bookpaths";
235         const QString KEY_BOOKPATHS = SETTINGS_GROUP + "/" + "empty_epub_bookpaths";
236         while (!ss.group().isEmpty()) {
237             ss.endGroup();
238         }
239         bookpaths = ss.value(KEY_BOOKPATHS,QStringList()).toStringList();
240     }
241 
242     if (bookpaths.isEmpty()) return;
243 
244     cleanEpubRoot();
245     m_BookPaths = QStringList();
246 
247     // first write the files you have loaded
248     QDir eroot(m_MainFolder + "/EpubRoot");
249     foreach(QString bkpath, bookpaths) {
250         // update the current state
251         if (bkpath.endsWith(".opf")) m_hasOPF = true;
252         if (bkpath.endsWith(".ncx")) m_hasNCX = true;
253         if (bkpath.endsWith(".xhtml") && !bkpath.contains("marker.xhtml")) m_hasNAV = true;
254         if (bkpath.startsWith('/')) bkpath.remove(0,1);
255         QString sdir = Utility::startingDir(bkpath);
256         if (!sdir.isEmpty()) eroot.mkpath(sdir);
257         // now we are finally ready to create the file itself
258         // use the equivalent of "touch" to create files
259         QString fpath = m_MainFolder + "/EpubRoot" + "/" + bkpath;
260         QFile afile(fpath);
261         if (afile.open(QFile::WriteOnly)) afile.close();
262     }
263 
264     // Now finally create a new Model and reset the view
265     m_fsmodel = new QFileSystemModel();
266     m_fsmodel->setReadOnly(false);
267     m_fsmodel->setFilter(QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Files);
268     m_fsmodel->setRootPath(m_MainFolder);
269 
270     // re - initialize QTreeView for our model
271     // view->reset();
272 
273     QItemSelectionModel * m = view->selectionModel();
274     view->setModel(m_fsmodel);
275     if (m) delete m;
276 
277     const QModelIndex rootIndex = m_fsmodel->index(m_MainFolder);
278     if (rootIndex.isValid()) {
279         view->setRootIndex(rootIndex);
280     }
281 
282     view->setAnimated(false);
283     view->setIndentation(20);
284     view->setSortingEnabled(true);
285     const QSize availableSize = QApplication::desktop()->availableGeometry(view).size();
286     view->resize(availableSize / 2);
287     view->setColumnWidth(0, view->width() / 3);
288     view->setWindowTitle(QObject::tr("Custom Epub Layout Designer"));
289     view->setRootIsDecorated(true);
290     // column 0 is name, 1 is size, 2 is kind, 3 is date modified
291     view->hideColumn(1);
292     view->hideColumn(3);
293     view->setHeaderHidden(false);
294     // do not allow inline file folder name editing
295     view->setEditTriggers(QAbstractItemView::NoEditTriggers);
296 
297     connect(view->selectionModel(),
298             SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
299             this, SLOT(updateActions()));
300 
301     view->show();
302     QModelIndex index = m_fsmodel->index(m_MainFolder + "/EpubRoot");
303     view->setCurrentIndex(index);
304     view->expandAll();
305     updateActions();
306 }
307 
308 
saveDesign()309 void EmptyLayout::saveDesign()
310 {
311     QString fullfolderpath = m_MainFolder + "/EpubRoot";
312     QString basepath = fullfolderpath;
313     QStringList bookpaths = GetPathsToFilesInFolder(fullfolderpath, basepath);
314 
315     QString filter_string = "*.ini;;*.*";
316     QString default_filter = "ini";
317     QString save_path = m_LastDirSaved + "/" + m_LastFileSaved;
318 
319     QFileDialog::Options options = QFileDialog::Options();
320 #ifdef Q_OS_MAC
321     options = options | QFileDialog::DontUseNativeDialog;
322 #endif
323 
324     QString destination = QFileDialog::getSaveFileName(this,
325                                                        tr("Save current design to an ini File"),
326                                                        save_path,
327                                                        filter_string,
328                                                        &default_filter,
329                                                        options);
330     if (destination.isEmpty()) {
331         return;
332     }
333 
334     // force destination setting store destructor to invoked before routine exits
335     {
336         SettingsStore ss(destination);
337         const QString SETTINGS_GROUP = "bookpaths";
338         const QString KEY_BOOKPATHS = SETTINGS_GROUP + "/" + "empty_epub_bookpaths";
339         while (!ss.group().isEmpty()) {
340             ss.endGroup();
341         }
342         ss.setValue(KEY_BOOKPATHS, bookpaths);
343     }
344 
345     m_LastDirSaved = QFileInfo(destination).absolutePath();
346     m_LastFileSaved = QFileInfo(destination).fileName();
347 
348     WriteSettings();
349 }
350 
351 
addFolder()352 void EmptyLayout::addFolder()
353 {
354     QModelIndex index = view->selectionModel()->currentIndex();
355     if (!index.isValid()) return;
356     if (m_fsmodel->isDir(index)) {
357         QString newname = GetInput(tr("Add a Folder"), tr("New Folder Name?"), tr("untitled_folder"));
358         if (newname.isEmpty()) return;
359         m_fsmodel->mkdir(index, newname);
360     }
361     view->expand(index);
362     updateActions();
363 }
364 
365 
addFile(QAction * act)366 void EmptyLayout::addFile(QAction * act)
367 {
368     QModelIndex index = view->selectionModel()->currentIndex();
369     QString filedata = act->data().toString();
370     if (!index.isValid()) return;
371     if (m_fsmodel->isDir(index)) {
372         QString fpath = m_fsmodel->filePath(index) + "/" + filedata;
373         QFile afile(fpath);
374         if (afile.open(QFile::WriteOnly)) afile.close();
375         if (filedata == "content.opf") m_hasOPF=true;
376         if (filedata == "toc.ncx") m_hasNCX=true;
377         if (filedata == "nav.xhtml") m_hasNAV=true;
378         QFileInfo file_info = m_fsmodel->fileInfo(m_fsmodel->index(fpath));
379     }
380     view->expand(index);
381     updateActions();
382 }
383 
384 
renameCurrent()385 void EmptyLayout::renameCurrent()
386 {
387     QModelIndex index = view->selectionModel()->currentIndex();
388     if (!index.isValid()) return;
389     QString dpath = m_fsmodel->filePath(index.parent());
390     QString current_name = m_fsmodel->fileName(index);
391     if (current_name == "EpubRoot") return;
392     if (current_name.startsWith("marker.")) return;
393     if (m_fsmodel->isDir(index)) {
394         QString newname = GetInput(tr("Rename a Folder"), tr("New Name for Folder?"), current_name);
395         if (newname.isEmpty()) return;
396         if ((newname != "EpubRoot") && (newname != current_name)) {
397             QDir folder(dpath);
398             bool success = folder.rename(current_name, newname);
399             if (!success) qDebug() << "folder rename failed";
400         }
401         view->expand(index);
402     } else {
403         // renaming a file
404         QFileInfo fi = m_fsmodel->fileInfo(index);
405         QString newname = GetInput(tr("Rename a File"), tr("New Name for File?"), fi.baseName());
406         if (newname.isEmpty()) return;
407         newname = newname + "." + fi.suffix();
408         if (newname != current_name) {
409             QDir folder(dpath);
410             bool success = folder.rename(current_name, newname);
411             if (!success) qDebug() << "file rename failed";
412         }
413         view->expand(index.parent());
414     }
415     updateActions();
416 }
417 
418 
deleteCurrent()419 void EmptyLayout::deleteCurrent()
420 {
421     QModelIndex index = view->selectionModel()->currentIndex();
422     if (!index.isValid()) return;
423     QString current_name = m_fsmodel->fileName(index);
424     if (current_name == "EpubRoot") return;
425     if (m_fsmodel->isDir(index)) {
426        bool success = m_fsmodel->remove(index);
427        if (!success) qDebug() << "folder removal failed";
428        view->expand(index);
429     } else {
430        QModelIndex parent = index.parent();
431        bool success = m_fsmodel->remove(index);
432        if (success) {
433            if (current_name.endsWith(".opf")) m_hasOPF = false;
434            if (current_name.endsWith(".ncx")) m_hasNCX = false;
435            if (!current_name.startsWith("marker.") && current_name.endsWith(".xhtml")) m_hasNAV = false;
436        }
437        if (!success) qDebug() << "file removal failed";
438        view->expand(parent);
439     }
440     updateActions();
441 }
442 
443 
saveData()444 void EmptyLayout::saveData()
445 {
446     QString fullfolderpath = m_MainFolder + "/EpubRoot";
447     QString basepath = fullfolderpath;
448     QStringList bookpaths = GetPathsToFilesInFolder(fullfolderpath, basepath);
449 
450     // perform simple sanity check
451     int numopf = 0; int numtxt = 0;
452     int numcss = 0; int numimg = 0;
453     int numncx = 0; int numnav = 0;
454     foreach(QString apath, bookpaths) {
455         if (apath.endsWith(".opf")) numopf++;
456         if (apath.endsWith("marker.xhtml")) numtxt++;
457         if (apath.endsWith("marker.css")) numcss++;
458         if (apath.endsWith("marker.jpg")) numimg++;
459         if (apath.endsWith(".ncx")) numncx++;
460         if (apath.endsWith(".xhtml") && !apath.contains("marker.xhtml")) numnav++;
461     }
462     QStringList Errors;
463     if (numopf != 1) Errors << tr("A single OPF file is required.");
464     if (numtxt < 1)  Errors << tr("At least one xhtml marker must exist.");
465     if (numimg < 1)  Errors << tr("At least one image marker must exist.");
466     if (numcss < 1)  Errors << tr("At least one css marker must exist.");
467     if (m_EpubVersion.startsWith("2")) {
468         if (numncx != 1) Errors << tr("A single NCX file is required.");
469     } else {
470         if (numnav != 1) Errors << tr("A single NAV file is required.");
471     }
472     if (!Errors.isEmpty()) {
473         QString error_message = Errors.join('\n');
474         QMessageBox::warning(this, tr("Errors Detected"), error_message, QMessageBox::Ok);
475         return;
476     }
477     m_BookPaths = bookpaths;
478 
479     // allow the user to set this layout as Sigil's default empty epub layout
480     bool make_default = QMessageBox::Yes == QMessageBox::warning(this, tr("Sigil"),
481                                    tr("Do you want to set this layout as the default empty "
482                                       "Epub layout for Sigil?\n\n"),
483                                    QMessageBox::Yes|QMessageBox::No);
484 
485     if (make_default) {
486         // create a sigil_empty_epub.ini file in Sigil Preferences folder
487         QString empty_epub_ini_path = Utility::DefinePrefsDir() + "/" + "sigil_empty_epub.ini";
488         SettingsStore ss(empty_epub_ini_path);
489         const QString SETTINGS_GROUP = "bookpaths";
490         const QString KEY_BOOKPATHS = SETTINGS_GROUP + "/" + "empty_epub_bookpaths";
491         while (!ss.group().isEmpty()) {
492             ss.endGroup();
493         }
494         ss.setValue(KEY_BOOKPATHS, bookpaths);
495     }
496 
497     WriteSettings();
498     cleanEpubRoot();
499     // do not reset m_BookPaths here
500     QDialog::accept();
501 }
502 
503 
reject()504 void EmptyLayout::reject()
505 {
506 
507     WriteSettings();
508     cleanEpubRoot();
509     m_BookPaths = QStringList();
510     QDialog::reject();
511 }
512 
513 
updateActions()514 void EmptyLayout::updateActions()
515 {
516     bool hasSelection = !view->selectionModel()->selection().isEmpty();
517     QModelIndex index = view->selectionModel()->currentIndex();
518     bool hasCurrent = index.isValid();
519     QString name = "";
520     if (hasCurrent) {
521         name = m_fsmodel->filePath(index).split("/").last();
522     }
523     bool isFile = name.startsWith("marker.") || name.endsWith(".opf") || name.endsWith(".ncx");
524     bool isEpubRoot = name == "EpubRoot";
525     bool isMarker = name.startsWith("marker.");
526     bool isOPFNCXNAV = name.endsWith(".opf") || name.endsWith(".ncx") || (name.endsWith(".xhtml") && !isMarker);
527     delButton->setEnabled(hasSelection && !isEpubRoot);
528     addButton->setEnabled(hasSelection && !isMarker && !isOPFNCXNAV);
529     renameButton->setEnabled(hasSelection && !isEpubRoot && !isMarker);
530     addFileButton->setEnabled(hasSelection && !isFile);
531 
532     // finally enable and disable file marker menu items
533     QList<QAction *> menuacts = m_filemenu->actions();
534     foreach(QAction * act, menuacts) {
535         bool enable = true;
536         QString filedata = act->data().toString();
537         if ((filedata == "content.opf") && m_hasOPF) enable = false;
538         if ((filedata == "toc.ncx") && m_hasNCX) enable = false;
539         if ((filedata == "nav.xhtml") && m_hasNAV) enable = false;
540         act->setEnabled(enable);
541     }
542 }
543 
544 
ReadSettings()545 void EmptyLayout::ReadSettings()
546 {
547     SettingsStore settings;
548     settings.beginGroup(SETTINGS_GROUP);
549 
550     m_LastDirSaved = settings.value("lastdirsaved", Utility::DefinePrefsDir()).toString();
551     m_LastFileSaved = settings.value("lastfilesaved", "layoutdesign.ini").toString();
552 
553     // The size of the window and it's full screen status
554     QByteArray geometry = settings.value("geometry").toByteArray();
555 
556     if (!geometry.isNull()) {
557         restoreGeometry(geometry);
558     }
559     settings.endGroup();
560 }
561 
562 
WriteSettings()563 void EmptyLayout::WriteSettings()
564 {
565     SettingsStore settings;
566     settings.beginGroup(SETTINGS_GROUP);
567     settings.setValue("lastdirsaved", m_LastDirSaved);
568     settings.setValue("lastfilesaved", m_LastFileSaved);
569 
570     // The size of the window and it's full screen status
571     settings.setValue("geometry", saveGeometry());
572     settings.endGroup();
573 }
574 
575 
GetPathsToFilesInFolder(const QString & fullfolderpath,const QString & basepath)576 QStringList EmptyLayout::GetPathsToFilesInFolder(const QString &fullfolderpath, const QString &basepath)
577 {
578     QDir folder(fullfolderpath);
579     QStringList paths;
580     foreach(QFileInfo fi, folder.entryInfoList()) {
581         if ((fi.fileName() != ".") && (fi.fileName() != "..")) {
582             if (fi.isFile()) {
583                 QString filepath = fi.absoluteFilePath();
584                 QString bookpath = filepath.right(filepath.length() - basepath.length() - 1);
585                 paths.append(bookpath);
586             } else {
587                 paths.append(GetPathsToFilesInFolder(fi.absoluteFilePath(), basepath));
588             }
589         }
590     }
591     return paths;
592 }
593