2 * Copyright (C) 2017 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
9 package eu
.etaxonomy
.vaadin
.mvp
;
11 import java
.util
.ArrayList
;
12 import java
.util
.Arrays
;
13 import java
.util
.HashSet
;
14 import java
.util
.List
;
16 import java
.util
.Optional
;
18 import java
.util
.Stack
;
20 import org
.apache
.logging
.log4j
.LogManager
;
21 import org
.apache
.logging
.log4j
.Logger
;
22 import org
.vaadin
.spring
.events
.EventScope
;
24 import com
.vaadin
.data
.Validator
.InvalidValueException
;
25 import com
.vaadin
.data
.fieldgroup
.BeanFieldGroup
;
26 import com
.vaadin
.data
.fieldgroup
.FieldGroup
;
27 import com
.vaadin
.data
.fieldgroup
.FieldGroup
.CommitEvent
;
28 import com
.vaadin
.data
.fieldgroup
.FieldGroup
.CommitException
;
29 import com
.vaadin
.data
.fieldgroup
.FieldGroup
.CommitHandler
;
30 import com
.vaadin
.data
.fieldgroup
.FieldGroup
.FieldGroupInvalidValueException
;
31 import com
.vaadin
.server
.AbstractErrorMessage
.ContentMode
;
32 import com
.vaadin
.server
.ErrorMessage
.ErrorLevel
;
33 import com
.vaadin
.server
.FontAwesome
;
34 import com
.vaadin
.server
.UserError
;
35 import com
.vaadin
.shared
.ui
.MarginInfo
;
36 import com
.vaadin
.ui
.AbstractComponentContainer
;
37 import com
.vaadin
.ui
.AbstractField
;
38 import com
.vaadin
.ui
.AbstractLayout
;
39 import com
.vaadin
.ui
.AbstractOrderedLayout
;
40 import com
.vaadin
.ui
.Alignment
;
41 import com
.vaadin
.ui
.Button
;
42 import com
.vaadin
.ui
.Button
.ClickListener
;
43 import com
.vaadin
.ui
.CheckBox
;
44 import com
.vaadin
.ui
.Component
;
45 import com
.vaadin
.ui
.CssLayout
;
46 import com
.vaadin
.ui
.Field
;
47 import com
.vaadin
.ui
.GridLayout
;
48 import com
.vaadin
.ui
.GridLayout
.OutOfBoundsException
;
49 import com
.vaadin
.ui
.GridLayout
.OverlapsException
;
50 import com
.vaadin
.ui
.HasComponents
;
51 import com
.vaadin
.ui
.HorizontalLayout
;
52 import com
.vaadin
.ui
.Label
;
53 import com
.vaadin
.ui
.Layout
;
54 import com
.vaadin
.ui
.Layout
.MarginHandler
;
55 import com
.vaadin
.ui
.Notification
;
56 import com
.vaadin
.ui
.Notification
.Type
;
57 import com
.vaadin
.ui
.PopupDateField
;
58 import com
.vaadin
.ui
.TextField
;
59 import com
.vaadin
.ui
.UI
;
60 import com
.vaadin
.ui
.VerticalLayout
;
61 import com
.vaadin
.ui
.themes
.ValoTheme
;
63 import eu
.etaxonomy
.cdm
.persistence
.permission
.PermissionDeniedException
;
64 import eu
.etaxonomy
.cdm
.vaadin
.component
.TextFieldNFix
;
65 import eu
.etaxonomy
.cdm
.vaadin
.component
.dialog
.ContinueAlternativeCancelDialog
;
66 import eu
.etaxonomy
.cdm
.vaadin
.event
.EditorActionContext
;
67 import eu
.etaxonomy
.cdm
.vaadin
.event
.EditorActionContextFormat
;
68 import eu
.etaxonomy
.cdm
.vaadin
.event
.EditorActionContextFormatter
;
69 import eu
.etaxonomy
.cdm
.vaadin
.ui
.PopupEditorDefaultStatusMessageSource
;
70 import eu
.etaxonomy
.vaadin
.component
.NestedFieldGroup
;
71 import eu
.etaxonomy
.vaadin
.component
.SwitchableTextField
;
72 import eu
.etaxonomy
.vaadin
.event
.FieldReplaceEvent
;
73 import eu
.etaxonomy
.vaadin
.mvp
.event
.EditorDeleteEvent
;
74 import eu
.etaxonomy
.vaadin
.mvp
.event
.EditorPreSaveEvent
;
75 import eu
.etaxonomy
.vaadin
.mvp
.event
.EditorSaveEvent
;
76 import eu
.etaxonomy
.vaadin
.ui
.view
.DoneWithPopupEvent
;
77 import eu
.etaxonomy
.vaadin
.ui
.view
.DoneWithPopupEvent
.Reason
;
78 import eu
.etaxonomy
.vaadin
.util
.PropertyIdPath
;
81 * Optional with a deleteBtn button which can be enabled with {@link #withDeleteButton(boolean)}
83 * @author a.kohlbecker
86 public abstract class AbstractPopupEditor
<DTO
extends Object
, P
extends AbstractEditorPresenter
<DTO
,P
,V
>, V
extends ApplicationView
<V
,P
>>
87 extends AbstractPopupView
<V
,P
> {
89 private static final long serialVersionUID
= 5944874629527570061L;
90 private static final Logger logger
= LogManager
.getLogger();
92 private static final String READ_ONLY_MESSAGE_TEXT
= "The editor is in read-only mode. You do not have authority to edit this data.";
94 private BeanFieldGroup
<DTO
> fieldGroup
;
96 private VerticalLayout mainLayout
;
98 private Layout fieldLayout
;
100 private HorizontalLayout buttonLayout
;
102 private Button saveBtn
;
104 private Button cancelBtn
;
106 private Button deleteBtn
;
108 private CssLayout toolBar
= new CssLayout();
110 private CssLayout toolBarButtonGroup
= new CssLayout();
112 private Label contextBreadcrumbsLabel
= new Label();
114 private Label statusMessageLabel
= new Label();
116 private Set
<String
> statusMessages
= new HashSet
<>();
118 private GridLayout gridLayoutCache
;
120 private boolean isBeanLoaded
;
122 private Stack
<EditorActionContext
> context
= new Stack
<>();
124 private boolean isContextUpdated
;
126 private boolean isAdvancedMode
= false;
128 protected List
<Component
> advancedModeComponents
= new ArrayList
<>();
130 private Button advancedModeButton
;
132 private EditorFormConfigurator
<?
extends AbstractPopupEditor
<DTO
, P
,V
>> editorComponentsConfigurator
;
134 private boolean withDeleteButton
;
136 public AbstractPopupEditor(Layout layout
, Class
<DTO
> dtoType
) {
138 mainLayout
= new VerticalLayout();
139 // IMPORTANT: mainLayout must be set to full size otherwise the
140 // popup window may have problems with automatic resizing of its
142 mainLayout
.setSizeFull();
144 setCompositionRoot(mainLayout
);
146 fieldGroup
= new BeanFieldGroup
<>(dtoType
);
147 fieldGroup
.addCommitHandler(new SaveHandler());
149 toolBar
.addStyleName(ValoTheme
.WINDOW_TOP_TOOLBAR
);
150 toolBar
.setWidth(100, Unit
.PERCENTAGE
);
151 contextBreadcrumbsLabel
.setId("context-breadcrumbs");
152 contextBreadcrumbsLabel
.setWidthUndefined();
153 contextBreadcrumbsLabel
.setContentMode(com
.vaadin
.shared
.ui
.label
.ContentMode
.HTML
);
154 toolBar
.addComponent(contextBreadcrumbsLabel
);
155 toolBarButtonGroup
.addStyleName(ValoTheme
.LAYOUT_COMPONENT_GROUP
);
156 toolBarButtonGroup
.setWidthUndefined();
157 toolBar
.addComponent(toolBarButtonGroup
);
158 toolBar
.setVisible(false);
160 fieldLayout
= layout
;
161 fieldLayout
.setWidthUndefined();
162 if(fieldLayout
instanceof AbstractOrderedLayout
){
163 ((AbstractOrderedLayout
)fieldLayout
).setSpacing(true);
165 if(MarginHandler
.class.isAssignableFrom(fieldLayout
.getClass())){
166 ((MarginHandler
)fieldLayout
).setMargin(new MarginInfo(false, true, true, true));
169 buttonLayout
= new HorizontalLayout();
170 buttonLayout
.setStyleName(ValoTheme
.WINDOW_BOTTOM_TOOLBAR
);
171 buttonLayout
.setWidth(100, Unit
.PERCENTAGE
);
172 buttonLayout
.setSpacing(true);
174 saveBtn
= new Button("Save", FontAwesome
.SAVE
);
175 saveBtn
.setStyleName(ValoTheme
.BUTTON_PRIMARY
);
176 saveBtn
.addClickListener(e
-> save());
178 cancelBtn
= new Button("Cancel", FontAwesome
.REMOVE
);
179 cancelBtn
.addClickListener(e
-> cancelEditorDialog());
181 deleteBtn
= new Button("Delete", FontAwesome
.TRASH
);
182 deleteBtn
.setStyleName(ValoTheme
.BUTTON_DANGER
);
183 deleteBtn
.addClickListener(e
-> delete());
184 deleteBtn
.setVisible(false);
186 buttonLayout
.addComponents(deleteBtn
, saveBtn
, cancelBtn
);
187 // deleteBtn is initially invisible, let saveBtn take all space
188 buttonLayout
.setExpandRatio(saveBtn
, 1);
189 buttonLayout
.setComponentAlignment(deleteBtn
, Alignment
.TOP_RIGHT
);
190 buttonLayout
.setComponentAlignment(saveBtn
, Alignment
.TOP_RIGHT
);
191 buttonLayout
.setComponentAlignment(cancelBtn
, Alignment
.TOP_RIGHT
);
193 statusMessageLabel
.setSizeFull();
194 statusMessageLabel
.setContentMode(com
.vaadin
.shared
.ui
.label
.ContentMode
.HTML
);
196 HorizontalLayout statusMessageLayout
= new HorizontalLayout();
197 statusMessageLayout
.setSizeFull();
198 statusMessageLayout
.addComponent(statusMessageLabel
);
199 statusMessageLayout
.setMargin(new MarginInfo(false, true, false, true));
201 mainLayout
.addComponents(toolBar
, fieldLayout
, statusMessageLayout
, buttonLayout
);
202 mainLayout
.setComponentAlignment(statusMessageLayout
, Alignment
.BOTTOM_RIGHT
);
203 mainLayout
.setComponentAlignment(toolBar
, Alignment
.TOP_RIGHT
);
205 updateToolBarVisibility();
207 UI currentUI
= UI
.getCurrent();
208 //Note AM: why not "currentUI instanceof PopupEditorDefaultStatusMessageSource"
209 if(PopupEditorDefaultStatusMessageSource
.class.isAssignableFrom(currentUI
.getClass())){
210 String message
= ((PopupEditorDefaultStatusMessageSource
)currentUI
).defaultStatusMarkup(this.getClass());
211 addStatusMessage(message
);
215 protected VerticalLayout
getMainLayout() {
219 protected Layout
getFieldLayout() {
223 private GridLayout
gridLayout() {
224 if(gridLayoutCache
== null){
225 if(fieldLayout
instanceof GridLayout
){
226 gridLayoutCache
= (GridLayout
)fieldLayout
;
228 throw new RuntimeException("The fieldlayout of this editor is not a GridLayout");
231 return gridLayoutCache
;
235 public void setReadOnly(boolean readOnly
) {
236 super.setReadOnly(readOnly
);
238 statusMessageLabel
.setValue(READ_ONLY_MESSAGE_TEXT
);
239 statusMessageLabel
.addStyleName(ValoTheme
.LABEL_COLORED
);
241 statusMessageLabel
.setValue(null);
243 statusMessageLabel
.setVisible(readOnly
);
244 logger
.info("Set saveBtn.visible to " + !readOnly
);
245 saveBtn
.setVisible(!readOnly
);
246 updateDeleteButtonState();
247 cancelBtn
.setCaption(readOnly ?
"Close" : "Cancel");
248 recursiveReadonly(readOnly
, (AbstractComponentContainer
)getFieldLayout());
251 protected void recursiveReadonly(boolean readOnly
, AbstractComponentContainer layout
) {
252 for(Component c
: layout
){
253 c
.setReadOnly(readOnly
);
254 if(c
instanceof AbstractComponentContainer
){
255 recursiveReadonly(readOnly
, (AbstractComponentContainer
)c
);
260 protected AbstractLayout
getToolBar() {
264 protected void toolBarAdd(Component c
) {
265 toolBar
.addComponent(c
, toolBar
.getComponentIndex(toolBarButtonGroup
) - 1);
266 updateToolBarVisibility();
269 protected void toolBarButtonGroupAdd(Component c
) {
270 toolBarButtonGroup
.addComponent(c
);
271 updateToolBarVisibility();
274 protected void toolBarButtonGroupRemove(Component c
) {
275 toolBarButtonGroup
.removeComponent(c
);
276 updateToolBarVisibility();
279 private void updateToolBarVisibility() {
280 boolean showToolbar
= toolBarButtonGroup
.getComponentCount() + toolBar
.getComponentCount() > 1;
281 toolBar
.setVisible(toolBarButtonGroup
.getComponentCount() + toolBar
.getComponentCount() > 1);
283 mainLayout
.setMargin(new MarginInfo(true, false, false, false));
285 mainLayout
.setMargin(false);
290 * The top tool-bar is initially invisible.
292 protected void setToolBarVisible(boolean visible
){
293 toolBar
.setVisible(true);
296 public boolean isAdvancedMode() {
297 return isAdvancedMode
;
300 public void setAdvancedMode(boolean isAdvancedMode
) {
301 this.isAdvancedMode
= isAdvancedMode
;
302 advancedModeComponents
.forEach(c
-> c
.setVisible(isAdvancedMode
));
305 public void setAdvancedModeEnabled(boolean activate
){
306 if(activate
&& advancedModeButton
== null){
307 advancedModeButton
= new Button(FontAwesome
.WRENCH
); // FontAwesome.FLASK
308 advancedModeButton
.setIconAlternateText("Advanced mode");
309 advancedModeButton
.addStyleName(ValoTheme
.BUTTON_TINY
);
310 toolBarButtonGroupAdd(advancedModeButton
);
311 advancedModeButton
.addClickListener(e
-> {
312 setAdvancedMode(!isAdvancedMode
);
316 } else if(advancedModeButton
!= null) {
317 toolBarButtonGroupRemove(advancedModeButton
);
318 advancedModeButton
= null;
322 public void registerAdvancedModeComponents(Component
... c
){
323 advancedModeComponents
.addAll(Arrays
.asList(c
));
326 // ------------------------ event handler ------------------------ //
328 private class SaveHandler
implements CommitHandler
{
330 private static final long serialVersionUID
= 2047223089707080659L;
333 public void preCommit(CommitEvent commitEvent
) throws CommitException
{
334 logger
.debug("preCommit(), publishing EditorPreSaveEvent");
335 // notify the presenter to start a transaction
336 viewEventBus
.publish(this, new EditorPreSaveEvent
<DTO
>(AbstractPopupEditor
.this, getBean()));
340 public void postCommit(CommitEvent commitEvent
) throws CommitException
{
342 if(logger
.isTraceEnabled()){
343 logger
.trace("postCommit() publishing EditorSaveEvent for " + getBean().toString());
345 // notify the presenter to persist the bean and to commit the transaction
346 viewEventBus
.publish(this, new EditorSaveEvent
<DTO
>(AbstractPopupEditor
.this, getBean()));
347 if(logger
.isTraceEnabled()){
348 logger
.trace("postCommit() publishing DoneWithPopupEvent");
350 // notify the NavigationManagerBean to close the window and to dispose the view
351 viewEventBus
.publish(EventScope
.UI
, this, new DoneWithPopupEvent(AbstractPopupEditor
.this, Reason
.SAVE
));
352 } catch (Exception e
) {
354 throw new CommitException("Failed to store data to backend", e
);
359 protected void addCommitHandler(CommitHandler commitHandler
) {
360 fieldGroup
.addCommitHandler(commitHandler
);
363 protected void cancelEditorDialog(){
365 if(fieldGroup
.isModified()){
367 ContinueAlternativeCancelDialog editorModifiedDialog
= new ContinueAlternativeCancelDialog(
369 "<p>The editor has been modified.<br>Do you want to save your changes or discard them?<p>",
372 ClickListener saveListener
= e
-> {editorModifiedDialog
.close(); save();};
373 ClickListener discardListener
= e
-> {editorModifiedDialog
.close(); cancel();};
374 ClickListener cancelListener
= e
-> editorModifiedDialog
.close();
375 editorModifiedDialog
.addAlternativeClickListener(saveListener
);
376 editorModifiedDialog
.addContinueClickListener(discardListener
);
377 editorModifiedDialog
.addCancelClickListener(cancelListener
);
379 UI
.getCurrent().addWindow(editorModifiedDialog
);
386 * Cancel editing and discard all modifications.
389 public void cancel() {
390 fieldGroup
.discard();
391 viewEventBus
.publish(EventScope
.UI
, this, new DoneWithPopupEvent(this, Reason
.CANCEL
));
394 private void delete() {
395 viewEventBus
.publish(this, new EditorDeleteEvent
<DTO
>(this, fieldGroup
.getItemDataSource().getBean()));
396 viewEventBus
.publish(EventScope
.UI
, this, new DoneWithPopupEvent(this, Reason
.DELETE
));
400 * Save the changes made in the editor.
402 private void save() {
405 } catch (CommitException e
) {
406 fieldGroup
.getFields().forEach(f
-> ((AbstractField
<?
>)f
).setValidationVisible(true));
407 Throwable cause
= e
.getCause();
408 while(cause
!= null) {
409 if(cause
instanceof FieldGroupInvalidValueException
){
410 FieldGroupInvalidValueException invalidValueException
= (FieldGroupInvalidValueException
)cause
;
411 updateFieldNotifications(invalidValueException
.getInvalidFields());
412 int invalidFieldsCount
= invalidValueException
.getInvalidFields().size();
413 Notification
.show("The entered data in " + invalidFieldsCount
+ " field" + (invalidFieldsCount
> 1 ?
"s": "") + " is incomplete or invalid.");
415 } else if(cause
instanceof PermissionDeniedException
){
416 PermissionDeniedException permissionDeniedException
= (PermissionDeniedException
)cause
;
417 Notification
.show("Permission denied", permissionDeniedException
.getMessage(), Type
.ERROR_MESSAGE
);
420 cause
= cause
.getCause();
423 // no known exception type found
425 PopupEditorException pee
= null;
427 pee
= new PopupEditorException("Error saving popup editor", this, e
);
428 } catch (Throwable t
) {
429 /* IGORE errors which happen during the construction of the PopupEditorException */
434 throw new RuntimeException(e
);
439 private void updateFieldNotifications(Map
<Field
<?
>, InvalidValueException
> invalidFields
) {
440 for(Field
<?
> f
: invalidFields
.keySet()){
441 if(f
instanceof AbstractField
){
442 String message
= invalidFields
.get(f
).getHtmlMessage();
443 ((AbstractField
<?
>)f
).setComponentError(new UserError(message
, ContentMode
.HTML
, ErrorLevel
.ERROR
));
448 // ------------------------ field adding methods ------------------------ //
450 protected TextField
addTextField(String caption
, String propertyId
) {
451 return addField(new TextFieldNFix(caption
), propertyId
);
454 protected TextField
addTextField(String caption
, String propertyId
, int column1
, int row1
,
455 int column2
, int row2
)
456 throws OverlapsException
, OutOfBoundsException
{
457 return addField(new TextFieldNFix(caption
), propertyId
, column1
, row1
, column2
, row2
);
460 protected TextField
addTextField(String caption
, String propertyId
, int column
, int row
)
461 throws OverlapsException
, OutOfBoundsException
{
462 return addField(new TextFieldNFix(caption
), propertyId
, column
, row
);
465 protected SwitchableTextField
addSwitchableTextField(String caption
, String textPropertyId
, String switchPropertyId
, int column1
, int row1
,
466 int column2
, int row2
)
467 throws OverlapsException
, OutOfBoundsException
{
469 SwitchableTextField field
= new SwitchableTextField(caption
);
470 field
.bindTo(fieldGroup
, textPropertyId
, switchPropertyId
);
471 addComponent(field
, column1
, row1
, column2
, row2
);
475 protected SwitchableTextField
addSwitchableTextField(String caption
, String textPropertyId
, String switchPropertyId
, int column
, int row
)
476 throws OverlapsException
, OutOfBoundsException
{
478 SwitchableTextField field
= new SwitchableTextField(caption
);
479 field
.bindTo(fieldGroup
, textPropertyId
, switchPropertyId
);
480 addComponent(field
, column
, row
);
484 protected PopupDateField
addDateField(String caption
, String propertyId
) {
485 return addField(new PopupDateField(caption
), propertyId
);
488 protected CheckBox
addCheckBox(String caption
, String propertyId
) {
489 return addField(new CheckBox(caption
), propertyId
);
492 protected CheckBox
addCheckBox(String caption
, String propertyId
, int column
, int row
){
493 return addField(new CheckBox(caption
), propertyId
, column
, row
);
496 protected <T
extends Field
> T
addField(T field
, String propertyId
) {
497 fieldGroup
.bind(field
, propertyId
);
498 if(NestedFieldGroup
.class.isAssignableFrom(field
.getClass())){
499 ((NestedFieldGroup
)field
).registerParentFieldGroup(fieldGroup
);
506 * Can only be used if the <code>fieldlayout</code> is a GridLayout.
509 * the field to be added, not <code>null</code>.
512 * the column index, starting from 0.
514 * the row index, starting from 0.
515 * @throws OverlapsException
516 * if the new component overlaps with any of the components
517 * already in the grid.
518 * @throws OutOfBoundsException
519 * if the cell is outside the grid area.
521 protected <T
extends Field
> T
addField(T field
, String propertyId
, int column
, int row
)
522 throws OverlapsException
, OutOfBoundsException
{
523 fieldGroup
.bind(field
, propertyId
);
524 if(NestedFieldGroup
.class.isAssignableFrom(field
.getClass())){
525 ((NestedFieldGroup
)field
).registerParentFieldGroup(fieldGroup
);
527 addComponent(field
, column
, row
);
532 * Can only be used if the <code>fieldlayout</code> is a GridLayout.
534 protected <T
extends Field
> T
addField(T field
, String propertyId
, int column1
, int row1
,
535 int column2
, int row2
)
536 throws OverlapsException
, OutOfBoundsException
{
537 if(propertyId
!= null){
538 fieldGroup
.bind(field
, propertyId
);
539 if(NestedFieldGroup
.class.isAssignableFrom(field
.getClass())){
540 ((NestedFieldGroup
)field
).registerParentFieldGroup(fieldGroup
);
543 addComponent(field
, column1
, row1
, column2
, row2
);
547 protected Field
<?
> getField(Object propertyId
){
548 return fieldGroup
.getField(propertyId
);
551 public PropertyIdPath
boundPropertyIdPath(Field
<?
> field
){
553 PropertyIdPath propertyIdPath
= null;
554 Object propertyId
= fieldGroup
.getPropertyId(field
);
556 if(propertyId
== null){
557 // not found in the editor field group. Maybe the field is bound to a nested fieldgroup?
558 // 1. find the NestedFieldGroup implementations from the field up to the editor
559 PropertyIdPath nestedPropertyIds
= new PropertyIdPath();
560 Field
<?
> parentField
= field
;
561 HasComponents parentComponent
= parentField
.getParent();
562 logger
.debug("field: " + parentField
.getClass().getSimpleName());
563 while(parentComponent
!= null){
564 if (logger
.isDebugEnabled()){logger
.debug("parentComponent: " + parentComponent
.getClass().getSimpleName());}
565 if(NestedFieldGroup
.class.isAssignableFrom(parentComponent
.getClass()) && AbstractField
.class.isAssignableFrom(parentComponent
.getClass())){
566 Optional
<FieldGroup
> parentFieldGroup
= ((NestedFieldGroup
)parentComponent
).getFieldGroup();
567 if(parentFieldGroup
.isPresent()){
568 Object propId
= parentFieldGroup
.get().getPropertyId(parentField
);
570 if (logger
.isDebugEnabled()){logger
.debug("propId: " + propId
.toString());}
571 nestedPropertyIds
.addParent(propId
);
573 if (logger
.isDebugEnabled()){logger
.debug("parentField: " + parentField
.getClass().getSimpleName());}
574 parentField
= (Field
<?
>)parentComponent
;
576 if (logger
.isDebugEnabled()){logger
.debug("parentFieldGroup is null, continuing ...");}
578 } else if(parentComponent
== this) {
579 // we reached the editor itself
580 Object propId
= fieldGroup
.getPropertyId(parentField
);
582 if (logger
.isDebugEnabled()){logger
.debug("propId: " + propId
.toString());}
583 nestedPropertyIds
.addParent(propId
);
585 propertyIdPath
= nestedPropertyIds
;
588 parentComponent
= parentComponent
.getParent();
590 // 2. check the NestedFieldGroup binding the field is direct or indirect child component of the editor
592 // parentComponent = parentField.getParent(); // get component containing the last parent field found
594 // if(parentComponent == getFieldLayout()){
595 // propertyIdPath = nestedPropertyIds;
598 // parentComponent = parentComponent.getParent();
601 propertyIdPath
= new PropertyIdPath(propertyId
);
603 return propertyIdPath
;
606 protected void addComponent(Component component
) {
607 fieldLayout
.addComponent(component
);
608 applyDefaultComponentStyles(component
);
611 protected void bindField(Field field
, String propertyId
){
612 fieldGroup
.bind(field
, propertyId
);
615 protected void unbindField(Field field
){
616 fieldGroup
.unbind(field
);
619 public void applyDefaultComponentStyles(Component component
) {
620 component
.addStyleName(getDefaultComponentStyles());
623 protected abstract String
getDefaultComponentStyles();
626 * Can only be used if the <code>fieldlayout</code> is a GridLayout.
628 * Adds the component to the grid in cells column1,row1 (NortWest corner of
629 * the area.) End coordinates (SouthEast corner of the area) are the same as
630 * column1,row1. The coordinates are zero-based. Component width and height
634 * the component to be added, not <code>null</code>.
636 * the column index, starting from 0.
638 * the row index, starting from 0.
639 * @throws OverlapsException
640 * if the new component overlaps with any of the components
641 * already in the grid.
642 * @throws OutOfBoundsException
643 * if the cell is outside the grid area.
645 public void addComponent(Component component
, int column
, int row
)
646 throws OverlapsException
, OutOfBoundsException
{
647 applyDefaultComponentStyles(component
);
648 gridLayout().addComponent(component
, column
, row
, column
, row
);
652 * Can only be used if the <code>fieldlayout</code> is a GridLayout.
654 * Adds a component to the grid in the specified area. The area is defined
655 * by specifying the upper left corner (column1, row1) and the lower right
656 * corner (column2, row2) of the area. The coordinates are zero-based.
660 * If the area overlaps with any of the existing components already present
661 * in the grid, the operation will fail and an {@link OverlapsException} is
666 * the component to be added, not <code>null</code>.
668 * the column of the upper left corner of the area <code>c</code>
669 * is supposed to occupy. The leftmost column has index 0.
671 * the row of the upper left corner of the area <code>c</code> is
672 * supposed to occupy. The topmost row has index 0.
674 * the column of the lower right corner of the area
675 * <code>c</code> is supposed to occupy.
677 * the row of the lower right corner of the area <code>c</code>
678 * is supposed to occupy.
679 * @throws OverlapsException
680 * if the new component overlaps with any of the components
681 * already in the grid.
682 * @throws OutOfBoundsException
683 * if the cells are outside the grid area.
685 public void addComponent(Component component
, int column1
, int row1
,
686 int column2
, int row2
)
687 throws OverlapsException
, OutOfBoundsException
{
688 applyDefaultComponentStyles(component
);
689 gridLayout().addComponent(component
, column1
, row1
, column2
, row2
);
692 public void setSaveButtonEnabled(boolean enabled
){
693 saveBtn
.setEnabled(enabled
);
696 protected void setSaveButtonVisible(boolean enabled
){
697 saveBtn
.setVisible(enabled
);
700 protected void setSaveButtonCaption(String caption
) {
701 saveBtn
.setCaption(caption
);
704 public void withDeleteButton(boolean withDelete
){
706 this.withDeleteButton
= withDelete
;
707 if(withDeleteButton
){
708 buttonLayout
.setExpandRatio(saveBtn
, 0);
709 buttonLayout
.setExpandRatio(deleteBtn
, 1);
711 buttonLayout
.setExpandRatio(saveBtn
, 1);
712 buttonLayout
.setExpandRatio(deleteBtn
, 0);
714 updateDeleteButtonState();
717 private void updateDeleteButtonState() {
718 deleteBtn
.setVisible(withDeleteButton
&& !isReadOnly());
721 public boolean addStatusMessage(String message
){
722 boolean returnVal
= statusMessages
.add(message
);
727 public boolean removeStatusMessage(String message
){
728 boolean returnVal
= statusMessages
.remove(message
);
733 private void updateStatusLabel() {
735 for(String s
: statusMessages
){
738 statusMessageLabel
.setValue(text
);
739 statusMessageLabel
.setVisible(!text
.isEmpty());
740 statusMessageLabel
.addStyleName(ValoTheme
.LABEL_COLORED
);
743 private void updateContextBreadcrumbs() {
745 List
<EditorActionContext
> contextInfo
= new ArrayList
<>(getEditorActionContext());
746 String breadcrumbs
= "";
747 EditorActionContextFormatter formatter
= new EditorActionContextFormatter();
750 for(EditorActionContext cntxt
: contextInfo
){
752 boolean isLast
= cnt
== contextInfo
.size();
753 boolean isFirst
= cnt
== 1;
755 boolean doClass
= false; // will be removed in future
756 boolean classNameForMissingPropertyPath
= true; // !doClass;
757 boolean doProperties
= true;
758 boolean doCreateOrNew
= !isFirst
;
759 String contextmarkup
= formatter
.format(
761 new EditorActionContextFormat(doClass
, doProperties
, classNameForMissingPropertyPath
, doCreateOrNew
,
762 EditorActionContextFormat
.TargetInfoType
.FIELD_CAPTION
, (isLast ?
"active" : ""))
765 // contextmarkup += " " + FontAwesome.ANGLE_RIGHT.getHtml() + " ";
768 contextmarkup
= "<li><span class=\"crumb active\">" + contextmarkup
+ "</span></li>";
770 contextmarkup
= "<li><span class=\"crumb\">" + contextmarkup
+ "</span></li>";
772 breadcrumbs
+= contextmarkup
;
774 contextBreadcrumbsLabel
.setValue("<ul class=\"breadcrumbs\">" + breadcrumbs
+ "</ul>");
777 // ------------------------ data binding ------------------------ //
779 protected void bindDesign(Component component
) {
780 fieldLayout
.removeAllComponents();
781 fieldGroup
.bindMemberFields(component
);
782 fieldLayout
.addComponent(component
);
786 public final void loadInEditor(Object identifier
) {
788 DTO beanToEdit
= getPresenter().loadBeanById(identifier
);
789 fieldGroup
.setItemDataSource(beanToEdit
);
790 afterItemDataSourceSet();
791 getPresenter().onViewFormReady(beanToEdit
);
792 updateContextBreadcrumbs();
797 * Passes the beanInstantiator to the presenter method {@link AbstractEditorPresenter#setBeanInstantiator(BeanInstantiator)}
799 * @param beanInstantiator
801 public final void setBeanInstantiator(BeanInstantiator
<DTO
> beanInstantiator
) {
802 if(AbstractCdmEditorPresenter
.class.isAssignableFrom(getPresenter().getClass())){
803 ((CdmEditorPresenterBase
)getPresenter()).setBeanInstantiator(beanInstantiator
);
805 throw new RuntimeException("BeanInstantiator can only be set for popup editors with a peresenter of the type CdmEditorPresenterBase");
810 * Returns the bean contained in the itemDatasource of the fieldGroup.
812 public DTO
getBean() {
813 if(fieldGroup
.getItemDataSource() != null){
814 return fieldGroup
.getItemDataSource().getBean();
821 * @return true once the bean has been loaded indicating that all fields have
822 * been setup configured so that the editor is ready for use.
824 public boolean isBeanLoaded() {
829 * This method should only be used by the presenter of this view
833 protected void updateItemDataSource(DTO bean
) {
834 fieldGroup
.getItemDataSource().setBean(bean
);
838 * This method is called after setting the item data source whereby the
839 * {@link FieldGroup#configureField(Field<?> field)} method will be called.
840 * In this method all fields are set to default states defined for the fieldGroup.
842 * You can now implement this method if you need to modify the state or value of individual fields.
844 protected void afterItemDataSourceSet() {
845 if(editorComponentsConfigurator
!= null){
846 editorComponentsConfigurator
.updateComponentStates(this);
851 // ------------------------ issue related temporary solutions --------------------- //
853 * Publicly accessible equivalent to getPreseneter(), needed for
854 * managing the presenter listeners.
856 * TODO: refactor the presenter listeners management to get rid of this method
859 * @deprecated marked deprecated to emphasize on the special character of this method
860 * which should only be used internally see #6673
863 public P
presenter() {
864 return getPresenter();
868 * Returns the context of editor actions for this editor.
869 * The context submitted with {@link #setParentContext(Stack)} will be updated
870 * to represent the current context.
872 * @return the context
874 public Stack
<EditorActionContext
> getEditorActionContext() {
875 if(!isContextUpdated
){
876 if(getBean() == null){
877 throw new RuntimeException("getContext() is only possible after the bean is loaded");
879 context
.push(new EditorActionContext(getBean(), this));
880 isContextUpdated
= true;
886 * Attempts to find an item in the context of editor actions for this editor,
887 * having a parentView matching the <code>viewType</code> by {@link Class#isAssignableFrom(Class)}.
889 @SuppressWarnings("unchecked")
890 public <VIEW
extends AbstractView
> Optional
<VIEW
> findViewInEditorActionContext(Class
<VIEW
> viewType
) {
891 Stack
<EditorActionContext
> ctxt
= getEditorActionContext();
892 for(int i
= ctxt
.size(); i
> 0; --i
) {
893 if(viewType
.isAssignableFrom(ctxt
.get(i
).getParentView().getClass())){
894 return Optional
.of((VIEW
) ctxt
.get(i
).getParentView());
897 return Optional
.empty();
901 * Set the context of editor actions parent to this editor
903 * @param context the context to set
905 public void setParentEditorActionContext(Stack
<EditorActionContext
> context
, Field
<?
> targetField
) {
907 this.context
.addAll(context
);
909 if(targetField
!= null){
910 this.context
.get(context
.size() - 1).setTargetField(targetField
);
914 protected AbstractField
<String
> replaceComponent(String propertyId
, AbstractField
<String
> oldField
,
915 AbstractField
<String
> newField
, int column1
, int row1
, int column2
, int row2
) {
917 String value
= oldField
.getValue();
918 newField
.setCaption(oldField
.getCaption());
919 GridLayout grid
= (GridLayout
)getFieldLayout();
920 grid
.removeComponent(oldField
);
922 unbindField(oldField
);
923 addField(newField
, propertyId
, column1
, row1
, column2
, row2
);
924 getViewEventBus().publish(this, new FieldReplaceEvent(this, oldField
, newField
));
925 // important: set newField value at last!
926 newField
.setValue(value
);
930 public EditorFormConfigurator
<?
extends AbstractPopupEditor
<DTO
,P
,V
>> getEditorComponentsConfigurator() {
931 return editorComponentsConfigurator
;
934 public void setEditorComponentsConfigurator(
935 EditorFormConfigurator
<?
extends AbstractPopupEditor
<DTO
,P
,V
>> editorComponentsConfigurator
) {
936 this.editorComponentsConfigurator
= editorComponentsConfigurator
;