added action chaining and added the resize protocol (see: features/ui.js#600)...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2016-05-01 21:27:12 +03:00
parent 9059b615fd
commit d02795935e
5 changed files with 285 additions and 153 deletions

View File

@ -310,7 +310,8 @@ module.CurrentImageIndicator = core.ImageGridFeatures.Feature({
// - before animation when scaling up
// - after when scaling down
// This is done to make the visuals consistent...
['fitImage.pre fitRibbon.pre setScale.pre',
//['fitImage.pre fitRibbon.pre setScale.pre',
['resizing.pre',
function(w1){
var w0 = this.screenwidth
w1 = w1 || 1

View File

@ -206,8 +206,8 @@ var SingleImageActions = actions.Actions({
// Set scale 'units' for different viewes...
//
// NOTE: the units are actually properties used to get/set the values.
'single-image-scale-unit': 'screenwidth',
'ribbon-scale-unit': 'screenfit',
'single-image-scale-unit': 'screenfit',
'ribbon-scale-unit': 'screenwidth',
// The threshold from which the image block starts to tend to
// screen proportions...
@ -252,30 +252,7 @@ module.SingleImageView = core.ImageGridFeatures.Feature({
actions: SingleImageActions,
handlers:[
// XXX HACK: force browser to redraw off-screen images...
// ...it appears that chrome cheats by not resizing off-screen
// images properly after changing scale...
// XXX this is still not perfect...
// ...if needed do a .reload() / ctrl-r
// XXX try doing this a timeout after resize....
['focusImage',
function(){
var img = this.ribbons.getImage()
var d = Math.max(img.attr('preview-width')*1, img.attr('preview-width')*1)
var D = this.ribbons.getVisibleImageSize('max')
if(this.config['-single-image-redraw-on-focus']
// NOTE: redraw only when close to original preview
// size -- this is where chrome cheats and
// shows images blurry...
// XXX this causes some images to jump a bit, aligning
// to nearest pixel might fix this...
&& Math.abs(D-d)/D < 0.30
&& this.toggleSingleImage('?') == 'on'){
this.scale = this.scale
}
}],
['fitImage.post setScale.post',
['resizing.post',
function(){
// prevent this from doing anything while no viewer...
if(!this.ribbons
@ -321,6 +298,7 @@ module.SingleImageView = core.ImageGridFeatures.Feature({
if(state != pre_state){
this.config['ribbon-scale'] =
this[this.config['ribbon-scale-unit']]
this[this.config['single-image-scale-unit']] =
this.config['single-image-scale']
|| this[this.config['single-image-scale-unit']]
@ -346,6 +324,7 @@ module.SingleImageView = core.ImageGridFeatures.Feature({
if(state != pre_state){
this.config['single-image-scale'] =
this[this.config['single-image-scale-unit']]
this[this.config['ribbon-scale-unit']] =
this.config['ribbon-scale']
|| this[this.config['ribbon-scale-unit']]
@ -353,6 +332,43 @@ module.SingleImageView = core.ImageGridFeatures.Feature({
}
}
}],
// Force browser to redraw off-screen images...
//
// This appears that chrome cheats by not resizing off-screen
// images properly after changing scale...
//
// XXX this is still not perfect...
// ...if needed do a .reload() / ctrl-r
[[
'resizing.post',
'toggleSingleImage.pre',
],
function(){
this.__did_resize = true
}],
[[
'focusImage',
'toggleSingleImage',
],
function(){
var img = this.ribbons.getImage()
var d = Math.max(img.attr('preview-width')*1, img.attr('preview-width')*1)
var D = this.ribbons.getVisibleImageSize('max')
if(this.config['-single-image-redraw-on-focus']
&& this.toggleSingleImage('?') == 'on'
&& this.__did_resize
// only when close to original preview size
&& Math.abs(D-d)/D < 0.30){
// this forces chrome to redraw off-screen images...
this.scale = this.scale
// reset the resize flag...
delete this.__did_resize
}
}],
],
})

View File

@ -175,41 +175,60 @@ module.ViewerActions = actions.Actions({
'ribbon-focus-mode': 'visual',
},
// Images...
// XXX this seems like a hack...
// ...should this be here???
get images(){
return this.ribbons != null ? this.ribbons.images : null
// Ribbons...
//
// NOTE: this expects that ribbons will maintain .parent.images...
// NOTE: when getting rid of ribbons need to also remove the .parent
// reference...
get ribbons(){
return this.__ribbons },
set ribbons(ribbons){
this.__ribbons = ribbons
ribbons.parent = this
},
// NOTE: if ribbons are null this will have no effect...
set images(value){
if(this.ribbons != null){
this.ribbons.images = value
// Current image data...
//
// XXX experimental...
get image(){
return this.images && this.images[this.current] },
set image(val){
if(this.images){
this.images[this.current] = val
}
},
// Scaling...
//
// Normal scale...
get scale(){
return this.ribbons != null ? this.ribbons.scale() : null
},
return this.ribbons != null ? this.ribbons.scale() : null },
set scale(s){
this.setScale(s)
},
this.setScale(s) },
// Screen width in image blocks...
//
// NOTE: this will change depending on image block sizing...
// NOTE: this not usable for image blocks of different sizes...
get screenwidth(){
return this.ribbons != null ? this.ribbons.getScreenWidthImages() : null
},
return this.ribbons != null ? this.ribbons.getScreenWidthImages() : null },
set screenwidth(n){
this.fitImage(n)
},
this.fitImage(n, false) },
// Screen height in image blocks...
get screenheight(){
return this.ribbons != null ? this.ribbons.getScreenHeightRibbons() : null
},
return this.ribbons != null ? this.ribbons.getScreenHeightRibbons() : null },
set screenheight(n){
this.fitRibbon(n, false)
},
this.fitRibbon(n, false) },
// this is the size in image radii on the narrow side of the screen...
// Screen size in image radii on the narrow side of the screen...
//
// E.g.
//
// min(screenwidth, screenheight)
// screenfit = --------------------------------------
// min(image.width, image.height)
//
get screenfit(){
if(!this.ribbons || !this.ribbons.viewer){
return null
@ -569,16 +588,65 @@ module.ViewerActions = actions.Actions({
this.nextImage(w)
}],
// zooming...
// Zooming/scaling root action...
//
// Protocol:
// - all root zoom/scale action bust be wrapped in the .resizing action
//
// This will enable clients to attach to a single in/out point.
//
// NOTE: not intended for direct use...
resizing: ['- Zoom/', function(){}],
// Root zoom/sclae actions...
//
// XXX need to account for animations...
setScale: ['- Zoom/',
function(scale){
this.ribbons && scale && this.ribbons.scale(scale)
this.refresh()
this.resizing.chainCall(this, function(){
this.ribbons && scale && this.ribbons.scale(scale)
this.refresh()
})
}],
fitOrig: ['Zoom/Fit to original scale',
function(){
this.resizing.chainCall(this, function(){
this.ribbons.scale(1)
this.refresh()
})
}],
// NOTE: if this gets a count argument it will fit count images,
// default is one.
// NOTE: this will add .config['fit-overflow'] to odd counts if no
// overflow if passed.
// ...this is done to add ability to control scroll indication.
fitImage: ['Zoom/Fit image',
function(count, overflow){
this.resizing.chainCall(this, function(){
if(count != null){
overflow = overflow == false ? 0 : overflow
var o = overflow != null ? overflow
: count % 2 != 1 ? 0
: (this.config['fit-overflow'] || 0)
count += o
}
this.ribbons.fitImage(count)
this.refresh()
})
}],
// NOTE: this does not accout for ribbon spacing...
fitRibbon: ['Zoom/Fit ribbon vertically',
function(count, whole){
this.resizing.chainCall(this, function(){
this.ribbons.fitRibbon(count, whole)
this.refresh()
})
}],
// Zooming...
//
// Zooming is done by multiplying the current scale by config['zoom-step']
// and rounding to nearest discrete number of images to fit on screen.
zoomIn: ['Zoom/Zoom in',
@ -609,51 +677,19 @@ module.ViewerActions = actions.Actions({
this.scale /= (this.config['zoom-step'] || 1.2)
}
}],
fitOrig: ['Zoom/Fit to original scale',
function(){
this.ribbons.scale(1)
this.refresh()
}],
// NOTE: if this gets a count argument it will fit count images,
// default is one.
// NOTE: this will add .config['fit-overflow'] to odd counts if no
// overflow if passed.
// ...this is done to add ability to control scroll indication.
//
// XXX make these neutral to screen and image proportions...
fitImage: ['Zoom/Fit image',
function(count, overflow){
if(count != null){
overflow = overflow == false ? 0 : overflow
var o = overflow != null ? overflow
: count % 2 != 1 ? 0
: (this.config['fit-overflow'] || 0)
count += o
}
this.ribbons.fitImage(count)
this.refresh()
}],
// Scale presets...
//
fitMax: ['Zoom/Fit the maximum number of images',
function(){ this.fitImage(this.config['max-screen-images']) }],
// XXX make this viewer/image proportion independent....
fitSmall: ['Zoom/Show small image',
function(){ this.screenfit = 4 }],
// XXX make this viewer/image proportion independent....
fitNormal: ['Zoom/Show normal image',
function(){ this.screenfit = 1.2 }],
fitScreen: ['Zoom/Fit image to screen',
function(){ this.screenfit = 1 }],
fitRibbon: ['Zoom/Fit ribbon vertically',
function(count, whole){
this.ribbons.fitRibbon(count, whole)
this.refresh()
}],
// NOTE: these work by getting the target position from .data...
shiftImageTo: [
function(target){ return updateImagePosition(this, target) }],
@ -1456,7 +1492,7 @@ module.PartialRibbons = core.ImageGridFeatures.Feature({
this.updateRibbon('current', this.screenwidth / s || 1)
//this.preCacheJumpTargets()
}],
['fitImage.pre',
['resizing.pre',
function(n){
this.updateRibbon('current', n || 1)
//this.preCacheJumpTargets()
@ -2088,7 +2124,7 @@ module.AutoSingleImage = core.ImageGridFeatures.Feature({
},
handlers: [
['fitImage.pre',
['resizing.pre',
function(count){
count = count || 1

View File

@ -270,70 +270,14 @@ function Action(name, doc, ldoc, func){
// create the actual instance we will be returning...
var meth = function(){
var that = this
var args = args2array(arguments)
var res = this
var getHandlers = this.getHandlers || MetaActions.getHandlers
var isToggler = this.isToggler || MetaActions.isToggler
// get the handler list...
var handlers = getHandlers.call(this, name)
// special case: toggler -- do not handle special args...
if(isToggler.call(this, name)
&& args.length == 1
&& (args[0] == '?' || args[0] == '??')){
return handlers.slice(-1)[0].pre.apply(this, args)
}
// handlers: pre phase...
handlers
// NOTE: current action will get included and called by the code
// above and below, so no need to explicitly call func...
// NOTE: pre handlers are called FIFO, i.e. the last defined first...
.map(function(a){
if(a.pre){
res = a.pre.apply(that, args)
// if a handler returns a function or a deferred,
// register is as a post handler...
if(res
&& res !== that
&& (res instanceof Function
|| res.resolve instanceof Function)){
a.post = res
// reset the result...
res = that
}
}
return a
})
// return this if nothing specific is returned...
res = res === undefined ? this : res
// the post handlers get the result as the first argument...
args.splice(0, 0, res)
// handlers: post phase...
handlers
// NOTE: post handlers are called LIFO -- last defined last...
.reverse()
.forEach(function(a){
a.post
&& (a.post.resolve ?
a.post.resolve.apply(a.post, args)
: a.post.apply(that, args))
})
// action result...
return res
}
return meth.chainApply(this, null, arguments) }
meth.__proto__ = this.__proto__
// populate the action attributes...
meth.name = name
//meth.name = name
Object.defineProperty(meth, 'name', {
value: name,
})
meth.doc = doc
meth.long_doc = ldoc
@ -344,6 +288,84 @@ function Action(name, doc, ldoc, func){
// this will make action instances behave like real functions...
Action.prototype.__proto__ = Function
// XXX revise the structure....
// ...is it a better idea to define these in an object and assign that???
Action.prototype.chainApply = function(context, inner, args){
args = [].slice.call(args || [])
var res = context
var outer = this.name
var getHandlers = context.getHandlers || MetaActions.getHandlers
var isToggler = context.isToggler || MetaActions.isToggler
// get the handler list...
var handlers = getHandlers.call(context, outer)
// special case: toggler -- do not handle special args...
if(isToggler.call(context, outer)
&& args.length == 1
&& (args[0] == '?' || args[0] == '??')){
return handlers.slice(-1)[0].pre.apply(context, args)
}
// handlers: pre phase...
handlers
// NOTE: current action will get included and called by the code
// above and below, so no need to explicitly call func...
// NOTE: pre handlers are called FIFO, i.e. the last defined first...
.map(function(a){
if(a.pre){
res = a.pre.apply(context, args)
// if a handler returns a function or a deferred,
// register is as a post handler...
if(res
&& res !== context
&& (res instanceof Function
|| res.resolve instanceof Function)){
a.post = res
// reset the result...
res = context
}
}
return a
})
// call the inner action/function if preset....
if(inner){
//res = inner instanceof Function ?
inner instanceof Function ?
inner.call(context, args)
: inner instanceof Array && inner.length > 0 ?
context[inner.pop()].chainCall(context, inner, args)
: typeof(inner) == typeof('str') ?
context[inner].chainCall(context, null, args)
: null
}
// return this if nothing specific is returned...
res = res === undefined ? context : res
// the post handlers get the result as the first argument...
args.splice(0, 0, res)
// handlers: post phase...
handlers
// NOTE: post handlers are called LIFO -- last defined last...
.reverse()
.forEach(function(a){
a.post
&& (a.post.resolve ?
a.post.resolve.apply(a.post, args)
: a.post.apply(context, args))
})
// action result...
return res
}
Action.prototype.chainCall = function(context, inner){
return this.chainApply(context, inner, args2array(arguments).slice(2))
}
// A base action-set object...
@ -403,7 +425,7 @@ module.MetaActions = {
// go up the proto chain...
while(cur.__proto__ != null){
if(cur[n] != null && cur[n].doc != null){
res[n] = [ cur[n].doc, cur[n].long_doc ]
res[n] = [ cur[n].doc, cur[n].long_doc, cur[n].name ]
break
}
cur = cur.__proto__
@ -516,7 +538,6 @@ module.MetaActions = {
isToggler: doWithRootAction(function(action){
return action instanceof toggler.Toggler }),
// Register an action callback...
//
// Register a post action callback
@ -704,6 +725,36 @@ module.MetaActions = {
return this
},
// Apply/call a function/action "inside" an action...
//
// .chainApply(outer, inner)
// .chainApply(outer, inner, arguments)
// -> result
//
// .chainCall(outer, inner)
// .chainCall(outer, inner, ..)
// -> result
//
//
// The inner action call is completely nested as base of the outer
// action.
//
// Outer action o-------x o-------x
// v ^
// Inner action o---|---x
//
// The given arguments are passed as-is to both the outer and inner
// actions.
// The base inner action return value is passed to the outer action
// .post handlers.
//
// NOTE: these call the action's .chainApply(..) and .chainCall(..)
// methods, thus is not compatible with non-action methods...
// NOTE: .chianCall('action', ..) is equivalent to .action.chianCall(..)
chainApply: function(outer, inner, args){
return this[outer].chainApply(this, inner, args) },
chainCall: function(outer, inner){
return this[outer].chainApply(this, inner, args2array(arguments).slice(2)) },
// Get mixin object in inheritance chain...
//

View File

@ -300,6 +300,34 @@ var RibbonsPrototype = {
setElemGID: RibbonsClassPrototype.setElemGID,
get parent(){
return this.__parent
},
// NOTE: this will reset locally referenced .images to .parent.images
set parent(parent){
this.__parent = parent
delete this.__images
},
// maintain images in .parent.images if available...
//
// NOTE: images can be stored locally if no parent is set but will
// get overridden as soon as .parent is set.
get images(){
return this.parent ? this.parent.images : this.__images
},
set images(images){
if(this.parent){
this.parent.images = images
delete this.__images
} else {
this.__images = images
}
},
// Helpers...
// Prevent CSS transitions...