View Javadoc

1   /**
2    * This file Copyright (c) 2012-2014 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.admincentral.shellapp.pulse.message;
35  
36  import static info.magnolia.ui.admincentral.shellapp.pulse.message.PulseMessagesPresenter.*;
37  
38  import info.magnolia.i18nsystem.SimpleTranslator;
39  import info.magnolia.ui.admincentral.shellapp.pulse.message.PulseMessageCategoryNavigator.CategoryChangedEvent;
40  import info.magnolia.ui.admincentral.shellapp.pulse.message.PulseMessageCategoryNavigator.MessageCategory;
41  import info.magnolia.ui.admincentral.shellapp.pulse.message.PulseMessageCategoryNavigator.MessageCategoryChangedListener;
42  import info.magnolia.ui.api.message.MessageType;
43  import info.magnolia.ui.api.shell.Shell;
44  import info.magnolia.ui.vaadin.grid.MagnoliaTreeTable;
45  import info.magnolia.ui.vaadin.icon.ErrorIcon;
46  import info.magnolia.ui.vaadin.icon.InfoIcon;
47  import info.magnolia.ui.vaadin.icon.WarningIcon;
48  import info.magnolia.ui.workbench.column.DateColumnFormatter;
49  
50  import java.util.Collection;
51  import java.util.HashSet;
52  import java.util.Set;
53  
54  import javax.inject.Inject;
55  
56  import org.apache.commons.lang.StringEscapeUtils;
57  
58  import com.vaadin.data.Container;
59  import com.vaadin.data.Container.ItemSetChangeEvent;
60  import com.vaadin.data.Item;
61  import com.vaadin.data.Property;
62  import com.vaadin.data.Property.ValueChangeEvent;
63  import com.vaadin.data.Property.ValueChangeListener;
64  import com.vaadin.event.ItemClickEvent;
65  import com.vaadin.shared.ui.label.ContentMode;
66  import com.vaadin.ui.CustomComponent;
67  import com.vaadin.ui.HasComponents;
68  import com.vaadin.ui.Label;
69  import com.vaadin.ui.Table;
70  import com.vaadin.ui.Table.GeneratedRow;
71  import com.vaadin.ui.TreeTable;
72  import com.vaadin.ui.VerticalLayout;
73  
74  /**
75   * Implementation of {@link PulseMessagesView}.
76   */
77  public final class PulseMessagesViewImpl extends CustomComponent implements PulseMessagesView {
78  
79      private static final String[] order = new String[] { NEW_PROPERTY_ID, TYPE_PROPERTY_ID, TEXT_PROPERTY_ID, SENDER_PROPERTY_ID, DATE_PROPERTY_ID };
80  
81      private final String[] headers;
82  
83      private final TreeTable messageTable = new MagnoliaTreeTable();
84  
85      private final VerticalLayout root = new VerticalLayout();
86  
87      private final PulseMessageCategoryNavigator navigator;
88  
89      private final SimpleTranslator i18n;
90  
91      private PulseMessagesView.Listener listener;
92  
93      private Label emptyPlaceHolder;
94  
95      private PulseMessagesFooter footer;
96  
97      private MessageCategory currentlySelectedCategory = MessageCategory.ALL;
98  
99      private boolean categoryFilterAlreadyApplied;
100 
101     @Inject
102     public PulseMessagesViewImpl(Shell shell, SimpleTranslator i18n) {
103         this.i18n = i18n;
104         headers = new String[] { i18n.translate("pulse.messages.new"), i18n.translate("pulse.messages.type"), i18n.translate("pulse.messages.text"), i18n.translate("pulse.messages.sender"), i18n.translate("pulse.messages.date") };
105         footer = new PulseMessagesFooter(messageTable, i18n);
106         navigator = new PulseMessageCategoryNavigator(i18n);
107         setSizeFull();
108         root.setSizeFull();
109         setCompositionRoot(root);
110         construct();
111     }
112 
113     @Override
114     public void setListener(PulseMessagesView.Listener listener) {
115         this.listener = listener;
116         this.footer.setListener(listener);
117     }
118 
119     @Override
120     public void setDataSource(Container dataSource) {
121         messageTable.setContainerDataSource(dataSource);
122         messageTable.setVisibleColumns(order);
123         messageTable.setSortContainerPropertyId(DATE_PROPERTY_ID);
124         messageTable.setSortAscending(false);
125         messageTable.setColumnHeaders(headers);
126     }
127 
128     private void construct() {
129         root.addComponent(navigator);
130         navigator.addCategoryChangeListener(new MessageCategoryChangedListener() {
131 
132             @Override
133             public void messageCategoryChanged(CategoryChangedEvent event) {
134                 final MessageCategory category = event.getCategory();
135                 currentlySelectedCategory = category;
136                 listener.filterByMessageCategory(category);
137                 categoryFilterAlreadyApplied = true;
138                 // TODO fgrilli Unselect all when switching categories or nasty side effects will happen. See MGNLUI-1447
139                 for (String id : (Set<String>) messageTable.getValue()) {
140                     messageTable.unselect(id);
141                 }
142                 if (category == MessageCategory.ALL) {
143                     navigator.showGroupByType(true);
144                 } else {
145                     navigator.showGroupByType(false);
146                 }
147                 refresh();
148             }
149         });
150 
151         constructTable();
152         root.addComponent(footer);
153 
154         emptyPlaceHolder = new Label();
155         emptyPlaceHolder.setContentMode(ContentMode.HTML);
156         emptyPlaceHolder.setValue(String.format("<span class=\"icon-pulse\"></span><div class=\"message\">%s</div>", i18n.translate("pulse.messages.empty")));
157         emptyPlaceHolder.addStyleName("emptyplaceholder");
158 
159         root.addComponent(emptyPlaceHolder);
160         setComponentVisibility(messageTable.getContainerDataSource());
161 
162     }
163 
164     private void constructTable() {
165         root.addComponent(messageTable);
166         root.setExpandRatio(messageTable, 1f);
167         messageTable.setSizeFull();
168         messageTable.addStyleName("message-table");
169         messageTable.setSelectable(true);
170         messageTable.setMultiSelect(true);
171         messageTable.addGeneratedColumn(NEW_PROPERTY_ID, newMessageColumnGenerator);
172         messageTable.setColumnWidth(NEW_PROPERTY_ID, 100);
173         messageTable.addGeneratedColumn(TYPE_PROPERTY_ID, typeColumnGenerator);
174         messageTable.setColumnWidth(TYPE_PROPERTY_ID, 50);
175         messageTable.addGeneratedColumn(TEXT_PROPERTY_ID, textColumnGenerator);
176         messageTable.setColumnWidth(TEXT_PROPERTY_ID, 450);
177         messageTable.addGeneratedColumn(DATE_PROPERTY_ID, new DateColumnFormatter(null));
178         messageTable.setColumnWidth(DATE_PROPERTY_ID, 150);
179         messageTable.setRowGenerator(groupingRowGenerator);
180 
181         navigator.addGroupingListener(groupingListener);
182 
183         messageTable.addItemClickListener(new ItemClickEvent.ItemClickListener() {
184             @Override
185             public void itemClick(ItemClickEvent event) {
186                 final String itemId = (String) event.getItemId();
187                 // clicking on the group type header does nothing.
188                 if (itemId.startsWith(GROUP_PLACEHOLDER_ITEMID)) {
189                     return;
190                 }
191                 if (event.isDoubleClick()) {
192                     listener.onMessageClicked(itemId);
193                 } else {
194                     if (messageTable.isSelected(itemId)) {
195                         messageTable.unselect(itemId);
196                     }
197                 }
198             }
199         });
200 
201         messageTable.addValueChangeListener(selectionListener);
202         messageTable.addItemSetChangeListener(new Container.ItemSetChangeListener() {
203             @Override
204             public void containerItemSetChange(ItemSetChangeEvent event) {
205                 setComponentVisibility(event.getContainer());
206             }
207         });
208     }
209 
210     private void setComponentVisibility(Container container) {
211         boolean isEmptyList = container.getItemIds().size() == 0;
212         if (isEmptyList) {
213             root.setExpandRatio(emptyPlaceHolder, 1f);
214             // Use expand ratio to hide message table.
215             // setVisible() would cause rendering issues.
216             root.setExpandRatio(messageTable, 0f);
217             root.setExpandRatio(footer, 0f);
218         } else {
219             root.setExpandRatio(emptyPlaceHolder, 0f);
220             root.setExpandRatio(messageTable, 1f);
221             root.setExpandRatio(footer, .1f);
222         }
223 
224         messageTable.setVisible(!isEmptyList);
225         footer.setVisible(!isEmptyList);
226         emptyPlaceHolder.setVisible(isEmptyList);
227     }
228 
229     private Property.ValueChangeListener selectionListener = new Property.ValueChangeListener() {
230 
231         private Set<Object> prevSelected = new HashSet<Object>();
232 
233         @Override
234         public void valueChange(ValueChangeEvent event) {
235             /*
236              * selecting/unselecting cause valueChange events and it is not
237              * preferred that an event handler generates more events.
238              */
239             messageTable.removeValueChangeListener(this);
240 
241             @SuppressWarnings("unchecked")
242             Set<Object> currSelected = new HashSet<Object>((Set<Object>) event.getProperty().getValue());
243             Set<Object> added = new HashSet<Object>(currSelected);
244             Set<Object> removed = new HashSet<Object>(prevSelected);
245 
246             added.removeAll(prevSelected);
247             removed.removeAll(currSelected);
248             // now know what has been added or removed
249 
250             prevSelected = currSelected;
251 
252             /*
253              * if group line was added/removed then select/unselect all it's
254              * children
255              */
256             selectChildren(added, true);
257             selectChildren(removed, false);
258 
259             // Item deselection will always deselect group
260             for (Object child : removed) {
261                 Object parent = listener.getParent(child);
262                 if (parent != null) {
263                     messageTable.unselect(parent);
264                     prevSelected.remove(parent);
265                 }
266             }
267 
268             /*
269              * Selecting item must check that all siblings are also selected
270              */
271             for (Object child : added) {
272                 Object parent = listener.getParent(child);
273                 if (isAllChildrenSelected(parent)) {
274                     messageTable.select(parent);
275                     prevSelected.add(parent);
276                 } else {
277                     messageTable.unselect(parent);
278                     prevSelected.remove(parent);
279                 }
280             }
281 
282             messageTable.addValueChangeListener(this);
283             footer.updateStatus();
284         }
285 
286         private boolean isAllChildrenSelected(Object parent) {
287             if (parent == null) {
288                 return false;
289             }
290 
291             Collection<?> siblings = listener.getGroup(parent);
292             boolean allSelected = true;
293 
294             if (siblings != null) {
295                 for (Object sibling : siblings) {
296                     if (!messageTable.isSelected(sibling)) {
297                         allSelected = false;
298                     }
299                 }
300             } else {
301                 return false;
302             }
303 
304             return allSelected;
305         }
306 
307         private void selectChildren(Set<Object> parents, boolean check) {
308             for (Object parent : parents) {
309                 Collection<?> group = listener.getGroup(parent);
310                 if (group != null) {
311                     for (Object child : group) {
312                         if (check) {
313                             messageTable.select(child);
314                             prevSelected.add(child);
315                         } else {
316                             messageTable.unselect(child);
317                             prevSelected.remove(child);
318                         }
319                     }
320                 }
321             }
322         }
323     };
324 
325     private ValueChangeListener groupingListener = new ValueChangeListener() {
326 
327         @Override
328         public void valueChange(ValueChangeEvent event) {
329             boolean checked = event.getProperty().getValue().equals(Boolean.TRUE);
330             doGrouping(checked);
331         }
332     };
333 
334     /*
335      * Row generator draws grouping headers if such are present in container
336      */
337     private Table.RowGenerator groupingRowGenerator = new Table.RowGenerator() {
338 
339         @Override
340         public GeneratedRow generateRow(Table table, Object itemId) {
341 
342             /*
343              * When sorting by type special items are inserted into Container to
344              * acts as a placeholder for grouping sub section. This row
345              * generator must render those special items.
346              */
347             if (itemId.toString().startsWith(GROUP_PLACEHOLDER_ITEMID)) {
348                 Item item = table.getItem(itemId);
349                 Property<MessageType> property = item.getItemProperty(TYPE_PROPERTY_ID);
350                 GeneratedRow generated = new GeneratedRow();
351 
352                 switch (property.getValue()) {
353                 case ERROR:
354                     generated.setText("", "", i18n.translate("pulse.messages.errors"));
355                     break;
356                 case WARNING:
357                     generated.setText("", "", i18n.translate("pulse.messages.warnings"));
358                     break;
359                 case INFO:
360                     generated.setText("", "", i18n.translate("pulse.messages.info"));
361                     break;
362                 case WORKITEM:
363                     generated.setText("", "", i18n.translate("pulse.messages.workitems"));
364                     break;
365                 }
366                 return generated;
367             }
368 
369             return null;
370         }
371     };
372 
373     private Table.ColumnGenerator newMessageColumnGenerator = new Table.ColumnGenerator() {
374 
375         @Override
376         public Object generateCell(Table source, Object itemId, Object columnId) {
377 
378             if (NEW_PROPERTY_ID.equals(columnId)) {
379                 final Property<Boolean> newProperty = source.getContainerProperty(itemId, columnId);
380                 final Boolean isNew = newProperty != null && newProperty.getValue();
381                 if (isNew) {
382                     final Label newMessage = new Label();
383                     newMessage.setSizeUndefined();
384                     newMessage.addStyleName("icon-tick");
385                     newMessage.addStyleName("new-message");
386                     return newMessage;
387                 }
388             }
389             return null;
390         }
391     };
392 
393     /**
394      * default visibility is for testing purposes.
395      */
396     Table.ColumnGenerator textColumnGenerator = new Table.ColumnGenerator() {
397 
398         @Override
399         public Object generateCell(Table source, Object itemId, Object columnId) {
400 
401             if (TEXT_PROPERTY_ID.equals(columnId)) {
402                 final Property<String> text = source.getContainerProperty(itemId, columnId);
403                 final Property<String> subject = source.getContainerProperty(itemId, SUBJECT_PROPERTY_ID);
404 
405                 final Label textLabel = new Label();
406                 textLabel.setSizeUndefined();
407                 textLabel.addStyleName("message-subject-text");
408                 textLabel.setContentMode(ContentMode.HTML);
409                 textLabel.setValue("<strong>" + StringEscapeUtils.escapeXml(subject.getValue()) + "</strong><div>" + StringEscapeUtils.escapeXml(text.getValue()) + "</div>");
410 
411                 return textLabel;
412 
413             }
414             return null;
415         }
416     };
417 
418     private Table.ColumnGenerator typeColumnGenerator = new Table.ColumnGenerator() {
419 
420         @Override
421         public Object generateCell(Table source, Object itemId, Object columnId) {
422 
423             if (TYPE_PROPERTY_ID.equals(columnId)) {
424                 final Property<MessageType> typeProperty = source.getContainerProperty(itemId, columnId);
425                 final MessageType messageType = typeProperty.getValue();
426 
427                 switch (messageType) {
428                 case INFO:
429                     return new InfoIcon();
430 
431                 case WARNING:
432                     return new WarningIcon();
433 
434                 case ERROR:
435                     return new ErrorIcon();
436 
437                 case WORKITEM:
438 
439                     final Label messageTypeIcon = new Label();
440                     messageTypeIcon.setSizeUndefined();
441                     messageTypeIcon.addStyleName("icon");
442                     messageTypeIcon.addStyleName("message-type");
443                     messageTypeIcon.addStyleName("icon-work-item");
444                     return messageTypeIcon;
445 
446                 }
447             }
448             return null;
449         }
450     };
451 
452     @Override
453     public HasComponents asVaadinComponent() {
454         return this;
455     }
456 
457     @Override
458     public void refresh() {
459         // skip this if we're displaying all messages or if the category category filter has just been applied (i.e. after clicking on a different tab)
460         if (currentlySelectedCategory != MessageCategory.ALL && !categoryFilterAlreadyApplied) {
461             listener.filterByMessageCategory(currentlySelectedCategory);
462         }
463         // now this can be reset to its initial value
464         categoryFilterAlreadyApplied = false;
465         footer.updateStatus();
466         messageTable.sort();
467         doGrouping(false);
468     }
469 
470     @Override
471     public void updateCategoryBadgeCount(MessageCategory category, int count) {
472         navigator.updateCategoryBadgeCount(category, count);
473     }
474 
475     private void doGrouping(boolean checked) {
476         listener.setGrouping(checked);
477 
478         if (checked) {
479             for (Object itemId : messageTable.getItemIds()) {
480                 messageTable.setCollapsed(itemId, false);
481             }
482         }
483     }
484 
485 }