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