1 /*
2 Copyright (C) 2005-2006 Remon Sijrier
3
4 This file is part of Traverso
5
6 Traverso 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 2 of the License, or
9 (at your option) any later version.
10
11 This program 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 this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 */
21
22 #include "ProjectManager.h"
23
24 #include <QApplication>
25 #include <QFileInfo>
26 #include <QDir>
27 #include <QFileDialog>
28 #include <QMessageBox>
29 #include <QFileSystemWatcher>
30 #include <QTextStream>
31
32
33 #include "Project.h"
34 #include "Sheet.h"
35 #include "ContextPointer.h"
36 #include "ResourcesManager.h"
37 #include "Information.h"
38 #include "Config.h"
39 #include "FileHelpers.h"
40 #include <AudioDevice.h>
41 #include <Utils.h>
42
43 // Always put me below _all_ includes, this is needed
44 // in case we run with memory leak detection enabled!
45 #include "Debugger.h"
46
47
48 /** \class ProjectManager
49 \brief ProjectManager is a singleton used for loading, creating and deleting Projects.
50
51
52 */
53
54 QUndoGroup ProjectManager::undogroup;
55
ProjectManager()56 ProjectManager::ProjectManager()
57 : ContextItem()
58 {
59 PENTERCONS;
60 currentProject = (Project*) 0;
61 m_exitInProgress = false;
62
63 m_watcher = new QFileSystemWatcher(0);
64
65 QString path = config().get_property("Project", "directory", "").toString();
66 set_current_project_dir(path);
67
68 cpointer().add_contextitem(this);
69
70 connect(m_watcher, SIGNAL(directoryChanged(const QString&)), this, SLOT(project_dir_rename_detected(const QString&)));
71 }
72
73 /**
74 * Used to get a reference to the ProjectManager
75 * @return A reference to the ProjectManager singleton
76 */
pm()77 ProjectManager& pm()
78 {
79 static ProjectManager projMan;
80 return projMan;
81 }
82
83 /**
84 * The Resources Manager for the currently loaded Project
85
86 * @return A pointer to the Resources Manager of the loaded Project, 0 if no Project is loaded
87 */
resources_manager()88 ResourcesManager* resources_manager()
89 {
90 Project* proj = pm().get_project();
91 if (proj) {
92 return proj->get_audiosource_manager();
93 }
94 return 0;
95 }
96
set_current_project(Project * project)97 void ProjectManager::set_current_project(Project* project)
98 {
99 PENTER;
100
101 emit projectLoaded(project);
102
103 QString oldprojectname = "";
104
105 if (currentProject) {
106 if (m_exitInProgress) {
107 QString oncloseaction = config().get_property("Project", "onclose", "save").toString();
108 if (oncloseaction == "save") {
109 currentProject->save();
110 } else if (oncloseaction == "ask") {
111 if (QMessageBox::question(0, tr("Save Project"),
112 tr("Do you want to save the Project before quiting?"),
113 QMessageBox::Yes | QMessageBox::No,
114 QMessageBox::Yes) == QMessageBox::Yes)
115 {
116 currentProject->save();
117 }
118 }
119 } else {
120 currentProject->save();
121 }
122
123 oldprojectname = currentProject->get_title();
124 delete currentProject;
125 }
126
127 currentProject = project;
128
129
130 QString title = "";
131
132 if (currentProject) {
133 title = currentProject->get_title();
134 config().set_property("Project", "current", title);
135 }
136
137 if ( ! oldprojectname.isEmpty() ) {
138 cleanup_backupfiles_for_project(oldprojectname);
139 }
140
141 }
142
create_new_project(int numSheets,int numTracks,const QString & projectName)143 Project* ProjectManager::create_new_project(int numSheets, int numTracks, const QString& projectName)
144 {
145 PENTER;
146
147 if (project_exists(projectName)) {
148 info().critical(tr("Project %1 already exists!").arg(projectName));
149 return 0;
150 }
151
152 QString newrootdir = config().get_property("Project", "directory", "/directory/unknown/").toString() + "/" + projectName;
153 m_projectDirs.append(newrootdir);
154
155 Project* newProject = new Project(projectName);
156
157 if (newProject->create(numSheets, numTracks) < 0) {
158 delete newProject;
159 info().critical(tr("Unable to create new Project %1").arg(projectName));
160 return 0;
161 }
162
163 return newProject;
164 }
165
create_new_project(const QString & templatefile,const QString & projectName)166 Project* ProjectManager::create_new_project(const QString& templatefile, const QString& projectName)
167 {
168 if (project_exists(projectName)) {
169 info().critical(tr("Project %1 already exists!").arg(projectName));
170 return 0;
171 }
172
173 QString newrootdir = config().get_property("Project", "directory", "/directory/unknown/").toString() + "/" + projectName;
174 m_projectDirs.append(newrootdir);
175
176 Project* newProject = new Project(projectName);
177
178 if (newProject->create(0, 0) < 0) {
179 delete newProject;
180 info().critical(tr("Unable to create new Project %1").arg(projectName));
181 return 0;
182 }
183
184 if (newProject->load(templatefile) < 0) {
185 return 0;
186 }
187
188 // title gets overwritten in newProject->load()
189 newProject->set_title(projectName);
190
191 return newProject;
192 }
193
load_project(const QString & projectName)194 int ProjectManager::load_project(const QString& projectName)
195 {
196 PENTER;
197
198 if( ! project_exists(projectName) ) {
199 PERROR("project %s doesn't exist!", projectName.toLatin1().data());
200 return -1;
201 }
202
203 Project* newProject = new Project(projectName);
204
205 if (!newProject)
206 return -1;
207
208 set_current_project(newProject);
209
210 int err;
211 if ((err = currentProject->load()) < 0) {
212 switch (err) {
213 case Project::PROJECT_FILE_VERSION_MISMATCH: {
214 emit projectFileVersionMismatch(currentProject->get_root_dir(), currentProject->get_title());
215 break;
216 }
217 default: {
218 emit projectLoadFailed(currentProject->get_title(), currentProject->get_error_string());
219 }
220 }
221 delete currentProject;
222 currentProject = 0;
223 set_current_project(0);
224 info().critical(tr("Unable to load Project %1").arg(projectName));
225 return -1;
226 }
227
228 return 1;
229 }
230
load_renamed_project(const QString & name)231 int ProjectManager::load_renamed_project(const QString & name)
232 {
233 Q_ASSERT(currentProject);
234
235 delete currentProject;
236 currentProject= 0;
237
238 return load_project(name);
239 }
240
241
remove_project(const QString & name)242 int ProjectManager::remove_project( const QString& name )
243 {
244 // check if we are removing the currentProject, and delete it before removing its files
245 if (project_is_current(name)) {
246 PMESG("removing current project\n");
247 set_current_project(0);
248 }
249
250 QString oldrootdir = config().get_property("Project", "directory", "/directory/unknown/").toString() + "/" + name;
251 m_projectDirs.removeAll(oldrootdir);
252
253 return FileHelper::remove_recursively( name );
254 }
255
project_is_current(const QString & title)256 bool ProjectManager::project_is_current(const QString& title)
257 {
258 QString path = config().get_property("Project", "directory", "/directory/unknown").toString();
259 path += "/" + title;
260
261 if (currentProject && (currentProject->get_root_dir() == path)) {
262 return true;
263 }
264
265 return false;
266 }
267
project_exists(const QString & title)268 bool ProjectManager::project_exists(const QString& title)
269 {
270 QString project_dir = config().get_property("Project", "directory", "/directory/unknown").toString();
271 QString project_path = project_dir + "/" + title;
272 QFileInfo fileInfo(project_path);
273
274 if (fileInfo.exists()) {
275 return true;
276 }
277
278 return false;
279 }
280
save_project()281 Command* ProjectManager::save_project()
282 {
283 if (currentProject) {
284 currentProject->save();
285 } else {
286 info().information( tr("No Project to save, open or create a Project first!"));
287 }
288
289 return (Command*) 0;
290 }
291
get_project()292 Project * ProjectManager::get_project( )
293 {
294 return currentProject;
295 }
296
start()297 void ProjectManager::start()
298 {
299 QString projectsPath = config().get_property("Project", "directory", "/unknown/directory/").toString();
300
301 QDir dir;
302 if ( (projectsPath.isEmpty()) || (!dir.exists(projectsPath)) ) {
303 if (projectsPath.isEmpty()) {
304 projectsPath = QDir::homePath();
305 }
306
307 QString newPath = QFileDialog::getExistingDirectory(0,
308 tr("Choose a directory to store your Projects in"),
309 projectsPath );
310
311 if (newPath.isEmpty()) {
312 QMessageBox::warning( 0, tr("Traverso - Warning"),
313 tr("No directory was selected, to retry open the 'Open Project Dialog' and "
314 "click 'Select Project Directory' button\n"));
315 return;
316 }
317
318 QFileInfo fi(newPath);
319 if (dir.exists(newPath) && !fi.isWritable()) {
320 QMessageBox::warning( 0, tr("Traverso - Warning"),
321 tr("This directory is not writable by you! \n") +
322 tr("Please check permission for this directory or "
323 "choose another one:\n\n %1").arg(newPath) );
324 return;
325 }
326
327 if (dir.exists(newPath)) {
328 info().information(tr("Using existing Project directory: %1\n").arg(newPath));
329 } else if (!dir.mkpath(newPath)) {
330 QMessageBox::warning( 0, tr("Traverso - Warning"),
331 tr("Unable to create Project directory! \n") +
332 tr("Please check permission for this directory: %1").arg(newPath) );
333 return;
334 } else {
335 info().information(tr("Created new Project directory for you here: %1\n").arg(newPath));
336 }
337
338 QDir newdir(newPath);
339 config().set_property("Project", "directory", newdir.canonicalPath());
340 }
341
342 bool loadProjectAtStartUp = config().get_property("Project", "loadLastUsed", 1).toBool();
343
344 if (loadProjectAtStartUp) {
345 QString projectToLoad = config().get_property("Project", "current", "").toString();
346
347 if (projectToLoad.isNull() || projectToLoad.isEmpty()) {
348 projectToLoad="Untitled";
349 }
350
351 if (project_exists(projectToLoad)) {
352 load_project(projectToLoad);
353 } else {
354 if (projectToLoad != "Untitled") {
355 info().critical(tr("Project %1 no longer could be found! (Did you remove or rename the Project directory ?)").arg(projectToLoad));
356 } else {
357 Project* project;
358 if ( (project = create_new_project(1, 4, "Untitled")) ) {
359 project->set_description(tr("Default Project created by Traverso"));
360 project->save();
361 delete project;
362 load_project("Untitled");
363 } else {
364 PWARN("Cannot create project Untitled. Continuing anyway...");
365 }
366 }
367 }
368 } else {
369 set_current_project(0);
370 }
371 }
372
start(const QString & basepath,const QString & projectname)373 void ProjectManager::start(const QString & basepath, const QString & projectname)
374 {
375 config().set_property("Project", "directory", basepath);
376
377 if (project_exists(projectname)) {
378 load_project(projectname);
379 }
380 }
381
get_undogroup() const382 QUndoGroup* ProjectManager::get_undogroup() const
383 {
384 return &undogroup;
385 }
386
387
exit()388 Command* ProjectManager::exit()
389 {
390 PENTER;
391
392 if (currentProject) {
393 if (currentProject->get_sheets().size() == 0) {
394 // No sheets to unregister from the audiodevice,
395 // just save and quit:
396 set_current_project(0);
397 QApplication::exit();
398 return 0;
399 } else if (currentProject->is_save_to_close()) {
400 m_exitInProgress = true;
401 set_current_project(0);
402 } else {
403 return 0;
404 }
405 } else {
406 QApplication::exit();
407 }
408
409
410 return (Command*) 0;
411 }
412
scheduled_for_deletion(Sheet * sheet)413 void ProjectManager::scheduled_for_deletion( Sheet * sheet )
414 {
415 PENTER;
416 m_deletionSheetList.append(sheet);
417 }
418
delete_sheet(Sheet * sheet)419 void ProjectManager::delete_sheet( Sheet * sheet )
420 {
421 PENTER;
422 m_deletionSheetList.removeAll(sheet);
423 emit aboutToDelete(sheet);
424 delete sheet;
425
426 if (m_deletionSheetList.isEmpty() && m_exitInProgress) {
427 QApplication::exit();
428 }
429
430 }
431
undo()432 Command* ProjectManager::undo()
433 {
434 undogroup.undo();
435 return 0;
436 }
437
redo()438 Command* ProjectManager::redo()
439 {
440 undogroup.redo();
441 return 0;
442 }
443
444
rename_project_dir(const QString & olddir,const QString & newdir)445 int ProjectManager::rename_project_dir(const QString & olddir, const QString & newdir)
446 {
447 QDir dir(olddir);
448
449 m_projectDirs.removeAll(olddir);
450 m_projectDirs.append(newdir);
451
452 if ( ! dir.rename(olddir, newdir)) {
453 info().critical(tr("Could not rename Project directory to %1").arg(newdir));
454 return - 1;
455 }
456
457 return 1;
458 }
459
set_current_project_dir(const QString & path)460 void ProjectManager::set_current_project_dir(const QString & path)
461 {
462 if (path.isEmpty()) {
463 return;
464 }
465
466 QDir newdir(path);
467
468 config().set_property("Project", "directory", newdir.canonicalPath());
469
470 QStringList list = newdir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
471 m_projectDirs.clear();
472
473 foreach(const QString &string, list) {
474 m_projectDirs += path + "/" + string;
475 }
476
477 m_watcher->addPath(path);
478
479 emit projectDirChangeDetected();
480 }
481
project_dir_rename_detected(const QString & dirname)482 void ProjectManager::project_dir_rename_detected(const QString & dirname)
483 {
484 Q_UNUSED(dirname);
485
486 emit projectDirChangeDetected();
487
488 QString path = config().get_property("Project", "directory", "").toString();
489 QDir dir(path);
490
491 QStringList list = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
492
493 bool startwhining = false;
494 foreach(const QString &string, list) {
495 if (!m_projectDirs.contains(path + "/" + string)) {
496 startwhining = true;
497 break;
498 }
499 }
500
501
502 if (!startwhining) {
503 return;
504 }
505
506 emit unsupportedProjectDirChangeDetected();
507 }
508
add_valid_project_path(const QString & path)509 void ProjectManager::add_valid_project_path(const QString & path)
510 {
511 m_projectDirs.append(path);
512 }
513
remove_wrong_project_path(const QString & path)514 void ProjectManager::remove_wrong_project_path(const QString & path)
515 {
516 m_projectDirs.removeAll(path);
517 }
518
519
start_incremental_backup(const QString & projectname)520 void ProjectManager::start_incremental_backup(const QString& projectname)
521 {
522 if (! project_exists(projectname)) {
523 return;
524 }
525
526 QString project_dir = config().get_property("Project", "directory", "/directory/unknown").toString();
527 QString project_path = project_dir + "/" + projectname;
528 QString fileName = project_path + "/project.tpf";
529 QString backupdir = project_path + "/projectfilebackup";
530
531 // Check if the projectfilebackup directory still exist
532 QDir dir(backupdir);
533 if (!dir.exists(backupdir)) {
534 create_projectfilebackup_dir(project_path);
535 }
536
537 QFile reader(fileName);
538 if (!reader.open(QIODevice::ReadOnly)) {
539 info().warning(tr("Projectfile backup: The project file %1 could not be opened for reading (Reason: %2)").arg(fileName).arg(reader.errorString()));
540 return;
541 }
542
543 QDateTime time = QDateTime::currentDateTime();
544 QString writelocation = backupdir + "/" + time.toString() + "__" + QString::number(time.toTime_t());
545 QFile compressedWriter(writelocation);
546
547 if (!compressedWriter.open( QIODevice::WriteOnly ) ) {
548 compressedWriter.close();
549 return;
550 }
551
552
553 QByteArray array = reader.readAll();
554 QByteArray compressed = qCompress(array, 9);
555 QDataStream stream(&compressedWriter);
556 stream << compressed;
557
558 compressedWriter.close();
559 }
560
561
cleanup_backupfiles_for_project(const QString & projectname)562 void ProjectManager::cleanup_backupfiles_for_project(const QString & projectname)
563 {
564 if (! project_exists(projectname)) {
565 return;
566 }
567
568 QString project_dir = config().get_property("Project", "directory", "/directory/unknown").toString();
569 QString project_path = project_dir + "/" + projectname;
570 QString backupdir = project_path + "/projectfilebackup";
571
572 // Check if the projectfilebackup directory still exist
573 QDir dir(backupdir);
574 // A map to insert files based on their time value,
575 // so it's sorted on date automatically
576 QMap<int, QString> map;
577 QStringList entrylist = dir.entryList(QDir::Files);
578
579 // If there are more then 1000 saves, remove the last 200!
580 if (entrylist.size() > 1000) {
581 printf("more then thousand backup files, deleting oldest 200\n");
582
583 int key;
584 foreach (QString file, dir.entryList(QDir::Files)) {
585 key = file.right(10).toUInt();
586 map.insert(key, file);
587 }
588
589 QList<QString> tobedeleted = map.values();
590
591 if (tobedeleted.size() < 201) {
592 return;
593 }
594
595 for(int i=0; i<200; ++i) {
596 QFile file(backupdir + "/" + tobedeleted.at(i));
597 if ( ! file.remove() ) {
598 printf("Could not remove file %s (Reason: %s)\n", QS_C(tobedeleted.at(i)), QS_C(FileHelper::fileerror_to_string(file.error())));
599 }
600 }
601 }
602 }
603
604
restore_project_from_backup(const QString & projectname,uint restoretime)605 int ProjectManager::restore_project_from_backup(const QString& projectname, uint restoretime)
606 {
607 if (! project_exists(projectname)) {
608 return -1;
609 }
610 QString project_dir = config().get_property("Project", "directory", "/directory/unknown").toString();
611 QString project_path = project_dir + "/" + projectname;
612 QString backupDir = project_path + "/projectfilebackup";
613
614 if (currentProject) {
615 currentProject->save();
616 set_current_project(0);
617 delete currentProject;
618 currentProject = 0;
619 }
620
621 QString fileName = project_path + "/project.tpf";
622
623 QDir dir(backupDir);
624 QString backupfile;
625
626 foreach (QString backup, dir.entryList(QDir::Files)) {
627 if (backup.right(10).toUInt() == restoretime) {
628 backupfile = backupDir + "/" + backup;
629 printf("backupfile %s\n", QS_C(backupfile));
630 break;
631 }
632 }
633
634 QFile reader(backupfile);
635 if (!reader.open(QIODevice::ReadOnly)) {
636 //
637 reader.close();
638 return -1;
639 }
640
641
642 QFile writer(fileName);
643 if (!writer.open( QIODevice::WriteOnly | QIODevice::Text) ) {
644 PERROR("Could not open %s for writing!", QS_C(fileName));
645 writer.close();
646 return -1;
647 }
648
649 QDataStream dataIn(&reader);
650 QByteArray compByteArray;
651 dataIn >> compByteArray;
652
653 QByteArray a = qUncompress(compByteArray);
654 QTextStream stream(&writer);
655 stream << a;
656
657 writer.close();
658
659 return 1;
660 }
661
get_backup_date_times(const QString & projectname)662 QList< uint > ProjectManager::get_backup_date_times(const QString& projectname)
663 {
664 if (! project_exists(projectname)) {
665 return QList<uint>();
666 }
667 QString project_dir = config().get_property("Project", "directory", "/directory/unknown").toString();
668 QString backupDir = project_dir + "/" + projectname + "/projectfilebackup";
669
670 QList<uint> dateList;
671 QDir dir(backupDir);
672
673 foreach (QString filename, dir.entryList(QDir::Files)) {
674 bool ok;
675 uint date = filename.right(10).toUInt(&ok);
676 if (ok) {
677 dateList.append(date);
678 } else {
679 printf("filename: %s is not backupfile made by Traverso, removing it!\n", QS_C(filename));
680 QFile::remove(backupDir + "/" + filename);
681 }
682 }
683
684 return dateList;
685 }
686
create_projectfilebackup_dir(const QString & rootDir)687 int ProjectManager::create_projectfilebackup_dir(const QString& rootDir)
688 {
689 QDir dir;
690 QString path = rootDir + "/projectfilebackup/";
691
692 if (dir.mkdir(path) < 0) {
693 info().critical(tr("Cannot create dir %1").arg(path));
694 return -1;
695 }
696
697 return 1;
698 }
699
700