')
			.appendTo($('.viewer'))
	}
	return makeIndicator(text)
			.addClass(cls)
			.appendTo(c)
}
function showContextIndicator(cls, text){
	var c = $('.context-mode-indicators')
	if(c.length == 0){
		c = $('
')
			.addClass('context-mode-indicators')
			.append('
Context status')
			.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 = $('
')
				.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 = $('
')
	var bar = $('
')
	// the progress bar widget...
	var widget = $('
'+name+'
')
		// progress state...
		.append(state)
	// the close button...
	if(close !== false){
		widget
			.append($('
×')
				.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: $('
')
			.append($('
')
				.html(text))
			.append($('
')
				.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: 
'+ state +'.', 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, '
')
	var tags = data.tags
	tags = tags == null ? '' : tags.join(', ')
	return formDialog(null,
			('
'+
				'
"'+ name +'"
'+
				'
'+
					// basic info...
					// XXX BUG: something here breaks when self-generated data is 
					// 		currently open -- .length of undefined...
					'| 
 | 
'+
					'| GID: | '+ gid +' | 
'+
					'| Date: | '+ date +' | 
'+
					'| Path: | "'+ unescape(data.path) +'" | 
'+
					'| Orientation: | '+ orientation +'°'+flipped+' | 
'+
					'| Order: | '+ order +' | 
'+
					'| Position (ribbon): | '+ (DATA.ribbons[r].indexOf(gid)+1) +
						'/'+ DATA.ribbons[r].length +' | 
'+
					'| Position (global): | '+ (order+1) +'/'+ DATA.order.length +' | 
'+
					'| Sorted: | '+ 
						//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+'') +
					'% | 
'+
					// editable fields...
					'| 
 | 
'+
					// XXX this expanding to a too big size will mess up the screen...
					// 		add per editable and global dialog max-height and overflow
					'| Comment: | 
'+
					'| Tags: | '+ tags +' | 
'+
				'
'+
				'
'+
			'
'),
			// 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(/
/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: '),
			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 :										 */