Browse Source

Merge pull request #407 from LasLabs/release/9.0/web_widget_darkroom

[9.0][ADD] web_widget_darkroom: Add o2m DarkroomJS widget
pull/592/head
Dave Lasley 7 years ago
committed by GitHub
parent
commit
6ede3b1673
  1. 99
      web_widget_darkroom/README.rst
  2. 5
      web_widget_darkroom/__init__.py
  3. 26
      web_widget_darkroom/__openerp__.py
  4. BIN
      web_widget_darkroom/static/description/icon.png
  5. BIN
      web_widget_darkroom/static/description/modal_screenshot_1.png
  6. BIN
      web_widget_darkroom/static/description/modal_screenshot_2.png
  7. 356
      web_widget_darkroom/static/lib/darkroomjs/core/darkroom.js
  8. 47
      web_widget_darkroom/static/lib/darkroomjs/core/plugin.js
  9. 43
      web_widget_darkroom/static/lib/darkroomjs/core/transformation.js
  10. 36
      web_widget_darkroom/static/lib/darkroomjs/core/utils.js
  11. 693
      web_widget_darkroom/static/src/js/plugins/darkroom.crop.js
  12. 76
      web_widget_darkroom/static/src/js/plugins/darkroom.history.js
  13. 64
      web_widget_darkroom/static/src/js/plugins/darkroom.rotate.js
  14. 148
      web_widget_darkroom/static/src/js/plugins/darkroom.zoom.js
  15. 224
      web_widget_darkroom/static/src/js/widget_darkroom.js
  16. 64
      web_widget_darkroom/static/src/js/widget_darkroom_modal.js
  17. 11
      web_widget_darkroom/static/src/less/darkroom.less
  18. 30
      web_widget_darkroom/static/src/xml/field_templates.xml
  19. 5
      web_widget_darkroom/tests/__init__.py
  20. 203
      web_widget_darkroom/tests/test_darkroom_modal.py
  21. 28
      web_widget_darkroom/views/assets.xml
  22. 5
      web_widget_darkroom/wizards/__init__.py
  23. 82
      web_widget_darkroom/wizards/darkroom_modal.py
  24. 27
      web_widget_darkroom/wizards/darkroom_modal.xml

99
web_widget_darkroom/README.rst

@ -0,0 +1,99 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
================================
DarkroomJS Image Editing for Web
================================
This module provides a `DarkroomJS`_ (v2.0.1) web widget for use with image
fields. It also adds a Darkroom button to the normal image widget, which can
be used to edit the image via Darkroom in a modal.
.. _DarkroomJS: https://github.com/MattKetmo/darkroomjs
The widget currently supports the following operations and can be extended to
allow others:
* Zoom and pan
* Rotate
* Crop
* Step back in history client-side (before save)
Usage
=====
After installing the module, you can use it in the following ways:
* Specify the ``darkroom`` widget when adding an image field to a view.
Configuration values can be provided using the ``options`` attribute::
<field name="image" widget="darkroom" options="{'minWidth': 100}"/>
The widget passes options directly through to DarkroomJS, which supports the
following:
* minWidth
* minHeight
* maxWidth
* maxHeight
* ratio (aspect ratio)
* backgroundColor
* Open a form view that contains an image in edit mode and hover over the
image widget. You should see a Darkoom button that can be clicked to open
the image in a Darkroom modal, where it can be edited and the changes can be
saved.
.. image:: /web_widget_darkroom/static/description/modal_screenshot_1.png
:alt: Darkroom Modal Screenshot 1
:class: img-thumbnail
:height: 260
.. image:: /web_widget_darkroom/static/description/modal_screenshot_2.png
:alt: Darkroom Modal Screenshot 2
:class: img-thumbnail col-xs-offset-1
:height: 260
Known Issues / Roadmap
======================
* Darkroom modals are currently not supported during record creation
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_. In
case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smash it by providing detailed and welcome
feedback.
Credits
=======
Images
------
* Odoo Community Association:
`Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Dave Lasley <dave@laslabs.com>
* Oleg Bulkin <obulkin@laslabs.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

5
web_widget_darkroom/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import wizards

26
web_widget_darkroom/__openerp__.py

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
{
'name': 'Web DarkroomJS Image Editing',
'summary': 'Provides web widget for image editing and adds it to standard'
' image widget as modal',
'version': '9.0.1.0.1',
'category': 'Web',
'website': 'https://laslabs.com/',
'author': 'LasLabs, Odoo Community Association (OCA)',
'license': 'LGPL-3',
'application': False,
'installable': True,
'depends': [
'web',
],
'data': [
'views/assets.xml',
'wizards/darkroom_modal.xml',
],
'qweb': [
'static/src/xml/field_templates.xml',
],
}

BIN
web_widget_darkroom/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

BIN
web_widget_darkroom/static/description/modal_screenshot_1.png

After

Width: 250  |  Height: 250  |  Size: 24 KiB

BIN
web_widget_darkroom/static/description/modal_screenshot_2.png

After

Width: 950  |  Height: 550  |  Size: 56 KiB

356
web_widget_darkroom/static/lib/darkroomjs/core/darkroom.js

@ -0,0 +1,356 @@
/**
* Copyright 2013 Matthieu Moquet
* Copyright 2016-2017 LasLabs Inc.
* License MIT (https://opensource.org/licenses/MIT)
**/
(function() {
'use strict';
window.Darkroom = Darkroom;
// Core object of DarkroomJS.
// Basically it's a single object, instanciable via an element
// (it could be a CSS selector or a DOM element), some custom options,
// and a list of plugin objects (or none to use default ones).
function Darkroom(element, options, plugins) {
return this.constructor(element, options, plugins);
}
// Create an empty list of plugin objects, which will be filled by
// other plugin scripts. This is the default plugin list if none is
// specified in Darkroom's constructor.
Darkroom.plugins = [];
Darkroom.prototype = {
// Reference to the main container element
containerElement: null,
// Reference to the Fabric canvas object
canvas: null,
// Reference to the Fabric image object
image: null,
// Reference to the Fabric source canvas object
sourceCanvas: null,
// Reference to the Fabric source image object
sourceImage: null,
// Track of the original image element
originalImageElement: null,
// Stack of transformations to apply to the image source
transformations: [],
// Default options
defaults: {
// Canvas properties (dimension, ratio, color)
minWidth: null,
minHeight: null,
maxWidth: null,
maxHeight: null,
ratio: null,
backgroundColor: '#fff',
// Plugins options
plugins: {},
// Post-initialisation callback
initialize: function() { /* noop */ }
},
// List of the instancied plugins
plugins: {},
// This options are a merge between `defaults` and the options passed
// through the constructor
options: {},
constructor: function(element, options) {
this.options = Darkroom.Utils.extend(options, this.defaults);
if (typeof element === 'string')
element = document.querySelector(element);
if (null === element)
return;
var image = new Image();
var parent = element.parentElement;
image.onload = function() {
// Initialize the DOM/Fabric elements
this._initializeDOM(element, parent);
this._initializeImage();
// Then initialize the plugins
this._initializePlugins(Darkroom.plugins);
// Public method to adjust image according to the canvas
this.refresh(function() {
// Execute a custom callback after initialization
this.options.initialize.bind(this).call();
}.bind(this));
}.bind(this);
image.src = element.src;
},
selfDestroy: function() {
var container = this.containerElement;
var image = new Image();
image.onload = function() {
container.parentNode.replaceChild(image, container);
};
image.src = this.sourceImage.toDataURL();
},
// Add ability to attach event listener on the core object.
// It uses the canvas element to process events.
addEventListener: function(eventName, callback) {
var el = this.canvas.getElement();
if (el.addEventListener) {
el.addEventListener(eventName, callback);
} else if (el.attachEvent) {
el.attachEvent('on' + eventName, callback);
}
},
dispatchEvent: function(eventName) {
// Use the old way of creating event to be IE compatible
// See https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
var event = document.createEvent('Event');
event.initEvent(eventName, true, true);
this.canvas.getElement().dispatchEvent(event);
},
// Adjust image & canvas dimension according to min/max width/height
// and ratio specified in the options.
// This method should be called after each image transformation.
refresh: function(next) {
var clone = new Image();
clone.onload = function() {
this._replaceCurrentImage(new fabric.Image(clone));
if (next) next();
}.bind(this);
clone.src = this.sourceImage.toDataURL();
},
_replaceCurrentImage: function(newImage) {
if (this.image) {
this.image.remove();
}
this.image = newImage;
this.image.selectable = false;
// Adjust width or height according to specified ratio
var viewport = Darkroom.Utils.computeImageViewPort(this.image);
var canvasWidth = viewport.width;
var canvasHeight = viewport.height;
if (null !== this.options.ratio) {
var canvasRatio = +this.options.ratio;
var currentRatio = canvasWidth / canvasHeight;
if (currentRatio > canvasRatio) {
canvasHeight = canvasWidth / canvasRatio;
} else if (currentRatio < canvasRatio) {
canvasWidth = canvasHeight * canvasRatio;
}
}
// Then scale the image to fit into dimension limits
var scaleMin = 1;
var scaleMax = 1;
var scaleX = 1;
var scaleY = 1;
if (null !== this.options.maxWidth && this.options.maxWidth < canvasWidth) {
scaleX = this.options.maxWidth / canvasWidth;
}
if (null !== this.options.maxHeight && this.options.maxHeight < canvasHeight) {
scaleY = this.options.maxHeight / canvasHeight;
}
scaleMin = Math.min(scaleX, scaleY);
scaleX = 1;
scaleY = 1;
if (null !== this.options.minWidth && this.options.minWidth > canvasWidth) {
scaleX = this.options.minWidth / canvasWidth;
}
if (null !== this.options.minHeight && this.options.minHeight > canvasHeight) {
scaleY = this.options.minHeight / canvasHeight;
}
scaleMax = Math.max(scaleX, scaleY);
var scale = scaleMax * scaleMin; // one should be equals to 1
canvasWidth *= scale;
canvasHeight *= scale;
// Finally place the image in the center of the canvas
this.image.setScaleX(1 * scale);
this.image.setScaleY(1 * scale);
this.canvas.add(this.image);
this.canvas.setWidth(canvasWidth);
this.canvas.setHeight(canvasHeight);
this.canvas.centerObject(this.image);
this.image.setCoords();
},
// Apply the transformation on the current image and save it in the
// transformations stack (in order to reconstitute the previous states
// of the image).
applyTransformation: function(transformation) {
this.transformations.push(transformation);
transformation.applyTransformation(
this.sourceCanvas,
this.sourceImage,
this._postTransformation.bind(this)
);
},
_postTransformation: function(newImage) {
if (newImage)
this.sourceImage = newImage;
this.refresh(function() {
this.dispatchEvent('core:transformation');
}.bind(this));
},
// Initialize image from original element plus re-apply every
// transformations.
reinitializeImage: function() {
this.sourceImage.remove();
this._initializeImage();
this._popTransformation(this.transformations.slice());
},
_popTransformation: function(transformations) {
if (0 === transformations.length) {
this.dispatchEvent('core:reinitialized');
this.refresh();
return;
}
var transformation = transformations.shift();
var next = function(newImage) {
if (newImage) this.sourceImage = newImage;
this._popTransformation(transformations);
};
transformation.applyTransformation(
this.sourceCanvas,
this.sourceImage,
next.bind(this)
);
},
// Create the DOM elements and instanciate the Fabric canvas.
// The image element is replaced by a new `div` element.
// However the original image is re-injected in order to keep a trace of it.
_initializeDOM: function(imageElement) {
// Container
var mainContainerElement = document.createElement('div');
mainContainerElement.className = 'darkroom-container';
// Toolbar
var toolbarElement = document.createElement('div');
toolbarElement.className = 'darkroom-toolbar';
mainContainerElement.appendChild(toolbarElement);
// Viewport canvas
var canvasContainerElement = document.createElement('div');
canvasContainerElement.className = 'darkroom-image-container';
var canvasElement = document.createElement('canvas');
canvasContainerElement.appendChild(canvasElement);
mainContainerElement.appendChild(canvasContainerElement);
// Source canvas
var sourceCanvasContainerElement = document.createElement('div');
sourceCanvasContainerElement.className = 'darkroom-source-container';
sourceCanvasContainerElement.style.display = 'none';
var sourceCanvasElement = document.createElement('canvas');
sourceCanvasContainerElement.appendChild(sourceCanvasElement);
mainContainerElement.appendChild(sourceCanvasContainerElement);
// Original image
imageElement.parentNode.replaceChild(mainContainerElement, imageElement);
imageElement.style.display = 'none';
mainContainerElement.appendChild(imageElement);
// Instanciate object from elements
this.containerElement = mainContainerElement;
this.originalImageElement = imageElement;
this.toolbar = new Darkroom.UI.Toolbar(toolbarElement);
this.canvas = new fabric.Canvas(canvasElement, {
selection: false,
backgroundColor: this.options.backgroundColor,
});
this.sourceCanvas = new fabric.Canvas(sourceCanvasElement, {
selection: false,
backgroundColor: this.options.backgroundColor,
});
},
// Instanciate the Fabric image object.
// The image is created as a static element with no control,
// then it is add in the Fabric canvas object.
_initializeImage: function() {
this.sourceImage = new fabric.Image(this.originalImageElement, {
// Some options to make the image static
selectable: false,
evented: false,
lockMovementX: true,
lockMovementY: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
lockUniScaling: true,
hasControls: false,
hasBorders: false,
});
this.sourceCanvas.add(this.sourceImage);
// Adjust width or height according to specified ratio
var viewport = Darkroom.Utils.computeImageViewPort(this.sourceImage);
var canvasWidth = viewport.width;
var canvasHeight = viewport.height;
this.sourceCanvas.setWidth(canvasWidth);
this.sourceCanvas.setHeight(canvasHeight);
this.sourceCanvas.centerObject(this.sourceImage);
this.sourceImage.setCoords();
},
// Initialize every plugins.
// Note that plugins are instanciated in the same order than they
// are declared in the parameter object.
_initializePlugins: function(plugins) {
for (var name in plugins) {
var plugin = plugins[name];
var options = this.options.plugins[name];
// Setting false into the plugin options will disable the plugin
if (options === false)
continue;
// Avoid any issues with _proto_
if (!plugins.hasOwnProperty(name))
continue;
this.plugins[name] = new plugin(this, options);
}
},
};
})();

47
web_widget_darkroom/static/lib/darkroomjs/core/plugin.js

@ -0,0 +1,47 @@
/**
* Copyright 2013 Matthieu Moquet
* Copyright 2016-2017 LasLabs Inc.
* License MIT (https://opensource.org/licenses/MIT)
**/
(function() {
'use strict';
Darkroom.Plugin = Plugin;
// Define a plugin object. This is the (abstract) parent class which
// has to be extended for each plugin.
function Plugin(darkroom, options) {
this.darkroom = darkroom;
this.options = Darkroom.Utils.extend(options, this.defaults);
this.initialize();
}
Plugin.prototype = {
defaults: {},
initialize: function() { /* no-op */ }
};
// Inspired by Backbone.js extend capability.
Plugin.extend = function(protoProps) {
var parent = this;
var child;
if (protoProps && protoProps.hasOwnProperty('constructor')) {
child = protoProps.constructor;
} else {
child = function() { return parent.apply(this, arguments); };
}
Darkroom.Utils.extend(child, parent);
var Surrogate = function() { this.constructor = child; };
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate();
if (protoProps) Darkroom.Utils.extend(child.prototype, protoProps);
child.__super__ = parent.prototype;
return child;
};
})();

43
web_widget_darkroom/static/lib/darkroomjs/core/transformation.js

@ -0,0 +1,43 @@
/**
* Copyright 2013 Matthieu Moquet
* Copyright 2016-2017 LasLabs Inc.
* License MIT (https://opensource.org/licenses/MIT)
**/
(function() {
'use strict';
Darkroom.Transformation = Transformation;
function Transformation(options) {
this.options = options;
}
Transformation.prototype = {
applyTransformation: function() { /* no-op */ }
};
// Inspired by Backbone.js extend capability.
Transformation.extend = function(protoProps) {
var parent = this;
var child;
if (protoProps && protoProps.hasOwnProperty('constructor')) {
child = protoProps.constructor;
} else {
child = function() { return parent.apply(this, arguments); };
}
Darkroom.Utils.extend(child, parent);
var Surrogate = function() { this.constructor = child; };
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate();
if (protoProps) Darkroom.Utils.extend(child.prototype, protoProps);
child.__super__ = parent.prototype;
return child;
};
})();

36
web_widget_darkroom/static/lib/darkroomjs/core/utils.js

@ -0,0 +1,36 @@
/**
* Copyright 2013 Matthieu Moquet
* Copyright 2016-2017 LasLabs Inc.
* License MIT (https://opensource.org/licenses/MIT)
**/
(function() {
'use strict';
Darkroom.Utils = {
extend: extend,
computeImageViewPort: computeImageViewPort,
};
// Utility method to easily extend objects.
function extend(b, a) {
var prop;
if (b === undefined) {
return a;
}
for (prop in a) {
if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop) === false) {
b[prop] = a[prop];
}
}
return b;
}
function computeImageViewPort(image) {
return {
height: Math.abs(image.getWidth() * (Math.sin(image.getAngle() * Math.PI/180))) + Math.abs(image.getHeight() * (Math.cos(image.getAngle() * Math.PI/180))),
width: Math.abs(image.getHeight() * (Math.sin(image.getAngle() * Math.PI/180))) + Math.abs(image.getWidth() * (Math.cos(image.getAngle() * Math.PI/180))),
};
}
})();

693
web_widget_darkroom/static/src/js/plugins/darkroom.crop.js

@ -0,0 +1,693 @@
/**
* Copyright 2013 Matthieu Moquet
* Copyright 2016-2017 LasLabs Inc.
* License MIT (https://opensource.org/licenses/MIT)
**/
odoo.define('web_widget_darkroom.darkroom_crop', function(){
'use strict';
var DarkroomPluginCrop = function() {
var Crop = Darkroom.Transformation.extend({
applyTransformation: function(canvas, image, next) {
// Snapshot the image delimited by the crop zone
var snapshot = new Image();
snapshot.onload = function() {
var width = this.width;
var height = this.height;
// Validate image
if (height < 1 || width < 1) {
return;
}
var imgInstance = new fabric.Image(this, {
// Options to make the image static
selectable: false,
evented: false,
lockMovementX: true,
lockMovementY: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
lockUniScaling: true,
hasControls: false,
hasBorders: false,
});
// Update canvas size
canvas.setWidth(width);
canvas.setHeight(height);
// Add image
image.remove();
canvas.add(imgInstance);
next(imgInstance);
};
var viewport = Darkroom.Utils.computeImageViewPort(image);
var imageWidth = viewport.width;
var imageHeight = viewport.height;
var left = this.options.left * imageWidth;
var top = this.options.top * imageHeight;
var width = Math.min(this.options.width * imageWidth, imageWidth - left);
var height = Math.min(this.options.height * imageHeight, imageHeight - top);
snapshot.src = canvas.toDataURL({
left: left,
top: top,
width: width,
height: height,
});
},
});
var CropZone = fabric.util.createClass(fabric.Rect, {
_render: function(ctx) {
this.callSuper('_render', ctx);
var dashWidth = 7;
// Set original scale
var flipX = this.flipX ? -1 : 1;
var flipY = this.flipY ? -1 : 1;
var scaleX = flipX / this.scaleX;
var scaleY = flipY / this.scaleY;
ctx.scale(scaleX, scaleY);
// Overlay rendering
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
this._renderOverlay(ctx);
// Set dashed borders
if (typeof ctx.setLineDash !== 'undefined') {
ctx.setLineDash([dashWidth, dashWidth]);
} else if (typeof ctx.mozDash !== 'undefined') {
ctx.mozDash = [dashWidth, dashWidth];
}
// First lines rendering with black
ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)';
this._renderBorders(ctx);
this._renderGrid(ctx);
// Re render lines in white
ctx.lineDashOffset = dashWidth;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
this._renderBorders(ctx);
this._renderGrid(ctx);
// Reset scale
ctx.scale(1/scaleX, 1/scaleY);
},
_renderOverlay: function(ctx) {
var canvas = ctx.canvas;
var borderOffset = 0;
//
// x0 x1 x2 x3
// y0 +------------------------+
// |\\\\\\\\\\\\\\\\\\\\\\\\|
// |\\\\\\\\\\\\\\\\\\\\\\\\|
// y1 +------+---------+-------+
// |\\\\\\| |\\\\\\\|
// |\\\\\\| 0 |\\\\\\\|
// |\\\\\\| |\\\\\\\|
// y2 +------+---------+-------+
// |\\\\\\\\\\\\\\\\\\\\\\\\|
// |\\\\\\\\\\\\\\\\\\\\\\\\|
// y3 +------------------------+
//
var x0 = Math.ceil(-this.getWidth() / 2 - this.getLeft());
var x1 = Math.ceil(-this.getWidth() / 2);
var x2 = Math.ceil(this.getWidth() / 2);
var x3 = Math.ceil(this.getWidth() / 2 + (canvas.width - this.getWidth() - this.getLeft()));
var y0 = Math.ceil(-this.getHeight() / 2 - this.getTop());
var y1 = Math.ceil(-this.getHeight() / 2);
var y2 = Math.ceil(this.getHeight() / 2);
var y3 = Math.ceil(this.getHeight() / 2 + (canvas.height - this.getHeight() - this.getTop()));
// Upper rect
ctx.fillRect(x0, y0, x3 - x0, y1 - y0 + borderOffset);
// Left rect
ctx.fillRect(x0, y1, x1 - x0, y2 - y1 + borderOffset);
// Right rect
ctx.fillRect(x2, y1, x3 - x2, y2 - y1 + borderOffset);
// Down rect
ctx.fillRect(x0, y2, x3 - x0, y3 - y2);
},
_renderBorders: function(ctx) {
ctx.beginPath();
// upper left
ctx.moveTo(-this.getWidth()/2, -this.getHeight()/2);
// upper right
ctx.lineTo(this.getWidth()/2, -this.getHeight()/2);
// down right
ctx.lineTo(this.getWidth()/2, this.getHeight()/2);
// down left
ctx.lineTo(-this.getWidth()/2, this.getHeight()/2);
// upper left
ctx.lineTo(-this.getWidth()/2, -this.getHeight()/2);
ctx.stroke();
},
_renderGrid: function(ctx) {
// Vertical lines
ctx.beginPath();
ctx.moveTo(-this.getWidth()/2 + 1/3 * this.getWidth(), -this.getHeight()/2);
ctx.lineTo(-this.getWidth()/2 + 1/3 * this.getWidth(), this.getHeight()/2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-this.getWidth()/2 + 2/3 * this.getWidth(), -this.getHeight()/2);
ctx.lineTo(-this.getWidth()/2 + 2/3 * this.getWidth(), this.getHeight()/2);
ctx.stroke();
// Horizontal lines
ctx.beginPath();
ctx.moveTo(-this.getWidth()/2, -this.getHeight()/2 + 1/3 * this.getHeight());
ctx.lineTo(this.getWidth()/2, -this.getHeight()/2 + 1/3 * this.getHeight());
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-this.getWidth()/2, -this.getHeight()/2 + 2/3 * this.getHeight());
ctx.lineTo(this.getWidth()/2, -this.getHeight()/2 + 2/3 * this.getHeight());
ctx.stroke();
},
});
Darkroom.plugins.crop = Darkroom.Plugin.extend({
// Init point
startX: null,
startY: null,
// Keycrop
isKeyCroping: false,
isKeyLeft: false,
isKeyUp: false,
defaults: {
// Min crop dimensions
minHeight: 1,
minWidth: 1,
// Ensure crop ratio
ratio: null,
// Quick crop feature (set a key code to enable it)
quickCropKey: false,
},
initialize: function InitDarkroomCropPlugin() {
var buttonGroup = this.darkroom.toolbar.createButtonGroup();
this.cropButton = buttonGroup.createButton({
image: 'fa fa-crop',
editOnly: true,
});
this.okButton = buttonGroup.createButton({
image: 'fa fa-check',
editOnly: true,
type: 'success',
hide: true
});
this.cancelButton = buttonGroup.createButton({
image: 'fa fa-times',
editOnly: true,
type: 'danger',
hide: true
});
// Button click events
this.cropButton.addEventListener('click', this.toggleCrop.bind(this));
this.okButton.addEventListener('click', this.cropCurrentZone.bind(this));
this.cancelButton.addEventListener('click', this.releaseFocus.bind(this));
// Canvas events
this.darkroom.canvas.on('mouse:down', this.onMouseDown.bind(this));
this.darkroom.canvas.on('mouse:move', this.onMouseMove.bind(this));
this.darkroom.canvas.on('mouse:up', this.onMouseUp.bind(this));
this.darkroom.canvas.on('object:moving', this.onObjectMoving.bind(this));
this.darkroom.canvas.on('object:scaling', this.onObjectScaling.bind(this));
fabric.util.addListener(fabric.document, 'keydown', this.onKeyDown.bind(this));
fabric.util.addListener(fabric.document, 'keyup', this.onKeyUp.bind(this));
this.darkroom.addEventListener('core:transformation', this.releaseFocus.bind(this));
},
// Avoid crop zone to go beyond the canvas edges
onObjectMoving: function(event) {
if (!this.hasFocus()) {
return;
}
var currentObject = event.target;
if (currentObject !== this.cropZone) {
return;
}
var canvas = this.darkroom.canvas;
var x = currentObject.getLeft(), y = currentObject.getTop();
var w = currentObject.getWidth(), h = currentObject.getHeight();
var maxX = canvas.getWidth() - w;
var maxY = canvas.getHeight() - h;
if (x < 0) {
currentObject.set('left', 0);
}
if (y < 0) {
currentObject.set('top', 0);
}
if (x > maxX) {
currentObject.set('left', maxX);
}
if (y > maxY) {
currentObject.set('top', maxY);
}
this.darkroom.dispatchEvent('crop:update');
},
// Prevent crop zone from going beyond the canvas edges (like mouseMove)
onObjectScaling: function(event) {
if (!this.hasFocus()) {
return;
}
var preventScaling = false;
var currentObject = event.target;
if (currentObject !== this.cropZone) {
return;
}
var canvas = this.darkroom.canvas;
var minX = currentObject.getLeft();
var minY = currentObject.getTop();
var maxX = currentObject.getLeft() + currentObject.getWidth();
var maxY = currentObject.getTop() + currentObject.getHeight();
if (this.options.ratio !== null) {
if (minX < 0 || maxX > canvas.getWidth() || minY < 0 || maxY > canvas.getHeight()) {
preventScaling = true;
}
}
if (minX < 0 || maxX > canvas.getWidth() || preventScaling) {
var lastScaleX = this.lastScaleX || 1;
currentObject.setScaleX(lastScaleX);
}
if (minX < 0) {
currentObject.setLeft(0);
}
if (minY < 0 || maxY > canvas.getHeight() || preventScaling) {
var lastScaleY = this.lastScaleY || 1;
currentObject.setScaleY(lastScaleY);
}
if (minY < 0) {
currentObject.setTop(0);
}
if (currentObject.getWidth() < this.options.minWidth) {
currentObject.scaleToWidth(this.options.minWidth);
}
if (currentObject.getHeight() < this.options.minHeight) {
currentObject.scaleToHeight(this.options.minHeight);
}
this.lastScaleX = currentObject.getScaleX();
this.lastScaleY = currentObject.getScaleY();
this.darkroom.dispatchEvent('crop:update');
},
// Init crop zone
onMouseDown: function(event) {
if (!this.hasFocus()) {
return;
}
var canvas = this.darkroom.canvas;
// Recalculate offset, in case canvas was manipulated since last `calcOffset`
canvas.calcOffset();
var pointer = canvas.getPointer(event.e);
var x = pointer.x;
var y = pointer.y;
var point = new fabric.Point(x, y);
// Check if user want to scale or drag the crop zone.
var activeObject = canvas.getActiveObject();
if (activeObject === this.cropZone || this.cropZone.containsPoint(point)) {
return;
}
canvas.discardActiveObject();
this.cropZone.setWidth(0);
this.cropZone.setHeight(0);
this.cropZone.setScaleX(1);
this.cropZone.setScaleY(1);
this.startX = x;
this.startY = y;
},
// Extend crop zone
onMouseMove: function(event) {
// Quick crop feature
if (this.isKeyCroping) {
return this.onMouseMoveKeyCrop(event);
}
if (this.startX === null || this.startY === null) {
return;
}
var canvas = this.darkroom.canvas;
var pointer = canvas.getPointer(event.e);
var x = pointer.x;
var y = pointer.y;
this._renderCropZone(this.startX, this.startY, x, y);
},
onMouseMoveKeyCrop: function(event) {
var canvas = this.darkroom.canvas;
var zone = this.cropZone;
var pointer = canvas.getPointer(event.e);
var x = pointer.x;
var y = pointer.y;
if (!zone.left || !zone.top) {
zone.setTop(y);
zone.setLeft(x);
}
this.isKeyLeft = x < zone.left + zone.width / 2;
this.isKeyUp = y < zone.top + zone.height / 2;
this._renderCropZone(
Math.min(zone.left, x),
Math.min(zone.top, y),
Math.max(zone.left+zone.width, x),
Math.max(zone.top+zone.height, y)
);
},
// Finish crop zone
onMouseUp: function() {
if (this.startX === null || this.startY === null) {
return;
}
var canvas = this.darkroom.canvas;
this.cropZone.setCoords();
canvas.setActiveObject(this.cropZone);
canvas.calcOffset();
this.startX = null;
this.startY = null;
},
onKeyDown: function(event) {
if (this.options.quickCropKey === false || event.keyCode !== this.options.quickCropKey || this.isKeyCroping) {
return;
}
// Active quick crop flow
this.isKeyCroping = true ;
this.darkroom.canvas.discardActiveObject();
this.cropZone.setWidth(0);
this.cropZone.setHeight(0);
this.cropZone.setScaleX(1);
this.cropZone.setScaleY(1);
this.cropZone.setTop(0);
this.cropZone.setLeft(0);
},
onKeyUp: function(event) {
if (this.options.quickCropKey === false || event.keyCode !== this.options.quickCropKey || !this.isKeyCroping) {
return;
}
// Inactive quick crop flow
this.isKeyCroping = false;
this.startX = 1;
this.startY = 1;
this.onMouseUp();
},
selectZone: function(x, y, width, height, forceDimension) {
if (!this.hasFocus()) {
this.requireFocus();
}
if (forceDimension) {
this.cropZone.set({
'left': x,
'top': y,
'width': width,
'height': height,
});
} else {
this._renderCropZone(x, y, x+width, y+height);
}
var canvas = this.darkroom.canvas;
canvas.bringToFront(this.cropZone);
this.cropZone.setCoords();
canvas.setActiveObject(this.cropZone);
canvas.calcOffset();
this.darkroom.dispatchEvent('crop:update');
},
toggleCrop: function() {
if (this.hasFocus()) {
this.releaseFocus();
} else {
this.requireFocus();
}
},
cropCurrentZone: function() {
if (!this.hasFocus()) {
return;
}
// Avoid croping empty zone
if (this.cropZone.width < 1 && this.cropZone.height < 1) {
return;
}
var image = this.darkroom.image;
// Compute crop zone dimensions
var top = this.cropZone.getTop() - image.getTop();
var left = this.cropZone.getLeft() - image.getLeft();
var width = this.cropZone.getWidth();
var height = this.cropZone.getHeight();
// Adjust dimensions to image only
if (top < 0) {
height += top;
top = 0;
}
if (left < 0) {
width += left;
left = 0;
}
// Apply crop transformation. Make sure to use relative
// dimension since the crop will be applied on the source image.
this.darkroom.applyTransformation(new Crop({
top: top / image.getHeight(),
left: left / image.getWidth(),
width: width / image.getWidth(),
height: height / image.getHeight(),
}));
},
// Test whether crop zone is set
hasFocus: function() {
return typeof this.cropZone !== 'undefined';
},
// Create the crop zone
requireFocus: function() {
this.cropZone = new CropZone({
fill: 'transparent',
hasBorders: false,
originX: 'left',
originY: 'top',
cornerColor: '#444',
cornerSize: 8,
transparentCorners: false,
lockRotation: true,
hasRotatingPoint: false,
});
if (this.options.ratio !== null) {
this.cropZone.set('lockUniScaling', true);
}
this.darkroom.canvas.add(this.cropZone);
this.darkroom.canvas.defaultCursor = 'crosshair';
this.cropButton.active(true);
this.okButton.hide(false);
this.cancelButton.hide(false);
},
// Remove the crop zone
releaseFocus: function() {
if (typeof this.cropZone === 'undefined') {
return;
}
this.cropZone.remove();
this.cropZone = undefined;
this.cropButton.active(false);
this.okButton.hide(true);
this.cancelButton.hide(true);
this.darkroom.canvas.defaultCursor = 'default';
this.darkroom.dispatchEvent('crop:update');
},
_renderCropZone: function(fromX, fromY, toX, toY) {
var canvas = this.darkroom.canvas;
var isRight = toX > fromX;
var isLeft = !isRight;
var isDown = toY > fromY;
var isUp = !isDown;
var minWidth = Math.min(Number(this.options.minWidth), canvas.getWidth());
var minHeight = Math.min(Number(this.options.minHeight), canvas.getHeight());
// Define corner coordinates
var leftX = Math.min(fromX, toX);
var rightX = Math.max(fromX, toX);
var topY = Math.min(fromY, toY);
var bottomY = Math.max(fromY, toY);
// Replace current point into the canvas
leftX = Math.max(0, leftX);
rightX = Math.min(canvas.getWidth(), rightX);
topY = Math.max(0, topY);
bottomY = Math.min(canvas.getHeight(), bottomY);
// Recalibrate coordinates according to given options
if (rightX - leftX < minWidth) {
if (isRight) {
rightX = leftX + minWidth;
} else {
leftX = rightX - minWidth;
}
}
if (bottomY - topY < minHeight) {
if (isDown) {
bottomY = topY + minHeight;
} else {
topY = bottomY - minHeight;
}
}
// Truncate truncate according to canvas dimensions
if (leftX < 0) {
// Translate to the left
rightX += Math.abs(leftX);
leftX = 0;
}
if (rightX > canvas.getWidth()) {
// Translate to the right
leftX -= rightX - canvas.getWidth();
rightX = canvas.getWidth();
}
if (topY < 0) {
// Translate to the bottom
bottomY += Math.abs(topY);
topY = 0;
}
if (bottomY > canvas.getHeight()) {
// Translate to the right
topY -= bottomY - canvas.getHeight();
bottomY = canvas.getHeight();
}
var width = rightX - leftX;
var height = bottomY - topY;
var currentRatio = width / height;
if (this.options.ratio && Number(this.options.ratio) !== currentRatio) {
var ratio = Number(this.options.ratio);
var newWidth = 0, newHeight = 0;
if(this.isKeyCroping) {
isLeft = this.isKeyLeft;
isUp = this.isKeyUp;
}
if (currentRatio < ratio) {
newWidth = height * ratio;
if (isLeft) {
leftX -= newWidth - width;
}
width = newWidth;
} else if (currentRatio > ratio) {
newHeight = height / (ratio * height/width);
if (isUp) {
topY -= newHeight - height;
}
height = newHeight;
}
if (leftX < 0) {
leftX = 0;
//TODO
}
if (topY < 0) {
topY = 0;
//TODO
}
if (leftX + width > canvas.getWidth()) {
newWidth = canvas.getWidth() - leftX;
height = newWidth * height / width;
width = newWidth;
if (isUp) {
topY = fromY - height;
}
}
if (topY + height > canvas.getHeight()) {
newHeight = canvas.getHeight() - topY;
width = width * newHeight / height;
height = newHeight;
if (isLeft) {
leftX = fromX - width;
}
}
}
// Apply coordinates
this.cropZone.left = leftX;
this.cropZone.top = topY;
this.cropZone.width = width;
this.cropZone.height = height;
this.darkroom.canvas.bringToFront(this.cropZone);
this.darkroom.dispatchEvent('crop:update');
}
});
};
return {DarkroomPluginCrop: DarkroomPluginCrop};
});

76
web_widget_darkroom/static/src/js/plugins/darkroom.history.js

@ -0,0 +1,76 @@
/**
* Copyright 2013 Matthieu Moquet
* Copyright 2016-2017 LasLabs Inc.
* License MIT (https://opensource.org/licenses/MIT)
**/
odoo.define('web_widget_darkroom.darkroom_history', function() {
'use strict';
var DarkroomPluginHistory = function() {
Darkroom.plugins.history = Darkroom.Plugin.extend({
undoTransformations: [],
initialize: function InitDarkroomHistoryPlugin() {
this._initButtons();
this.darkroom.addEventListener('core:transformation', this._onTranformationApplied.bind(this));
},
undo: function() {
if (this.darkroom.transformations.length === 0) {
return;
}
var lastTransformation = this.darkroom.transformations.pop();
this.undoTransformations.unshift(lastTransformation);
this.darkroom.reinitializeImage();
this._updateButtons();
},
redo: function() {
if (this.undoTransformations.length === 0) {
return;
}
var cancelTransformation = this.undoTransformations.shift();
this.darkroom.transformations.push(cancelTransformation);
this.darkroom.reinitializeImage();
this._updateButtons();
},
_initButtons: function() {
var buttonGroup = this.darkroom.toolbar.createButtonGroup();
this.backButton = buttonGroup.createButton({
image: 'fa fa-step-backward',
disabled: true,
editOnly: true,
});
this.forwardButton = buttonGroup.createButton({
image: 'fa fa-step-forward',
disabled: true,
editOnly: true,
});
this.backButton.addEventListener('click', this.undo.bind(this));
this.forwardButton.addEventListener('click', this.redo.bind(this));
return this;
},
_updateButtons: function() {
this.backButton.disable(this.darkroom.transformations.length === 0);
this.forwardButton.disable(this.undoTransformations.length === 0);
},
_onTranformationApplied: function() {
this.undoTransformations = [];
this._updateButtons();
},
});
};
return {DarkroomPluginHistory: DarkroomPluginHistory};
});

64
web_widget_darkroom/static/src/js/plugins/darkroom.rotate.js

@ -0,0 +1,64 @@
/**
* Copyright 2013 Matthieu Moquet
* Copyright 2016-2017 LasLabs Inc.
* License MIT (https://opensource.org/licenses/MIT)
**/
odoo.define('web_widget_darkroom.darkroom_rotate', function() {
'use strict';
var DarkroomPluginRotate = function() {
var Rotation = Darkroom.Transformation.extend({
applyTransformation: function(canvas, image, next) {
var angle = (image.getAngle() + this.options.angle) % 360;
image.rotate(angle);
var height = Math.abs(image.getWidth()*Math.sin(angle*Math.PI/180))+Math.abs(image.getHeight()*Math.cos(angle*Math.PI/180));
var width = Math.abs(image.getHeight()*Math.sin(angle*Math.PI/180))+Math.abs(image.getWidth()*Math.cos(angle*Math.PI/180));
canvas.setWidth(width);
canvas.setHeight(height);
canvas.centerObject(image);
image.setCoords();
canvas.renderAll();
next();
},
});
Darkroom.plugins.rotate = Darkroom.Plugin.extend({
initialize: function InitDarkroomRotatePlugin() {
var buttonGroup = this.darkroom.toolbar.createButtonGroup();
var leftButton = buttonGroup.createButton({
image: 'fa fa-undo oe_edit_only',
editOnly: true,
});
var rightButton = buttonGroup.createButton({
image: 'fa fa-repeat oe_edit_only',
editOnly: true,
});
leftButton.addEventListener('click', this.rotateLeft.bind(this));
rightButton.addEventListener('click', this.rotateRight.bind(this));
},
rotateLeft: function rotateLeft() {
this.rotate(-90);
},
rotateRight: function rotateRight() {
this.rotate(90);
},
rotate: function rotate(angle) {
this.darkroom.applyTransformation(
new Rotation({angle: angle})
);
}
});
};
return {DarkroomPluginRotate: DarkroomPluginRotate};
});

148
web_widget_darkroom/static/src/js/plugins/darkroom.zoom.js

@ -0,0 +1,148 @@
/**
* Copyright 2013 Matthieu Moquet
* Copyright 2016-2017 LasLabs Inc.
* License MIT (https://opensource.org/licenses/MIT)
**/
odoo.define('web_widget_darkroom.darkroom_zoom', function() {
'use strict';
var DarkroomPluginZoom = function() {
Darkroom.plugins.zoom = Darkroom.Plugin.extend({
inZoom: false,
zoomLevel: 0,
zoomFactor: 0.1,
initialize: function() {
var self = this;
var buttonGroup = this.darkroom.toolbar.createButtonGroup();
this.zoomButton = buttonGroup.createButton({
image: 'fa fa-search',
});
this.zoomInButton = buttonGroup.createButton({
image: 'fa fa-plus',
});
this.zoomOutButton = buttonGroup.createButton({
image: 'fa fa-minus',
});
this.cancelButton = buttonGroup.createButton({
image: 'fa fa-times',
type: 'danger',
hide: true
});
// Button click events
this.zoomButton.addEventListener('click', this.toggleZoom.bind(this));
this.zoomInButton.addEventListener('click', this.zoomIn.bind(this));
this.zoomOutButton.addEventListener('click', this.zoomOut.bind(this));
this.cancelButton.addEventListener('click', this.releaseFocus.bind(this));
// Canvas events
this.darkroom.canvas.on('mouse:down', this.onMouseDown.bind(this));
this.darkroom.canvas.on('mouse:move', this.onMouseMove.bind(this));
this.darkroom.canvas.on('mouse:up', this.onMouseUp.bind(this));
$(this.darkroom.canvas.wrapperEl).on('mousewheel', function(event){
self.onMouseWheel(event);
});
this.toggleElements(false);
},
toggleZoom: function() {
if (this.hasFocus()) {
this.releaseFocus();
} else {
this.requireFocus();
}
},
hasFocus: function() {
return this.inZoom;
},
releaseFocus: function() {
this.toggleElements(false);
},
requireFocus: function() {
this.toggleElements(true);
},
toggleElements: function(bool) {
var toggle = bool;
if (typeof bool === 'undefined') {
toggle = !this.hasFocus();
}
this.zoomButton.active(toggle);
this.inZoom = toggle;
this.zoomInButton.hide(!toggle);
this.zoomOutButton.hide(!toggle);
this.cancelButton.hide(!toggle);
this.darkroom.canvas.default_cursor = toggle ? 'move' : 'default';
},
zoomIn: function() {
return this.setZoomLevel(this.zoomFactor, this.getCenterPoint());
},
zoomOut: function() {
return this.setZoomLevel(-this.zoomFactor, this.getCenterPoint());
},
// Return fabric.Point object for center of canvas
getCenterPoint: function() {
var center = this.darkroom.canvas.getCenter();
return new fabric.Point(center.left, center.top);
},
// Set internal zoom
setZoomLevel: function(factor, point) {
var zoomLevel = this.zoomLevel + factor;
if (zoomLevel < 0) {
zoomLevel = 0;
}
if (zoomLevel === this.zoomLevel) {
return false;
}
if (point) {
var canvas = this.darkroom.canvas;
// Add one for zero index
canvas.zoomToPoint(point, zoomLevel + 1);
this.zoomLevel = zoomLevel;
}
return true;
},
onMouseWheel: function(event) {
if (this.hasFocus() && event && event.originalEvent) {
var modifier = event.originalEvent.wheelDelta < 0 ? -1 : 1;
var pointer = this.darkroom.canvas.getPointer(event.originalEvent);
var mousePoint = new fabric.Point(pointer.x, pointer.y);
this.setZoomLevel(modifier * this.zoomFactor, mousePoint);
return event.preventDefault();
}
},
onMouseDown: function() {
if (this.hasFocus()) {
this.panning = true;
}
},
onMouseUp: function() {
this.panning = false;
},
onMouseMove: function(event) {
if (this.panning && event && event.e) {
var delta = new fabric.Point(event.e.movementX, event.e.movementY);
this.darkroom.canvas.relativePan(delta);
}
},
});
};
return {DarkroomPluginZoom: DarkroomPluginZoom};
});

224
web_widget_darkroom/static/src/js/widget_darkroom.js

@ -0,0 +1,224 @@
/**
* Copyright 2013 Matthieu Moquet
* Copyright 2016-2017 LasLabs Inc.
* License MIT (https://opensource.org/licenses/MIT)
**/
odoo.define('web_widget_darkroom.darkroom_widget', function(require) {
'use strict';
var core = require('web.core');
var common = require('web.form_common');
var session = require('web.session');
var utils = require('web.utils');
var _ = require('_');
var QWeb = core.qweb;
var FieldDarkroomImage = common.AbstractField.extend(common.ReinitializeFieldMixin, {
className: 'darkroom-widget',
template: 'FieldDarkroomImage',
placeholder: "/web/static/src/img/placeholder.png",
darkroom: null,
no_rerender: false,
defaults: {
// Canvas initialization size
minWidth: 100,
minHeight: 100,
maxWidth: 700,
maxHeight: 500,
// Plugin options
plugins: {
crop: {
minHeight: 50,
minWidth: 50,
ratio: 1
},
},
},
init: function(field_manager, node) {
this._super(field_manager, node);
this.options = _.defaults(this.options, this.defaults);
},
_init_darkroom: function() {
if (!this.darkroom) {
this._init_darkroom_icons();
this._init_darkroom_ui();
this._init_darkroom_plugins();
}
},
_init_darkroom_icons: function() {
var element = document.createElement('div');
element.id = 'darkroom-icons';
element.style.height = 0;
element.style.width = 0;
element.style.position = 'absolute';
element.style.visibility = 'hidden';
element.innerHTML = '<!-- inject:svg --><!-- endinject -->';
this.el.appendChild(element);
},
_init_darkroom_plugins: function() {
require('web_widget_darkroom.darkroom_crop').DarkroomPluginCrop();
require('web_widget_darkroom.darkroom_history').DarkroomPluginHistory();
require('web_widget_darkroom.darkroom_rotate').DarkroomPluginRotate();
require('web_widget_darkroom.darkroom_zoom').DarkroomPluginZoom();
},
_init_darkroom_ui: function() {
// Button object
function Button(element) {
this.element = element;
}
Button.prototype = {
addEventListener: function(eventName, listener) {
if (this.element.addEventListener) {
this.element.addEventListener(eventName, listener);
} else if (this.element.attachEvent) {
this.element.attachEvent('on' + eventName, listener);
}
},
removeEventListener: function(eventName, listener) {
if (this.element.removeEventListener) {
this.element.removeEventListener(eventName, listener);
} else if (this.element.detachEvent) {
this.element.detachEvent('on' + eventName, listener);
}
},
active: function(bool) {
if (bool) {
this.element.classList.add('darkroom-button-active');
} else {
this.element.classList.remove('darkroom-button-active');
}
},
hide: function(bool) {
if (bool) {
this.element.classList.add('hidden');
} else {
this.element.classList.remove('hidden');
}
},
disable: function(bool) {
this.element.disabled = bool;
},
};
// ButtonGroup object
function ButtonGroup(element) {
this.element = element;
}
ButtonGroup.prototype = {
createButton: function(options) {
var defaults = {
image: 'fa fa-question-circle',
type: 'default',
group: 'default',
hide: false,
disabled: false,
editOnly: false,
addClass: '',
};
var optionsMerged = Darkroom.Utils.extend(options, defaults);
var buttonElement = document.createElement('button');
buttonElement.type = 'button';
buttonElement.className = 'darkroom-button darkroom-button-' + optionsMerged.type;
buttonElement.innerHTML = '<i class="' + optionsMerged.image + ' fa-2x"></i>';
if (optionsMerged.editOnly) {
buttonElement.classList.add('oe_edit_only');
}
if (optionsMerged.addClass) {
buttonElement.classList.add(optionsMerged.addClass);
}
this.element.appendChild(buttonElement);
var button = new Button(buttonElement);
button.hide(optionsMerged.hide);
button.disable(optionsMerged.disabled);
return button;
}
};
// Toolbar object
function Toolbar(element) {
this.element = element;
}
Toolbar.prototype = {
createButtonGroup: function() {
var buttonGroupElement = document.createElement('div');
buttonGroupElement.className = 'darkroom-button-group';
this.element.appendChild(buttonGroupElement);
return new ButtonGroup(buttonGroupElement);
}
};
Darkroom.UI = {
Toolbar: Toolbar,
ButtonGroup: ButtonGroup,
Button: Button,
};
},
destroy_content: function() {
if (this.darkroom && this.darkroom.containerElement) {
this.darkroom.containerElement.remove();
this.darkroom = null;
}
},
set_value: function(value) {
return this._super(value);
},
render_value: function() {
this.destroy_content();
this._init_darkroom();
var url = null;
if (this.get('value') && !utils.is_bin_size(this.get('value'))) {
url = 'data:image/png;base64,' + this.get('value');
} else if (this.get('value')) {
var id = JSON.stringify(this.view.datarecord.id || null);
var field = this.name;
if (this.options.preview_image) {
field = this.options.preview_image;
}
url = session.url('/web/image', {
model: this.view.dataset.model,
id: id,
field: field,
unique: (this.view.datarecord.__last_update || '').replace(/[^0-9]/g, ''),
});
} else {
url = this.placeholder;
}
var $img = $(QWeb.render("FieldBinaryImage-img", {widget: this, url: url}));
this.$el.find('> img').remove();
this.$el.append($img);
this.darkroom = new Darkroom($img.get(0), this.options);
this.darkroom.widget = this;
},
commit_value: function() {
if (this.darkroom.sourceImage) {
this.set_value(this.darkroom.sourceImage.toDataURL().split(',')[1]);
}
},
});
core.form_widget_registry.add("darkroom", FieldDarkroomImage);
return {FieldDarkroomImage: FieldDarkroomImage};
});

64
web_widget_darkroom/static/src/js/widget_darkroom_modal.js

@ -0,0 +1,64 @@
/**
* Copyright 2017 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
**/
odoo.define('web_widget_darkroom.darkroom_modal_button', function(require) {
'use strict';
var core = require('web.core');
var DataModel = require('web.DataModel');
core.form_widget_registry.get('image').include({
// Used in template to prevent Darkroom buttons from being added to
// forms for new records, which are not supported
darkroom_supported: function() {
if (this.field_manager.dataset.index === null) {
return false;
}
return true;
},
render_value: function() {
this._super();
var imageWidget = this;
var activeModel = imageWidget.field_manager.dataset._model.name;
var activeRecordId = imageWidget.field_manager.datarecord.id;
var activeField = imageWidget.node.attrs.name;
var updateImage = function() {
var ActiveModel = new DataModel(activeModel);
ActiveModel.query([activeField]).
filter([['id', '=', activeRecordId]]).
all().
then(function(result) {
imageWidget.set_value(result[0].image);
});
};
var openModal = function() {
var context = {
active_model: activeModel,
active_record_id: activeRecordId,
active_field: activeField,
};
var modalAction = {
type: 'ir.actions.act_window',
res_model: 'darkroom.modal',
name: 'Darkroom',
views: [[false, 'form']],
target: 'new',
context: context,
};
var options = {on_close: updateImage};
imageWidget.do_action(modalAction, options);
};
var $button = this.$('.oe_form_binary_image_darkroom_modal');
if ($button.length > 0) {
$button.click(openModal);
}
},
});
});

11
web_widget_darkroom/static/src/less/darkroom.less

@ -0,0 +1,11 @@
.darkroom-button-group {
display: inline;
}
.darkroom-button-active {
color: @odoo-brand-primary;
}
.oe_form_field_image_controls i {
margin: 0 5%;
}

30
web_widget_darkroom/static/src/xml/field_templates.xml

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016-2017 LasLabs Inc.
License LGPL-3 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<templates id="field_templates" xml:space="preserve">
<t t-name="FieldDarkroomImage">
<span class="oe_form_field o_form_field_darkroom" t-att-style="widget.node.attrs.style">
<t t-if="!widget.get('effective_readonly')">
<div class="darkroom-toolbar"/>
</t>
</span>
</t>
<t t-extend="FieldBinaryImage">
<t t-jquery=".oe_form_binary_file_edit" t-operation="after">
<t t-if="widget.darkroom_supported()">
<i class="fa fa-picture-o fa-lg oe_form_binary_image_darkroom_modal" title="Darkroom"></i>
</t>
</t>
<t t-jquery=".oe_form_binary_file_edit" t-operation="replace">
<i class="fa fa-pencil fa-lg oe_form_binary_file_edit" title="Edit"></i>
</t>
<t t-jquery=".oe_form_binary_file_clear" t-operation="replace">
<i class="fa fa-trash-o fa-lg oe_form_binary_file_clear" title="Clear"></i>
</t>
</t>
</templates>

5
web_widget_darkroom/tests/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import test_darkroom_modal

203
web_widget_darkroom/tests/test_darkroom_modal.py

@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from openerp.tests.common import TransactionCase
class TestDarkroomModal(TransactionCase):
def test_default_res_model_id_model_in_context(self):
"""Should return correct ir.model record when context has model name"""
active_model = 'res.users'
test_model = self.env['darkroom.modal'].with_context({
'active_model': active_model,
})
test_result = test_model._default_res_model_id()
expected = self.env['ir.model'].search([('model', '=', active_model)])
self.assertEqual(test_result, expected)
def test_default_res_model_id_no_valid_info_in_context(self):
"""Should return empty ir.model recordset when missing/invalid info"""
test_model = self.env['darkroom.modal'].with_context({})
test_result = test_model._default_res_model_id()
self.assertEqual(test_result, self.env['ir.model'])
def test_default_res_record_id_id_in_context(self):
"""Should return correct value when ID in context"""
active_record_id = 5
test_model = self.env['darkroom.modal'].with_context({
'active_record_id': active_record_id,
})
test_result = test_model._default_res_record_id()
self.assertEqual(test_result, active_record_id)
def test_default_res_record_id_no_id_in_context(self):
"""Should return 0 when no ID in context"""
test_model = self.env['darkroom.modal'].with_context({})
test_result = test_model._default_res_record_id()
self.assertEqual(test_result, 0)
def test_default_res_record_model_and_id_in_context(self):
"""Should return correct record when context has model name and ID"""
active_model = 'res.users'
active_record_id = 1
test_model = self.env['darkroom.modal'].with_context({
'active_model': active_model,
'active_record_id': active_record_id,
})
test_result = test_model._default_res_record()
expected = self.env[active_model].browse(active_record_id)
self.assertEqual(test_result, expected)
def test_default_res_record_model_but_no_id_in_context(self):
"""Should return right empty recordset if model but no ID in context"""
active_model = 'res.users'
test_model = self.env['darkroom.modal'].with_context({
'active_model': active_model,
})
test_result = test_model._default_res_record()
self.assertEqual(test_result, self.env[active_model])
def test_default_res_record_no_valid_model_info_in_context(self):
"""Should return None if context has missing/invalid model info"""
active_model = 'bad.model.name'
test_model = self.env['darkroom.modal'].with_context({
'active_model': active_model,
})
test_result = test_model._default_res_record()
self.assertIsNone(test_result)
def test_default_res_field_id_model_and_field_in_context(self):
"""Should return correct ir.model.fields record when info in context"""
active_model = 'res.users'
active_field = 'name'
test_model = self.env['darkroom.modal'].with_context({
'active_model': active_model,
'active_field': active_field,
})
test_result = test_model._default_res_field_id()
self.assertEqual(test_result.name, active_field)
self.assertEqual(test_result.model_id.model, active_model)
def test_default_res_field_id_no_valid_field_in_context(self):
"""Should return empty recordset if field info missing/invalid"""
active_model = 'res.users'
active_field = 'totally.not.a.real.field.name'
test_model = self.env['darkroom.modal'].with_context({
'active_model': active_model,
'active_field': active_field,
})
test_result = test_model._default_res_field_id()
self.assertEqual(test_result, self.env['ir.model.fields'])
def test_default_res_field_id_no_valid_model_in_context(self):
"""Should return empty recordset if model info missing/invalid"""
active_field = 'name'
test_model = self.env['darkroom.modal'].with_context({
'active_field': active_field,
})
test_result = test_model._default_res_field_id()
self.assertEqual(test_result, self.env['ir.model.fields'])
def test_default_image_all_info_in_context(self):
"""Should return value of correct field if all info in context"""
active_model = 'res.users'
active_record_id = 1
active_field = 'name'
test_model = self.env['darkroom.modal'].with_context({
'active_model': active_model,
'active_record_id': active_record_id,
'active_field': active_field,
})
test_result = test_model._default_image()
expected = self.env[active_model].browse(active_record_id).name
self.assertEqual(test_result, expected)
def test_default_image_no_valid_field_in_context(self):
"""Should return None if missing/invalid field info in context"""
active_model = 'res.users'
active_record_id = 1
test_model = self.env['darkroom.modal'].with_context({
'active_model': active_model,
'active_record_id': active_record_id,
})
test_result = test_model._default_image()
self.assertIsNone(test_result)
def test_default_image_no_valid_id_in_context(self):
"""Should return False/None if missing/invalid record ID in context"""
active_model = 'res.users'
active_field = 'name'
test_model = self.env['darkroom.modal'].with_context({
'active_model': active_model,
'active_field': active_field,
})
test_result = test_model._default_image()
self.assertFalse(test_result)
def test_default_image_no_valid_model_in_context(self):
"""Should return None if missing/invalid model info in context"""
active_record_id = 1
active_field = 'name'
test_model = self.env['darkroom.modal'].with_context({
'active_record_id': active_record_id,
'active_field': active_field,
})
test_result = test_model._default_image()
self.assertIsNone(test_result)
def test_action_save_record_count_in_self(self):
"""Should raise correct error if not called on recordset of 1"""
test_wizard = self.env['darkroom.modal'].with_context({
'active_model': 'res.users',
'active_record_id': 1,
'active_field': 'name',
}).create({})
test_wizard_set = test_wizard + test_wizard.copy()
with self.assertRaises(ValueError):
self.env['darkroom.modal'].action_save()
with self.assertRaises(ValueError):
test_wizard_set.action_save()
def test_action_save_update_source(self):
"""Should update source record correctly"""
active_model = 'res.users'
active_record_id = 1
test_wizard = self.env['darkroom.modal'].with_context({
'active_model': active_model,
'active_record_id': active_record_id,
'active_field': 'name',
}).create({})
test_name = 'Test Name'
test_wizard.image = test_name
test_wizard.action_save()
result = self.env[active_model].browse(active_record_id).name
self.assertEqual(result, test_name)
def test_action_save_return_action(self):
"""Should return correct action"""
test_wizard = self.env['darkroom.modal'].with_context({
'active_model': 'res.users',
'active_record_id': 1,
'active_field': 'name',
}).create({})
test_value = test_wizard.action_save()
self.assertEqual(test_value, {'type': 'ir.actions.act_window_close'})

28
web_widget_darkroom/views/assets.xml

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016-2017 LasLabs Inc.
License LGPL-3 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<odoo>
<template id="assets_darkroom" name="web_widget_darkroom Assets" inherit_id="web.assets_backend">
<xpath expr="//script[last()]" position="after">
<link href="/web_widget_darkroom/static/src/less/darkroom.less" rel="stylesheet" type="text/less"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.require.min.js"/>
<script src="/web_widget_darkroom/static/lib/darkroomjs/core/darkroom.js"/>
<script src="/web_widget_darkroom/static/lib/darkroomjs/core/plugin.js"/>
<script src="/web_widget_darkroom/static/lib/darkroomjs/core/transformation.js"/>
<script src="/web_widget_darkroom/static/lib/darkroomjs/core/utils.js"/>
<script src="/web_widget_darkroom/static/src/js/plugins/darkroom.crop.js"/>
<script src="/web_widget_darkroom/static/src/js/plugins/darkroom.history.js"/>
<script src="/web_widget_darkroom/static/src/js/plugins/darkroom.rotate.js"/>
<script src="/web_widget_darkroom/static/src/js/plugins/darkroom.zoom.js"/>
<script src="/web_widget_darkroom/static/src/js/widget_darkroom.js"/>
<script src="/web_widget_darkroom/static/src/js/widget_darkroom_modal.js"/>
</xpath>
</template>
</odoo>

5
web_widget_darkroom/wizards/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import darkroom_modal

82
web_widget_darkroom/wizards/darkroom_modal.py

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from openerp import api, fields, models
from openerp.exceptions import MissingError
class DarkroomModal(models.TransientModel):
_name = 'darkroom.modal'
_description = 'Darkroom Modal - Wizard Model'
@api.model
def _default_res_model_id(self):
res_model_name = self.env.context.get('active_model')
return self.env['ir.model'].search([('model', '=', res_model_name)])
@api.model
def _default_res_record_id(self):
return self.env.context.get('active_record_id', 0)
@api.model
def _default_res_record(self):
res_model_name = self._default_res_model_id().model
try:
res_model_model = self.env[res_model_name]
except KeyError:
return None
return res_model_model.browse(self._default_res_record_id())
@api.model
def _default_res_field_id(self):
res_model_id = self._default_res_model_id()
res_field_name = self.env.context.get('active_field')
return self.env['ir.model.fields'].search([
('model_id', '=', res_model_id.id),
('name', '=', res_field_name),
])
@api.model
def _default_image(self):
res_record = self._default_res_record()
res_field_name = self._default_res_field_id().name
try:
return getattr(res_record, res_field_name, None)
except (TypeError, MissingError):
return None
res_model_id = fields.Many2one(
comodel_name='ir.model',
string='Source Model',
required=True,
default=lambda s: s._default_res_model_id(),
)
res_record_id = fields.Integer(
string='Source Record ID',
required=True,
default=lambda s: s._default_res_record_id(),
)
res_field_id = fields.Many2one(
comodel_name='ir.model.fields',
string='Source Field',
required=True,
default=lambda s: s._default_res_field_id(),
)
image = fields.Binary(
string='Darkroom Image',
required=True,
default=lambda s: s._default_image(),
)
@api.multi
def action_save(self):
self.ensure_one()
res_record = self._default_res_record()
res_field_name = self._default_res_field_id().name
setattr(res_record, res_field_name, self.image)
return {'type': 'ir.actions.act_window_close'}

27
web_widget_darkroom/wizards/darkroom_modal.xml

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<odoo>
<record id="darkroom_modal_view_form" model="ir.ui.view">
<field name="name">Darkroom Modal Wizard</field>
<field name="model">darkroom.modal</field>
<field name="arch" type="xml">
<form string="Darkroom Modal">
<header />
<sheet>
<group name="data">
<field name="image" widget="darkroom" nolabel="1"/>
</group>
</sheet>
<footer>
<button special="cancel" string="Cancel" class="pull-left"/>
<button name="action_save" type="object" string="Save" class="oe_highlight pull-right"/>
</footer>
</form>
</field>
</record>
</odoo>
Loading…
Cancel
Save