Jakub Niedermertl has uploaded a new change for review. Change subject: VM Icons - editor ......................................................................
VM Icons - editor Change-Id: Ibd5e8c120282e35d5940a66286d600b128c57753 Signed-off-by: Jakub Niedermertl <jnied...@redhat.com> --- M backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/VmManagementParametersBase.java M backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/queries/OsQueryParameters.java M frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/CommonApplicationConstants.java M frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/CommonApplicationTemplates.java M frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/gin/CommonGinUiBinderWidgets.java M frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/HasAccess.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/editor/CompositeHandlerRegistration.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/editor/IconEditorWidget.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/editor/IconEditorWidget.ui.xml M frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/AbstractVmPopupWidget.java M frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/AbstractVmPopupWidget.ui.xml M frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/Linq.java M frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/TabName.java A frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/IconWithDefault.java M frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/UnitVmModel.java A frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/IconValidation.java A frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/IconWithDefaultValidation.java M frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/ValidationResult.java M frontend/webadmin/modules/uicompat/src/main/java/org/ovirt/engine/ui/uicompat/UIConstants.java M frontend/webadmin/modules/uicompat/src/main/java/org/ovirt/engine/ui/uicompat/UIMessages.java 20 files changed, 830 insertions(+), 9 deletions(-) git pull ssh://gerrit.ovirt.org:29418/ovirt-engine refs/changes/85/37985/1 diff --git a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/VmManagementParametersBase.java b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/VmManagementParametersBase.java index bc9d9a8..c02e05e 100644 --- a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/VmManagementParametersBase.java +++ b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/action/VmManagementParametersBase.java @@ -34,6 +34,7 @@ private boolean copyTemplatePermissions; private boolean applyChangesLater; private boolean updateNuma; + // TODO jakub add private String vmIcon; /* * This parameter is needed at update to make sure that when we get a null watchdog from rest-api it is not meant to diff --git a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/queries/OsQueryParameters.java b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/queries/OsQueryParameters.java index 4a807f3..0dab93a 100644 --- a/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/queries/OsQueryParameters.java +++ b/backend/manager/modules/common/src/main/java/org/ovirt/engine/core/common/queries/OsQueryParameters.java @@ -57,5 +57,6 @@ GetOsNames, GetOsArchitectures, GetDefaultOSes + // TODO jakub add 'GetOsIcons' } } diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/CommonApplicationConstants.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/CommonApplicationConstants.java index 6ede749..7123c41 100644 --- a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/CommonApplicationConstants.java +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/CommonApplicationConstants.java @@ -748,6 +748,27 @@ @DefaultStringValue("Clone Name") String clonedVmName(); + @DefaultStringValue("Icon") + String iconTabVmPopup(); + + @DefaultStringValue("New Icon") + String newIconVmPopup(); + + @DefaultStringValue("Current Icon") + String currentIconVmPopup(); + + @DefaultStringValue("Upload") + String uploadIconVmPopup(); + + @DefaultStringValue("Use default") + String useDefaultIconVmPopup(); + + @DefaultStringValue("Discard changes") + String discardChangesIconVmPopup(); + + @DefaultStringValue("Icons limitations: max dimensions: width 150px, heigth 120px; max size 24kB; supported formats: jpg, png, gif") + String iconLimitationsIconVmPopup(); + // Permissions @DefaultStringValue("Inherited Permission") String inheritedPermission(); diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/CommonApplicationTemplates.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/CommonApplicationTemplates.java index fb94750..f054bcd 100644 --- a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/CommonApplicationTemplates.java +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/CommonApplicationTemplates.java @@ -47,9 +47,15 @@ @Template("<ul style='margin-top:0'>{0}</ul>") SafeHtml unsignedList(SafeHtml list); + @Template("<ul>{0}</ul>") + SafeHtml unorderedList(SafeHtml items); + @Template("<li>{0}</li>") SafeHtml listItem(SafeHtml item); + @Template("<li>{0}</li>") + SafeHtml listItem(String item); + @Template("{0} <sub>{1}</sub>") SafeHtml sub(String main, String sub); diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/gin/CommonGinUiBinderWidgets.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/gin/CommonGinUiBinderWidgets.java index 0d0c7ed..19f68cb 100644 --- a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/gin/CommonGinUiBinderWidgets.java +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/gin/CommonGinUiBinderWidgets.java @@ -1,25 +1,29 @@ package org.ovirt.engine.ui.common.gin; import com.google.gwt.inject.client.Ginjector; +import org.ovirt.engine.ui.common.widget.editor.IconEditorWidget; import org.ovirt.engine.ui.common.widget.uicommon.popup.vm.SerialNumberPolicyWidget; /** * Ginjector extension containing views that make use of the GWTP GinUiBinder. * * Every view that makes use of GIN dependency injection and is to be embedded in a .ui.xml file --ex: + * <pre> * <code> - * <w:MyWidget /> + * <w:MyWidget /> * * public class MyWidget { - * @Inject + * {@literal @}Inject * public MyWidget(ApplicationResources resources, MyOtherDependency dep) { * // ... * } * } * </code> + * </pre> * * Must be registered in this interface. */ public interface CommonGinUiBinderWidgets extends Ginjector { SerialNumberPolicyWidget getSerialNumberPolicyWidget(); + IconEditorWidget getIconEditorWidget(); } diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/HasAccess.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/HasAccess.java index df4eb9b..71512b7 100644 --- a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/HasAccess.java +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/HasAccess.java @@ -2,6 +2,10 @@ /** * Widgets that implement this interface have a user access policy associated with them. + * <p> + * Inaccessible widgets should be hidden. Usually using + * {@link com.google.gwt.user.client.ui.Widget#setVisible(boolean)} + * </p> */ public interface HasAccess { diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/editor/CompositeHandlerRegistration.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/editor/CompositeHandlerRegistration.java new file mode 100644 index 0000000..fcc8b6d --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/editor/CompositeHandlerRegistration.java @@ -0,0 +1,26 @@ +package org.ovirt.engine.ui.common.widget.editor; + +import com.google.gwt.event.shared.HandlerRegistration; + +import java.util.Arrays; +import java.util.List; + +public class CompositeHandlerRegistration implements HandlerRegistration { + + private final List<HandlerRegistration> registrations; + + private CompositeHandlerRegistration(List<HandlerRegistration> registrations) { + this.registrations = registrations; + } + + public static CompositeHandlerRegistration of(HandlerRegistration... registrations) { + return new CompositeHandlerRegistration(Arrays.asList(registrations)); + } + + @Override + public void removeHandler() { + for(HandlerRegistration registration : registrations) { + registration.removeHandler(); + } + } +} diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/editor/IconEditorWidget.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/editor/IconEditorWidget.java new file mode 100644 index 0000000..04dd68f --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/editor/IconEditorWidget.java @@ -0,0 +1,349 @@ +package org.ovirt.engine.ui.common.widget.editor; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Element; +import com.google.gwt.editor.client.LeafValueEditor; +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +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.KeyPressEvent; +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.HTML; +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; +import com.google.inject.Inject; +import org.ovirt.engine.ui.common.CommonApplicationConstants; +import org.ovirt.engine.ui.common.CommonApplicationResources; +import org.ovirt.engine.ui.common.CommonApplicationTemplates; +import org.ovirt.engine.ui.common.editor.UiCommonEditor; +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.IconWithDefault; +import org.ovirt.engine.ui.uicommonweb.validation.IconValidation; +import org.ovirt.engine.ui.uicommonweb.validation.ValidationResult; + +import java.util.List; + +/** + * Icon editor. It allows to set custom icon to VM-like entities. + */ +public class IconEditorWidget extends AbstractValidatedWidget + implements LeafValueEditor<IconWithDefault>, + HasValueChangeHandlers<IconWithDefault>, + UiCommonEditor<IconWithDefault> { + + interface ViewUiBinder extends UiBinder<HTMLPanel, IconEditorWidget> { + ViewUiBinder uiBinder = GWT.create(ViewUiBinder.class); + } + + protected interface Style extends CssResource { + String grey(); + String iconImageDisabled(); + } + + @UiField + protected Style style; + + @UiField + protected Image image; + + @UiField + protected Button uploadButton; + + @UiField(provided = true) + protected InfoIcon uploadInfoIcon; + + @UiField + protected FileUpload fileUpload; + + @UiField + protected Button defaultButton; + + @UiField + protected HTML errorMessageHtml; + + protected final CommonApplicationTemplates templates; + + /** + * 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; + + /** + * 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; + + @Inject + public IconEditorWidget(CommonApplicationConstants constants, + CommonApplicationTemplates templates, + CommonApplicationResources resources) { + this.templates = templates; + uploadInfoIcon = new InfoIcon( + SafeHtmlUtils.fromTrustedString(constants.iconLimitationsIconVmPopup()), resources); + initWidget(ViewUiBinder.uiBinder.createAndBindUi(this)); + fileUpload.getElement().setAttribute("accept", "image/gif,image/jpeg,image/png"); //$NON-NLS-1$ //$NON-NLS-2$ + fileUpload.addChangeHandler(new ChangeHandler() { + @Override + public void onChange(ChangeEvent event) { + IconEditorWidget.this.readUploadedIconFile(); + } + }); + fileUpload.getElement().setTabIndex(-1); // can be moved to *.ui.xml file in form of attribute `tabIndex="-1"` since GWT 2.7.0 + + KeyPressHandler preventEnterKeyPressHandler = createPreventEnterKeyPressHandler(); + uploadButton.addKeyPressHandler(preventEnterKeyPressHandler); + defaultButton.addKeyPressHandler(preventEnterKeyPressHandler); + + setEnabled(true); + setAccessible(true); + } + + private KeyPressHandler createPreventEnterKeyPressHandler() { + return new KeyPressHandler() { + @Override + public void onKeyPress(KeyPressEvent event) { + if (!event.isAnyModifierKeyDown() + && event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER + && event.getUnicodeCharCode() == 0) { + event.preventDefault(); + } + } + }; + } + + @Override + public void setValue(IconWithDefault value) { + setIcon(value.getIcon()); + defaultIcon = value.getDefaultIcon(); + } + + @Override + public IconWithDefault getValue() { + return new IconWithDefault(icon, defaultIcon); + } + + @Override + protected Widget getValidatedWidget() { + return this; + } + + @UiHandler("uploadButton") + void onUploadIconButton(ClickEvent event) { + clickElement(fileUpload.getElement()); + } + + @UiHandler("defaultButton") + void onDefaultIconButton(ClickEvent event) { + setIcon(defaultIcon); + } + + /** + * There is FileUpload#click() method since GWT 2.7.0. + */ + native void clickElement(Element element) /*-{ + element.click(); + }-*/; + + protected void setIcon(String icon) { + final String oldIcon = this.icon; + this.icon = icon; + final ValidationResult validation = (new IconValidation()).validate(icon); + updateErrorIconLabel(validation); + image.getElement().setAttribute("src", icon); //$NON-NLS-1$ + ValueChangeEvent.fireIfNotEqual(this, + new IconWithDefault(oldIcon, defaultIcon), + new IconWithDefault(icon, defaultIcon)); + } + + private void updateErrorIconLabel(ValidationResult validation) { + if (!validation.getSuccess() && validation.getReasons().isEmpty()) { + throw new IllegalArgumentException("Unsuccessful validation without any reason not allowed."); //$NON-NLS-1$ + } + updateErrorIconLabel(validation.getReasons()); + } + + private void updateErrorIconLabel(List<String> reasons) { + if (reasons.isEmpty()) { + errorMessageHtml.setHTML(SafeHtmlUtils.EMPTY_SAFE_HTML); + } else { + final SafeHtml htmlReasons = toUnorderedList(reasons); + errorMessageHtml.setHTML(htmlReasons); + } + } + + private SafeHtml toUnorderedList(List<String> stringItems) { + SafeHtmlBuilder builder = new SafeHtmlBuilder(); + for (String stringItem : stringItems) { + builder.append(templates.listItem(stringItem)); + } + return templates.unorderedList(builder.toSafeHtml()); + } + + native void readUploadedIconFile() /*-{ + var inputFileElement = th...@org.ovirt.engine.ui.common.widget.editor.IconEditorWidget::fileuplo...@com.google.gwt.user.client.ui.FileUpload::getElement()(); + var self = this; + var javaCallback = $entry(function (dataUri) { + return se...@org.ovirt.engine.ui.common.widget.editor.IconEditorWidget::setIcon(Ljava/lang/String;)(dataUri); + }); + 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); + } + }-*/; + + native void preventDefaultEnterBehavior(Element element) /*-{ + element.addEvent + }-*/; + + + // see com.google.gwt.user.client.ui.ValueListBox.addValueChangeHandler() + @Override + public HandlerRegistration addValueChangeHandler(ValueChangeHandler<IconWithDefault> handler) { + return this.addHandler(handler, ValueChangeEvent.getType()); + } + + @Override + public void markAsValid() { + super.markAsValid(); + getValidatedWidgetStyle().setBorderColor("transparent"); //$NON-NLS-1$ + } + + @Override + public void markAsInvalid(List<String> validationHints) { + super.markAsInvalid(validationHints); + updateErrorIconLabel(validationHints); + } + + @Override + public LeafValueEditor<IconWithDefault> 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(true, ""); //$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(errorMessageHtml, !enabled, style.grey()); + errorMessageHtml.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)); + } + + +} diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/editor/IconEditorWidget.ui.xml b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/editor/IconEditorWidget.ui.xml new file mode 100644 index 0000000..ec7142f --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/editor/IconEditorWidget.ui.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent"> +<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" + xmlns:g="urn:import:com.google.gwt.user.client.ui" + xmlns:d="urn:import:org.ovirt.engine.ui.common.widget.dialog"> + + <ui:with field='resources' type='org.ovirt.engine.ui.common.CommonApplicationResources'/> + <ui:with field='constants' type='org.ovirt.engine.ui.common.CommonApplicationConstants'/> + + <ui:style type="org.ovirt.engine.ui.common.widget.editor.IconEditorWidget.Style"> + + .iconImage { + display: block; + height: 120px; + width: 150px; + border: thin solid rgb(211, 211, 211); + } + + .iconImageDisabled { + opacity: 0.4; + } + + .horizontal-spacing > tbody > tr > td { + padding-left: 10px; + } + + .inline-block { + display: inline-block + } + + .iconButton { + margin: 0px 0px 10px; + min-width: 110px; + } + + .iconButton:focus { + outline: thin black dotted; + } + + .hidden { + display: none; + } + + .grey { + color: grey; + } + + .iconInfoIcon { + margin: 0px 7px; + } + + .iconErrorHtml { + position: relative; + bottom: 0px; + left: 0px; + margin: 10px; + } + + .iconErrorHtml ul { + padding-left: 13px; + } + + .no-border { + border: none; + } + + </ui:style> + + + <g:HTMLPanel addStyleNames="{style.no-border}"> + <g:HorizontalPanel addStyleNames="{style.horizontal-spacing}"> + <g:Image ui:field="image" addStyleNames="{style.iconImage}"/> + <g:VerticalPanel> + <g:FlowPanel> + <g:Button ui:field="uploadButton" text="{constants.uploadIconVmPopup}" + addStyleNames="{style.inline-block} {style.iconButton}"/> + <d:InfoIcon ui:field="uploadInfoIcon" + addStyleNames="{style.inline-block} {style.iconInfoIcon}"/> + <g:HTMLPanel addStyleNames="{style.hidden}"> + <g:FileUpload ui:field="fileUpload"/> + </g:HTMLPanel> + </g:FlowPanel> + <g:Button ui:field="defaultButton" text="{constants.useDefaultIconVmPopup}" + addStyleNames="{style.iconButton}"/> + </g:VerticalPanel> + </g:HorizontalPanel> + <g:HTML ui:field="errorMessageHtml" addStyleNames="{style.iconErrorHtml}"/> + </g:HTMLPanel> + +</ui:UiBinder> diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/AbstractVmPopupWidget.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/AbstractVmPopupWidget.java index d3b2867..16319c4 100644 --- a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/AbstractVmPopupWidget.java +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/AbstractVmPopupWidget.java @@ -27,6 +27,7 @@ import com.google.gwt.user.client.ui.RadioButton; import com.google.gwt.user.client.ui.ValueLabel; import com.google.gwt.user.client.ui.Widget; + import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -71,6 +72,7 @@ import org.ovirt.engine.ui.common.widget.dialog.tab.DialogTab; import org.ovirt.engine.ui.common.widget.dialog.tab.DialogTabPanel; import org.ovirt.engine.ui.common.widget.editor.EntityModelCellTable; +import org.ovirt.engine.ui.common.widget.editor.IconEditorWidget; import org.ovirt.engine.ui.common.widget.editor.ListModelListBoxEditor; import org.ovirt.engine.ui.common.widget.editor.ListModelListBoxOnlyEditor; import org.ovirt.engine.ui.common.widget.editor.ListModelTypeAheadChangeableListBoxEditor; @@ -864,6 +866,15 @@ @Ignore protected DialogTabPanel mainTabPanel; + // ==Icon Tab== + @UiField + @Ignore + protected DialogTab iconTab; + + @UiField + @Path("icon.entity") + protected IconEditorWidget iconEditorWidget; + private UnitVmModel unitVmModel; private final Driver driver = GWT.create(Driver.class); @@ -993,6 +1004,7 @@ getTabNameMapping().put(TabName.POOL_TAB, this.poolTab); getTabNameMapping().put(TabName.RESOURCE_ALLOCATION_TAB, this.resourceAllocationTab); getTabNameMapping().put(TabName.SYSTEM_TAB, this.systemTab); + getTabNameMapping().put(TabName.ICON_TAB, this.iconTab); } private void initDetachableFields() { @@ -1501,6 +1513,9 @@ numOfSocketsEditorWithDetachable.setLabel(constants.numOfSockets()); emulatedMachine.setLabel(constants.emulatedMachineLabel()); customCpu.setLabel(constants.cpuModelLabel()); + + // Icon tab + iconTab.setLabel(constants.iconTabVmPopup()); } protected void applyStyles() { @@ -2004,6 +2019,10 @@ // ==Custom Properties Tab== nextTabIndex = customPropertiesTab.setTabIndexes(nextTabIndex); + // ==Icon Tab== + nextTabIndex = iconTab.setTabIndexes(nextTabIndex); + iconEditorWidget.setTabIndex(nextTabIndex++); + return nextTabIndex; } @@ -2063,7 +2082,8 @@ rngDeviceTab, highAvailabilityTab, poolTab, - systemTab); + systemTab, + iconTab); } protected List<Widget> adancedFieldsFromGeneralTab() { @@ -2119,7 +2139,8 @@ highAvailabilityTab, resourceAllocationTab, customPropertiesTab, - rngDeviceTab + rngDeviceTab, + iconTab ); } @@ -2151,7 +2172,8 @@ bootOptionsTab, customPropertiesTab, systemTab, - rngDeviceTab + rngDeviceTab, + iconTab ); } diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/AbstractVmPopupWidget.ui.xml b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/AbstractVmPopupWidget.ui.xml index c6c8901..ae89f1f 100644 --- a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/AbstractVmPopupWidget.ui.xml +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/uicommon/popup/AbstractVmPopupWidget.ui.xml @@ -426,6 +426,42 @@ .overrideMigrationDowntimeContentEditor { width: 20px; } + + .iconImage { + height: 120px; + width: 150px; + border: thin solid rgb(211, 211, 211); + display: block; + } + + .horizontal-spacing > tbody > tr > td { + margin-left: 10px; + } + + .inline-block { + display: inline-block + } + + .iconButton { + margin: 0px 0px 5px; + min-width: 110px; + } + + .hidden { + display: none; + } + + .iconInfoIcon { + margin: 0px 7px; + } + + .iconErrorLabel { + position: relative; + bottom: 0px; + left: 0px; + margin: 10px; + } + </ui:style> <t:DialogTabPanel width="100%" height="100%" ui:field="mainTabPanel"> @@ -784,6 +820,13 @@ </t:content> </t:DialogTab> </t:tab> + <t:tab> + <t:DialogTab ui:field="iconTab"> + <t:content> + <e:IconEditorWidget ui:field="iconEditorWidget" /> + </t:content> + </t:DialogTab> + </t:tab> </t:DialogTabPanel> </ui:UiBinder> diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/Linq.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/Linq.java index f330cda..30a8eef 100644 --- a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/Linq.java +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/Linq.java @@ -770,6 +770,10 @@ return result; } + public static <T> List<T> concatTypesafe(List<T>... lists) { + return concat(lists); + } + public static <T> ArrayList<T> union(ArrayList<ArrayList<T>> lists) { HashSet<T> set = new HashSet<T>(); diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/TabName.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/TabName.java index ccbd4d9..02e81b1 100644 --- a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/TabName.java +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/TabName.java @@ -24,6 +24,7 @@ RESOURCE_ALLOCATION_TAB, SYSTEM_TAB, TAB_RNG, - FIRST_RUN; + FIRST_RUN, + ICON_TAB; } diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/IconWithDefault.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/IconWithDefault.java new file mode 100644 index 0000000..fcb9731 --- /dev/null +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/IconWithDefault.java @@ -0,0 +1,66 @@ +package org.ovirt.engine.ui.uicommonweb.models.vms; + +/** + * Bean holding two icons in dataUri format. + * + * <p> + * {@link #icon} carries data in both directions between {@link UnitVmModel} and + * {@link IconEditor} + * </p> + * <p> + * {@link #defaultIcon} is used to one-direction communication {@link UnitVmModel} -> {@link IconEditor} + * </p> + */ +public class IconWithDefault { + + /** + * Icon - this value changes over time during model-view communication + */ + private final String icon; + + /** + * Default (VM OS based) icon. It allows user to reset custom icon settings. + */ + private final String defaultIcon; + + public IconWithDefault(String icon, String defaultIcon) { + this.icon = icon; + this.defaultIcon = defaultIcon; + } + + public String getIcon() { + return icon; + } + + public String getDefaultIcon() { + return defaultIcon; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof IconWithDefault)) return false; + + IconWithDefault that = (IconWithDefault) o; + + if (defaultIcon != null ? !defaultIcon.equals(that.defaultIcon) : that.defaultIcon != null) return false; + if (icon != null ? !icon.equals(that.icon) : that.icon != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = icon != null ? icon.hashCode() : 0; + result = 31 * result + (defaultIcon != null ? defaultIcon.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "IconWithDefault{" + //$NON-NLS-1$ + "icon='" + icon.substring(0, 30) + "…'" + //$NON-NLS-1$ //$NON-NLS-2$ + ", defaultIcon='" + defaultIcon.substring(0, 30) + "'" + //$NON-NLS-1$ //$NON-NLS-2$ + '}'; + } +} diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/UnitVmModel.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/UnitVmModel.java index d1cef78..360558f 100644 --- a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/UnitVmModel.java +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/models/vms/UnitVmModel.java @@ -67,6 +67,7 @@ import org.ovirt.engine.ui.uicommonweb.validation.I18NExtraNameOrNoneValidation; import org.ovirt.engine.ui.uicommonweb.validation.I18NNameValidation; import org.ovirt.engine.ui.uicommonweb.validation.IValidation; +import org.ovirt.engine.ui.uicommonweb.validation.IconWithDefaultValidation; import org.ovirt.engine.ui.uicommonweb.validation.IntegerValidation; import org.ovirt.engine.ui.uicommonweb.validation.LengthValidation; import org.ovirt.engine.ui.uicommonweb.validation.NoTrimmingWhitespacesValidation; @@ -200,6 +201,20 @@ this.nicsWithLogicalNetworks = nicsWithLogicalNetworks; } + //TODO jakub add initialization, validation, saving + /** + * VM icon + */ + private EntityModel<IconWithDefault> icon; + + public EntityModel<IconWithDefault> getIcon() { + return icon; + } + + public void setIcon(EntityModel<IconWithDefault> icon) { + this.icon = icon; + } + /** * Note: We assume that this method is called only once, on the creation stage of the model. if this assumption is * changed (i.e the VM can attached/detached from a pool after the model is created), this method should be modified @@ -284,6 +299,9 @@ // ==Custom Properties Tab== getCustomProperties().setIsChangable(false); getCustomPropertySheet().setIsChangable(false); + + // ==Icon Tab== + getIcon().setIsChangable(false); vmAttachedToPool = true; } @@ -1713,6 +1731,11 @@ getAutoConverge().setItems(Arrays.asList(null, true, false)); setMigrateCompressed(new NotChangableForVmInPoolListModel<Boolean>()); getMigrateCompressed().setItems(Arrays.asList(null, true, false)); + setIcon(new EntityModel<IconWithDefault>()); + // TODO jakub delete + this.getIcon().setEntity(new IconWithDefault( + "data:image/gif;base64,R0lGODdhlgB4AOf/AAUCCAIEAAAFCQMGEQQJGAIOCQUMHgYPJgwRBAkWCAcTMggbBw8cGg4cKA8dMw0dPwokDBIjEBkhEiAfEg0pBBgmChQmTRgoNRcoQhoqLxkuERssKhAyExIxHB8vGyQvFR4wIhg1CyI0DiE1GEcsFDEyGi0zIyQ2QSQ3OyA1XTQ3EiY4NSQ2UB4/Hh1BEyg9JyZAFCo+ICk9LzE/Di4/GDI9ITRBCC1FCis/ZTBDSjJEQzJFPzJDVjBDYzNDXjNKHy5NKytQIj9JLS9QGj1LIzdMLENKITpOGzZMPTNRFThNNj5PFUhMFjBKgDtSJ0RNP1BNJzxNaD1OYk5OMz9QUEBRSztPcVhRGkJSWkJUbzteKURVaUVcL1ZZHExaMEhcKEFfIEVaSFBXSThjG0tdIEhcPERhF1ZdNUZmE2FZTEtccKFKLkZdf0pfelVgW1RhYlFha0JvIjRjxmRmKVJkdFNqOFFkeltqI05tLFhqLVVnVURmm2JnPlFvJV1oSEtvOVVvHkFnq0dyL0d0H2JsHzlrzUFsv1RsintoP3BqUFhsg3dsLlxufoRtHl9veGZxbGVxcVV+InF2H1R+KkRy1Ft7OFN+OXN1OVR/Ml98MWh5N3ByZW15K2N6R2F+LHN2TGt4anF4WUN71mp6g2h6jFZ+vnZ9eXJ+fmCML16NO2aBokyD22GMRWWMO2uLO3OJOnaHSWuLSHSCi4ODaWSFtYGEd1WI2ZKIQIOJZI+LLo2IVoWOP3mJnX+LlHiXRZSLanKaPnKZSK6LJLWGRISNj4WWOIGWRIiNiW2cSF2S4WOS1HacVoaXWZWTiJqTepeVgXmlUKicMYSkUY2iTZaam5mhT4CpS56hRW+k5oSuSa2hU3mn4auhj6Wil42nzbGji4Op25yvXJ6ubrKmhZ2ov7GsdsSodrazQqqsr! buyn7izqMWwosGymt+vZbK4wca+daS+476+u83DtNTErLzL4tXPq97Om8bR3+TNsN3Uw+TUu9Pa5e3m0PTnsvDnxPn35iwAAAAAlgB4AAAI/gDvCRw4cJ/BgwgRFtx3jx48cODgwduGLZnFZMqUXbyYUdk2cN68SQwJblvHjslsqVypcpXLlzBFyZwpKibNm5RyisrJk2chSoWCCh0qVI7Ro0iTEiSYsKlCgvQcQoTnDSK2jLSy0srIseO2jxDBXcVocWVKlithqn15U+1Nmjp39pybk6jdpHiRLmXqNCHUqBLDhvXGixSpQ4i3dj2J7aPJlBfNop1say3bt6veapbJUy4lz0DtFs2bV+DBe+5SL2TotCHgwCXBelNFihHiQ4rYqNKIESXGxiU1qixbljJLyzFdbsYZtydon6ILyZFOWqnBe/vIkUKX2iG9fVHp/mFn6FriQ6oQv2IDR5tUbja425DiRcui74zrwXHdeNY4ZeRrLdfZTpzNVYghdRVlCHVDVWfUafcURkwv7kwEj1QXhvcQWF9xiE1jtDHiXhtW0MEIHbIQw4t+/ClDizcm+QaZf/4BaNNyns2kkyGBZIVgUEBNF5SQRJGG3UDzySILOTA69pF5DlH04ZTqgZgiMbKMskUbjnRJjCnq9MKifYfI4s42GIXEW3E00gggjgXKNZMhe/iATjPk/BhadKPhdaFrhQW6opMdplfllIjSMkoz3XTTzDGjqAGHI4yccow83LgDjn202AGJOpsq40g85KzZX5v/IQenTKXQguBM/ntIwcMxv8RTSlA8GrLggqKRNpVEhhnWiyy8VNXYoYgmWxEtbByzzjrqxLOJOo6YSMcopuTDDjqbYsQGHfGgUyotkMiDjje8KTMcqpNZVlNlq1RGky0y7bEdLaIYIkogVkghBSTPyOMNj02w4c0euvJq1JB4BRXbR7QNS80xsnBX0rHHdogousocYkc3+OCzTjzczEONG! ic6Qow63DiyGy2HtMFIN7LIQ84hdMBRSzzoEncqu2vB61JaNGVWCBuQdENOKfk20YOssmwCiauB+ODDKG3sEYh0BzbodSGHqBIWbdR0s043l+pzZsYdHkqOO7zEDAc3Ic+jzjzzdKOG/h05U0NNL2q0wYYVWaB8DJj5OKKGGpDkk4+mtmBUGbvHwTR5KaqgozS9NwXSRjObsDPwHv3yIMUWp0jRhCh78GC662xsbRevRB2GGG2ymA1tyf7kQ4/GGn8IDjnqrCPPKXY4QrfIYOqDzhZqXEuMI20IboUVbGxBBzHdcKNPN1LYwQg6+uiTjzeRR0a50EOvUgobjLAzDy+lZBZvTXts0YwpiZCzBw4+iIKsfAAHNTSBTjyIQhSsJgUrBOKBuuKaaG6DGJXRbR7rSMc8/MEPfZDDUBnzRjzkIbJ0xMMOo1hHyNTxBmqYC3p24AU16GAHNkQhC1FoQ/RWxg155OMN/nAgBffmwY/zbSR9qBoa+0rRBik8qxf1s0Up6MUvRzQDEjnghRV6EMAF+kAKBuRRFHpwPTL2AAdR4IED+QQ2xNhBEbxARzrwIQ91pIMfHTQfL4olFuDwooN2m9AhRqGOkHEDEmWTRRbagBtG2EFwasgC4VBWNm7kAx3HeIOlLoXHffTsiOuDly32ULhnEaMJgSBdIAyBAyno7A1SoEMUcMBFH3AxfFYwhNNwYIVDDK4HPTDdFgJBJEMsLCi+jBkjZKGODrJjHfPQRx75EY9RyEJsX6FFL/iBj3z0wnaE7CA3qHGKUzBikbghxSPZsEjC2UEW8ajFPNLxiH1Q4RTE/jhGNP3hD0/a52c0qkm8pMhOLHDjG9SQAg5o2YQ6bWFScHBlFFKQAmA+jQckCsQuyYg9L/IAC3tgoyqqpwhGENJxGMSbNPXRu1PIohdiYw81OpgPWdRGZujoYC265AhFVA83hckCG0hkhejJgh/d0Ic8ILGPZjwCEsfQxwb9oY99hAoyAKVRKfZgQywcY39bSMFCU9CvKKgBeq7MggXESksBzpKtXMyCXBUYQCkcYldBWaWQ! YoYbJcWjGfmQB96imQ98+CMejhiFYnmhilF0w3HqOAwjSiqLfPADEont6U/5tk42DE5Sp/hHN9RxvEvV4g2mmIdh/TGOfNzDVFlF/gtX6/e+LURhC44wBSToYIWxcrEHLNhS4dRKS2BuwbYUPSMXbzjXMfJyD5RYkBxwEFJkIkYN6PCbI0jruO4SER2kkIUjbDqKU9hxhorAzbXMi1RKOYIUJE2vLz372WsFFh3EgAMdqJGPRzRDpfz4BjtsphHYUiYZpMtCEwiaheN2CQ5b6K1FLWpWO0SYosW1gnNpudDryRWHwMSBUHWl0QBqDWyepUN2R7Gd7kqziPxAh0khkaVyHqMb1KjeG5dZtnz8gxqTTa8d3oiYwbUzCiiMRz4mBAc7RJUb3GDHN1TLDvmpo1QcKcVZ+pOMUmg4Ck1olVwdHNEo9JaXWwRm/hbsoIYc4iC5CgwxLXt7PcJJMs0kakITUvDFLfDAB3uIGRtIEY9esJi068gHEeWBR3QooryjoMMpEHmMuDFSEYrIUo8TR0FMK+KRhCORD+igsnhMqBeS1kc6uJGOZkATU/JAKC32kBVVsEHL/HmfW3HQhENADw5YcAQcxnjGLVqtBzc8Ky7XKkAp2FKsIeboXNNshRRYwALB9Bey7SrUUcRDFtRARw9zKs1m/CPG8hEvbiFhaFJ4NmaK6FLZSEsNOyDG05iOmVzZMOounYIao+gFbuMBKWqYohmMKuf+SLHILChCDbfeSIKz8OwUnFVSuI2oLXlZSwUGzqx0yMKb/jVsyx5U9IbKJRzKLWrtFChwrACMmRp6gQ7H1lEd6pDm8f7xYzUoQp1RqFSWbMeGUTDi6POOFnzdaAf3PFKobTDr0RMroig4ouZS6IUpykkMOmBhFNUihcfbvIeLeNlfIn9zCixMh9MpztlnpCULeHDcLeBwcWqo9vX4bHI0ShKYXUT5m6/NcYoavnpqqFgv5BEP/kZTH7Wo2T+e! dwihRkHSSvomI9UhCxGVrRksqzcjPYbpIbdhkTc8Og1NdHmAS8ERp1AsKdoORikcxgd7g7hFvOxWkVP08trjQfK2MGGLns62tz1um4FJUR6YHNlmBrwCb1htwsuZotf2ZRZG/hFufjQjp/ykJzrk8Q91bKHybcBt7BX76TYgVrGN4l6rGRFfOGJah23PwtE/3fT0uxSMWfJedmA1aiAFPxdJgWMFGbEH0CdJI7cFQLQFuOcIY2RLdVY4WyAFCtRgW2AHW5QFFLVxcycFFgViZjZ40HZGhmdtgjNoK4NUx8APLQUHvVAu+UAHp5d+sGdTh/Fz8mAijkUxx5AOx+BTa4Zv8JV1WzJZjyQ+dpAF+mVh40VDiqBmW8AIlCIFbWAHPbBVX3ZDs4QD0VNAfyYpY7RAc1U4keRxx1WBbVZRJvdFJdcDcpVmaidWG3eHKUAi25dz3xNV/tANdFBSJ0UNMERq/rH3c4eAhfIwWYtCDKNwDJynb21QepjWYCezhabHNwhIQ9FjGG/0gdpjdFKwZlvgWSSIbLbVA5UYSRSnPbw1fRsIRpHkA1ngL87GAsDHVizgAyywUMBEfXGndnSogthnAZ8FT+XDDd3AD/PwCCZCCoRkLtVDByZiCiyWXr3wBugAB5HYDaYgC8dwDHTgMYvkaW+UBY5ADdSjiZ9mjdCTM9bYJbVhB8l3LeI1ZI90erY0OAD0aZJ0W9aoBtBnVn5Wdx5nOl4EixjGAsBlUQFkd32ndsn3ZmqHjFbwaEoWWEmVDqcQjaOQD+mQD6SAMtYYe4ahCAaHarnzKJA2iPuI/m9sBgdZomNvxDd0AD1q8GDCFixnBT3j1VP6GDMb2FsOZ48gZ41naFaL4y8ZqIoLtECno18i54vHJn0aOJHY9lBqYJGDNzgq6UNKJQ+r1nks5m13ZIiYRmruoQh0oA6C+E4IN2lZcpP1N2TRI4APh5cmEo80mElBpFiRtjjjNQolRYVnJVfANGQ49FDwyECkVkAZ6Gdx1Yt1dTp7M3cJRGxWaTV5iG08EFFxh33Vw0zRBE3z8A0t83P! SWDbm4ghNR4+ehnOUAm6QQmNt+Ui4MWT8d3S8GTiVaI2MMIbUwChvMApf4lKOgAXeaJjWWHrHRXFIVosQdTpW81Bdclyn/pOKY+SQX+QvCpRAARR3wPVFaeQDFoAD6flFf7aCFuBwxBBYSqVBdtNT0sh98yYL4nN0kxVe3XMK40UNnRdku2lvbIaX+zmPwzlkJ2JhD3UM7JAObnAKtdAMtWAKFwoJ69cl1hRpaKV8DSZsDmZLkgIJkIAFBZiBc9iLYJSTCpSBplNy3ulFtuSQ0PeLKLiHhJYOfihgrUZDJuUIx0AMoNcL+3l0hmFezTA9LyWUlYhpe/Nwe3OThgF2nWegn/aTdCBP87A/zcANFvoMz7A/pzAsJlpOAAcHjFB3QKkGPtAlpRhvjgBEP+lsCuSdYJSBhcNmauA6vcgCgGo13umZ/lajhwv1TiLZDfKgVGgDoOIDdsSQTzRTG9JoIiZlcNNjaEFWevp4oJ5mGEg6IltIQ9FJB28wR612UOzwDAeVDmgzpMRQTpbCjooFbNrzBliAW0Y3htkpgThEqAGkbIsjPoOYRrJimYAKqBiAAbroixZgo2P1Tj7EKIlKDZAYmypjKd1gmCLCU4mVIh3qSDTkMUMGim/kdFNqjSYSSaoXPdDjBnTzDetwUN8gplDmpY8ypKZgClgyKb3AU29QLS+FJcI2KcNJd7JCo7hIhdYiga7zZw+brMkqqMDkkIZnUo91OPIwr2jDrZRSTqbAjsHCoV2iJIoFm/r4qTalegMZ/jjaAwdYsjjqmjNYgAWP4AzfIK84yw0VWgvMuAlgWgu1MI77CqDH+a88hY+9YGjVEo0nQgeL47Cns67CeSJ+ZjpSoJl/5pm9OH2AynwpoFj8NaTsAJfWOqD4WF4oyQsBWC3j5VJGR0MM2iWQ8Ahz+gZv4AZ4q6akkFv8sFTx+GtYIAWmIKbj8A3OAGVCWwvO0A0VOqaLyyhDegpv0LbWWKU! meyJnpV8IKUAdmKVQa6mzpz2y8rBbW6Pb5os2ygJ6AArqkA/HIEetNqQDalKygE+x1wt+A3b6BXvH4FL8yQjARgVYQAVUkANUoANVEAZhoAd64AaY1Q3+gA/dUC1s/no6byC0zjAO4wC5m3BwFtoNz+C4+1oLo/Uo4Yi0lKKumeUIhZEzVqt8UapOeCecoJqTpes6FKuLzaa6gFoEfhAKrpoOEaoOSypeR4ec+UQM1IBzOMeONtYNpxBeYIcFOcADyhqoc0cFVbC8zvsItSBV6TA9OcmmWCC0mzALs/AMtQAKjwAKtUAM2Tu+x2Cim2C+8WC27MhTowAHk+KthfG0Hchm+Jai8SgioBo9bfewngmGspKLgCoEZVAHuBAKuCA/eXMMAFcb+BSy6EBajycPB0cM0zLBjmC8xnvBzAqoJ5ADJ4ACxru8zKsHz8CjkBi/wFa3F1q+s1AL37tT/rmFcNPjCG7wCJvgauqQXZNGY2DHww+WJcOpxPyJrjwwnKEqImtqLVD7Z3nqLzEKqFEgBELABXVQyuOgWq3WDCipWOiQw4FlWfhgPq3Grz1svDpwwRiMATxwArucxserA0gwx3oQBm5Qwz0lKVjwBo/gBoW8r6AAwy1st767pJFGyMwcu5ozPR06p46cMwMaPU+LxLRnv8Giesd1dMqGVlZDd3+GAT4QAzFQBF7gBXXAB5+AC+PAo9QgjcSQwz5URCvFUvygDtTwBjmAvMebyxfAAm28y7t80Mi7A8E8x8yrt3UKbMyZAzULBwaNt7nqCJBgCkPqtm9QBW5wTwxM/rAP5sMzm4Em9bRKXM6uo65IXBsPloHWeHxtZ5nHuqxC4AdTIARFoARcUNR1EArPoCTPEFh3VD54A0jzBApVgMa3fAJzx8sn0Ma4DKg8kAM6wMFy3LzDXAUXLAXLetZonda6DHuQQLCM8AZojAWZJ17JnKtampM8AM5Wa6mCKStVC3a868M168lgBGGWSXcswKw//QlTUAMvAARO4AVlUARl4LwCfEd4JD9kaaGPIAZPkLxpjNWi3dWJjdYX0M! bKG9ZiXQVWjQHX5trXdm0PYAEPsKwP8AAsICmJZXTA1sHEqyRKgtGDHY8pijKzp9ffCmw82ciBLdw5MJVZy9X+/oIBF3AGZ/ALuCAENRDPRdDdZTDPZRDA44BH+dANmyAGyXvQoE0FKJDVXs3LGKysaJ3YWq28zDvRzDvVJwDbso0Bt/3fAI7bD2V0W4AFVaC8VHCc1iQLNdvgG93go6h6maXg3JzM4hWwk+bRDq7RWJDVWc0DwpYDMSAEn1DiUxADSsDdRUAETvDdkv0M8zALYuDbEA3R7H3BBy3fFrCsO57VF/DjWp0DSDDk973aOeDftB3gSn7bujx7porGxPutsVrXDl6zOsCcJKuho6BJsapJCX4Ky+wGIg0JzMzMCc7hEI0CCE0FI+4H9xwKQT0CI77ilE3ZZRDeZYAEUJ4D/u3N56C9364926+9rHxe6CdwAQd9AsobzDqwA8N853rA2v+d5Ev+3w7gADzwcF7dwcz5Bm1NDI/Qwb4dx09QBVDVzM8MCoWMcEPrBptg3puQBq/ODTIOCpuwCQe+ASiQAReQASsAAiuwARsAzwEMBX4ABTXgAR4gykBQBEIAAkrw3VxQBkJQBU8QBlN9vAdtwfu944PuAAuNAYnuxqft4yewAjtQBSiAAlUgA8xbBsxLBSfgAEuuALdt7w+gAPquACyQA8urBE9Q5kLruGLg2QVfBTuwA2JwyKkuBq2rB5vAqtzwDCpcx88QChX/DJvQ2U8Q7CuwAijQ679uAsIO/s8/bQRuLgQysOwx0OwvsAEgUARO4ARFMAVFUNRFgARejQI8cNa17QDLytDsfgEOEPLlzgKnnQGHjgISve46wAASfedhAO85QO+Vru/5vu8PAO7+/gRi4MeJsMLc4AyJkAYO3/Eo8AQU7/BiMAVmvwNPMAs5S/ZhP/G14AYRhQENkAEoIAM78PEfz/cb0PEbwAAdEAM14OZnQAQx8AIgAAIyAAIvUA! QMIPndTQTeXdTwvgOAbtvLetpubLyHjtBanQEZ4NUrIANIoO4o0Oh/P+R3Du/q7t/3vu+2f/sKAO7oDvFKEAazsL3j8AuJ8AR60Oh6kA6moPQ7oARTIAY6/qADeuAMOIvrQH4AW68AB2AABNAA22/6pn8B2z8A4g8Aj/8CU+AHRWDyJvD4lg/5L6Di6R8Dmg/vh/7fGJDV687nan7oOqDmCb8CAKGjyg4dBVFkyKBDRpWCSJSEgagnzI4TFxwowJhR48YDDhqgqMIwTKhfJXGJGYgkFDtuJxxcOFHlCQoUOnY8caOjwQGMB3wSABp0wNABBIgOBQAABIgOSz28gArVxNKoL2JAVYK1CBcuTspUsYgBQw4dJw7mMIv2AgqzKwYOXIHwhFkUOTLkoLkDyY4dMsIoeVgFBcYHGzlufHlCRxg/oUL5kVgFSZlx7GZVOdHgRIYLDRo4/vDZ8aKCA0CPnj6aVLUMqCAiPH0qFYQMDzFsx1jK+kWZIk6cAAFSZu8OmmVzHJ9Lc27ntSvKLoepg61mtih2ON/QFwlEJC4VOABN2rBhnxd2hClTxk8ZPWXQz3LmJ8zez6FJG/BoQL9p1KlV/7fKAxAYyO2FDqwCwaqrrHphqRiKgNCJIryIkIsylCBOB7x0oAKvucADrwGYNMvgoBV24OyCCzZQboUVDspgBRkW4uwlEH0aLzQdHcjgPN7+Yqyx9SDSATQDQtuvqP78+081AQR4AYgRQJBAggRt80BABWOorUHWgHiQiAkhLMO3C0+8gIccVlgORPA686yBGPna/uGztl5MDoW4YjwRBRV3Cm00HAfVMbQGNkAipAwaONEm4mosdD+jliSqyf+e7ABLEDzI9EENBLzStiJiiECCF4oYAcIxUwViKwvLkGGHKlZQETzRNIuTz4LiYkFEPVfw1aYVNtDBRedQqO+AI0ErtFkk9fPsoo58Gu2BSA0QitIBLHXyydpiOPA2JyjEjalWHwQzghFG0KCIH1Rl90EnuKrDCy/OzMBNX5krCyEV//3IxYP05EvgDWTU4QJnm9VPP5+Od! BbiQK8NatLTuMVYAN0W8CDVH5z4QcwXPNAATA5+4GLUByFMFd5TfZPQXi/q8EOJRT8TMa4LEvo3RDhL/iTWxYOve/FgGdhktlBbj2wYYoeVfXrhhyXN9mKML/X2KgkWiJIGVGN417YWOhjBtzApFOIDVSV09+OYubC3jjpgfcKzEznjuaLP/vRs5xX22iBoYyGFGj/wnpZYR6cRlxrqio3ir9KMA6BcAMo9KMIDCRI4lQYibKMhhhFEf80J0YUAHcIfPuYCZQlR5uKLOmKW2486jnYxzwwqAjjOnJVAwtjr/G2AaWh3MhLxppdnvvmGH7f6agAsp7z6ADwQIoECFhidCCJoGL2GGDQQ4jVUIxBihA9imJcr2bmooysn6tACjz+cIMKLrrjIY+4NSpSOn3gHp77ZDTDAewES/jZAAM8YL2rKKpzzCEC1oEgKW1Rb0tWoZz0OLqUABZBA97zwPfW9QG0SwFypahABEMTgC2DQQv3AgIf6faF+dcBDDuXGBSKgrH9KyA4IWDQXvcWpVox6QhHKAATgbYBphYOiBB83RSpSETWXstQGOVi9AjDggxHogLpoMC7bEAF7VeKSa1j4Af05QXZ1AAMY8jDHONZRh++CX/yUeCadETBXegFMDNIDvJ04z2HME4rFilIxbWWQW0960harRz0QJoABlkxABTQQAxrQ4GMLqEGVPBCBF0QgARKIAMvmB0c85OELeIgFLDQBC1gwgxn1IgJXxvWxucVACbTynWdQ/rABv2BFCUUATFwM2bQJNpORRIFeI6/ILUlukXoVIIIECoAAARRgdJsMmRMSUAMQgDBLVALBAj4wggiE4Ah/AMMXjvCFPLzCF/f0hStsqYk6yO5dLfBNJ+ZVhh38D2eMkpEMkCCDAx4zDE6U4vOseBpF9gcA26rUtrAWyWpaj3of8F4EPoCABZSBC6J7EBcgMCoEFCABCIDp9j7wAQ1owAk5bF89kYGMZSwjG! D/96TJiUYn7wWCVcpubDG7GAGLORkYOkYEQCHoBAxRveVO8YDSlySTpdbSaH0XZD/JghBFw4QhFiEAEbCqC70WgANVraW1oCoEfaAEMKeNfJmKx/tdYLAOfxliGNAS7DKLODn79K8P/EgqCvmwABA5ZqAKtyszIVQxbz9wqUqSnGq9KknrucuMd5kAEeRYhAQmIQA9JyMUHrVOtFXCXFpwwgh/QE5aVqEQsXLFXWTIjGNCAhjSE2gmkyq0IHVCC3ISg0KwowZcL3UADKajIiUbvohbd7PQ621EukAFlZCBtHo6ggbQuIAZEqEAMhPDWAEggl1xKACdpAAMgtKCT39MCM8DQh0y4ohW8tWcwejrgZbCip6yIxR/+kMNRFaFmrJGBEBkQJ2dWDZoVfVJ2NSyApGhxu9bzQh4+dgTa1oHEtklAx1BVBAkEwKVFgMGDIsC+/tDVVHTq+wEr+/tTX8TCF8FoBSuEjInfQmOnRg6sLTvBlR8ggQFCY8CEKaxVSk0vwxrWYOU+XE0VnMFzdQgd/GibzQRwjwMj8ABMOQcBDXzBCescHwRG6YEKVAACHLgpHjLxW8FKI6hCtgQrgPpTZATDF+EIx2+FWoQCBE7KQGkA5JzZH0hWGpJYntyWO2oEJpy3DpuEHwyI4IQWXiWtHljAAiBQSrD9QAMp/tQHT7uABGggCHHEg4GFuotbDNiWsTDGoIHqC8ECt6exAMKAGuDFCUeZARYTAFGeNIArTw9r192opS2taa/WgAgi+AJ4wcZDNC9gxmU2d6o5NmNS/mmTeyOFaboX4AQtJKGGfMiDNsyBCCMIAQq6sMc7wuFnYQcDGcCFhjV4ygotdMCLBWBgA6aNmolz+JEWb5K2IcntD9dgBBWobQwqMMYipBqVtEbt+CJAa/OiGgETUGcMTosAFNI6Ak5wAcpoEApNcMIevzBBCRLRj3YEvMh9NnhQEb5ToZ50QFHuYrRPY+mMcbjaVtY4x7c7OnXNdsZEGEECRofWAKSYdNpDQAfMXYISjFICLS17WhEQgBi0gANB6AoOmfGJT/Dh! F/awhzlg0QqEI1yw9/ypNIBrcJ5WImUxYMBQCiB1aV96cgXI4pU3rnWv0sALtCXDF1Q24/39/uCUo+MSzWsAARCcskpz5+IpGVD2Bdi0E0GggSYyUQlb8r4cn/gCK/o8/Gkg2vh93qmghVyJOgDhBR+seMUngHnLWZxyJqgB5Trs0QBwlPNcRq/3QmYbDXABDB/Dwxd+ELoQOKEAU/iEW9/6wQ9ykb2UyyQM/qAFDTDDE5ioBFZIhVhgBlioBDyoBFdIPMEqPuNzQERDOiCLhU7ohDKIABnwookbAPeSAA5zsdlDgPiqgwgwge/7vg+ogPX5AHPTgDKYLy34ARjQgj4gAyeoABGogyIgAliggZf6oJi6PxejHJeCgFcCM1jYPdzCLU0whkrQgiOYId0KBmvoM2OY/oYrDIdqeMAHnAZmiAV+ioG3CwAASAAa+IJPmAAAaK8Y+AAh6JjZkoFTmjsrM8Ht+gCReoFZEx0IgACQoYDacjPzcrMv0AQy4AIaULdUayn2eis1gwD4kS0yyIQcWsI5MsQRMCoozARkGD6CCwZXAMVMeAVjIEVSvMIGDAdxUEUla6U86IQFQIARYCsmGIEFWJ1TgpC587A6lKQpaLu0igA+rIAyQxkYCwHZIQJv+x4yOALfgIBE1B5GhKkQXAARQBkc+kJN0IQ82EYn8JwKWAAlWIAQAIM9kwZrQEdrAK6dQoZUcEd3BMUgE7JWaAVMwARXyIQkxINRnMAPoIE5/ugCPqgBEeACBPAAIhiCGODFD9ukj6uA8uIY7oGBIJCtI8iDOZiDGTACJ3AlEWAz8lq504KrWdOeBIAAGIgfszoCz7mB9WsXETCvakwCBMyEHyu8dASGdkwFVsAtAUwFS7CEA1wwPBCE8yODPjhAPGDDLiAEQ5ynERCCF+KCNFTDzqpKr/JADrKEP6AfGFydHHOCIzgCQJwhTbgEgMwDBNSCILA7DoCAkBzCbVK3kaOA! svoBTPwBPIOBUWGzMDQB7jmCIRiCGwCDBCyydVw6d8SEoLSEVjjAtcw5GPiBGIABMriBJIgnJ1iCQiSDJVgCODKDPtAEPjiD1EsK6+Gs/sq5Mo/aRcoRhNd8TaCUTUsYqhyiRG18BU3ILVawhLVsSz5EudNCpRkTqzrIMQ0QgfmiqxBwl9CJAXWqtR8YOQ34gTiqhFY4OCPbqVT4rZ36rwMEgyH4ASAwpUxRqyOwxhDopCXAAzLgzHL0hFeoJWPosU44AxOAve7jIMuZABPwzydIAxOYgI56zT+ITUGwBNiMg9dUQiWkxwfFBEFgSw7ogAqlUKbYJDb0pCM4QPEyK1eDgb2srUNMlQRwF+4BOTIggzjCTnc8uHrUs1aQBmRoBTBIAifQgAWgHLmLgSEIAS64AS6AAcy00SV4oXxshf5SwCqEBfuMgenbIhWA/oISSIN8+Ad20IUSqB7t6r4DhE3GBMovbdBKAMoHTQV5/IMg4AAOoAAKtdAW0AAVSK8jWNEZ0jNNCLfXscjYqYMKmBfVogHCJFJYygQw+AOejAMzQMDAcoLKHEYleMQfEIEhAIP2E0xKDU/MVNEb0DPc0itQDAZgoEJfWAYvrE25oUDi6oR6+Id/4Ic06D4TqAAPcDEToCVXaNAE1dWgzFXZBLQY+gPZXLIK7QAOeA0NuAQvoIEZuARJUFEyqARNOIL8SZkbpIEkcLPVETnTCQEYgIAQCBlcMwMzMMP++YIkGILa+4G361Ea+oGZNKsQtashgAFKTYIQhQHPtKNM/nhQehS2VtitPsADS7hHWlIy9NqaF7ieGKiGatiFXXiFiH0F3OLVXvXV5dMCF9BYF7A7C22dlVOBGfACJpCEc+gCJiCDV8gEG4yBJeACb6pXNeUADdicFXypEKy1I1iCPNCFS4ACKOiCS2CGTtCCmXMpGMCD8Gy/G6gfGBABkPlKLYCBj1OXH1hRzKyEOJiEfuVaewRKTDjTeEQFVPAFPHgpKrnBhtWGhm3YhyXT5cstBJ1NoHxHSwACYq1QDTjGPCCCKRCCEDuCXZAETpg! jwv2CTjoCLeCe1alFo71ZNSuz/KkBTjOCM/iESygHJkgz/UyAEJjXChjSGcLRrwwC/icYAhcgr5eigSUAA8KMozgAW8W0x9ldTIKN3TNVzCQwLzWLgIa9BrZ1WIglWAHkTYKdzeJ1x61M07bkABiAGy8YLTNsJVh4hTogAjLIAxUFGW/VpBF4xq1JtZUDxrSqs5mSHZlLACiwh+l7KzWMxcHE0RDATDPgggrASyhMAheAgNMqQ8wUzNel3Xd0x0nwWti83UoIghAYOZj8IGNgW21Y210gxVeQRyGLR66FUEEYgiCoLyA4ghkgrc9Jq+3NBE8oxDn6ghT+ATjVgA5gswrlwxiW4bcMRj7UALCrACjohynIzxAUgSMwqgTGVDB4SEw8AswMAbdKtRE4AsIc/uLYtF0Blt0AboUfeMb8Q90CuIOe2wW2fdhRnOBX0K18MjgzrVsJbYEW0AI9goHUnbHa0DNRlGA8Xb+21IA1XVMNmOE9pmFg/AAd1gUuQoCRC9EfoNfLjKOHpAAYOGIwEAFtOq0I+AGxdN0ZQlADtsd3JGDaxQQ8eMtZ4wAXgIEEYMYvqIRXKEBx6IQ8sCVxKIdyYAZ+DTJLAIMvTdD9m1AY4IBS2t9Ze7kK4B9NeFhYyAPTqbsWuLGamlk+3uNSOa0PmIJ+sIcAmDtq/IAfCAHB1Nh6A4Mi0AAKcKc4ogFIlgB/bOLLHALMtM2kRVDYtUdB2L8NptRR9kHUkt8K/tAEGuBQZiQDZtgKWIgFVy4HcWiF5JVNLcBjCkWCUVoAO3vGaaS1ADgDI4gAJmCG3ATiNE7jM1sX0akpZuZD8VXEU5oCe+iHCajmSBYBIk2CG8BUGvhmRk6CJAjD9lIrF2jdmQ4CF2jiSyXSMQDqMYChIYghFwiBBdgm2BvHJBABMhCBPNCrZqyDJDhAVxi+dRy0VBAEyHTLlUu1GF65FETqObiFCVABJsgD3Fqy22DrdUnojwZ! rcwvGldumEijpfvgEOaSx2lpRM4gjzLRBvb1Mp4a9MqzXdE5nPFhJGtCkel1hiizKBNYC2GyB05o/DQhREYABz8mhGwhL3MID/k8AhilUvIRbx+xcR97UYBdYZnNDLVNaALKOANAdWEvAgzoIAg4GjmNugdFZl5qiAApw6Gf06pstAUToB7z+gDoQAREAgyMA10pOWhhqwxm4ASMVAZii5iH1zJkG6hsQAamNwZYsSdemOVTSnBCEu3a6ARqggCIw5zqAgVcCz0oAA//CznRER2gAslbISdRehj9o40RMtW+7syAYgziIg9kkqrXM7fpC5htr3hCoKc0JRkVEABPQ4X7QBRqwzCHlguSkAcGc6emegYleAmakgSmYgBKQAPm9ASB2Jy1g7Fyj1yGogw+IxkmiHsvRbmoOAaM+yZliF322b/G+qUwg/jJ1XLoc2jGDc9FgoOXTdUs+DB0XuDWgFoQxQFdKLVAF04INZss09twgoIAYqIEJkICaioCXS4RQ8Ad/mAW1KSMhKIFQANJ6nV8wmIEZoNPOVAEVMAIV+GMmWAKxTE4jqBI+iJ0fIEwn+OQg1DJdFAAEoACjjgAaeI20MkNcy4MZqM7rPEzgSgX+ykd2RPWdkvKtrlcExLV1TlONhYEgx/IgAHMHx2ktUE8auMPZbvMJSIRZ4Id8EAP/NIEX8M8p4AdcCNF03q874ARCuIM7IARCONk+v4JhaIQrAIM76IIuGPQSEAIj8IIfWII7QGAbdKshDEHUBAAEwOxRZkM6/hs581NRTZDUWm6F/T5tx6wEsE31gE+w/coEPNUfPOCnM/ADL+CDT8jLNQVnjSXq3CYDPGjUI5jQrq4Ay52FTTD2j3+Gf6iHb7uBFcUDTsiFlE/5W5gDMxAtYYD5RsAD0bqES2ACz2GrFOcECk6/yWzzsss+7fJx5JwX62XsTnICMnhYfB8BLYiDfddvdQyGGQL4IzvtpIsFG+0D0fQDPziDORhNP0iEobOHT/AeIkCdB8njmvKnLwgCGGBeDahcMRADD/h4/+QHV8WFH86DOCKDLp! iDXciFXegC5+6DO8iFRoD5OSADJmCCOWBsYgQD7N3iO5jp8AxDGtAFEygA/v+cADW3RhoQhFaSnW0Mt1dIxSWggBsYRWnIhqiXhhw6x6hXRypUx7KdoYLv+jPIgyblAz5gwmrQBkTQBb4r/lB4zgWgkg9wJSLwVlj8wSnggycQAxL8+FpoVZEPy1eafCYgghlIcUnVWTKYgytQ/O9vbsZeoQoIZW8VASPgghDI5vYLSybg4c///BLgAhjcTt6cBIEFCC1JNL2qJJCZJjKxrGVjmC1bsGAPrVGsKE0aNGvIMIEhQ+aOpk9nPn26xEeIkyV5QH7iU/LWLUQ1IEBAEEACkzxLjlQQM+vnr1+fxDyRYOKoiRL5/jH9h2vnl45M5sxRIYIGViZk8lxp/nTLhQgNGiBUiFEBhgsXYIZUSJCApoYINPJwIcJlyoQACCbgIrMWGTRkgpFFbGX4sOFUhjGlSiVYWkSMFCdCg5wKU6U6ISpU+LHVjx8+U4QQOXIED5gkYC4Nu3JN2BoVFCAkKIDgw5Rx49glSuMsaNBZUxKBDo2IX9N/zI7EgHFkhogunCoFoaFixowld5YsucH9xo8QC2LMofHjBtsIC2oHSKBBRB4KR/LgmmWiwAQ+R7iQEVxYMSsBsuJYRAUCyEorlvwRBAxBBKFFHZ38EUtEwAQTixNuubUAEXlM8cQUU9SggRNcqAYGGK+0M4w27yBSQU0BQAHFBBNM8c03/uwI54xu5ZQzji6h4DLkJ/zw448/Ts3wBRHWqXDFHETAAIMIXszARBdMwFjBCCPQENcIizABww1aVGBTAGkuQIEKTIgAg0dMGFHDLOyM1AUyjemZoCWCCGIJoIEKiiAmrSwTyx9cgFZGgzBo0BYCCUSqIWk1HPWCBxW8eUMSecCySz/+2POLHhHEuIg2KpRQQhrfmPOqOf4Y+Y8uLX0iZCL+OGOPPf38ogIufgjxQZtzjEgGdljO0QUNs0! laQAEgfNDIIjQckQQZTKhwZgATqKCCJHPMkMccklyiySX1BHWJYHqmEgyAgfqZ2hBBDAEGHvL6+Ucn/f4hiBZDDPED/g20uYUAAhEgIMELZn0ghHk3gJHHLrfUY887QRpBQwkmNBLNIh2XgIg57bRjTj1NiQQFSZ8goi6v/YwDBY+JTEHDF3ec8cMS2XmUByd3gHEElWfWwEQui1RAgwjEMqFtCY00AoUkucxxyS3a3ALLJbpcolVjiBlWYCtkW4IH2multXYLa7vtgr1a/BBBAhF4sIAEU5QgRBkVEPfmvbCUU4095kQTDSGXzDHFGajeAsUUJJs8DDuyMpXIjKC1VA8/9aRsziLj6FMPLiJkRwgssHDCiSZ55KHJHWaYgUe2Uy2yCI01rIowsVELc0UXiyiujTZdeKqJJ0swYZgrzR8W/szYhRWG2NlDpBUC9hxwQAH33dO0Ht4J2GjCFHq8UENpWqBdjj8Xa3P4LnfMUULjWs+4yDDm9GOPkUk6ZUQJQsSHK9TDH/vrx6tiZg8fleMdiuPEuZawi2rsYheAQAMG79AFQmhLBUZQQR2IgIAKfHARuQjXGc7ABz5AgQbjygMeaLC8VmSihq9oHg5z2Dxf+MIVfTADJlyBIFZYwnoUCEH3uPe9BYQvTQEogAn0IAQ+lIMMldAELNLBFHtoIxeHe8cnoDAHmNyCKu8oHCLeMQdOEM5XUChBIsZRDj6QwQi74MTt8niLd7xDHNWoRuvwkIcvkEETuyCEJK5xw1iQ/qsLu5gDE2agApUkoAJnUIENRGCEOXlAAhr6QAVCgYAadKEVzXvFKzLhCVTeEIeeeGUmmheLWVaiD33ABCZYMUs8aCEILkgiTSDwgQ9IIFILE0Mi6lGOTzCja1rURzkIkQthROMW5WDGLiQhCUKQgRBa+wUJEOEFMOyiHK/ShY+6sIQgzE6VmSDX6yqRiUm0AhjAUIwrytYKTNTBC1i6RFa0lQsIeoEISyDD0oZZAwHopZJOLMACELCJCZRghs2r4SpfqdFXeEKVGgVEDWvovBrioRKVsIRJe! RmEH8BgCF9wQpOo9IEafIsIRPgAFPjwC3+U4wqXkMThrrELX7zi/g5MmA8hbrELMjhhBj/AAxZhAotPzOEMTrgBJwhhwTis5A41pGcrUoEKxIi1FejigzgKQjubyu8DImASDWKwANtMAAB6kYAEAmDXAuQ1ESQgwRUKBL1TZqIPsAxpYW3ZB7ThYbGpOUJUTPqHSqCGsVr4Q0lj6YpYUPYIqqrRXqagiyxVzYvR2IUxODEDGtjgDpyY4DSIWsPVvWJ1mtBEJchwUCN44Qj4siUuMTEJXB7GFb4whjHEIY5wGKMVqDiuMaZhjGIgFxaty4MXdGeCGtWoAE7Ui038CgUVSKMi9ryoRztaQ8WmDUWmmZIIknhE9EDoB7OhCQdaWlIudGwC/nsBbQn82YVcEDga10BuHmhwUE1wIrrT+COEKVjBO+4ilZStQx1eB4vohiMcyv3wh6thDFRCF7nVuAaKUxzhP054F9b9QkFjoDsJJMIEOgjARSpiDchArxWTmERI2YsaFKlmK3kgww2oFBYlQmABS2Sik1UFXgkQEynW6UI0THuODh/hB0cggyqRO+I7AO0SLW6xmFGJ3Ad3uM0dlq6YTSzhM1cwwnSu8CtQy8pU1qF1TqiBLwRbXvNCD5eJVawZ8IUvoLEuD1IJJqSd/L0E6AWAHxhBBCTggQ+UQALfAqppb8EELkDWI314RTGuIeJK/MF1XrVhno0hjWnQuta2/j6uradRjAdDmNcrZjGa74xnV6DSE8ROZSbs+a7j2hMY0zBvKlyRCUz0IQ627Mii70CI1W1FE18IAaSbLKkEGOUoJfCA3Y6yaZo+yYQnBFkFFhABMoGhD4BwhUZdKbZ8hvUwYy0bDwPu4FnPWroG56ExShznhTM8zaw89ivdZZhmX2TQO46IK4ar2GwDwhN9uINGNcEHJUA5AiCQAQgkMFdyI6AE432jCqCAiGUl7RagFgETNQCD13kCE2MYxCB+/! ONUTEKsqDg60YUeCaEzfRJLX3rTEZPPHQa86lYf8cP33MMc4lJPR0eFPYMRaIr4whO+mIY99xkHFL3uDtrG/lqLyqGLM3iBC70MAQeaDGUPkO+DM5I5IhxJCOFxIpGa+EF2bmCDPPRhDGOIQxyCHvSif73yqIjE052Oecw3XfJAh/wgMIgGoE8erP4+OjCM20Me6jCHah+DINwVNoG/8hXTtZC0Bfm6S/iIeHKHRUgn8Uo8OAEGISD1TgqqgmJ6ekZtkk7hd5ELTwACEHfQ7RJQ5PjHk/7Hlv/65jE/CPFHwvNAnwTp0096yD++/ZEXrunFVtzmeeIwlxlCEpIQe8W0AuHIfWWDFQN1GUPq6IIupM5tcdRw/Rj8Ud8Q0IQILMG2ScJ23IAIbIkKDBMmldYJEYL1kdkSnAgYmIHjgkWe5E3C9x1d+KUf+g0C6Klf+kGeDMbB9tVgDZ4f00Fd0xUdRziIC8SBWDnbNVCXR3HUKzyY3B2g62hCY3nC5aEC0+GSIPzS6SCSNgVNalDJdRjBVFzDCW3TR7jd9YXgCNagCUJdJKTg5bVgDL7g+s0gHLKfDdqgCY5f+X0e0LVfQAAAOw==", //$NON-NLS-1$ + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAAB4CAYAAAAQTwsQAAADNklEQVR4nO3cTUpbYQCF4aPioBZ0AdoN9G8FFgSdNNQOFe0KbEe2VCcKWY9TR8VRodz7xWZP6SBKLdWg1yTnevI+cCeOzHdeot7ESAAAAAAAAAAAwKBIg9TLfbYzzT0+YYVyj09YodzjE1Yo9/iEFco9PmGFco9PWKHc4xNWKPf4hBXKPT5hhXKPT1ih3OMTVij3+IQVyj0+YYVyj09YodzjE1Yo9/iEFco9PmGFco9PWKHc4xNWKPf4hBXKPT5hhXKPT1ih3OMTVij3+IQVyj0+YYVyj09YodzjE1Yo9/iEFco9PmGFco9PWKHc4xNWKPf4hBXKPT5hhXKPT1ih3OMTVij3+IQVyj0+YYVyj09YodzjE1Yo9/iEFco9PmGFco9PWKHc4xNWKPf4hBXKPT5hhXKPT1ih3OMTVij3+IQVyj0+YSFPV/MqWlNP71RrV0VfVOtYRaeqteH+9vDUXGhFtbZU9E21uiOuDfe3iqegryXV2r56RhoVFHHhniq9Ua3jBwRFXBjhTAuq9bFhUMSFW/S1qFqfxhAVceHK8JlqnFERF6Qx/fgjLtxQ9HaCURHXTPqh5+rpaAphEddM6enDlKIirplxoZUH3vwkLtxDrS1DVMQVbaA51fpqDIu4Iv3UC3NUxBVp+NYXd1TEFafWbguCIq44RZ9bEBNxxWn+lhjiwgie+1fEFa/NYRWduo8HTbX3R2FXPR25jwdNtfOX9+vrwH08aKp9txtuXjvu40FT7bpB+u9VtO4+HjRVtGYP6K6r0qr7eNBUO16E/v/q6VADzbmPB4/hfdvM7delNt3HgscqWm7Z/awTFS27jwXjUE39rcl3X0Ud93FgXM61NMV/prj76um7Kj1zHwfGafg5Dd6wfumV+xgwCbW2jWG9d! z98TMqZFlRpb+pRVdpTV/Puh49J6mtRlfanGNW++lp0P2xMw5kWpvJPrEUdDXimmj2/9Vr1RP5aPFLRS/fDg9O5llTUGdNN1BMVdbilgL+KljV8+eewQVCHutQmd9Rxt4HmVGlVReuqtaNaB7r+OO7hs9rx1dd2VLSuSqu8oAwAAAAAAAAAAAAAAAAAAAAAQDv9AeKA+xpCWfFnAAAAAElFTkSuQmCC")); //$NON-NLS-1$ } public void initialize(SystemTreeItemModel SystemTreeSelectedItem) @@ -2709,6 +2732,9 @@ boolean vmInitIsValid = getVmInitModel().validate(); setValidTab(TabName.FIRST_RUN, vmInitIsValid); + getIcon().validateEntity(new IValidation[]{new IconWithDefaultValidation()}); + setValidTab(TabName.ICON_TAB, getIcon().getIsValid()); + boolean isValid = hwPartValid && vmInitIsValid && allTabsValid(); getValid().setEntity(isValid); ValidationCompleteEvent.fire(getEventBus(), this); @@ -2797,6 +2823,7 @@ setValidTab(TabName.CONSOLE_TAB, true); setValidTab(TabName.INITIAL_RUN_TAB, true); setValidTab(TabName.GENERAL_TAB, true); + setValidTab(TabName.ICON_TAB, true); getValid().setEntity(true); } diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/IconValidation.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/IconValidation.java new file mode 100644 index 0000000..036265e --- /dev/null +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/IconValidation.java @@ -0,0 +1,112 @@ +package org.ovirt.engine.ui.uicommonweb.validation; + +import com.google.gwt.user.client.ui.Image; +import org.ovirt.engine.ui.uicommonweb.Linq; +import org.ovirt.engine.ui.uicompat.ConstantsManager; + +import java.util.List; + +/** + * It validates icons size, dimensions and file format. + */ +public class IconValidation implements IValidation { + + @Override + public ValidationResult validate(Object iconObject) { + if (iconObject instanceof String) { + final String iconString = (String) iconObject; + return validate(iconString); + } + throw new IllegalArgumentException( + "Illegal argument type: " + iconObject == null ? "null" : iconObject.getClass().toString()); //$NON-NLS-1$ //$NON-NLS-2$ + } + + private ValidationResult validate(String icon) { + ValidationResult typeValidation = validateType(icon); + if (!typeValidation.getSuccess()) { + return typeValidation; + } + ValidationResult dimensionsValidation = validateDimensions(icon); + ValidationResult sizeValidation = validateSize(icon); + if (dimensionsValidation.getSuccess() && sizeValidation.getSuccess()) { + return ValidationResult.ok(); + } + final List<String> reasons = Linq.concatTypesafe(dimensionsValidation.getReasons(), sizeValidation.getReasons()); + return new ValidationResult(false, reasons); + } + + /** + * Max width 150px, max height 120px + */ + private ValidationResult validateDimensions(String icon) { + final int maxWidth = 150; + final int maxHeight = 120; + final Image image = new Image(icon); + boolean valid = image.getWidth() <= maxWidth + && image.getHeight() <= maxHeight; + if (valid) { + return ValidationResult.ok(); + } + return ValidationResult.fail( + ConstantsManager.getInstance().getMessages().iconDimensionsTooLarge( + image.getWidth(), image.getWidth(), maxWidth, maxHeight)); + } + + /** + * The dataUri string has to fit in 32kB. + * Ratio base64encoded/raw data is approx. 4/3. + */ + private ValidationResult validateSize(String icon) { + final int maxEncodedSize = 32 * 1024; + final int maxRawSize = new Double(maxEncodedSize * (3d/4d) / 1024).intValue(); // just estimate for users, in kB + boolean valid = maxEncodedSize > icon.length(); + if (valid) { + return ValidationResult.ok(); + } + return ValidationResult.fail( + ConstantsManager.getInstance().getMessages().iconFileTooLarge(maxRawSize)); + } + + /** + * Magic numbers + * <pre> + * png 89 50 4e 47 0d 0a 1a 0a + * jpg ff d8 ff + * gif 'GIF87a' or 'GIF89a' + * </pre> + */ + private ValidationResult validateType(String icon) { + final String iconBase64Data = dataUriToBase64Data(icon); + final String iconRawData = atob(iconBase64Data); + final String[] magicNumbers = new String[] { + "\u0089\u0050\u004e\u0047\r\n\u001a\n", //$NON-NLS-1$ // png + "\u00ff\u00d8\u00ff", //$NON-NLS-1$ // png + "GIF87a", //$NON-NLS-1$ // gif + "GIF89a" //$NON-NLS-1$ // gif + }; + for (String magicNumber : magicNumbers) { + if (iconRawData.startsWith(magicNumber)) { + return ValidationResult.ok(); + } + } + return ValidationResult.fail(ConstantsManager.getInstance().getMessages().invalidIconFormat("png, jpg, gif")); //$NON-NLS-1$ + } + + private native String atob(String encodedString) /*-{ + return atob(encodedString); + }-*/; + + + /** + * Datauri format: + * <pre> + * data:[<MIME-type>][;charset=<encoding>][;base64],<data> + * </pre> + * @param dataUri + * @return base46 part of datauri + */ + private String dataUriToBase64Data(String dataUri) { + final int commaIndex = dataUri.indexOf(","); //$NON-NLS-1$ + return dataUri.substring(commaIndex + 1); + } +} diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/IconWithDefaultValidation.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/IconWithDefaultValidation.java new file mode 100644 index 0000000..c0e7adca --- /dev/null +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/IconWithDefaultValidation.java @@ -0,0 +1,23 @@ +package org.ovirt.engine.ui.uicommonweb.validation; + +import org.ovirt.engine.ui.uicommonweb.models.vms.IconWithDefault; + +/** + * TODO jakub add javadoc + */ +public class IconWithDefaultValidation implements IValidation { + + @Override + public ValidationResult validate(Object value) { + if (value instanceof IconWithDefault) { + final IconWithDefault iconWithDefault = (IconWithDefault) value; + return validate(iconWithDefault); + } + throw new IllegalArgumentException( + "Illegal argument type: " + value == null ? "null" : value.getClass().toString()); //$NON-NLS-1$ //$NON-NLS-2$ + } + + private ValidationResult validate(IconWithDefault iconWithDefault) { + return new IconValidation().validate(iconWithDefault.getIcon()); + } +} diff --git a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/ValidationResult.java b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/ValidationResult.java index 99ded31..fef57e7 100644 --- a/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/ValidationResult.java +++ b/frontend/webadmin/modules/uicommonweb/src/main/java/org/ovirt/engine/ui/uicommonweb/validation/ValidationResult.java @@ -33,7 +33,7 @@ public ValidationResult() { - this(true, new ArrayList<String>()); + this(true, new ArrayList<String>(0)); } public ValidationResult(boolean success, List<String> reasons) { @@ -48,4 +48,12 @@ public static ValidationResult fail(String... reasons) { return new ValidationResult(false, Arrays.asList(reasons)); } + + @Override + public String toString() { + return "ValidationResult{" + //$NON-NLS-1$ + "success=" + privateSuccess + //$NON-NLS-1$ + ", reasons=" + privateReasons + //$NON-NLS-1$ + "}"; //$NON-NLS-1$ + } } diff --git a/frontend/webadmin/modules/uicompat/src/main/java/org/ovirt/engine/ui/uicompat/UIConstants.java b/frontend/webadmin/modules/uicompat/src/main/java/org/ovirt/engine/ui/uicompat/UIConstants.java index aa836f0..c0cd5d7 100644 --- a/frontend/webadmin/modules/uicompat/src/main/java/org/ovirt/engine/ui/uicompat/UIConstants.java +++ b/frontend/webadmin/modules/uicompat/src/main/java/org/ovirt/engine/ui/uicompat/UIConstants.java @@ -944,7 +944,9 @@ String diskExistsOnAllActiveStorageDomainsMsg(); @DefaultStringValue("The Template that the VM is based on does not exist on any active Storage Domain") - String noActiveStorageDomainWithTemplateMsg();; + String noActiveStorageDomainWithTemplateMsg(); + + ; @DefaultStringValue("Field value should follow: <parameter=value;parameter=value;...>") String fieldValueShouldFollowMsg(); @@ -2500,4 +2502,4 @@ @DefaultStringValue("This field is not a valid Guid (use 0-9,A-F format: 00000000-0000-0000-0000-000000000000)") String invalidGuidMsg(); -} +} \ No newline at end of file diff --git a/frontend/webadmin/modules/uicompat/src/main/java/org/ovirt/engine/ui/uicompat/UIMessages.java b/frontend/webadmin/modules/uicompat/src/main/java/org/ovirt/engine/ui/uicompat/UIMessages.java index 64d019b..3eb59e4 100644 --- a/frontend/webadmin/modules/uicompat/src/main/java/org/ovirt/engine/ui/uicompat/UIMessages.java +++ b/frontend/webadmin/modules/uicompat/src/main/java/org/ovirt/engine/ui/uicompat/UIMessages.java @@ -1,5 +1,7 @@ package org.ovirt.engine.ui.uicompat; +import com.google.gwt.i18n.client.Messages; + import java.util.Date; import java.util.List; @@ -415,4 +417,13 @@ @DefaultMessage("Do you approve trusting self signed certificate subject {0}, SHA-1 fingerprint {1}?") String approveRootCertificateTrust(String subject, String sha1Fingerprint); + + @DefaultMessage("Icon dimensions are too large: {0}x{1}, maximum allowed: {2}x{3}") + String iconDimensionsTooLarge(int width, int height, int maxWidht, int maxHeight); + + @DefaultMessage("Icon file is too large. Maximum size is {0}kB.") + String iconFileTooLarge(int maxSize); + + @DefaultMessage("Unknown file format. Supported formats are {0}.") + String invalidIconFormat(String s); } -- To view, visit http://gerrit.ovirt.org/37985 To unsubscribe, visit http://gerrit.ovirt.org/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ibd5e8c120282e35d5940a66286d600b128c57753 Gerrit-PatchSet: 1 Gerrit-Project: ovirt-engine Gerrit-Branch: master Gerrit-Owner: Jakub Niedermertl <jnied...@redhat.com> _______________________________________________ Engine-patches mailing list Engine-patches@ovirt.org http://lists.ovirt.org/mailman/listinfo/engine-patches