Added a progress dialog for file loading to respond to desktops while loading files. With large files the no-response policy could be a bad thing. My first real use case of multi-threading...

Fri, 24 May 2013 03:22:11 +0300

author
Santeri Piippo <crimsondusk64@gmail.com>
date
Fri, 24 May 2013 03:22:11 +0300
changeset 248
4431371f3ffe
parent 247
1a2ca515f683
child 249
6b2cc2d82ba6

Added a progress dialog for file loading to respond to desktops while loading files. With large files the no-response policy could be a bad thing. My first real use case of multi-threading...

src/addObjectDialog.cpp file | annotate | diff | comparison | revisions
src/colors.cpp file | annotate | diff | comparison | revisions
src/common.h file | annotate | diff | comparison | revisions
src/dialogs.cpp file | annotate | diff | comparison | revisions
src/dialogs.h file | annotate | diff | comparison | revisions
src/file.cpp file | annotate | diff | comparison | revisions
src/file.h file | annotate | diff | comparison | revisions
--- a/src/addObjectDialog.cpp	Thu May 23 17:37:25 2013 +0300
+++ b/src/addObjectDialog.cpp	Fri May 24 03:22:11 2013 +0300
@@ -185,7 +185,7 @@
 		break;
 	
 	default:
-		assert (false);
+		critical (fmt ("Unhandled LDObject type %d (%s) in AddObjectDialog", (int) type, g_saObjTypeNames[type]));
 		return;
 	}
 	
--- a/src/colors.cpp	Thu May 23 17:37:25 2013 +0300
+++ b/src/colors.cpp	Fri May 24 03:22:11 2013 +0300
@@ -20,6 +20,7 @@
 #include "colors.h"
 #include "file.h"
 #include "misc.h"
+#include "gui.h"
 #include <qcolor.h>
 
 static color* g_LDColors[MAX_COLORS];
@@ -79,8 +80,10 @@
 void parseLDConfig () {
 	FILE* fp = openLDrawFile ("LDConfig.ldr", false);
 	
-	if (!fp)
+	if (!fp) {
+		critical (fmt ("Unable to open LDConfig.ldr for parsing! (%s)", strerror (errno)));
 		return;
+	}
 	
 	// Read in the lines
 	char buf[1024];
--- a/src/common.h	Thu May 23 17:37:25 2013 +0300
+++ b/src/common.h	Fri May 24 03:22:11 2013 +0300
@@ -88,13 +88,42 @@
 private: \
 	T m_##GET; \
 public: \
-	const T& GET () const { return m_##GET; } \
+	T const& GET () const { return m_##GET; } \
 
 // Read/write private property with get and set accessors
 #define PROPERTY(T, GET, SET) \
 	READ_PROPERTY(T, GET) \
 	void SET (T val) { m_##GET = val; }
 
+// Property that triggers a callback when it is changed
+#define CALLBACK_PROPERTY(T, GET, SET) \
+	READ_PROPERTY(T, GET) \
+	void callback_##SET (); \
+	void SET (T val) { m_##GET = val; callback_##SET (); }
+
+// Property with thread locking, use when multiple threads access the same property
+// Comes with a callback function for detecting when the value is changed.
+#define THREAD_PROPERTY(T, GET, SET) \
+private: \
+	T m_##GET; \
+	bool m_threadLock_##GET; \
+public: \
+	const T& GET () const { \
+		while (m_threadLock_##GET) \
+			; \
+		return m_##GET; \
+	} \
+	void callback_##SET (); \
+	void SET (T val) { \
+		while (m_threadLock_##GET) \
+			; \
+		 \
+		m_threadLock_##GET = true; \
+		m_##GET = val; \
+		callback_##SET (); \
+		m_threadLock_##GET = false; \
+	}
+
 #ifdef null
 #undef null
 #endif // null
@@ -112,6 +141,7 @@
 
 void assertionFailure (const char* file, const ulong line, const char* funcname, const char* expr);
 void fatalError (const char* file, const ulong line, const char* funcname, str errmsg);
+
 #define assert(N) \
 	(N) ? ((void)(0)) : assertionFailure(__FILE__, __LINE__, FUNCNAME, #N)
 
@@ -169,6 +199,8 @@
 static const double pi = 3.14159265358979323846f;
 
 #ifdef IN_IDE_PARSER // KDevelop workarounds:
+#error IN_IDE_PARSER is defined (this code is only for KDevelop workarounds)
+
 // Current function name
 static const char* __func__ = "";
 
@@ -179,6 +211,8 @@
 #ifndef va_end
 #define va_end(va)
 #endif // va_end
+
+typedef void FILE; // :|
 #endif // IN_IDE_PARSER
 
 // -----------------------------------------------------------------------------
--- a/src/dialogs.cpp	Thu May 23 17:37:25 2013 +0300
+++ b/src/dialogs.cpp	Fri May 24 03:22:11 2013 +0300
@@ -25,6 +25,7 @@
 #include <QPushButton>
 #include <QBoxLayout>
 #include <QGridLayout>
+#include <qprogressbar.h>
 
 #include "dialogs.h"
 #include "widgets.h"
@@ -423,6 +424,9 @@
 	g_win->fullRefresh ();
 }
 
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
 RotationPointDialog::RotationPointDialog (QWidget* parent, Qt::WindowFlags f) : QDialog (parent, f) {
 	rb_rotpoint = new RadioBox ("Rotation Point", { "Object center", "Custom" }, 0, Qt::Vertical, this);
 	connect (rb_rotpoint, SIGNAL (valueChanged (int)), this, SLOT (radioBoxChanged ()));
@@ -471,4 +475,38 @@
 
 void RotationPointDialog::radioBoxChanged () {
 	setCustom (rb_rotpoint->value ());
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+OpenFileDialog::OpenFileDialog (QWidget* parent, Qt::WindowFlags f) : QDialog (parent, f) {
+	progressBar = new QProgressBar;
+	progressText = new QLabel;
+	setNumLines (0);
+	m_progress = 0;
+	
+	QDialogButtonBox* dbb_buttons = new QDialogButtonBox;
+	dbb_buttons->addButton (QDialogButtonBox::Cancel);
+	connect (dbb_buttons, SIGNAL (rejected ()), this, SLOT (reject ()));
+	
+	QVBoxLayout* layout = new QVBoxLayout (this);
+	layout->addWidget (progressText);
+	layout->addWidget (progressBar);
+	layout->addWidget (dbb_buttons);
+}
+
+void OpenFileDialog::callback_setNumLines () {
+	progressBar->setRange (0, numLines ());
+	updateValues ();
+}
+
+void OpenFileDialog::updateValues () {
+	progressBar->setValue (progress ());
+	progressText->setText (fmt ("%s: %lu/%lu lines parsed", fileName ().c (), progress (), numLines ()));
+}
+
+void OpenFileDialog::updateProgress (int progress) {
+	m_progress = progress;
+	updateValues ();
 }
\ No newline at end of file
--- a/src/dialogs.h	Thu May 23 17:37:25 2013 +0300
+++ b/src/dialogs.h	Fri May 24 03:22:11 2013 +0300
@@ -22,6 +22,7 @@
 #include <QDialog>
 #include "common.h"
 
+class QProgressBar;
 class QGroupBox;
 class QDialogButtonBox;
 class QDoubleSpinBox;
@@ -154,4 +155,24 @@
 	void radioBoxChanged ();
 };
 
+// =============================================================================
+class OpenFileDialog : public QDialog {
+	Q_OBJECT
+	READ_PROPERTY (ulong, progress)
+	CALLBACK_PROPERTY (ulong, numLines, setNumLines)
+	PROPERTY (str, fileName, setFileName)
+	
+public:
+	explicit OpenFileDialog (QWidget* parent = null, Qt::WindowFlags f = 0);
+	
+public slots:
+	void updateProgress (int progress);
+	
+private:
+	QProgressBar* progressBar;
+	QLabel* progressText;
+	
+	void updateValues ();
+};
+
 #endif // DIALOGS_H
\ No newline at end of file
--- a/src/file.cpp	Thu May 23 17:37:25 2013 +0300
+++ b/src/file.cpp	Fri May 24 03:22:11 2013 +0300
@@ -19,6 +19,7 @@
 #include <QMessageBox>
 #include <QFileDialog>
 #include <QDir>
+#include <qthread.h>
 
 #include <stdlib.h>
 #include "common.h"
@@ -34,6 +35,8 @@
 cfg (str, io_ldpath, "");
 cfg (str, io_recentfiles, "");
 
+static bool g_loadingMainFile = false;
+
 // =============================================================================
 namespace LDPaths {
 	static str pathError;
@@ -102,9 +105,9 @@
 }
 
 // =============================================================================
-LDOpenFile* findLoadedFile (str zName) {
+LDOpenFile* findLoadedFile (str name) {
 	for (LDOpenFile* file : g_loadedFiles)
-		if (file->m_filename == zName)
+		if (file->m_filename == name)
 			return file;
 	
 	return null;
@@ -138,6 +141,7 @@
 // =============================================================================
 FILE* openLDrawFile (str relpath, bool subdirs) {
 	printf ("%s: Try to open %s\n", __func__, relpath.c ());
+	
 #ifndef WIN32
 	relpath.replace ("\\", "/");
 #endif // WIN32
@@ -187,16 +191,12 @@
 // =============================================================================
 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 // =============================================================================
-std::vector<LDObject*> loadFileContents (FILE* fp, ulong* numWarnings) {
+void FileLoader::work () {
 	char line[1024];
-	vector<str> lines;
-	vector<LDObject*> objs;
-	ulong lnum = 0;
+	m_progress = 0;
+	abortflag = false;
 	
-	if (numWarnings)
-		*numWarnings = 0;
-	
-	while (fgets (line, sizeof line, fp)) {
+	while (fgets (line, sizeof line, filePointer ())) {
 		// Trim the trailing newline
 		str data = line;
 		while (data[~data - 1] == '\n' || data[~data - 1] == '\r')
@@ -206,27 +206,99 @@
 		assert (obj != null);
 		
 		// Check for parse errors and warn about tthem
-		if (obj->getType() == LDObject::Gibberish) {
+		if (obj->getType () == LDObject::Gibberish) {
 			logf (LOG_Warning, "Couldn't parse line #%lu: %s\n",
-				lnum, static_cast<LDGibberish*> (obj)->reason.chars());
+				m_progress + 1, static_cast<LDGibberish*> (obj)->reason.chars());
 			
 			logf (LOG_Warning, "- Line was: %s\n", data.chars());
 			
-			if (numWarnings)
-				(*numWarnings)++;
+			if (m_warningsPointer)
+				(*m_warningsPointer)++;
 		}
 		
-		objs.push_back (obj);
-		lnum++;
+		m_objs.push_back (obj);
+		m_progress++;
+		emit progressUpdate (m_progress);
+		
+		if (abortflag) {
+			// We were flagged for abortion, so abort.
+			for (LDObject* obj : m_objs)
+				delete obj;
+			
+			m_objs.clear ();
+			return;
+		}
 	}
 	
+	emit workDone ();
+	m_done = true;
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+std::vector<LDObject*> loadFileContents (FILE* fp, ulong* numWarnings, bool* ok) {
+	vector<str> lines;
+	vector<LDObject*> objs;
+	
+	if (numWarnings)
+		*numWarnings = 0;
+	
+	FileLoader* loader = new FileLoader;
+	loader->setFilePointer (fp);
+	loader->setWarningsPointer (numWarnings);
+	
+	// Calculate the amount of lines
+	ulong numLines = 0;
+	char line[1024];
+	while (fgets (line, sizeof line, fp))
+		numLines++;
+	
+	rewind (fp);
+	
+	if (g_loadingMainFile) {
+		// Show a progress dialog if we're loading the main file here and move
+		// the actual work to a separate thread as this can be a rather intensive
+		// operation and if we don't respond quickly enough, the program can be
+		// deemed inresponsive.. which is a bad thing.
+		
+		// Init the thread and move the loader into it
+		QThread* loaderThread = new QThread;
+		QObject::connect (loaderThread, SIGNAL (started ()), loader, SLOT (work ()));
+		QObject::connect (loaderThread, SIGNAL (finished ()), loader, SLOT (deleteLater ()));
+		loader->moveToThread (loaderThread);
+		loaderThread->start ();
+		
+		// Now create a progress dialog for the operation
+		OpenFileDialog* dlg = new OpenFileDialog (g_win);
+		dlg->setFileName ("???");
+		dlg->setNumLines (numLines);
+		
+		// Connect the loader in so we can actually show updates
+		QObject::connect (loader, SIGNAL (progressUpdate (int)), dlg, SLOT (updateProgress (int)));
+		QObject::connect (loader, SIGNAL (workDone ()), dlg, SLOT (accept ()));
+		
+		// Show the dialog. If the user hits cancel, tell the loader to abort.
+		if (!dlg->exec ())
+			loader->abortflag = true;
+	} else
+		loader->work ();
+	
+	// If we wanted the success value, supply that now
+	if (ok)
+		*ok = loader->done ();
+	
+	// If the loader was done, return the objects it generated
+	if (loader->done ())
+		objs = loader->objs ();
+	
 	return objs;
 }
 
 // =============================================================================
 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 // =============================================================================
-LDOpenFile* openDATFile (str path, bool search, bool mainfile) {
+LDOpenFile* openDATFile (str path, bool search) {
 	logf ("Opening %s...\n", path.chars());
 	
 	// Convert the file name to lowercase since some parts contain uppercase
@@ -243,14 +315,21 @@
 		return null;
 	}
 	
+	LDOpenFile* oldLoad = g_curfile;
 	LDOpenFile* load = new LDOpenFile;
 	load->m_filename = path;
 	
-	if (mainfile)
+	if (g_loadingMainFile)
 		g_curfile = load;
 	
 	ulong numWarnings;
-	std::vector<LDObject*> objs = loadFileContents (fp, &numWarnings);
+	bool ok;
+	std::vector<LDObject*> objs = loadFileContents (fp, &numWarnings, &ok);
+	
+	if (!ok) {
+		load = oldLoad;
+		return null;
+	}
 	
 	for (LDObject* obj : objs)
 		load->m_objs.push_back (obj);
@@ -392,14 +471,17 @@
 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 // =============================================================================
 void openMainFile (str path) {
+	g_loadingMainFile = true;
 	closeAll ();
 	
-	LDOpenFile* file = openDATFile (path, false, true);
+	LDOpenFile* file = openDATFile (path, false);
 	
 	if (!file) {
 		// Tell the user loading failed.
 		setlocale (LC_ALL, "C");
 		critical (fmt ("Failed to open %s: %s", path.chars(), strerror (errno)));
+		
+		g_loadingMainFile = false;
 		return;
 	}
 	
@@ -418,6 +500,7 @@
 	
 	// Add it to the recent files list.
 	addRecentFile (path);
+	g_loadingMainFile = false;
 }
 
 // =============================================================================
@@ -598,11 +681,15 @@
 			CHECK_TOKEN_COUNT (15)
 			CHECK_TOKEN_NUMBERS (1, 13)
 			
-			// Try open the file
-			LDOpenFile* pFile = loadSubfile (tokens[14]);
+			// Try open the file. Disable g_loadingMainFile temporarily since we're
+			// not loading the main file now, but the subfile
+			bool oldLoadingMainFile = g_loadingMainFile;
+			g_loadingMainFile = false;
+			LDOpenFile* load = loadSubfile (tokens[14]);
+			g_loadingMainFile = oldLoadingMainFile;
 			
 			// If we cannot open the file, mark it an error
-			if (!pFile)
+			if (!load)
 				return new LDGibberish (line, "Could not open referred file");
 			
 			LDSubfile* obj = new LDSubfile;
@@ -613,7 +700,7 @@
 				obj->transform[i] = atof (tokens[i + 5]); // 5 - 13
 			
 			obj->fileName = tokens[14];
-			obj->fileInfo = pFile;
+			obj->fileInfo = load;
 			return obj;
 		}
 	
@@ -683,13 +770,15 @@
 // =============================================================================
 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 // =============================================================================
-LDOpenFile* loadSubfile (str zFile) {
-	// Try open the file
-	LDOpenFile* pFile = findLoadedFile (zFile);
-	if (!pFile)
-		pFile = openDATFile (zFile, true, false);
+LDOpenFile* loadSubfile (str fname) {
+	// Try find the subfile in the list of loaded files
+	LDOpenFile* load = findLoadedFile (fname);
 	
-	return pFile;
+	// If it's not loaded, try open it
+	if (!load)
+		load = openDATFile (fname, true);
+	
+	return load;
 }
 
 // =============================================================================
@@ -710,12 +799,12 @@
 	// Go through all objects in the current file and reload the subfiles
 	for (LDObject* obj : g_curfile->m_objs) {
 		if (obj->getType() == LDObject::Subfile) {
-			// Note: ref->pFile is invalid right now since all subfiles were closed.
+			// Note: ref->fileInfo is invalid right now since all subfiles were closed.
 			LDSubfile* ref = static_cast<LDSubfile*> (obj);
-			LDOpenFile* pFile = loadSubfile (ref->fileName);
+			LDOpenFile* fileInfo = loadSubfile (ref->fileName);
 			
-			if (pFile)
-				ref->fileInfo = pFile;
+			if (fileInfo)
+				ref->fileInfo = fileInfo;
 			else {
 				// Couldn't load the file, mark it an error
 				ref->replace (new LDGibberish (ref->getContents (), "Could not open referred file"));
@@ -723,7 +812,7 @@
 		}
 		
 		// Reparse gibberish files. It could be that they are invalid because
-		// the file could not be opened. Circumstances may be different now.
+		// of loading errors. Circumstances may be different now.
 		if (obj->getType() == LDObject::Gibberish)
 			obj->replace (parseLine (static_cast<LDGibberish*> (obj)->contents));
 	}
--- a/src/file.h	Thu May 23 17:37:25 2013 +0300
+++ b/src/file.h	Fri May 24 03:22:11 2013 +0300
@@ -21,7 +21,9 @@
 
 #include "common.h"
 #include "ldtypes.h"
+#include <QObject>
 
+class OpenFileDialog;
 namespace LDPaths {
 	void initPaths ();
 	bool tryConfigure (str path);
@@ -94,7 +96,7 @@
 
 // Opens the given file and parses the LDraw code within. Returns a pointer
 // to the opened file or null on error.
-LDOpenFile* openDATFile (str path, bool search, bool mainfile);
+LDOpenFile* openDATFile (str path, bool search);
 
 // Opens the given file and returns a pointer to it, potentially looking in /parts and /p
 FILE* openLDrawFile (str path, bool bSubDirectories);
@@ -121,7 +123,7 @@
 // Init and parse parts.lst
 void initPartList ();
 
-std::vector< LDObject* > loadFileContents (FILE* fp, ulong* numWarnings);
+std::vector<LDObject*> loadFileContents (FILE* fp, ulong* numWarnings, bool* ok = null);
 
 extern vector<LDOpenFile*> g_loadedFiles;
 extern vector<partListEntry> g_PartList;
@@ -129,4 +131,24 @@
 str basename (str path);
 str dirname (str path);
 
+class FileLoader : public QObject {
+	Q_OBJECT
+	
+	READ_PROPERTY (std::vector<LDObject*>, objs)
+	READ_PROPERTY (bool, done)
+	READ_PROPERTY (ulong, progress)
+	PROPERTY (FILE*, filePointer, setFilePointer)
+	PROPERTY (ulong*, warningsPointer, setWarningsPointer)
+	
+public:
+	bool abortflag;
+	
+public slots:
+	void work ();
+	
+signals:
+	void progressUpdate (int progress);
+	void workDone ();
+};
+
 #endif // FILE_H
\ No newline at end of file

mercurial