Resources/js/d/dascyllus.js
author Mahlon E. Smith <mahlon@martini.nu>
Mon, 23 Sep 2013 09:10:55 -0700
changeset 1 b3419d05eabb
parent 0 80c32ef237c6
permissions -rw-r--r--
Checkpoint. * Got fonts working cross platform by only using SVG. This will change with the new webkit engine when Tide 1.4 is released. * Use the Tide API accessors for cross-window data. * Make the ThingFish server object an observable, so models and their URI bases can be changed easily. * Add options for saving window state and position. * Contextual menu updates/tests for later.


/*######################################################################
### D A S C Y L L U S   N A M E S P A C E
######################################################################*/
var D = {

	// Versions
	platform:     Ti.getPlatform(),
	name:         Ti.API.Application.getName(),
	version:      Ti.API.Application.getVersion(),
	ti_version:   Ti.getVersion(),
	architecture: Ti.Platform.getArchitecture(),
	copyright:    Ti.App.getCopyright(),

	// Declared windows, for easy referencing
	window: {},

	// Very first run of Dasycllus?
	firstrun: false,

	// Persistent storage DB handle.
	db: null,

	// The primary main tray menu.
	menu: null,

	// The current TF server we're connected to.
	//
	//     uri: http://localhost:8080/
	//     version: Thingfish 0.5.0 (build ...)
	//
	tf: new can.Observe({
		uri: null,
		version: null
	}),


	// Actions to perform at startup.
	//
	init: function() {
		var D = this;

		D.initDB();
		D.window.main = Ti.UI.getMainWindow();
		D.initMenu();

		// Thingfish server object event handlers.
		//
		D.tf.bind( 'uri', D.initTF );
		var server_uri = D.getPref( 'server_uri' );
		if ( server_uri ) D.tf.attr( 'uri', server_uri );

		// Main window event handlers.
		//
		D.window.main.addEventListener( 'moved', function() { D.saveWindowState(); });
		D.window.main.addEventListener( 'resized', function() {
			D.delay( function() {
				D.saveWindowState();
			},
			500, true );
		});
		$( document ).contextmenu( function() { D.window.main.setContextMenu( D.menu ); });

		// Each Tide window is an environment unto itself.  There is a global
		// key/val namespace, though.  Make the 'D' namespace accessible to other windows here.
		Ti.API.set( 'D', D );
	},


	// Determine if we should be running in developer mode.
	// This requires that the user has previously enabled it in preferences,
	// and the --debug command line option was passed.
	//
	devmode: function() {
		return Ti.App.getArguments()[0] == "--debug" && this.getBoolPref( 'devmode' );
	},


	// Event handler for changing the Thingfish server URI.
	// Sanity check and instantiate appropriate Model(s).
	//
	initTF: function( event, uri, old_uri ) {
		var serverOk = function( data, status, xhr ) {
			var info = xhr.getResponseHeader( 'x-thingfish' );
			if ( ! info ) {
				D.tf.attr( 'version', null );
				D.notify( 'Unable to connect.', 'Not a Thingfish server?' );
				return;
			}

			D.tf.attr( 'version', data.version )
			D.notify( 'Connected.', info );

			// FIXME: models?
		};

		try {
			$.ajax({
				url:      uri + '/serverinfo',
				async:    false,
				success:  serverOk,
				error:    function( xhr, status, err ) {
					D.tf.attr( 'version', null );
					D.notify( 'Unable to connect.', err ? err : 'Network error.' );
				},
				dataType: 'json'
			});
		}
		catch( err ) {
			console.log( err );
		};
	},


	// Open a handle to the local database, initializing it
	// if necessary and performing rudamentary upwards migrations.
	//
	initDB: function() {
		this.db = Ti.Database.openFile(
			Ti.Filesystem.getFile(
				Ti.Filesystem.getApplicationDataDirectory(), 'dascyllus.db'
			)
		);

		var rv = this.db.execute( 'PRAGMA user_version' );
		var schema_version = parseInt( rv.fieldByName('user_version') );

		var schemas_dir = Ti.Filesystem.getFile(
				Ti.Filesystem.getApplicationDirectory(), 'sql' );
		var current_version = schemas_dir.getDirectoryListing().length;

		if ( schema_version == 0 ) {
			this.notify( 'Welcome!', 'Dascyllus defaults installed.' );
			this.firstrun = true;
		}

		if ( schema_version != current_version ) {
			console.log( 'Dascyllus DB at version ' + schema_version +
					', needs to be upgraded to ' + current_version + '.' );

			while ( schema_version != current_version ) {
				var update_version = schema_version + 1;
				console.log( '  updating to version ' + update_version );

				var line;
				var schema_file = Ti.Filesystem.getFileStream(
						Ti.Filesystem.getApplicationDirectory(), 'sql', update_version + '.sql' );
				schema_file.open();

				this.db.execute( 'BEGIN' );
				while ( line = schema_file.readLine() ) this.db.execute( line.toString() );

				this.db.execute( 'PRAGMA user_version = ' + update_version  );
				this.db.execute( 'COMMIT' );

				schema_version = update_version;
				schema_file.close();
			}
		}

		return this.db;
	},


	// Return a preference's current value, or null if nonexistent.
	//
	getPref: function( pref ) {
		var row = this.db.execute( "SELECT val FROM prefs WHERE key=?", pref );
		if ( ! row.isValidRow() ) return null;

		var value = row.fieldByName( 'val' );
		row.close();

		return value;
	},


	// Return a preference's current value as a boolean.
	//
	getBoolPref: function( pref ) {
		return this.getPref( pref ) == '1'
	},


	// Insert or update a preference value.
	//
	setPref: function( pref, value ) {
		this.db.execute( 'BEGIN' );

		// Row already exists, remove it first.
		if ( this.getPref( pref ) != null )
			this.db.execute( 'DELETE FROM prefs WHERE key=?', pref );

		this.db.execute( 'INSERT INTO prefs VALUES ( ?, ? )', pref, value );
		this.db.execute( 'COMMIT' );
	},


	// Set a bool preference value.
	//
	setBoolPref: function( pref, value ) {
		this.setPref( pref, (value ? '1' : '0') );
	},


	// Generic delayed callback that returns the timer handle.
	//
	delay: (function() {
		var timer = null;
		return function( callback, ms, clear_previous ) {
			if ( clear_previous) clearTimeout( timer );
			timer = setTimeout( callback, ms );
			return timer;
		};
	})(),


	// Create and show a new notification.
	//
	notify: function( title, message, callback ) {
		if ( ! callback ) callback = function(){};

		var notification = Ti.Notification.createNotification({
			'title' : title,
			'message' : message,
			'timeout' : 10,
			'callback' : callback,
		});

		notification.show();
		return notification;
	},


	// Record the main window position and state.
	//
	saveWindowState: function() {
		var w      = this.window.main;
		var x      = w.getX();
		var y      = w.getY();
		var width  = w.getWidth();
		var height = w.getHeight();

		var hiddenMenu     = this.menu.getItemAt( 2 ).getSubmenu().getItemAt( 0 );
		var fullScreenMenu = this.menu.getItemAt( 2 ).getSubmenu().getItemAt( 1 );

		var pos = this.getPref( 'window_pos' );
		pos = Ti.JSON.parse( pos );

		// first time
		//
		if ( ! pos ) {
			pos =  {
				x: x,
				y: y,
				width: width,
				height: height,
				fullscreen: false,
				hidden: false
			};
		}

		if ( w.isVisible() ) {
			pos.hidden = false;
			fullScreenMenu.enable();
		}
		else {
			pos.hidden = true;
			fullScreenMenu.disable();
		}

		if ( w.isFullScreen() ) {
			pos.fullscreen = true;
			hiddenMenu.disable();
		}
		else {
			pos.fullscreen = false;
			hiddenMenu.enable();
		}

		if ( ! pos.fullscreen ) {
			pos.x      = x;
			pos.y      = y;
			pos.width  = width;
			pos.height = height;
		}

		this.setPref( 'window_pos', Ti.JSON.stringify(pos) );
	},


	// Set the main window according to previously saved values.
	//
	setWindowState: function() {
		var w   = this.window.main;
		var pos = this.getPref( 'window_pos' );

		var hiddenMenu     = this.menu.getItemAt( 2 ).getSubmenu().getItemAt( 0 );
		var fullScreenMenu = this.menu.getItemAt( 2 ).getSubmenu().getItemAt( 1 );

		pos = Ti.JSON.parse( pos );
		if ( ! pos ) return;

		if ( pos.fullscreen ) {
			w.setFullscreen( true );
			hiddenMenu.disable();
		}
		else {

			console.log( "Moving to x:" + pos.x + ", y:" + pos.y );
			console.log( "Sizing to w:" + pos.width + ", h:" + pos.height );
			w.moveTo(  pos.x, pos.y );
			w.setSize( pos.width, pos.height );

			hiddenMenu.enable();

			if ( pos.hidden ) {
				w.hide();
				fullScreenMenu.disable();
			}
			else {
				w.show();
				fullScreenMenu.enable();
			}
		}

		this.saveWindowState();
	},


	// Build the main menu.
	//
	initMenu: function() {
		this.menu = Ti.UI.createMenu();
		var tray  = Ti.UI.addTray( 'app://img/tray_icon.png' );

		// windows
		// ---------------------------------------------------------------
		this.menu.addItem( 'About', function() {
			if ( D.window.about && D.window.about.isVisible() ) {
				D.window.about.focus();
			}
			else {
				D.window.about = D.window.main.createWindow( 'app://window/about.html' );
				D.window.about.setTopMost( true );
				D.window.about.open();
			}
		});

		this.menu.addItem( 'Preferences', function() {
			if ( D.window.prefs && D.window.prefs.isVisible() ) {
				D.window.prefs.focus();
			}
			else {
				D.window.prefs = D.window.main.createWindow( 'app://window/prefs.html' );
				D.window.prefs.open();
			}
		});

		// view menu
		// ---------------------------------------------------------------
		var viewMenu = Ti.UI.createMenuItem( 'View' );

		viewMenu.addItem( 'Toggle Hidden', function() {
			if ( D.window.main.isVisible() ) {
				D.window.main.hide();
			}
			else {
				D.window.main.show();
			}
			D.saveWindowState();
		});

		viewMenu.addItem( 'Toggle Full Screen', function() {
			D.window.main.setFullscreen( ! D.window.main.isFullscreen() );
			D.saveWindowState();
		});

		this.menu.appendItem( viewMenu );

		// other
		// --------------------------------------------------------------
		if ( this.getBoolPref('devmode') ) {
			this.menu.addItem( 'Restart', function() {
				Ti.App.restart();
			});
	   }

		tray.setMenu( this.menu );
		Ti.UI.setDockMenu( this.menu );
		// D.window.main.setContextMenu( this.menu );
		// Ti.UI.setMenu( menu );
	},


	/*######################################################################
	### M O D E L S
	######################################################################*/

	// // A Thingfish asset.
	// //
	// initModel: function() {
	//     D.Asset = can.Model({
	//         id     : 'oid',
	//         findAll: 'GET '    + D.tf.uri + '/',
	//         findOne: 'GET '    + D.tf.uri + '/{oid}',
	//         create : 'POST '   + D.tf.uri + '/',
	//         update : 'PUT '    + D.tf.uri + '/{oid}',
	//         destroy: 'DELETE ' + D.tf.uri + '/{oid}',

	//         // TODO: other metadata hooks?  diff model?
	//         metadata: 'GET ' + D.tf.uri + '/{oid}/metadata',

	//     }, {}).
	//         bind( 'created', function( ev, asset ) {
	//             console.debug( "Created a new asset " + asset.id );
	//         }).
	//         bind( 'updated', function( ev, asset ) {
	//             console.debug( "Updated asset " + asset.id );
	//         }).
	//         bind( 'destroyed', function( ev, asset ) {
	//             console.debug( "Removed asset " + asset.id );
	//         }
	//     );
	// },
};