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