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}