diff --git a/ui (gen4)/features/history.js b/ui (gen4)/features/history.js index 1c7c1dea..b5d06f9d 100755 --- a/ui (gen4)/features/history.js +++ b/ui (gen4)/features/history.js @@ -342,7 +342,7 @@ var URLHistoryLocalStorageActions = actions.Actions({ } }], - saveURLHistory: ['History/', + storeURLHistory: ['History/', function(){ var history = this.config['url-history-local-storage-key'] if(history != null){ @@ -350,9 +350,9 @@ var URLHistoryLocalStorageActions = actions.Actions({ JSON.stringify(this.url_history) } - this.saveLocation() + this.storeLocation() }], - saveLocation: ['History/', + storeLocation: ['History/', function(){ var loaded = this.config['url-history-loaded-local-storage-key'] @@ -404,16 +404,16 @@ module.URLHistoryLocalStorage = core.ImageGridFeatures.Feature({ function(){ this.loadLastSavedBasePath() }], ['stop.pre', - function(){ this.saveURLHistory() }], + function(){ this.storeURLHistory() }], // save base_path... ['load', - function(){ this.location && this.location.path && this.saveLocation() }], + function(){ this.location && this.location.path && this.storeLocation() }], // save... ['pushURLToHistory dropURLFromHistory setTopURLHistory', function(){ - this.saveURLHistory() + this.storeURLHistory() }], // clear... ['clearURLHistory.pre', @@ -465,7 +465,7 @@ module.URLHistoryFSWriter = core.ImageGridFeatures.Feature({ var e = that.url_history[l.path] if(e != null){ e.open = l.method - that.saveURLHistory() + that.storeURLHistory() } else { that.pushURLToHistory(l.path, l.method) diff --git a/ui (gen4)/features/location.js b/ui (gen4)/features/location.js index d586f1b8..2616b716 100755 --- a/ui (gen4)/features/location.js +++ b/ui (gen4)/features/location.js @@ -97,8 +97,7 @@ var LocationActions = actions.Actions({ // NOTE: the method is needed to enable us to get the action return // value... set location(value){ - this.loadLocation(value) - }, + this.loadLocation(value) }, clearLoaction: ['File/Clear location', @@ -178,74 +177,124 @@ var LocationActions = actions.Actions({ return res }], - // XXX need a way to get save method... - // - rename .location.method to .location.load and add .location.save - // - treat method as protocol and add a method registry API - // this is more flexible as we can add as many methods per - // protocol as we need and add a command action: - // - execute command in current protocol - // .locationCall("command", ...) - // - execute command in specific protocol - // .locationCall('protocol:command', ..) - // - show current protocol - // .locationCall("?") - // - list commands - // .locationCall("??") - // .locationCall('protocol:??') - // we can implicitly define protocols via action attrs: - // loadIndex: ['...', - // {locationProtocol: 'file:load'}, - // function(){ ... }], - locationDispatch: ['- File/', + // XXX + // XXX should these have the same effect as .dispatch('location:*:load', location)??? + // ...another way to put it is should we call this from dispatch? + // ...feels like yes -- this will turn into another Action-like + // protocol... + // ...or we can use this to wrap the actual matching action... + _loadLocation: ['- File/Save location', + {protocol: 'location:*:load'}, + function(location){ + this.location.method = this.locationMethod(location) + this.dispatch('location:*:load', location) + }], + _saveLocation: ['- File/Save location', + {protocol: 'location:*:save'}, + function(location){ + this.location.method = this.locationMethod(location) + this.dispatch('location:*:save', location) }], + _locationMethod: ['- File/', + {protocol: 'location:?'}, + function(location){ + return (location || this.location).method || null }], + + + // format: + // { + // 'protocol:method': 'actionName', + // + // 'family:protocol:method': 'actionName', + // + // 'family:*': 'actionName', + // ... + // } + get protocols(){ + var cache = this.__location_protocol_cache = this.__location_protocol_cache + || this.cacheProtocols() + return cache + }, + cacheProtocols: ['- File/', + function(){ + var that = this + var res = {} + this.actions.forEach(function(n){ + var proto = that.getActionAttr(n, 'protocol') + if(proto){ + res[proto] = n + } + }) + return res + }], + dispatch: ['- File/', + core.doc` + + Execute command in specific protocol... + .dispatch('protocol:command', ..) + .dispatch('family:protocol:command', ..) + -> result + + XXX defaults... + + XXX introspection... + `, function(spec){ - spec = spec instanceof Array ? spec : spec.split(':') var args = [].slice.call(arguments, 1) + spec = spec instanceof Array ? spec : spec.split(':') - var action = spec.pop() - var protocol = spec.shift() || this.location.method + var cache = this.protocols + var protocols = Object.keys(cache) - // format: - // { - // 'protocol:method': 'actionName', - // ... - // } - var cache = this.__location_protocol_cache = - this.__location_protocol_cache || this.cacheLocationProtocols() + // get all matching paths... + var matches = protocols.slice() + .map(function(p){ return p.split(':') }) + spec.forEach(function(e, i){ + matches = matches + .filter(function(p){ return e == '*' || p[i] == e }) }) + matches = matches + // remove matches ending with '*'... (XXX ???) + .filter(function(p){ return p.slice(-1)[0] != '*' }) + .map(function(p){ return p.join(':') }) - // get protocol... - if(action == '?'){ - return protocol + // fill in the gaps... + var i = spec.indexOf('*') + while(spec.indexOf('*') >= 0){ + var handler = cache[spec.slice(0, i).concat('?').join(':')] + if(handler){ + spec[i] = this[handler].apply(this, args) + i = spec.indexOf('*') - // get available methods for protocol... - } else if(action == '??'){ - return Object.keys(cache) - .filter(function(e){ return e.startsWith(protocol + ':') }) - .map(function(e){ return e.split(':').pop() }) - - // list all protocols... - } else if(protocol == '??' && action == '*'){ - return Object.keys(cache) - .map(function(e){ return e.split(':').pop() }) - .unique() + // error... + // XXX how do we break out of this??? + } else { + throw ('No default defined for: '+ spec.slice(0, i+1).join(':')) + } + } - // list protocols implementing specific action... - } else if(protocol == '??'){ - return Object.keys(cache) - .filter(function(e){ return e.endsWith(':'+ action) }) - .map(function(e){ return e.split(':')[0] }) - .unique() + // introspection... + // XXX this supports only one '??' + var i = spec.indexOf('??') + if(i >= 0){ + var head = spec.slice(0, i).join(':') + var tail = spec.slice(i+1).join(':') + console.log(head, tail) + return protocols + .filter(function(p){ + return p.startsWith(head) + && (tail == '' + || (p.endsWith(tail) + && p.length > (head.length + tail.length + 2))) }) // call method... } else { - // XXX args??? - this[cache[protocol +':'+ action]].call(this) + var m = spec.join(':') + console.log('>>>', m) + + // XXX take all the matches and chain call them... + return this[cache[m]].apply(this, args) } }], - saveLocation: ['- File/Save location', - function(location){ - // XXX - this.locationDispatch('save') - }], + }) module.Location = core.ImageGridFeatures.Feature({ diff --git a/ui (gen4)/features/meta.js b/ui (gen4)/features/meta.js index 2fde753f..16d9a0bc 100755 --- a/ui (gen4)/features/meta.js +++ b/ui (gen4)/features/meta.js @@ -64,6 +64,7 @@ core.ImageGridFeatures.Feature('viewer-testing', [ 'ui-single-image', //'ui-partial-ribbons', + // XXX this still has problems... 'ui-partial-ribbons-2', 'marks', diff --git a/ui (gen4)/features/ui-partial-ribbons-2.js b/ui (gen4)/features/ui-partial-ribbons-2.js index 65037875..205c8d65 100755 --- a/ui (gen4)/features/ui-partial-ribbons-2.js +++ b/ui (gen4)/features/ui-partial-ribbons-2.js @@ -35,7 +35,10 @@ var PartialRibbonsActions = actions.Actions({ // 'resize' 'ribbons-in-place-update-mode': 'resize', - 'ribbons-in-place-update-timeout': 200, + 'ribbons-in-place-update-timeout': 100, + + // XXX + 'ribbon-update-timeout': 120, }, updateRibbon: ['- Interface/Update partial ribbon size', @@ -63,6 +66,7 @@ var PartialRibbonsActions = actions.Actions({ var t = Date.now() this.__last_ribbon_update = this.__last_ribbon_update || t var timeout = this.config['ribbons-in-place-update-timeout'] + var update_timeout = this.config['ribbon-update-timeout'] // localize transition prevention... // NOTE: we can't get ribbon via target directly here as @@ -93,8 +97,10 @@ var PartialRibbonsActions = actions.Actions({ // ribbon shorter than we expect... || (loaded < size && na + pa > loaded) // ribbon too long... - || loaded > size * threshold){ - //console.log('RESIZE') + || loaded > size * threshold + // passed hard threshold -- too close to edge... + || (nl < w && na > nl) || (pl < w && pa > pl)){ + //console.log('RESIZE (sync)') this.resizeRibbon(target, size) // more complex cases... @@ -104,7 +110,7 @@ var PartialRibbonsActions = actions.Actions({ || (pl < update_threshold && pa > pl) // loaded more than we need by threshold... || nl + pl + 1 > size + update_threshold){ - // resize mode... + // resize... if(this.config['ribbons-in-place-update-mode'] == 'resize' // no ribbon loaded... || r.length == 0 @@ -114,26 +120,36 @@ var PartialRibbonsActions = actions.Actions({ // full screen... || (this.toggleSingleImage && this.toggleSingleImage('?') == 'on')){ - //console.log('RESIZE', t-this.__last_ribbon_update) - this.resizeRibbon(target, size) + return function(){ + var that = this + // sync update... + if(update_timeout == null){ + //console.log('RESIZE (post)', t-this.__last_ribbon_update) + this.resizeRibbon(target, size) + + // async update... + } else { + this.__update_timeout + && clearTimeout(this.__update_timeout) + this.__update_timeout = setTimeout(function(){ + //console.log('RESIZE (timeout)', t-this.__last_ribbon_update) + delete that.__update_timeout + that.resizeRibbon(target, size) + }, update_timeout) + } + } // in-place update... // XXX this is faster than .resizeRibbon(..) but it's not - // used unconditionally because I can't get rid or + // used unconditionally because I can't get rid of // sync up images being replaced... // ...note that .resizeRibbon(..) is substantially // slower (updates DOM), i.e. introduces a lag, but // the results look OK... - // - // Approaches: - // - preloading a target section off-screen - // ...results in two freezes instead of one - // - CSS will-change: background-image (???) - // - revise .updateImage(..) - // - // Q: can this be done within 1/60s??? - // XXX one approach here might be: - // wait for images to preload and only then update... + // XXX approaches to try: + // - wait for images to preload and only then update... + // - preload images in part of a ribbon and when ready update... + // ...this is like the first but we wait for less images... } else { //console.log('UPDATE', t - this.__last_ribbon_update) var c = gids.indexOf(data.getImage('current', r_gid)) diff --git a/ui (gen4)/features/ui.js b/ui (gen4)/features/ui.js index 6d11bdba..c4fb0094 100755 --- a/ui (gen4)/features/ui.js +++ b/ui (gen4)/features/ui.js @@ -1927,6 +1927,8 @@ var ControlActions = actions.Actions({ // if true and ribbon is panned off screen, the image will be // centered, else behave just like partially off screen... 'center-off-screen-paned-images': false, + + 'mouse-wheel-scale': 0.5, }, // Image click events... @@ -2386,6 +2388,112 @@ var ControlActions = actions.Actions({ } })], + // XXX need: + // - prevent ribbon from scrolling off screen... + // - handle acceleration -- stop and update just before scrolling off the edge... + // - update... + // XXX might be a good idea to use the viewer instead of ribbons as source... + // ...this will prevent losing control of the ribbon when it goes out + // from under the cursor... + // ...detect via cursor within the vertical band of the ribbon... + toggleMouseWheelHandling: ['Interface/Mouse wheel handling', + toggler.Toggler(null, + function(){ + return this.ribbons + && this.ribbons.viewer + && this.ribbons.viewer.hasClass('mouse-wheel-scroll') ? + 'handling-mouse-wheel' + : 'none' }, + 'handling-mouse-wheel', + function(state){ + var that = this + + /* + var focus_central = function(rgid){ + // see if we need to change focus... + var current_ribbon = that.data.getRibbon() + if(current_ribbon == rgid){ + var central = that.ribbons.getImageByPosition('center', r) + var gid = that.ribbons.getElemGID(central) + // silently focus central image... + if(that.config['focus-central-image'] == 'silent'){ + that.data.focusImage(gid) + that.ribbons.focusImage(that.current) + + // focus central image in a normal manner... + } else if(that.config['focus-central-image']){ + that.data.focusImage(gid) + that.focusImage() + } + } + } + */ + + var setup = this.__wheel_handler_setup = this.__wheel_handler_setup + || function(_, target){ + var that = this + + var r = this.ribbons.getRibbon(target) + var rgid = this.ribbons.getElemGID(r) + + // XXX vertical scroll... + this.ribbons.viewer + .on('wheel', function(){ + }) + + // horizontal scroll... + r.on('wheel', function(){ + event.preventDefault() + + var s = that.config['mouse-wheel-scale'] || 1 + var vmin = Math.min(document.body.offsetWidth, document.body.offsetHeight) + var left = parseFloat(($(this).transform('translate3d') || [0])[0])/100 * vmin + + // XXX inertia problem -- it's too easy to scroll a ribbon off the screen... + // try: + // - limit speed + // - limit distance + // 1-2 screens -> stop for timeout before continue + // ...need to keep track of "scroll sessions" + + // XXX prevent scroll off screen.... + + // XXX prevent scroll off loaded edge... + + // XXX focus_central(rgid) when scroll slows down... + // (small deltaX or longer time between triggerings)... + + // XXX do we need to do requestAnimationFrame(..) render... + // ...see toggleRibbonPanHandling(..) for an implementation... + + // do the actual move... + r.transform({ + x: ((left - (event.deltaX * s)) / vmin * 100) + 'vmin', + }) + }) + + } + + // on... + if(state == 'on'){ + this.ribbons.viewer.addClass('mouse-wheel-scroll') + // NOTE: we are resetting this to avoid multiple setting + // handlers... + this.off('updateRibbon', setup) + this.on('updateRibbon', setup) + + this.data.ribbon_order.forEach(function(gid){ + setup.call(that, null, gid) }) + + // off... + } else { + this.ribbons.viewer.removeClass('mouse-wheel-scroll') + this.off('updateRibbon', setup) + + this.data.ribbon_order.forEach(function(gid){ + that.ribbons.getRibbon(gid).off('wheel') }) + } + })], togglePinchHandling: ['Interface/Pinch zoom handling', function(){ diff --git a/ui (gen4)/index.html b/ui (gen4)/index.html index 6bf41713..651e588a 100755 --- a/ui (gen4)/index.html +++ b/ui (gen4)/index.html @@ -58,7 +58,6 @@ if(window.require && window.nw){ - diff --git a/ui (gen4)/lib/transform.js b/ui (gen4)/lib/transform.js index 2dff98d9..6d40ac66 100755 --- a/ui (gen4)/lib/transform.js +++ b/ui (gen4)/lib/transform.js @@ -299,7 +299,11 @@ var transformEditor = function(){ var aliases = Object.keys(spec) - var r = reduce == 'sum' ? function(a, b){ return a + b } + var r = reduce == 'sum' ? function(a, b){ + return a == 0 ? b + : b == 0 ? a + : a == 0 && b == 0 ? 0 + : a + b } : reduce == 'mul' ? function(a, b){ return a * b } : reduce == 'last' ? function(a, b){ return b != null ? b : a } : reduce