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