Wednesday 9 May 2012

Qt way to Include all source files

About a year ago I got roped into becoming a Qt developer for its nice and easy support for the Symbian platform. Using Qt gave you the ability to create cross-platform desktop (and Symbian) applications using the same IDE.

The big pain has always been whenever I added a new directory to a project.

In xCode I could re-drag and drop the folder into the project and it create the make file based on the files it found.

In Eclipse for Android, you could just modify the makefile to include *.cpp and *.h files in certain subfolders.

In Qt however, you had to individually specify the files to be included.
There were some workarounds where you'd create a .pro file per subfolder and somehow link them. I found this approach too confusing and didn't want to bloat my project source folders for the other platforms I supported.

The solution I came up with was to move out all the source files into a sources.pri file which was to be included by the main project file. I'd then run a Java script I wrote which parsed through all the directories and write in the cpp, c, and h files found.

project.pro file

# CCRELEASE: Remove for release
debug:DEFINES += DEBUGON

QT              += opengl network webkit network script phonon
TARGET    = 2play
TEMPLATE   = app

OTHER_FILES += \
    ../../Engine/* \
    ../../Engine/Shaders/basic.fx \
    ../../Engine/Shaders/phong.fx

include( sources.pri )
...
The important part is the one in bold stating include(sources.pri)

sources.pri file
INCLUDEPATH += \ 
  \ 
 ../Source \ 
 ../Source/Rendering \ 
 ../Source/Tools \ 
 ../../Engine/Source \ 
 ../../Engine/Source/AI \ 
 ../../Engine/Source/Objects \ 
 ../../Engine/Source/Rendering \ 
 ../../Engine/Source/Scenes \ 
 ../../Engine/Source/Tools \ 
 ../../Engine/Source/UI \ 
 ../../External/3dsloader \ 
 ../../External/jansson-2.1/src \ 
 ../../External/ObjLoader3 \ 
 ../../App/Source \ 

SOURCES += \ 
  \ 
 ../Source/CCFBWebView.cpp \ 
 ../Source/CCGLView.cpp \ 
 ../Source/CCMainWindow.cpp \ 
 ../Source/CCPlatform.cpp \ 
 ../Source/CCRenderThread.cpp \ 
 ../Source/CCVideoView.cpp \ 
 ../Source/main.cpp \ 
  \ 
 ../Source/Rendering/CCDeviceRenderer.cpp \ 
 ../Source/Rendering/CCTexturePNG.cpp \ 
  \ 
 ../Source/Tools/CCDeviceControls.cpp \ 
 ../Source/Tools/CCDeviceFileManager.cpp \ 
...

HEADERS += \ 
  \ 
 ../Source/CCFBWebView.h \ 
 ../Source/CCGLView.h \ 
 ../Source/CCMainWindow.h \ 
 ../Source/CCPlatform.h \ 
 ../Source/CCRenderThread.h \ 
 ../Source/CCVideoView.h \ 
  \ 
 ../Source/Rendering/CCDeviceRenderer.h \ 
 ../Source/Rendering/CCTexturePNG.h \ 
  \ 
 ../Source/Tools/CCDeviceControls.h \ 
 ../Source/Tools/CCDeviceFileManager.h \ 
...

Now the Java file simply loads in the sources.pri file, inputs in the folders listed in the INCLUDEPATH, then re-writes the file with all the source and header files.
import java.io.*;
import java.util.ArrayList;

import javax.swing.JOptionPane;

public class QtMaker 
{
 /**
  * @param args
  */
 public static void main(String[] args) 
 {
  String currentDirectory = System.getProperty( "user.dir" );
  String qtProjectDirectory = currentDirectory + "/";
  
  boolean findProjectDirectory = true;
  {
   // See if we're in a folder with a .pri file
   File dir = new File( qtProjectDirectory );
   String[] files = dir.list();
   for( int i=0; i<files.length; ++i )
   {
    String file = files[i];
    if( file.endsWith( ".pri" ) )
    {
     findProjectDirectory = false;
     break;
    }
   }
  }
  
  // Our project specific case when running the program from our Tools directory structure
  if( findProjectDirectory )
  {
   String[] splitResults = qtProjectDirectory.split( "/" );
   qtProjectDirectory = "";
   for( int i=0; i<splitResults.length; ++i )
   {
    String directory = splitResults[i];
    if( directory.equals( "Tools" ) )
    {
     break;
    }
    qtProjectDirectory += directory;
    qtProjectDirectory += "/";
   }
   
   qtProjectDirectory += "Dev/Qt/Project/";
  }
  
  // Find the first .pri file to play with
  String projectFilename = "sources.pri";
  {
   File dir = new File( qtProjectDirectory );
   String[] files = dir.list();
   for( int i=0; i<files.length; ++i )
   {
    String file = files[i];
    if( file.endsWith( ".pri" ) )
    {
     projectFilename = file;
     break;
    }
   }
  }
  
  File projectFile = new File( qtProjectDirectory + projectFilename );
  if( !projectFile.exists() )
  {
   JOptionPane.showMessageDialog( null, "Unable to find sources.pri\n" + qtProjectDirectory );
  }
  else
  {
   ArrayList<String> projectFileContents = new ArrayList<String>();
   try
   {
      FileInputStream fstream = new FileInputStream( projectFile.getPath() );
      DataInputStream in = new DataInputStream(fstream);
      BufferedReader br = new BufferedReader( new InputStreamReader( in ) );
      
      //Read File Line By Line
      String readLine;
      while( ( readLine = br.readLine() ) != null )
      {
       projectFileContents.add( readLine );
      }
      
      //Close the input stream
      in.close();   
   } 
   catch ( Exception e )
   {
    System.err.println( "Error: " + e.getMessage() ); 
   }
   
   if( projectFileContents.size() > 0 )
   {
    ArrayList<String> sourceDirectories = new ArrayList<String>();
    findSourceFilePaths( sourceDirectories, projectFileContents );
    
    ArrayList<String[]> sourceFiles = new ArrayList<String[]>(); 
    for( int i=0; i<sourceDirectories.size(); ++i )
    {
     String path = sourceDirectories.get( i );
     String directoryPath = qtProjectDirectory + path;
     File dir = new File( directoryPath );
     String[] files = dir.list();
     sourceFiles.add( files );
    }
    
    String outputString = "INCLUDEPATH += \\ \n";
    for( int i=0; i<sourceDirectories.size(); ++i )
    {
     String path = sourceDirectories.get( i );
     outputString += "\t" + path + " \\ \n";
    }
    
    outputString += "\n";
    
    outputString += "SOURCES\t+= \\ \n";
    for( int i=0; i<sourceFiles.size(); ++i )
    {
     String path = sourceDirectories.get( i );
     String[] files = sourceFiles.get( i );
     for( int j=0; j<files.length; ++j )
     {
      String file = files[j];
      if( file.endsWith( ".cpp" ) || file.endsWith( ".c" ) )
      {
       outputString += "\t" + path + "/" + file + " \\ \n";
      }
     }

     outputString += "\t \\ \n";
    }
    
    
    outputString += "\n";
    
    outputString += "HEADERS\t+= \\ \n";
    for( int i=0; i<sourceFiles.size(); ++i )
    {
     String path = sourceDirectories.get( i );
     String[] files = sourceFiles.get( i );
     for( int j=0; j<files.length; ++j )
     {
      String file = files[j];
      if( file.endsWith( ".h" ) )
      {
       outputString += "\t" + path + "/" + file + " \\ \n";
      }
     }

     outputString += "\t \\ \n";
    }
    
    try
    { 
     FileWriter fstream = new FileWriter( qtProjectDirectory + projectFilename );
     BufferedWriter out = new BufferedWriter( fstream );
     out.write( outputString );
     out.close();
    }
    catch ( Exception e )
    {
     System.err.println("Error: " + e.getMessage());
    }
    
    //JOptionPane.showMessageDialog( null, outputString );
   }
     }
 }

 static void findSourceFilePaths( ArrayList<String> sourceDirectories, ArrayList<String> projectFileContents )
 {
  int includePathStartIndex = 0;
  for( int i=0; i<projectFileContents.size(); ++i )
  {
   String line = projectFileContents.get( i );
   String[] splitResults = line.split( "INCLUDEPATH" );
   if( splitResults.length > 0 )
   {
    includePathStartIndex = i;
    break;
   }
  }
  
  for( int i=includePathStartIndex; i<projectFileContents.size(); ++i )
  {
   String line = projectFileContents.get( i );
   if( line.length() == 0 )
   {
    break;
   }
   
   if( line.equals( "\\" ) )
   {
    continue;
   }
   
   String[] splitResults = line.split( "\\\\" );
   if( splitResults.length == 0 )
   {
    break;
   }
   else
   {
    String directoryString = findDirectoryString( line );
    
    // Trim any invalid characters
    directoryString = directoryString.replaceAll( "\t", "" );
    
    // Make sure we don't add repeat folders
    boolean found = false;
    for( int j=0; j<sourceDirectories.size(); ++j )
    {
     String currentDirectory = sourceDirectories.get( j );
     if( currentDirectory.equals( directoryString ) )
     {
      found = true;
      break;
     }
    }
    
    if( found == false )
    {
     sourceDirectories.add( directoryString );
    }
   }
  }
  
  for( int i=0; i<sourceDirectories.size(); ++i )
  {
   String line = sourceDirectories.get( i );
   System.out.println( line );
  }
 }
 
 static String findDirectoryString(String line)
 {
  String[] splitResults = line.split( " " );

  int longestLength = 0;
  String longestString = "";
  for( int i=0; i<splitResults.length; ++i )
  {
   String result = splitResults[i];
   if( result.contains( "/" ) )
   {
    if( result.length() > longestLength )
    {
     longestLength = result.length();
     longestString = result;
    }
   }
  }
  
  return longestString;
 }
}

It's simple. It works. It saves me the pain of having to include files individually.

Hopefully it'll help someone out there too.

Monday 7 May 2012

Quran Project Release Candidate

It's been around 5 months since starting work with the Quran Project charity to bring an iOS port for their hardback book which provides a complete westernised introduction to Islam for non-muslims. A lot of work has been done over the months experimenting with the design and trying to programitcally convert a Microsoft Word document into a fluid 3D iOS app.

Initially we concepted out a 3D scene where you get to select the chapters via a 3D book interface.

However, this design was deemed to be too confusing and demanded a lot of 3d modelling concepts to go forward.

So we switched over to a Windows Phone 7 inspired 3D blended mix. Where you'd select the chapters via tiles over a 3D scene featuring the book.


However, the design didn't really work. It looked too unprofessional. Which lead us to scrapping the 3D scenery and going towards a more art driven chapter selection design.

It's taken 5 months, but I feel like we're close to having a build worth submitting. If anyone has any feedback or suggestions for further improvements, please feel free to leave a comment below.

For more information on the Quran Project charity, please visit their webpage at quranproject.org.