package org.ovirt.engine.ui.common.widget.editor; import java.util.List; import java.util.Objects; import org.gwtbootstrap3.client.ui.Alert; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.ui.common.CommonApplicationConstants; import org.ovirt.engine.ui.common.editor.UiCommonEditor; import org.ovirt.engine.ui.common.gin.AssetProvider; import org.ovirt.engine.ui.common.widget.AbstractValidatedWidget; import org.ovirt.engine.ui.common.widget.dialog.InfoIcon; import org.ovirt.engine.ui.uicommonweb.models.vms.IconWithOsDefault; import org.ovirt.engine.ui.uicommonweb.validation.IconValidation; import org.ovirt.engine.ui.uicommonweb.validation.ValidationResult; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.ImageElement; import com.google.gwt.editor.client.LeafValueEditor; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.KeyPressHandler; import com.google.gwt.event.dom.client.KeyUpHandler; import com.google.gwt.event.logical.shared.HasValueChangeHandlers; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.resources.client.CssResource; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.safehtml.shared.SafeHtmlUtils; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.uibinder.client.UiHandler; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.FileUpload; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.Widget; /** * Icon editor. It allows to set custom icon to VM-like entities. */ public class IconEditorWidget extends AbstractValidatedWidget implements LeafValueEditor<IconWithOsDefault>, HasValueChangeHandlers<IconWithOsDefault>, UiCommonEditor<IconWithOsDefault> { interface ViewUiBinder extends UiBinder<HTMLPanel, IconEditorWidget> { ViewUiBinder uiBinder = GWT.create(ViewUiBinder.class); } protected interface Style extends CssResource { String iconImageDisabled(); } @UiField protected Style style; @UiField protected Image image; @UiField protected HTMLPanel hiddenPanel; @UiField protected Button uploadButton; @UiField(provided = true) protected InfoIcon uploadInfoIcon; @UiField protected Button defaultButton; @UiField protected Alert errorMessage; /** * current value, the visible image <br/> * in dataUri format */ private String icon; /** * default value (given by OS of VM) <br/> * in dataUri format */ private String defaultIcon; /** * Small icon id matching the icon downloaded from server or null. */ private Guid smallIconId; /** * relates to {@link com.google.gwt.user.client.ui.HasEnabled} implementation */ private boolean enabled; /** * relates to {@link org.ovirt.engine.ui.common.widget.HasAccess} implementation */ private boolean accessible; /** * Result of validation of current icon. * <p> * null means that current icon hasn't been validated yet. * </p> */ private ValidationResult validationResult; private static final CommonApplicationConstants constants = AssetProvider.getConstants(); public IconEditorWidget() { uploadInfoIcon = new InfoIcon( SafeHtmlUtils.fromTrustedString(constants.iconLimitationsIconVmPopup())); initWidget(ViewUiBinder.uiBinder.createAndBindUi(this)); KeyPressHandler preventEnterKeyPressHandler = createPreventEnterKeyPressHandler(); uploadButton.addKeyPressHandler(preventEnterKeyPressHandler); defaultButton.addKeyPressHandler(preventEnterKeyPressHandler); setEnabled(true); setAccessible(true); } private void validateIcon() { final IconWithOsDefault oldValue = getValue(); createValidationImageElement(icon, new ImageElementCallback() { @Override public void onElementReady(ImageElement imageElement) { validationResult = new IconValidation(imageElement).validate(icon); updateErrorIconLabel(validationResult); final IconWithOsDefault newValue = getValue(); ValueChangeEvent.fireIfNotEqual(IconEditorWidget.this, oldValue, newValue); } }); } /** * {@link Image} widget can't be used because it loads images lazily. {@link ImageElement} can't be used because it * doesn't have Java methods register 'load' and 'error' callbacks. */ private native void createValidationImageElement(String imageUrl, ImageElementCallback imageElementCallback) /*-{ var imageElement = document.createElement('img'); var callback = $entry(function () { imageElementCallback.@org.ovirt.engine.ui.common.widget.editor.IconEditorWidget.ImageElementCallback::onJavaScriptImageObjectReady(Lcom/google/gwt/core/client/JavaScriptObject;)(imageElement); }); imageElement.addEventListener('load', callback); imageElement.addEventListener('error', callback); imageElement.src = imageUrl; }-*/; private KeyPressHandler createPreventEnterKeyPressHandler() { return event -> { if (!event.isAnyModifierKeyDown() && event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) { event.preventDefault(); event.stopPropagation(); } }; } @Override public void setValue(IconWithOsDefault value) { final IconWithOsDefault oldPair = getValue(); if (Objects.equals(value, oldPair)) { return; } if (value == null) { defaultIcon = null; smallIconId = null; validationResult = null; setIcon(null); } else { defaultIcon = value.getOsDefaultIcon(); smallIconId = value.getSmallIconId(); validationResult = value.getValidationResult(); setIcon(value.getIcon()); } final IconWithOsDefault newPair = getValue(); ValueChangeEvent.fireIfNotEqual(this, oldPair, newPair); } @Override public IconWithOsDefault getValue() { if (icon == null || defaultIcon == null) { return null; } return new IconWithOsDefault(icon, defaultIcon, smallIconId, validationResult); } @Override protected Widget getValidatedWidget() { return this; } @UiHandler("uploadButton") void onUploadIconButton(ClickEvent event) { hiddenPanel.clear(); final FileUpload inputFileWidget = new FileUpload(); inputFileWidget.getElement().setAttribute("accept", "image/gif,image/jpeg,image/png"); //$NON-NLS-1$ //$NON-NLS-2$ inputFileWidget.addChangeHandler(e -> readUploadedIconFile(inputFileWidget.getElement())); inputFileWidget.getElement().setTabIndex(-1); hiddenPanel.add(inputFileWidget); inputFileWidget.click(); } @UiHandler("defaultButton") void onDefaultIconButton(ClickEvent event) { setIconAndFireChangeEvent(defaultIcon, ValidationResult.ok()); } protected void setIcon(final String icon) { this.icon = icon; image.setUrl(icon == null ? "" : icon); //$NON-NLS-1$ if (validationResult == null) { validateIcon(); } else { updateErrorIconLabel(validationResult); } } protected void setIconAndFireChangeEvent(String icon, ValidationResult validationResult) { final IconWithOsDefault oldPair = getValue(); this.validationResult = validationResult; smallIconId = null; setIcon(icon); final IconWithOsDefault newPair = getValue(); ValueChangeEvent.fireIfNotEqual(this, oldPair, newPair); } private void updateErrorIconLabel(ValidationResult validationResult) { if (!validationResult.getSuccess() && validationResult.getReasons().isEmpty()) { throw new IllegalArgumentException("Unsuccessful validation without any reason not allowed."); //$NON-NLS-1$ } updateErrorIconLabel(validationResult.getReasons()); } private void updateErrorIconLabel(List<String> reasons) { if (reasons.isEmpty()) { errorMessage.setText(SafeHtmlUtils.EMPTY_SAFE_HTML.asString()); } else { final SafeHtml htmlReasons = toList(reasons); errorMessage.setText(htmlReasons.asString()); errorMessage.setVisible(true); } } private SafeHtml toList(List<String> stringItems) { SafeHtmlBuilder builder = new SafeHtmlBuilder(); builder.appendEscaped(stringItems.get(0)); return builder.toSafeHtml(); } native void readUploadedIconFile(Element inputFileElement) /*-{ var self = this; var javaCallback = $entry(function (dataUri) { return self.@org.ovirt.engine.ui.common.widget.editor.IconEditorWidget::setIconAndFireChangeEvent(Ljava/lang/String;Lorg/ovirt/engine/ui/uicommonweb/validation/ValidationResult;)(dataUri, null); }); if (inputFileElement.files.length > 0) { var file = inputFileElement.files[0]; var fileReader = new FileReader(); fileReader.onload = onFileRead; fileReader.readAsDataURL(file); } function onFileRead(event) { var iconDataUri = event.target.result; javaCallback(iconDataUri); } }-*/; /* * see com.google.gwt.user.client.ui.ValueListBox.addValueChangeHandler() */ @Override public HandlerRegistration addValueChangeHandler(ValueChangeHandler<IconWithOsDefault> handler) { return this.addHandler(handler, ValueChangeEvent.getType()); } @Override public void markAsValid() { super.markAsValid(); errorMessage.setVisible(false); getValidatedWidgetStyle().setBorderColor("transparent"); //$NON-NLS-1$ } @Override public void markAsInvalid(List<String> validationHints) { super.markAsInvalid(validationHints); updateErrorIconLabel(validationHints); errorMessage.setVisible(true); } @Override public LeafValueEditor<IconWithOsDefault> getActualEditor() { return this; } @Override public int getTabIndex() { return uploadButton.getTabIndex(); } @Override public void setAccessKey(char key) { uploadButton.setAccessKey(key); } @Override public void setFocus(boolean focused) { uploadButton.setFocus(focused); } @Override public void setTabIndex(int index) { uploadButton.setTabIndex(index); defaultButton.setTabIndex(index); } @Override public void disable(String disabilityHint) { setEnabled(false, disabilityHint); } @Override public boolean isEnabled() { return enabled; } @Override public void setEnabled(boolean enabled) { setEnabled(enabled, ""); //$NON-NLS-1$ } protected void setEnabled(boolean enabled, String hint) { this.enabled = enabled; uploadButton.setEnabled(enabled); uploadButton.setTitle(hint); defaultButton.setEnabled(enabled); defaultButton.setTitle(hint); ensureStyleNamePresent(image, !enabled, style.iconImageDisabled()); image.setTitle(hint); } private static void ensureStyleNamePresent(UIObject object, boolean styleNameExists, String styleName) { if (styleNameExists) { object.addStyleName(styleName); } else { object.removeStyleName(styleName); } } @Override public boolean isAccessible() { return accessible; } @Override public void setAccessible(boolean accessible) { this.accessible = accessible; setVisible(accessible); } @Override public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { return CompositeHandlerRegistration.of( uploadButton.addKeyDownHandler(handler), defaultButton.addKeyDownHandler(handler)); } @Override public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { return CompositeHandlerRegistration.of( uploadButton.addKeyPressHandler(handler), defaultButton.addKeyPressHandler(handler)); } @Override public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) { return CompositeHandlerRegistration.of( uploadButton.addKeyUpHandler(handler), defaultButton.addKeyUpHandler(handler)); } private abstract class ImageElementCallback { private void onJavaScriptImageObjectReady(JavaScriptObject jsImageObject) { if (!ImageElement.is(jsImageObject)) { throw new RuntimeException("Unexpected type of JavaScript object"); //$NON-NLS-1$ } final ImageElement imageElement = (ImageElement) ImageElement.as(jsImageObject); onElementReady(imageElement); } public abstract void onElementReady(ImageElement imageElement); } }