001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ade.galleries.client.preview;
029
030import org.opencms.ade.galleries.client.preview.ui.CmsFocalPoint;
031import org.opencms.ade.galleries.client.preview.util.CmsBoxFit;
032import org.opencms.ade.galleries.client.preview.util.CmsCompositeTransform;
033import org.opencms.ade.galleries.client.preview.util.CmsRectangle;
034import org.opencms.ade.galleries.client.preview.util.CmsTranslate;
035import org.opencms.ade.galleries.client.preview.util.I_CmsTransform;
036import org.opencms.ade.galleries.shared.CmsImageInfoBean;
037import org.opencms.ade.galleries.shared.CmsPoint;
038import org.opencms.gwt.client.CmsCoreProvider;
039import org.opencms.gwt.client.rpc.CmsRpcAction;
040import org.opencms.gwt.client.util.CmsClientStringUtil;
041import org.opencms.gwt.shared.CmsGwtConstants;
042import org.opencms.gwt.shared.property.CmsPropertyChangeSet;
043import org.opencms.gwt.shared.property.CmsPropertyModification;
044import org.opencms.util.CmsStringUtil;
045import org.opencms.util.CmsUUID;
046
047import java.util.ArrayList;
048import java.util.List;
049import java.util.function.Supplier;
050
051import com.google.gwt.dom.client.Element;
052import com.google.gwt.dom.client.NativeEvent;
053import com.google.gwt.event.dom.client.LoadEvent;
054import com.google.gwt.event.dom.client.LoadHandler;
055import com.google.gwt.event.shared.HandlerRegistration;
056import com.google.gwt.user.client.Event;
057import com.google.gwt.user.client.Event.NativePreviewEvent;
058import com.google.gwt.user.client.Event.NativePreviewHandler;
059import com.google.gwt.user.client.ui.FlowPanel;
060import com.google.gwt.user.client.ui.Image;
061
062/**
063 * Handles manipulation of the focal point in the gallery dialog.<p>
064 */
065public class CmsFocalPointController {
066
067    /** Global static flag to enable / disable focal point modification. */
068    public static final boolean ENABLED = true;
069
070    /** Preview handler registration for the event handler used for drag / drop. */
071    private static HandlerRegistration m_previewRegistration;
072
073    /** The container. */
074    private FlowPanel m_container;
075
076    /** The transformation for transforming from the image parent's coordinate system to the true underlying image's native coordinate system. */
077    private CmsCompositeTransform m_coordinateTransform;
078
079    /** The source of the cropping parameter information. */
080    private Supplier<CmsCroppingParamBean> m_croppingProvider;
081
082    /** The current focal point location. */
083    private CmsPoint m_focalPoint;
084
085    /** The currently displayed image. */
086    private Image m_image;
087
088    /** The source of the image information. */
089    private Supplier<CmsImageInfoBean> m_imageInfoProvider;
090
091    /** The action to execute when the focal point is changed. */
092    private Runnable m_nextAction;
093
094    /** The widget representing the focal point. */
095    private CmsFocalPoint m_pointWidget;
096
097    /** The region in which the user should be able to move the focal point widget on the screen. */
098    private CmsRectangle m_region;
099
100    /** The focal point location which was last saved. */
101    private CmsPoint m_savedFocalPoint;
102
103    /**
104     * Creates a new instance.<p>
105     *
106     * @param croppingProvider the source of the cropping information
107     * @param infoProvider the source of the image info
108     * @param nextAction the action to execute when the focal point is changed
109     */
110    public CmsFocalPointController(
111        Supplier<CmsCroppingParamBean> croppingProvider,
112        Supplier<CmsImageInfoBean> infoProvider,
113        Runnable nextAction) {
114
115        m_croppingProvider = croppingProvider;
116        m_imageInfoProvider = infoProvider;
117        m_nextAction = nextAction;
118    }
119
120    /**
121     * Clears the static event handler for the drag and drop.<p>
122     */
123    private static void clearEventHandler() {
124
125        if (m_previewRegistration != null) {
126            m_previewRegistration.removeHandler();
127        }
128        m_previewRegistration = null;
129    }
130
131    /**
132     * Gets the pageX offset for a mouse event.<p>
133     *
134     * @param event the event
135     *
136     * @return the pageX offset
137     */
138    private static native double pageX(NativeEvent event) /*-{
139        return event.pageX;
140    }-*/;
141
142    /**
143     * Gets the pageY offset for a mouse event.<p>
144     *
145     * @param event the event
146     *
147     * @return the pageY offset
148     */
149    private static native double pageY(NativeEvent event) /*-{
150        return event.pageY;
151    }-*/;
152
153    /**
154     * Transforms a rectangle with the inverse of a coordinate transform.<p>
155     *
156     * @param transform the coordinate transform
157     * @param region the rectangle to transform
158     * @return the transformed rectangle
159     */
160    private static CmsRectangle transformRegionBack(I_CmsTransform transform, CmsRectangle region) {
161
162        CmsPoint topLeft = region.getTopLeft();
163        CmsPoint bottomRight = region.getBottomRight();
164        return CmsRectangle.fromPoints(transform.transformBack(topLeft), transform.transformBack(bottomRight));
165    }
166
167    /**
168     * Called when the user clicks on the focal point widget.<p>
169     *
170     * This starts drag and drop.
171     */
172    public void onStartDrag() {
173
174        if (ENABLED && isEditable()) {
175            registerEventHandler();
176        }
177    }
178
179    /**
180     * Saves the focal point to a property on the image.<p>
181     */
182    public void reset() {
183
184        if (isEditable()) {
185
186            String val = "";
187            CmsUUID sid = m_imageInfoProvider.get().getStructureId();
188            List<CmsPropertyModification> propChanges = new ArrayList<>();
189            propChanges.add(new CmsPropertyModification(sid, CmsGwtConstants.PROPERTY_IMAGE_FOCALPOINT, val, false));
190            propChanges.add(new CmsPropertyModification(sid, CmsGwtConstants.PROPERTY_IMAGE_FOCALPOINT, val, true));
191            final CmsPropertyChangeSet changeSet = new CmsPropertyChangeSet(sid, propChanges);
192            CmsRpcAction<Void> action = new CmsRpcAction<Void>() {
193
194                @Override
195                public void execute() {
196
197                    CmsCoreProvider.getVfsService().saveProperties(changeSet, false, this);
198
199                }
200
201                @SuppressWarnings("synthetic-access")
202                @Override
203                protected void onResponse(Void result) {
204
205                    m_focalPoint = null;
206                    m_savedFocalPoint = null;
207                    m_imageInfoProvider.get().setFocalPoint(null);
208                    updatePoint();
209                    if (m_nextAction != null) {
210                        m_nextAction.run();
211                    }
212                }
213            };
214            action.execute();
215        }
216    }
217
218    /**
219     * Updates the image.<p>
220     *
221     * @param container the parent widget for the image
222     * @param previewImage the image
223     */
224    public void updateImage(FlowPanel container, Image previewImage) {
225
226        if (!ENABLED) {
227            return;
228        }
229        String path = m_imageInfoProvider.get().getResourcePath();
230        if (CmsClientStringUtil.checkIsPathOrLinkToSvg(path)) {
231            return;
232        }
233        m_image = previewImage;
234        clearImagePoint();
235        m_savedFocalPoint = m_imageInfoProvider.get().getFocalPoint();
236        m_focalPoint = m_savedFocalPoint;
237        m_container = container;
238
239        previewImage.addLoadHandler(new LoadHandler() {
240
241            @SuppressWarnings("synthetic-access")
242            public void onLoad(LoadEvent event) {
243
244                updateScaling();
245                updatePoint();
246            }
247
248        });
249    }
250
251    /**
252     * Removes the focal point widget.<p>
253     */
254    private void clearImagePoint() {
255
256        if (m_pointWidget != null) {
257            m_pointWidget.removeFromParent();
258            m_pointWidget = null;
259        }
260    }
261
262    /**
263     * Gets the point which is the center of the crop region, or the center of the original image if it isn't cropped, in the image's coordinate system.<p>
264     *
265     * @return the center point of the crop region
266     */
267    private CmsPoint getCropCenter() {
268
269        CmsCroppingParamBean crop = m_croppingProvider.get();
270        CmsImageInfoBean info = m_imageInfoProvider.get();
271        if ((crop == null) || !crop.isCropped()) {
272            return new CmsPoint(info.getWidth() / 2, info.getHeight() / 2);
273        } else {
274            return new CmsPoint(
275                crop.getCropX() + (crop.getCropWidth() / 2),
276                crop.getCropY() + (crop.getCropHeight() / 2));
277        }
278    }
279
280    /**
281     * Gets the rectangle in the image's coordinate system which corresponds to the crop region (or the whole image,
282     * in case cropping is not used).<p>
283     *
284     * @return the crop region
285     */
286    private CmsRectangle getNativeCropRegion() {
287
288        CmsCroppingParamBean crop = m_croppingProvider.get();
289        CmsImageInfoBean info = m_imageInfoProvider.get();
290        if ((crop == null) || !crop.isCropped()) {
291            return CmsRectangle.fromLeftTopWidthHeight(0, 0, info.getWidth(), info.getHeight());
292        } else {
293            return CmsRectangle.fromLeftTopWidthHeight(
294                crop.getCropX(),
295                crop.getCropY(),
296                crop.getCropWidth(),
297                crop.getCropHeight());
298        }
299    }
300
301    /**
302     * Handles mouse drag.<p>
303     *
304     * @param nativeEvent the mousemove event
305     */
306    private void handleMove(NativeEvent nativeEvent) {
307
308        Element imageElem = m_image.getElement();
309        int offsetX = ((int)pageX(nativeEvent)) - imageElem.getParentElement().getAbsoluteLeft();
310        int offsetY = ((int)pageY(nativeEvent)) - imageElem.getParentElement().getAbsoluteTop();
311        if (m_coordinateTransform != null) {
312            CmsPoint screenPoint = new CmsPoint(offsetX, offsetY);
313            screenPoint = m_region.constrain(screenPoint); // make sure we remain in the screen region corresponding to original image (or crop).
314            m_pointWidget.setCenterCoordsRelativeToParent((int)screenPoint.getX(), (int)screenPoint.getY());
315            CmsPoint logicalPoint = m_coordinateTransform.transformForward(screenPoint);
316            m_focalPoint = logicalPoint;
317        }
318    }
319
320    /**
321     * Check if the image is editable.<p>
322     *
323     * @return true if the image is editable
324     */
325    private boolean isEditable() {
326
327        String noEditReason = m_imageInfoProvider.get().getNoEditReason();
328        boolean result = CmsStringUtil.isEmptyOrWhitespaceOnly(noEditReason);
329        return result;
330    }
331
332    /**
333     * Registers the preview event handler used for drag and drop.<p>
334     */
335    private void registerEventHandler() {
336
337        clearEventHandler();
338        m_previewRegistration = Event.addNativePreviewHandler(new NativePreviewHandler() {
339
340            @SuppressWarnings("synthetic-access")
341            public void onPreviewNativeEvent(NativePreviewEvent event) {
342
343                if (!(m_pointWidget.isAttached())) {
344                    clearEventHandler();
345                    return;
346                }
347
348                NativeEvent nativeEvent = event.getNativeEvent();
349                if (nativeEvent == null) {
350                    return;
351                }
352                int eventType = event.getTypeInt();
353                switch (eventType) {
354                    case Event.ONMOUSEUP:
355                        clearEventHandler();
356                        save();
357                        break;
358                    case Event.ONMOUSEMOVE:
359                        handleMove(nativeEvent);
360                        break;
361                    default:
362                        break;
363                }
364            }
365
366        });
367    }
368
369    /**
370     * Saves the focal point to a property on the image.<p>
371     */
372    private void save() {
373
374        if ((m_focalPoint != null) && isEditable()) {
375            int x = (int)m_focalPoint.getX();
376            int y = (int)m_focalPoint.getY();
377            String val = "" + x + "," + y;
378            CmsUUID sid = m_imageInfoProvider.get().getStructureId();
379            List<CmsPropertyModification> propChanges = new ArrayList<>();
380            propChanges.add(new CmsPropertyModification(sid, CmsGwtConstants.PROPERTY_IMAGE_FOCALPOINT, val, false));
381            final CmsPropertyChangeSet changeSet = new CmsPropertyChangeSet(sid, propChanges);
382            CmsRpcAction<Void> action = new CmsRpcAction<Void>() {
383
384                @Override
385                public void execute() {
386
387                    CmsCoreProvider.getVfsService().saveProperties(changeSet, false, this);
388
389                }
390
391                @SuppressWarnings("synthetic-access")
392                @Override
393                protected void onResponse(Void result) {
394
395                    m_savedFocalPoint = m_focalPoint;
396
397                    if (m_pointWidget != null) {
398                        m_pointWidget.setIsDefault(false);
399                    }
400                    m_imageInfoProvider.get().setFocalPoint(m_focalPoint);
401                    if (m_nextAction != null) {
402                        m_nextAction.run();
403                    }
404
405                }
406            };
407            action.execute();
408
409        }
410    }
411
412    /**
413     * Updates the focal point widget.<p>
414     */
415    private void updatePoint() {
416
417        clearImagePoint();
418        CmsPoint nativePoint;
419        if (m_focalPoint == null) {
420            CmsPoint cropCenter = getCropCenter();
421            nativePoint = cropCenter;
422        } else if (!getNativeCropRegion().contains(m_focalPoint)) {
423            return;
424        } else {
425            nativePoint = m_focalPoint;
426        }
427        m_pointWidget = new CmsFocalPoint(CmsFocalPointController.this);
428        boolean isDefault = m_savedFocalPoint == null;
429        m_pointWidget.setIsDefault(isDefault);
430        m_container.add(m_pointWidget);
431        CmsPoint screenPoint = m_coordinateTransform.transformBack(nativePoint);
432        m_pointWidget.setCenterCoordsRelativeToParent((int)screenPoint.getX(), (int)screenPoint.getY());
433    }
434
435    /**
436     * Sets up the coordinate transformations between the coordinate system of the parent element of the image element and the native coordinate system
437     * of the original image.
438     */
439    private void updateScaling() {
440
441        List<I_CmsTransform> transforms = new ArrayList<>();
442        CmsCroppingParamBean crop = m_croppingProvider.get();
443        CmsImageInfoBean info = m_imageInfoProvider.get();
444
445        double wv = m_image.getElement().getParentElement().getOffsetWidth();
446        double hv = m_image.getElement().getParentElement().getOffsetHeight();
447        if (crop == null) {
448            transforms.add(
449                new CmsBoxFit(CmsBoxFit.Mode.scaleOnlyIfNecessary, wv, hv, info.getWidth(), info.getHeight()));
450        } else {
451            int wt, ht;
452            wt = crop.getTargetWidth() >= 0 ? crop.getTargetWidth() : info.getWidth();
453            ht = crop.getTargetHeight() >= 0 ? crop.getTargetHeight() : info.getHeight();
454            transforms.add(new CmsBoxFit(CmsBoxFit.Mode.scaleOnlyIfNecessary, wv, hv, wt, ht));
455            if (crop.isCropped()) {
456                transforms.add(
457                    new CmsBoxFit(CmsBoxFit.Mode.scaleAlways, wt, ht, crop.getCropWidth(), crop.getCropHeight()));
458                transforms.add(new CmsTranslate(crop.getCropX(), crop.getCropY()));
459            } else {
460                transforms.add(
461                    new CmsBoxFit(CmsBoxFit.Mode.scaleAlways, wt, ht, crop.getOrgWidth(), crop.getOrgHeight()));
462            }
463        }
464        CmsCompositeTransform chain = new CmsCompositeTransform(transforms);
465        m_coordinateTransform = chain;
466        if ((crop == null) || !crop.isCropped()) {
467            m_region = transformRegionBack(
468                m_coordinateTransform,
469                CmsRectangle.fromLeftTopWidthHeight(0, 0, info.getWidth(), info.getHeight()));
470        } else {
471            m_region = transformRegionBack(
472                m_coordinateTransform,
473                CmsRectangle.fromLeftTopWidthHeight(
474                    crop.getCropX(),
475                    crop.getCropY(),
476                    crop.getCropWidth(),
477                    crop.getCropHeight()));
478        }
479    }
480
481}