Creates an image view object and adds it to the specified pane
function _createImageView(file, pane) {
var view = pane.getViewForPath(file.fullPath);
if (view) {
pane.showView(view);
} else {
view = new ImageView(file, pane.$content);
pane.addView(view, true);
}
return new $.Deferred().resolve().promise();
}
Handles file system change events so we can refresh image viewers for the files that changed on disk due to external editors
function _handleFileSystemChange(event, entry, added, removed) {
// this may have been called because files were added
// or removed to the file system. We don't care about those
if (!entry || entry.isDirectory) {
return;
}
// Look for a viewer for the changed file
var viewer = _viewers[entry.fullPath];
// viewer found, call its refresh method
if (viewer) {
viewer.refresh();
}
}
ImageView objects are constructed when an image is opened
function ImageView(file, $container) {
this.file = file;
this.$el = $(Mustache.render(ImageViewTemplate, {fullPath: file.encodedPath || 'file:///' + FileUtils.encodeFilePath(file.fullPath),
now: new Date().valueOf()}));
$container.append(this.$el);
this._naturalWidth = 0;
this._naturalHeight = 0;
this._scale = 100; // 100%
this._scaleDivInfo = null; // coordinates of hidden scale sticker
this.relPath = ProjectManager.makeProjectRelativeIfPossible(this.file.fullPath);
this.$imagePath = this.$el.find(".image-path");
this.$imagePreview = this.$el.find(".image-preview");
this.$imageData = this.$el.find(".image-data");
this.$image = this.$el.find(".image");
this.$imageTip = this.$el.find(".image-tip");
this.$imageGuides = this.$el.find(".image-guide");
this.$imageScale = this.$el.find(".image-scale");
this.$x_value = this.$el.find(".x-value");
this.$y_value = this.$el.find(".y-value");
this.$horzGuide = this.$el.find(".horz-guide");
this.$vertGuide = this.$el.find(".vert-guide");
this.$imagePath.text(this.relPath).attr("title", this.relPath);
this.$imagePreview.on("load", _.bind(this._onImageLoaded, this));
_viewers[file.fullPath] = this;
}
Check mouse entering/exiting the scale sticker. Hide it when entering and show it again when exiting.
ImageView.prototype._handleMouseEnterOrExitScaleSticker = function (offsetX, offsetY) {
var imagePos = this.$imagePreview.position(),
scaleDivPos = this.$imageScale.position(),
imgWidth = this.$imagePreview.width(),
imgHeight = this.$imagePreview.height(),
scaleDivLeft,
scaleDivTop,
scaleDivRight,
scaleDivBottom;
if (this._scaleDivInfo) {
scaleDivLeft = this._scaleDivInfo.left;
scaleDivTop = this._scaleDivInfo.top;
scaleDivRight = this._scaleDivInfo.right;
scaleDivBottom = this._scaleDivInfo.bottom;
if ((imgWidth + imagePos.left) < scaleDivRight) {
scaleDivRight = imgWidth + imagePos.left;
}
if ((imgHeight + imagePos.top) < scaleDivBottom) {
scaleDivBottom = imgHeight + imagePos.top;
}
} else {
scaleDivLeft = scaleDivPos.left;
scaleDivTop = scaleDivPos.top;
scaleDivRight = this.$imageScale.width() + scaleDivLeft;
scaleDivBottom = this.$imageScale.height() + scaleDivTop;
}
if (this._scaleDivInfo) {
// See whether the cursor is no longer inside the hidden scale div.
// If so, show it again.
if ((offsetX < scaleDivLeft || offsetX > scaleDivRight) ||
(offsetY < scaleDivTop || offsetY > scaleDivBottom)) {
this._scaleDivInfo = null;
this.$imageScale.show();
}
} else if ((offsetX >= scaleDivLeft && offsetX <= scaleDivRight) &&
(offsetY >= scaleDivTop && offsetY <= scaleDivBottom)) {
// Handle mouse inside image scale div.
// But hide it only if the pixel under mouse is also in the image.
if (offsetX < (imagePos.left + imgWidth) &&
offsetY < (imagePos.top + imgHeight)) {
// Remember image scale div coordinates before hiding it.
this._scaleDivInfo = {left: scaleDivPos.left,
top: scaleDivPos.top,
right: scaleDivRight,
bottom: scaleDivBottom};
this.$imageScale.hide();
}
}
};
Hides both guides and the tip
ImageView.prototype._hideGuidesAndTip = function () {
this.$imageTip.hide();
this.$imageGuides.hide();
};
Hide image coordinates info tip
ImageView.prototype._hideImageTip = function (e) {
var $target = $(e.target),
targetPos = $target.position(),
imagePos = this.$imagePreview.position(),
right = imagePos.left + this.$imagePreview.width(),
bottom = imagePos.top + this.$imagePreview.height(),
x = targetPos.left + e.offsetX,
y = targetPos.top + e.offsetY;
// Hide image tip and guides only if the cursor is outside of the image.
if (x < imagePos.left || x >= right ||
y < imagePos.top || y >= bottom) {
this._hideGuidesAndTip();
if (this._scaleDivInfo) {
this._scaleDivInfo = null;
this.$imageScale.show();
}
}
};
DocumentManger.fileNameChange handler - when an image is renamed, we must update the view
ImageView.prototype._onFilenameChange = function (e, oldPath, newPath) {
<img>.on("load") handler - updates content of the image view initializes computed values installs event handlers
ImageView.prototype._onImageLoaded = function (e) {
// add dimensions and size
this._naturalWidth = e.currentTarget.naturalWidth;
this._naturalHeight = e.currentTarget.naturalHeight;
var extension = FileUtils.getFileExtension(this.file.fullPath);
var dimensionString = this._naturalWidth + " × " + this._naturalHeight + " " + Strings.UNIT_PIXELS;
if (extension === "ico") {
dimensionString += " (" + Strings.IMAGE_VIEWER_LARGEST_ICON + ")";
}
// get image size
var self = this;
this.file.stat(function (err, stat) {
if (err) {
self.$imageData.html(dimensionString);
} else {
var sizeString = "";
if (stat.size) {
sizeString = " — " + StringUtils.prettyPrintBytes(stat.size, 2);
}
var dimensionAndSize = dimensionString + sizeString;
self.$imageData.html(dimensionAndSize)
.attr("title", dimensionAndSize
.replace("×", "x")
.replace("—", "-"));
}
});
// make sure we always show the right file name
DocumentManager.on("fileNameChange.ImageView", _.bind(this._onFilenameChange, this));
this.$imageTip.hide();
this.$imageGuides.hide();
this.$image.on("mousemove.ImageView", ".image-preview", _.bind(this._showImageTip, this))
.on("mouseleave.ImageView", ".image-preview", _.bind(this._hideImageTip, this));
this._updateScale();
};
Show image coordinates under the mouse cursor
ImageView.prototype._showImageTip = function (e) {
// Don't show image tip if this._scale is close to zero.
// since we won't have enough room to show tip anyway.
if (Math.floor(this._scale) === 0) {
return;
}
var x = Math.round(e.offsetX * 100 / this._scale),
y = Math.round(e.offsetY * 100 / this._scale),
imagePos = this.$imagePreview.position(),
left = e.offsetX + imagePos.left,
top = e.offsetY + imagePos.top,
width = this.$imagePreview.width(),
height = this.$imagePreview.height(),
windowWidth = $(window).width(),
fourDigitImageWidth = this._naturalWidth.toString().length === 4,
// @todo -- seems a bit strange that we're computing sizes
// using magic numbers
infoWidth1 = 112, // info div width 96px + vertical toolbar width 16px
infoWidth2 = 120, // info div width 104px (for 4-digit image width) + vertical toolbar width 16px
tipOffsetX = 10, // adjustment for info div left from x coordinate of cursor
tipOffsetY = -54, // adjustment for info div top from y coordinate of cursor
tipMinusOffsetX1 = -82, // for less than 4-digit image width
tipMinusOffsetX2 = -90; // for 4-digit image width
// Check whether we're getting mousemove events beyond the image boundaries due to a browser bug
// or the rounding calculation above for a scaled image. For example, if an image is 120 px wide,
// we should get mousemove events in the range of 0 <= x < 120, but not 120 or more. If we get
// a value beyond the range, then simply handle the event as if it were a mouseleave.
if (x < 0 || x >= this._naturalWidth || y < 0 || y >= this._naturalHeight) {
this._hideImageTip(e);
this.$imagePreview.css("cursor", "auto");
return;
}
this.$imagePreview.css("cursor", "none");
this._handleMouseEnterOrExitScaleSticker(left, top);
// Check whether to show the image tip on the left.
if ((e.pageX + infoWidth1) > windowWidth ||
(fourDigitImageWidth && (e.pageX + infoWidth2) > windowWidth)) {
tipOffsetX = fourDigitImageWidth ? tipMinusOffsetX2 : tipMinusOffsetX1;
}
this.$x_value.text(x + "px");
this.$y_value.text(y + "px");
this.$imageTip.css({
left: left + tipOffsetX,
top: top + tipOffsetY
}).show();
this.$horzGuide.css({
left: imagePos.left,
top: top,
width: width - 1
}).show();
this.$vertGuide.css({
left: left,
top: imagePos.top,
height: height - 1
}).show();
};
Update the scale element
ImageView.prototype._updateScale = function () {
var currentWidth = this.$imagePreview.width();
if (currentWidth && currentWidth < this._naturalWidth) {
this._scale = currentWidth / this._naturalWidth * 100;
this.$imageScale.text(Math.floor(this._scale) + "%")
// Keep the position of the image scale div relative to the image.
.css("left", this.$imagePreview.position().left + 5)
.show();
} else {
// Reset everything related to the image scale sticker before hiding it.
this._scale = 100;
this._scaleDivInfo = null;
this.$imageScale.text("").hide();
}
};
Destroys the view
ImageView.prototype.destroy = function () {
delete _viewers[this.file.fullPath];
DocumentManager.off(".ImageView");
this.$image.off(".ImageView");
this.$el.remove();
};
Retrieves the file object for this view return {!File} the file object for this view
ImageView.prototype.getFile = function () {
return this.file;
};
Refreshes the image preview with what's on disk
ImageView.prototype.refresh = function () {
var noCacheUrl = this.$imagePreview.attr("src"),
now = new Date().valueOf(),
index = noCacheUrl.indexOf("?");
// strip the old param off
if (index > 0) {
noCacheUrl = noCacheUrl.slice(0, index);
}
// add a new param which will force chrome to
// re-read the image from disk
noCacheUrl = noCacheUrl + "?ver=" + now;
// Update the DOM node with the src URL
this.$imagePreview.attr("src", noCacheUrl);
};
Updates the layout of the view
ImageView.prototype.updateLayout = function () {
this._hideGuidesAndTip();
var $container = this.$el.parent();
var pos = $container.position(),
iWidth = $container.innerWidth(),
iHeight = $container.innerHeight(),
oWidth = $container.outerWidth(),
oHeight = $container.outerHeight();
// $view is "position:absolute" so
// we have to update the height, width and position
this.$el.css({top: pos.top + ((oHeight - iHeight) / 2),
left: pos.left + ((oWidth - iWidth) / 2),
width: iWidth,
height: iHeight});
this._updateScale();
};