/**********************************************************************
*
*
*
**********************************************************************/
//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(
'single-ribbon-mode',
'Single ribbon mode (F3)')
.css('cursor', 'hand')
.click(function(){ toggleSingleRibbonMode() })
}
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 $('
')
.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 = $('
')
.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 = $('
')
.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 = $('')
}
elem
.addClass('overlay-info')
.html('')
.off()
if(typeof(data) == typeof('abc')){
elem.html(data)
} else {
elem.append(data)
}
return elem
.appendTo(target)
}
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 = $('')
}
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 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 $(''+
''+ text +''+
''+ text[0] +''+
'')
}
function showGlobalIndicator(cls, text){
var c = $('.global-mode-indicators')
if(c.length == 0){
c = $('
')
.addClass('global-mode-indicators')
.append('Global status')
.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...
//
function progressBar(name, container, close, hide_timeout){
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
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...
.append($('×')
.click(function(){
$(this).trigger('progressClose')
}))
.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){
done = done == null ? bar.attr('value') : done
bar.attr('value', done)
state.text(' (done)')
widget.find('.close').hide()
setTimeout(function(){
widget.hide()
}, hide_timeout)
})
.on('progressReset', function(){
widget
.css('display', '')
.find('.close')
.css('display', '')
state.text('')
bar.attr({
value: '',
max: '',
})
})
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){
return triggerProgressBarEvent(name, 'progressClose')
}
/**********************************************************************
* Modal dialogs...
*/
/********************************************************* Helpers ***/
// Set element text and tooltip
//
// NOTE: when text is a list, we will only use the first and the last
// elements...
// NOTE: if tip_elem is not given then both the text and tip will be set
// on text_elem
//
// XXX add support for quoted '|'...
function setTextWithTooltip(text, text_elem, tip_elem){
text_elem = $(text_elem)
tip_elem = tip_elem == null ? text_elem : tip_elem
if(typeof(text) != typeof('str')){
tip = text
} else {
var tip = text.split(/\s*\|\s*/)
}
// set elemnt text...
text_elem
.html(tip[0])
// do the tooltip...
tip = tip.slice(1)
tip = tip[tip.length-1]
if(tip != null && tip.trim().length > 0){
$(' *')
.attr('tooltip', tip)
.appendTo(tip_elem)
}
return text_elem
}
function getOverlay(root){
root = $(root)
var overlay = root.find('.overlay-block')
if(overlay.length == 0){
return $('
'+
''+
''+
'
').appendTo(root)
}
return overlay
}
function showInOverlay(root, data){
root = $(root)
var overlay = getOverlay(root)
if(data != null){
var container = $('
',
test: function(val){
return val === true || val === false
},
set: function(field, value){
if(value){
$(field).find('.value').attr('checked', '')
} else {
$(field).find('.value').removeAttr('checked')
}
},
get: function(field){
return $(field).find('.value').attr('checked') == 'checked'
},
},
// NOTE: this will not work without node-webkit...
// format:
// { dir: }
dir: {
type: 'dir',
text: null,
default: false,
html: '
'+
''+
''+
'
',
test: function(val){
return typeof(val) == typeof({}) && 'dir' in val
},
set: function(field, value){
field.find('.value').attr('nwworkingdir', value.dir)
},
get: function(field){
var f = $(field).find('.value')[0].files
if(f.length == 0){
return ''
}
return f[0].path
},
},
// NOTE: this will not work without node-webkit...
// format:
// { dir: }
// XXX add datalist option...
ndir: {
type: 'ndir',
text: null,
default: false,
html: '
'+
''+
''+
''+
'
',
test: function(val){
return typeof(val) == typeof({}) && 'ndir' in val
},
set: function(field, value){
var that = this
// NOTE: we are attaching the file browser to body to avoid
// click events on it closing the dialog...
// ...for some reason stopPropagation(...) does not do
// the job...
var file = $('')
.attr('nwworkingdir', value.ndir)
.change(function(){
var p = file[0].files
if(p.length != 0){
field.find('.path').val(p[0].path)
}
file.detach()
// focus+select the path field...
// NOTE: this is here to enable fast select-open
// keyboard cycle (tab, enter,