/**********************************************************************
* 
*
*
**********************************************************************/

//var DEBUG = DEBUG != null ? DEBUG : true

var CURSOR_SHOW_THRESHOLD = 20
var CURSOR_HIDE_TIMEOUT = 1000

var STATUS_QUEUE = []
var STATUS_QUEUE_TIME = 200

var CONTEXT_INDICATOR_UPDATERS = []


// this can be:
// 	- 'floating'
// 	- 'panel'
var PROGRESS_WIDGET_CONTAINER = 'floating'
// can be between 0 and 3000
var PROGRESS_HIDE_TIMEOUT = 1500



/*********************************************************************/

// XXX revise...
// NOTE: to catch the click event correctly while the cursor is hidden
//		this must be the first to get the event...
// NOTE: this uses element.data to store the timer and cursor position...
function autoHideCursor(elem){
	elem = $(elem)
	var data = elem.data()
	elem
		.on('mousemove', function(evt){
			var cursor = elem.css('cursor')

			data._cursor_pos = data._cursor_pos == null || cursor != 'none' ?
						[evt.clientX, evt.clientY] 
					: data._cursor_pos

			// cursor visible -- extend visibility...
			if(cursor != 'none'){

				if(data._cursor_timeout != null){
					clearTimeout(data._cursor_timeout)
				}
				data._cursor_timeout = setTimeout(function(){
						if(Math.abs(evt.clientX - data._cursor_pos[0]) < CURSOR_SHOW_THRESHOLD 
								|| Math.abs(evt.clientY - data._cursor_pos[1]) < CURSOR_SHOW_THRESHOLD){

							elem.css('cursor', 'none')
						}
					}, CURSOR_HIDE_TIMEOUT)


			// cursor hidden -- if outside the threshold, show...
			} else if(Math.abs(evt.clientX - data._cursor_pos[0]) > CURSOR_SHOW_THRESHOLD 
				|| Math.abs(evt.clientY - data._cursor_pos[1]) > CURSOR_SHOW_THRESHOLD){

				elem.css('cursor', '')
			}
		})
		.click(function(evt){
			if(elem.css('cursor') == 'none'){
				//event.stopImmediatePropagation()
				//event.preventDefault()

				if(data._cursor_timeout != null){
					clearTimeout(data._cursor_timeout)
					data._cursor_timeout = null
				}

				elem.css('cursor', '')
				//return false
			}
		})
	return elem
}


/*
// XXX does not work...
// 		...does not show the cursor without moving it...
function showCursor(elem){
	elem = $(elem)
	var data = elem.data()
	if(data._cursor_timeout != null){
		clearTimeout(data._cursor_timeout)
	}
	elem.css('cursor', '')
}
*/


function setupIndicators(){
	showGlobalIndicator(
			'cropped-view', 
			'Cropped view (shift-F2/F3/C/F)')
		.css('cursor', 'hand')
		.click(function(){ uncropData() })
}


function makeContextIndicatorUpdater(image_class){
	var _updater = function(image){
		var indicator = $('.context-mode-indicators .current-image-'+image_class)
		if(image.hasClass(image_class)){
			indicator.addClass('shown')
		} else {
			indicator.removeClass('shown')
		}
	}
	CONTEXT_INDICATOR_UPDATERS.push(_updater)
	return _updater
}


function updateContextIndicators(image){
	image = image == null ? getImage() : $(image)

	CONTEXT_INDICATOR_UPDATERS.map(function(update){
		update(image)
	})	
}


function showCurrentMarker(){
	return $('<div/>')
		.addClass('current-marker')
		.css({
			opacity: '0',
			top: '0px',
			left: '0px',
		})
		.appendTo($('.ribbon-set'))
		.animate({
			'opacity': 1
		}, 500)
		.mouseover(function(){
			$('.current.image')
		})
}

function updateCurrentMarker(){
	var scale = getElementScale($('.ribbon-set'))
	var marker = $('.current-marker')
	var cur = $('.current.image')
	var w = cur.outerWidth(true)
	var h = cur.outerHeight(true)
	marker = marker.length == 0 ? showCurrentMarker() : marker 
	var d = getRelativeVisualPosition(marker, cur)
	return marker.css({
		top: parseFloat(marker.css('top')) + d.top/scale,
		left: parseFloat(marker.css('left')) + d.left/scale,
		// keep size same as the image...
		width: w,
		height: h,
	})
}


function flashIndicator(direction){
	var cls = {
		// shift up/down...
		prev: '.up-indicator',
		next: '.down-indicator',
		// hit start/end/top/bottom of view...
		start: '.start-indicator',
		end: '.end-indicator',
		top: '.top-indicator',
		bottom: '.bottom-indicator',
	}[direction]

	var indicator = $(cls)

	if(indicator.length == 0){
		indicator = $('<div>')
			.addClass(cls.replace('.', ''))
			.appendTo($('.viewer'))
	}

	return indicator
		// NOTE: this needs to be visible in all cases and key press 
		// 		rhythms... 
		.show()
		.delay(100)
		.fadeOut(300)
}


function showRibbonIndicator(){
	var cls = '.ribbon-indicator'
	var indicator = $(cls)

	if(indicator.length == 0){
		indicator = $('<div>')
			.addClass(cls.replace('.', ''))
			.appendTo($('.viewer'))
	}

	var r = getRibbonIndex()

	// get the base ribbon...
	var base = getBaseRibbonIndex()

	var r =  r == base ? r+'*' : r
	return indicator.text(r)
}


function flashRibbonIndicator(){
	var indicator = showRibbonIndicator()
	var cls = '.flashing-ribbon-indicator'

	var flashing_indicator = $(cls)

	if(flashing_indicator.length == 0){
		flashing_indicator = indicator
			.clone()
			.addClass(cls.replace('.', ''))
			.appendTo($('.viewer'))
	}

	return flashing_indicator
//		.stop()
//		.show()
//		.delay(200)
//		.fadeOut(500)
		.show()
		.delay(100)
		.fadeOut(300)
}


// Update an info element
//
// align can be:
// 	- top
// 	- bottom
//
// If target is an existing info container (class: overlay-info) then 
// just fill that.
function updateInfo(elem, data, target){
	var viewer = $('.viewer')
	target = target == null ? viewer : $(target)
	elem = elem == null ? $('.overlay-info') : $(elem)

	if(elem.length == 0){
		elem = $('<div/>')
	}

	elem
		.addClass('overlay-info')
		.html('')
		.off()

	if(typeof(data) == typeof('abc')){
		elem.html(data)
	} else {
		elem.append(data)
	}

	elem 
		.appendTo(target)

	return elem
}


function showInfo(elem, data, target){
	elem = elem == null ? $('.overlay-info') : elem
	elem = data == null ? elem : updateInfo(elem, data, traget)
	return elem.fadeIn()
}


function hideInfo(elem){
	elem = elem == null ? $('.overlay-info') : elem
	return elem.fadeOut()
}


// Update status message
//
// NOTE: this will update message content and return it as-is, things 
// 		like showing the message are to be done manually...
// 		see: showStatus(...) and showErrorStatus(...) for a higher level
// 		API...
// NOTE: in addition to showing user status, this will also log the 
// 		satus to browser console...
// NOTE: the message will be logged to console via either console.log(...)
// 		or console.error(...), if the message starts with "Error".
// NOTE: if message is null, then just return the status element...
//
// XXX add abbility to append and clear status...
function updateStatus(message){

	var elem = $('.global-status')
	if(elem.length == 0){
		elem = $('<div class="global-status"/>')
	}
	if(message == null){
		return elem
	}

	if(typeof(message) == typeof('s') && /^error.*/i.test(message)){
		console.error.apply(console, arguments)
	} else {
		console.log.apply(console, arguments)
	}

	if(arguments.length > 1){
		message = Array.apply(Array, arguments).join(' ')
	}

	return updateInfo(elem, message)
}


// Same as updateInfo(...) but will aslo show and animate-close the message
//
// XXX the next call will not reset the animation of the previous, rather 
// 		it will pause it and rezume...
// 		...not sure if this is correct.
function showStatus(message){
	return updateStatus.apply(null, arguments)
		//.stop()
		.stop(true, false)
		//.finish()
		.show()
		.delay(500)
		.fadeOut(800)
}


// Same as showStatus(...) but queue the message so as to display it for
// a meaningful amount of time...
//
//	- This will print the first message right away.
//	- Each consecutive message if STATUS_QUEUE_TIME has not passed yet 
//		will get queued.
//	- Once the STATUS_QUEUE_TIME has passed the next message is reported 
// 		and so on until the queue is empty.
//
// NOTE: for very a fast and large sequence of messages the reporting 
// 		may (will) take longer (significantly) than the actual "job"...
// NOTE: this will delay the logging also...
function showStatusQ(message){
	if(STATUS_QUEUE.length == 0){

		// end marker...
		STATUS_QUEUE.push(0)

		showStatus.apply(null, arguments)

		function _printer(){
			// if queue is empty we have nothing to do...
			if(STATUS_QUEUE.length == 1){
				STATUS_QUEUE.pop()
				return
			}
			// if not empty show a status and repeat...
			showStatus.apply(null, STATUS_QUEUE.pop())
			setTimeout(_printer, STATUS_QUEUE_TIME)
		}

		setTimeout(_printer, STATUS_QUEUE_TIME)

	// queue not empty...
	} else {
		STATUS_QUEUE.splice(1, 0, Array.apply(Array, arguments))
	}
}


// Same as showStatus(...) but will always add 'Error: ' to the start 
// of the message
//
// NOTE: this will show the message but will not hide it.
function showErrorStatus(message){
	message = Array.apply(Array, arguments)
	message.splice(0, 0, 'Error:')
	//return showStatusQ.apply(null, message)
	return updateStatus.apply(null, message)
		.one('click', function(){ $(this).fadeOut() })
		//.stop()
		.stop(true, false)
		//.finish()
		.show()
}


// shorthand methods...
function hideStatus(){
	// yes, this indeed looks funny -- to hide a status you need to show
	// it without any arguments... ;)
	return showStatus()
}
function getStatus(){
	return updateStatus()
}


function makeIndicator(text){
	return $('<span class="indicator expanding-text">'+
				'<span class="hidden">'+ text +'</span>'+
				'<span class="shown">'+ text[0] +'</span>'+
			'</span>')
}

function showGlobalIndicator(cls, text){
	var c = $('.global-mode-indicators')
	if(c.length == 0){
		c = $('<div>')
			.addClass('global-mode-indicators')
			.append('<span class="mode-tip">Global status</span>')
			.appendTo($('.viewer'))
	}
	return makeIndicator(text)
			.addClass(cls)
			.appendTo(c)
}
function showContextIndicator(cls, text){
	var c = $('.context-mode-indicators')
	if(c.length == 0){
		c = $('<div>')
			.addClass('context-mode-indicators')
			.append('<span class="mode-tip">Context status</span>')
			.appendTo($('.viewer'))
	}
	return makeIndicator(text)
			.addClass(cls)
			.appendTo(c)
}



/**********************************************************************
* Progress bar...
*/

// Make or get progress bar container...
//
// mode can be:
// 	- null			- default
// 	- 'floating'
// 	- 'panel'
function getProgressContainer(mode, parent){
	parent = parent == null ? $('.viewer') : parent
	mode = mode == null ? PROGRESS_WIDGET_CONTAINER : mode

	if(mode == 'floating'){
		// widget container...
		var container = parent.find('.progress-container')
		if(container.length == 0){
			container = $('<div class="progress-container"/>')
				.appendTo(parent)
		}
	} else {
		var container = getPanel('Progress')
		if(container.length == 0){
			container = makeSubPanel('Progress')
				.addClass('.progress-container')
		}

		container = container.find('.content')
	}

	return container
}


// Make or get progress bar by name...
//
// Setting close to false will disable the close button...
//
// Events:
// 	- progressUpdate (done, total)
// 		Triggered by user to update progress bar state.
//
// 		takes two arguments:
// 			done	- the number of done tasks
// 			total	- the total number of tasks
//
// 		Usage:
// 			widget.trigger('progressUpdate', [done, total])
// 			
// 		Shorthand:
// 			updateProgressBar(name, done[, total])
//
// 	- progressClose
// 		Triggered by the close button.
//		By default triggers the progressDone event.
//
// 		Shorthand:
// 			closeProgressBar(name[, msg])
//
// 	- progressDone
// 		Triggered by user or progressClose handler.
// 		Set the progress bar to done state and hide after hide_timeout.
//
// 	- progressReset
// 		Triggered by user or progressBar(..) if the progress bar already 
// 		exists and is hidden (display: none).
// 		Reset the progress bar to it's initial (indeterminite) state 
// 		and show it.
//
// 		Shorthand:
// 			resetProgressBar(name)
//
function progressBar(name, container, close, hide_timeout, auto_remove){
	container = container == null 
		? getProgressContainer() 
		: container
	close = close === undefined 
		? function(){ 
			$(this).trigger('progressDone') } 
		: close
	hide_timeout = hide_timeout == null ? PROGRESS_HIDE_TIMEOUT 
		: hide_timeout < 0 ? 0
		: hide_timeout > 3000 ? 3000
		: hide_timeout
	auto_remove = auto_remove == null ? true : auto_remove

	var widget = getProgressBar(name)

	// a progress bar already exists, reset it and return...
	// XXX should we re-bind the event handlers here???
	if(widget.length > 0 && widget.css('display') == 'none'){
		return widget.trigger('progressReset')
	}

	// fields we'll need to update...
	var state = $('<span class="progress-details"/>')
	var bar = $('<progress/>')

	// the progress bar widget...
	var widget = $('<div class="progress-bar" name="'+name+'">'+name+'</div>')
		// progress state...
		.append(state)
	// the close button...
	if(close !== false){
		widget
			.append($('<span class="close">&times;</span>')
				.click(function(){
					$(this).trigger('progressClose')
				}))
	}
	widget
		.append(bar)
		.appendTo(container)
		.on('progressUpdate', function(evt, done, total){
			done = done == null ? bar.attr('value') : done
			total = total == null ? bar.attr('max') : total
			bar.attr({
				value: done,
				max: total
			})
			state.text(' ('+done+' of '+total+')')
		})
		.on('progressDone', function(evt, done, msg){
			done = done == null ? bar.attr('value') : done
			msg = msg == null ? 'done' : msg
			bar.attr('value', done)
			state.text(' ('+msg+')')
			widget.find('.close').hide()

			setTimeout(function(){
				widget.hide()

				// XXX this is not a good way to go... 
				// 		need a clean way to reset...
				if(auto_remove){
					widget.remove()
				}
			}, hide_timeout)
		})
		.on('progressReset', function(){
			widget
				.css('display', '')
				.find('.close')
					.css('display', '')
			state.text('')
			bar.attr({
				value: '',
				max: '',
			})
		})

	if(close === false){
		widget.on('progressClose', function(evt, msg){
			if(msg != null){
				widget.trigger('progressDone', [null, msg]) 
			} else {
				widget.trigger('progressDone') 
			}
		})
	} else if(close != null){
		widget.on('progressClose', close)
	}

	bar = $(bar[0])
	state = $(state[0])
	widget = $(widget[0])

	return widget
}


function getProgressBar(name){
	return $('.progress-bar[name="'+name+'"]')
}



/******************************************* Event trigger helpers ***/

function triggerProgressBarEvent(name, evt, args){
	var widget = typeof(name) == typeof('str') 
		? getProgressBar(name) 
		: name
	return widget.trigger(evt, args)
}


function resetProgressBar(name){
	return triggerProgressBarEvent(name, 'progressReset')
}
function updateProgressBar(name, done, total){
	return triggerProgressBarEvent(name, 'progressUpdate', [done, total])
}
function closeProgressBar(name, msg){
	if(msg != null){
		return triggerProgressBarEvent(name, 'progressClose', [msg])
	}	
	return triggerProgressBarEvent(name, 'progressClose')
}



/**********************************************************************
* Dialogs...
*/

function detailedAlert(text, description, button){
	return formDialog(null, '', {'': {
		html: $('<details/>')
			.append($('<summary/>')
				.html(text))
			.append($('<span/>')
				.html(description))
	}}, button == null ? false : button, 'detailed-alert')
}


// NOTE: this will not work without node-webkit...
function getDir(message, dfl, btn){
	btn = btn == null ? 'OK' : btn
	dfl = dfl == null ? '' : dfl
	var res = $.Deferred()

	formDialog(null, message, {'': {ndir: dfl}}, btn, 'getDir')
		.done(function(data){ res.resolve(data['']) })
		.fail(function(){ res.reject() })

	return res
}



/***************************************** Domain-specific dialogs ***/

// XXX do reporting...
// XXX would be nice to save settings...
// 		...might be good to use datalist...
function exportPreviewsDialog(state, dfl){
	dfl = dfl == null ? BASE_URL : dfl

	// XXX make this more generic...
	// tell the user what state are we exporting...
	if(state == null){
		var imgs = 0
		// NOTE: we are not using order or image count as these sets may
		// 		be larger that the current crop...
		DATA.ribbons.map(function(e){
			imgs += e.length
		})
		state = toggleSingleImageMode('?') == 'on' ? 'current image' : state
		state = state == null && isViewCropped() ? 
			'cropped view: '+
				imgs+' images in '+
				DATA.ribbons.length+' ribbons' 
			: state
		state = state == null ?
			'all: '+
				imgs+' images in '+
				DATA.ribbons.length+' ribbons' 
			: state
	}

	var res = $.Deferred()

	updateStatus('Export...').show()

	// NOTE: we are not defining the object in-place here because some 
	// 		keys become unreadable with JS syntax preventing us from 
	// 		splitting the key into several lines...
	var cfg = {}
	var img_pattern = 'Image name pattern | '+
		'%f - full filename (same as %n%e)\n'+
		'%n - filename\n'+
		'%e - extension (with leading dot)\n'+
		'%(abc)m - if marked insert "abc"\n'+
		'%(abc)b - if bookmarked insert "abc"\n'+
		'%gid - long gid\n'+
		'%g - short gid\n'
	// multiple images...
	if(state != 'current image'){
		cfg[img_pattern +
				'%I - global order\n'+
				'%i - current selection order'] = '%f'
		cfg['Level directory name'] = 'fav'
	// single image...
	} else {
		cfg[img_pattern +
				'\n'+
				'NOTE: %i and %I are not supported for single\n'+
				'image exporting.'] = '%f'
	}
	cfg['Size | '+
			'The selected size is aproximate, the actual\n'+
			'preview will be copied from cache.\n'+
			'\n'+
			'NOTE: if not all previews are yet generated,\n'+
			'this will save the available previews, not all\n'+
			'of which may be of the right size, if this\n'+
			'happens wait till all the previews are done\n'+
			'and export again.'] = {
		select: ['Original image'].concat(PREVIEW_SIZES.slice().sort()),
		default: 1
	}
	cfg['Destination | '+
			'Relative paths are supported.\n\n'+
			'NOTE: All paths are relative to the curent\n'+
			'directory.'] = {ndir: dfl}

	var keys = Object.keys(cfg)

	formDialog(null, 'Export: <b>'+ state +'</b>.', cfg, 'OK', 'exportPreviewsDialog')
		.done(function(data){
			// get the form data...
			var name = data[keys[0]]
			if(state != 'current image'){
				var size = data[keys[2]]
				var path = normalizePath(data[keys[3]]) 
				var dir = data[keys[1]]

			} else {
				var size = data[keys[1]]
				var path = normalizePath(data[keys[2]])
			}
			size = size == 'Original image' ? Math.max.apply(null, PREVIEW_SIZES)*2 : parseInt(size)-5

			// do the actual exporting...
			// full state...
			if(state != 'current image'){
				exportImagesTo(path, name, dir, size)

			// single image...
			} else {
				exportImageTo(getImageGID(), path, name, size)
			}

			// XXX do real reporting...
			showStatusQ('Copying data...')
			res.resolve(data[''])
		})
		.fail(function(){ 
			showStatusQ('Export: canceled.')
			res.reject() 
		})

	return res
}


function loadDirectoryDialog(dfl){
	dfl = dfl == null ? BASE_URL : dfl

	updateStatus('Open...').show()

	formDialog(null, 'Path to open | To see list of previously loaded urls press ctrl-H.', {
		'': {ndir: dfl},
		'Precess previews': true,
	}, 'OK', 'loadDirectoryDialog')
		.done(function(data){
			var path = normalizePath(data[''].trim())
			var process_previews = data['Precess previews']

			// reset the modes...
			toggleSingleImageMode('off')
			toggleSingleRibbonMode('off')
			toggleMarkedOnlyView('off')

			// do the loading...
			statusNotify(loadDir(path, !process_previews))
				/*
				.done(function(){
					if(process_previews){ 
						showStatusQ('Previews: processing started...')
						// generate/attach previews...
						makeImagesPreviewsQ(DATA.order) 
							.done(function(){ 
								showStatusQ('Previews: processing done.')
							})
					}
				})
				*/
				.done(function(){
					// XXX is this the right place for this???
					pushURLHistory(BASE_URL)
				})
		})
		.fail(function(){
			showStatusQ('Open: canceled.')
		})
}


// XXX get EXIF, IPTC...
function showImageInfo(){
	var gid = getImageGID(getImage())
	var r = getRibbonIndex(getRibbon())
	var data = IMAGES[gid]
	var orientation = data.orientation
	orientation = orientation == null ? 0 : orientation
	var flipped = data.flipped
	flipped = flipped == null ? '' : ', flipped '+flipped+'ly'
	var order = DATA.order.indexOf(gid)
	var name = getImageFileName(gid)
	var date = new Date(data.ctime * 1000)
	var comment = data.comment
	comment = comment == null ? '' : comment
	comment = comment.replace(/\n/g, '<br>')
	var tags = data.tags
	tags = tags == null ? '' : tags.join(', ')

	return formDialog(null,
			('<div>'+
				'<h2>"'+ name +'"</h2>'+

				'<table>'+
					// basic info...
					// XXX BUG: something here breaks when self-generated data is 
					// 		currently open -- .length of undefined...
					'<tr><td colspan="2"><hr></td></tr>'+
					'<tr><td>GID: </td><td>'+ gid +'</td></tr>'+
					'<tr><td>Date: </td><td>'+ date +'</td></tr>'+
					'<tr><td>Path: </td><td>"'+ unescape(data.path) +'"</td></tr>'+
					'<tr><td>Orientation: </td><td>'+ orientation +'&deg;'+flipped+'</td></tr>'+
					'<tr><td>Order: </td><td>'+ order +'</td></tr>'+
					'<tr><td>Position (ribbon): </td><td>'+ (DATA.ribbons[r].indexOf(gid)+1) +
						'/'+ DATA.ribbons[r].length +'</td></tr>'+
					'<tr><td>Position (global): </td><td>'+ (order+1) +'/'+ DATA.order.length +'</td></tr>'+
					'<tr><td>Sorted: </td><td>'+ 
						//Math.round(((DATA.order.length-tagSelectAND('unsorted', DATA.order).length)/DATA.order.length)*100+'') +
						//Math.round(((DATA.order.length-tagSelectAND('unsorted').length)/DATA.order.length)*100+'') +
						Math.round(((DATA.order.length-TAGS['unsorted'].length)/DATA.order.length)*100+'') +
					'%</td></tr>'+

					// editable fields...
					'<tr><td colspan="2"><hr></td></tr>'+
					// XXX this expanding to a too big size will mess up the screen...
					// 		add per editable and global dialog max-height and overflow
					'<tr><td>Comment: </td><td class="comment" contenteditable>'+ comment +'</td></tr>'+
					'<tr><td>Tags: </td><td class="tags" contenteditable>'+ tags +'</td></tr>'+
				'</table>'+
				'<br>'+
			'</div>'),
			// NOTE: without a save button, there will be no way to accept the 
			// 		form on a touch-only device...
			{}, 'OK', 'showImageInfoDialog')

		// save the form data...
		.done(function(_, form){
			// comment...
			var ncomment = form.find('.comment').html()
			if(ncomment != comment){
				ncomment = ncomment.replace(/<br>/ig, '\n')
				if(ncomment.trim() == ''){
					delete data.comment
				} else {
					data.comment = ncomment
				}
				imageUpdated(gid)
			}

			// tags...
			var ntags = form.find('.tags').text().trim()
			if(ntags != tags){
				ntags = ntags.split(/\s*,\s*/)

				updateTags(ntags, gid)
			}
		})
}



/*********************************************************************/

// XXX need a propper:
// 		- update mechanics...
// 		- save mechanics
function makeCommentPanel(panel){
	return makeSubPanel(
			'Info: Comment', 
			$('Comment: <div class="comment" contenteditable/>'),
			panel, 
			true, 
			true)
}



/*********************************************************************/

function setupUI(viewer){
	console.log('UI: setup...')

	setupIndicators()

	return viewer
		.click(function(){
			if($('.ribbon').length == 0){
				loadDirectoryDialog()
			}
		})
		.on([
				'focusingImage',
				'fittingImages',
				//'updatingImageProportions',
				'horizontalShiftedImage',
			].join(' '), 
			function(){
				updateCurrentMarker()
			})

}
SETUP_BINDINGS.push(setupUI)



/**********************************************************************
* vim:set ts=4 sw=4 nowrap :										 */
