View Javadoc
1   /**
2    * This file Copyright (c) 2019 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.dialog;
35  
36  import static com.vaadin.server.Sizeable.Unit.PERCENTAGE;
37  import static java.util.stream.Collectors.toMap;
38  
39  import info.magnolia.objectfactory.ComponentProvider;
40  import info.magnolia.ui.CloseHandler;
41  import info.magnolia.ui.DialogBuilder;
42  import info.magnolia.ui.api.action.AbstractActionExecutor;
43  import info.magnolia.ui.api.action.ActionDefinition;
44  import info.magnolia.ui.api.action.ActionExecutionException;
45  import info.magnolia.ui.api.availability.AvailabilityChecker;
46  import info.magnolia.ui.api.i18n.I18NAuthoringSupport;
47  import info.magnolia.ui.dialog.layout.DefaultEditorActionLayoutProducer;
48  import info.magnolia.ui.form.LocaleContext;
49  import info.magnolia.ui.framework.databinding.view.EditorView;
50  import info.magnolia.ui.framework.layout.LayoutDefinition;
51  import info.magnolia.ui.framework.layout.LayoutProducer;
52  
53  import java.util.ArrayList;
54  import java.util.Collections;
55  import java.util.HashMap;
56  import java.util.List;
57  import java.util.Locale;
58  import java.util.Map;
59  import java.util.Optional;
60  import java.util.function.Supplier;
61  
62  import com.vaadin.event.ShortcutAction;
63  import com.vaadin.event.ShortcutListener;
64  import com.vaadin.server.Responsive;
65  import com.vaadin.ui.Button;
66  import com.vaadin.ui.ComboBox;
67  import com.vaadin.ui.Component;
68  import com.vaadin.ui.Window;
69  
70  import lombok.Builder;
71  import lombok.NonNull;
72  
73  /**
74   * Wrapper for {@link info.magnolia.ui.dialog.DialogComponent}.
75   */
76  public class DialogHelper {
77  
78      private EditorView editorView;
79      private Component content;
80      private ComponentProvider componentProvider;
81      private LocaleContext localeContext;
82      private Supplier<Locale> userLanguage = Locale::getDefault;
83      private Map<String, ActionDefinition> actions;
84      private List<Object> items;
85      private String caption;
86      private LayoutDefinition layoutDefinition;
87  
88      private DialogComponent dialogComponent;
89  
90      public DialogHelper withContent(EditorView editorView) {
91          this.editorView = editorView;
92          return withContent(editorView.asVaadinComponent())
93                  .withComponentProvider(editorView.getComponentProvider());
94      }
95  
96      public DialogHelper withContent(Component content) {
97          this.content = content;
98          return this;
99      }
100 
101     public DialogHelper withComponentProvider(ComponentProvider componentProvider) {
102         this.componentProvider = componentProvider;
103         return this;
104     }
105 
106     public DialogHelper withUserLanguage(Supplier<Locale> userLanguage) {
107         this.userLanguage = userLanguage;
108         return this;
109     }
110 
111     public DialogHelper withLocaleContext(LocaleContext localeContext) {
112         this.localeContext = localeContext;
113         return this;
114     }
115 
116     public DialogHelper withActions(Map<String, ActionDefinition> actions) {
117         this.actions = actions;
118         return this;
119     }
120 
121     public DialogHelper withItems(List<Object> items) {
122         this.items = items;
123         return this;
124     }
125 
126     public DialogHelper withCaption(String caption) {
127         this.caption = caption;
128         return this;
129     }
130 
131     public DialogHelper withLayoutDefinition(LayoutDefinition layoutDefinition) {
132         this.layoutDefinition = layoutDefinition;
133         return this;
134     }
135 
136     public Component createDialogComponent() {
137         dialogComponent = getBuilder().build();
138         addKeyboardShortcuts();
139         return dialogComponent;
140     }
141 
142     public void buildAndOpen() {
143         Window window = getBuilder().buildAndOpen();
144         dialogComponent = (DialogComponent) window.getContent();
145         addKeyboardShortcuts();
146         window.addStyleNames("dialog");
147         window.setWidth(95, PERCENTAGE);
148         window.setDraggable(false);
149         window.setResizable(true);
150         Responsive.makeResponsive(window);
151         Optional.ofNullable(editorView).ifPresent(editorView -> editorView.bindInstance(CloseHandler.class, window::close));
152     }
153 
154     private Optional<LayoutProducer> getLayoutProducer() {
155         return Optional.ofNullable(layoutDefinition)
156                 .map(layoutDefinition -> componentProvider.newInstance(DefaultEditorActionLayoutProducer.class));
157     }
158 
159     private void executeAction(ActionDefinition actionDefinition, Object... args) {
160         try {
161             DialogActionExecutor.builder()
162                     .componentProvider(componentProvider)
163                     .definition(actionDefinition)
164                     .build()
165                     .execute(actionDefinition.getName(), args);
166         } catch (ActionExecutionException e) {
167             throw new RuntimeException(e);
168         }
169     }
170 
171     private void addKeyboardShortcuts() {
172         dialogComponent.addShortcutListener(new ShortcutListener("commit", ShortcutAction.KeyCode.ENTER, new int[]{}) {
173             @Override
174             public void handleAction(Object sender, Object target) {
175                 Optional.ofNullable(DialogHelper.this.actions.get("commit"))
176                         .filter(DialogHelper.this::isAvailable)
177                         .ifPresent(actionDefinition -> executeAction(actionDefinition));
178             }
179         });
180         dialogComponent.addShortcutListener(new ShortcutListener("cancel", ShortcutAction.KeyCode.ESCAPE, new int[]{}) {
181             @Override
182             public void handleAction(Object sender, Object target) {
183                 Optional.ofNullable(DialogHelper.this.actions.get("cancel")).ifPresent(actionDefinition -> executeAction(actionDefinition));
184             }
185         });
186     }
187 
188     private DialogBuilder getBuilder() {
189         Map<String, Component> actions = createActions(this.actions);
190 
191         Optional.ofNullable(this.localeContext)
192                 .ifPresent(context -> actions.put("localeSelector", createLocaleSelector()));
193 
194         return DialogBuilder.dialog()
195                 .modal()
196                 .withTitle(caption)
197                 .withContent(content)
198                 .withActions(new ArrayList<>(actions.values()))
199                 .withFooter(getLayoutProducer()
200                         .map(layoutProducer -> layoutProducer.createLayout(layoutDefinition, actions))
201                         .orElse(null)
202                 );
203     }
204 
205     private <T> Map<String, Component> createActions(Map<String, ActionDefinition> actions) {
206         return actions == null ? new HashMap<>() : actions.entrySet().stream()
207                 .collect(toMap(Map.Entry::getKey, entry -> {
208                     Component component = new Button(entry.getValue().getName(), event -> executeAction(entry.getValue()));
209                     component.addStyleNames(entry.getValue().getName());
210                     if (!isAvailable(entry.getValue())) {
211                         component.setEnabled(false);
212                     }
213                     return component;
214                 }));
215     }
216 
217     private Component createLocaleSelector() {
218 
219         final ComboBox<Locale> locales = new ComboBox<>();
220 
221         I18NAuthoringSupport i18NAuthoringSupport = componentProvider.getComponent(I18NAuthoringSupport.class);
222         List<Locale> availableLocales = i18NAuthoringSupport.getAvailableLocales();
223         if (availableLocales.isEmpty()) {
224             availableLocales = Collections.singletonList(i18NAuthoringSupport.getDefaultLocale());
225         }
226 
227         locales.setItems(availableLocales);
228         locales.setItemCaptionGenerator(locale -> {
229             // display languages in user's preferred locale, not system's
230             Locale preferredLocale = userLanguage.get();
231             String label = locale.getDisplayLanguage(preferredLocale);
232             if (!locale.getDisplayCountry(preferredLocale).isEmpty()) {
233                 label += " (" + locale.getDisplayCountry(preferredLocale) + ")";
234             }
235             return label;
236         });
237         locales.setEmptySelectionAllowed(false);
238         locales.setTextInputAllowed(false);
239         locales.setSelectedItem(localeContext.current().value().orElse(i18NAuthoringSupport.getDefaultLocale()));
240         locales.addValueChangeListener(locale -> localeContext.current().set(locale.getValue()));
241 
242         Optional.ofNullable(editorView).ifPresent(editorView ->
243                 localeContext.current().observeNullable(locale -> Optional.ofNullable(dialogComponent)
244                         .ifPresent(dialogComponent -> dialogComponent.setContent(editorView.getLayout(locale)))
245                 ));
246 
247         return locales;
248     }
249 
250     private boolean isAvailable(ActionDefinition actionDefinition) {
251         return Optional.ofNullable(componentProvider)
252                 .map(componentProvider -> componentProvider.getComponent(AvailabilityChecker.class))
253                 .map(availabilityChecker -> availabilityChecker.isAvailable(actionDefinition.getAvailability(), items))
254                 .orElse(true);
255     }
256 
257     private static class DialogActionExecutor extends AbstractActionExecutor {
258 
259         private final ActionDefinition definition;
260 
261         @Builder
262         private DialogActionExecutor(@NonNull ComponentProvider componentProvider, @NonNull ActionDefinition definition) {
263             super(componentProvider);
264             this.definition = definition;
265         }
266 
267         @Override
268         public ActionDefinition getActionDefinition(String actionName) {
269             return definition;
270         }
271     }
272 
273 }