Skip to content

Instantly share code, notes, and snippets.

@johnspackman
Created August 9, 2019 14:49
Show Gist options
  • Save johnspackman/00f52b0a91308957654237517912a0a3 to your computer and use it in GitHub Desktop.
Save johnspackman/00f52b0a91308957654237517912a0a3 to your computer and use it in GitHub Desktop.
/**
* Image which preserves the aspect ratio while scaling the image and constrains
* the dimensions to stay within the min/max width/height. The image is placed
* centrally within the dimensions of the widget.
*
* Based on the Qooxdoo image
*/
qx.Class.define("grasshopper.af.ui.image.Image", {
extend : qx.ui.core.Widget,
/*
* ****************************************************************************
* CONSTRUCTOR
* ****************************************************************************
*/
/**
* @param source
* {String?null} The URL of the image to display.
*/
construct : function(source) {
this.__contentElements = {};
this.base(arguments);
if (source) {
this.setSource(source);
}
},
/*
* ****************************************************************************
* PROPERTIES
* ****************************************************************************
*/
properties : {
/** The URL of the image */
source : {
check : "String",
init : null,
nullable : true,
event : "changeSource",
apply : "_applySource",
themeable : true
},
/**
* Whether the image should be scaled to the given dimensions
*
* This is disabled by default because it prevents the usage of image
* clipping when enabled.
*/
scale : {
check : "Boolean",
init : true,
themeable : true,
apply : "_applyScale"
},
/**
* Whether to preserve the image ratio (ie prevent distortion), and which dimension
* to prioritise
*/
forceRatio : {
init : 'auto',
check : [ 'disabled', 'height', 'width', 'auto' ],
apply : '_applyDimension'
},
/**
* Whether to allow scaling the image up
*/
allowScaleUp : {
init : false,
check : "Boolean",
apply : "_applyDimension"
},
// overridden
appearance : {
refine : true,
init : "image"
},
// overridden
allowShrinkX : {
refine : true,
init : false
},
// overridden
allowShrinkY : {
refine : true,
init : false
},
// overridden
allowGrowX : {
refine : true,
init : false
},
// overridden
allowGrowY : {
refine : true,
init : false
}
},
/*
* ****************************************************************************
* EVENTS
* ****************************************************************************
*/
events : {
/**
* Fired if the image source can not be loaded.
*
* *Attention*: This event is only used for images which are loaded
* externally (aka unmanaged images).
*/
loadingFailed : "qx.event.type.Event",
/**
* Fired if the image has been loaded.
*
* *Attention*: This event is only used for images which are loaded
* externally (aka unmanaged images).
*/
loaded : "qx.event.type.Event"
},
/*
* ****************************************************************************
* MEMBERS
* ****************************************************************************
*/
members : {
__width : null,
__height : null,
__mode : null,
__contentElements : null,
__currentContentElement : null,
__wrapper : null,
// overridden
_onChangeTheme : function() {
this.base(arguments);
// restyle source (theme change might have changed the resolved url)
this._styleSource();
},
/*
* ---------------------------------------------------------------------------
* WIDGET API
* ---------------------------------------------------------------------------
*/
// overridden
getContentElement : function() {
return this.__getSuitableContentElement();
},
// overridden
_createContentElement : function() {
return this.__getSuitableContentElement();
},
// overridden
_getContentHint : function() {
return {
width : this.__width || 0,
height : this.__height || 0
};
},
_applyDimension: function(value, oldValue) {
this.base(arguments, value, oldValue);
this.__updateContentHint();
},
// overridden
_applyDecorator : function(value, old) {
this.base(arguments, value, old);
var source = this.getSource();
source = qx.util.AliasManager.getInstance().resolve(source);
var el = this.getContentElement();
if (this.__wrapper) {
el = el.getChild(0);
}
this.__setSource(el, source);
},
// overridden
_applyPadding : function(value, old, name) {
this.base(arguments, value, old, name);
var element = this.getContentElement();
if (this.__wrapper) {
element.getChild(0).setStyles({
top : this.getPaddingTop() || 0,
left : this.getPaddingLeft() || 0
});
} else {
element.setPadding(this.getPaddingLeft() || 0, this.getPaddingTop() || 0);
}
},
renderLayout : function(left, top, width, height) {
this.base(arguments, left, top, width, height);
var element = this.getContentElement();
if (this.__wrapper) {
element.getChild(0).setStyles({
width : width - (this.getPaddingLeft() || 0) - (this.getPaddingRight() || 0),
height : height - (this.getPaddingTop() || 0) - (this.getPaddingBottom() || 0),
top : this.getPaddingTop() || 0,
left : this.getPaddingLeft() || 0
});
}
},
/*
* ---------------------------------------------------------------------------
* IMAGE API
* ---------------------------------------------------------------------------
*/
// property apply, overridden
_applyEnabled : function(value, old) {
this.base(arguments, value, old);
if (this.getSource()) {
this._styleSource();
}
},
// property apply
_applySource : function(value) {
this._styleSource();
},
// property apply
_applyScale : function(value) {
this._styleSource();
},
/**
* Remembers the mode to keep track which contentElement is currently in
* use.
*
* @param mode
* {String} internal mode (alphaScaled|scaled|nonScaled)
*/
__setMode : function(mode) {
this.__mode = mode;
},
/**
* Returns the current mode if set. Otherwise checks the current source and
* the current scaling to determine the current mode.
*
* @return {String} current internal mode
*/
__getMode : function() {
if (this.__mode == null) {
var source = this.getSource();
var isPng = false;
if (source != null) {
isPng = qx.lang.String.endsWith(source, ".png");
}
if (this.getScale() && isPng && qx.core.Environment.get("css.alphaimageloaderneeded")) {
this.__mode = "alphaScaled";
} else if (this.getScale()) {
this.__mode = "scaled";
} else {
this.__mode = "nonScaled";
}
}
return this.__mode;
},
/**
* Creates a contentElement suitable for the current mode
*
* @param mode
* {String} internal mode
* @return {qx.html.Image} suitable image content element
*/
__createSuitableContentElement : function(mode) {
var scale;
var tagName;
if (mode == "alphaScaled") {
scale = true;
tagName = "div";
} else if (mode == "nonScaled") {
scale = false;
tagName = "div";
} else {
scale = true;
tagName = "img";
}
var element = new qx.html.Image(tagName);
element.setAttribute("$$widget", this.toHashCode());
element.setScale(scale);
element.setStyles({
"overflowX" : "hidden",
"overflowY" : "hidden",
"boxSizing" : "border-box"
});
if (qx.core.Environment.get("css.alphaimageloaderneeded")) {
var wrapper = this.__wrapper = new qx.html.Element("div");
wrapper.setAttribute("$$widget", this.toHashCode());
wrapper.setStyle("position", "absolute");
wrapper.add(element);
return wrapper;
}
return element;
},
/**
* Returns a contentElement suitable for the current mode
*
* @return {qx.html.Image} suitable image contentElement
*/
__getSuitableContentElement : function() {
if (this.$$disposed) {
return null;
}
var mode = this.__getMode();
if (this.__contentElements[mode] == null) {
this.__contentElements[mode] = this.__createSuitableContentElement(mode);
}
var element = this.__contentElements[mode];
if (!this.__currentContentElement) {
this.__currentContentElement = element;
}
return element;
},
/**
* Applies the source to the clipped image instance or preload an image to
* detect sizes and apply it afterwards.
*
*/
_styleSource : function() {
var source = qx.util.AliasManager.getInstance().resolve(this.getSource());
var element = this.getContentElement();
if (this.__wrapper) {
element = element.getChild(0);
}
if (!source) {
element.resetSource();
return;
}
this.__checkForContentElementSwitch(source);
if ((qx.core.Environment.get("engine.name") == "mshtml")
&& (parseInt(qx.core.Environment.get("engine.version"), 10) < 9 || qx.core.Environment.get("browser.documentmode") < 9)) {
var repeat = this.getScale() ? "scale" : "no-repeat";
element.tagNameHint = qx.bom.element.Decoration.getTagName(repeat, source);
}
var contentEl = this.__currentContentElement;
if (this.__wrapper) {
contentEl = contentEl.getChild(0);
}
// Detect if the image registry knows this image
if (qx.util.ResourceManager.getInstance().has(source)) {
this.__setManagedImage(contentEl, source);
} else if (qx.io.ImageLoader.isLoaded(source)) {
this.__setUnmanagedImage(contentEl, source);
} else {
this.__loadUnmanagedImage(contentEl, source);
}
},
/**
* Checks if the current content element is capable to display the image
* with the current settings (scaling, alpha PNG)
*
* @param source
* {String} source of the image
*/
__checkForContentElementSwitch : qx.core.Environment.select("engine.name", {
"mshtml" : function(source) {
var alphaImageLoader = qx.core.Environment.get("css.alphaimageloaderneeded");
var isPng = qx.lang.String.endsWith(source, ".png");
if (alphaImageLoader && isPng) {
if (this.getScale() && this.__getMode() != "alphaScaled") {
this.__setMode("alphaScaled");
} else if (!this.getScale() && this.__getMode() != "nonScaled") {
this.__setMode("nonScaled");
}
} else {
if (this.getScale() && this.__getMode() != "scaled") {
this.__setMode("scaled");
} else if (!this.getScale() && this.__getMode() != "nonScaled") {
this.__setMode("nonScaled");
}
}
this.__checkForContentElementReplacement(this.__getSuitableContentElement());
},
"default" : function(source) {
if (this.getScale() && this.__getMode() != "scaled") {
this.__setMode("scaled");
} else if (!this.getScale() && this.__getMode("nonScaled")) {
this.__setMode("nonScaled");
}
this.__checkForContentElementReplacement(this.__getSuitableContentElement());
}
}),
/**
* Checks the current child and replaces it if necessary
*
* @param elementToAdd
* {qx.html.Image} content element to add
*/
__checkForContentElementReplacement : function(elementToAdd) {
var currentContentElement = this.__currentContentElement;
if (currentContentElement != elementToAdd) {
if (currentContentElement != null) {
var pixel = "px";
var styles = {};
// Copy dimension and location of the current content element
var bounds = this.getBounds();
if (bounds != null) {
styles.width = bounds.width + pixel;
styles.height = bounds.height + pixel;
}
var insets = this.getInsets();
styles.left = parseInt(currentContentElement.getStyle("left") || insets.left) + pixel;
styles.top = parseInt(currentContentElement.getStyle("top") || insets.top) + pixel;
styles.zIndex = 10;
var newEl = this.__wrapper ? elementToAdd.getChild(0) : elementToAdd;
newEl.setStyles(styles, true);
newEl.setSelectable(this.getSelectable());
var container = currentContentElement.getParent();
if (container) {
var index = container.getChildren().indexOf(currentContentElement);
container.removeAt(index);
container.addAt(elementToAdd, index);
}
// force re-application of source so __setSource is called again
var hint = newEl.getNodeName();
newEl.setSource(null);
var currentEl = this.__wrapper ? this.__currentContentElement.getChild(0) : this.__currentContentElement;
newEl.tagNameHint = hint;
newEl.setAttribute("class", currentEl.getAttribute("class"));
// Flush elements to make sure the DOM elements are created.
qx.html.Element.flush();
var currentDomEl = currentEl.getDomElement();
var newDomEl = elementToAdd.getDomElement();
if (currentDomEl && newDomEl) {
// Switch the DOM elements' hash codes. This is required for the
// event
// layer to work [BUG #7447]
var currentHash = currentDomEl.$$hash;
currentDomEl.$$hash = newDomEl.$$hash;
newDomEl.$$hash = currentHash;
}
this.__currentContentElement = elementToAdd;
}
}
},
/**
* Use the ResourceManager to set a managed image
*
* @param el
* {Element} image DOM element
* @param source
* {String} source path
*/
__setManagedImage : function(el, source) {
var ResourceManager = qx.util.ResourceManager.getInstance();
// Try to find a disabled image in registry
if (!this.getEnabled()) {
var disabled = source.replace(/\.([a-z]+)$/, "-disabled.$1");
if (ResourceManager.has(disabled)) {
source = disabled;
this.addState("replacement");
} else {
this.removeState("replacement");
}
}
// Optimize case for enabled changes when no disabled image was found
if (el.getSource() === source) {
return;
}
// Apply source
this.__setSource(el, source);
// Compare with old sizes and relayout if necessary
this.__updateContentHint(ResourceManager.getImageWidth(source), ResourceManager.getImageHeight(source));
},
/**
* Use the infos of the ImageLoader to set an unmanaged image
*
* @param el
* {Element} image DOM element
* @param source
* {String} source path
*/
__setUnmanagedImage : function(el, source) {
var ImageLoader = qx.io.ImageLoader;
// Apply source
this.__setSource(el, source);
// Compare with old sizes and relayout if necessary
var width = ImageLoader.getWidth(source);
var height = ImageLoader.getHeight(source);
this.__updateContentHint(width, height);
},
/**
* Use the ImageLoader to load an unmanaged image
*
* @param el
* {Element} image DOM element
* @param source
* {String} source path
*/
__loadUnmanagedImage : function(el, source) {
var ImageLoader = qx.io.ImageLoader;
if (qx.core.Environment.get("qx.debug")) {
// loading external images via HTTP/HTTPS is a common usecase, as is
// using data URLs.
var sourceLC = source.toLowerCase();
var startsWith = qx.lang.String.startsWith;
if (!startsWith(sourceLC, "http") && !startsWith(sourceLC, "data:image/")) {
var self = this.self(arguments);
if (!self.__warned) {
self.__warned = {};
}
if (!self.__warned[source]) {
this.debug("try to load an unmanaged relative image: " + source);
self.__warned[source] = true;
}
}
}
// only try to load the image if it not already failed
if (!ImageLoader.isFailed(source)) {
ImageLoader.load(source, this.__loaderCallback, this);
} else {
if (el != null) {
el.resetSource();
}
}
},
/**
* Combines the decorator's image styles with our own image to make sure
* gradient and backgroundImage decorators work on Images.
*
* @param el
* {Element} image DOM element
* @param source
* {String} source path
*/
__setSource : function(el, source) {
if (el.getNodeName() == "div") {
var dec = qx.theme.manager.Decoration.getInstance().resolve(this.getDecorator());
// if the decorator defines any CSS background-image
if (dec) {
var hasGradient = (dec.getStartColor() && dec.getEndColor());
var hasBackground = dec.getBackgroundImage();
if (hasGradient || hasBackground) {
var repeat = this.getScale() ? "scale" : "no-repeat";
// get the style attributes for the given source
var attr = qx.bom.element.Decoration.getAttributes(source, repeat);
// get the background image(s) defined by the decorator
var decStyle = dec.getStyles(true);
var combinedStyles = {
"backgroundImage" : attr.style.backgroundImage,
"backgroundPosition" : (attr.style.backgroundPosition || "0 0"),
"backgroundRepeat" : (attr.style.backgroundRepeat || "no-repeat")
};
if (hasBackground) {
combinedStyles["backgroundPosition"] += "," + decStyle["background-position"] || "0 0";
combinedStyles["backgroundRepeat"] += ", " + dec.getBackgroundRepeat();
}
if (hasGradient) {
combinedStyles["backgroundPosition"] += ", 0 0";
combinedStyles["backgroundRepeat"] += ", no-repeat";
}
combinedStyles["backgroundImage"] += "," + decStyle["background-image"];
// apply combined background images
el.setStyles(combinedStyles);
return;
}
} else {
// force re-apply to remove old decorator styles
el.setSource(null);
}
}
el.setSource(source);
},
/**
* Event handler fired after the preloader has finished loading the icon
*
* @param source
* {String} Image source which was loaded
* @param imageInfo
* {Map} Dimensions of the loaded image
*/
__loaderCallback : function(source, imageInfo) {
// Ignore the callback on already disposed images
if (this.$$disposed === true) {
return;
}
// Ignore when the source has already been modified
if (source !== qx.util.AliasManager.getInstance().resolve(this.getSource())) {
return;
}
// Output a warning if the image could not loaded and quit
if (imageInfo.failed) {
this.warn("Image could not be loaded: " + source);
this.fireEvent("loadingFailed");
} else if (imageInfo.aborted) {
// ignore the rest because it is aborted
return;
} else {
this.fireEvent("loaded");
}
// Update image (again)
this._styleSource();
},
/**
* Updates the content hint when the image size has been changed
*
* @param width
* {Integer} width of the image
* @param height
* {Integer} height of the image
*/
__updateContentHint : function(width, height) {
if (width === undefined || height == undefined) {
width = this.__width;
height = this.__height;
}
if (this._recalc(width, height))
qx.ui.core.queue.Layout.add(this);
},
/**
* Recalculates the size of the image, according to scaling parameters
* @param maxWidth {Integer?} maximum width restriction
* @param maxHeight {Integer?} minimum height restriction
*/
_recalc: function(originalWidth, originalHeight) {
var maxWidth = this.getMaxWidth();
var maxHeight = this.getMaxHeight();
var minWidth = this.getMinWidth();
var minHeight = this.getMinHeight();
var width = originalWidth;
var height = originalHeight;
var ratio = originalHeight / originalWidth;
switch(this.getForceRatio()) {
case 'height':
if (maxHeight !== null && height > maxHeight) {
height = maxHeight;
width = height / ratio;
} else if (height < minHeight) {
height = minHeight;
width = height / ratio;
}
if (height < maxHeight && this.isAllowScaleUp()) {
height = maxHeight;
width = height / ratio;
}
break;
case 'width':
if (maxWidth !== null && width > maxWidth) {
width = maxWidth;
height = width * ratio;
} else if (width < minWidth) {
width = minWidth;
height = width * ratio;
}
if (width < maxWidth && this.isAllowScaleUp()) {
width = maxWidth;
height = width * ratio;
}
break;
case 'auto':
case 'bestfit':
if (maxWidth !== null && width > maxWidth) {
width = maxWidth;
height = width * ratio;
} else if (width < minWidth) {
width = minWidth;
height = width * ratio;
}
if (width < maxWidth && this.isAllowScaleUp()) {
width = maxWidth;
height = width * ratio;
}
if (maxHeight !== null && height > maxHeight) {
height = maxHeight;
width = height / ratio;
}
break;
}
width = Math.round(width);
height = Math.round(height);
if (width != this.__width || height != this.__height) {
this.__width = width;
this.__height = height;
return true;
}
return false;
}
},
/*
* ****************************************************************************
* DESTRUCTOR
* ****************************************************************************
*/
destruct : function() {
delete this.__currentContentElement;
this._disposeMap("__contentElements");
}
});
/*
qx.Class.define('grasshopper.af.ui.image.Image', {
extend: qx.ui.basic.Image,
construct: function (source) {
this.base(arguments, source);
},
properties: {
},
members: {
// Cached hint size for the container, based on min/max width/height
__hint: null,
// Cached image size, from the actual image
__imageSize: null,
_applySource : function(value, old) {
this.__imageSize = null;
this.__hint = null;
this.base(arguments, value, old);
this._applyDimension();
},
/ **
* Suggest size for the image; respects the min/max height/width settings
* /
_getContentHint: function() {
if (this.__hint)
return this.__hint;
var fixedWidth = this.getWidth();
var fixedHeight = this.getHeight();
var maxWidth = this.getMaxWidth();
var maxHeight = this.getMaxHeight();
var minWidth = this.getMinWidth();
var minHeight = this.getMinHeight();
var width = fixedWidth;
if (!width || width < minWidth)
width = minWidth;
if (!width || width > maxWidth)
width = maxWidth;
var height = fixedHeight;
if (!height || height < minHeight)
height = minHeight;
if (!height || height > maxHeight)
height = maxHeight;
//this.debug("_getContentHint: width="+width+", height="+height+" "+this.getSource());
return {
width: width||0,
height: height||0
};
},
/ **
* Suggests a size for the container, respects min/max height/width
* /
getSizeHint : function(compute) {
var dims = {};
var ch = this._getContentHint();
dims.width = dims.minWidth = dims.maxWidth = ch.width;
dims.height = dims.minHeight = dims.maxHeight = ch.height;
return dims;
},
/ **
* Recalculates the sizes
* @Override
* /
_styleSource: function() {
var source = qx.util.AliasManager.getInstance().resolve(this.getSource());
if (qx.io.ImageLoader.isLoaded(source)) {
this._calcPreferredSize();
}
this.base(arguments);
},
/ **
* Recalcs the size of the image, caching the result
* /
_calcPreferredSize: function() {
if (this.__imageSize)
return;
var maxWidth = this.getMaxWidth();
var maxHeight = this.getMaxHeight();
if (this._recalc(maxWidth, maxHeight))
qx.ui.core.queue.Layout.add(this);
},
/ **
* Handles the rendering; the image will be centralised
* @Override
* /
renderLayout: function(left, top, width, height) {
this._recalc(width, height);
if (this.__imageSize) {
//this.debug("renderLayout: received left="+left+",top="+top+",width="+width+", height="+height);
if (this.__imageSize.width < width) {
left += parseInt((width - this.__imageSize.width) / 2);
width = this.__imageSize.width;
}
if (this.__imageSize.height < height) {
top += parseInt((height - this.__imageSize.height) / 2);
height = this.__imageSize.height;
}
//this.debug("renderLayout: changed to left="+left+",top="+top+",width="+width+", height="+height+", __imageSize="+qx.lang.Json.stringify(this.__imageSize)+" "+this.getSource());
}
this.base(arguments, left, top, width, height);
}
}
});
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment